diff --git a/.changeset/stack-spacing-options.md b/.changeset/stack-spacing-options.md new file mode 100644 index 00000000000..40610098a23 --- /dev/null +++ b/.changeset/stack-spacing-options.md @@ -0,0 +1,5 @@ +--- +'@primer/react': minor +--- + +**Stack**: Add `tight` (4px) and `cozy` (12px) spacing values to `gap` and `padding` props. Add `paddingBlock` and `paddingInline` props for directional padding control. diff --git a/packages/react/src/Stack/Stack.docs.json b/packages/react/src/Stack/Stack.docs.json index 55c9211662d..ae3c0e1f489 100644 --- a/packages/react/src/Stack/Stack.docs.json +++ b/packages/react/src/Stack/Stack.docs.json @@ -8,7 +8,7 @@ "props": [ { "name": "gap", - "type": "'none' | 'condensed' | 'normal' | 'spacious' | ResponsiveValue<'none' | 'condensed' | 'normal' | 'spacious'>", + "type": "'none' | 'tight' | 'condensed' | 'cozy' | 'normal' | 'spacious' | ResponsiveValue<'none' | 'tight' | 'condensed' | 'cozy' | 'normal' | 'spacious'>", "description": "Specify the gap between children elements in the stack." }, { @@ -33,9 +33,19 @@ }, { "name": "padding", - "type": "'none' | 'condensed' | 'normal' | 'spacious' | ResponsiveValue<'none' | 'condensed' | 'normal' | 'spacious'>", + "type": "'none' | 'tight' | 'condensed' | 'cozy' | 'normal' | 'spacious' | ResponsiveValue<'none' | 'tight' | 'condensed' | 'cozy' | 'normal' | 'spacious'>", "description": "Specify the padding of the stack container." }, + { + "name": "paddingBlock", + "type": "'none' | 'tight' | 'condensed' | 'cozy' | 'normal' | 'spacious' | ResponsiveValue<'none' | 'tight' | 'condensed' | 'cozy' | 'normal' | 'spacious'>", + "description": "Specify the block (vertical) padding of the stack container. Overrides the block axis of padding when both are set." + }, + { + "name": "paddingInline", + "type": "'none' | 'tight' | 'condensed' | 'cozy' | 'normal' | 'spacious' | ResponsiveValue<'none' | 'tight' | 'condensed' | 'cozy' | 'normal' | 'spacious'>", + "description": "Specify the inline (horizontal) padding of the stack container. Overrides the inline axis of padding when both are set." + }, { "name": "className", "type": "string" diff --git a/packages/react/src/Stack/Stack.features.stories.tsx b/packages/react/src/Stack/Stack.features.stories.tsx index 1b0ce0aa736..c94395a6468 100644 --- a/packages/react/src/Stack/Stack.features.stories.tsx +++ b/packages/react/src/Stack/Stack.features.stories.tsx @@ -7,6 +7,66 @@ export default { component: Stack, } as Meta +const Placeholder = ({label}: {label: string}) => ( +
+ {label} +
+) + +export const GapScale = () => ( + + {(['none', 'tight', 'condensed', 'cozy', 'normal', 'spacious'] as const).map(gap => ( + + + gap="{gap}" + + + + + + + + ))} + +) + +export const DirectionalPadding = () => ( + + + + + + + + + + + +) + +export const PaddingScale = () => ( + + {(['none', 'tight', 'condensed', 'cozy', 'normal', 'spacious'] as const).map(padding => ( + + + padding="{padding}" + + + + + + ))} + +) + export const ShrinkingStackItems = () => (
diff --git a/packages/react/src/Stack/Stack.module.css b/packages/react/src/Stack/Stack.module.css index cdb652b07b8..8a18624c8b7 100644 --- a/packages/react/src/Stack/Stack.module.css +++ b/packages/react/src/Stack/Stack.module.css @@ -7,25 +7,44 @@ &[data-padding='none'], &[data-padding-narrow='none'] { - padding: 0; + padding-block: 0; + padding-inline: 0; + } + + &[data-padding='tight'], + &[data-padding-narrow='tight'] { + padding-block: var(--base-size-4); + padding-inline: var(--base-size-4); } &[data-padding='condensed'], &[data-padding-narrow='condensed'] { /* stylelint-disable-next-line primer/spacing */ - padding: var(--stack-padding-condensed); + padding-block: var(--stack-padding-condensed); + /* stylelint-disable-next-line primer/spacing */ + padding-inline: var(--stack-padding-condensed); + } + + &[data-padding='cozy'], + &[data-padding-narrow='cozy'] { + padding-block: var(--base-size-12); + padding-inline: var(--base-size-12); } &[data-padding='normal'], &[data-padding-narrow='normal'] { /* stylelint-disable-next-line primer/spacing */ - padding: var(--stack-padding-normal); + padding-block: var(--stack-padding-normal); + /* stylelint-disable-next-line primer/spacing */ + padding-inline: var(--stack-padding-normal); } &[data-padding='spacious'], &[data-padding-narrow='spacious'] { /* stylelint-disable-next-line primer/spacing */ - padding: var(--stack-padding-spacious); + padding-block: var(--stack-padding-spacious); + /* stylelint-disable-next-line primer/spacing */ + padding-inline: var(--stack-padding-spacious); } &[data-direction='horizontal'], @@ -43,11 +62,21 @@ --stack-gap: 0; } + &[data-gap='tight'], + &[data-gap-narrow='tight'] { + --stack-gap: var(--base-size-4); + } + &[data-gap='condensed'], &[data-gap-narrow='condensed'] { --stack-gap: var(--stack-gap-condensed); } + &[data-gap='cozy'], + &[data-gap-narrow='cozy'] { + --stack-gap: var(--base-size-12); + } + &[data-gap='normal'], &[data-gap-narrow='normal'] { --stack-gap: var(--stack-gap-normal); @@ -113,24 +142,107 @@ flex-wrap: nowrap; } + &[data-padding-block='none'], + &[data-padding-block-narrow='none'] { + padding-block: 0; + } + + &[data-padding-block='tight'], + &[data-padding-block-narrow='tight'] { + padding-block: var(--base-size-4); + } + + &[data-padding-block='condensed'], + &[data-padding-block-narrow='condensed'] { + /* stylelint-disable-next-line primer/spacing */ + padding-block: var(--stack-padding-condensed); + } + + &[data-padding-block='cozy'], + &[data-padding-block-narrow='cozy'] { + padding-block: var(--base-size-12); + } + + &[data-padding-block='normal'], + &[data-padding-block-narrow='normal'] { + /* stylelint-disable-next-line primer/spacing */ + padding-block: var(--stack-padding-normal); + } + + &[data-padding-block='spacious'], + &[data-padding-block-narrow='spacious'] { + /* stylelint-disable-next-line primer/spacing */ + padding-block: var(--stack-padding-spacious); + } + + &[data-padding-inline='none'], + &[data-padding-inline-narrow='none'] { + padding-inline: 0; + } + + &[data-padding-inline='tight'], + &[data-padding-inline-narrow='tight'] { + padding-inline: var(--base-size-4); + } + + &[data-padding-inline='condensed'], + &[data-padding-inline-narrow='condensed'] { + /* stylelint-disable-next-line primer/spacing */ + padding-inline: var(--stack-padding-condensed); + } + + &[data-padding-inline='cozy'], + &[data-padding-inline-narrow='cozy'] { + padding-inline: var(--base-size-12); + } + + &[data-padding-inline='normal'], + &[data-padding-inline-narrow='normal'] { + /* stylelint-disable-next-line primer/spacing */ + padding-inline: var(--stack-padding-normal); + } + + &[data-padding-inline='spacious'], + &[data-padding-inline-narrow='spacious'] { + /* stylelint-disable-next-line primer/spacing */ + padding-inline: var(--stack-padding-spacious); + } + @media (--viewportRange-regular) { &[data-padding-regular='none'] { - padding: 0; + padding-block: 0; + padding-inline: 0; + } + + &[data-padding-regular='tight'] { + padding-block: var(--base-size-4); + padding-inline: var(--base-size-4); } &[data-padding-regular='condensed'] { /* stylelint-disable-next-line primer/spacing */ - padding: var(--stack-padding-condensed); + padding-block: var(--stack-padding-condensed); + /* stylelint-disable-next-line primer/spacing */ + padding-inline: var(--stack-padding-condensed); + } + + &[data-padding-regular='cozy'] { + padding-block: var(--base-size-12); + padding-inline: var(--base-size-12); } &[data-padding-regular='normal'] { /* stylelint-disable-next-line primer/spacing */ - padding: var(--stack-padding-normal); + padding-block: var(--stack-padding-normal); + /* stylelint-disable-next-line primer/spacing */ + padding-inline: var(--stack-padding-normal); } &[data-padding-regular='spacious'] { /* stylelint-disable-next-line primer/spacing */ - padding: var(--stack-padding-spacious); + padding-block: var(--stack-padding-spacious); + /* stylelint-disable-next-line primer/spacing */ + padding-inline: var(--stack-padding-spacious); } &[data-direction-regular='horizontal'] { @@ -145,10 +257,18 @@ --stack-gap: 0; } + &[data-gap-regular='tight'] { + --stack-gap: var(--base-size-4); + } + &[data-gap-regular='condensed'] { --stack-gap: var(--stack-gap-condensed); } + &[data-gap-regular='cozy'] { + --stack-gap: var(--base-size-12); + } + &[data-gap-regular='normal'] { --stack-gap: var(--stack-gap-normal); } @@ -200,26 +320,97 @@ &[data-wrap-regular='nowrap'] { flex-wrap: nowrap; } + + &[data-padding-block-regular='none'] { + padding-block: 0; + } + + &[data-padding-block-regular='tight'] { + padding-block: var(--base-size-4); + } + + &[data-padding-block-regular='condensed'] { + /* stylelint-disable-next-line primer/spacing */ + padding-block: var(--stack-padding-condensed); + } + + &[data-padding-block-regular='cozy'] { + padding-block: var(--base-size-12); + } + + &[data-padding-block-regular='normal'] { + /* stylelint-disable-next-line primer/spacing */ + padding-block: var(--stack-padding-normal); + } + + &[data-padding-block-regular='spacious'] { + /* stylelint-disable-next-line primer/spacing */ + padding-block: var(--stack-padding-spacious); + } + + &[data-padding-inline-regular='none'] { + padding-inline: 0; + } + + &[data-padding-inline-regular='tight'] { + padding-inline: var(--base-size-4); + } + + &[data-padding-inline-regular='condensed'] { + /* stylelint-disable-next-line primer/spacing */ + padding-inline: var(--stack-padding-condensed); + } + + &[data-padding-inline-regular='cozy'] { + padding-inline: var(--base-size-12); + } + + &[data-padding-inline-regular='normal'] { + /* stylelint-disable-next-line primer/spacing */ + padding-inline: var(--stack-padding-normal); + } + + &[data-padding-inline-regular='spacious'] { + /* stylelint-disable-next-line primer/spacing */ + padding-inline: var(--stack-padding-spacious); + } } @media (--viewportRange-wide) { &[data-padding-wide='none'] { - padding: 0; + padding-block: 0; + padding-inline: 0; + } + + &[data-padding-wide='tight'] { + padding-block: var(--base-size-4); + padding-inline: var(--base-size-4); } &[data-padding-wide='condensed'] { /* stylelint-disable-next-line primer/spacing */ - padding: var(--stack-padding-condensed); + padding-block: var(--stack-padding-condensed); + /* stylelint-disable-next-line primer/spacing */ + padding-inline: var(--stack-padding-condensed); + } + + &[data-padding-wide='cozy'] { + padding-block: var(--base-size-12); + padding-inline: var(--base-size-12); } &[data-padding-wide='normal'] { /* stylelint-disable-next-line primer/spacing */ - padding: var(--stack-padding-normal); + padding-block: var(--stack-padding-normal); + /* stylelint-disable-next-line primer/spacing */ + padding-inline: var(--stack-padding-normal); } &[data-padding-wide='spacious'] { /* stylelint-disable-next-line primer/spacing */ - padding: var(--stack-padding-spacious); + padding-block: var(--stack-padding-spacious); + /* stylelint-disable-next-line primer/spacing */ + padding-inline: var(--stack-padding-spacious); } &[data-direction-wide='horizontal'] { @@ -234,10 +425,18 @@ --stack-gap: 0; } + &[data-gap-wide='tight'] { + --stack-gap: var(--base-size-4); + } + &[data-gap-wide='condensed'] { --stack-gap: var(--stack-gap-condensed); } + &[data-gap-wide='cozy'] { + --stack-gap: var(--base-size-12); + } + &[data-gap-wide='normal'] { --stack-gap: var(--stack-gap-normal); } @@ -289,6 +488,60 @@ &[data-wrap-wide='nowrap'] { flex-wrap: nowrap; } + + &[data-padding-block-wide='none'] { + padding-block: 0; + } + + &[data-padding-block-wide='tight'] { + padding-block: var(--base-size-4); + } + + &[data-padding-block-wide='condensed'] { + /* stylelint-disable-next-line primer/spacing */ + padding-block: var(--stack-padding-condensed); + } + + &[data-padding-block-wide='cozy'] { + padding-block: var(--base-size-12); + } + + &[data-padding-block-wide='normal'] { + /* stylelint-disable-next-line primer/spacing */ + padding-block: var(--stack-padding-normal); + } + + &[data-padding-block-wide='spacious'] { + /* stylelint-disable-next-line primer/spacing */ + padding-block: var(--stack-padding-spacious); + } + + &[data-padding-inline-wide='none'] { + padding-inline: 0; + } + + &[data-padding-inline-wide='tight'] { + padding-inline: var(--base-size-4); + } + + &[data-padding-inline-wide='condensed'] { + /* stylelint-disable-next-line primer/spacing */ + padding-inline: var(--stack-padding-condensed); + } + + &[data-padding-inline-wide='cozy'] { + padding-inline: var(--base-size-12); + } + + &[data-padding-inline-wide='normal'] { + /* stylelint-disable-next-line primer/spacing */ + padding-inline: var(--stack-padding-normal); + } + + &[data-padding-inline-wide='spacious'] { + /* stylelint-disable-next-line primer/spacing */ + padding-inline: var(--stack-padding-spacious); + } } } diff --git a/packages/react/src/Stack/Stack.tsx b/packages/react/src/Stack/Stack.tsx index 75a4bbc383e..9a96c6e71f6 100644 --- a/packages/react/src/Stack/Stack.tsx +++ b/packages/react/src/Stack/Stack.tsx @@ -6,7 +6,7 @@ import classes from './Stack.module.css' import {clsx} from 'clsx' import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic' -type GapScale = 'none' | 'condensed' | 'normal' | 'spacious' +type GapScale = 'none' | 'tight' | 'condensed' | 'cozy' | 'normal' | 'spacious' type Gap = GapScale | ResponsiveValue type DirectionScale = 'horizontal' | 'vertical' @@ -21,7 +21,7 @@ type Wrap = WrapScale | ResponsiveValue type JustifyScale = 'start' | 'center' | 'end' | 'space-between' | 'space-evenly' type Justify = JustifyScale | ResponsiveValue -type PaddingScale = 'none' | 'condensed' | 'normal' | 'spacious' +type PaddingScale = 'none' | 'tight' | 'condensed' | 'cozy' | 'normal' | 'spacious' type Padding = PaddingScale | ResponsiveValue type StackProps = React.PropsWithChildren<{ @@ -64,6 +64,19 @@ type StackProps = React.PropsWithChildren<{ * @default none */ padding?: Padding + + /** + * Specify the block (vertical) padding of the stack container. + * Overrides the block axis of `padding` when both are set. + */ + paddingBlock?: Padding + + /** + * Specify the inline (horizontal) padding of the stack container. + * Overrides the inline axis of `padding` when both are set. + */ + paddingInline?: Padding + className?: string }> @@ -77,6 +90,8 @@ const Stack = forwardRef( gap, justify = 'start', padding = 'none', + paddingBlock, + paddingInline, wrap = 'nowrap', className, ...rest @@ -94,6 +109,8 @@ const Stack = forwardRef( {...getResponsiveAttributes('wrap', wrap)} {...getResponsiveAttributes('justify', justify)} {...getResponsiveAttributes('padding', padding)} + {...getResponsiveAttributes('padding-block', paddingBlock)} + {...getResponsiveAttributes('padding-inline', paddingInline)} > {children} diff --git a/packages/react/src/Stack/__tests__/Stack.test.tsx b/packages/react/src/Stack/__tests__/Stack.test.tsx index 92527d86101..4a04ccb0718 100644 --- a/packages/react/src/Stack/__tests__/Stack.test.tsx +++ b/packages/react/src/Stack/__tests__/Stack.test.tsx @@ -110,12 +110,16 @@ describe('Stack', () => { it('should support specifying the stack gap with the `gap` prop', () => { render( <> + + , ) + expect(screen.getByTestId('tight')).toHaveAttribute('data-gap', 'tight') expect(screen.getByTestId('condensed')).toHaveAttribute('data-gap', 'condensed') + expect(screen.getByTestId('cozy')).toHaveAttribute('data-gap', 'cozy') expect(screen.getByTestId('normal')).toHaveAttribute('data-gap', 'normal') expect(screen.getByTestId('spacious')).toHaveAttribute('data-gap', 'spacious') }) @@ -177,12 +181,16 @@ describe('Stack', () => { it('should support specifying the stack padding with the `padding` prop', () => { render( <> + + , ) + expect(screen.getByTestId('tight')).toHaveAttribute('data-padding', 'tight') expect(screen.getByTestId('condensed')).toHaveAttribute('data-padding', 'condensed') + expect(screen.getByTestId('cozy')).toHaveAttribute('data-padding', 'cozy') expect(screen.getByTestId('normal')).toHaveAttribute('data-padding', 'normal') expect(screen.getByTestId('spacious')).toHaveAttribute('data-padding', 'spacious') }) @@ -202,6 +210,83 @@ describe('Stack', () => { expect(screen.getByTestId('responsive')).toHaveAttribute('data-padding-regular', 'condensed') expect(screen.getByTestId('responsive')).toHaveAttribute('data-padding-wide', 'spacious') }) + + it('should render both padding and paddingBlock/paddingInline attributes when combined', () => { + render() + expect(screen.getByTestId('combined')).toHaveAttribute('data-padding', 'normal') + expect(screen.getByTestId('combined')).toHaveAttribute('data-padding-block', 'condensed') + expect(screen.getByTestId('combined')).toHaveAttribute('data-padding-inline', 'spacious') + }) + }) + + describe('paddingBlock', () => { + it('should support specifying the block padding with the `paddingBlock` prop', () => { + render( + <> + + + + + + , + ) + expect(screen.getByTestId('tight')).toHaveAttribute('data-padding-block', 'tight') + expect(screen.getByTestId('condensed')).toHaveAttribute('data-padding-block', 'condensed') + expect(screen.getByTestId('cozy')).toHaveAttribute('data-padding-block', 'cozy') + expect(screen.getByTestId('normal')).toHaveAttribute('data-padding-block', 'normal') + expect(screen.getByTestId('spacious')).toHaveAttribute('data-padding-block', 'spacious') + }) + + it('should support responsive `paddingBlock` values', () => { + render( + , + ) + expect(screen.getByTestId('responsive')).toHaveAttribute('data-padding-block-narrow', 'none') + expect(screen.getByTestId('responsive')).toHaveAttribute('data-padding-block-regular', 'condensed') + expect(screen.getByTestId('responsive')).toHaveAttribute('data-padding-block-wide', 'spacious') + }) + }) + + describe('paddingInline', () => { + it('should support specifying the inline padding with the `paddingInline` prop', () => { + render( + <> + + + + + + , + ) + expect(screen.getByTestId('tight')).toHaveAttribute('data-padding-inline', 'tight') + expect(screen.getByTestId('condensed')).toHaveAttribute('data-padding-inline', 'condensed') + expect(screen.getByTestId('cozy')).toHaveAttribute('data-padding-inline', 'cozy') + expect(screen.getByTestId('normal')).toHaveAttribute('data-padding-inline', 'normal') + expect(screen.getByTestId('spacious')).toHaveAttribute('data-padding-inline', 'spacious') + }) + + it('should support responsive `paddingInline` values', () => { + render( + , + ) + expect(screen.getByTestId('responsive')).toHaveAttribute('data-padding-inline-narrow', 'tight') + expect(screen.getByTestId('responsive')).toHaveAttribute('data-padding-inline-regular', 'normal') + expect(screen.getByTestId('responsive')).toHaveAttribute('data-padding-inline-wide', 'spacious') + }) }) describe('wrap', () => {