-
Notifications
You must be signed in to change notification settings - Fork 582
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
Add expand to NavList
#4686
base: main
Are you sure you want to change the base?
Add expand to NavList
#4686
Conversation
🦋 Changeset detectedLatest commit: 25ee7e6 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
size-limit report 📦
|
const childCount = React.Children.count(children) | ||
|
||
React.useEffect(() => { | ||
if (expanded && targetFocused.current !== currentPage) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm curious if this conditional is enough to ensure that focus won't be stolen upon re-renders, as mentioned in this comment.
When a given "show more" button is expanded, we store the "page" that was expanded in the useRef
. So if there's 2 pages, and a user expands once, the page placed in targetFocused
would be 1, and the value in currentPage
would also be 1. This means that if there's a re-render, the conditional (targetFocused.current !== currentPage
) wouldn't apply, as both targetFocused
and currentPage
values are the same.
We're essentially checking if the most recent page has been expanded, if it hasn't, then we'll focus the first element of the new "group", and store that page's value in the targetFocused
useRef.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@joshblack, you mentioned if we could potentially set focus during user interaction, I'm curious on what type of interaction that could be? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@TylerJDev is it possible to do this on the onClick
for expand or not really?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could utilize setTimeout
here. Basically, using setTimeout
with a timeout of 0 (or none at all) seems to query the elements after they are rendered in the DOM.
This is a bit hacky though. I believe we already utilize this pattern in other places such as TreeView
, and LabelGroup
, so it could work. Not ideal though.
focusTarget[pages ? nextItemToFocus : focusTarget.length - childCount].focus() | ||
targetFocused.current = currentPage |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a little hesitant with a programmatic focus()
in a useEffect
since it might not sequence with a user interaction (e.g. if some unrelated thing occurs, like child count, we would steal focus unintentionally). Would it be possible to have focus behavior occur during a user interaction?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You might've already read this, but curious on what you think about this: #4686 (comment). Would this be enough to safeguard us from focus being stolen? I'm wondering if there are gaps I might not be seeing.
I can play around with the implementation more! The only thing I'm running into is getting the element after it is placed in the DOM, and not before 🤔 I tried other methods like flushSync
, but am a bit hesitant on it since we don't use it elsewhere, and it didn't work exactly as I expected 😅
children: React.ReactNode | ||
label?: string | ||
pages?: number | ||
} & SxProp |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Feel free to just remove any sx
stuff if you don't want to have to deal with toggling behind the flag!
} & SxProp | |
} |
<> | ||
{expanded && ( | ||
<ItemWithinGroup.Provider value={groupId}> | ||
{React.Children.toArray(children).filter((child, index, arr) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would reading from children in this way work if a fragment is used or if someone has a wrapper component that then renders multiple items? e.g.
<NavList.ShowMoreItem label="Show more">
<CustomNavItems /> {/* This might return three items */}
<NavList.Item>...</NavList.Item>
<NavList.Item>...</NavList.Item>
<NavList.Item>...</NavList.Item>
</NavList.ShowMoreItem>
Or would this just be limited to direct descendant NavList.Item's?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe it should still render! I went ahead and tested this, let me know if this isn't what you're envisioning!
const CustomNavItems = () => {
return (
<>
<NavList.Item href="#">Item 2D</NavList.Item>
<NavList.Item href="#">Item 2E</NavList.Item>
<NavList.Item href="#">Item 2F</NavList.Item>
</>
)
}
const NavListGroup = () => {
return (
<NavList>
<NavList.Group title="Group 2">
<NavList.Item href="#">Item 2A</NavList.Item>
<NavList.Item href="#">Item 2B</NavList.Item>
<NavList.Item href="#">Item 2C</NavList.Item>
<NavList.ShowMoreItem pages={2} label="Show">
<CustomNavItems />
<NavList.Item>Item 2G</NavList.Item>
<NavList.Item>Item 2H</NavList.Item>
<NavList.Item>Item 2I</NavList.Item>
</NavList.ShowMoreItem>
</NavList.Group>
</NavList>
)
}
It seems to have correctly rendered all the children as you'd expect. The only issue I see now is that focus is set to "Item 2D", instead of "Item 2G" when expanded. Will take a look on why this is 👀
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I misunderstood! I see the issue now. We calculate how many items to show at a given time based on the amount of children. If there are 100 children in a fragment then it will consider the fragment as one child, rather than 100.
I restricted the children to only being NavList.Item
. I didn't really want to restrict children based on type, but I don't envision many cases where we'd want anything but NavList.Item
. This also solves the fragment issue. We utilize a very similar pattern for SubNav
in this component, so this isn't too different.
Curious on your thoughts here 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@TylerJDev sadly this is a pattern that we're trying to move away from since we're seeing folks downstream wrap the component in some way (either with the fragment case or just a wrapper around a specific component)
It's definitely a pattern that exists in PRC today though, like you said, but the more we add the harder it will be to dig ourselves out of this hole 😵
Adds new component
NavList.ShowMoreItem
, allows native support for "expanding" content within aNavList
.Closes https://github.com/github/primer/issues/2637
Proposed API
Basic example:
Multiple expands:
Alternative API:
Group example (storybook)
Changelog
New
NavList.ShowMoreItem
Rollout strategy
Testing & Reviewing
Merge checklist