Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ Input is a low-level building block for custom compositions. When using Input wi

### With Adornments

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

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

<Canvas of={ComponentStories.WithAdornments} />

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { StoryFn, Meta } from '@storybook/react-vite'
import { anchor, search } from '@equinor/eds-icons'
import { anchor, search, close } from '@equinor/eds-icons'
import { Input } from './Input'
import type { InputProps } from './Input.types'
import { Field } from '../Field'
import { Stack } from './../../../../.storybook/components'
import { Icon } from '../Icon'
import { Button } from '../../Button'
import { Button } from '../Button'
import page from './Input.docs.mdx'

const meta: Meta<typeof Input> = {
Expand All @@ -17,6 +17,7 @@ const meta: Meta<typeof Input> = {
invalid: false,
readOnly: false,
required: false,
hideErrorIcon: false,
},
parameters: {
docs: {
Expand Down Expand Up @@ -57,7 +58,16 @@ const meta: Meta<typeof Input> = {
// States
invalid: {
control: 'boolean',
description: 'Shows error styling with red border and adornment colors',
description: 'Shows error styling with red border',
table: {
category: 'States',
defaultValue: { summary: 'false' },
},
},
hideErrorIcon: {
control: 'boolean',
description:
'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).',
table: {
category: 'States',
defaultValue: { summary: 'false' },
Expand Down Expand Up @@ -92,33 +102,29 @@ const meta: Meta<typeof Input> = {
// Adornments
startText: {
control: 'text',
description:
'Text at the start (e.g., "$", "https://"). Always neutral color.',
description: 'Text at the start (e.g., "https://", "NOK")',
table: {
category: 'Adornments',
},
},
endText: {
control: 'text',
description:
'Text at the end (e.g., "kg", "%", "USD"). Always neutral color.',
description: 'Text at the end (e.g., "kg", "%", "NOK")',
table: {
category: 'Adornments',
},
},
startAdornment: {
control: false,
description:
'ReactNode at the start (icons, buttons). Inherits state color (red when invalid).',
description: 'ReactNode at the start (icons, buttons, etc.)',
table: {
category: 'Adornments',
type: { summary: 'ReactNode' },
},
},
endAdornment: {
control: false,
description:
'ReactNode at the end (icons, buttons). Inherits state color (red when invalid).',
description: 'ReactNode at the end (icons, buttons, etc.)',
table: {
category: 'Adornments',
type: { summary: 'ReactNode' },
Expand Down Expand Up @@ -163,6 +169,7 @@ Introduction.args = {
invalid: false,
disabled: false,
readOnly: false,
hideErrorIcon: false,
}

export const Types: StoryFn<InputProps> = () => (
Expand Down Expand Up @@ -289,8 +296,8 @@ export const WithAdornments: StoryFn<InputProps> = () => {
aria-label="With text prefix and suffix"
type="number"
placeholder="Amount"
startText="$"
endText="USD"
startText=""
endText="EUR"
/>
<Input
aria-label="With icon"
Expand All @@ -299,15 +306,13 @@ export const WithAdornments: StoryFn<InputProps> = () => {
endAdornment={<Icon data={anchor} />}
/>
<Input
aria-label="With button"
aria-label="Search"
type="text"
placeholder="Search"
startAdornment={<Icon data={search} />}
endAdornment={
<Button
variant="ghost_icon"
style={{ height: '24px', width: '24px' }}
>
<Icon data={search} />
<Button variant="ghost" icon size="small" aria-label="Clear">
<Icon data={close} />
</Button>
}
/>
Expand All @@ -328,26 +333,30 @@ export const WithAdornments: StoryFn<InputProps> = () => {
endText=".com"
/>
<Input
aria-label="Invalid with button"
aria-label="Search with error"
type="text"
defaultValue="Invalid with button"
defaultValue="Invalid search"
invalid
hideErrorIcon
startAdornment={<Icon data={search} />}
endAdornment={
<Button
variant="ghost_icon"
color="danger"
style={{ height: '24px', width: '24px' }}
variant="ghost"
icon
size="small"
tone="neutral"
aria-label="Clear"
>
<Icon data={search} />
<Icon data={close} />
</Button>
}
/>
<Input
aria-label="Disabled with adornments"
type="text"
type="number"
disabled
value="Disabled"
startText="$"
value="100"
endText="kg"
endAdornment={<Icon data={anchor} />}
/>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,46 @@ describe('Input (Next EDS 2.0)', () => {
})
})

describe('Error icon', () => {
it('Shows error icon when invalid by default', () => {
const { container } = render(<Input invalid aria-label="Invalid input" />)
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
const errorIcon = container.querySelector('.eds-error-icon')
expect(errorIcon).toBeInTheDocument()
})

it('Hides error icon when hideErrorIcon is true', () => {
const { container } = render(
<Input invalid hideErrorIcon aria-label="Invalid input" />,
)
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
const errorIcon = container.querySelector('.eds-error-icon')
expect(errorIcon).not.toBeInTheDocument()
})

it('Does not show error icon when disabled even if invalid', () => {
const { container } = render(
<Input invalid disabled aria-label="Disabled invalid input" />,
)
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
const errorIcon = container.querySelector('.eds-error-icon')
expect(errorIcon).not.toBeInTheDocument()
})

it('Does not show error icon when readOnly even if invalid', () => {
const { container } = render(
<Input invalid readOnly aria-label="ReadOnly invalid input" />,
)
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
const errorIcon = container.querySelector('.eds-error-icon')
expect(errorIcon).not.toBeInTheDocument()
})
})

describe('Adornments', () => {
it('Renders left text', () => {
render(<Input startText="$" value="100" readOnly />)
expect(screen.getByText('$')).toBeInTheDocument()
render(<Input startText="NOK" value="100" readOnly />)
expect(screen.getByText('NOK')).toBeInTheDocument()
})

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

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

it('Sets neutral appearance when readOnly regardless of invalid state', () => {
render(<Input invalid readOnly />)
const wrapper = getInputWrapper()
expect(wrapper).toHaveAttribute('data-color-appearance', 'neutral')
})

it('Input element has correct font styling attributes', () => {
render(<Input invalid value="test" readOnly />)
const input = screen.getByDisplayValue('test')
Expand Down
27 changes: 8 additions & 19 deletions packages/eds-core-react/src/components/next/Input/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const Input: OverridableComponent<InputProps, HTMLInputElement> =
forwardRef<HTMLInputElement, InputProps>(function Input(
{
invalid = false,
hideErrorIcon = false,
disabled,
readOnly,
type = 'text',
Expand All @@ -22,9 +23,9 @@ export const Input: OverridableComponent<InputProps, HTMLInputElement> =
},
ref,
) {
const tone = invalid && !disabled ? 'danger' : 'neutral'
const tone = invalid && !disabled && !readOnly ? 'danger' : 'neutral'

const showErrorIcon = invalid && !disabled
const displayErrorIcon = !hideErrorIcon && invalid && !disabled && !readOnly
const hasStartAdornment = startText || startAdornment
const hasEndAdornment = endText || endAdornment

Expand All @@ -43,7 +44,7 @@ export const Input: OverridableComponent<InputProps, HTMLInputElement> =
data-readonly={readOnly || undefined}
data-invalid={invalid || undefined}
>
{showErrorIcon && (
{displayErrorIcon && (
<span
className="eds-error-icon"
data-font-size="xs"
Expand All @@ -54,11 +55,10 @@ export const Input: OverridableComponent<InputProps, HTMLInputElement> =
</span>
)}
{hasStartAdornment && (
<div className="eds-adornment" data-font-size="xs">
<div className="eds-adornment" data-color-appearance="neutral">
{startText && (
<span
className="eds-adornment__text"
data-color-appearance="neutral"
data-font-family="ui"
data-font-size="xs"
data-baseline="center"
Expand All @@ -67,12 +67,7 @@ export const Input: OverridableComponent<InputProps, HTMLInputElement> =
</span>
)}
{startAdornment && (
<span
className="eds-adornment__adornment"
data-font-size="xs"
data-font-family="ui"
data-baseline="center"
>
<span className="eds-adornment__adornment" data-font-size="xs">
{startAdornment}
</span>
)}
Expand All @@ -94,11 +89,10 @@ export const Input: OverridableComponent<InputProps, HTMLInputElement> =
aria-invalid={invalid || undefined}
/>
{hasEndAdornment && (
<div className="eds-adornment" data-font-size="xs">
<div className="eds-adornment" data-color-appearance="neutral">
{endText && (
<span
className="eds-adornment__text"
data-color-appearance="neutral"
data-font-family="ui"
data-font-size="xs"
data-baseline="center"
Expand All @@ -107,12 +101,7 @@ export const Input: OverridableComponent<InputProps, HTMLInputElement> =
</span>
)}
{endAdornment && (
<span
className="eds-adornment__adornment"
data-font-size="xs"
data-font-family="ui"
data-baseline="center"
>
<span className="eds-adornment__adornment" data-font-size="xs">
{endAdornment}
</span>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import { InputHTMLAttributes, ReactNode } from 'react'
export type InputProps = {
/** Invalid state - shows error styling */
invalid?: boolean
/** Text at the start (e.g., "$", "USD") - always neutral color */
/** Hide error icon when invalid - defaults to false (icon shows by default) */
hideErrorIcon?: boolean
/** Text at the start (e.g., "https://", "NOK") */
startText?: string
/** Adornment at the start (icons, buttons, etc.) - inherits state color (red when invalid) */
/** Adornment at the start (icons, buttons, etc.) */
startAdornment?: ReactNode
/** Text at the end (e.g., "km", "%") - always neutral color */
/** Text at the end (e.g., "km", "%") */
endText?: string
/** Adornment at the end (icons, buttons, etc.) - inherits state color (red when invalid) */
/** Adornment at the end (icons, buttons, etc.) */
endAdornment?: ReactNode
/** Render as input or textarea */
as?: 'input' | 'textarea'
Expand Down
Loading
Loading