[Tech Design] Terra Select Responsive Dropdown
See original GitHub issueTech Design
Description
Tech design for https://github.com/cerner/terra-core/issues/2383
TLDR
- Consolidate use of Dropdown and DropdownMenu
<DropdownMenu />
is an internal implementation detail of<Dropdown />
and it’s unnecessarily complicating the component by rendering the menu as a renderProp in the<Select />
component
- Implement (name pending)
<ResponsiveDropdown />
(or<Modal />
, whatever the UX defined “responsive dropdown” is) - Remove render props pattern in Select.jsx
- In Frame, if
this.state.isOpen
, then use either the<Dropdown />
or<Modal />
- In Frame, if
- Flex between
<Dropdown />
and<Modal />
based on if we’re mobile or at'small'
or'tiny'
breakpoints- Add
isMobile
method to SharedUtil which uses theUserAgent
to check foriPhone
,iPad
, andAndroid
- Use ActiveBreakpointContext to determine the
activeBreakpoint
const useModal = isMobile() || activeBreakpoint === 'tiny' || activeBreakpoint === 'small';
- Add
Details
Dropdown, DropdownMenu, and the Unnecessary renderProps
Pattern
<DropdownMenu />
is just an internal implementation detail of <Dropdown />
. We don’t need to be rendering it via render props in <Select />
. We can just render the <Frame />
and delegate the dropdown and responsive behavior to the Frame. This allows us to reduce the complexity of the Select
component while also giving us the ability to add new behavior easily to the Frame
.
Responsive Dropdown Implementation
NOTE: See additional screenshots below for visuals.
The responsive dropdown workflow will live inside a DialogModal
. When rendered, it will have an action header and action footer, while the content will be displayed with an input styled just like the current dropdown variant (including tags/pills) and a list of all options.
Action Header
We will need to give the action header a label to display as the title. The action header will call the responsive dropdown component’s onClose
prop when the action header’s close button is pressed/clicked and close the modal.
Content
The content will contain an input (for multiple/search/tag variants) and a list of options as well as any extra visual indicators for workflows that exist outside the happy-path (e.g. “No results found”) or normal usage (e.g. “Enter ‘x’ characters to search”).
Action Footer
The action footer will have two buttons. The start button is labeled "Clear All"
and will call the responsive dropdown’s onClear
prop when pressed/clicked. The end button is labeled "Apply"
and will simply approve/commit the state change authored by the user. It will call the responsive dropdown’s onApply
prop when pressed/clicked. It is assumed that changes will only be committed when the Apply
button is used, while also closing the modal.
isMobile()
class SharedUtil {
// ...
static isMobile() {
const { userAgent } = navigator;
const isAndroid = userAgent.includes('Android');
const isIOS = userAgent.includes('iPhone') || userAgent.includes('iPad');
return isAndroid || isIOS;
}
}
Flexing between <Dropdown /> and <Modal /> workflows
Right now in Frame, we render a <Dropdown />
(using the renderProps pattern to pass props to a <DropdownMenu />
in <Select />
) based on the boolean this.state.isOpen
. We can use this plus another boolean to flex between <Dropdown />
and <Modal />
workflows. Using the above isMobile()
method and the ActiveBreakpointContext, we can determine if we want to render the <Modal />
or not:
const activeBreakpoint = useContext(ActiveBreakpointContext);
const useModal = isMobile() || activeBreakpoint === 'tiny' || activeBreakpoint === 'small';
const { isOpen } = this.state;
return (
<div {...stuff}>
{isOpen &&
useModal ? <Modal {...modalProps} /> : <Dropdown {...dropdownProps} />
}
</div>
);
Additional Context / Screenshots
@ Mentions
Issue Analytics
- State:
- Created 4 years ago
- Comments:10 (10 by maintainers)
Top GitHub Comments
A little more context about the render prop so the decision to remove it isn’t done under misapprehension.
The Select menu was originally designed to allow a custom implementation of the menu within the dropdown. The render prop was added to allow an entry point for this functionality. A user could implement a Select dropdown using the Frame and provide their own menu which would receive all of the require props from the render prop.
This would allow a user to create a custom menu implementation to meet application specific requirements. This requirement is also why the Frame itself exists as a separate component from the Select and why the dropdown and menu exist separately.
With the introduction of the accessibility changes, general complexity of this component, and the drive towards a standard behavior a custom menu is becoming increasingly more difficult and discouraged to implement.
If we decide to remove the custom render functionality we should also investigate merging the Frame into the Select to cut down on complexity and potential extra renders.
Update: We’ve talked about this offline and have landed on implementing a solution that renders the input within the portal instead of rendering the select as a modal on mobile devices. Closing this issue as we’ve decided on a direction to go with this that won’t require the implementation described in the tech design.