-
Notifications
You must be signed in to change notification settings - Fork 574
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: 7ad35ed 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 📦
|
👋 Hi, this pull request contains changes to the source code that github/github depends on. If you are GitHub staff, we recommend testing these changes with github/github using the integration workflow. Thanks! |
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.
|
||
return ( | ||
<> | ||
{expanded && ( |
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.
Small style suggestion, we could a ternary here instead of the short-circuit syntax to avoid rendering any unexpected values (reasoning from: https://eslint-react.xyz/docs/rules/no-leaked-conditional-rendering)
{expanded && ( | |
{expanded ? (...) : null} |
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.
Oh wow, TIL. Thank you for the link, will start using this pattern moving forward 🙏
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 👀
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:
Group example (storybook)
Changelog
New
NavList.ShowMoreItem
Rollout strategy
Testing & Reviewing
Merge checklist