From 7b5cc029730ca0707d358f93c4ea45f6abaf74db Mon Sep 17 00:00:00 2001 From: Josh Black Date: Tue, 9 Jun 2026 13:30:37 -0500 Subject: [PATCH 1/2] feat: add CSS Layers to 12-navigation-composites Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../css-layers-navigation-composites.md | 5 + .../src/LabelGroup/LabelGroup.module.css | 88 +-- packages/react/src/NavList/NavList.module.css | 14 +- .../SegmentedControl.module.css | 524 +++++++++--------- .../react/src/TreeView/TreeView.module.css | 476 ++++++++-------- .../src/UnderlineNav/UnderlineNav.module.css | 38 +- .../UnderlineNav/UnderlineNavItem.module.css | 10 +- .../react/src/__tests__/css-layers.test.ts | 131 +---- 8 files changed, 611 insertions(+), 675 deletions(-) create mode 100644 .changeset/css-layers-navigation-composites.md diff --git a/.changeset/css-layers-navigation-composites.md b/.changeset/css-layers-navigation-composites.md new file mode 100644 index 00000000000..7e15221efef --- /dev/null +++ b/.changeset/css-layers-navigation-composites.md @@ -0,0 +1,5 @@ +--- +'@primer/react': patch +--- + +LabelGroup, NavList, SegmentedControl, TreeView, UnderlineNav: Improve custom class override behavior for component styles diff --git a/packages/react/src/LabelGroup/LabelGroup.module.css b/packages/react/src/LabelGroup/LabelGroup.module.css index 08cdc11fe77..2b104b8bb54 100644 --- a/packages/react/src/LabelGroup/LabelGroup.module.css +++ b/packages/react/src/LabelGroup/LabelGroup.module.css @@ -1,52 +1,54 @@ -.Container { - display: flex; - flex-wrap: nowrap; - gap: var(--base-size-4); - /* stylelint-disable-next-line primer/typography */ - line-height: 1; - max-width: 100%; - overflow: hidden; +@layer primer.components.LabelGroup { + .Container { + display: flex; + flex-wrap: nowrap; + gap: var(--base-size-4); + /* stylelint-disable-next-line primer/typography */ + line-height: 1; + max-width: 100%; + overflow: hidden; - &:where([data-overflow='inline']) { - flex-wrap: wrap; - } + &:where([data-overflow='inline']) { + flex-wrap: wrap; + } - &:where([data-list]) { - padding-inline-start: 0; - margin-block-start: 0; - margin-block-end: 0; - list-style-type: none; + &:where([data-list]) { + padding-inline-start: 0; + margin-block-start: 0; + margin-block-end: 0; + list-style-type: none; + } } -} -.ItemWrapper { - display: flex; - align-items: center; - /* This min-height matches the height of the expand/collapse button. - Without it, the labels/tokens will do a slight layout shift when expanded. - This is because the height of the first row will match the token sizes, - but the height of the second row will be the height of the collapse button. - */ - min-height: 28px; -} + .ItemWrapper { + display: flex; + align-items: center; + /* This min-height matches the height of the expand/collapse button. + Without it, the labels/tokens will do a slight layout shift when expanded. + This is because the height of the first row will match the token sizes, + but the height of the second row will be the height of the collapse button. + */ + min-height: 28px; + } -.ItemWrapper--hidden { - order: 9999; - pointer-events: none; - visibility: hidden; -} + .ItemWrapper--hidden { + order: 9999; + pointer-events: none; + visibility: hidden; + } -.OverlayContainer { - align-items: flex-start; - display: flex; -} + .OverlayContainer { + align-items: flex-start; + display: flex; + } -.OverlayInner { - display: flex; - flex-wrap: wrap; - gap: var(--base-size-4); -} + .OverlayInner { + display: flex; + flex-wrap: wrap; + gap: var(--base-size-4); + } -.CloseButton { - flex-shrink: 0; + .CloseButton { + flex-shrink: 0; + } } diff --git a/packages/react/src/NavList/NavList.module.css b/packages/react/src/NavList/NavList.module.css index 9908361e112..e071dec505c 100644 --- a/packages/react/src/NavList/NavList.module.css +++ b/packages/react/src/NavList/NavList.module.css @@ -1,8 +1,10 @@ -.GroupHeading > a { - color: var(--fgColor-default); - text-decoration: inherit; -} +@layer primer.components.NavList { + .GroupHeading > a { + color: var(--fgColor-default); + text-decoration: inherit; + } -.GroupHeading > a:hover { - text-decoration: underline; + .GroupHeading > a:hover { + text-decoration: underline; + } } diff --git a/packages/react/src/SegmentedControl/SegmentedControl.module.css b/packages/react/src/SegmentedControl/SegmentedControl.module.css index 05bc0feed8f..1dc0ec540b1 100644 --- a/packages/react/src/SegmentedControl/SegmentedControl.module.css +++ b/packages/react/src/SegmentedControl/SegmentedControl.module.css @@ -1,352 +1,354 @@ -.SegmentedControl { - /* TODO: use primitive `control.medium.size` when it is available instead of '32px' */ - --segmented-control-icon-width: 32px; - - display: inline-flex; - - /* TODO: use primitive `control.{small|medium}.size` when it is available */ - height: 32px; - padding: 0; - margin: 0; - font-size: var(--text-body-size-medium); - background-color: var(--controlTrack-bgColor-rest); - border: var(--borderWidth-thin) solid var(--controlTrack-borderColor-rest, transparent); - border-radius: var(--borderRadius-medium); - - /* Responsive full-width */ - &[data-full-width='true'] { - display: flex; - width: 100%; - - --segmented-control-icon-width: 100%; - } +@layer primer.components.SegmentedControl { + .SegmentedControl { + /* TODO: use primitive `control.medium.size` when it is available instead of '32px' */ + --segmented-control-icon-width: 32px; - &[data-full-width='false'] { display: inline-flex; - width: auto; - - --segmented-control-icon-width: 32px; - } - @media (--viewportRange-narrow) { - &[data-full-width-narrow='true'] { + /* TODO: use primitive `control.{small|medium}.size` when it is available */ + height: 32px; + padding: 0; + margin: 0; + font-size: var(--text-body-size-medium); + background-color: var(--controlTrack-bgColor-rest); + border: var(--borderWidth-thin) solid var(--controlTrack-borderColor-rest, transparent); + border-radius: var(--borderRadius-medium); + + /* Responsive full-width */ + &[data-full-width='true'] { display: flex; width: 100%; --segmented-control-icon-width: 100%; } - &[data-full-width-narrow='false'] { + &[data-full-width='false'] { display: inline-flex; width: auto; --segmented-control-icon-width: 32px; } - } - @media (--viewportRange-regular) { - &[data-full-width-regular='true'] { - display: flex; - width: 100%; + @media (--viewportRange-narrow) { + &[data-full-width-narrow='true'] { + display: flex; + width: 100%; - --segmented-control-icon-width: 100%; - } + --segmented-control-icon-width: 100%; + } - &[data-full-width-regular='false'] { - display: inline-flex; - width: auto; + &[data-full-width-narrow='false'] { + display: inline-flex; + width: auto; - --segmented-control-icon-width: 32px; + --segmented-control-icon-width: 32px; + } } - } - @media (--viewportRange-wide) { - &[data-full-width-wide='true'] { - display: flex; - width: 100%; + @media (--viewportRange-regular) { + &[data-full-width-regular='true'] { + display: flex; + width: 100%; - --segmented-control-icon-width: 100%; - } + --segmented-control-icon-width: 100%; + } - &[data-full-width-wide='false'] { - display: inline-flex; - width: auto; + &[data-full-width-regular='false'] { + display: inline-flex; + width: auto; - --segmented-control-icon-width: 32px; + --segmented-control-icon-width: 32px; + } } - &[data-full-width-regular='true']:not([data-full-width-wide='true']) { - display: inline-flex; - width: auto; + @media (--viewportRange-wide) { + &[data-full-width-wide='true'] { + display: flex; + width: 100%; - --segmented-control-icon-width: 32px; - } - } + --segmented-control-icon-width: 100%; + } - /* Hide when dropdown variant is active */ - &[data-variant='dropdown'] { - display: none; - } + &[data-full-width-wide='false'] { + display: inline-flex; + width: auto; - /* Handle hideLabels variant - hide button text */ - &[data-variant='hideLabels'] .Text { - display: none; - } + --segmented-control-icon-width: 32px; + } - @media (--viewportRange-narrow) { - &[data-variant-narrow='dropdown'] { - display: none; - } + &[data-full-width-regular='true']:not([data-full-width-wide='true']) { + display: inline-flex; + width: auto; - &[data-variant-narrow='hideLabels'] .Text { - display: none; + --segmented-control-icon-width: 32px; + } } - } - @media (--viewportRange-regular) { - &[data-variant-regular='dropdown'] { + /* Hide when dropdown variant is active */ + &[data-variant='dropdown'] { display: none; } - &[data-variant-regular='hideLabels'] .Text { + /* Handle hideLabels variant - hide button text */ + &[data-variant='hideLabels'] .Text { display: none; } - } - @media (--viewportRange-wide) { - &[data-variant-wide='dropdown'] { - display: none; + @media (--viewportRange-narrow) { + &[data-variant-narrow='dropdown'] { + display: none; + } + + &[data-variant-narrow='hideLabels'] .Text { + display: none; + } } - &[data-variant-wide='hideLabels'] .Text { - display: none; + @media (--viewportRange-regular) { + &[data-variant-regular='dropdown'] { + display: none; + } + + &[data-variant-regular='hideLabels'] .Text { + display: none; + } } - } - &:where([data-size='small']) { - /* TODO: use primitive `control.{small|medium}.size` when it is available */ - height: 28px; - font-size: var(--text-body-size-small); - } -} + @media (--viewportRange-wide) { + &[data-variant-wide='dropdown'] { + display: none; + } -.DropdownContainer { - display: none; + &[data-variant-wide='hideLabels'] .Text { + display: none; + } + } - /* Show when dropdown variant is active */ - &[data-variant='dropdown'] { - display: block; + &:where([data-size='small']) { + /* TODO: use primitive `control.{small|medium}.size` when it is available */ + height: 28px; + font-size: var(--text-body-size-small); + } } - @media (--viewportRange-narrow) { - &[data-variant-narrow='dropdown'] { + .DropdownContainer { + display: none; + + /* Show when dropdown variant is active */ + &[data-variant='dropdown'] { display: block; } - } - @media (--viewportRange-regular) { - &[data-variant-regular='dropdown'] { - display: block; + @media (--viewportRange-narrow) { + &[data-variant-narrow='dropdown'] { + display: block; + } } - } - @media (--viewportRange-wide) { - &[data-variant-wide='dropdown'] { - display: block; + @media (--viewportRange-regular) { + &[data-variant-regular='dropdown'] { + display: block; + } } - } -} -.Item { - position: relative; - display: block; - /* stylelint-disable-next-line primer/spacing */ - margin-top: -1px; - /* stylelint-disable-next-line primer/spacing */ - margin-bottom: -1px; - flex-grow: 1; + @media (--viewportRange-wide) { + &[data-variant-wide='dropdown'] { + display: block; + } + } + } - &:not(:last-child) { + .Item { + position: relative; + display: block; + /* stylelint-disable-next-line primer/spacing */ + margin-top: -1px; /* stylelint-disable-next-line primer/spacing */ - margin-right: 1px; - - &::after { - position: absolute; - top: var(--base-size-8); - right: calc(-1 * var(--base-size-2)); - bottom: var(--base-size-8); - width: 1px; - content: ''; - /* stylelint-disable-next-line primer/colors */ - background-color: var(--borderColor-default); + margin-bottom: -1px; + flex-grow: 1; + + &:not(:last-child) { + /* stylelint-disable-next-line primer/spacing */ + margin-right: 1px; + + &::after { + position: absolute; + top: var(--base-size-8); + right: calc(-1 * var(--base-size-2)); + bottom: var(--base-size-8); + width: 1px; + content: ''; + /* stylelint-disable-next-line primer/colors */ + background-color: var(--borderColor-default); + } + + /* stylelint-disable-next-line selector-pseudo-class-disallowed-list -- scoped to CSS Module, audited (github/github-ui#17224) */ + &:has(+ [data-selected])::after, + &:where([data-selected])::after { + background-color: transparent; + } } /* stylelint-disable-next-line selector-pseudo-class-disallowed-list -- scoped to CSS Module, audited (github/github-ui#17224) */ - &:has(+ [data-selected])::after, - &:where([data-selected])::after { + &:focus-within:has(:focus-visible) { background-color: transparent; } - } - - /* stylelint-disable-next-line selector-pseudo-class-disallowed-list -- scoped to CSS Module, audited (github/github-ui#17224) */ - &:focus-within:has(:focus-visible) { - background-color: transparent; - } - &:first-child { - /* stylelint-disable-next-line primer/spacing */ - margin-left: -1px; - } + &:first-child { + /* stylelint-disable-next-line primer/spacing */ + margin-left: -1px; + } - &:last-child { - /* stylelint-disable-next-line primer/spacing */ - margin-right: -1px; - } + &:last-child { + /* stylelint-disable-next-line primer/spacing */ + margin-right: -1px; + } - .Counter { - margin-inline-start: var(--base-size-8); - display: flex; - align-items: center; + .Counter { + margin-inline-start: var(--base-size-8); + display: flex; + align-items: center; + } } -} -.Button { - /* TODO: use primitive `primer.control.medium.paddingInline.normal` when it is available */ - --segmented-control-button-inner-padding: 12px; - --segmented-control-button-bg-inset: 4px; - --segmented-control-outer-radius: var(--borderRadius-medium); - - width: 100%; - height: 100%; - /* stylelint-disable-next-line primer/spacing */ - padding: var(--segmented-control-button-bg-inset); - font-family: inherit; - font-size: inherit; - font-weight: var(--base-text-weight-normal); - color: currentColor; - cursor: pointer; - background-color: transparent; - border-color: transparent; - border-width: 0; - /* stylelint-disable-next-line primer/borders */ - border-radius: var(--segmented-control-outer-radius); - - & svg { - fill: var(--fgColor-muted); - color: var(--fgColor-muted); - } + .Button { + /* TODO: use primitive `primer.control.medium.paddingInline.normal` when it is available */ + --segmented-control-button-inner-padding: 12px; + --segmented-control-button-bg-inset: 4px; + --segmented-control-outer-radius: var(--borderRadius-medium); - /* fallback :focus state */ - &:focus:not(:disabled) { - outline: var(--base-size-2) solid var(--fgColor-accent); - outline-offset: -1px; - box-shadow: none; + width: 100%; + height: 100%; + /* stylelint-disable-next-line primer/spacing */ + padding: var(--segmented-control-button-bg-inset); + font-family: inherit; + font-size: inherit; + font-weight: var(--base-text-weight-normal); + color: currentColor; + cursor: pointer; + background-color: transparent; + border-color: transparent; + border-width: 0; + /* stylelint-disable-next-line primer/borders */ + border-radius: var(--segmented-control-outer-radius); - /* remove fallback :focus if :focus-visible is supported */ - &:not(:focus-visible) { - outline: solid 1px transparent; + & svg { + fill: var(--fgColor-muted); + color: var(--fgColor-muted); } - } - /* default focus state */ - &:focus-visible:not(:disabled) { - outline: var(--base-size-2) solid var(--fgColor-accent); - outline-offset: -1px; - box-shadow: none; - } + /* fallback :focus state */ + &:focus:not(:disabled) { + outline: var(--base-size-2) solid var(--fgColor-accent); + outline-offset: -1px; + box-shadow: none; - /* stylelint-disable-next-line selector-max-specificity */ - &:focus:focus-visible:not(:last-child)::after { - /* fixes an issue where the focus outline shows over the pseudo-element */ - width: 0; - } + /* remove fallback :focus if :focus-visible is supported */ + &:not(:focus-visible) { + outline: solid 1px transparent; + } + } - &[aria-disabled='true']:not([aria-current='true']) { - cursor: not-allowed; - color: var(--fgColor-disabled); - background-color: transparent; + /* default focus state */ + &:focus-visible:not(:disabled) { + outline: var(--base-size-2) solid var(--fgColor-accent); + outline-offset: -1px; + box-shadow: none; + } - & svg { - fill: var(--fgColor-disabled); + /* stylelint-disable-next-line selector-max-specificity */ + &:focus:focus-visible:not(:last-child)::after { + /* fixes an issue where the focus outline shows over the pseudo-element */ + width: 0; + } + + &[aria-disabled='true']:not([aria-current='true']) { + cursor: not-allowed; color: var(--fgColor-disabled); + background-color: transparent; + + & svg { + fill: var(--fgColor-disabled); + color: var(--fgColor-disabled); + } } - } - @media (pointer: coarse) { - &::before { - position: absolute; - top: 50%; - right: 0; - left: 0; - min-height: 44px; - content: ''; - transform: translateY(-50%); + @media (pointer: coarse) { + &::before { + position: absolute; + top: 50%; + right: 0; + left: 0; + min-height: 44px; + content: ''; + transform: translateY(-50%); + } } } -} - -.IconButton { - width: var(--segmented-control-icon-width, 32px); -} - -.Content { - display: flex; - height: 100%; - /* stylelint-disable-next-line primer/spacing */ - padding-right: calc(var(--segmented-control-button-inner-padding) - var(--segmented-control-button-bg-inset)); - /* stylelint-disable-next-line primer/spacing */ - padding-left: calc(var(--segmented-control-button-inner-padding) - var(--segmented-control-button-bg-inset)); - border-color: transparent; - border-style: solid; - border-width: var(--borderWidth-thin); - - /* - innerRadius = outerRadius - distance/2 - https://stackoverflow.com/questions/2932146/math-problem-determine-the-corner-radius-of-an-inner-border-based-on-outer-corn - */ - /* stylelint-disable-next-line primer/borders */ - border-radius: calc(var(--segmented-control-outer-radius) - var(--segmented-control-button-bg-inset) / 2); - align-items: center; - justify-content: center; -} -.Button[aria-current='true'] { - padding: 0; - font-weight: var(--base-text-weight-semibold); + .IconButton { + width: var(--segmented-control-icon-width, 32px); + } .Content { + display: flex; + height: 100%; /* stylelint-disable-next-line primer/spacing */ - padding-right: var(--segmented-control-button-inner-padding); + padding-right: calc(var(--segmented-control-button-inner-padding) - var(--segmented-control-button-bg-inset)); /* stylelint-disable-next-line primer/spacing */ - padding-left: var(--segmented-control-button-inner-padding); - background-color: var(--controlKnob-bgColor-rest); - border-color: var(--controlKnob-borderColor-rest); + padding-left: calc(var(--segmented-control-button-inner-padding) - var(--segmented-control-button-bg-inset)); + border-color: transparent; + border-style: solid; + border-width: var(--borderWidth-thin); + + /* + innerRadius = outerRadius - distance/2 + https://stackoverflow.com/questions/2932146/math-problem-determine-the-corner-radius-of-an-inner-border-based-on-outer-corn + */ /* stylelint-disable-next-line primer/borders */ - border-radius: var(--segmented-control-outer-radius); + border-radius: calc(var(--segmented-control-outer-radius) - var(--segmented-control-button-bg-inset) / 2); + align-items: center; + justify-content: center; } -} -.Button:not([aria-current='true'], [aria-disabled='true']) { - &:hover .Content { - background-color: var(--controlTrack-bgColor-hover); + .Button[aria-current='true'] { + padding: 0; + font-weight: var(--base-text-weight-semibold); + + .Content { + /* stylelint-disable-next-line primer/spacing */ + padding-right: var(--segmented-control-button-inner-padding); + /* stylelint-disable-next-line primer/spacing */ + padding-left: var(--segmented-control-button-inner-padding); + background-color: var(--controlKnob-bgColor-rest); + border-color: var(--controlKnob-borderColor-rest); + /* stylelint-disable-next-line primer/borders */ + border-radius: var(--segmented-control-outer-radius); + } } - &:active .Content { - background-color: var(--controlTrack-bgColor-active); + .Button:not([aria-current='true'], [aria-disabled='true']) { + &:hover .Content { + background-color: var(--controlTrack-bgColor-hover); + } + + &:active .Content { + background-color: var(--controlTrack-bgColor-active); + } } -} -.Text::after { - display: block; - height: 0; - overflow: hidden; - font-weight: var(--base-text-weight-semibold); - pointer-events: none; - visibility: hidden; - content: attr(data-text); - user-select: none; -} + .Text::after { + display: block; + height: 0; + overflow: hidden; + font-weight: var(--base-text-weight-semibold); + pointer-events: none; + visibility: hidden; + content: attr(data-text); + user-select: none; + } -.LeadingIcon { - margin-right: var(--base-size-4); + .LeadingIcon { + margin-right: var(--base-size-4); + } } diff --git a/packages/react/src/TreeView/TreeView.module.css b/packages/react/src/TreeView/TreeView.module.css index 2c3ea4ee472..df567d87ffd 100644 --- a/packages/react/src/TreeView/TreeView.module.css +++ b/packages/react/src/TreeView/TreeView.module.css @@ -1,291 +1,293 @@ -.TreeViewRootUlStyles { - padding: 0; - margin: 0; - list-style: none; - - /* - * WARNING: This is a performance optimization. - * - * We define styles for the tree items at the root level of the tree - * to avoid recomputing the styles for each item when the tree updates. - * We're sacrificing maintainability for performance because TreeView - * needs to be performant enough to handle large trees (thousands of items). - * - * This is intended to be a temporary solution until we can improve the - * performance of our styling patterns. - * - * Do NOT copy this pattern without understanding the tradeoffs. - */ - .TreeViewItem { +@layer primer.components.TreeView { + .TreeViewRootUlStyles { + padding: 0; + margin: 0; + list-style: none; + /* - * `overflow-clip-margin` extends the paint clip edge by 8px so the current-item indicator - * (positioned at `left: -8px` of the row container) remains visible when a consumer applies - * `contain: paint` (or `contain: strict`, or `content-visibility: auto`) to this `
  • `. Has - * no effect when no paint containment is active, so default rendering is unchanged. + * WARNING: This is a performance optimization. + * + * We define styles for the tree items at the root level of the tree + * to avoid recomputing the styles for each item when the tree updates. + * We're sacrificing maintainability for performance because TreeView + * needs to be performant enough to handle large trees (thousands of items). + * + * This is intended to be a temporary solution until we can improve the + * performance of our styling patterns. + * + * Do NOT copy this pattern without understanding the tradeoffs. */ - overflow-clip-margin: var(--base-size-8); - outline: none; + .TreeViewItem { + /* + * `overflow-clip-margin` extends the paint clip edge by 8px so the current-item indicator + * (positioned at `left: -8px` of the row container) remains visible when a consumer applies + * `contain: paint` (or `contain: strict`, or `content-visibility: auto`) to this `
  • `. Has + * no effect when no paint containment is active, so default rendering is unchanged. + */ + overflow-clip-margin: var(--base-size-8); + outline: none; - &:focus-visible > div, - &:global(.focus-visible) > div { - box-shadow: var(--boxShadow-thick) var(--fgColor-accent); + &:focus-visible > div, + &:global(.focus-visible) > div { + box-shadow: var(--boxShadow-thick) var(--fgColor-accent); - @media (forced-colors: active) { - outline: 2px solid HighlightText; - outline-offset: -2px; + @media (forced-colors: active) { + outline: 2px solid HighlightText; + outline-offset: -2px; + } + } + + &[data-has-leading-action] { + --has-leading-action: 1; } } - &[data-has-leading-action] { - --has-leading-action: 1; + .TreeViewItemContainer { + --level: 1; + --toggle-width: 1rem; + --min-item-height: 2rem; + + position: relative; + display: grid; + width: 100%; + /* + * Mirrors the `overflow-clip-margin` on `.TreeViewItem` so the indicator also stays + * visible when `containIntrinsicSize` is set on this row (which sets + * `content-visibility: auto` on this container and implies paint containment). + */ + overflow-clip-margin: var(--base-size-8); + font-size: var(--text-body-size-medium); + color: var(--fgColor-default); + cursor: pointer; + border-radius: var(--borderRadius-medium); + grid-template-columns: var(--spacer-width) var(--leading-action-width) var(--toggle-width) 1fr auto; + grid-template-areas: 'spacer leadingAction toggle content trailingAction'; + + --leading-action-width: calc(var(--has-leading-action, 0) * 1.5rem); + --spacer-width: calc(calc(var(--level) - 1) * (var(--toggle-width) / 2)); + + &:hover { + background-color: var(--control-transparent-bgColor-hover); + + @media (forced-colors: active) { + outline: 2px solid transparent; + outline-offset: -2px; + } + } + + @media (pointer: coarse) { + --toggle-width: 1.5rem; + --min-item-height: 2.75rem; + } } - } - .TreeViewItemContainer { - --level: 1; - --toggle-width: 1rem; - --min-item-height: 2rem; + &:where([data-omit-spacer='true']) .TreeViewItemContainer { + grid-template-columns: 0 0 0 1fr auto; + } - position: relative; - display: grid; - width: 100%; /* - * Mirrors the `overflow-clip-margin` on `.TreeViewItem` so the indicator also stays - * visible when `containIntrinsicSize` is set on this row (which sets - * `content-visibility: auto` on this container and implies paint containment). + * Suppress hover affordances on rows being used as skeleton loading placeholders. + * Marked positively via `data-loading` from `LoadingItem` so we avoid the broad + * invalidation cost of `:has(.TreeViewItemSkeleton)` across every row in large trees. */ - overflow-clip-margin: var(--base-size-8); - font-size: var(--text-body-size-medium); - color: var(--fgColor-default); - cursor: pointer; - border-radius: var(--borderRadius-medium); - grid-template-columns: var(--spacer-width) var(--leading-action-width) var(--toggle-width) 1fr auto; - grid-template-areas: 'spacer leadingAction toggle content trailingAction'; - - --leading-action-width: calc(var(--has-leading-action, 0) * 1.5rem); - --spacer-width: calc(calc(var(--level) - 1) * (var(--toggle-width) / 2)); - - &:hover { - background-color: var(--control-transparent-bgColor-hover); + .TreeViewItem:where([data-loading]) > .TreeViewItemContainer:hover { + cursor: default; + background-color: transparent; @media (forced-colors: active) { - outline: 2px solid transparent; - outline-offset: -2px; + outline: none; } } - @media (pointer: coarse) { - --toggle-width: 1.5rem; - --min-item-height: 2.75rem; + .TreeViewItem[aria-current='true'] > .TreeViewItemContainer { + background-color: var(--control-transparent-bgColor-selected); + + /* Current item indicator */ + /* stylelint-disable-next-line selector-max-specificity */ + &::after { + position: absolute; + top: calc(50% - var(--base-size-12)); + left: calc(-1 * var(--base-size-8)); + width: 0.25rem; + height: 1.5rem; + content: ''; + + /* + * Use fgColor accent for consistency across all themes. Using the "correct" variable, + * --bgColor-accent-emphasis, causes vrt failures for dark high contrast mode + */ + /* stylelint-disable-next-line primer/colors */ + background-color: var(--fgColor-accent); + border-radius: var(--borderRadius-medium); + + @media (forced-colors: active) { + background-color: HighlightText; + } + } } - } - &:where([data-omit-spacer='true']) .TreeViewItemContainer { - grid-template-columns: 0 0 0 1fr auto; - } - - /* - * Suppress hover affordances on rows being used as skeleton loading placeholders. - * Marked positively via `data-loading` from `LoadingItem` so we avoid the broad - * invalidation cost of `:has(.TreeViewItemSkeleton)` across every row in large trees. - */ - .TreeViewItem:where([data-loading]) > .TreeViewItemContainer:hover { - cursor: default; - background-color: transparent; - - @media (forced-colors: active) { - outline: none; + .TreeViewItemToggle { + display: flex; + height: 100%; + + /* The toggle should appear vertically centered for single-line items, but remain at the top for items that wrap + across more lines. */ + /* stylelint-disable-next-line primer/spacing */ + padding-top: calc(var(--min-item-height) / 2 - var(--base-size-12) / 2); + color: var(--fgColor-muted); + grid-area: toggle; + justify-content: center; + align-items: flex-start; } - } - - .TreeViewItem[aria-current='true'] > .TreeViewItemContainer { - background-color: var(--control-transparent-bgColor-selected); - - /* Current item indicator */ - /* stylelint-disable-next-line selector-max-specificity */ - &::after { - position: absolute; - top: calc(50% - var(--base-size-12)); - left: calc(-1 * var(--base-size-8)); - width: 0.25rem; - height: 1.5rem; - content: ''; - - /* - * Use fgColor accent for consistency across all themes. Using the "correct" variable, - * --bgColor-accent-emphasis, causes vrt failures for dark high contrast mode - */ - /* stylelint-disable-next-line primer/colors */ - background-color: var(--fgColor-accent); - border-radius: var(--borderRadius-medium); - @media (forced-colors: active) { - background-color: HighlightText; - } + .TreeViewItemToggleHover:hover { + background-color: var(--control-transparent-bgColor-hover); } - } - .TreeViewItemToggle { - display: flex; - height: 100%; - - /* The toggle should appear vertically centered for single-line items, but remain at the top for items that wrap - across more lines. */ - /* stylelint-disable-next-line primer/spacing */ - padding-top: calc(var(--min-item-height) / 2 - var(--base-size-12) / 2); - color: var(--fgColor-muted); - grid-area: toggle; - justify-content: center; - align-items: flex-start; - } + .TreeViewItemToggleEnd { + border-top-left-radius: var(--borderRadius-medium); + border-bottom-left-radius: var(--borderRadius-medium); + } - .TreeViewItemToggleHover:hover { - background-color: var(--control-transparent-bgColor-hover); - } + .TreeViewItemContent { + display: flex; + height: 100%; + padding: 0 var(--base-size-8); + + /* The dynamic top and bottom padding to maintain the minimum item height for single line items */ + /* stylelint-disable-next-line primer/spacing */ + padding-top: calc((var(--min-item-height) - var(--custom-line-height, 1.3rem)) / 2); + /* stylelint-disable-next-line primer/spacing */ + padding-bottom: calc((var(--min-item-height) - var(--custom-line-height, 1.3rem)) / 2); + line-height: var(--custom-line-height, var(--text-body-lineHeight-medium, 1.4285)); + grid-area: content; + gap: var(--stack-gap-condensed); + } - .TreeViewItemToggleEnd { - border-top-left-radius: var(--borderRadius-medium); - border-bottom-left-radius: var(--borderRadius-medium); - } + .TreeViewItemContentText { + flex: 1 1 auto; + width: 0; + } - .TreeViewItemContent { - display: flex; - height: 100%; - padding: 0 var(--base-size-8); - - /* The dynamic top and bottom padding to maintain the minimum item height for single line items */ - /* stylelint-disable-next-line primer/spacing */ - padding-top: calc((var(--min-item-height) - var(--custom-line-height, 1.3rem)) / 2); - /* stylelint-disable-next-line primer/spacing */ - padding-bottom: calc((var(--min-item-height) - var(--custom-line-height, 1.3rem)) / 2); - line-height: var(--custom-line-height, var(--text-body-lineHeight-medium, 1.4285)); - grid-area: content; - gap: var(--stack-gap-condensed); - } + &:where([data-truncate-text='true']) .TreeViewItemContentText { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } - .TreeViewItemContentText { - flex: 1 1 auto; - width: 0; - } + &:where([data-truncate-text='false']) .TreeViewItemContentText { + /* stylelint-disable-next-line declaration-property-value-keyword-no-deprecated */ + word-break: break-word; + } - &:where([data-truncate-text='true']) .TreeViewItemContentText { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } + .TreeViewItemVisual { + display: flex; - &:where([data-truncate-text='false']) .TreeViewItemContentText { - /* stylelint-disable-next-line declaration-property-value-keyword-no-deprecated */ - word-break: break-word; - } + /* 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); + color: var(--fgColor-muted); + align-items: center; + } - .TreeViewItemVisual { - display: flex; + .TreeViewItemLeadingAction { + display: flex; + color: var(--fgColor-muted); + grid-area: leadingAction; - /* 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); - color: var(--fgColor-muted); - align-items: center; - } + & > button { + flex-shrink: 1; + } + } - .TreeViewItemLeadingAction { - display: flex; - color: var(--fgColor-muted); - grid-area: leadingAction; + .TreeViewItemTrailingAction { + display: flex; + color: var(--fgColor-muted); + grid-area: trailingAction; + } - & > button { + .TreeViewItemTrailingActionButton { flex-shrink: 1; } - } - - .TreeViewItemTrailingAction { - display: flex; - color: var(--fgColor-muted); - grid-area: trailingAction; - } - .TreeViewItemTrailingActionButton { - flex-shrink: 1; - } + .TreeViewItemLevelLine { + width: 100%; + height: 100%; + border-right: var(--borderWidth-thin) solid; - .TreeViewItemLevelLine { - width: 100%; - height: 100%; - border-right: var(--borderWidth-thin) solid; + /* + * `--tree-line-color` is set on the root `