Skip to content

Commit 7564780

Browse files
millusclaude
andauthored
fix: improve Input component alignment and sizing (#4531)
* fix(next): improve Input component alignment and sizing - Add showErrorIcon prop to optionally hide error icon (for Search pattern) - Fix button alignment in adornments (constrain to 20x20px) - Fix adornment colors to stay subtle in all states - Update comfortable density to 24px height (from 28px) - Add negative margins to align buttons with icon adornments - Remove data-font-family from adornment wrappers - Use tokens instead of hardcoded values where possible - Update stories with Search patterns and European currency examples - Add comprehensive tests for showErrorIcon behavior - Update documentation to remove incorrect color inheritance claims Fixes #4522 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix: address Input component review feedback - Remove incorrect placeholder color override in readonly state (lines 102-106) This was creating a CSS variable scoping issue where placeholder used the default color instead of readonly color - Add test for readonly + invalid showing neutral appearance - Add test for error icon hidden in readonly + invalid state Addresses review feedback on PR #4531 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 87f51e2 commit 7564780

7 files changed

Lines changed: 186 additions & 109 deletions

File tree

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,9 @@ Input is a low-level building block for custom compositions. When using Input wi
7171

7272
### With Adornments
7373

74-
Use `startText` and `endText` for prefix/suffix text (e.g., "$", "USD", "km"). These stay neutral color in all states.
74+
Use `startText` and `endText` for prefix/suffix text (e.g., "https://", "NOK", "kg").
7575

76-
Use `startAdornment` and `endAdornment` for elements (icons, buttons, etc.) that should inherit state colors (e.g., red when invalid).
76+
Use `startAdornment` and `endAdornment` for elements (icons, buttons, etc.).
7777

7878
<Canvas of={ComponentStories.WithAdornments} />
7979

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

Lines changed: 37 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { StoryFn, Meta } from '@storybook/react-vite'
2-
import { anchor, search } from '@equinor/eds-icons'
2+
import { anchor, search, close } from '@equinor/eds-icons'
33
import { Input } from './Input'
44
import type { InputProps } from './Input.types'
55
import { Field } from '../Field'
66
import { Stack } from './../../../../.storybook/components'
77
import { Icon } from '../Icon'
8-
import { Button } from '../../Button'
8+
import { Button } from '../Button'
99
import page from './Input.docs.mdx'
1010

1111
const meta: Meta<typeof Input> = {
@@ -17,6 +17,7 @@ const meta: Meta<typeof Input> = {
1717
invalid: false,
1818
readOnly: false,
1919
required: false,
20+
hideErrorIcon: false,
2021
},
2122
parameters: {
2223
docs: {
@@ -57,7 +58,16 @@ const meta: Meta<typeof Input> = {
5758
// States
5859
invalid: {
5960
control: 'boolean',
60-
description: 'Shows error styling with red border and adornment colors',
61+
description: 'Shows error styling with red border',
62+
table: {
63+
category: 'States',
64+
defaultValue: { summary: 'false' },
65+
},
66+
},
67+
hideErrorIcon: {
68+
control: 'boolean',
69+
description:
70+
'Hide error icon when invalid. Only set to true when composing custom components like Search where the error icon is not applicable. For standard form inputs and TextField, keep this false (default).',
6171
table: {
6272
category: 'States',
6373
defaultValue: { summary: 'false' },
@@ -92,33 +102,29 @@ const meta: Meta<typeof Input> = {
92102
// Adornments
93103
startText: {
94104
control: 'text',
95-
description:
96-
'Text at the start (e.g., "$", "https://"). Always neutral color.',
105+
description: 'Text at the start (e.g., "https://", "NOK")',
97106
table: {
98107
category: 'Adornments',
99108
},
100109
},
101110
endText: {
102111
control: 'text',
103-
description:
104-
'Text at the end (e.g., "kg", "%", "USD"). Always neutral color.',
112+
description: 'Text at the end (e.g., "kg", "%", "NOK")',
105113
table: {
106114
category: 'Adornments',
107115
},
108116
},
109117
startAdornment: {
110118
control: false,
111-
description:
112-
'ReactNode at the start (icons, buttons). Inherits state color (red when invalid).',
119+
description: 'ReactNode at the start (icons, buttons, etc.)',
113120
table: {
114121
category: 'Adornments',
115122
type: { summary: 'ReactNode' },
116123
},
117124
},
118125
endAdornment: {
119126
control: false,
120-
description:
121-
'ReactNode at the end (icons, buttons). Inherits state color (red when invalid).',
127+
description: 'ReactNode at the end (icons, buttons, etc.)',
122128
table: {
123129
category: 'Adornments',
124130
type: { summary: 'ReactNode' },
@@ -163,6 +169,7 @@ Introduction.args = {
163169
invalid: false,
164170
disabled: false,
165171
readOnly: false,
172+
hideErrorIcon: false,
166173
}
167174

168175
export const Types: StoryFn<InputProps> = () => (
@@ -289,8 +296,8 @@ export const WithAdornments: StoryFn<InputProps> = () => {
289296
aria-label="With text prefix and suffix"
290297
type="number"
291298
placeholder="Amount"
292-
startText="$"
293-
endText="USD"
299+
startText=""
300+
endText="EUR"
294301
/>
295302
<Input
296303
aria-label="With icon"
@@ -299,15 +306,13 @@ export const WithAdornments: StoryFn<InputProps> = () => {
299306
endAdornment={<Icon data={anchor} />}
300307
/>
301308
<Input
302-
aria-label="With button"
309+
aria-label="Search"
303310
type="text"
304311
placeholder="Search"
312+
startAdornment={<Icon data={search} />}
305313
endAdornment={
306-
<Button
307-
variant="ghost_icon"
308-
style={{ height: '24px', width: '24px' }}
309-
>
310-
<Icon data={search} />
314+
<Button variant="ghost" icon size="small" aria-label="Clear">
315+
<Icon data={close} />
311316
</Button>
312317
}
313318
/>
@@ -328,26 +333,30 @@ export const WithAdornments: StoryFn<InputProps> = () => {
328333
endText=".com"
329334
/>
330335
<Input
331-
aria-label="Invalid with button"
336+
aria-label="Search with error"
332337
type="text"
333-
defaultValue="Invalid with button"
338+
defaultValue="Invalid search"
334339
invalid
340+
hideErrorIcon
341+
startAdornment={<Icon data={search} />}
335342
endAdornment={
336343
<Button
337-
variant="ghost_icon"
338-
color="danger"
339-
style={{ height: '24px', width: '24px' }}
344+
variant="ghost"
345+
icon
346+
size="small"
347+
tone="neutral"
348+
aria-label="Clear"
340349
>
341-
<Icon data={search} />
350+
<Icon data={close} />
342351
</Button>
343352
}
344353
/>
345354
<Input
346355
aria-label="Disabled with adornments"
347-
type="text"
356+
type="number"
348357
disabled
349-
value="Disabled"
350-
startText="$"
358+
value="100"
359+
endText="kg"
351360
endAdornment={<Icon data={anchor} />}
352361
/>
353362
</>

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

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,46 @@ describe('Input (Next EDS 2.0)', () => {
102102
})
103103
})
104104

105+
describe('Error icon', () => {
106+
it('Shows error icon when invalid by default', () => {
107+
const { container } = render(<Input invalid aria-label="Invalid input" />)
108+
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
109+
const errorIcon = container.querySelector('.eds-error-icon')
110+
expect(errorIcon).toBeInTheDocument()
111+
})
112+
113+
it('Hides error icon when hideErrorIcon is true', () => {
114+
const { container } = render(
115+
<Input invalid hideErrorIcon aria-label="Invalid input" />,
116+
)
117+
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
118+
const errorIcon = container.querySelector('.eds-error-icon')
119+
expect(errorIcon).not.toBeInTheDocument()
120+
})
121+
122+
it('Does not show error icon when disabled even if invalid', () => {
123+
const { container } = render(
124+
<Input invalid disabled aria-label="Disabled invalid input" />,
125+
)
126+
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
127+
const errorIcon = container.querySelector('.eds-error-icon')
128+
expect(errorIcon).not.toBeInTheDocument()
129+
})
130+
131+
it('Does not show error icon when readOnly even if invalid', () => {
132+
const { container } = render(
133+
<Input invalid readOnly aria-label="ReadOnly invalid input" />,
134+
)
135+
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
136+
const errorIcon = container.querySelector('.eds-error-icon')
137+
expect(errorIcon).not.toBeInTheDocument()
138+
})
139+
})
140+
105141
describe('Adornments', () => {
106142
it('Renders left text', () => {
107-
render(<Input startText="$" value="100" readOnly />)
108-
expect(screen.getByText('$')).toBeInTheDocument()
143+
render(<Input startText="NOK" value="100" readOnly />)
144+
expect(screen.getByText('NOK')).toBeInTheDocument()
109145
})
110146

111147
it('Renders right text', () => {
@@ -132,7 +168,7 @@ describe('Input (Next EDS 2.0)', () => {
132168
})
133169

134170
it('Text has text class', () => {
135-
const { container } = render(<Input startText="$" endText="kg" />)
171+
const { container } = render(<Input startText="NOK" endText="kg" />)
136172
/* eslint-disable testing-library/no-container, testing-library/no-node-access */
137173
const startText = container.querySelector('.eds-adornment__text')
138174
const endText = container.querySelectorAll('.eds-adornment__text')[1]
@@ -181,6 +217,12 @@ describe('Input (Next EDS 2.0)', () => {
181217
expect(wrapper).toHaveAttribute('data-color-appearance', 'neutral')
182218
})
183219

220+
it('Sets neutral appearance when readOnly regardless of invalid state', () => {
221+
render(<Input invalid readOnly />)
222+
const wrapper = getInputWrapper()
223+
expect(wrapper).toHaveAttribute('data-color-appearance', 'neutral')
224+
})
225+
184226
it('Input element has correct font styling attributes', () => {
185227
render(<Input invalid value="test" readOnly />)
186228
const input = screen.getByDisplayValue('test')

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

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const Input: OverridableComponent<InputProps, HTMLInputElement> =
88
forwardRef<HTMLInputElement, InputProps>(function Input(
99
{
1010
invalid = false,
11+
hideErrorIcon = false,
1112
disabled,
1213
readOnly,
1314
type = 'text',
@@ -22,9 +23,9 @@ export const Input: OverridableComponent<InputProps, HTMLInputElement> =
2223
},
2324
ref,
2425
) {
25-
const tone = invalid && !disabled ? 'danger' : 'neutral'
26+
const tone = invalid && !disabled && !readOnly ? 'danger' : 'neutral'
2627

27-
const showErrorIcon = invalid && !disabled
28+
const displayErrorIcon = !hideErrorIcon && invalid && !disabled && !readOnly
2829
const hasStartAdornment = startText || startAdornment
2930
const hasEndAdornment = endText || endAdornment
3031

@@ -43,7 +44,7 @@ export const Input: OverridableComponent<InputProps, HTMLInputElement> =
4344
data-readonly={readOnly || undefined}
4445
data-invalid={invalid || undefined}
4546
>
46-
{showErrorIcon && (
47+
{displayErrorIcon && (
4748
<span
4849
className="eds-error-icon"
4950
data-font-size="xs"
@@ -54,11 +55,10 @@ export const Input: OverridableComponent<InputProps, HTMLInputElement> =
5455
</span>
5556
)}
5657
{hasStartAdornment && (
57-
<div className="eds-adornment" data-font-size="xs">
58+
<div className="eds-adornment" data-color-appearance="neutral">
5859
{startText && (
5960
<span
6061
className="eds-adornment__text"
61-
data-color-appearance="neutral"
6262
data-font-family="ui"
6363
data-font-size="xs"
6464
data-baseline="center"
@@ -67,12 +67,7 @@ export const Input: OverridableComponent<InputProps, HTMLInputElement> =
6767
</span>
6868
)}
6969
{startAdornment && (
70-
<span
71-
className="eds-adornment__adornment"
72-
data-font-size="xs"
73-
data-font-family="ui"
74-
data-baseline="center"
75-
>
70+
<span className="eds-adornment__adornment" data-font-size="xs">
7671
{startAdornment}
7772
</span>
7873
)}
@@ -94,11 +89,10 @@ export const Input: OverridableComponent<InputProps, HTMLInputElement> =
9489
aria-invalid={invalid || undefined}
9590
/>
9691
{hasEndAdornment && (
97-
<div className="eds-adornment" data-font-size="xs">
92+
<div className="eds-adornment" data-color-appearance="neutral">
9893
{endText && (
9994
<span
10095
className="eds-adornment__text"
101-
data-color-appearance="neutral"
10296
data-font-family="ui"
10397
data-font-size="xs"
10498
data-baseline="center"
@@ -107,12 +101,7 @@ export const Input: OverridableComponent<InputProps, HTMLInputElement> =
107101
</span>
108102
)}
109103
{endAdornment && (
110-
<span
111-
className="eds-adornment__adornment"
112-
data-font-size="xs"
113-
data-font-family="ui"
114-
data-baseline="center"
115-
>
104+
<span className="eds-adornment__adornment" data-font-size="xs">
116105
{endAdornment}
117106
</span>
118107
)}

packages/eds-core-react/src/components/next/Input/Input.types.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ import { InputHTMLAttributes, ReactNode } from 'react'
33
export type InputProps = {
44
/** Invalid state - shows error styling */
55
invalid?: boolean
6-
/** Text at the start (e.g., "$", "USD") - always neutral color */
6+
/** Hide error icon when invalid - defaults to false (icon shows by default) */
7+
hideErrorIcon?: boolean
8+
/** Text at the start (e.g., "https://", "NOK") */
79
startText?: string
8-
/** Adornment at the start (icons, buttons, etc.) - inherits state color (red when invalid) */
10+
/** Adornment at the start (icons, buttons, etc.) */
911
startAdornment?: ReactNode
10-
/** Text at the end (e.g., "km", "%") - always neutral color */
12+
/** Text at the end (e.g., "km", "%") */
1113
endText?: string
12-
/** Adornment at the end (icons, buttons, etc.) - inherits state color (red when invalid) */
14+
/** Adornment at the end (icons, buttons, etc.) */
1315
endAdornment?: ReactNode
1416
/** Render as input or textarea */
1517
as?: 'input' | 'textarea'

0 commit comments

Comments
 (0)