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: Align tree item toggle and visual icons to top of item #4572

Merged
merged 13 commits into from
May 20, 2024
5 changes: 5 additions & 0 deletions .changeset/rich-pans-press.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@primer/react": patch
---

TreeView: Always align expand/collapse chevron icon, leading visual, and trailing visual to top of item
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
84 changes: 84 additions & 0 deletions packages/react/src/TreeView/TreeView.features.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1040,4 +1040,88 @@ export const LeadingAction: Story = () => {
)
}

export const MultilineItems: Story = () => (
<nav aria-label="Files changed">
<TreeView aria-label="Files changed">
<TreeView.Item id="src" defaultExpanded>
<TreeView.LeadingVisual>
<TreeView.DirectoryIcon />
</TreeView.LeadingVisual>
<div style={{whiteSpace: 'wrap'}}>
this is a very long directory name that we have intentionally allowed to wrap over multiple lines to
demonstrate alignment
</div>
<TreeView.SubTree>
<TreeView.Item id="src/Avatar.tsx">
<TreeView.LeadingVisual>
<FileIcon />
</TreeView.LeadingVisual>
Avatar.tsx
<TreeView.TrailingVisual>
<Octicon icon={DiffAddedIcon} color="success.fg" aria-label="Added" />
</TreeView.TrailingVisual>
</TreeView.Item>
</TreeView.SubTree>
</TreeView.Item>
<TreeView.Item id="src" defaultExpanded>
<TreeView.LeadingVisual>
<TreeView.DirectoryIcon />
</TreeView.LeadingVisual>
<div style={{whiteSpace: 'wrap'}}>
this is a medium directory name that we wrap over 2 lines to demonstrate alignment
</div>
<TreeView.TrailingVisual>
<Octicon icon={DiffAddedIcon} color="success.fg" aria-label="Added" />
</TreeView.TrailingVisual>
<TreeView.SubTree>
<TreeView.Item id="src/Avatar.tsx">
<TreeView.LeadingVisual>
<FileIcon />
</TreeView.LeadingVisual>
Avatar.tsx
<TreeView.TrailingVisual>
<Octicon icon={DiffAddedIcon} color="success.fg" aria-label="Added" />
</TreeView.TrailingVisual>
</TreeView.Item>
</TreeView.SubTree>
</TreeView.Item>
<TreeView.Item id="src" defaultExpanded>
<TreeView.LeadingVisual>
<TreeView.DirectoryIcon />
</TreeView.LeadingVisual>
this is a very long directory name that we have intentionally NOT allowed to wrap over multiple lines to
demonstrate alignment
<TreeView.SubTree>
<TreeView.Item id="src/Avatar.tsx">
<TreeView.LeadingVisual>
<FileIcon />
</TreeView.LeadingVisual>
Avatar.tsx
<TreeView.TrailingVisual>
<Octicon icon={DiffAddedIcon} color="success.fg" aria-label="Added" />
</TreeView.TrailingVisual>
</TreeView.Item>
</TreeView.SubTree>
</TreeView.Item>
<TreeView.Item id="src" defaultExpanded>
<TreeView.LeadingVisual>
<TreeView.DirectoryIcon />
</TreeView.LeadingVisual>
short name
<TreeView.SubTree>
<TreeView.Item id="src/Avatar.tsx">
<TreeView.LeadingVisual>
<FileIcon />
</TreeView.LeadingVisual>
Avatar.tsx
<TreeView.TrailingVisual>
<Octicon icon={DiffAddedIcon} color="success.fg" aria-label="Added" />
</TreeView.TrailingVisual>
</TreeView.Item>
</TreeView.SubTree>
</TreeView.Item>
</TreeView>
</nav>
)

export default meta
29 changes: 24 additions & 5 deletions packages/react/src/TreeView/TreeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ export type TreeViewProps = {
className?: string
}

/* Size of toggle icon in pixels. */
const TOGGLE_ICON_SIZE = 12

const UlBox = styled.ul<SxProp>`
list-style: none;
padding: 0;
Expand Down Expand Up @@ -105,14 +108,14 @@ const UlBox = styled.ul<SxProp>`
.PRIVATE_TreeView-item-container {
--level: 1; /* default level */
--toggle-width: 1rem; /* 16px */
--min-item-height: 2rem; /* 32px */
position: relative;
display: grid;
--leading-action-width: calc(var(--has-leading-action, 0) * 1.5rem);
--spacer-width: calc(calc(var(--level) - 1) * (var(--toggle-width) / 2));
grid-template-columns: var(--spacer-width) var(--leading-action-width) var(--toggle-width) 1fr;
grid-template-areas: 'spacer leadingAction toggle content';
width: 100%;
min-height: 2rem; /* 32px */
font-size: ${get('fontSizes.1')};
color: ${get('colors.fg.default')};
border-radius: ${get('radii.2')};
Expand All @@ -129,7 +132,7 @@ const UlBox = styled.ul<SxProp>`

@media (pointer: coarse) {
--toggle-width: 1.5rem; /* 24px */
min-height: 2.75rem; /* 44px */
--min-item-height: 2.75rem; /* 44px */
}

&:has(.PRIVATE_TreeView-item-skeleton):hover {
Expand Down Expand Up @@ -169,8 +172,11 @@ const UlBox = styled.ul<SxProp>`
.PRIVATE_TreeView-item-toggle {
grid-area: toggle;
display: flex;
iansan5653 marked this conversation as resolved.
Show resolved Hide resolved
align-items: center;
justify-content: center;
align-items: flex-start;
/* The toggle should appear vertically centered for single-line items, but remain at the top for items that wrap
across more lines. */
padding-top: calc(var(--min-item-height) / 2 - ${TOGGLE_ICON_SIZE}px / 2);
height: 100%;
color: ${get('colors.fg.muted')};
}
Expand All @@ -187,10 +193,14 @@ const UlBox = styled.ul<SxProp>`
.PRIVATE_TreeView-item-content {
grid-area: content;
display: flex;
align-items: center;
align-items: flex-start;
JelloBagel marked this conversation as resolved.
Show resolved Hide resolved
height: 100%;
padding: 0 ${get('space.2')};
gap: ${get('space.2')};
line-height: var(--custom-line-height, var(--text-body-lineHeight-medium, 1.4285));
/* The dynamic top and bottom padding to maintain the minimum item height for single line items */
padding-top: calc((var(--min-item-height) - var(--custom-line-height, 1.3rem)) / 2);
padding-bottom: calc((var(--min-item-height) - var(--custom-line-height, 1.3rem)) / 2);
}

.PRIVATE_TreeView-item-content-text {
Expand All @@ -200,11 +210,16 @@ const UlBox = styled.ul<SxProp>`
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
align-self: center;
JelloBagel marked this conversation as resolved.
Show resolved Hide resolved
}

.PRIVATE_TreeView-item-visual {
display: flex;
align-items: center;
color: ${get('colors.fg.muted')};
/* The visual icons should appear vertically centered for single-line items, but remain at the top for items that wrap
across more lines. */
height: var(--custom-line-height, 1.3rem);
}

.PRIVATE_TreeView-item-leading-action {
Expand Down Expand Up @@ -524,7 +539,11 @@ const Item = React.forwardRef<HTMLElement, TreeViewItemProps>(
}
}}
>
{isExpanded ? <ChevronDownIcon size={12} /> : <ChevronRightIcon size={12} />}
{isExpanded ? (
<ChevronDownIcon size={TOGGLE_ICON_SIZE} />
) : (
<ChevronRightIcon size={TOGGLE_ICON_SIZE} />
)}
</div>
) : null}
<div id={labelId} className="PRIVATE_TreeView-item-content">
Expand Down
Loading