Skip to content

Commit

Permalink
[WNMGDS-2369] Derive all generated ids from a standard id prop (#2633)
Browse files Browse the repository at this point in the history
* Create a helper hook for generating ids and start to use it

* Updated the AccordionItem tests but found that the SVGs still generate their own ids

Which means even if you supply an id to the accordionitem, the ids still won't be deterministic. We actually will have to change the way we render those icons in this component in order to fix this

* BREAKING: Change the AccordionItem icon props to be able to pass an id

* Update Alert tests

* Work on a few more components

* Update Autocomplete tests and snapshots

* Update choice tests and snapshots

The snapshots aren't perfect yet because the inline error renders icons that have their own generated ids. This'll get updated when we get to the error component and the useFormLabel hook

* Update Dialog tests and snapshots

* Update all other affected snapshots, some of which will change later

* Allow `useId` to be used like React's `useId` with no parameters

* Make the downshift item ids deterministic as well

* [WNMGDS-2369] Id update set 1 (#2639)

* Normalize ids for Drawer and DrawerManager

* Update filter chip and tests

* Update Dropdown and tests

* Update HelpDrawer snapshots

* Change name of check icon to match other variable name

* [WNMGDS-2369] Id update set 2 (#2640)

* Update useFormLabel and InlineError

Next step is for individual field components to supply a sensible `id` to `useFormLabel`

* Updated everything affected by the useFormLabel change

* Pass ids down to inputs in the MultiInputDateField

* Pass ChoiceList id on to choices

* Fix a bunch of text field stuff and fixed an error with the Mask tests

Changing the Mask tests to use `TextInput` fixed an error being logged to the console

* Fix undefined id in choice list

* [WNMGDS-2369] Standardize id prop for components S-V (#2644)

* update ids on svgicon and usabanner, update snapshots.

* update id for table component.

* add custom id hook to tooltip.

* add id hook to vertical nav item

* update dynamic ids in tests.

* add logic to only show id on svg if screen reader compliant. update tests/snaps.

* update S-V components, removing icon ids and cleaning up tests/snaps.

* update snaps for components using svg icons.

* remove unused prop from vert nav item label interface

* update svg icon logic to optionally set id if prop  is passed.

* update snaps where id on icons are defined.

* remove iconId from vert nav test. whoopsy

* [WNMGDS-2369] Remove icon ids that are now optional (#2648)

Remove icon ids that are now optional

* [WNMGDS-2369] Fix dynamic icon ids in SingleInputDateField (#2651)

Fix dynamic icon ids in SingleInputDateField

* [WNMGDS-2369] Standardize generated ids in Tabs (#2650)

Standardize the tab ids and add unit tests

* [WNMGDS-2369] Standardize id prop in theme components (#2654)

* export useId hook for themes to use

* update hgov header to use useId hook

* update footer ids to match format established by header (hc-c-<COMPONENT>-*).

* roll back useId use on header for usabanner to follow established pattern of hardcoding ids.

* update mgov svg to use useId.

* remove sneaky sneaky import brought in by vs code. i'm on to you robots

* remove banner id from header - not used.

---------

Co-authored-by: Sarah <zarahzachz@users.noreply.github.com>
  • Loading branch information
pwolfert and zarahzachz authored Aug 24, 2023
1 parent 6661712 commit 3f09f12
Show file tree
Hide file tree
Showing 90 changed files with 711 additions and 509 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import userEvent from '@testing-library/user-event';
const defaultChildren = '<p>Content for accordion item</p>';
const defaultProps = {
heading: 'Heading for accordion item',
id: 'static-id',
};

function renderAccordionItem(customProps = {}) {
Expand Down Expand Up @@ -59,14 +60,14 @@ describe('AccordionItem', function () {
});

it('renders an id automatically', () => {
renderAccordionItem();
renderAccordionItem({ heading: defaultProps.heading });

const buttonEl = screen.getByRole('button');
const contentEl = screen.getByText(defaultChildren);

expect(buttonEl).toHaveAttribute('aria-controls');
expect(buttonEl).toHaveAttribute('aria-controls', contentEl.id);
expect(buttonEl).toHaveAttribute('id');
expect(contentEl).toHaveAttribute('aria-labelledby');
expect(contentEl).toHaveAttribute('aria-labelledby', buttonEl.id);
expect(contentEl).toHaveAttribute('id');
});

Expand All @@ -77,8 +78,8 @@ describe('AccordionItem', function () {
const contentEl = screen.getByText(defaultChildren);

expect(buttonEl).toHaveAttribute('aria-controls', 'test-id');
expect(buttonEl).toHaveAttribute('id', 'test-id-button');
expect(contentEl).toHaveAttribute('aria-labelledby', 'test-id-button');
expect(buttonEl).toHaveAttribute('id', 'test-id__button');
expect(contentEl).toHaveAttribute('aria-labelledby', 'test-id__button');
expect(contentEl).toHaveAttribute('id', 'test-id');
});

Expand Down
49 changes: 28 additions & 21 deletions packages/design-system/src/components/Accordion/AccordionItem.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React, { useState } from 'react';
import { AddIcon, RemoveIcon } from '../Icons';
import classNames from 'classnames';
import uniqueId from 'lodash/uniqueId';
import { t } from '../i18n';
import useId from '../utilities/useId';

export interface AccordionItemProps {
/**
Expand Down Expand Up @@ -43,11 +43,11 @@ export interface AccordionItemProps {
/**
* Icon to overwrite default close icon
*/
closeIcon?: React.ReactNode;
closeIconComponent?: React.ReactElement<any> | any | ((...args: any[]) => any);
/**
* Icon to overwrite default open icon
*/
openIcon?: React.ReactNode;
openIconComponent?: React.ReactElement<any> | any | ((...args: any[]) => any);
}

export interface AccordionItemState {
Expand All @@ -65,15 +65,15 @@ export const AccordionItem: React.FC<AccordionItemProps> = ({
// TODO: Explore deprecating `isControlledOpen` in favor of `isOpen`
isControlledOpen,
onChange,
closeIcon,
openIcon,
closeIconComponent,
openIconComponent,
}) => {
const contentClasses = classNames('ds-c-accordion__content', contentClassName);
const buttonClasses = classNames('ds-c-accordion__button', buttonClassName);
const HeadingTag = `h${headingLevel}` as const;
const isControlled = !!onChange;
const contentId = id || uniqueId('accordionItem_');
const buttonId = `${contentId}-button`;
const contentId = useId('accordion-item--', id);
const buttonId = `${contentId}__button`;
const [isOpen, setIsOpen] = useState(isControlled ? isControlledOpen : defaultOpen);

// Set the state for opening and closing an accordion item
Expand All @@ -87,6 +87,25 @@ export const AccordionItem: React.FC<AccordionItemProps> = ({

const isItemOpen = isControlled ? isControlledOpen : isOpen;

const CloseIconComponent = closeIconComponent;
const OpenIconComponent = openIconComponent;
const closeIcon = (
<CloseIconComponent
className="ds-c-accordion__button-icon"
title={t('accordion.close')}
ariaHidden={false}
id={`${contentId}__icon`}
/>
);
const openIcon = (
<OpenIconComponent
className="ds-c-accordion__button-icon"
title={t('accordion.open')}
ariaHidden={false}
id={`${contentId}__icon`}
/>
);

if (heading) {
return (
<>
Expand Down Expand Up @@ -119,20 +138,8 @@ export const AccordionItem: React.FC<AccordionItemProps> = ({
AccordionItem.defaultProps = {
defaultOpen: false,
headingLevel: '2',
closeIcon: (
<RemoveIcon
className="ds-c-accordion__button-icon"
title={t('accordion.close')}
ariaHidden={false}
/>
),
openIcon: (
<AddIcon
className="ds-c-accordion__button-icon"
title={t('accordion.open')}
ariaHidden={false}
/>
),
closeIconComponent: RemoveIcon,
openIconComponent: AddIcon,
};

export default AccordionItem;
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,22 @@ exports[`Accordion renders accordion 1`] = `
aria-controls="1"
aria-expanded="false"
class="ds-c-accordion__button"
id="1-button"
id="1__button"
type="button"
>
First amendment
<svg
aria-hidden="false"
aria-labelledby="icon-1__title"
aria-labelledby="1__icon__title"
class="ds-c-icon ds-c-icon--add ds-c-accordion__button-icon"
focusable="false"
id="icon-1"
id="1__icon"
role="img"
viewBox="3 3 18 18"
xmlns="http://www.w3.org/2000/svg"
>
<title
id="icon-1__title"
id="1__icon__title"
>
Open
</title>
Expand All @@ -38,7 +38,7 @@ exports[`Accordion renders accordion 1`] = `
</button>
</h2>
<div
aria-labelledby="1-button"
aria-labelledby="1__button"
class="ds-c-accordion__content"
hidden=""
id="1"
Expand All @@ -54,22 +54,22 @@ exports[`Accordion renders accordion 1`] = `
aria-controls="2"
aria-expanded="false"
class="ds-c-accordion__button"
id="2-button"
id="2__button"
type="button"
>
Second amendment
<svg
aria-hidden="false"
aria-labelledby="icon-2__title"
aria-labelledby="2__icon__title"
class="ds-c-icon ds-c-icon--add ds-c-accordion__button-icon"
focusable="false"
id="icon-2"
id="2__icon"
role="img"
viewBox="3 3 18 18"
xmlns="http://www.w3.org/2000/svg"
>
<title
id="icon-2__title"
id="2__icon__title"
>
Open
</title>
Expand All @@ -80,7 +80,7 @@ exports[`Accordion renders accordion 1`] = `
</button>
</h2>
<div
aria-labelledby="2-button"
aria-labelledby="2__button"
class="ds-c-accordion__content"
hidden=""
id="2"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,25 @@ exports[`AccordionItem renders an accordion item 1`] = `
class="ds-c-accordion__heading"
>
<button
aria-controls="accordionItem_1"
aria-controls="static-id"
aria-expanded="false"
class="ds-c-accordion__button"
id="accordionItem_1-button"
id="static-id__button"
type="button"
>
Heading for accordion item
<svg
aria-hidden="false"
aria-labelledby="icon-2__title"
aria-labelledby="static-id__icon__title"
class="ds-c-icon ds-c-icon--add ds-c-accordion__button-icon"
focusable="false"
id="icon-2"
id="static-id__icon"
role="img"
viewBox="3 3 18 18"
xmlns="http://www.w3.org/2000/svg"
>
<title
id="icon-2__title"
id="static-id__icon__title"
>
Open
</title>
Expand All @@ -35,10 +35,10 @@ exports[`AccordionItem renders an accordion item 1`] = `
</button>
</h2>
<div
aria-labelledby="accordionItem_1-button"
aria-labelledby="static-id__button"
class="ds-c-accordion__content"
hidden=""
id="accordionItem_1"
id="static-id"
>
&lt;p&gt;Content for accordion item&lt;/p&gt;
</div>
Expand Down
7 changes: 2 additions & 5 deletions packages/design-system/src/components/Alert/Alert.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,9 @@ function expectHasClass(className: string) {

describe('Alert', function () {
it('renders alert', () => {
renderAlert();
renderAlert({ id: 'static-id' });
const alert = screen.getByRole('region');
expect(alert.className).toContain('ds-c-alert');

const body = screen.getByText(defaultText);
expect(body).toBeDefined();
expect(alert).toMatchSnapshot();
});

it('renders a heading', () => {
Expand Down
11 changes: 8 additions & 3 deletions packages/design-system/src/components/Alert/Alert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import React from 'react';
import { useRef } from 'react';
import classNames from 'classnames';
import mergeRefs from '../utilities/mergeRefs';
import uniqueId from 'lodash/uniqueId';
import useAutofocus from '../utilities/useAutoFocus';
import useAlertAnalytics from './useAlertAnalytics';
import { InfoCircleIcon, AlertCircleIcon, WarningIcon, CheckCircleIcon } from '../Icons';
import { t } from '../i18n';
import { AnalyticsOverrideProps } from '../analytics';
import useId from '../utilities/useId';

export type AlertHeadingLevel = '1' | '2' | '3' | '4' | '5' | '6';
export type AlertRole = 'alert' | 'alertdialog' | 'region' | 'status';
Expand Down Expand Up @@ -44,6 +44,10 @@ export interface BaseAlertProps extends AnalyticsOverrideProps {
* Boolean to hide the `Alert` icon
*/
hideIcon?: boolean;
/**
* A unique ID for this element. A unique ID will be generated if one isn't provided.
*/
id?: string;
/**
* ARIA `role`, defaults to 'region'
*/
Expand All @@ -68,8 +72,9 @@ export type AlertProps = BaseAlertProps &
export const Alert: React.FC<AlertProps> = (props: AlertProps) => {
const { headingRef, bodyRef } = useAlertAnalytics(props);
const focusRef = useAutofocus(props.autoFocus);
const headingId = useRef(props.headingId ?? uniqueId('alert_')).current;
const a11yLabelId = useRef(uniqueId('alert_a11y_label_')).current;
const rootId = useId('alert--', props.id);
const headingId = props.headingId ?? `${rootId}__heading`;
const a11yLabelId = `${rootId}__a11y-label`;

if (process.env.NODE_ENV !== 'production') {
if (!props.heading && !props.children) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,36 @@ Array [
},
]
`;

exports[`Alert renders alert 1`] = `
<div
aria-labelledby="static-id__a11y-label"
class="ds-c-alert"
id="static-id"
role="region"
>
<svg
aria-hidden="true"
class="ds-c-icon ds-c-icon--info-circle ds-c-alert__icon"
focusable="false"
viewBox="37 2 135 135"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M162.18 41.592c-5.595-9.586-13.185-17.176-22.771-22.771-9.588-5.595-20.055-8.392-31.408-8.392-11.352 0-21.822 2.797-31.408 8.392-9.587 5.594-17.177 13.184-22.772 22.771-5.596 9.587-8.393 20.057-8.393 31.408 0 11.352 2.798 21.82 8.392 31.408 5.595 9.585 13.185 17.176 22.772 22.771 9.587 5.595 20.056 8.392 31.408 8.392s21.822-2.797 31.408-8.392c9.586-5.594 17.176-13.185 22.771-22.771 5.594-9.587 8.391-20.057 8.391-31.408 0-11.352-2.797-21.822-8.39-31.408zM97.572 26.071c0-.761.244-1.385.733-1.874.489-.488 1.114-.733 1.874-.733h15.644c.76 0 1.385.245 1.872.733.488.489.734 1.113.734 1.874v13.036c0 .76-.246 1.385-.734 1.873-.487.489-1.112.733-1.872.733h-15.644c-.76 0-1.385-.244-1.874-.733-.489-.488-.733-1.113-.733-1.873V26.071zm31.285 86.036c0 .76-.246 1.385-.733 1.872-.487.489-1.112.733-1.874.733h-36.5c-.761 0-1.385-.244-1.874-.733-.489-.488-.733-1.113-.733-1.873V99.07c0-.762.244-1.385.733-1.874.489-.488 1.114-.733 1.874-.733h7.822V70.392H89.75c-.761 0-1.385-.244-1.874-.733-.489-.488-.733-1.113-.733-1.874V54.75c0-.761.244-1.385.733-1.874.489-.489 1.114-.733 1.874-.733h26.073c.76 0 1.385.244 1.872.733.488.489.734 1.113.734 1.874v41.714h7.82c.761 0 1.386.245 1.874.733.487.488.733 1.113.733 1.874v13.036z"
/>
</svg>
<div
class="ds-c-alert__body"
id="static-id__heading"
>
<span
class="ds-c-alert__a11y-label ds-u-visibility--screen-reader"
id="static-id__a11y-label"
>
Notice:
</span>
Ruhroh
</div>
</div>
`;
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ function makeAutocomplete(customProps = {}) {
const props = {
items: defaultItems,
children: <TextField label="autocomplete" name="autocomplete_field" />,
id: 'static-id',
...customProps,
};
return render(<Autocomplete {...props} />);
Expand Down Expand Up @@ -73,6 +74,13 @@ describe('Autocomplete', () => {
expect(ul).toMatchSnapshot();
});

it('generates ids when no id is provided', () => {
makeAutocomplete({ isOpen: true, id: undefined });
const idRegex = /autocomplete--\d+/;
expect(screen.getByRole('listbox').id).toMatch(idRegex);
expect(screen.getByRole('combobox').id).toMatch(idRegex);
});

it('renders item with custom className', () => {
const items = [
{ id: '1a', name: 'Normal item' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ import Button from '../Button/Button';
import React, { useRef } from 'react';
import TextField from '../TextField/TextField';
import classNames from 'classnames';
import uniqueId from 'lodash/uniqueId';
import { errorPlacementDefault } from '../flags';
import { t } from '../i18n';
import createFilteredA11yStatusMessageFn from './createFilteredA11yStatusMessageFn';
import useId from '../utilities/useId';

export interface AutocompleteItem {
/**
Expand Down Expand Up @@ -163,11 +163,11 @@ function isTextField(child: React.ReactElement): boolean {
* [refer to its full documentation page](https://design.cms.gov/components/autocomplete/).
*/
export const Autocomplete = (props: AutocompleteProps) => {
const id = useRef(props.id ?? uniqueId('autocomplete__input--')).current;
const labelId = useRef(props.labelId ?? uniqueId('autocomplete__label--')).current;
const menuId = useRef(uniqueId('autocomplete__menu--')).current;
const menuContainerId = useRef(uniqueId('autocomplete__menu-container--')).current;
const menuHeadingId = useRef(uniqueId('autocomplete__heading--')).current;
const id = useId('autocomplete--', props.id);
const labelId = props.labelId ?? `${id}__label`;
const menuId = `${id}__menu`;
const menuContainerId = `${id}__menu-container`;
const menuHeadingId = `${id}__heading`;

const {
ariaClearLabel,
Expand Down Expand Up @@ -227,7 +227,7 @@ export const Autocomplete = (props: AutocompleteProps) => {
})}
key={item.id}
role="option"
{...getItemProps({ item })}
{...getItemProps({ item, id: `${id}__item--${index}` })}
>
{item.children ?? props.itemToString(item)}
</li>
Expand Down
Loading

0 comments on commit 3f09f12

Please sign in to comment.