-
Notifications
You must be signed in to change notification settings - Fork 646
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
Changes from all commits
91203b8
d425776
6523985
30bbae4
15dc5b2
46fd1d5
a980f76
9961985
945c58c
5b03f71
4072099
109f05e
49303b0
cbebf14
b3a3096
2b666bd
df5444b
ba2166a
3437e11
d6002e3
769715b
c4bc882
bbec34b
e22679d
dbece82
2f58284
2ad9f73
0065cfa
d41f319
515a9fc
f99388b
b165dcf
96ac6d8
311c27f
1325ca4
c0c1745
8303416
9106046
847053d
e949bc6
79d0ccc
e1dcac4
d1f2945
13473b2
b9b4415
9f9e93c
f10aabd
a4e3e76
6456565
ca3a56d
ab4a0ce
96fd464
b62f693
b3e6da4
cacc441
fb11143
File filter
Filter by extension
Conversations
Jump to
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)
Contributor
Author
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
Contributor
Author
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?
Contributor
Author
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!