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

feat(listitembase): dynamic list support #600

Open
wants to merge 34 commits into
base: master
Choose a base branch
from

Conversation

Coread
Copy link
Collaborator

@Coread Coread commented Aug 30, 2024

Description

This PR contains a number of improvements to List/ListItemBase:

  1. Improved handling of adding and removing items from the list
  2. Exclusion of single tabbable elements from the focus handling logic in ListItemBase
  3. onFocus/blur and onFocusWithin/blurWithin

Links

Links to relevent resources.

@@ -271,6 +271,24 @@ const ListItemBase = (props: Props, providedRef: RefOrCallbackRef) => {
};
}, []);

useEffect(() => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

issue: This breaks looping lists if one of the ends is considered empty - maybe not a huge issue, but it doesn't behave as expected.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

this is now fixed

}
}
}
}, [focus, lastCurrentFocus, listContext.listSize]);
Copy link
Collaborator

Choose a reason for hiding this comment

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

suggestion: I think this should include indexItem and probably listContext.setCurrentFocus (the latter is admittedly the result of a useState so not hugely important, but this module doesn't really know what it is based on the context).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I've added the full list of dependencies to the useEffect

Comment on lines 276 to 287
if (lastCurrentFocus > itemIndex) {
if (itemIndex <= 0) {
listContext.setCurrentFocus(itemIndex + 1);
} else {
listContext.setCurrentFocus(itemIndex - 1);
}
} else {
if (itemIndex >= listContext.listSize - 1) {
listContext.setCurrentFocus(itemIndex - 1);
} else {
listContext.setCurrentFocus(itemIndex + 1);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

suggestion: Some comments to explain what is happening would be useful - with some analysis you can see that you are actively keeping items at the (direction dependent) previous item if this item is at one of the ends, and otherwise skipping it, but it's not necessarily simple at a glance (the comment would have also made it clear that looping wouldn't work any more).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

this is now fixed

const ListWithNullElementsWrapper = () => {
return (
<>
<List noLoop listSize={7}>
Copy link
Collaborator

Choose a reason for hiding this comment

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

issue: This is a list of length 9 (looping will happen to work going down because this number is wrong!).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

fixed

Comment on lines 18 to 24
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
width: 1px;
white-space: nowrap;
Copy link
Collaborator

Choose a reason for hiding this comment

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

nitpick: This is another instance of the "visually hidden" styling which is in various parts of the library, would be nice to mixin / include this in scss terms.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

as far as I know, we don't currently have shared scss between the new components?

@Coread Coread marked this pull request as ready for review September 3, 2024 08:16
Comment on lines 105 to 122
/**
* event fired when any of the children or the element itself are focused
*/
onFocusWithin?: (e: React.FocusEvent<Element>) => void;

/**
* event fired when any of the children or the element itself lose focus
*/
onBlurWithin?: (e: React.FocusEvent<Element>) => void;
/**
* event fired when any of the children or the element itself are focused
*/
onFocus?: (e: React.FocusEvent<Element>) => void;

/**
* event fired when any of the children or the element itself lose focus
*/
onBlur?: (e: React.FocusEvent<Element>) => void;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Props should extends FocusProps and FocusWithinProps instead of manually adding the props. JSDocs should be moved to the original definition.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'm now extending this definition using the props from react-aria. Note that it isn't as simple as extending directly, because I haven't added support for all of FocusProps or all of FocusWithinProps

return (
<>
<List shouldFocusOnPress listSize={2}>
<ListItemBase data-exclude-focus-children size="auto" itemIndex={0} key={0}>
Copy link
Collaborator

Choose a reason for hiding this comment

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

What is the expected behaviour for data-exclude-focus and data-exclude-focus-children? I can see they might be excluded from intractable element selection, but it is not prevent the browser focus the element with Tab.

I check this story, and I can focus on button 1 with Tab.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I've removed the exclude-focus-children as this was covered by your recent changes. data-exclude-focus is used by the Aria-Toolbar. The toolbar only wants to expose one of its tabable buttons to preserve focus order when you tab forward/backward from within a ListItemBase. It sets data-exclude-focus on all but the first one so the first element is the only one to ever be in the tab order.

Comment on lines 43 to 45
if (props.onFocusWithin) {
props.onFocusWithin.apply(this, args);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

nitpicking: this can be simplified to:

Suggested change
if (props.onFocusWithin) {
props.onFocusWithin.apply(this, args);
}
props.onFocusWithin?.(...args);

because the arrow functions this is undefined anyway

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I've changed this

@Coread Coread changed the title feat(listitembase): allow empty items feat(listitembase): dynamic list support Sep 22, 2024
@martroos martroos dismissed their stale review September 25, 2024 10:20

Stale review

.eslintrc.js Outdated Show resolved Hide resolved
Copy link
Collaborator

@jor-row jor-row left a comment

Choose a reason for hiding this comment

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

Tested locally and changes look good to me. A few small comments

Comment on lines +382 to +398
const ListWithNonFocusableChildrenWrapper = () => {
return (
<>
<List shouldFocusOnPress listSize={2}>
<ListItemBase data-preserve-tabindex size="auto" itemIndex={0} key={0}>
<ButtonPill>1</ButtonPill>
</ListItemBase>
<ListItemBase itemIndex={1} key={1}>
<ButtonPill>2</ButtonPill>
</ListItemBase>
</List>
</>
);
};
const ListWithNonFocusableChildren = Template<unknown>(ListWithNonFocusableChildrenWrapper).bind(
{}
);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This story doesn't seem to be working as expected

}
};

useEffect(() => {
// When the toolbar is rendered inside a list, only item in the toolbar
Copy link
Collaborator

Choose a reason for hiding this comment

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

"only one item" or "only the first item"

getContext: () => {
listSize: number;
currentFocus: number;
setCurrentFocus: Dispatch<SetStateAction<number>>;
shouldFocusOnPress?: boolean;
shouldItemFocusBeInset?: boolean;
noLoop?: boolean;
updateFocusBlocked?: boolean;
supressFocus?: boolean;
Copy link
Collaborator

Choose a reason for hiding this comment

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

supressFocus is never used so can be removed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants