@react-aria/listbox
Version:
Spectrum UI components in React
180 lines (154 loc) • 5.86 kB
text/typescript
/*
* Copyright 2020 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
import {chain, filterDOMProps, isMac, isWebKit, mergeProps, useLinkProps, useSlotId} from '@react-aria/utils';
import {DOMAttributes, FocusableElement, Key, RefObject} from '@react-types/shared';
import {getItemCount} from '@react-stately/collections';
import {getItemId, listData} from './utils';
import {isFocusVisible, useHover} from '@react-aria/interactions';
import {ListState} from '@react-stately/list';
import {SelectableItemStates, useSelectableItem} from '@react-aria/selection';
export interface OptionAria extends SelectableItemStates {
/** Props for the option element. */
optionProps: DOMAttributes,
/** Props for the main text element inside the option. */
labelProps: DOMAttributes,
/** Props for the description text element inside the option, if any. */
descriptionProps: DOMAttributes,
/** Whether the option is currently focused. */
isFocused: boolean,
/** Whether the option is keyboard focused. */
isFocusVisible: boolean
}
export interface AriaOptionProps {
/**
* Whether the option is disabled.
* @deprecated
*/
isDisabled?: boolean,
/**
* Whether the option is selected.
* @deprecated
*/
isSelected?: boolean,
/** A screen reader only label for the option. */
'aria-label'?: string,
/** The unique key for the option. */
key: Key,
/**
* Whether selection should occur on press up instead of press down.
* @deprecated
*/
shouldSelectOnPressUp?: boolean,
/**
* Whether the option should be focused when the user hovers over it.
* @deprecated
*/
shouldFocusOnHover?: boolean,
/**
* Whether the option is contained in a virtual scrolling listbox.
* @deprecated
*/
isVirtualized?: boolean,
/**
* Whether the option should use virtual focus instead of being focused directly.
* @deprecated
*/
shouldUseVirtualFocus?: boolean
}
/**
* Provides the behavior and accessibility implementation for an option in a listbox.
* See `useListBox` for more details about listboxes.
* @param props - Props for the option.
* @param state - State for the listbox, as returned by `useListState`.
*/
export function useOption<T>(props: AriaOptionProps, state: ListState<T>, ref: RefObject<FocusableElement | null>): OptionAria {
let {
key
} = props;
let data = listData.get(state);
let isDisabled = props.isDisabled ?? state.selectionManager.isDisabled(key);
let isSelected = props.isSelected ?? state.selectionManager.isSelected(key);
let shouldSelectOnPressUp = props.shouldSelectOnPressUp ?? data?.shouldSelectOnPressUp;
let shouldFocusOnHover = props.shouldFocusOnHover ?? data?.shouldFocusOnHover;
let shouldUseVirtualFocus = props.shouldUseVirtualFocus ?? data?.shouldUseVirtualFocus;
let isVirtualized = props.isVirtualized ?? data?.isVirtualized;
let labelId = useSlotId();
let descriptionId = useSlotId();
let optionProps = {
role: 'option',
'aria-disabled': isDisabled || undefined,
'aria-selected': state.selectionManager.selectionMode !== 'none' ? isSelected : undefined
};
// Safari with VoiceOver on macOS misreads options with aria-labelledby or aria-label as simply "text".
// We should not map slots to the label and description on Safari and instead just have VoiceOver read the textContent.
// https://bugs.webkit.org/show_bug.cgi?id=209279
if (!(isMac() && isWebKit())) {
optionProps['aria-label'] = props['aria-label'];
optionProps['aria-labelledby'] = labelId;
optionProps['aria-describedby'] = descriptionId;
}
let item = state.collection.getItem(key);
if (isVirtualized) {
let index = Number(item?.index);
optionProps['aria-posinset'] = Number.isNaN(index) ? undefined : index + 1;
optionProps['aria-setsize'] = getItemCount(state.collection);
}
let onAction = data?.onAction ? () => data?.onAction?.(key) : undefined;
let id = getItemId(state, key);
let {itemProps, isPressed, isFocused, hasAction, allowsSelection} = useSelectableItem({
selectionManager: state.selectionManager,
key,
ref,
shouldSelectOnPressUp,
allowsDifferentPressOrigin: shouldSelectOnPressUp && shouldFocusOnHover,
isVirtualized,
shouldUseVirtualFocus,
isDisabled,
onAction: onAction || item?.props?.onAction ? chain(item?.props?.onAction, onAction) : undefined,
linkBehavior: data?.linkBehavior,
// @ts-ignore
UNSTABLE_itemBehavior: data?.['UNSTABLE_itemBehavior'],
id
});
let {hoverProps} = useHover({
isDisabled: isDisabled || !shouldFocusOnHover,
onHoverStart() {
if (!isFocusVisible()) {
state.selectionManager.setFocused(true);
state.selectionManager.setFocusedKey(key);
}
}
});
let domProps = filterDOMProps(item?.props);
delete domProps.id;
let linkProps = useLinkProps(item?.props);
return {
optionProps: {
...optionProps,
...mergeProps(domProps, itemProps, hoverProps, linkProps),
id
},
labelProps: {
id: labelId
},
descriptionProps: {
id: descriptionId
},
isFocused,
isFocusVisible: isFocused && state.selectionManager.isFocused && isFocusVisible(),
isSelected,
isDisabled,
isPressed,
allowsSelection,
hasAction
};
}