Skip to content

Commit 79fa2d6

Browse files
authored
refactor: simplify Button CSS by removing TypographyNext and using explicit heights (#4514)
* refactor: ♻️ refactor button w/o TypographyNext * refactor: remove text padding for alignment * refactor: 💄 update css to use hardcoded height (TODO: UPDATE W TOKEN) * fix: 🚧 use TypographyNext inside Button (small label not aligned - WIP) * fix(eds-core-react): use min-height instead of height for button sizing Replace span height: round(1cap, 4px) with min-height-only approach. In @supports, remove padding-block (instead of min-height) so flexbox centers the span's natural line-height within the min-height box. Improves text centering from -1px to -0.5px max offset. * refactor(eds-core-react): remove padding-block fallback and @supports block Cap unit is supported in all major browsers for 2+ years — no need for padding-block fallback or @supports feature gate.
1 parent 83ee0f0 commit 79fa2d6

5 files changed

Lines changed: 57 additions & 223 deletions

File tree

.claude/settings.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313
"Bash(*.env*)",
1414
"Read(./id_rsa*)",
1515
"Read(./**/*.pem)",
16-
"Read(./**/*.key)"
16+
"Read(./**/*.key)",
17+
"Read(./secrets/**)",
18+
"Read(./config/credentials.json)",
19+
"Read(./build)"
1720
]
1821
},
1922
"hooks": {

packages/eds-core-react/src/components/next/Button/Button.test.tsx

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -132,14 +132,10 @@ describe('Button (next)', () => {
132132
expect(screen.getAllByTestId('mock-icon')).toHaveLength(2)
133133
})
134134

135-
it('wraps text children in typography', () => {
135+
it('renders text children directly', () => {
136136
render(<Button>Text Content</Button>)
137137
const button = screen.getByRole('button')
138-
// Text should be wrapped in a span (TypographyNext)
139-
// eslint-disable-next-line testing-library/no-node-access
140-
const span = button.querySelector('span')
141-
expect(span).toBeInTheDocument()
142-
expect(span).toHaveTextContent('Text Content')
138+
expect(button).toHaveTextContent('Text Content')
143139
})
144140

145141
it('preserves child order', () => {
@@ -151,12 +147,8 @@ describe('Button (next)', () => {
151147
</Button>,
152148
)
153149
const button = screen.getByRole('button')
154-
// Children are wrapped in TypographyNext span
155-
// eslint-disable-next-line testing-library/no-node-access
156-
const span = button.children[0]
157-
expect(span).toHaveTextContent('Middle')
150+
expect(button).toHaveTextContent('Middle')
158151

159-
// Get all icons using data-testid
160152
const icons = screen.getAllByTestId('mock-icon')
161153
expect(icons).toHaveLength(2)
162154
})

packages/eds-core-react/src/components/next/Button/Button.tsx

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { forwardRef } from 'react'
2-
import { TypographyNext } from '../../Typography'
32
import type { ButtonProps } from './Button.types'
3+
import { TypographyNext } from '../../Typography'
44

55
const SIZE_MAPPING = {
66
small: 'sm',
@@ -40,22 +40,24 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
4040
data-variant={variant}
4141
data-selectable-space={selectableSpace}
4242
data-space-proportions="squished"
43-
data-font-family="ui"
44-
data-font-size={typographySize}
45-
data-line-height="squished"
4643
data-color-appearance={disabled ? 'neutral' : tone}
4744
data-icon-only={icon || undefined}
4845
data-round={icon && round ? true : undefined}
4946
{...rest}
5047
>
51-
<TypographyNext
52-
family="ui"
53-
size={typographySize}
54-
baseline="center"
55-
lineHeight="squished"
56-
>
57-
{children}
58-
</TypographyNext>
48+
{icon ? (
49+
children
50+
) : (
51+
<TypographyNext
52+
as="span"
53+
family="ui"
54+
size={typographySize}
55+
lineHeight="squished"
56+
baseline="center"
57+
>
58+
{children}
59+
</TypographyNext>
60+
)}
5961
</button>
6062
)
6163
},

packages/eds-core-react/src/components/next/Button/__snapshots__/Button.test.tsx.snap

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@ exports[`Button (next) Rendering matches snapshot 1`] = `
55
<button
66
class="eds-button"
77
data-color-appearance="accent"
8-
data-font-family="ui"
9-
data-font-size="md"
10-
data-line-height="squished"
118
data-selectable-space="md"
129
data-space-proportions="squished"
1310
data-variant="primary"

packages/eds-core-react/src/components/next/Button/button.css

Lines changed: 36 additions & 196 deletions
Original file line numberDiff line numberDiff line change
@@ -18,125 +18,55 @@
1818
background-color 150ms ease-in-out,
1919
border-color 150ms ease-in-out,
2020
color 150ms ease-in-out;
21-
22-
/* Comfortable density asymmetric padding for visual text centering */
23-
/* These values compensate for font metrics (ascenders/descenders) */
24-
/* Empirically determined from Figma design */
25-
--_comfortable-padding-top: 0.313rem; /* 5px */
26-
--_comfortable-padding-bottom: 0.188rem; /* 3px */
27-
28-
/* Comfortable density icon-only padding to match label button heights */
29-
/* Formula: (target-height - icon-size) / 2 where target-height = label button height */
30-
/* Empirically determined from Figma design */
31-
--_comfortable-icon-padding-sm: 0.125rem; /* 2px: (20px - 16px) / 2 */
32-
--_comfortable-icon-padding-md: 0.188rem; /* 3px: (24px - 18px) / 2 */
33-
--_comfortable-icon-padding-lg: 0.5rem; /* 8px: (36px - 20px) / 2 */
3421
}
3522

36-
/* ===== SIZE VARIANTS (Button with label) ===== */
23+
/* ===== SIZE VARIANTS ===== */
3724
/*
38-
* Spacing adjusted from design tokens to compensate for icon negative margins
39-
* Icons have margin-block: -0.25em and margin-inline: -0.1em for optical alignment
40-
*
41-
* Token reference (from --eds-selectable-space-*):
42-
* - lg: horizontal=20px, vertical=16px (spacious) / 12px (comfortable)
43-
* - md: horizontal=16px, vertical=12px (spacious) / 8px (comfortable)
44-
* - sm: horizontal=12px, vertical=8px (spacious) / 6px (comfortable)
25+
* Button height is controlled by min-height.
26+
* The span keeps its natural line-height and is centered by flexbox.
4527
*
46-
* Adjustments made:
47-
* - Vertical padding reduced by ~4px to account for icon negative margins
48-
* - Gap values slightly adjusted for visual balance with icons
28+
* Spacious (default): sm=24px, md=36px, lg=44px
29+
* Comfortable: sm=20px, md=24px, lg=36px
4930
*/
5031

5132
.eds-button[data-selectable-space='lg'] {
52-
/* Base from token (16px spacious / 12px comfortable) → Adjusted: -4px vertical for icon margins */
53-
padding-block: calc(var(--eds-selectable-space-vertical) - 0.25rem); /* 16px → 12px / 12px → 8px */
54-
padding-inline: var(--eds-selectable-space-horizontal); /* 20px spacious / 16px comfortable */
33+
min-height: 2.75rem; /* 44px */
34+
padding-inline: var(--eds-selectable-space-horizontal);
5535
gap: var(--eds-typography-gap-horizontal);
5636
}
5737

5838
.eds-button[data-selectable-space='md'] {
59-
/* Base from token (12px spacious / 8px comfortable) → Adjusted: -2px vertical for icon margins */
60-
padding-block: calc(var(--eds-selectable-space-vertical) - 0.125rem); /* 12px → 10px / 8px → 6px */
61-
padding-inline: var(--eds-selectable-space-horizontal); /* 16px spacious / 12px comfortable */
39+
min-height: 2.25rem; /* 36px */
40+
padding-inline: var(--eds-selectable-space-horizontal);
6241
gap: var(--eds-typography-gap-horizontal);
6342
}
6443

6544
.eds-button[data-selectable-space='sm'] {
66-
/* Base from token (8px spacious / 6px comfortable) → Adjusted: -2px vertical for icon margins */
67-
padding-block: calc(var(--eds-selectable-space-vertical) - 0.125rem); /* 8px → 6px / 6px → 4px */
68-
padding-inline: var(--eds-selectable-space-horizontal); /* 12px spacious / 8px comfortable */
45+
min-height: 1.5rem; /* 24px */
46+
padding-inline: var(--eds-selectable-space-horizontal);
6947
gap: var(--eds-typography-gap-horizontal);
7048
}
7149

72-
/* ===== DENSITY OVERRIDES ===== */
73-
/*
74-
* Note: Density-specific overrides are defined OUTSIDE @layer (after this block closes)
75-
* to ensure they can override the typography tokens from @equinor/eds-tokens
76-
*/
50+
/* Comfortable density */
7751

78-
/* ===== ICON-ONLY BUTTON ===== */
79-
/*
80-
* Icon-only buttons maintain same height as label buttons but adapt to density.
81-
* Formula: padding = base-padding + (line-height - icon-size) / 2
82-
*
83-
* This ensures icon buttons:
84-
* - Match label button height in both spacious and comfortable density
85-
* - Center the icon vertically regardless of density mode
86-
* - Adapt automatically via --eds-typography-line-height-squished token
87-
*
88-
* Density behavior:
89-
* - Spacious: Uses larger line-height → more padding → taller buttons
90-
* - Comfortable: Uses smaller line-height → less padding → shorter buttons
91-
*/
92-
93-
.eds-button[data-icon-only] {
94-
aspect-ratio: 1;
52+
[data-density='comfortable'] .eds-button[data-selectable-space='lg'] {
53+
min-height: 2.25rem; /* 36px */
9554
}
9655

97-
.eds-button[data-icon-only][data-selectable-space='lg'] {
98-
/* Base padding from token minus icon compensation: (16px - 4px) = 12px spacious / (12px - 4px) = 8px comfortable */
99-
/* Adapts with line-height (20px spacious / 16px comfortable) */
100-
/* Target height: 44px spacious / 40px comfortable */
101-
--_base-padding: calc(var(--eds-selectable-space-vertical) - 0.25rem);
102-
padding: calc(
103-
var(--_base-padding) +
104-
(
105-
var(--eds-typography-line-height-squished) -
106-
var(--eds-typography-icon-size)
107-
) /
108-
2
109-
);
56+
[data-density='comfortable'] .eds-button[data-selectable-space='md'] {
57+
min-height: 1.5rem; /* 24px */
11058
}
11159

112-
.eds-button[data-icon-only][data-selectable-space='md'] {
113-
/* Base padding from token minus icon compensation: (12px - 2px) = 10px spacious / (8px - 2px) = 6px comfortable */
114-
/* Adapts with line-height (16px spacious / 12px comfortable) */
115-
/* Target height: 36px spacious / 32px comfortable */
116-
--_base-padding: calc(var(--eds-selectable-space-vertical) - 0.125rem);
117-
padding: calc(
118-
var(--_base-padding) +
119-
(
120-
var(--eds-typography-line-height-squished) -
121-
var(--eds-typography-icon-size)
122-
) /
123-
2
124-
);
60+
[data-density='comfortable'] .eds-button[data-selectable-space='sm'] {
61+
min-height: 1.25rem; /* 20px */
12562
}
12663

127-
.eds-button[data-icon-only][data-selectable-space='sm'] {
128-
/* Base padding from token minus icon compensation: (8px - 2px) = 6px spacious / (6px - 2px) = 4px comfortable */
129-
/* Line-height (12px) same for both densities */
130-
/* Target height: 24px (consistent across densities) */
131-
--_base-padding: calc(var(--eds-selectable-space-vertical) - 0.125rem);
132-
padding: calc(
133-
var(--_base-padding) +
134-
(
135-
var(--eds-typography-line-height-squished) -
136-
var(--eds-typography-icon-size)
137-
) /
138-
2
139-
);
64+
/* ===== ICON-ONLY BUTTON ===== */
65+
/* Height from size variant, aspect-ratio makes it square, flexbox centers icon */
66+
67+
.eds-button[data-icon-only] {
68+
aspect-ratio: 1;
69+
padding: 0;
14070
}
14171

14272
/* ===== ROUND ICON-ONLY BUTTON ===== */
@@ -232,116 +162,26 @@
232162
flex-shrink: 0;
233163
}
234164

235-
/* Icon-only buttons: remove negative margins, use padding for sizing */
165+
/* Icon-only buttons: remove negative margins since flexbox handles centering */
236166
.eds-button[data-icon-only] .icon {
237167
margin: 0;
238168
}
239169
}
240170

241-
/* ===== LABEL WRAPPER (TypographyNext) ===== */
242-
/* Outside @layer to override typography.css which sets display: block on [data-font-family] */
243-
244-
.eds-button {
245-
display: inline-flex;
246-
}
247-
248-
.eds-button > span {
249-
display: inline-flex;
250-
align-items: center;
251-
gap: inherit;
252-
}
253-
254-
/* Reset baseline padding and text-box-trim for proper centering */
255-
.eds-button > span[data-baseline] {
256-
padding: 0;
257-
/* Disable text-box-trim so text sits centered within its line-height box */
258-
/* Flexbox will then center the entire line-height box vertically in the button */
259-
text-box-trim: none;
260-
text-box-edge: auto;
261-
}
262-
263-
/* Icon-only: span should not affect layout */
264-
.eds-button[data-icon-only] > span {
265-
display: contents;
266-
}
267-
268-
/* ===== DENSITY OVERRIDES ===== */
269171
/*
270-
* Comfortable density uses smaller font-sizes (one size down) and tighter spacing.
172+
* Outside @layer to override typography.css [data-font-family] { display: block }.
173+
* The span wraps text children via TypographyNext and needs inline-flex for
174+
* icon+text gap and vertical centering.
271175
*
272-
* Font-size mapping (from Figma):
273-
* - small: XS (10.5px) instead of SM (12px)
274-
* - default: SM (12px) instead of MD (14px)
275-
* - large: MD (14px) instead of LG (16px)
276-
*
277-
* Target heights in comfortable mode:
278-
* - small: 20px (4px × 2 + 12px line-height)
279-
* - default: 24px (4px × 2 + 16px line-height)
280-
* - large: 36px (8px × 2 + 20px line-height)
281-
*
282-
* These overrides are OUTSIDE @layer to ensure they can override the typography
283-
* tokens from @equinor/eds-tokens/css/variables.
176+
* text-box/padding-block reset disables typography.css baseline padding —
177+
* button handles its own sizing via min-height + flexbox centering.
284178
*/
285179

286-
[data-density='comfortable'] .eds-button[data-selectable-space='sm'][data-font-size='sm'],
287-
[data-density='comfortable'] .eds-button[data-selectable-space='sm'] [data-font-size='sm'] {
288-
/* Override line-height to maintain SM height (12px instead of XS's 12px) */
289-
--eds-typography-line-height: 0.75rem; /* 12px (SM line-height) */
290-
--eds-typography-line-height-squished: 0.75rem; /* 12px */
291-
}
292-
293-
[data-density='comfortable'] .eds-button[data-selectable-space='sm'][data-font-size='sm'] {
294-
/* Target: 20px = (5px + 3px) + 12px line-height */
295-
/* Asymmetric padding to visually center text (more top, less bottom) */
296-
padding-top: var(--_comfortable-padding-top);
297-
padding-bottom: var(--_comfortable-padding-bottom);
298-
}
299-
300-
[data-density='comfortable'] .eds-button[data-selectable-space='md'][data-font-size='md'],
301-
[data-density='comfortable'] .eds-button[data-selectable-space='md'] [data-font-size='md'] {
302-
/* Override line-height to maintain MD height (16px instead of SM's 12px) */
303-
--eds-typography-line-height: 1rem; /* 16px (MD line-height, NOT SM) */
304-
--eds-typography-line-height-squished: 1rem; /* 16px */
305-
}
306-
307-
[data-density='comfortable'] .eds-button[data-selectable-space='md'][data-font-size='md'] {
308-
/* Target: 24px = (5px + 3px) + 16px line-height */
309-
/* Asymmetric padding to visually center text (more top, less bottom) */
310-
padding-top: var(--_comfortable-padding-top);
311-
padding-bottom: var(--_comfortable-padding-bottom);
312-
}
313-
314-
[data-density='comfortable'] .eds-button[data-selectable-space='lg'][data-font-size='lg'],
315-
[data-density='comfortable'] .eds-button[data-selectable-space='lg'] [data-font-size='lg'] {
316-
/* Override line-height to maintain LG height (20px instead of MD's 16px) */
317-
--eds-typography-line-height: 1.25rem; /* 20px (LG line-height, NOT MD) */
318-
--eds-typography-line-height-squished: 1.25rem; /* 20px */
319-
}
320-
321-
[data-density='comfortable'] .eds-button[data-selectable-space='lg'][data-font-size='lg'] {
322-
/* Target: 36px = (8px × 2) + 20px */
323-
/* Token gives 12px in comfortable mode, minus icon compensation = 8px */
324-
padding-block: calc(var(--eds-selectable-space-vertical) - 0.25rem); /* 12px → 8px */
325-
}
326-
327-
/* Icon-only button overrides for comfortable density */
328-
/* Set fixed padding to ensure square buttons that match labeled button heights */
329-
/* Formula: padding = (target-height - icon-size) / 2 */
330-
331-
[data-density='comfortable'] .eds-button[data-icon-only][data-selectable-space='sm'] {
332-
/* Target: 20×20px to match labeled small button (20px height) */
333-
/* Icon size: 16px, padding needed: (20 - 16) / 2 = 2px */
334-
padding: var(--_comfortable-icon-padding-sm);
335-
}
336-
337-
[data-density='comfortable'] .eds-button[data-icon-only][data-selectable-space='md'] {
338-
/* Target: 24×24px to match labeled default button (24px height) */
339-
/* Icon size: 18px, padding needed: (24 - 18) / 2 = 3px */
340-
padding: var(--_comfortable-icon-padding-md);
180+
.eds-button > span {
181+
display: inline-flex;
182+
align-items: center;
183+
gap: var(--eds-typography-gap-horizontal);
184+
text-box: normal;
185+
padding-block: 0;
341186
}
342187

343-
[data-density='comfortable'] .eds-button[data-icon-only][data-selectable-space='lg'] {
344-
/* Target: 36×36px to match labeled large button (36px height) */
345-
/* Icon size: 20px, padding needed: (36 - 20) / 2 = 8px */
346-
padding: var(--_comfortable-icon-padding-lg);
347-
}

0 commit comments

Comments
 (0)