Skip to content

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, joshblack and mperrotti 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.

4 participants