Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

strict type for aria combobox #6167

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
20 changes: 10 additions & 10 deletions packages/@react-aria/combobox/src/useComboBox.ts
Expand Up @@ -86,7 +86,7 @@ export function useComboBox<T>(props: AriaComboBoxOptions<T>, state: ComboBoxSta
);

// Set listbox id so it can be used when calling getItemId later
listData.set(state, {id: menuProps.id});
listData.set(state, {id: menuProps.id!});

// By default, a KeyboardDelegate is provided which uses the DOM to query layout information (e.g. for page up/page down).
// When virtualized, the layout object will be passed in as a prop and override this.
Expand Down Expand Up @@ -122,9 +122,9 @@ export function useComboBox<T>(props: AriaComboBoxOptions<T>, state: ComboBoxSta
// If the focused item is a link, trigger opening it. Items that are links are not selectable.
if (state.isOpen && state.selectionManager.focusedKey != null && state.selectionManager.isLink(state.selectionManager.focusedKey)) {
if (e.key === 'Enter') {
let item = listBoxRef.current.querySelector(`[data-key="${CSS.escape(state.selectionManager.focusedKey.toString())}"]`);
if (item instanceof HTMLAnchorElement) {
let collectionItem = state.collection.getItem(state.selectionManager.focusedKey);
let item = listBoxRef.current?.querySelector(`[data-key="${CSS.escape(state.selectionManager.focusedKey.toString())}"]`);
const collectionItem = state.collection.getItem(state.selectionManager.focusedKey);
if (item instanceof HTMLAnchorElement && collectionItem) {
router.open(item, e, collectionItem.props.href, collectionItem.props.routerOptions as RouterOptions);
}
}
Expand Down Expand Up @@ -201,14 +201,14 @@ export function useComboBox<T>(props: AriaComboBoxOptions<T>, state: ComboBoxSta
let onPress = (e: PressEvent) => {
if (e.pointerType === 'touch') {
// Focus the input field in case it isn't focused yet
inputRef.current.focus();
inputRef.current?.focus();
state.toggle(null, 'manual');
}
};

let onPressStart = (e: PressEvent) => {
if (e.pointerType !== 'touch') {
inputRef.current.focus();
inputRef.current?.focus();
state.toggle((e.pointerType === 'keyboard' || e.pointerType === 'virtual') ? 'first' : null, 'manual');
}
};
Expand All @@ -235,7 +235,7 @@ export function useComboBox<T>(props: AriaComboBoxOptions<T>, state: ComboBoxSta
// Sometimes VoiceOver on iOS fires two touchend events in quick succession. Ignore the second one.
if (e.timeStamp - lastEventTime.current < 500) {
e.preventDefault();
inputRef.current.focus();
inputRef.current?.focus();
return;
}

Expand All @@ -247,7 +247,7 @@ export function useComboBox<T>(props: AriaComboBoxOptions<T>, state: ComboBoxSta

if (touch.clientX === centerX && touch.clientY === centerY) {
e.preventDefault();
inputRef.current.focus();
inputRef.current?.focus();
state.toggle(null, 'manual');

lastEventTime.current = e.timeStamp;
Expand All @@ -271,7 +271,7 @@ export function useComboBox<T>(props: AriaComboBoxOptions<T>, state: ComboBoxSta
let sectionTitle = section?.['aria-label'] || (typeof section?.rendered === 'string' ? section.rendered : '') || '';

let announcement = stringFormatter.format('focusAnnouncement', {
isGroupChange: section && sectionKey !== lastSection.current,
isGroupChange: Boolean(section && sectionKey !== lastSection.current),
groupTitle: sectionTitle,
groupCount: section ? [...getChildNodes(section, state.collection)].length : 0,
optionText: focusedItem['aria-label'] || focusedItem.textValue || '',
Expand Down Expand Up @@ -320,7 +320,7 @@ export function useComboBox<T>(props: AriaComboBoxOptions<T>, state: ComboBoxSta

useEffect(() => {
if (state.isOpen) {
return ariaHideOutside([inputRef.current, popoverRef.current]);
return ariaHideOutside([inputRef.current, popoverRef.current] as Element[]);
ishank-s marked this conversation as resolved.
Show resolved Hide resolved
}
}, [state.isOpen, inputRef, popoverRef]);

Expand Down
4 changes: 2 additions & 2 deletions packages/@react-aria/combobox/stories/example.tsx
Expand Up @@ -86,7 +86,7 @@ export function ComboBox(props) {
}

function Popover(props) {
let ref = React.useRef();
let ref = React.useRef(null);
let {
popoverRef = ref,
isOpen,
Expand Down Expand Up @@ -152,7 +152,7 @@ function ListBox(props) {
}

function Option({item, state}) {
let ref = React.useRef();
let ref = React.useRef<HTMLLIElement>(null);
let {optionProps, isSelected, isFocused, isDisabled} = useOption({key: item.key}, state, ref);

let backgroundColor;
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-aria/menu/src/useMenuTrigger.ts
Expand Up @@ -45,7 +45,7 @@ export interface MenuTriggerAria<T> {
* @param state - State for the menu trigger.
* @param ref - Ref to the HTML element trigger for the menu.
*/
export function useMenuTrigger<T>(props: AriaMenuTriggerProps, state: MenuTriggerState, ref: RefObject<Element>): MenuTriggerAria<T> {
export function useMenuTrigger<T>(props: AriaMenuTriggerProps, state: MenuTriggerState, ref?: RefObject<Element>): MenuTriggerAria<T> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think useMenuTrigger should have its ref as optional, it would interfere with longpress handling and arrow keys.

@devongovett how would you feel if combobox used useOverlayTrigger directly? it doesn't need longpress behavior

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At a glance moving to using useOverlayTrigger seems feasible, pretty similar to what we do for MobileCombobox. Like you said, we don't need the long press stuff so we'd only need to copy over the id/pressProps stuff from useMenuTrigger into useComboBox.

Copy link
Member Author

@ishank-s ishank-s Apr 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hi do you want me to contribute useOverlayTrigger change.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely feel free to give it a shot if you'd like! It would be good to know if something I'm overlooking would breaks with that change, just haven't had time to try it myself

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh okay so there is these 2 props -

  • aria-labelledby which would break with using only overlay trigger and doesnt make sense to have arial labeled by in overlay trigger
  • onKeyDown - this im assuming is invoked whenever you press any key and it shows the combobox with relevant list item
    Both of these props dont fit in useOverlayTrigger.

We can do either of these -

  • make non null assertion for the useMenuTrigger invocation in useCombobox
  • Keep the ref optional.

I don't think useMenuTrigger should have its ref as optional, it would interfere with longpress handling and arrow keys.

On this point by @snowystinger would that not be true for arrow keys for combobox? Ignoring the longpress handling which doesnt make sense for combobox

  • create a sepeate trigger hook that takes in relevant combobox hook
    solves all problem but with code duplication as the new hook would essentially by useMenuTrigger without the required prop

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apologies about the delay, mind clarifying these two:

aria-labelledby which would break with using only overlay trigger and doesnt make sense to have arial labeled by in overlay trigger

Could we set up a useId call in useComboBox and use that for the trigger id and the menu's aria-labelledby and override the ones that will be provided by useOverlayTrigger?

onKeyDown - this im assuming is invoked whenever you press any key and it shows the combobox with relevant list item

similar to above, perhaps we could copy over the menutrigger's onKeyDown over to useComboBox as well?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hi sorry, been busy with work. will try your suggestions soon thanks for guidance.

let {
type = 'menu' as AriaMenuTriggerProps['type'],
isDisabled,
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-types/textfield/src/index.d.ts
Expand Up @@ -30,7 +30,7 @@ import {
} from '@react-types/shared';
import {ReactElement} from 'react';

export interface TextFieldProps extends InputBase, Validation<string>, HelpTextProps, FocusableProps, TextInputBase, ValueBase<string>, LabelableProps {}
export interface TextFieldProps<T = HTMLInputElement> extends InputBase, Validation<string>, HelpTextProps, FocusableProps<T>, TextInputBase, ValueBase<string>, LabelableProps {}
snowystinger marked this conversation as resolved.
Show resolved Hide resolved

export interface AriaTextFieldProps extends TextFieldProps, AriaLabelingProps, FocusableDOMProps, TextInputDOMProps, AriaValidationProps {
// https://www.w3.org/TR/wai-aria-1.2/#textbox
Expand Down
1 change: 1 addition & 0 deletions tsconfig.json
Expand Up @@ -38,6 +38,7 @@
"./packages/@react-aria/a",
"./packages/@react-aria/b",
"./packages/@react-aria/checkbox",
"./packages/@react-aria/combobox",
"./packages/@react-aria/l",
"./packages/@react-aria/meter",
"./packages/@react-aria/color",
Expand Down