From c22f0eabbd3a0e336f6c26f2bcf0ac323406535c Mon Sep 17 00:00:00 2001 From: Maximo Macchi <232606069+maximo-macchi-cb@users.noreply.github.com> Date: Sun, 14 Dec 2025 22:19:38 -0600 Subject: [PATCH 01/14] Add default prevention escape to FocusTrap and support selecting value with Tab in Select --- packages/web/src/alpha/select/DefaultSelectOption.tsx | 10 ++++++++++ packages/web/src/overlays/FocusTrap.tsx | 1 + 2 files changed, 11 insertions(+) diff --git a/packages/web/src/alpha/select/DefaultSelectOption.tsx b/packages/web/src/alpha/select/DefaultSelectOption.tsx index 69ccd05db..9e6209d27 100644 --- a/packages/web/src/alpha/select/DefaultSelectOption.tsx +++ b/packages/web/src/alpha/select/DefaultSelectOption.tsx @@ -134,6 +134,15 @@ const DefaultSelectOptionComponent = memo( ); const handleClick = useCallback(() => onClick?.(value), [onClick, value]); + const handleKeyDown = useCallback( + (event: React.KeyboardEvent) => { + if (event.key === 'Tab') { + event.preventDefault(); + handleClick(); + } + }, + [handleClick], + ); // Since Cell's ref prop is type HTMLDivElement, we need to wrap it in a Pressable to get ref forwarding. // On web, the option role doesn't work well with ara-checked and screen readers @@ -147,6 +156,7 @@ const DefaultSelectOptionComponent = memo( className={cx(selectOptionCss, className)} disabled={disabled} onClick={handleClick} + onKeyDown={handleKeyDown} role={accessibilityRole} {...props} > diff --git a/packages/web/src/overlays/FocusTrap.tsx b/packages/web/src/overlays/FocusTrap.tsx index 43830f654..845b3a97b 100644 --- a/packages/web/src/overlays/FocusTrap.tsx +++ b/packages/web/src/overlays/FocusTrap.tsx @@ -133,6 +133,7 @@ export const FocusTrap = memo(function FocusTrap({ // trap focus for accessibility const handleKeyboardNavigation = useCallback( (event: KeyboardEvent, element: RefObject['current']) => { + if (event.defaultPrevented) return; const document = getBrowserGlobals()?.document; const activeElement = document?.activeElement as HTMLElement; From a1e8cb8d744a547354cd97970ad7480c79cabbcd Mon Sep 17 00:00:00 2001 From: Maximo Macchi <232606069+maximo-macchi-cb@users.noreply.github.com> Date: Sun, 14 Dec 2025 23:26:54 -0600 Subject: [PATCH 02/14] Change default a11y labels --- packages/mobile/src/alpha/select/Select.tsx | 2 +- packages/web/src/alpha/select/Select.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/mobile/src/alpha/select/Select.tsx b/packages/mobile/src/alpha/select/Select.tsx index 194d06cf6..fce0f6a35 100644 --- a/packages/mobile/src/alpha/select/Select.tsx +++ b/packages/mobile/src/alpha/select/Select.tsx @@ -62,7 +62,7 @@ const SelectBase = memo( compact, label, labelVariant, - accessibilityLabel = type === 'multi' ? 'Multi select control' : undefined, + accessibilityLabel = 'Select control', accessibilityHint, accessibilityRoles = defaultAccessibilityRoles, selectAllLabel, diff --git a/packages/web/src/alpha/select/Select.tsx b/packages/web/src/alpha/select/Select.tsx index 701c90f8c..5ea5ff42e 100644 --- a/packages/web/src/alpha/select/Select.tsx +++ b/packages/web/src/alpha/select/Select.tsx @@ -74,9 +74,9 @@ const SelectBase = memo( compact, label, labelVariant, - accessibilityLabel = 'Select control', + accessibilityLabel = 'Select dropdown', accessibilityRoles = defaultAccessibilityRoles, - controlAccessibilityLabel, + controlAccessibilityLabel = 'Select control', selectAllLabel, emptyOptionsLabel, clearAllLabel, From b4bb39f92d7a22d4c32cd6c0c194a19e26b0d979 Mon Sep 17 00:00:00 2001 From: Maximo Macchi <232606069+maximo-macchi-cb@users.noreply.github.com> Date: Sun, 14 Dec 2025 23:27:36 -0600 Subject: [PATCH 03/14] Update select controls to announce control label and selected value or placeholder --- .../src/alpha/select/DefaultSelectControl.tsx | 54 +++++++++++++++---- .../src/alpha/select/DefaultSelectControl.tsx | 48 +++++++++++++---- 2 files changed, 81 insertions(+), 21 deletions(-) diff --git a/packages/mobile/src/alpha/select/DefaultSelectControl.tsx b/packages/mobile/src/alpha/select/DefaultSelectControl.tsx index befce1f50..1856094fe 100644 --- a/packages/mobile/src/alpha/select/DefaultSelectControl.tsx +++ b/packages/mobile/src/alpha/select/DefaultSelectControl.tsx @@ -122,6 +122,37 @@ export const DefaultSelectControlComponent = memo( return map; }, [options]); + const singleValueContent = useMemo(() => { + const option = !isMultiSelect ? optionsMap.get(value as SelectOptionValue) : undefined; + const label = option?.label ?? option?.description ?? option?.value ?? placeholder; + return hasValue ? label : placeholder; + }, [hasValue, isMultiSelect, optionsMap, placeholder, value]); + + const computedControlAccessibilityLabel = useMemo(() => { + // For multi-select, set the label to the content of each selected value and the hidden selected options label + if (isMultiSelect) { + const selectedValues = (value as SelectOptionValue[]) + .map((v) => { + const option = optionsMap.get(v); + return option?.label ?? option?.description ?? option?.value ?? v; + }) + .slice(0, maxSelectedOptionsToShow) + .join(', '); + return `${accessibilityLabel}, ${(value as SelectOptionValue[]).length > 0 ? selectedValues : placeholder}, ${(value as SelectOptionValue[]).length > maxSelectedOptionsToShow ? hiddenSelectedOptionsLabel : ''}`; + } + // If value is React node, fallback to only using passed in accessibility label + return `${accessibilityLabel ?? ''}, ${typeof singleValueContent === 'string' ? singleValueContent : ''}`; + }, [ + accessibilityLabel, + hiddenSelectedOptionsLabel, + isMultiSelect, + maxSelectedOptionsToShow, + optionsMap, + placeholder, + singleValueContent, + value, + ]); + // Prop value doesn't have default value because it affects the color of the // animated caret const focusedVariant = useInputVariant(!!open, variant ?? 'foregroundMuted'); @@ -214,24 +245,21 @@ export const DefaultSelectControlComponent = memo( ); } - const option = !isMultiSelect ? optionsMap.get(value as SelectOptionValue) : undefined; - const label = option?.label ?? option?.description ?? option?.value ?? placeholder; - const content = hasValue ? label : placeholder; - return typeof content === 'string' ? ( + return typeof singleValueContent === 'string' ? ( - {content} + {singleValueContent} ) : ( - content + singleValueContent ); }, [ hasValue, isMultiSelect, - optionsMap, - placeholder, + singleValueContent, value, maxSelectedOptionsToShow, hiddenSelectedOptionsLabel, + optionsMap, removeSelectedOptionAccessibilityLabel, disabled, onChange, @@ -242,7 +270,7 @@ export const DefaultSelectControlComponent = memo( setOpen((s) => !s)} @@ -288,7 +316,7 @@ export const DefaultSelectControlComponent = memo( [ ref, accessibilityHint, - accessibilityLabel, + computedControlAccessibilityLabel, disabled, styles?.controlInputNode, styles?.controlStartNode, @@ -307,7 +335,11 @@ export const DefaultSelectControlComponent = memo( const endNode = useMemo( () => ( - setOpen((s) => !s)}> + setOpen((s) => !s)} + > { + const option = !isMultiSelect ? optionsMap.get(value as SelectOptionValue) : undefined; + const label = option?.label ?? option?.description ?? option?.value ?? placeholder; + return hasValue ? label : placeholder; + }, [hasValue, isMultiSelect, optionsMap, placeholder, value]); + + const computedControlAccessibilityLabel = useMemo(() => { + // For multi-select, set the label to the content of each selected value and the hidden selected options label + if (isMultiSelect) { + const selectedValues = (value as SelectOptionValue[]) + .map((v) => { + const option = optionsMap.get(v); + return option?.label ?? option?.description ?? option?.value ?? v; + }) + .slice(0, maxSelectedOptionsToShow) + .join(', '); + return `${accessibilityLabel}, ${(value as SelectOptionValue[]).length > 0 ? selectedValues : placeholder}, ${(value as SelectOptionValue[]).length > maxSelectedOptionsToShow ? hiddenSelectedOptionsLabel : ''}`; + } + // If value is React node, fallback to only using passed in accessibility label + return `${accessibilityLabel ?? ''}, ${typeof singleValueContent === 'string' ? singleValueContent : ''}`; + }, [ + accessibilityLabel, + hiddenSelectedOptionsLabel, + isMultiSelect, + maxSelectedOptionsToShow, + optionsMap, + placeholder, + singleValueContent, + value, + ]); + const controlPressableRef = useRef(null); const valueNodeContainerRef = useRef(null); const handleUnselectValue = useCallback( @@ -263,10 +294,7 @@ const DefaultSelectControlComponent = memo( ); } - const option = !isMultiSelect ? optionsMap.get(value as SelectOptionValue) : undefined; - const label = option?.label ?? option?.description ?? option?.value ?? placeholder; - const content = hasValue ? label : placeholder; - return typeof content === 'string' ? ( + return typeof singleValueContent === 'string' ? ( - {content} + {singleValueContent} ) : ( - content + singleValueContent ); }, [ hasValue, isMultiSelect, - optionsMap, - placeholder, + singleValueContent, value, maxSelectedOptionsToShow, hiddenSelectedOptionsLabel, + optionsMap, removeSelectedOptionAccessibilityLabel, handleUnselectValue, ]); @@ -298,7 +326,7 @@ const DefaultSelectControlComponent = memo( ), [ - accessibilityLabel, + computedControlAccessibilityLabel, ariaHaspopup, interactableBlendStyles, classNames?.controlInputNode, From 78fa9dfac7ea86a2a71467f360843c9d1ead9f53 Mon Sep 17 00:00:00 2001 From: Maximo Macchi <232606069+maximo-macchi-cb@users.noreply.github.com> Date: Sun, 14 Dec 2025 23:27:48 -0600 Subject: [PATCH 04/14] Update stories to use more cleary custom a11y labels --- .../mobile/src/alpha/select/__stories__/AlphaSelect.stories.tsx | 2 +- packages/web/src/alpha/select/__stories__/Select.stories.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/mobile/src/alpha/select/__stories__/AlphaSelect.stories.tsx b/packages/mobile/src/alpha/select/__stories__/AlphaSelect.stories.tsx index f43ffa1ac..57f6f26a1 100644 --- a/packages/mobile/src/alpha/select/__stories__/AlphaSelect.stories.tsx +++ b/packages/mobile/src/alpha/select/__stories__/AlphaSelect.stories.tsx @@ -271,7 +271,7 @@ const AccessibilityLabelExample = () => { return ( Date: Sun, 14 Dec 2025 23:35:30 -0600 Subject: [PATCH 05/14] Remove unnecessary usage of a11y hint prop --- packages/mobile/src/alpha/select/DefaultSelectControl.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/mobile/src/alpha/select/DefaultSelectControl.tsx b/packages/mobile/src/alpha/select/DefaultSelectControl.tsx index 1856094fe..dddba8b9b 100644 --- a/packages/mobile/src/alpha/select/DefaultSelectControl.tsx +++ b/packages/mobile/src/alpha/select/DefaultSelectControl.tsx @@ -60,7 +60,6 @@ export const DefaultSelectControlComponent = memo( compact, maxSelectedOptionsToShow = 3, accessibilityLabel, - accessibilityHint, hiddenSelectedOptionsLabel = 'more', removeSelectedOptionAccessibilityLabel = 'Remove', style, @@ -269,7 +268,6 @@ export const DefaultSelectControlComponent = memo( () => ( Date: Sun, 14 Dec 2025 23:43:00 -0600 Subject: [PATCH 06/14] Update tests --- packages/mobile/src/alpha/select/DefaultSelectControl.tsx | 4 ++-- .../src/alpha/select/__tests__/DefaultSelectControl.test.tsx | 2 +- packages/mobile/src/alpha/select/__tests__/Select.test.tsx | 2 +- .../src/alpha/select/__tests__/DefaultSelectControl.test.tsx | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/mobile/src/alpha/select/DefaultSelectControl.tsx b/packages/mobile/src/alpha/select/DefaultSelectControl.tsx index dddba8b9b..0eb89f485 100644 --- a/packages/mobile/src/alpha/select/DefaultSelectControl.tsx +++ b/packages/mobile/src/alpha/select/DefaultSelectControl.tsx @@ -137,10 +137,10 @@ export const DefaultSelectControlComponent = memo( }) .slice(0, maxSelectedOptionsToShow) .join(', '); - return `${accessibilityLabel}, ${(value as SelectOptionValue[]).length > 0 ? selectedValues : placeholder}, ${(value as SelectOptionValue[]).length > maxSelectedOptionsToShow ? hiddenSelectedOptionsLabel : ''}`; + return `${accessibilityLabel}, ${(value as SelectOptionValue[]).length > 0 ? selectedValues : placeholder}${(value as SelectOptionValue[]).length > maxSelectedOptionsToShow ? ', ' + hiddenSelectedOptionsLabel : ''}`; } // If value is React node, fallback to only using passed in accessibility label - return `${accessibilityLabel ?? ''}, ${typeof singleValueContent === 'string' ? singleValueContent : ''}`; + return `${accessibilityLabel ?? ''}${typeof singleValueContent === 'string' ? ', ' + singleValueContent : ''}`; }, [ accessibilityLabel, hiddenSelectedOptionsLabel, diff --git a/packages/mobile/src/alpha/select/__tests__/DefaultSelectControl.test.tsx b/packages/mobile/src/alpha/select/__tests__/DefaultSelectControl.test.tsx index 40268d3a9..eec720812 100644 --- a/packages/mobile/src/alpha/select/__tests__/DefaultSelectControl.test.tsx +++ b/packages/mobile/src/alpha/select/__tests__/DefaultSelectControl.test.tsx @@ -60,7 +60,7 @@ describe('DefaultSelectControl', () => { ); const button = screen.getByRole('button'); - expect(button.props.accessibilityLabel).toBe('Custom accessibility label'); + expect(button.props.accessibilityLabel).toBe('Custom accessibility label, Option 1'); expect(button.props.accessibilityHint).toBe('Custom accessibility hint'); }); diff --git a/packages/mobile/src/alpha/select/__tests__/Select.test.tsx b/packages/mobile/src/alpha/select/__tests__/Select.test.tsx index d2fb7acf8..4b2df96a4 100644 --- a/packages/mobile/src/alpha/select/__tests__/Select.test.tsx +++ b/packages/mobile/src/alpha/select/__tests__/Select.test.tsx @@ -57,7 +57,7 @@ describe('Select', () => { ); const button = screen.getByRole('button'); - expect(button.props.accessibilityLabel).toBe('Custom accessibility label'); + expect(button.props.accessibilityLabel).toBe('Custom accessibility label, Select an option'); expect(button.props.accessibilityHint).toBe('Custom accessibility hint'); }); diff --git a/packages/web/src/alpha/select/__tests__/DefaultSelectControl.test.tsx b/packages/web/src/alpha/select/__tests__/DefaultSelectControl.test.tsx index ecda52198..d43988eab 100644 --- a/packages/web/src/alpha/select/__tests__/DefaultSelectControl.test.tsx +++ b/packages/web/src/alpha/select/__tests__/DefaultSelectControl.test.tsx @@ -43,7 +43,7 @@ describe('DefaultSelectControl', () => { const button = screen.getByRole('button'); expect(button).toHaveAttribute('aria-haspopup', 'listbox'); - expect(button).toHaveAttribute('aria-label', 'Custom accessibility label'); + expect(button).toHaveAttribute('aria-label', 'Custom accessibility label, Option 1'); }); it('renders with proper focus management', async () => { From 8047c049893fe5f08dc311e03b8eec90f7a7db2b Mon Sep 17 00:00:00 2001 From: Maximo Macchi <232606069+maximo-macchi-cb@users.noreply.github.com> Date: Mon, 15 Dec 2025 10:59:57 -0600 Subject: [PATCH 07/14] Update behavior of a11y labels for combobox --- packages/mobile/src/alpha/combobox/Combobox.tsx | 1 + .../src/alpha/combobox/DefaultComboboxControl.tsx | 12 +++++++++++- .../src/alpha/select/DefaultSelectControl.tsx | 2 +- packages/web/src/alpha/combobox/Combobox.tsx | 6 +++--- .../src/alpha/combobox/DefaultComboboxControl.tsx | 13 +++++++++++-- .../web/src/alpha/select/DefaultSelectControl.tsx | 4 ++-- 6 files changed, 29 insertions(+), 9 deletions(-) diff --git a/packages/mobile/src/alpha/combobox/Combobox.tsx b/packages/mobile/src/alpha/combobox/Combobox.tsx index d0319a94d..b4449cef0 100644 --- a/packages/mobile/src/alpha/combobox/Combobox.tsx +++ b/packages/mobile/src/alpha/combobox/Combobox.tsx @@ -298,6 +298,7 @@ const ComboboxBase = memo( header={ ) => { const theme = useTheme(); const hasValue = hasSelectedValue(value); const shouldRenderSearchInput = !hideSearchInput && (!hasValue || open); + const computedAccessibilityLabel = useMemo(() => { + let label = accessibilityLabel; + if (!hasValue && typeof placeholder === 'string') { + label = `${label}, ${placeholder}`; + } + return label; + }, [hasValue, accessibilityLabel, placeholder]); + return ( ) } - placeholder={null} styles={{ ...props.styles, controlEndNode: { diff --git a/packages/mobile/src/alpha/select/DefaultSelectControl.tsx b/packages/mobile/src/alpha/select/DefaultSelectControl.tsx index 0eb89f485..5b1e710ab 100644 --- a/packages/mobile/src/alpha/select/DefaultSelectControl.tsx +++ b/packages/mobile/src/alpha/select/DefaultSelectControl.tsx @@ -137,7 +137,7 @@ export const DefaultSelectControlComponent = memo( }) .slice(0, maxSelectedOptionsToShow) .join(', '); - return `${accessibilityLabel}, ${(value as SelectOptionValue[]).length > 0 ? selectedValues : placeholder}${(value as SelectOptionValue[]).length > maxSelectedOptionsToShow ? ', ' + hiddenSelectedOptionsLabel : ''}`; + return `${accessibilityLabel}, ${(value as SelectOptionValue[]).length > 0 ? selectedValues : (placeholder ?? '')}${(value as SelectOptionValue[]).length > maxSelectedOptionsToShow ? ', ' + hiddenSelectedOptionsLabel : ''}`; } // If value is React node, fallback to only using passed in accessibility label return `${accessibilityLabel ?? ''}${typeof singleValueContent === 'string' ? ', ' + singleValueContent : ''}`; diff --git a/packages/web/src/alpha/combobox/Combobox.tsx b/packages/web/src/alpha/combobox/Combobox.tsx index 51ca6e22c..ac0057f3a 100644 --- a/packages/web/src/alpha/combobox/Combobox.tsx +++ b/packages/web/src/alpha/combobox/Combobox.tsx @@ -162,8 +162,8 @@ const ComboboxBase = memo( options, open: openProp, setOpen: setOpenProp, - placeholder, - accessibilityLabel = 'Combobox control', + accessibilityLabel = 'Combobox dropdown', + controlAccessibilityLabel = 'Combobox control', defaultOpen, searchText: searchTextProp, onSearch: onSearchProp, @@ -252,11 +252,11 @@ const ComboboxBase = memo( ref={controlRef} SelectControlComponent={ComboboxControl} accessibilityLabel={accessibilityLabel} + controlAccessibilityLabel={controlAccessibilityLabel} defaultOpen={defaultOpen} onChange={handleChange} open={open} options={filteredOptions} - placeholder={placeholder} setOpen={setOpen} type={type} value={value} diff --git a/packages/web/src/alpha/combobox/DefaultComboboxControl.tsx b/packages/web/src/alpha/combobox/DefaultComboboxControl.tsx index a68310e1a..7d28071f3 100644 --- a/packages/web/src/alpha/combobox/DefaultComboboxControl.tsx +++ b/packages/web/src/alpha/combobox/DefaultComboboxControl.tsx @@ -1,4 +1,4 @@ -import { memo, useCallback, useEffect, useRef } from 'react'; +import { memo, useCallback, useEffect, useMemo, useRef } from 'react'; import { NativeInput } from '../../controls/NativeInput'; import { HStack } from '../../layout'; @@ -27,6 +27,7 @@ export const DefaultComboboxControl = memo( compact, searchText, onSearch, + accessibilityLabel, ...props }: ComboboxControlProps) => { const searchInputRef = useRef(null); @@ -54,9 +55,18 @@ export const DefaultComboboxControl = memo( [setOpen], ); + const computedAccessibilityLabel = useMemo(() => { + let label = accessibilityLabel; + if (!hasValue && typeof placeholder === 'string') { + label = `${label}, ${placeholder}`; + } + return label; + }, [hasValue, accessibilityLabel, placeholder]); + return ( ) } - placeholder={null} styles={{ ...props.styles, controlEndNode: { diff --git a/packages/web/src/alpha/select/DefaultSelectControl.tsx b/packages/web/src/alpha/select/DefaultSelectControl.tsx index 94b63db83..bdc4fddf0 100644 --- a/packages/web/src/alpha/select/DefaultSelectControl.tsx +++ b/packages/web/src/alpha/select/DefaultSelectControl.tsx @@ -153,10 +153,10 @@ const DefaultSelectControlComponent = memo( }) .slice(0, maxSelectedOptionsToShow) .join(', '); - return `${accessibilityLabel}, ${(value as SelectOptionValue[]).length > 0 ? selectedValues : placeholder}, ${(value as SelectOptionValue[]).length > maxSelectedOptionsToShow ? hiddenSelectedOptionsLabel : ''}`; + return `${accessibilityLabel}, ${(value as SelectOptionValue[]).length > 0 ? selectedValues : (placeholder ?? '')}${(value as SelectOptionValue[]).length > maxSelectedOptionsToShow ? ', ' + hiddenSelectedOptionsLabel : ''}`; } // If value is React node, fallback to only using passed in accessibility label - return `${accessibilityLabel ?? ''}, ${typeof singleValueContent === 'string' ? singleValueContent : ''}`; + return `${accessibilityLabel ?? ''}${typeof singleValueContent === 'string' ? ', ' + singleValueContent : ''}`; }, [ accessibilityLabel, hiddenSelectedOptionsLabel, From 001041ff83c799d5bacb123755d464cde82170b4 Mon Sep 17 00:00:00 2001 From: Maximo Macchi <232606069+maximo-macchi-cb@users.noreply.github.com> Date: Mon, 15 Dec 2025 11:00:49 -0600 Subject: [PATCH 08/14] Update stories --- .../combobox/__stories__/Combobox.stories.tsx | 19 +++++++++++++++++++ .../combobox/__stories__/Combobox.stories.tsx | 3 ++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/mobile/src/alpha/combobox/__stories__/Combobox.stories.tsx b/packages/mobile/src/alpha/combobox/__stories__/Combobox.stories.tsx index 0dbf7f276..92c211218 100644 --- a/packages/mobile/src/alpha/combobox/__stories__/Combobox.stories.tsx +++ b/packages/mobile/src/alpha/combobox/__stories__/Combobox.stories.tsx @@ -169,6 +169,22 @@ const ControlledSearchExample = () => { ); }; +const AccessibilityLabelExample = () => { + const { value, onChange } = useMultiSelect({ initialValue: [] }); + + return ( + + ); +}; + const WithDescriptionsExample = () => { const { value, onChange } = useMultiSelect({ initialValue: [] }); @@ -757,6 +773,9 @@ const Default = () => { + + + diff --git a/packages/web/src/alpha/combobox/__stories__/Combobox.stories.tsx b/packages/web/src/alpha/combobox/__stories__/Combobox.stories.tsx index 917f597f4..360eed3e5 100644 --- a/packages/web/src/alpha/combobox/__stories__/Combobox.stories.tsx +++ b/packages/web/src/alpha/combobox/__stories__/Combobox.stories.tsx @@ -681,7 +681,8 @@ export const AccessibilityLabel = () => { return ( Date: Mon, 15 Dec 2025 11:12:07 -0600 Subject: [PATCH 09/14] Update tests --- .../mobile/src/alpha/combobox/__tests__/Combobox.test.tsx | 6 ++++-- packages/web/src/alpha/combobox/__tests__/Combobox.test.tsx | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/mobile/src/alpha/combobox/__tests__/Combobox.test.tsx b/packages/mobile/src/alpha/combobox/__tests__/Combobox.test.tsx index e4f051f2e..ac91ac75a 100644 --- a/packages/mobile/src/alpha/combobox/__tests__/Combobox.test.tsx +++ b/packages/mobile/src/alpha/combobox/__tests__/Combobox.test.tsx @@ -74,7 +74,7 @@ describe('Combobox', () => { ); const button = screen.getByRole('button'); - expect(button.props.accessibilityLabel).toBe('Custom combobox'); + expect(button.props.accessibilityLabel).toBe('Custom combobox, Search and select...'); }); it('handles disabled prop', () => { @@ -338,7 +338,9 @@ describe('Combobox', () => { ); const button = screen.getByRole('button'); - expect(button.props.accessibilityLabel).toBe('Custom accessibility label'); + expect(button.props.accessibilityLabel).toBe( + 'Custom accessibility label, Search and select...', + ); expect(button.props.accessibilityHint).toBe('Custom accessibility hint'); }); diff --git a/packages/web/src/alpha/combobox/__tests__/Combobox.test.tsx b/packages/web/src/alpha/combobox/__tests__/Combobox.test.tsx index a037763b0..70c4e82ff 100644 --- a/packages/web/src/alpha/combobox/__tests__/Combobox.test.tsx +++ b/packages/web/src/alpha/combobox/__tests__/Combobox.test.tsx @@ -96,7 +96,7 @@ describe('Combobox', () => { , ); - expect(screen.getByLabelText('Custom combobox')).toBeTruthy(); + expect(screen.getByLabelText('Custom combobox, Search and select...')).toBeTruthy(); const button = screen.getByRole('button'); fireEvent.click(button); expect(screen.getByLabelText('Combobox menu')).toBeTruthy(); From e23eea05371ac96ea5c2044651ea8bf8b0641b03 Mon Sep 17 00:00:00 2001 From: Maximo Macchi <232606069+maximo-macchi-cb@users.noreply.github.com> Date: Thu, 18 Dec 2025 13:50:25 -0500 Subject: [PATCH 10/14] Fallback to using label or default string for a11y labels --- packages/mobile/src/alpha/select/Select.tsx | 6 ++++-- packages/web/src/alpha/select/Select.tsx | 12 ++++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/mobile/src/alpha/select/Select.tsx b/packages/mobile/src/alpha/select/Select.tsx index fce0f6a35..5d2d82afd 100644 --- a/packages/mobile/src/alpha/select/Select.tsx +++ b/packages/mobile/src/alpha/select/Select.tsx @@ -62,7 +62,7 @@ const SelectBase = memo( compact, label, labelVariant, - accessibilityLabel = 'Select control', + accessibilityLabel, accessibilityHint, accessibilityRoles = defaultAccessibilityRoles, selectAllLabel, @@ -104,6 +104,8 @@ const SelectBase = memo( 'Select component must be fully controlled or uncontrolled: "open" and "setOpen" props must be provided together or not at all', ); + const fallbackAccessibilityLabel = typeof label === 'string' ? label : 'Select control'; + const rootStyles = useMemo(() => { return [style, styles?.root]; }, [style, styles?.root]); @@ -169,7 +171,7 @@ const SelectBase = memo( ], }); + const fallbackAccessibilityLabel = typeof label === 'string' ? label : 'Select dropdown'; + const fallbackControlAccessibilityLabel = + typeof label === 'string' ? label : 'Select control'; + const rootStyles = useMemo( () => ({ ...style, @@ -252,7 +256,7 @@ const SelectBase = memo( > Date: Thu, 18 Dec 2025 13:58:46 -0500 Subject: [PATCH 11/14] Update Tab key press so it only closes menu and doesn't select vlaue --- .../web/src/alpha/select/DefaultSelectDropdown.tsx | 10 ++++++++++ packages/web/src/alpha/select/DefaultSelectOption.tsx | 10 ---------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/web/src/alpha/select/DefaultSelectDropdown.tsx b/packages/web/src/alpha/select/DefaultSelectDropdown.tsx index 86186bc97..413cb7d03 100644 --- a/packages/web/src/alpha/select/DefaultSelectDropdown.tsx +++ b/packages/web/src/alpha/select/DefaultSelectDropdown.tsx @@ -251,6 +251,15 @@ const DefaultSelectDropdownComponent = memo( ); const handleEscPress = useCallback(() => setOpen(false), [setOpen]); + const handleKeyDown = useCallback( + (event: React.KeyboardEvent) => { + if (event.key === 'Tab') { + event.preventDefault(); + setOpen(false); + } + }, + [setOpen], + ); useEffect(() => { if (!controlRef.current) return; @@ -274,6 +283,7 @@ const DefaultSelectDropdownComponent = memo( aria-multiselectable={isMultiSelect} className={cx(classNames?.root, className)} display="block" + onKeyDown={handleKeyDown} role={accessibilityRoles?.dropdown} style={dropdownStyles} {...props} diff --git a/packages/web/src/alpha/select/DefaultSelectOption.tsx b/packages/web/src/alpha/select/DefaultSelectOption.tsx index 9e6209d27..69ccd05db 100644 --- a/packages/web/src/alpha/select/DefaultSelectOption.tsx +++ b/packages/web/src/alpha/select/DefaultSelectOption.tsx @@ -134,15 +134,6 @@ const DefaultSelectOptionComponent = memo( ); const handleClick = useCallback(() => onClick?.(value), [onClick, value]); - const handleKeyDown = useCallback( - (event: React.KeyboardEvent) => { - if (event.key === 'Tab') { - event.preventDefault(); - handleClick(); - } - }, - [handleClick], - ); // Since Cell's ref prop is type HTMLDivElement, we need to wrap it in a Pressable to get ref forwarding. // On web, the option role doesn't work well with ara-checked and screen readers @@ -156,7 +147,6 @@ const DefaultSelectOptionComponent = memo( className={cx(selectOptionCss, className)} disabled={disabled} onClick={handleClick} - onKeyDown={handleKeyDown} role={accessibilityRole} {...props} > From c560f071b00a43e3b74266cd72441a7654b5fe30 Mon Sep 17 00:00:00 2001 From: Maximo Macchi <232606069+maximo-macchi-cb@users.noreply.github.com> Date: Thu, 18 Dec 2025 14:10:58 -0500 Subject: [PATCH 12/14] Fix combobox lint error --- packages/mobile/src/alpha/combobox/Combobox.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/mobile/src/alpha/combobox/Combobox.tsx b/packages/mobile/src/alpha/combobox/Combobox.tsx index b4449cef0..744ff6d85 100644 --- a/packages/mobile/src/alpha/combobox/Combobox.tsx +++ b/packages/mobile/src/alpha/combobox/Combobox.tsx @@ -315,6 +315,7 @@ const ComboboxBase = memo( [ ComboboxControl, SelectDropdownComponent, + accessibilityLabel, closeButtonLabel, endNode, handleTrayVisibilityChange, From e301d86d70e3c6d1b7ac6d19243059b21d31e1f8 Mon Sep 17 00:00:00 2001 From: Maximo Macchi <232606069+maximo-macchi-cb@users.noreply.github.com> Date: Thu, 18 Dec 2025 14:54:18 -0500 Subject: [PATCH 13/14] Add fallback a11y labels to Combobox --- packages/mobile/src/alpha/combobox/Combobox.tsx | 9 ++++++--- packages/web/src/alpha/combobox/Combobox.tsx | 16 ++++++++++++---- .../combobox/__stories__/Combobox.stories.tsx | 4 ++-- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/packages/mobile/src/alpha/combobox/Combobox.tsx b/packages/mobile/src/alpha/combobox/Combobox.tsx index 744ff6d85..f55f5c042 100644 --- a/packages/mobile/src/alpha/combobox/Combobox.tsx +++ b/packages/mobile/src/alpha/combobox/Combobox.tsx @@ -179,7 +179,7 @@ const ComboboxBase = memo( variant, startNode, endNode, - accessibilityLabel = 'Combobox control', + accessibilityLabel, defaultOpen, searchText: searchTextProp, onSearch: onSearchProp, @@ -211,6 +211,8 @@ const ComboboxBase = memo( 'Combobox component must be fully controlled or uncontrolled: "open" and "setOpen" props must be provided together or not at all', ); + const fallbackAccessibilityLabel = typeof label === 'string' ? label : 'Combobox control'; + const fuse = useMemo( () => new Fuse(options, { @@ -298,7 +300,7 @@ const ComboboxBase = memo( header={ new Fuse(options, { @@ -251,9 +256,12 @@ const ComboboxBase = memo(