diff --git a/.changeset/css-layers-filtered-form-select-panel.md b/.changeset/css-layers-filtered-form-select-panel.md new file mode 100644 index 00000000000..f2b83eea0a2 --- /dev/null +++ b/.changeset/css-layers-filtered-form-select-panel.md @@ -0,0 +1,5 @@ +--- +'@primer/react': patch +--- + +FilteredActionList, FormControl, SelectPanel: Add CSS layer support for component styles diff --git a/packages/react/src/FilteredActionList/FilteredActionList.module.css b/packages/react/src/FilteredActionList/FilteredActionList.module.css index 1f3dd981726..c3157866a6f 100644 --- a/packages/react/src/FilteredActionList/FilteredActionList.module.css +++ b/packages/react/src/FilteredActionList/FilteredActionList.module.css @@ -1,82 +1,84 @@ -.Root { - display: flex; - flex-direction: column; - overflow: hidden; -} +@layer primer.components.FilteredActionList { + .Root { + display: flex; + flex-direction: column; + overflow: hidden; + } -.Header { - /* stylelint-disable-next-line primer/box-shadow */ - box-shadow: 0 1px 0 var(--borderColor-default); - z-index: 1; -} + .Header { + /* stylelint-disable-next-line primer/box-shadow */ + box-shadow: 0 1px 0 var(--borderColor-default); + z-index: 1; + } -.Container { - display: flex; - height: 100%; - overflow: auto; - flex-grow: 1; + .Container { + display: flex; + height: 100%; + overflow: auto; + flex-grow: 1; - /* Allow the browser to skip rendering for off-screen items, reducing style recalc and layout costs in long lists. - Exclude items that show the active indicator line, as content-visibility: auto applies paint containment - which clips the absolutely-positioned ::after pseudo-element that renders outside the item bounds. */ - & .ActionListItem:not(:focus, [data-is-active-descendant], [data-active], [data-input-focused]) { - content-visibility: auto; - contain-intrinsic-size: auto 32px; - } + /* Allow the browser to skip rendering for off-screen items, reducing style recalc and layout costs in long lists. + Exclude items that show the active indicator line, as content-visibility: auto applies paint containment + which clips the absolutely-positioned ::after pseudo-element that renders outside the item bounds. */ + & .ActionListItem:not(:focus, [data-is-active-descendant], [data-active], [data-input-focused]) { + content-visibility: auto; + contain-intrinsic-size: auto 32px; + } - /* When showDividers is enabled, divider ::before pseudo-elements on ActionListSubContent are - positioned outside the item bounds (top: -7px). content-visibility: auto applies paint containment - which clips these, so we must disable it for items in lists with dividers. */ - & [data-dividers='true'] .ActionListItem { - content-visibility: visible; + /* When showDividers is enabled, divider ::before pseudo-elements on ActionListSubContent are + positioned outside the item bounds (top: -7px). content-visibility: auto applies paint containment + which clips these, so we must disable it for items in lists with dividers. */ + & [data-dividers='true'] .ActionListItem { + content-visibility: visible; + } } -} -.ActionList { - flex-grow: 1; -} + .ActionList { + flex-grow: 1; + } -.ActionListItem:focus { - background: var(--control-transparent-bgColor-selected); + .ActionListItem:focus { + background: var(--control-transparent-bgColor-selected); - &::after { - @mixin activeIndicatorLine; + &::after { + @mixin activeIndicatorLine; + } } -} -.ActionListItem:where([data-input-focused]):where([data-first-child]) { - background: var(--control-transparent-bgColor-selected); + .ActionListItem:where([data-input-focused]):where([data-first-child]) { + background: var(--control-transparent-bgColor-selected); - &::after { - @mixin activeIndicatorLine; + &::after { + @mixin activeIndicatorLine; + } } -} -.FullScreenTextInput { - @media screen and (--viewportRange-narrow) { - /* Ensures inputs don't zoom on mobile iPhone but are body-font size on iPad */ - @supports (-webkit-touch-callout: none) { - font-size: var(--text-title-size-small); + .FullScreenTextInput { + @media screen and (--viewportRange-narrow) { + /* Ensures inputs don't zoom on mobile iPhone but are body-font size on iPad */ + @supports (-webkit-touch-callout: none) { + font-size: var(--text-title-size-small); + } } } -} -.SelectAllContainer { - display: flex; - align-items: center; - padding-block: var(--base-size-4); - padding-inline: var(--base-size-16); - background: var(--bgColor-muted); - border-bottom: var(--borderWidth-thin) solid var(--borderColor-default); -} + .SelectAllContainer { + display: flex; + align-items: center; + padding-block: var(--base-size-4); + padding-inline: var(--base-size-16); + background: var(--bgColor-muted); + border-bottom: var(--borderWidth-thin) solid var(--borderColor-default); + } -.SelectAllCheckbox { - /* -1px hack to offset 1px border-bottom causing uneven alignment */ - /* stylelint-disable-next-line primer/spacing */ - margin: var(--base-size-4) var(--base-size-8) calc(var(--base-size-4) - 1px) 0; -} + .SelectAllCheckbox { + /* -1px hack to offset 1px border-bottom causing uneven alignment */ + /* stylelint-disable-next-line primer/spacing */ + margin: var(--base-size-4) var(--base-size-8) calc(var(--base-size-4) - 1px) 0; + } -.SelectAllLabel { - font-size: var(--text-body-size-medium); - color: var(--fgColor-muted); + .SelectAllLabel { + font-size: var(--text-body-size-medium); + color: var(--fgColor-muted); + } } diff --git a/packages/react/src/FilteredActionList/FilteredActionListLoaders.module.css b/packages/react/src/FilteredActionList/FilteredActionListLoaders.module.css index 603d07a0c66..0eb8d2e3560 100644 --- a/packages/react/src/FilteredActionList/FilteredActionListLoaders.module.css +++ b/packages/react/src/FilteredActionList/FilteredActionListLoaders.module.css @@ -1,19 +1,21 @@ -.LoadingSkeleton { - /* stylelint-disable-next-line primer/borders */ - border-radius: 4px; -} +@layer primer.components.FilteredActionList { + .LoadingSkeleton { + /* stylelint-disable-next-line primer/borders */ + border-radius: 4px; + } -.LoadingSpinner { - padding: var(--base-size-16); - flex-grow: 1; - align-content: center; - text-align: center; - height: 100%; -} + .LoadingSpinner { + padding: var(--base-size-16); + flex-grow: 1; + align-content: center; + text-align: center; + height: 100%; + } -.LoadingSkeletonContainer { - padding: var(--base-size-8); - display: flex; - flex-grow: 1; - flex-direction: column; + .LoadingSkeletonContainer { + padding: var(--base-size-8); + display: flex; + flex-grow: 1; + flex-direction: column; + } } diff --git a/packages/react/src/FormControl/FormControl.module.css b/packages/react/src/FormControl/FormControl.module.css index 911f04bbde7..ff82c37303b 100644 --- a/packages/react/src/FormControl/FormControl.module.css +++ b/packages/react/src/FormControl/FormControl.module.css @@ -1,57 +1,59 @@ -.ControlHorizontalLayout { - display: flex; +@layer primer.components.FormControl { + .ControlHorizontalLayout { + display: flex; - &:where([data-has-leading-visual]) { - align-items: center; + &:where([data-has-leading-visual]) { + align-items: center; + } } -} -.ControlVerticalLayout { - display: flex; - flex-direction: column; - align-items: flex-start; + .ControlVerticalLayout { + display: flex; + flex-direction: column; + align-items: flex-start; - & > *:not(label) + * { - margin-top: var(--base-size-4); - } + & > *:not(label) + * { + margin-top: var(--base-size-4); + } - &[data-has-label] > * + * { - margin-top: var(--base-size-4); + &[data-has-label] > * + * { + margin-top: var(--base-size-4); + } } -} - -.ControlChoiceInputs > input { - margin-right: 0; - margin-left: 0; -} -.LabelContainer { - > * { - /* stylelint-disable-next-line primer/spacing */ - padding-left: var(--stack-gap-condensed); + .ControlChoiceInputs > input { + margin-right: 0; + margin-left: 0; } - > label { - font-weight: var(--base-text-weight-normal); + .LabelContainer { + > * { + /* stylelint-disable-next-line primer/spacing */ + padding-left: var(--stack-gap-condensed); + } + + > label { + font-weight: var(--base-text-weight-normal); + } } -} -.LeadingVisual { - margin-left: var(--base-size-8); - color: var(--fgColor-muted); + .LeadingVisual { + margin-left: var(--base-size-8); + color: var(--fgColor-muted); - &:where([data-disabled]) { - color: var(--control-fgColor-disabled); - } + &:where([data-disabled]) { + color: var(--control-fgColor-disabled); + } - > * { - min-width: var(--text-body-size-large); - min-height: var(--text-body-size-large); - fill: currentColor; - } + > * { + min-width: var(--text-body-size-large); + min-height: var(--text-body-size-large); + fill: currentColor; + } - > *:where([data-has-caption]) { - min-width: var(--base-size-24); - min-height: var(--base-size-24); + > *:where([data-has-caption]) { + min-width: var(--base-size-24); + min-height: var(--base-size-24); + } } } diff --git a/packages/react/src/FormControl/FormControlCaption.module.css b/packages/react/src/FormControl/FormControlCaption.module.css index b51b54b53ca..36593ea7e28 100644 --- a/packages/react/src/FormControl/FormControlCaption.module.css +++ b/packages/react/src/FormControl/FormControlCaption.module.css @@ -1,9 +1,11 @@ -.Caption { - display: block; - font-size: var(--text-body-size-small); - color: var(--fgColor-muted); +@layer primer.components.FormControl { + .Caption { + display: block; + font-size: var(--text-body-size-small); + color: var(--fgColor-muted); - &:where([data-control-disabled]) { - color: var(--control-fgColor-disabled); + &:where([data-control-disabled]) { + color: var(--control-fgColor-disabled); + } } } diff --git a/packages/react/src/FormControl/FormControlLeadingVisual.module.css b/packages/react/src/FormControl/FormControlLeadingVisual.module.css index 35c81bfe8d5..b9494be2b35 100644 --- a/packages/react/src/FormControl/FormControlLeadingVisual.module.css +++ b/packages/react/src/FormControl/FormControlLeadingVisual.module.css @@ -1,21 +1,23 @@ -.LeadingVisual { - --leadingVisual-size: 16px; +@layer primer.components.FormControl { + .LeadingVisual { + --leadingVisual-size: 16px; - color: var(--fgColor-default); - display: flex; - align-items: center; + color: var(--fgColor-default); + display: flex; + align-items: center; - &:where([data-control-disabled]) { - color: var(--control-fgColor-disabled); - } + &:where([data-control-disabled]) { + color: var(--control-fgColor-disabled); + } - & > * { - min-width: var(--leadingVisual-size); - min-height: var(--leadingVisual-size); - fill: currentColor; - } + & > * { + min-width: var(--leadingVisual-size); + min-height: var(--leadingVisual-size); + fill: currentColor; + } - &:where([data-has-caption]) { - --leadingVisual-size: 24px; + &:where([data-has-caption]) { + --leadingVisual-size: 24px; + } } } diff --git a/packages/react/src/SelectPanel/SelectPanel.module.css b/packages/react/src/SelectPanel/SelectPanel.module.css index 7fc46e2b1fb..b15461473f5 100644 --- a/packages/react/src/SelectPanel/SelectPanel.module.css +++ b/packages/react/src/SelectPanel/SelectPanel.module.css @@ -1,236 +1,238 @@ -.Overlay { - /* CSS variables values are passed in via styles */ - --max-height: 0; -} +@layer primer.components.SelectPanel { + .Overlay { + /* CSS variables values are passed in via styles */ + --max-height: 0; + } -.Wrapper { - display: flex; - height: inherit; - max-height: inherit; - flex-direction: column; -} + .Wrapper { + display: flex; + height: inherit; + max-height: inherit; + flex-direction: column; + } -.Header { - display: flex; - justify-content: space-between; - align-items: flex-start; - padding-top: var(--base-size-8); - padding-right: var(--base-size-8); - padding-left: var(--base-size-8); + .Header { + display: flex; + justify-content: space-between; + align-items: flex-start; + padding-top: var(--base-size-8); + padding-right: var(--base-size-8); + padding-left: var(--base-size-8); - &:where([data-variant='fullscreen']) { - min-height: 40px; - flex-shrink: 0; + &:where([data-variant='fullscreen']) { + min-height: 40px; + flex-shrink: 0; + } } -} - -.Title { - margin-left: var(--base-size-8); - font-size: var(--text-body-size-medium); -} -.Wrapper[data-variant='modal'] .Title { - margin-top: var(--base-size-8); - /* styling specific to the modal variant */ -} + .Title { + margin-left: var(--base-size-8); + font-size: var(--text-body-size-medium); + } -/* - * Align SelectPanel header text with AnchoredOverlay close button - * - * Ensures the title properly aligns with the close button position - * in anchor variant on narrow viewports. - */ -.Wrapper[data-variant='anchored'] .Title { - @media screen and (--viewportRange-narrow) { + .Wrapper[data-variant='modal'] .Title { margin-top: var(--base-size-8); + /* styling specific to the modal variant */ } -} -.Subtitle { - margin-left: var(--base-size-8); - font-size: var(--text-body-size-small); - color: var(--fgColor-muted); -} - -.Notice { - margin-top: var(--base-size-4); - margin-right: var(--base-size-8); - margin-left: var(--base-size-8); -} + /* + * Align SelectPanel header text with AnchoredOverlay close button + * + * Ensures the title properly aligns with the close button position + * in anchor variant on narrow viewports. + */ + .Wrapper[data-variant='anchored'] .Title { + @media screen and (--viewportRange-narrow) { + margin-top: var(--base-size-8); + } + } -.Notice a { - color: inherit; - text-decoration: underline; -} + .Subtitle { + margin-left: var(--base-size-8); + font-size: var(--text-body-size-small); + color: var(--fgColor-muted); + } -.Notice:where([data-variant='info']) { - color: var(--fgColor-accent); - background-color: var(--bgColor-accent-muted); - border-color: var(--borderColor-accent-muted); -} + .Notice { + margin-top: var(--base-size-4); + margin-right: var(--base-size-8); + margin-left: var(--base-size-8); + } -.Notice:where([data-variant='warning']) { - color: var(--fgColor-attention); - background-color: var(--bgColor-attention-muted); - border-color: var(--borderColor-attention-muted); -} + .Notice a { + color: inherit; + text-decoration: underline; + } -.Notice:where([data-variant='critical']) { - color: var(--fgColor-danger); - background-color: var(--bgColor-danger-muted); - border-color: var(--borderColor-danger-muted); -} + .Notice:where([data-variant='info']) { + color: var(--fgColor-accent); + background-color: var(--bgColor-accent-muted); + border-color: var(--borderColor-accent-muted); + } -.Footer { - display: flex; - padding: var(--base-size-8); - border-top: var(--borderWidth-thin) solid; - border-top-color: var(--borderColor-default); -} + .Notice:where([data-variant='warning']) { + color: var(--fgColor-attention); + background-color: var(--bgColor-attention-muted); + border-color: var(--borderColor-attention-muted); + } -.FilteredActionList { - /* inheriting height and maxHeight ensures that the FilteredActionList is never taller - than the Overlay (which would break scrolling the items) */ - height: inherit; - max-height: inherit; -} + .Notice:where([data-variant='critical']) { + color: var(--fgColor-danger); + background-color: var(--bgColor-danger-muted); + border-color: var(--borderColor-danger-muted); + } -.Message { - display: flex; - height: 100%; - padding: var(--base-size-24); - text-align: center; - flex-direction: column; - justify-content: center; - align-items: center; - flex-grow: 1; - gap: var(--base-size-4); - - a { - color: inherit; - text-decoration: underline; + .Footer { + display: flex; + padding: var(--base-size-8); + border-top: var(--borderWidth-thin) solid; + border-top-color: var(--borderColor-default); } -} -.MessageTitle { - font-size: var(--text-body-size-medium); - font-weight: var(--base-text-weight-semibold); -} + .FilteredActionList { + /* inheriting height and maxHeight ensures that the FilteredActionList is never taller + than the Overlay (which would break scrolling the items) */ + height: inherit; + max-height: inherit; + } -.MessageBody { - font-size: var(--text-body-size-small); - color: var(--fgColor-muted); - align-items: center; - gap: var(--stack-gap-condensed); -} + .Message { + display: flex; + height: 100%; + padding: var(--base-size-24); + text-align: center; + flex-direction: column; + justify-content: center; + align-items: center; + flex-grow: 1; + gap: var(--base-size-4); + + a { + color: inherit; + text-decoration: underline; + } + } -.MessageIcon { - margin-bottom: var(--base-size-8); - color: var(--fgColor-attention); + .MessageTitle { + font-size: var(--text-body-size-medium); + font-weight: var(--base-text-weight-semibold); + } - &:where([data-variant='error']) { - color: var(--fgColor-danger); + .MessageBody { + font-size: var(--text-body-size-small); + color: var(--fgColor-muted); + align-items: center; + gap: var(--stack-gap-condensed); } -} -.MessageAction { - margin-top: var(--base-size-8); -} + .MessageIcon { + margin-bottom: var(--base-size-8); + color: var(--fgColor-attention); -.ResponsiveCloseButton { - display: inline-grid; -} + &:where([data-variant='error']) { + color: var(--fgColor-danger); + } + } -.ResponsiveFooter { - display: none; - align-items: center; - padding: var(--base-size-8); - justify-content: center; + .MessageAction { + margin-top: var(--base-size-8); + } - &:where([data-display-footer='always']) { - display: flex; + .ResponsiveCloseButton { + display: inline-grid; } - &:where([data-display-footer='only-small']) { - @media screen and (--viewportRange-narrow) { + .ResponsiveFooter { + display: none; + align-items: center; + padding: var(--base-size-8); + justify-content: center; + + &:where([data-display-footer='always']) { display: flex; } - } - &[data-stretch-secondary-action='never'] { - justify-content: space-between; - } + &:where([data-display-footer='only-small']) { + @media screen and (--viewportRange-narrow) { + display: flex; + } + } - &:where([data-stretch-secondary-action='only-big']) { - @media screen and (--viewportRange-narrow) { + &[data-stretch-secondary-action='never'] { justify-content: space-between; } - } - - &:where([data-stretch-save-button='only-small']) { - justify-content: space-between; - @media screen and (--viewportRange-narrow) { - justify-content: center; + &:where([data-stretch-secondary-action='only-big']) { + @media screen and (--viewportRange-narrow) { + justify-content: space-between; + } } - } -} -.SecondaryAction { - flex-grow: 1; - align-items: stretch; - display: flex; - justify-content: center; + &:where([data-stretch-save-button='only-small']) { + justify-content: space-between; - &[data-stretch-secondary-action='never'] { - flex-grow: 0; - align-items: flex-start; + @media screen and (--viewportRange-narrow) { + justify-content: center; + } + } } - &:where([data-stretch-secondary-action='only-big']) { - @media screen and (--viewportRange-narrow) { + .SecondaryAction { + flex-grow: 1; + align-items: stretch; + display: flex; + justify-content: center; + + &[data-stretch-secondary-action='never'] { flex-grow: 0; align-items: flex-start; } - } -} -.CancelSaveButtons { - display: flex; - gap: var(--stack-gap-condensed); - justify-content: flex-end; -} - -.ResponsiveCancelSaveButtons { - display: none; + &:where([data-stretch-secondary-action='only-big']) { + @media screen and (--viewportRange-narrow) { + flex-grow: 0; + align-items: flex-start; + } + } + } - @media screen and (--viewportRange-narrow) { + .CancelSaveButtons { display: flex; gap: var(--stack-gap-condensed); justify-content: flex-end; } -} -.ResponsiveSaveButton { - display: none; + .ResponsiveCancelSaveButtons { + display: none; - @media screen and (--viewportRange-narrow) { - display: flex; + @media screen and (--viewportRange-narrow) { + display: flex; + gap: var(--stack-gap-condensed); + justify-content: flex-end; + } } - &:where([data-stretch-save-button='only-small']) { + .ResponsiveSaveButton { + display: none; + @media screen and (--viewportRange-narrow) { - flex-grow: 1; + display: flex; + } + + &:where([data-stretch-save-button='only-small']) { + @media screen and (--viewportRange-narrow) { + flex-grow: 1; + } } } -} -.Backdrop { - position: fixed; - inset: 0; - background-color: var(--overlay-backdrop-bgColor); -} + .Backdrop { + position: fixed; + inset: 0; + background-color: var(--overlay-backdrop-bgColor); + } -.TextInput { - margin: var(--base-size-8); + .TextInput { + margin: var(--base-size-8); + } } diff --git a/packages/react/src/__tests__/css-layers.test.ts b/packages/react/src/__tests__/css-layers.test.ts index 1e234139ba2..3a8b243291a 100644 --- a/packages/react/src/__tests__/css-layers.test.ts +++ b/packages/react/src/__tests__/css-layers.test.ts @@ -99,6 +99,12 @@ const allowlist = new Set([ path.resolve(import.meta.dirname, '../ActionMenu/ActionMenu.module.css'), path.resolve(import.meta.dirname, '../ActionBar/ActionBar.module.css'), path.resolve(import.meta.dirname, '../experimental/SelectPanel2/SelectPanel.module.css'), + path.resolve(import.meta.dirname, '../FilteredActionList/FilteredActionList.module.css'), + path.resolve(import.meta.dirname, '../FilteredActionList/FilteredActionListLoaders.module.css'), + path.resolve(import.meta.dirname, '../FormControl/FormControl.module.css'), + path.resolve(import.meta.dirname, '../FormControl/FormControlCaption.module.css'), + path.resolve(import.meta.dirname, '../FormControl/FormControlLeadingVisual.module.css'), + path.resolve(import.meta.dirname, '../SelectPanel/SelectPanel.module.css'), ]) const files = Array.from(allowlist).map(file => { return [path.relative(path.resolve(import.meta.dirname, '..'), file), file]