Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 677
TreeView: Add current prop#2348
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
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
91203b8d425776652398530bbae415dc5b246fd1d5a980f769961985945c58c5b03f714072099109f05e49303b0cbebf14b3a30962b666bddf5444bba2166a3437e11d6002e3769715bc4bc882bbec34be22679ddbece822f582842ad9f730065cfad41f319515a9fcf99388bb165dcf96ac6d8311c27f1325ca4c0c174583034169106046847053de949bc679d0ccce1dcac4d1f294513473b2b9b44159f9e93cf10aabda4e3e766456565ca3a56dab4a0ce96fd464b62f693b3e6da4cacc441fb11143File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@primer/react": patch | ||
| --- | ||
| Add a `current` prop to `TreeView.Item` and `TreeView.LinkItem` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -36,19 +36,29 @@ export type TreeViewProps = { | ||
| const UlBox = styled.ul<SxProp>(sx) | ||
| const Root: React.FC<TreeViewProps> = ({'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, children}) => { | ||
| const rootRef = React.useRef<HTMLUListElement>(null) | ||
| const [activeDescendant, setActiveDescendant] = React.useState('') | ||
| React.useEffect(() => { | ||
| // Initialize the active descendant to the first item in the tree | ||
| if (!activeDescendant) { | ||
| const firstItem = document.querySelector('[role="treeitem"]') | ||
| if (firstItem) setActiveDescendant(firstItem.id) | ||
| if (rootRef.current && !activeDescendant) { | ||
| const currentItem = rootRef.current.querySelector('[role="treeitem"][aria-current="true"]') | ||
| const firstItem = rootRef.current.querySelector('[role="treeitem"]') | ||
| // If current item exists, use it as the initial value for active descendant | ||
| if (currentItem) { | ||
| setActiveDescendant(currentItem.id) | ||
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it weird that It seemed strange to me at first, but then I thought about it and was ok with it. (non-blocking) ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I think of ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I update the code comments to make that more clear 👍 | ||
| } | ||
| // Otherwise, initialize the active descendant to the first item in the tree | ||
| else if (firstItem) { | ||
| setActiveDescendant(firstItem.id) | ||
| } | ||
| } | ||
| }, [activeDescendant]) | ||
| }, [rootRef, activeDescendant]) | ||
| return ( | ||
| <RootContext.Provider value={{activeDescendant, setActiveDescendant}}> | ||
| <UlBox | ||
| ref={rootRef} | ||
| tabIndex={0} | ||
| role="tree" | ||
| aria-label={ariaLabel} | ||
| @@ -226,12 +236,19 @@ function getLastElement(element: HTMLElement): HTMLElement | undefined { | ||
| export type TreeViewItemProps = { | ||
| children: React.ReactNode | ||
| current?: boolean | ||
| defaultExpanded?: boolean | ||
| onSelect?: (event: React.MouseEvent<HTMLElement> | KeyboardEvent) => void | ||
| onToggle?: (isExpanded: boolean) => void | ||
| } | ||
| const Item: React.FC<TreeViewItemProps> = ({defaultExpanded = false, onSelect, onToggle, children}) => { | ||
| const Item: React.FC<TreeViewItemProps> = ({ | ||
| current: isCurrent = false, | ||
| defaultExpanded = false, | ||
| onSelect, | ||
| onToggle, | ||
| children | ||
| }) => { | ||
| const {setActiveDescendant} = React.useContext(RootContext) | ||
| const itemId = useSSRSafeId() | ||
| const labelId = useSSRSafeId() | ||
| @@ -250,6 +267,13 @@ const Item: React.FC<TreeViewItemProps> = ({defaultExpanded = false, onSelect, o | ||
| [isExpanded, onToggle] | ||
| ) | ||
| // Expand item if it is the current item or contains the current item | ||
| React.useLayoutEffect(() => { | ||
| if (isCurrent || itemRef.current?.querySelector('[aria-current=true]')) { | ||
| setIsExpanded(true) | ||
| } | ||
| }, [itemRef, isCurrent, subTree]) | ||
| React.useEffect(() => { | ||
| const element = itemRef.current | ||
| @@ -271,11 +295,11 @@ const Item: React.FC<TreeViewItemProps> = ({defaultExpanded = false, onSelect, o | ||
| break | ||
| case 'ArrowRight': | ||
| if (!isExpanded) setIsExpanded(true) | ||
| if (!isExpanded) toggle() | ||
| break | ||
| case 'ArrowLeft': | ||
| if (isExpanded) setIsExpanded(false) | ||
| if (isExpanded) toggle() | ||
| break | ||
| } | ||
| } | ||
| @@ -293,6 +317,7 @@ const Item: React.FC<TreeViewItemProps> = ({defaultExpanded = false, onSelect, o | ||
| aria-labelledby={labelId} | ||
| aria-level={level} | ||
| aria-expanded={hasSubTree ? isExpanded : undefined} | ||
| aria-current={isCurrent ? 'true' : undefined} | ||
| > | ||
| <Box | ||
| onClick={event => { | ||
| @@ -304,6 +329,7 @@ const Item: React.FC<TreeViewItemProps> = ({defaultExpanded = false, onSelect, o | ||
| } | ||
| }} | ||
| sx={{ | ||
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The value of (Non-blocking for this PR) | ||
| position: 'relative', | ||
| display: 'grid', | ||
| gridTemplateColumns: `calc(${level - 1} * 8px) 16px 1fr`, | ||
| gridTemplateAreas: `"spacer toggle content"`, | ||
| @@ -319,6 +345,19 @@ const Item: React.FC<TreeViewItemProps> = ({defaultExpanded = false, onSelect, o | ||
| }, | ||
| [`[role=tree][aria-activedescendant="${itemId}"]:focus-visible &`]: { | ||
| boxShadow: (theme: Theme) => `0 0 0 2px ${theme.colors.accent.emphasis}` | ||
| }, | ||
| '[role=treeitem][aria-current=true] > &': { | ||
| bg: 'actionListItem.default.selectedBg', | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This primitive looks tidied to the ActionList, I haven't seen many primitives like this so far in Primer. Is it temporary or any other reasoning behind using a component scoped primitive name? ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TreeView builds on ActionList styles so we're reusing some ActionList tokens/primitives here | ||
| '&::after': { | ||
| position: 'absolute', | ||
| top: 'calc(50% - 12px)', | ||
| left: -2, | ||
| width: '4px', | ||
| height: '24px', | ||
| content: '""', | ||
| bg: 'accent.fg', | ||
| borderRadius: 2 | ||
| } | ||
| } | ||
| }} | ||
| > | ||
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 this is cool!