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

Add expand to NavList #4686

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

Add expand to NavList #4686

wants to merge 34 commits into from

Conversation

TylerJDev
Copy link
Contributor

@TylerJDev TylerJDev commented Jun 19, 2024

Adds new component NavList.ShowMoreItem, allows native support for "expanding" content within a NavList.

Closes https://github.com/github/primer/issues/2637

Proposed API

Basic example:

<NavList>
  <NavList.Item href="#" aria-current="page">Item 1</NavList.Item>
  <NavList.Item href="#">Item 2</NavList.Item>
  <NavList.ShowMoreItem label="Show more">
    <NavList.Item href="#">Item 3</NavList.Item>
    <NavList.Item href="#">Item 4</NavList.Item>
  </NavList.ShowMoreItem>
</NavList>

Multiple expands:

<NavList>
  <NavList.Item href="#" aria-current="page">Item 1</NavList.Item>
  <NavList.Item href="#">Item 2</NavList.Item>
  <NavList.ShowMoreItem label="Show more">
    <NavList.Item href="#">Item 3</NavList.Item>
    <NavList.Item href="#">Item 4</NavList.Item>
    <NavList.ShowMoreItem label="Show more">
      <NavList.Item href="#">Item 5</NavList.Item>
      <NavList.Item href="#">Item 6</NavList.Item>
     </NavList.ShowMoreItem>
  </NavList.ShowMoreItem>
</NavList>

Group example (storybook)

Changelog

New

  • Adds new component NavList.ShowMoreItem

Rollout strategy

  • Patch release
  • Minor release
  • Major release; if selected, include a written rollout or migration plan
  • None; if selected, include a brief description as to why

Testing & Reviewing

Merge checklist

Copy link

changeset-bot bot commented Jun 19, 2024

🦋 Changeset detected

Latest commit: 7ad35ed

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@primer/react Minor

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

Copy link
Contributor

github-actions bot commented Jun 19, 2024

size-limit report 📦

Path Size
packages/react/dist/browser.esm.js 104.83 KB (+0.27% 🔺)
packages/react/dist/browser.umd.js 105.19 KB (+0.3% 🔺)

@github-actions github-actions bot temporarily deployed to storybook-preview-4686 June 19, 2024 19:32 Inactive
@github-actions github-actions bot temporarily deployed to storybook-preview-4686 June 19, 2024 19:49 Inactive
@github-actions github-actions bot temporarily deployed to storybook-preview-4686 June 19, 2024 20:15 Inactive
@github-actions github-actions bot temporarily deployed to storybook-preview-4686 June 20, 2024 15:42 Inactive
@github-actions github-actions bot temporarily deployed to storybook-preview-4686 June 21, 2024 18:24 Inactive
@github-actions github-actions bot temporarily deployed to storybook-preview-4686 June 21, 2024 19:26 Inactive
@github-actions github-actions bot temporarily deployed to storybook-preview-4686 June 25, 2024 12:33 Inactive
@github-actions github-actions bot temporarily deployed to storybook-preview-4686 June 25, 2024 13:10 Inactive
@github-actions github-actions bot added the integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm label Jan 22, 2025
Copy link
Contributor

👋 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!

@github-actions github-actions bot temporarily deployed to storybook-preview-4686 January 22, 2025 14:44 Inactive
@github-actions github-actions bot temporarily deployed to storybook-preview-4686 January 28, 2025 18:50 Inactive
@github-actions github-actions bot temporarily deployed to storybook-preview-4686 January 28, 2025 19:00 Inactive
@github-actions github-actions bot temporarily deployed to storybook-preview-4686 January 28, 2025 19:16 Inactive
@github-actions github-actions bot temporarily deployed to storybook-preview-4686 January 28, 2025 19:29 Inactive
@github-actions github-actions bot temporarily deployed to storybook-preview-4686 January 28, 2025 20:07 Inactive
@TylerJDev TylerJDev requested a review from joshblack January 28, 2025 20:21
const childCount = React.Children.count(children)

React.useEffect(() => {
if (expanded && targetFocused.current !== currentPage) {
Copy link
Contributor Author

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 && (
Copy link
Member

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)

Suggested change
{expanded && (
{expanded ? (...) : null}

Copy link
Contributor Author

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 🙏

Comment on lines +447 to +448
focusTarget[pages ? nextItemToFocus : focusTarget.length - childCount].focus()
targetFocused.current = currentPage
Copy link
Member

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?

Copy link
Contributor Author

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
Copy link
Member

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!

Suggested change
} & SxProp
}

<>
{expanded && (
<ItemWithinGroup.Provider value={groupId}>
{React.Children.toArray(children).filter((child, index, arr) => {
Copy link
Member

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?

Copy link
Contributor Author

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 👀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants