diff --git a/.changeset/early-books-tap.md b/.changeset/early-books-tap.md new file mode 100644 index 000000000..428f55e3d --- /dev/null +++ b/.changeset/early-books-tap.md @@ -0,0 +1,5 @@ +--- +'@radix-ui/react-select': patch +--- + +resolved incorrect accessibility from Select diff --git a/packages/react/select/src/select.tsx b/packages/react/select/src/select.tsx index 042a0f333..cf1639119 100644 --- a/packages/react/select/src/select.tsx +++ b/packages/react/select/src/select.tsx @@ -36,7 +36,7 @@ const SELECTION_KEYS = [' ', 'Enter']; const SELECT_NAME = 'Select'; -type ItemData = { value: string; disabled: boolean; textValue: string }; +type ItemData = { value: string; disabled: boolean; textValue: string; id: string }; const [Collection, useCollection, createCollectionScope] = createCollection< SelectItemElement, ItemData @@ -497,6 +497,8 @@ type SelectContentContextValue = { position?: SelectContentProps['position']; isPositioned?: boolean; searchRef?: React.RefObject; + focusedItem?: SelectItemElement | null; + onItemFocus?: (node: SelectItemElement | null) => void; }; const [SelectContentProvider, useSelectContentContext] = @@ -565,6 +567,7 @@ const SelectContentImpl = React.forwardRef( null ); + const [focusedItem, setFocusedItem] = React.useState(null); const getItems = useCollection(__scopeSelect); const [isPositioned, setIsPositioned] = React.useState(false); const firstValidItemFoundRef = React.useRef(false); @@ -592,10 +595,14 @@ const SelectContentImpl = React.forwardRef (nextItem.ref.current as HTMLElement).focus()); + setTimeout(() => { + (nextItem.ref.current as HTMLElement).focus(); + setFocusedItem(nextItem.ref.current as SelectItemElement); + }); } }); @@ -684,7 +694,10 @@ const SelectContentImpl = React.forwardRef content?.focus(), [content]); + const handleItemLeave = React.useCallback(() => { + content?.focus(); + setFocusedItem(null); + }, [content, setFocusedItem]); const itemTextRefCallback = React.useCallback( (node: SelectItemTextElement | null, value: string, disabled: boolean) => { const isFirstValidItem = !firstValidItemFoundRef.current && !disabled; @@ -696,6 +709,10 @@ const SelectContentImpl = React.forwardRef { + setFocusedItem(node); + }, []); + const SelectPosition = position === 'popper' ? SelectPopperPosition : SelectItemAlignedPosition; // Silently ignore props that are not supported by `SelectItemAlignedPosition` @@ -730,6 +747,8 @@ const SelectContentImpl = React.forwardRef event.preventDefault()} @@ -1261,10 +1281,13 @@ const SelectItem = React.forwardRef( const isSelected = context.value === value; const [textValue, setTextValue] = React.useState(textValueProp ?? ''); const [isFocused, setIsFocused] = React.useState(false); - const composedRefs = useComposedRefs(forwardedRef, (node) => - contentContext.itemRefCallback?.(node, value, disabled) - ); + const itemRef = React.useRef(null); + const composedRefs = useComposedRefs(forwardedRef, (node) => { + itemRef.current = node; + contentContext.itemRefCallback?.(node, value, disabled); + }); const textId = useId(); + const optionId = useId(); const pointerTypeRef = React.useRef('touch'); const handleSelect = () => { @@ -1274,6 +1297,13 @@ const SelectItem = React.forwardRef( } }; + React.useEffect(() => { + if (isFocused && itemRef.current) { + contentContext.onItemFocus?.(itemRef.current); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isFocused, contentContext.onItemFocus]); + if (value === '') { throw new Error( 'A must have a value prop that is not an empty string. This is because the Select value can be set to an empty string to clear the selection and show the placeholder.' @@ -1296,13 +1326,14 @@ const SelectItem = React.forwardRef( value={value} disabled={disabled} textValue={textValue} + id={optionId} >