Skip to content

Commit ff41418

Browse files
committed
style(eds-core-react): 🎨 Input - implement Figma EDS 2.0 design + refactor to invalid prop
- Replace box-shadow underline with 1px border + rounded corners - Implement 2px focus ring using outline with -2px offset (no layout shift) - Use squished line-height for better vertical text centering - Fix hover to exclude disabled and readonly states - Ensure text always uses strong color (not affected by validation state) - Force adornments to neutral appearance - Force disabled inputs to neutral appearance with subtle text BREAKING: Refactor variant to invalid prop - Remove success and warning variants (only error/invalid remains) - Change API from variant="error" to invalid (boolean) - More semantic and aligns with HTML5 form validation - Prepares for future ValidationMessage component architecture - Add comprehensive Light & Dark Mode story - Update all stories, documentation, and tests - Rename "Variants" to "Validation States" throughout
1 parent 79f9cfc commit ff41418

File tree

6 files changed

+193
-135
lines changed

6 files changed

+193
-135
lines changed

‎packages/eds-core-react/src/components/Input/Input.new.docs.mdx‎

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,14 @@ This component uses the **EDS 2.0 token system** with fixed spacing values:
3838
- **Proportions:** Stretched (50% more vertical padding)
3939
- **Font size:** LG (16px)
4040

41-
Control the visual state using the `variant` prop:
41+
Control the validation state using the `invalid` prop:
4242

4343
```tsx
4444
{/* Default - neutral appearance */}
4545
<InputNew />
4646

47-
{/* Different variants */}
48-
<InputNew variant="success" /> {/* Green outline */}
49-
<InputNew variant="warning" /> {/* Orange outline */}
50-
<InputNew variant="error" /> {/* Red outline */}
47+
{/* Invalid state */}
48+
<InputNew invalid /> {/* Red border for validation errors */}
5149
```
5250

5351
### Density
@@ -84,9 +82,9 @@ This is the native type property and here is a small selection of examples of al
8482

8583
<Canvas of={ComponentStories.Types} />
8684

87-
### Variants
85+
### Validation States
8886

89-
<Canvas of={ComponentStories.Variants} />
87+
<Canvas of={ComponentStories.ValidationStates} />
9088

9189
### Disabled
9290

@@ -108,6 +106,31 @@ Demonstrates spacious (default) and comfortable density modes using the `data-de
108106

109107
<Canvas of={ComponentStories.Compact} />
110108

109+
### Light & Dark Mode
110+
111+
The Input component automatically adapts to light and dark color schemes using the `data-color-scheme` attribute. All color tokens adjust accordingly to maintain proper contrast and readability.
112+
113+
<Canvas of={ComponentStories.ColorSchemes} />
114+
115+
Set the color scheme on a parent element or use `EdsProvider`:
116+
117+
```tsx
118+
{/* Light mode (default) */}
119+
<div data-color-scheme="light">
120+
<InputNew />
121+
</div>
122+
123+
{/* Dark mode */}
124+
<div data-color-scheme="dark">
125+
<InputNew />
126+
</div>
127+
128+
{/* With EdsProvider */}
129+
<EdsProvider colorScheme="dark">
130+
<InputNew />
131+
</EdsProvider>
132+
```
133+
111134
### With Adornments
112135

113136
<Canvas of={ComponentStories.WithAdornments} />

‎packages/eds-core-react/src/components/Input/Input.new.stories.tsx‎

Lines changed: 118 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export const Types: StoryFn<InputProps> = () => (
6464
</>
6565
)
6666

67-
export const Variants: StoryFn<InputProps> = () => (
67+
export const ValidationStates: StoryFn<InputProps> = () => (
6868
<>
6969
<div>
7070
<Label htmlFor="input-new-default" label="Default" />
@@ -75,34 +75,17 @@ export const Variants: StoryFn<InputProps> = () => (
7575
/>
7676
</div>
7777
<div>
78-
<Label htmlFor="input-new-success" label="Success" />
78+
<Label htmlFor="input-new-invalid" label="Invalid" />
7979
<InputNew
80-
id="input-new-success"
80+
id="input-new-invalid"
8181
placeholder="Placeholder text"
8282
autoComplete="off"
83-
variant="success"
84-
/>
85-
</div>
86-
<div>
87-
<Label htmlFor="input-new-warning" label="Warning" />
88-
<InputNew
89-
id="input-new-warning"
90-
placeholder="Placeholder text"
91-
autoComplete="off"
92-
variant="warning"
93-
/>
94-
</div>
95-
<div>
96-
<Label htmlFor="input-new-error" label="Error" />
97-
<InputNew
98-
id="input-new-error"
99-
placeholder="Placeholder text"
100-
autoComplete="off"
101-
variant="error"
83+
invalid
10284
/>
10385
</div>
10486
</>
10587
)
88+
ValidationStates.storyName = 'Validation States'
10689

10790
export const Disabled: StoryFn<InputProps> = () => (
10891
<>
@@ -123,7 +106,7 @@ Disabled.decorators = [
123106
export const ReadOnly: StoryFn<InputProps> = () => (
124107
<>
125108
<Label htmlFor="input-new-readonly" label="Read only" />
126-
<InputNew id="input-new-readonly" placeholder="Placeholder text" readOnly />
109+
<InputNew id="input-new-readonly" defaultValue="Read only value" readOnly />
127110
</>
128111
)
129112
ReadOnly.storyName = 'Read only'
@@ -245,51 +228,11 @@ export const WithAdornments: StoryFn<InputProps> = () => {
245228
</>
246229
}
247230
/>
248-
<Label htmlFor="input-new-adornments-error" label="Error" />
249-
<InputNew
250-
type="text"
251-
id="input-new-adornments-error"
252-
variant="error"
253-
leftAdornments={
254-
<Button
255-
variant="ghost_icon"
256-
style={{ height: '24px', width: '24px' }}
257-
>
258-
IT
259-
</Button>
260-
}
261-
rightAdornments={
262-
<>
263-
unit
264-
<Icon data={anchor} size={18}></Icon>
265-
</>
266-
}
267-
/>
268-
<Label htmlFor="input-new-adornments-warning" label="Warning" />
231+
<Label htmlFor="input-new-adornments-invalid" label="Invalid" />
269232
<InputNew
270233
type="text"
271-
id="input-new-adornments-warning"
272-
variant="warning"
273-
leftAdornments={
274-
<Button
275-
variant="ghost_icon"
276-
style={{ height: '24px', width: '24px' }}
277-
>
278-
IT
279-
</Button>
280-
}
281-
rightAdornments={
282-
<>
283-
unit
284-
<Icon data={anchor} size={18}></Icon>
285-
</>
286-
}
287-
/>
288-
<Label htmlFor="input-new-adornments-success" label="Success" />
289-
<InputNew
290-
type="text"
291-
id="input-new-adornments-success"
292-
variant="success"
234+
id="input-new-adornments-invalid"
235+
invalid
293236
leftAdornments={
294237
<Button
295238
variant="ghost_icon"
@@ -373,3 +316,112 @@ OverrideBackground.decorators = [
373316
)
374317
},
375318
]
319+
320+
export const ColorSchemes: StoryFn<InputProps> = () => {
321+
return (
322+
<div
323+
style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '32px' }}
324+
>
325+
{/* Light mode */}
326+
<div
327+
data-color-scheme="light"
328+
style={{ padding: '24px', background: 'var(--eds-color-bg-canvas)' }}
329+
>
330+
<h3 style={{ marginTop: 0 }}>Light Mode</h3>
331+
<div>
332+
<Label htmlFor="input-light-default" label="Default" />
333+
<InputNew
334+
id="input-light-default"
335+
placeholder="Placeholder text"
336+
autoComplete="off"
337+
/>
338+
</div>
339+
<div style={{ marginTop: '16px' }}>
340+
<Label htmlFor="input-light-filled" label="With value" />
341+
<InputNew
342+
id="input-light-filled"
343+
defaultValue="Input value"
344+
autoComplete="off"
345+
/>
346+
</div>
347+
<div style={{ marginTop: '16px' }}>
348+
<Label htmlFor="input-light-invalid" label="Invalid" />
349+
<InputNew
350+
id="input-light-invalid"
351+
defaultValue="Invalid value"
352+
invalid
353+
autoComplete="off"
354+
/>
355+
</div>
356+
<div style={{ marginTop: '16px' }}>
357+
<Label htmlFor="input-light-readonly" label="Read only" />
358+
<InputNew
359+
id="input-light-readonly"
360+
defaultValue="Read only value"
361+
readOnly
362+
/>
363+
</div>
364+
<div style={{ marginTop: '16px' }}>
365+
<Label htmlFor="input-light-disabled" label="Disabled" />
366+
<InputNew
367+
id="input-light-disabled"
368+
placeholder="Placeholder text"
369+
disabled
370+
/>
371+
</div>
372+
</div>
373+
374+
{/* Dark mode */}
375+
<div
376+
data-color-scheme="dark"
377+
style={{ padding: '24px', background: 'var(--eds-color-bg-canvas)' }}
378+
>
379+
<h3 style={{ marginTop: 0, color: 'var(--eds-color-text-strong)' }}>
380+
Dark Mode
381+
</h3>
382+
<div>
383+
<Label htmlFor="input-dark-default" label="Default" />
384+
<InputNew
385+
id="input-dark-default"
386+
placeholder="Placeholder text"
387+
autoComplete="off"
388+
/>
389+
</div>
390+
<div style={{ marginTop: '16px' }}>
391+
<Label htmlFor="input-dark-filled" label="With value" />
392+
<InputNew
393+
id="input-dark-filled"
394+
defaultValue="Input value"
395+
autoComplete="off"
396+
/>
397+
</div>
398+
<div style={{ marginTop: '16px' }}>
399+
<Label htmlFor="input-dark-invalid" label="Invalid" />
400+
<InputNew
401+
id="input-dark-invalid"
402+
defaultValue="Invalid value"
403+
invalid
404+
autoComplete="off"
405+
/>
406+
</div>
407+
<div style={{ marginTop: '16px' }}>
408+
<Label htmlFor="input-dark-readonly" label="Read only" />
409+
<InputNew
410+
id="input-dark-readonly"
411+
defaultValue="Read only value"
412+
readOnly
413+
/>
414+
</div>
415+
<div style={{ marginTop: '16px' }}>
416+
<Label htmlFor="input-dark-disabled" label="Disabled" />
417+
<InputNew
418+
id="input-dark-disabled"
419+
placeholder="Placeholder text"
420+
disabled
421+
/>
422+
</div>
423+
</div>
424+
</div>
425+
)
426+
}
427+
ColorSchemes.storyName = 'Light & Dark Mode'

‎packages/eds-core-react/src/components/Input/Input.new.test.tsx‎

Lines changed: 8 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -31,29 +31,17 @@ describe('Input (New)', () => {
3131
expect(inputElement).toHaveValue(value)
3232
})
3333

34-
it('Renders input with success variant', () => {
35-
render(<Input aria-label="Success input" variant="success" />)
36-
const input = screen.getByRole('textbox', { name: 'Success input' })
37-
expect(input).toBeInTheDocument()
38-
})
39-
40-
it('Renders input with warning variant', () => {
41-
render(<Input aria-label="Warning input" variant="warning" />)
42-
const input = screen.getByRole('textbox', { name: 'Warning input' })
43-
expect(input).toBeInTheDocument()
44-
})
45-
46-
it('Renders input with error variant', () => {
47-
render(<Input aria-label="Error input" variant="error" />)
48-
const input = screen.getByRole('textbox', { name: 'Error input' })
34+
it('Renders input with invalid state', () => {
35+
render(<Input aria-label="Invalid input" invalid />)
36+
const input = screen.getByRole('textbox', { name: 'Invalid input' })
4937
expect(input).toBeInTheDocument()
5038
})
5139

5240
it('Can extend the css of the component with className and style', () => {
5341
render(
5442
<Input
5543
id="test-css-extend"
56-
variant="error"
44+
invalid
5745
value="textfield"
5846
className="custom-class"
5947
style={{ marginTop: '48px' }}
@@ -119,30 +107,16 @@ describe('Input (New)', () => {
119107
})
120108

121109
describe('EDS 2.0 Token System', () => {
122-
describe('Variant mapping to color appearance', () => {
123-
it('Sets neutral appearance by default when no variant specified', () => {
110+
describe('Invalid state mapping to color appearance', () => {
111+
it('Sets neutral appearance by default when not invalid', () => {
124112
render(<Input id="test-default" />)
125113
// eslint-disable-next-line testing-library/no-node-access
126114
const wrapper = screen.getByDisplayValue('').parentElement
127115
expect(wrapper).toHaveAttribute('data-color-appearance', 'neutral')
128116
})
129117

130-
it('Sets success appearance when variant is success', () => {
131-
render(<Input variant="success" />)
132-
// eslint-disable-next-line testing-library/no-node-access
133-
const wrapper = screen.getByDisplayValue('').parentElement
134-
expect(wrapper).toHaveAttribute('data-color-appearance', 'success')
135-
})
136-
137-
it('Sets warning appearance when variant is warning', () => {
138-
render(<Input variant="warning" />)
139-
// eslint-disable-next-line testing-library/no-node-access
140-
const wrapper = screen.getByDisplayValue('').parentElement
141-
expect(wrapper).toHaveAttribute('data-color-appearance', 'warning')
142-
})
143-
144-
it('Maps error variant to danger appearance', () => {
145-
render(<Input variant="error" />)
118+
it('Maps invalid state to danger appearance', () => {
119+
render(<Input invalid />)
146120
// eslint-disable-next-line testing-library/no-node-access
147121
const wrapper = screen.getByDisplayValue('').parentElement
148122
expect(wrapper).toHaveAttribute('data-color-appearance', 'danger')

0 commit comments

Comments
 (0)