-
Notifications
You must be signed in to change notification settings - Fork 84
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[WNMGDS-2427] Filter-dialog prototype for doc site theme and version …
…switching (#2599) * We never did actually export `NativeDialog` even though it looked like it Remove the export line from our index so it's not confusing. The export never worked. We haven't released this component to the public. * Basic, untested FilterDialog commponent added to docs package * WIP: Started with a dumb component and logical component, but... The problem with that is that the versions list is dynamic; it's based on the currently selected theme. That means I need logic inside the component that renders the dialog to dynamically change the options in the version dropdown based on the theme * Moved it all into one component, and it makes sense * Hook it up and start working out kinks in version/theme switcher Problem right now is that if you previously had one theme selected and you select a different one, it will think you changed versions too because the version numbers don't match. While I could convert all versions to the core version equivalent to compare, what if it can't find the corresponding core version and has to fall back? Then the comparison would be wrong. But that doesn't happen, because there are no versions that don't have core equivalents. So maybe that'll actually work * Fixed `useTheme` causing a second render for every component that uses it It would always start with "core" and then switch to the actual current value. That second render was causing problems in the theme switcher * Reorganize modules and create a basic ThemeVersionSection for the sidenav * Use inverse colors * Fix bug in my useTheme fix * Some work on styling * Styling for the triggering button * Add arrow icon to the "Change settings" button * Use theme `displayName` in sidebar * Remove old theme and version switcher components * Turns out we don't need the onExit function * Add some basic unit tests * Try to fix lost selection message with custom getA11ySelectionMessage fn Unfortunately it does not work because of the debouncing done inside Downshift. [Here's my explanation](downshift-js/downshift#1244): > For what it's worth, I have just come across an issue with the existing a11y-message implementation and specifically [how updateA11yStatus is debounced](https://github.com/downshift-js/downshift/blob/master/src/hooks/utils.js#L85). In my application, I have one dropdown on a page that modifies the available options for another dropdown further down the page. (This isn't ideal from an accessibility standpoint, but [the WCAG docs](https://www.w3.org/WAI/WCAG21/Understanding/on-input) do say we can do that if we warn users that it's going to happen, and it will take a while before we can design out all the instances of it across multiple applications.) I wanted to write a custom `getA11ySelectionMessage` function that would determine when it's one of those incidentally changed dropdowns rather than the one the user is interacting with so that only the desired `${itemToString(selectedItem)} has been selected.` gets printed to the a11y-message div. However, due to the debouncing, none of the other `getA11ySelectionMessage` functions even get called except for the last one, which is the one I don't want to announce. If Downshift called those `getA11yMessage` functions instead of passing them to `updateA11yStatus` and then ignored them if they returned, say, `undefined`, this plan would work. But right now the agency from each individual dropdown is taken from it to determine whether its status message should be announced. It's getting lost in the debounce. * Get rid of new function that didn't work See previous commit * Pivot to using two dialogs This should be a much more straightforward experience and not create accessibility problems with the theme dropdown dynamically changing the options of the version dropdown. * Move focus back to the triggering button when closing a filter dialog Need to figure out a clean way of making this the easy thing to do for teams * Add a close button * Add the border radius used for dropdowns * Allow the close icon label to be announced Forgot that it is off by default * Show which design system theme we're on in the page (tab) title * Fix HealthCare.gov theme text wrapping Don't constrain the theme/version section more than we need to * Update snapshot * Better screen reader text for the theme and version buttons * Move the theme and version section out of side nav on mobile * Fine tune the spacing after making the hit box large enough * Fix mobile spacing on page header * Fixes according to feedback * Don't let multiple dialogs be open at once * Update this prop's documentation to make it more clear what they need to do * Update snapshot
- Loading branch information
Showing
23 changed files
with
710 additions
and
147 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
35 changes: 35 additions & 0 deletions
35
packages/docs/src/components/layout/FilterDialog/CloseButton.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import React from 'react'; | ||
import classNames from 'classnames'; | ||
import { CloseIconThin } from '@cmsgov/design-system'; | ||
|
||
interface BaseCloseButtonProps { | ||
/** | ||
* Additional classes to be added to the root dialog element. | ||
*/ | ||
className?: string; | ||
/** | ||
* A custom `id` attribute for the dialog element | ||
*/ | ||
id?: string; | ||
} | ||
|
||
export type CloseButtonProps = Omit< | ||
React.ComponentPropsWithRef<'button'>, | ||
keyof BaseCloseButtonProps | ||
> & | ||
BaseCloseButtonProps; | ||
|
||
/** | ||
* | ||
*/ | ||
export const CloseButton = ({ className, ...buttonAttributes }: CloseButtonProps) => ( | ||
<button | ||
className={classNames('ds-c-close-button', className)} | ||
type="button" | ||
{...buttonAttributes} | ||
> | ||
<CloseIconThin ariaHidden={false} /> | ||
</button> | ||
); | ||
|
||
export default CloseButton; |
40 changes: 40 additions & 0 deletions
40
packages/docs/src/components/layout/FilterDialog/FilterDialog.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import React from 'react'; | ||
import FilterDialog from './FilterDialog'; | ||
import { render, screen } from '@testing-library/react'; | ||
import { Button } from '@cmsgov/design-system'; | ||
|
||
const defaultProps = { | ||
actions: ( | ||
<> | ||
<Button variation="solid">Submit</Button> | ||
<Button variation="ghost">Cancel</Button> | ||
</> | ||
), | ||
children: 'Hello, this is the dialog content.', | ||
className: 'a-custom-class', | ||
heading: 'FilterDialog heading', | ||
onExit: jest.fn(), | ||
}; | ||
|
||
function renderFilterDialog(overwriteProps = {}) { | ||
const props = Object.assign({}, defaultProps, overwriteProps); | ||
return render(<FilterDialog {...props} />); | ||
} | ||
|
||
describe('FilterDialog', () => { | ||
it('renders a dialog', () => { | ||
renderFilterDialog(); | ||
expect(screen.getByRole('dialog')).toMatchSnapshot(); | ||
}); | ||
|
||
it('passes a ref to the heading', () => { | ||
const headingRef = React.createRef<HTMLHeadingElement>(); | ||
renderFilterDialog({ headingRef }); | ||
expect(headingRef.current.textContent).toEqual(defaultProps.heading); | ||
}); | ||
|
||
it('allows a custom headingLevel to be set', () => { | ||
renderFilterDialog({ headingLevel: '5' }); | ||
expect(screen.getByText(defaultProps.heading).tagName).toEqual('H5'); | ||
}); | ||
}); |
86 changes: 86 additions & 0 deletions
86
packages/docs/src/components/layout/FilterDialog/FilterDialog.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import React, { useRef } from 'react'; | ||
import NativeDialog from '@cmsgov/design-system/src/components/NativeDialog/NativeDialog.tsx'; | ||
import uniqueId from 'lodash/uniqueId'; | ||
import classNames from 'classnames'; | ||
import mergeRefs from '@cmsgov/design-system/src/components/utilities/mergeRefs.ts'; | ||
import CloseButton from './CloseButton'; | ||
|
||
export interface FilterDialogProps { | ||
/** | ||
* Buttons or other HTML to be rendered in the "actions" bar at the bottom of | ||
* the dialog. Should include a button for applying the user's selections and | ||
* one for closing the dialog. | ||
*/ | ||
actions: React.ReactNode; | ||
/** | ||
* The main dialog content | ||
*/ | ||
children: React.ReactNode; | ||
/** | ||
* Additional classes to be added to the root dialog element. | ||
*/ | ||
className?: string; | ||
/** | ||
* Text for the FilterDialog heading. Required because the `heading` will be focused on mount. | ||
*/ | ||
heading: string | React.ReactNode; | ||
/** | ||
* A unique `id` to be used on heading element to label multiple instances of FilterDialog. | ||
*/ | ||
headingId?: string; | ||
/** | ||
* Heading type to override default `<h3>` | ||
*/ | ||
headingLevel?: '1' | '2' | '3' | '4' | '5'; | ||
/** | ||
* Ref to heading element | ||
*/ | ||
headingRef?: React.MutableRefObject<any>; | ||
/** | ||
* A custom `id` attribute for the dialog element | ||
*/ | ||
id?: string; | ||
/** | ||
* Called when the user triggers an exit event, like by pressing the ESC key. | ||
* The parent of this component is responsible for showing or not showing the | ||
* dialog, so you need to use this callback to make that happen. The dialog | ||
* does not hide itself. | ||
*/ | ||
onExit(event: React.MouseEvent | React.KeyboardEvent): void; | ||
} | ||
|
||
/** | ||
* | ||
*/ | ||
export const FilterDialog = (props: FilterDialogProps) => { | ||
const id = useRef(props.id || uniqueId('filter-dialog-')).current; | ||
const headingRef = mergeRefs([props.headingRef, useRef()]); | ||
const headingId = props.headingId ?? `${id}__heading`; | ||
const Heading = `h${props.headingLevel}` as const; | ||
|
||
return ( | ||
<NativeDialog | ||
className={classNames(props.className, 'ds-c-filter-dialog')} | ||
// We're not using the NativeDialog as a modal, so exit is never called | ||
exit={() => {}} | ||
id={id} | ||
> | ||
<div className="ds-c-filter-dialog__window" tabIndex={-1} aria-labelledby={headingId}> | ||
<div className="ds-c-filter-dialog__header"> | ||
<Heading id={headingId} className="ds-c-filter-dialog__heading" ref={headingRef}> | ||
{props.heading} | ||
</Heading> | ||
<CloseButton className="ds-c-filter-dialog__close" onClick={props.onExit} /> | ||
</div> | ||
<div className={classNames('ds-c-filter-dialog__body')}>{props.children}</div> | ||
<div className="ds-c-filter-dialog__actions">{props.actions}</div> | ||
</div> | ||
</NativeDialog> | ||
); | ||
}; | ||
|
||
FilterDialog.defaultProps = { | ||
headingLevel: '3', | ||
}; | ||
|
||
export default FilterDialog; |
38 changes: 38 additions & 0 deletions
38
packages/docs/src/components/layout/FilterDialog/FilterDialogManager.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import React from 'react'; | ||
import { createContext, useState, useContext, useRef } from 'react'; | ||
import uniqueId from 'lodash/uniqueId'; | ||
|
||
export const FilterDialogContext = createContext(null); | ||
|
||
/** | ||
* This is just copied and pasted from our unreleased `DrawerManager` component | ||
* and hook. I just want something that will make sure only one is open at a | ||
* time, but in the future this could possibly be expanded to help render | ||
* a UI for mobile where someone can click "View all filters" and get one | ||
* dialog that is an aggregate of all the individual ones. | ||
* | ||
* The problem I'm seeing with this strategy is that I can't actually render | ||
* the provider and use the hook within the same component. I could see someone | ||
* wanting to both define the FilterDialogManager and use the hook to manage | ||
* the individual dialogs in the set in the same component, which doesn't work. | ||
* This idea will need a lot of work. Going to leave it as-is for the doc site | ||
* prototype, though. | ||
*/ | ||
export const FilterDialogManager = (props: any) => { | ||
const [currentID, setCurrentID] = useState(null); | ||
|
||
return <FilterDialogContext.Provider value={{ currentID, setCurrentID }} {...props} />; | ||
}; | ||
|
||
export const useFilterDialogManager = () => { | ||
const { currentID, setCurrentID } = useContext(FilterDialogContext); | ||
const id = useRef(uniqueId('filterDialogManagerID')).current; | ||
|
||
const isOpen = currentID === id; | ||
const toggleClick = () => setCurrentID(isOpen ? null : id); | ||
const closeClick = () => setCurrentID(null); | ||
|
||
return { toggleClick, closeClick, isOpen }; | ||
}; | ||
|
||
export default FilterDialogManager; |
76 changes: 76 additions & 0 deletions
76
packages/docs/src/components/layout/FilterDialog/__snapshots__/FilterDialog.test.tsx.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
// Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
||
exports[`FilterDialog renders a dialog 1`] = ` | ||
<dialog | ||
class="a-custom-class ds-c-filter-dialog" | ||
id="filter-dialog-1" | ||
open="" | ||
role="dialog" | ||
> | ||
<div | ||
aria-labelledby="filter-dialog-1__heading" | ||
class="ds-c-filter-dialog__window" | ||
tabindex="-1" | ||
> | ||
<div | ||
class="ds-c-filter-dialog__header" | ||
> | ||
<h3 | ||
class="ds-c-filter-dialog__heading" | ||
id="filter-dialog-1__heading" | ||
> | ||
FilterDialog heading | ||
</h3> | ||
<button | ||
class="ds-c-close-button ds-c-filter-dialog__close" | ||
type="button" | ||
> | ||
<svg | ||
aria-hidden="false" | ||
aria-labelledby="icon-2__title" | ||
class="ds-c-icon ds-c-icon--close ds-c-icon--close-thin " | ||
focusable="false" | ||
id="icon-2" | ||
role="img" | ||
viewBox="-2 -2 18 18" | ||
xmlns="http://www.w3.org/2000/svg" | ||
> | ||
<title | ||
id="icon-2__title" | ||
> | ||
Close | ||
</title> | ||
<path | ||
d="M0 13.0332964L13.0332964 0M13.0332964 13.0332964L0 0" | ||
fill="none" | ||
stroke="currentColor" | ||
stroke-linecap="round" | ||
stroke-width="2" | ||
/> | ||
</svg> | ||
</button> | ||
</div> | ||
<div | ||
class="ds-c-filter-dialog__body" | ||
> | ||
Hello, this is the dialog content. | ||
</div> | ||
<div | ||
class="ds-c-filter-dialog__actions" | ||
> | ||
<button | ||
class="ds-c-button ds-c-button--solid" | ||
type="button" | ||
> | ||
Submit | ||
</button> | ||
<button | ||
class="ds-c-button ds-c-button--ghost" | ||
type="button" | ||
> | ||
Cancel | ||
</button> | ||
</div> | ||
</div> | ||
</dialog> | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './FilterDialog'; | ||
export * from './FilterDialogManager'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.