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

TreeView: Allow expanded state to be controlled #2373

Merged
merged 23 commits into from Sep 28, 2022

Conversation

colebemis
Copy link
Contributor

@colebemis colebemis commented Sep 26, 2022

Summary

Allows the expanded state of tree items to be controlled (as requested by @jdrush89 from Repos). Example:

function ControlledTreeItem() {
  const [expanded, setExpanded] = useState(false)
  return <TreeView.Item expanded={expanded} onExpandedChange={setExpanded} ... />
}

// Uncontrolled version for comparison:
function UncontrolledTreeItem() {
    return <TreeView.Item ... />
}

I created a new story to demonstrate how you might use the controlled behavior:

👉 Try it out

CleanShot.2022-09-26.at.13.35.41.mp4

Shoutouts

Shoutout to @joshblack for writing the useControllableState hook 💖

Merge checklist

  • Added/updated tests
  • Added/updated documentation
  • Tested in Chrome
  • Tested in Firefox
  • Tested in Safari
  • Tested in Edge

Take a look at the What we look for in reviews section of the contributing guidelines for more information on how we review PRs.

@changeset-bot
Copy link

changeset-bot bot commented Sep 26, 2022

🦋 Changeset detected

Latest commit: 4750160

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 Patch

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

@github-actions
Copy link
Contributor

github-actions bot commented Sep 26, 2022

size-limit report 📦

Path Size
dist/browser.esm.js 77.1 KB (0%)
dist/browser.umd.js 77.71 KB (0%)

@colebemis colebemis temporarily deployed to github-pages September 26, 2022 20:33 Inactive
@colebemis colebemis marked this pull request as ready for review September 26, 2022 20:47
@colebemis colebemis requested review from a team, mperrotti and joshblack September 26, 2022 20:47
@colebemis colebemis temporarily deployed to github-pages September 26, 2022 20:55 Inactive
description="The controlled expanded state of item. Must be used in conjunction with onExpandedChange."
/>
<PropsTableRow
name="onExpandedChange"
Copy link
Member

Choose a reason for hiding this comment

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

Is there a good rule of thumb to follow for when to use on<State>Change(value), as in onExpandedChange(), versus onChange(state)?

Copy link
Member

@joshblack joshblack left a comment

Choose a reason for hiding this comment

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

Looks great! Left a couple comments around ideas for refactoring if they're helpful, totally get if not 👍

})

React.useLayoutEffect(() => {
setParentContainsCurrentRef.current = setParentContainsCurrent
Copy link
Member

Choose a reason for hiding this comment

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

With setContainsCurrentItem coming from React.useState(), we might be able to rely on that being stable instead of having to create a stable reference.

const setParentContainsCurrentRef = React.useRef(setParentContainsCurrent)

React.useLayoutEffect(() => {
setIsExpandedRef.current = setIsExpanded
Copy link
Member

Choose a reason for hiding this comment

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

I bet we could update useControllableState to provide a stable reference for the state setter to match useState() if that would help out here with not having to mess around with stable refs

controlled.current = value !== undefined
}

function setState(stateOrUpdater: T | ((prevState: T) => T)) {
Copy link
Member

Choose a reason for hiding this comment

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

Idea from the stable state setter comment, this might be able to become:

const savedOnChange = React.useRef(onChange);

React.useEffect(() => {
  savedOnChange.current = onChange;
});

const setState = React.useCallback((stateOrUpdater: T | ((prevState: T) => T)) => {
  const value =
    typeof stateOrUpdater === 'function'
      ? // @ts-ignore stateOrUpdater is a function
        stateOrUpdater(state)
      : stateOrUpdater

  if (controlled.current === false) {
    internalSetState(value)
  }

  if (onChange) {
    savedOnChange.current?.(value)
  }
}, [state]);

// --snip--

return [state, setState];

It'd be great to also get rid of state from the dep array but I didn't see a quick way to do so 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is great feedback. Thank you! Is this what you had in mind? eeaf486

@colebemis colebemis temporarily deployed to github-pages September 28, 2022 03:50 Inactive
@colebemis colebemis temporarily deployed to github-pages September 28, 2022 19:43 Inactive
@colebemis colebemis merged commit e4af7a7 into main Sep 28, 2022
@colebemis colebemis deleted the treeview-control-expanded branch September 28, 2022 21:45
@primer-css primer-css mentioned this pull request Sep 28, 2022
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.

None yet

3 participants