diff --git a/src/components/input/SelectNext.tsx b/src/components/input/SelectNext.tsx index 029cb687e..4732b3abe 100644 --- a/src/components/input/SelectNext.tsx +++ b/src/components/input/SelectNext.tsx @@ -15,8 +15,8 @@ import { useFocusAway } from '../../hooks/use-focus-away'; import { useKeyPress } from '../../hooks/use-key-press'; import { useSyncedRef } from '../../hooks/use-synced-ref'; import type { PresentationalProps } from '../../types'; +import { downcastRef } from '../../util/typing'; import { MenuCollapseIcon, MenuExpandIcon } from '../icons'; -import Button from './Button'; import { inputGroupStyles } from './InputGroup'; import SelectContext from './SelectContext'; @@ -149,6 +149,9 @@ export type SelectProps = PresentationalProps & { */ buttonId?: string; + 'aria-label'?: string; + 'aria-labelledby'?: string; + /** @deprecated Use buttonContent instead */ label?: ComponentChildren; }; @@ -163,6 +166,8 @@ function SelectMain({ elementRef, classes, buttonId, + 'aria-label': ariaLabel, + 'aria-labelledby': ariaLabelledBy, }: SelectProps) { const [listboxOpen, setListboxOpen] = useState(false); const closeListbox = useCallback(() => setListboxOpen(false), []); @@ -212,11 +217,11 @@ function SelectMain({ className={classnames('relative w-full border rounded', inputGroupStyles)} ref={wrapperRef} > - +
    { name: 'Closed Select listbox', content: () => createComponent( - { buttonContent: 'Select' }, + { buttonContent: 'Select', 'aria-label': 'Select' }, { optionsChildrenAsCallback: false }, ), }, @@ -297,7 +297,7 @@ describe('SelectNext', () => { name: 'Open Select listbox', content: () => { const wrapper = createComponent( - { buttonContent: 'Select' }, + { buttonContent: 'Select', 'aria-label': 'Select' }, { optionsChildrenAsCallback: false }, ); toggleListbox(wrapper); diff --git a/src/pattern-library/components/Library.tsx b/src/pattern-library/components/Library.tsx index 842b83379..18a8b7766 100644 --- a/src/pattern-library/components/Library.tsx +++ b/src/pattern-library/components/Library.tsx @@ -171,7 +171,7 @@ export type LibraryDemoProps = { classes?: string | string[]; /** Inline styles to apply to the demo container */ style?: JSX.CSSProperties; - title?: string; + title?: ComponentChildren; /** * Should the demo also render the source? When true, a "Source" tab will be * rendered, which will display the JSX source of the Demo's children. diff --git a/src/pattern-library/components/patterns/prototype/SelectNextPage.tsx b/src/pattern-library/components/patterns/prototype/SelectNextPage.tsx index 765e16ba8..f2acc4da9 100644 --- a/src/pattern-library/components/patterns/prototype/SelectNextPage.tsx +++ b/src/pattern-library/components/patterns/prototype/SelectNextPage.tsx @@ -1,7 +1,8 @@ import classnames from 'classnames'; -import { useCallback, useMemo, useState } from 'preact/hooks'; +import { useCallback, useId, useMemo, useState } from 'preact/hooks'; import { ArrowLeftIcon, ArrowRightIcon } from '../../../../components/icons'; +import type { SelectNextProps } from '../../../../components/input'; import { IconButton, InputGroup } from '../../../../components/input'; import SelectNext from '../../../../components/input/SelectNext'; import Library from '../../Library'; @@ -23,65 +24,77 @@ const defaultItems: ItemType[] = [ function SelectExample({ disabled, textOnly, - classes, items = defaultItems, -}: { - disabled?: boolean; + ...rest +}: Pick< + SelectNextProps, + 'aria-label' | 'aria-labelledby' | 'classes' | 'disabled' +> & { textOnly?: boolean; - classes?: string; - items?: typeof defaultItems; + items?: ItemType[]; }) { const [value, setValue] = useState(); + const buttonId = useId(); return ( - - {textOnly && value.name} - {!textOnly && ( -
    -
    {value.name}
    -
    - {value.id} -
    -
    - )} - - ) : disabled ? ( - <>This is disabled - ) : ( - <>Select one... - ) - } - > - {items.map(item => ( - - {({ disabled }) => - textOnly ? ( - item.name - ) : ( - <> - {item.name} -
    -
    - {item.id} + <> + {!rest['aria-label'] && !rest['aria-labelledby'] && ( + + )} + + {textOnly && value.name} + {!textOnly && ( +
    +
    {value.name}
    +
    + {value.id} +
    - - ) - } - - ))} -
    + )} + + ) : disabled ? ( + <>This is disabled + ) : ( + <>Select one… + ) + } + > + {items.map(item => ( + + {({ disabled }) => + textOnly ? ( + item.name + ) : ( + <> + {item.name} +
    +
    + {item.id} +
    + + ) + } + + ))} + + ); } @@ -99,53 +112,58 @@ function InputGroupSelectExample({ classes }: { classes?: string }) { const newIndex = selectedIndex - 1; setSelected(defaultItems[newIndex] ?? selected); }, [selected, selectedIndex]); + const buttonId = useId(); return ( - - - -
    {selected.name}
    -
    - {selected.id} + <> + + + + +
    {selected.name}
    +
    + {selected.id} +
    -
    - ) : ( - <>Select one... - ) - } - > - {defaultItems.map(item => ( - - {item.name} -
    -
    - {item.id} -
    - - ))} - - = defaultItems.length - 1} - /> - + ) : ( + <>Select one… + ) + } + > + {defaultItems.map(item => ( + + {item.name} +
    +
    + {item.id} +
    + + ))} + + = defaultItems.length - 1} + /> + + ); } @@ -228,6 +246,44 @@ export default function SelectNextPage() { + +

    + There are three ways to label a SelectNext. Make sure + you always use one of them. +

    + + + Via{' '} + + {'<'}label {'/>'} + {' '} + linked to buttonId + + } + > +
    + +
    +
    + + +
    + +
    +
    + + +
    +

    + Select a person with aria labelledby +

    + +
    +
    +
    +

    SelectNext makes sure the button content never @@ -403,7 +459,7 @@ export default function SelectNextPage() {

    ) : ( - <>Select one... + <>Select one… ) } >