diff --git a/.changeset/tame-nails-live.md b/.changeset/tame-nails-live.md new file mode 100644 index 00000000000..5c39b099be4 --- /dev/null +++ b/.changeset/tame-nails-live.md @@ -0,0 +1,5 @@ +--- +"@primer/react": minor +--- + +TreeView: Add support for `TreeView.LeadingAction` diff --git a/.playwright/snapshots/components/TreeView.test.ts-snapshots/TreeView-Leading-Action-dark-colorblind-linux.png b/.playwright/snapshots/components/TreeView.test.ts-snapshots/TreeView-Leading-Action-dark-colorblind-linux.png new file mode 100644 index 00000000000..7720ef27c8e Binary files /dev/null and b/.playwright/snapshots/components/TreeView.test.ts-snapshots/TreeView-Leading-Action-dark-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/TreeView.test.ts-snapshots/TreeView-Leading-Action-dark-dimmed-linux.png b/.playwright/snapshots/components/TreeView.test.ts-snapshots/TreeView-Leading-Action-dark-dimmed-linux.png new file mode 100644 index 00000000000..6a6ace273e0 Binary files /dev/null and b/.playwright/snapshots/components/TreeView.test.ts-snapshots/TreeView-Leading-Action-dark-dimmed-linux.png differ diff --git a/.playwright/snapshots/components/TreeView.test.ts-snapshots/TreeView-Leading-Action-dark-high-contrast-linux.png b/.playwright/snapshots/components/TreeView.test.ts-snapshots/TreeView-Leading-Action-dark-high-contrast-linux.png new file mode 100644 index 00000000000..5ecd5b32e16 Binary files /dev/null and b/.playwright/snapshots/components/TreeView.test.ts-snapshots/TreeView-Leading-Action-dark-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/TreeView.test.ts-snapshots/TreeView-Leading-Action-dark-linux.png b/.playwright/snapshots/components/TreeView.test.ts-snapshots/TreeView-Leading-Action-dark-linux.png new file mode 100644 index 00000000000..c695e47df2a Binary files /dev/null and b/.playwright/snapshots/components/TreeView.test.ts-snapshots/TreeView-Leading-Action-dark-linux.png differ diff --git a/.playwright/snapshots/components/TreeView.test.ts-snapshots/TreeView-Leading-Action-dark-tritanopia-linux.png b/.playwright/snapshots/components/TreeView.test.ts-snapshots/TreeView-Leading-Action-dark-tritanopia-linux.png new file mode 100644 index 00000000000..0aaca4b1b03 Binary files /dev/null and b/.playwright/snapshots/components/TreeView.test.ts-snapshots/TreeView-Leading-Action-dark-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/TreeView.test.ts-snapshots/TreeView-Leading-Action-light-colorblind-linux.png b/.playwright/snapshots/components/TreeView.test.ts-snapshots/TreeView-Leading-Action-light-colorblind-linux.png new file mode 100644 index 00000000000..1225ebc74f6 Binary files /dev/null and b/.playwright/snapshots/components/TreeView.test.ts-snapshots/TreeView-Leading-Action-light-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/TreeView.test.ts-snapshots/TreeView-Leading-Action-light-high-contrast-linux.png b/.playwright/snapshots/components/TreeView.test.ts-snapshots/TreeView-Leading-Action-light-high-contrast-linux.png new file mode 100644 index 00000000000..5b08dd9d293 Binary files /dev/null and b/.playwright/snapshots/components/TreeView.test.ts-snapshots/TreeView-Leading-Action-light-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/TreeView.test.ts-snapshots/TreeView-Leading-Action-light-linux.png b/.playwright/snapshots/components/TreeView.test.ts-snapshots/TreeView-Leading-Action-light-linux.png new file mode 100644 index 00000000000..9f8888445bf Binary files /dev/null and b/.playwright/snapshots/components/TreeView.test.ts-snapshots/TreeView-Leading-Action-light-linux.png differ diff --git a/.playwright/snapshots/components/TreeView.test.ts-snapshots/TreeView-Leading-Action-light-tritanopia-linux.png b/.playwright/snapshots/components/TreeView.test.ts-snapshots/TreeView-Leading-Action-light-tritanopia-linux.png new file mode 100644 index 00000000000..e5428c1f3a9 Binary files /dev/null and b/.playwright/snapshots/components/TreeView.test.ts-snapshots/TreeView-Leading-Action-light-tritanopia-linux.png differ diff --git a/e2e/components/TreeView.test.ts b/e2e/components/TreeView.test.ts index 1f665117e85..62d3d4ab11b 100644 --- a/e2e/components/TreeView.test.ts +++ b/e2e/components/TreeView.test.ts @@ -138,4 +138,37 @@ test.describe('TreeView', () => { }) } }) + + test.describe('Leading Action', () => { + for (const theme of themes) { + test.describe(theme, () => { + test('default @vrt', async ({page}) => { + await visit(page, { + id: 'components-treeview-features--leading-action', + globals: { + colorScheme: theme, + }, + }) + + expect(await page.screenshot()).toMatchSnapshot(`TreeView.Leading Action.${theme}.png`) + }) + + test('axe @aat', async ({page}) => { + await visit(page, { + id: 'components-treeview-features--leading-action', + globals: { + colorScheme: theme, + }, + }) + await expect(page).toHaveNoViolations({ + rules: { + 'color-contrast': { + enabled: theme !== 'dark_dimmed', + }, + }, + }) + }) + }) + } + }) }) diff --git a/packages/react/src/TreeView/TreeView.docs.json b/packages/react/src/TreeView/TreeView.docs.json index 5f820b0b02a..fb8d83893ef 100644 --- a/packages/react/src/TreeView/TreeView.docs.json +++ b/packages/react/src/TreeView/TreeView.docs.json @@ -105,6 +105,16 @@ } ] }, + { + "name": "TreeView.LeadingAction", + "props": [ + { + "name": "children", + "required": true, + "type": "React.ReactNode" + } + ] + }, { "name": "TreeView.DirectoryIcon", "props": [] diff --git a/packages/react/src/TreeView/TreeView.examples.stories.tsx b/packages/react/src/TreeView/TreeView.examples.stories.tsx new file mode 100644 index 00000000000..47e2b1f291d --- /dev/null +++ b/packages/react/src/TreeView/TreeView.examples.stories.tsx @@ -0,0 +1,76 @@ +import {GrabberIcon} from '@primer/octicons-react' +import type {Meta, Story} from '@storybook/react' +import React from 'react' +import Box from '../Box' +import {TreeView} from './TreeView' +import {IconButton} from '../Button' + +const meta: Meta = { + title: 'Components/TreeView/Examples', + component: TreeView, + decorators: [ + Story => { + return ( + // Prevent TreeView from expanding to the full width of the screen + + + + ) + }, + ], +} + +export const DraggableListItem: Story = () => { + return ( + + + Item 1 + + Item 2 + + sub task 1 + sub task 2 + + + Item 3 + + + ) +} + +const ControlledDraggableItem: React.FC<{id: string; children: React.ReactNode}> = ({id, children}) => { + const [expanded, setExpanded] = React.useState(false) + + return ( + <> + + + { + setExpanded(false) + // other drag logic to follow + }} + /> + + {children} + + + ) +} + +export default meta diff --git a/packages/react/src/TreeView/TreeView.features.stories.tsx b/packages/react/src/TreeView/TreeView.features.stories.tsx index 9470ad91730..73d02f13d75 100644 --- a/packages/react/src/TreeView/TreeView.features.stories.tsx +++ b/packages/react/src/TreeView/TreeView.features.stories.tsx @@ -4,7 +4,10 @@ import { DiffRemovedIcon, DiffRenamedIcon, FileIcon, + GrabberIcon, KebabHorizontalIcon, + IssueClosedIcon, + IssueOpenedIcon, } from '@primer/octicons-react' import type {Meta, Story} from '@storybook/react' import React from 'react' @@ -989,4 +992,52 @@ export const WithoutIndentation: Story = () => ( ) +export const LeadingAction: Story = () => { + return ( + + + + + + + + + Item 1 + + + + + + + + + Item 2 + + + + + + sub task 1 + + + + + + sub task 2 + + + + + + + + + + + Item 3 + + + ) +} + export default meta diff --git a/packages/react/src/TreeView/TreeView.tsx b/packages/react/src/TreeView/TreeView.tsx index 212bb9bf49c..53e86b916f5 100644 --- a/packages/react/src/TreeView/TreeView.tsx +++ b/packages/react/src/TreeView/TreeView.tsx @@ -97,6 +97,9 @@ const UlBox = styled.ul` outline-offset: -2; } } + &[data-has-leading-action] { + --has-leading-action: 1; + } } .PRIVATE_TreeView-item-container { @@ -104,8 +107,10 @@ const UlBox = styled.ul` --toggle-width: 1rem; /* 16px */ position: relative; display: grid; - grid-template-columns: calc(calc(var(--level) - 1) * (var(--toggle-width) / 2)) var(--toggle-width) 1fr; - grid-template-areas: 'spacer toggle content'; + --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')}; @@ -138,7 +143,7 @@ const UlBox = styled.ul` } &[data-omit-spacer='true'] .PRIVATE_TreeView-item-container { - grid-template-columns: 0 0 1fr; + grid-template-columns: 0 0 0 1fr; } .PRIVATE_TreeView-item[aria-current='true'] > .PRIVATE_TreeView-item-container { @@ -202,6 +207,12 @@ const UlBox = styled.ul` color: ${get('colors.fg.muted')}; } + .PRIVATE_TreeView-item-leading-action { + display: flex; + color: ${get('colors.fg.muted')}; + grid-area: leadingAction; + } + .PRIVATE_TreeView-item-level-line { width: 100%; height: 100%; @@ -354,11 +365,16 @@ const Item = React.forwardRef( }, ref, ) => { - const [slots, rest] = useSlots(children, {leadingVisual: LeadingVisual, trailingVisual: TrailingVisual}) + const [slots, rest] = useSlots(children, { + leadingAction: LeadingAction, + leadingVisual: LeadingVisual, + trailingVisual: TrailingVisual, + }) const {expandedStateCache} = React.useContext(RootContext) const labelId = useId() const leadingVisualId = useId() const trailingVisualId = useId() + const [isExpanded, setIsExpanded] = useControllableState({ name: itemId, // If the item was previously mounted, it's expanded state might be cached. @@ -449,6 +465,7 @@ const Item = React.forwardRef( aria-expanded={isSubTreeEmpty ? undefined : isExpanded} aria-current={isCurrentItem ? 'true' : undefined} aria-selected={isFocused ? 'true' : 'false'} + data-has-leading-action={slots.leadingAction ? true : undefined} onKeyDown={handleKeyDown} onFocus={event => { // Scroll the first child into view when the item receives focus @@ -488,6 +505,7 @@ const Item = React.forwardRef(
+ {slots.leadingAction} {hasSubTree ? ( // This lint rule is disabled due to the guidelines in the `TreeView` api docs. // https://github.com/github/primer/blob/main/apis/tree-view-api.md#the-expandcollapse-chevron-toggle @@ -829,6 +847,25 @@ const TrailingVisual: React.FC = props => { TrailingVisual.displayName = 'TreeView.TrailingVisual' +// ---------------------------------------------------------------------------- +// TreeView.LeadingAction + +const LeadingAction: React.FC = props => { + const {isExpanded} = React.useContext(ItemContext) + const children = typeof props.children === 'function' ? props.children({isExpanded}) : props.children + return ( + <> +
+ {props.label} +
+
+ {children} +
+ + ) +} + +LeadingAction.displayName = 'TreeView.LeadingAction' // ---------------------------------------------------------------------------- // TreeView.DirectoryIcon @@ -898,6 +935,7 @@ ErrorDialog.displayName = 'TreeView.ErrorDialog' export const TreeView = Object.assign(Root, { Item, SubTree, + LeadingAction, LeadingVisual, TrailingVisual, DirectoryIcon,