Merged in fix/3697-prettier-configs (pull request #3396)
fix(SW-3691): Setup one prettier config for whole repo * Setup prettierrc in root and remove other configs Approved-by: Joakim Jäderberg Approved-by: Linus Flood
This commit is contained in:
@@ -1,14 +1,14 @@
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import type { Meta, StoryObj } from "@storybook/nextjs-vite"
|
||||
|
||||
import { expect } from 'storybook/test'
|
||||
import { expect } from "storybook/test"
|
||||
|
||||
import { Input } from './Input'
|
||||
import { TextField } from 'react-aria-components'
|
||||
import { MaterialIcon } from '../Icons/MaterialIcon'
|
||||
import type { SymbolCodepoints } from '../Icons/MaterialIcon/MaterialSymbol/types'
|
||||
import { Input } from "./Input"
|
||||
import { TextField } from "react-aria-components"
|
||||
import { MaterialIcon } from "../Icons/MaterialIcon"
|
||||
import type { SymbolCodepoints } from "../Icons/MaterialIcon/MaterialSymbol/types"
|
||||
|
||||
const meta: Meta<typeof Input> = {
|
||||
title: 'Core Components/Input',
|
||||
title: "Core Components/Input",
|
||||
// @ts-expect-error Input does not support this, but wrapping <TextField> does
|
||||
component: ({ isInvalid, validationState, ...props }) => (
|
||||
<TextField isInvalid={isInvalid} data-validation-state={validationState}>
|
||||
@@ -17,117 +17,117 @@ const meta: Meta<typeof Input> = {
|
||||
),
|
||||
argTypes: {
|
||||
label: {
|
||||
control: 'text',
|
||||
description: 'The label text displayed for the input field',
|
||||
control: "text",
|
||||
description: "The label text displayed for the input field",
|
||||
table: {
|
||||
type: { summary: 'string' },
|
||||
type: { summary: "string" },
|
||||
},
|
||||
},
|
||||
labelPosition: {
|
||||
control: 'select',
|
||||
options: ['floating', 'top'],
|
||||
description: 'Position of the label relative to the input',
|
||||
control: "select",
|
||||
options: ["floating", "top"],
|
||||
description: "Position of the label relative to the input",
|
||||
table: {
|
||||
type: { summary: "'floating' | 'top'" },
|
||||
defaultValue: { summary: "'floating'" },
|
||||
},
|
||||
},
|
||||
placeholder: {
|
||||
control: 'text',
|
||||
description: 'Placeholder text shown when input is empty',
|
||||
control: "text",
|
||||
description: "Placeholder text shown when input is empty",
|
||||
table: {
|
||||
type: { summary: 'string' },
|
||||
defaultValue: { summary: 'undefined' },
|
||||
type: { summary: "string" },
|
||||
defaultValue: { summary: "undefined" },
|
||||
},
|
||||
},
|
||||
required: {
|
||||
control: 'boolean',
|
||||
description: 'Whether the input is required',
|
||||
control: "boolean",
|
||||
description: "Whether the input is required",
|
||||
table: {
|
||||
type: { summary: 'boolean' },
|
||||
defaultValue: { summary: 'false' },
|
||||
type: { summary: "boolean" },
|
||||
defaultValue: { summary: "false" },
|
||||
},
|
||||
},
|
||||
disabled: {
|
||||
control: 'boolean',
|
||||
description: 'Whether the input is disabled',
|
||||
control: "boolean",
|
||||
description: "Whether the input is disabled",
|
||||
table: {
|
||||
type: { summary: 'boolean' },
|
||||
defaultValue: { summary: 'false' },
|
||||
type: { summary: "boolean" },
|
||||
defaultValue: { summary: "false" },
|
||||
},
|
||||
},
|
||||
showClearContentIcon: {
|
||||
control: 'boolean',
|
||||
description: 'Whether the clear content icon is shown',
|
||||
control: "boolean",
|
||||
description: "Whether the clear content icon is shown",
|
||||
table: {
|
||||
type: { summary: 'boolean' },
|
||||
defaultValue: { summary: 'false' },
|
||||
type: { summary: "boolean" },
|
||||
defaultValue: { summary: "false" },
|
||||
},
|
||||
},
|
||||
showLeftIcon: {
|
||||
control: 'boolean',
|
||||
description: 'Whether to show a left icon',
|
||||
control: "boolean",
|
||||
description: "Whether to show a left icon",
|
||||
table: {
|
||||
type: { summary: 'boolean' },
|
||||
defaultValue: { summary: 'false' },
|
||||
type: { summary: "boolean" },
|
||||
defaultValue: { summary: "false" },
|
||||
},
|
||||
},
|
||||
showRightIcon: {
|
||||
control: 'boolean',
|
||||
description: 'Whether to show a right icon',
|
||||
control: "boolean",
|
||||
description: "Whether to show a right icon",
|
||||
table: {
|
||||
type: { summary: 'boolean' },
|
||||
defaultValue: { summary: 'false' },
|
||||
type: { summary: "boolean" },
|
||||
defaultValue: { summary: "false" },
|
||||
},
|
||||
},
|
||||
leftIconName: {
|
||||
control: 'select',
|
||||
control: "select",
|
||||
options: [
|
||||
'calendar_month',
|
||||
'credit_card',
|
||||
'email',
|
||||
'info_circle',
|
||||
'location_on',
|
||||
'lock',
|
||||
'phone',
|
||||
'search',
|
||||
'sell',
|
||||
'visibility',
|
||||
'visibility_off',
|
||||
"calendar_month",
|
||||
"credit_card",
|
||||
"email",
|
||||
"info_circle",
|
||||
"location_on",
|
||||
"lock",
|
||||
"phone",
|
||||
"search",
|
||||
"sell",
|
||||
"visibility",
|
||||
"visibility_off",
|
||||
],
|
||||
description: 'Icon name for the left icon',
|
||||
description: "Icon name for the left icon",
|
||||
table: {
|
||||
type: { summary: 'string' },
|
||||
type: { summary: "string" },
|
||||
defaultValue: { summary: "'person'" },
|
||||
},
|
||||
},
|
||||
rightIconName: {
|
||||
control: 'select',
|
||||
control: "select",
|
||||
options: [
|
||||
'calendar_month',
|
||||
'credit_card',
|
||||
'email',
|
||||
'info_circle',
|
||||
'location_on',
|
||||
'lock',
|
||||
'phone',
|
||||
'search',
|
||||
'sell',
|
||||
'visibility',
|
||||
'visibility_off',
|
||||
"calendar_month",
|
||||
"credit_card",
|
||||
"email",
|
||||
"info_circle",
|
||||
"location_on",
|
||||
"lock",
|
||||
"phone",
|
||||
"search",
|
||||
"sell",
|
||||
"visibility",
|
||||
"visibility_off",
|
||||
],
|
||||
description: 'Icon name for the right icon',
|
||||
description: "Icon name for the right icon",
|
||||
table: {
|
||||
type: { summary: 'string' },
|
||||
type: { summary: "string" },
|
||||
defaultValue: { summary: "'lock'" },
|
||||
},
|
||||
},
|
||||
showWarning: {
|
||||
control: 'boolean',
|
||||
description: 'Whether to show warning validation state',
|
||||
control: "boolean",
|
||||
description: "Whether to show warning validation state",
|
||||
table: {
|
||||
type: { summary: 'boolean' },
|
||||
defaultValue: { summary: 'false' },
|
||||
type: { summary: "boolean" },
|
||||
defaultValue: { summary: "false" },
|
||||
},
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -140,13 +140,13 @@ type Story = StoryObj<typeof Input>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
label: 'Label',
|
||||
name: 'foo',
|
||||
label: "Label",
|
||||
name: "foo",
|
||||
required: false,
|
||||
showLeftIcon: false,
|
||||
showRightIcon: false,
|
||||
leftIconName: 'person',
|
||||
rightIconName: 'lock',
|
||||
leftIconName: "person",
|
||||
rightIconName: "lock",
|
||||
showWarning: false,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any,
|
||||
@@ -167,7 +167,7 @@ export const Default: Story = {
|
||||
showWarning?: boolean
|
||||
}
|
||||
|
||||
const validationState = showWarning ? 'warning' : undefined
|
||||
const validationState = showWarning ? "warning" : undefined
|
||||
|
||||
return (
|
||||
<TextField data-validation-state={validationState}>
|
||||
@@ -189,15 +189,15 @@ export const Default: Story = {
|
||||
)
|
||||
},
|
||||
play: async ({ canvas, userEvent }) => {
|
||||
const textbox = canvas.getByRole('textbox')
|
||||
const textbox = canvas.getByRole("textbox")
|
||||
expect(textbox).not.toBeDisabled()
|
||||
|
||||
expect(textbox).toHaveValue('')
|
||||
expect(textbox).toHaveValue("")
|
||||
|
||||
await userEvent.type(textbox, 'Hello World')
|
||||
expect(textbox).toHaveValue('Hello World')
|
||||
await userEvent.type(textbox, "Hello World")
|
||||
expect(textbox).toHaveValue("Hello World")
|
||||
|
||||
await userEvent.clear(textbox)
|
||||
expect(textbox).toHaveValue('')
|
||||
expect(textbox).toHaveValue("")
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { describe, expect, it, vi, afterEach } from 'vitest'
|
||||
import { render, screen, fireEvent, cleanup } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { Input } from './Input'
|
||||
import { TextField } from 'react-aria-components'
|
||||
import { describe, expect, it, vi, afterEach } from "vitest"
|
||||
import { render, screen, fireEvent, cleanup } from "@testing-library/react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
import { Input } from "./Input"
|
||||
import { TextField } from "react-aria-components"
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
@@ -22,158 +22,158 @@ const renderInputStandalone = (props: React.ComponentProps<typeof Input>) => {
|
||||
return render(<Input {...props} />)
|
||||
}
|
||||
|
||||
describe('Input', () => {
|
||||
describe('props', () => {
|
||||
it('applies required attribute', () => {
|
||||
renderInput({ label: 'Email', required: true })
|
||||
expect(screen.getByRole('textbox')).toHaveProperty('required', true)
|
||||
describe("Input", () => {
|
||||
describe("props", () => {
|
||||
it("applies required attribute", () => {
|
||||
renderInput({ label: "Email", required: true })
|
||||
expect(screen.getByRole("textbox")).toHaveProperty("required", true)
|
||||
})
|
||||
|
||||
it('applies readOnly attribute', () => {
|
||||
renderInput({ label: 'Email', readOnly: true })
|
||||
expect(screen.getByRole('textbox')).toHaveProperty('readOnly', true)
|
||||
it("applies readOnly attribute", () => {
|
||||
renderInput({ label: "Email", readOnly: true })
|
||||
expect(screen.getByRole("textbox")).toHaveProperty("readOnly", true)
|
||||
})
|
||||
|
||||
it('applies placeholder for floating label when provided', () => {
|
||||
renderInput({ label: 'Email', placeholder: 'Enter email' })
|
||||
expect(screen.getByRole('textbox').getAttribute('placeholder')).toBe(
|
||||
'Enter email'
|
||||
it("applies placeholder for floating label when provided", () => {
|
||||
renderInput({ label: "Email", placeholder: "Enter email" })
|
||||
expect(screen.getByRole("textbox").getAttribute("placeholder")).toBe(
|
||||
"Enter email"
|
||||
)
|
||||
})
|
||||
|
||||
it('applies empty placeholder for top label by default', () => {
|
||||
renderInput({ label: 'Email', labelPosition: 'top' })
|
||||
expect(screen.getByRole('textbox').getAttribute('placeholder')).toBe('')
|
||||
it("applies empty placeholder for top label by default", () => {
|
||||
renderInput({ label: "Email", labelPosition: "top" })
|
||||
expect(screen.getByRole("textbox").getAttribute("placeholder")).toBe("")
|
||||
})
|
||||
|
||||
it('applies custom id', () => {
|
||||
it("applies custom id", () => {
|
||||
// Use standalone render since TextField overrides id via context
|
||||
renderInputStandalone({ label: 'Email', id: 'custom-id' })
|
||||
expect(screen.getByRole('textbox').getAttribute('id')).toBe('custom-id')
|
||||
renderInputStandalone({ label: "Email", id: "custom-id" })
|
||||
expect(screen.getByRole("textbox").getAttribute("id")).toBe("custom-id")
|
||||
})
|
||||
|
||||
it('applies aria-describedby', () => {
|
||||
renderInput({ label: 'Email', 'aria-describedby': 'error-message' })
|
||||
expect(screen.getByRole('textbox').getAttribute('aria-describedby')).toBe(
|
||||
'error-message'
|
||||
it("applies aria-describedby", () => {
|
||||
renderInput({ label: "Email", "aria-describedby": "error-message" })
|
||||
expect(screen.getByRole("textbox").getAttribute("aria-describedby")).toBe(
|
||||
"error-message"
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('clear content button', () => {
|
||||
it('does not show clear button when showClearContentIcon is false', () => {
|
||||
describe("clear content button", () => {
|
||||
it("does not show clear button when showClearContentIcon is false", () => {
|
||||
renderInput({
|
||||
label: 'Email',
|
||||
value: 'test',
|
||||
label: "Email",
|
||||
value: "test",
|
||||
onChange: vi.fn(),
|
||||
showClearContentIcon: false,
|
||||
})
|
||||
expect(screen.queryByLabelText('Clear content')).toBeNull()
|
||||
expect(screen.queryByLabelText("Clear content")).toBeNull()
|
||||
})
|
||||
|
||||
it('does not show clear button when input is empty', () => {
|
||||
it("does not show clear button when input is empty", () => {
|
||||
renderInput({
|
||||
label: 'Email',
|
||||
value: '',
|
||||
label: "Email",
|
||||
value: "",
|
||||
onChange: vi.fn(),
|
||||
showClearContentIcon: true,
|
||||
})
|
||||
expect(screen.queryByLabelText('Clear content')).toBeNull()
|
||||
expect(screen.queryByLabelText("Clear content")).toBeNull()
|
||||
})
|
||||
|
||||
it('shows clear button when input has value and showClearContentIcon is true', () => {
|
||||
it("shows clear button when input has value and showClearContentIcon is true", () => {
|
||||
renderInput({
|
||||
label: 'Email',
|
||||
value: 'test',
|
||||
label: "Email",
|
||||
value: "test",
|
||||
onChange: vi.fn(),
|
||||
showClearContentIcon: true,
|
||||
})
|
||||
expect(screen.getByLabelText('Clear content')).toBeTruthy()
|
||||
expect(screen.getByLabelText("Clear content")).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('icons', () => {
|
||||
it('renders left icon when provided', () => {
|
||||
describe("icons", () => {
|
||||
it("renders left icon when provided", () => {
|
||||
renderInput({
|
||||
label: 'Search',
|
||||
label: "Search",
|
||||
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
||||
leftIcon: <span data-testid="left-icon">🔍</span>,
|
||||
})
|
||||
expect(screen.getByTestId('left-icon')).toBeTruthy()
|
||||
expect(screen.getByTestId("left-icon")).toBeTruthy()
|
||||
})
|
||||
|
||||
it('renders right icon when provided', () => {
|
||||
it("renders right icon when provided", () => {
|
||||
renderInput({
|
||||
label: 'Password',
|
||||
label: "Password",
|
||||
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
||||
rightIcon: <span data-testid="right-icon">👁</span>,
|
||||
})
|
||||
expect(screen.getByTestId('right-icon')).toBeTruthy()
|
||||
expect(screen.getByTestId("right-icon")).toBeTruthy()
|
||||
})
|
||||
|
||||
it('hides right icon when clear button is shown', () => {
|
||||
it("hides right icon when clear button is shown", () => {
|
||||
renderInput({
|
||||
label: 'Email',
|
||||
value: 'test',
|
||||
label: "Email",
|
||||
value: "test",
|
||||
onChange: vi.fn(),
|
||||
showClearContentIcon: true,
|
||||
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
||||
rightIcon: <span data-testid="right-icon">👁</span>,
|
||||
})
|
||||
expect(screen.queryByTestId('right-icon')).toBeNull()
|
||||
expect(screen.getByLabelText('Clear content')).toBeTruthy()
|
||||
expect(screen.queryByTestId("right-icon")).toBeNull()
|
||||
expect(screen.getByLabelText("Clear content")).toBeTruthy()
|
||||
})
|
||||
|
||||
it('shows right icon when clear button condition not met', () => {
|
||||
it("shows right icon when clear button condition not met", () => {
|
||||
renderInput({
|
||||
label: 'Email',
|
||||
value: '',
|
||||
label: "Email",
|
||||
value: "",
|
||||
onChange: vi.fn(),
|
||||
showClearContentIcon: true,
|
||||
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
|
||||
rightIcon: <span data-testid="right-icon">👁</span>,
|
||||
})
|
||||
expect(screen.getByTestId('right-icon')).toBeTruthy()
|
||||
expect(screen.getByTestId("right-icon")).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('controlled input', () => {
|
||||
it('displays the controlled value', () => {
|
||||
describe("controlled input", () => {
|
||||
it("displays the controlled value", () => {
|
||||
renderInput({
|
||||
label: 'Email',
|
||||
value: 'test@example.com',
|
||||
label: "Email",
|
||||
value: "test@example.com",
|
||||
onChange: vi.fn(),
|
||||
})
|
||||
expect(screen.getByRole('textbox')).toHaveProperty(
|
||||
'value',
|
||||
'test@example.com'
|
||||
expect(screen.getByRole("textbox")).toHaveProperty(
|
||||
"value",
|
||||
"test@example.com"
|
||||
)
|
||||
})
|
||||
|
||||
it('calls onChange when typing', async () => {
|
||||
it("calls onChange when typing", async () => {
|
||||
const onChange = vi.fn()
|
||||
renderInput({ label: 'Email', value: '', onChange })
|
||||
renderInput({ label: "Email", value: "", onChange })
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
await userEvent.type(input, 'a')
|
||||
const input = screen.getByRole("textbox")
|
||||
await userEvent.type(input, "a")
|
||||
|
||||
expect(onChange).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not change value without onChange updating it', () => {
|
||||
it("does not change value without onChange updating it", () => {
|
||||
const onChange = vi.fn()
|
||||
renderInput({ label: 'Email', value: 'initial', onChange })
|
||||
renderInput({ label: "Email", value: "initial", onChange })
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
fireEvent.change(input, { target: { value: 'changed' } })
|
||||
const input = screen.getByRole("textbox")
|
||||
fireEvent.change(input, { target: { value: "changed" } })
|
||||
|
||||
// Value stays the same because it's controlled
|
||||
expect(input).toHaveProperty('value', 'initial')
|
||||
expect(input).toHaveProperty("value", "initial")
|
||||
})
|
||||
})
|
||||
|
||||
describe('ref forwarding', () => {
|
||||
it('forwards ref to the input element', () => {
|
||||
describe("ref forwarding", () => {
|
||||
it("forwards ref to the input element", () => {
|
||||
const ref = { current: null as HTMLInputElement | null }
|
||||
render(
|
||||
<TextField>
|
||||
@@ -183,7 +183,7 @@ describe('Input', () => {
|
||||
expect(ref.current).toBeInstanceOf(HTMLInputElement)
|
||||
})
|
||||
|
||||
it('allows focusing via ref', () => {
|
||||
it("allows focusing via ref", () => {
|
||||
const ref = { current: null as HTMLInputElement | null }
|
||||
render(
|
||||
<TextField>
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import { cx } from 'class-variance-authority'
|
||||
import { cx } from "class-variance-authority"
|
||||
import {
|
||||
type ForwardedRef,
|
||||
forwardRef,
|
||||
useId,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import { Input as AriaInput, Label as AriaLabel } from 'react-aria-components'
|
||||
} from "react"
|
||||
import { Input as AriaInput, Label as AriaLabel } from "react-aria-components"
|
||||
|
||||
import { InputLabel } from '../InputLabel'
|
||||
import { InputLabel } from "../InputLabel"
|
||||
|
||||
import styles from './input.module.css'
|
||||
import styles from "./input.module.css"
|
||||
|
||||
import { MaterialIcon } from '../Icons/MaterialIcon'
|
||||
import { Typography } from '../Typography'
|
||||
import type { InputProps } from './types'
|
||||
import { clearInput, useInputHasValue } from './utils'
|
||||
import { MaterialIcon } from "../Icons/MaterialIcon"
|
||||
import { Typography } from "../Typography"
|
||||
import type { InputProps } from "./types"
|
||||
import { clearInput, useInputHasValue } from "./utils"
|
||||
|
||||
const InputComponent = forwardRef(function AriaInputWithLabelComponent(
|
||||
{
|
||||
label,
|
||||
labelPosition = 'floating',
|
||||
labelPosition = "floating",
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@@ -29,9 +29,9 @@ const InputComponent = forwardRef(function AriaInputWithLabelComponent(
|
||||
placeholder,
|
||||
id,
|
||||
required,
|
||||
'data-validation-state': validationState,
|
||||
"data-validation-state": validationState,
|
||||
...props
|
||||
}: InputProps & { 'data-validation-state'?: string },
|
||||
}: InputProps & { "data-validation-state"?: string },
|
||||
ref: ForwardedRef<HTMLInputElement>
|
||||
) {
|
||||
// Create an internal ref that we can access
|
||||
@@ -57,7 +57,7 @@ const InputComponent = forwardRef(function AriaInputWithLabelComponent(
|
||||
}
|
||||
// When labelPosition is 'top', restructure to have label outside container
|
||||
// We need an ID for proper label-input association
|
||||
if (labelPosition === 'top') {
|
||||
if (labelPosition === "top") {
|
||||
const inputId = id || generatedId
|
||||
|
||||
return (
|
||||
@@ -93,7 +93,7 @@ const InputComponent = forwardRef(function AriaInputWithLabelComponent(
|
||||
// Avoid duplicating label text in placeholder when label is positioned above
|
||||
// Screen readers would announce the label twice (once as label, once as placeholder)
|
||||
// Only use placeholder if explicitly provided, otherwise use empty string
|
||||
placeholder={placeholder ?? ''}
|
||||
placeholder={placeholder ?? ""}
|
||||
className={cx(
|
||||
styles.input,
|
||||
styles.inputTopLabel,
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { Input } from './Input'
|
||||
export { Input } from "./Input"
|
||||
|
||||
@@ -22,11 +22,11 @@
|
||||
|
||||
&:has(.input:focus):not(:has(.input:disabled)):not(
|
||||
:has(.input:read-only)
|
||||
):not(:has(.input[data-invalid='true'])):not(
|
||||
:has(.input[aria-invalid='true'])
|
||||
):not(:has(.input[data-warning='true'])):not(
|
||||
:has(.input[data-validation-state='warning'])
|
||||
):not([data-validation-state='warning']) {
|
||||
):not(:has(.input[data-invalid="true"])):not(
|
||||
:has(.input[aria-invalid="true"])
|
||||
):not(:has(.input[data-warning="true"])):not(
|
||||
:has(.input[data-validation-state="warning"])
|
||||
):not([data-validation-state="warning"]) {
|
||||
outline-offset: -2px;
|
||||
outline: 2px solid var(--Border-Interactive-Focus);
|
||||
}
|
||||
@@ -38,7 +38,7 @@
|
||||
cursor: unset;
|
||||
}
|
||||
|
||||
&:has(.input[data-invalid='true'], .input[aria-invalid='true']) {
|
||||
&:has(.input[data-invalid="true"], .input[aria-invalid="true"]) {
|
||||
border-color: var(--Border-Interactive-Error);
|
||||
|
||||
&:focus-within,
|
||||
@@ -49,9 +49,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
&:has(.input[data-warning='true']),
|
||||
&:has(.input[data-validation-state='warning']),
|
||||
&[data-validation-state='warning'] {
|
||||
&:has(.input[data-warning="true"]),
|
||||
&:has(.input[data-validation-state="warning"]),
|
||||
&[data-validation-state="warning"] {
|
||||
background-color: var(--Surface-Feedback-Warning-light);
|
||||
border-color: var(--Border-Interactive-Focus);
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
|
||||
&:focus,
|
||||
&:placeholder-shown,
|
||||
&[value]:not([value='']) {
|
||||
&[value]:not([value=""]) {
|
||||
height: 24px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ComponentProps } from 'react'
|
||||
import { Input } from 'react-aria-components'
|
||||
import { ComponentProps } from "react"
|
||||
import { Input } from "react-aria-components"
|
||||
|
||||
export interface InputProps extends ComponentProps<typeof Input> {
|
||||
label: string
|
||||
labelPosition?: 'floating' | 'top'
|
||||
labelPosition?: "floating" | "top"
|
||||
leftIcon?: React.ReactNode
|
||||
rightIcon?: React.ReactNode
|
||||
onRightIconClick?: () => void
|
||||
|
||||
@@ -1,56 +1,56 @@
|
||||
import { describe, expect, it, vi, beforeEach } from 'vitest'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { useInputHasValue, clearInput } from './utils'
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest"
|
||||
import { renderHook } from "@testing-library/react"
|
||||
import { useInputHasValue, clearInput } from "./utils"
|
||||
|
||||
describe('useInputHasValue', () => {
|
||||
const createMockRef = (value = '') => ({
|
||||
describe("useInputHasValue", () => {
|
||||
const createMockRef = (value = "") => ({
|
||||
current: { value } as HTMLInputElement,
|
||||
})
|
||||
|
||||
describe('controlled input (value prop)', () => {
|
||||
it('returns true when value has content', () => {
|
||||
describe("controlled input (value prop)", () => {
|
||||
it("returns true when value has content", () => {
|
||||
const ref = createMockRef()
|
||||
const { result } = renderHook(() => useInputHasValue('hello', ref))
|
||||
const { result } = renderHook(() => useInputHasValue("hello", ref))
|
||||
expect(result.current).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false when value is empty string', () => {
|
||||
it("returns false when value is empty string", () => {
|
||||
const ref = createMockRef()
|
||||
const { result } = renderHook(() => useInputHasValue('', ref))
|
||||
const { result } = renderHook(() => useInputHasValue("", ref))
|
||||
expect(result.current).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false when value is only whitespace', () => {
|
||||
it("returns false when value is only whitespace", () => {
|
||||
const ref = createMockRef()
|
||||
const { result } = renderHook(() => useInputHasValue(' ', ref))
|
||||
const { result } = renderHook(() => useInputHasValue(" ", ref))
|
||||
expect(result.current).toBe(false)
|
||||
})
|
||||
|
||||
it('updates when value prop changes', () => {
|
||||
it("updates when value prop changes", () => {
|
||||
const ref = createMockRef()
|
||||
const { result, rerender } = renderHook(
|
||||
({ value }) => useInputHasValue(value, ref),
|
||||
{ initialProps: { value: '' } }
|
||||
{ initialProps: { value: "" } }
|
||||
)
|
||||
|
||||
expect(result.current).toBe(false)
|
||||
|
||||
rerender({ value: 'new value' })
|
||||
rerender({ value: "new value" })
|
||||
expect(result.current).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('clearInput', () => {
|
||||
describe("clearInput", () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('calls onChange with empty value for controlled input', () => {
|
||||
it("calls onChange with empty value for controlled input", () => {
|
||||
const onChange = vi.fn()
|
||||
const inputRef = {
|
||||
current: {
|
||||
value: 'test',
|
||||
value: "test",
|
||||
focus: vi.fn(),
|
||||
} as unknown as HTMLInputElement,
|
||||
}
|
||||
@@ -58,22 +58,22 @@ describe('clearInput', () => {
|
||||
clearInput({
|
||||
inputRef,
|
||||
onChange,
|
||||
value: 'test',
|
||||
value: "test",
|
||||
})
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
target: { value: '' },
|
||||
target: { value: "" },
|
||||
})
|
||||
)
|
||||
expect(inputRef.current.focus).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('sets input value directly for uncontrolled input', () => {
|
||||
const input = document.createElement('input')
|
||||
input.value = 'test'
|
||||
const focusSpy = vi.spyOn(input, 'focus')
|
||||
const dispatchSpy = vi.spyOn(input, 'dispatchEvent')
|
||||
it("sets input value directly for uncontrolled input", () => {
|
||||
const input = document.createElement("input")
|
||||
input.value = "test"
|
||||
const focusSpy = vi.spyOn(input, "focus")
|
||||
const dispatchSpy = vi.spyOn(input, "dispatchEvent")
|
||||
|
||||
const inputRef = { current: input }
|
||||
|
||||
@@ -83,19 +83,19 @@ describe('clearInput', () => {
|
||||
value: undefined,
|
||||
})
|
||||
|
||||
expect(input.value).toBe('')
|
||||
expect(input.value).toBe("")
|
||||
expect(dispatchSpy).toHaveBeenCalled()
|
||||
expect(focusSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does nothing when ref is null', () => {
|
||||
it("does nothing when ref is null", () => {
|
||||
const onChange = vi.fn()
|
||||
const inputRef = { current: null }
|
||||
|
||||
clearInput({
|
||||
inputRef,
|
||||
onChange,
|
||||
value: 'test',
|
||||
value: "test",
|
||||
})
|
||||
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ChangeEvent, ChangeEventHandler, RefObject } from 'react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import type { ChangeEvent, ChangeEventHandler, RefObject } from "react"
|
||||
import { useState, useEffect } from "react"
|
||||
|
||||
interface ClearInputOptions {
|
||||
inputRef: RefObject<HTMLInputElement | null>
|
||||
@@ -26,7 +26,7 @@ export function clearInput({
|
||||
if (isControlled && onChange) {
|
||||
// For controlled components: call onChange with a synthetic event
|
||||
const syntheticEvent = {
|
||||
target: { value: '' },
|
||||
target: { value: "" },
|
||||
currentTarget: inputRef.current,
|
||||
} as ChangeEvent<HTMLInputElement>
|
||||
onChange(syntheticEvent)
|
||||
@@ -35,24 +35,24 @@ export function clearInput({
|
||||
// This triggers React's change detection properly
|
||||
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
||||
window.HTMLInputElement.prototype,
|
||||
'value'
|
||||
"value"
|
||||
)?.set
|
||||
|
||||
if (nativeInputValueSetter) {
|
||||
nativeInputValueSetter.call(inputRef.current, '')
|
||||
nativeInputValueSetter.call(inputRef.current, "")
|
||||
} else {
|
||||
inputRef.current.value = ''
|
||||
inputRef.current.value = ""
|
||||
}
|
||||
|
||||
// Dispatch input event to trigger any listeners
|
||||
const inputEvent = new Event('input', {
|
||||
const inputEvent = new Event("input", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
})
|
||||
inputRef.current.dispatchEvent(inputEvent)
|
||||
|
||||
// Also dispatch change event
|
||||
const changeEvent = new Event('change', {
|
||||
const changeEvent = new Event("change", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
})
|
||||
@@ -74,16 +74,16 @@ export function useInputHasValue(
|
||||
const [hasValue, setHasValue] = useState(() => {
|
||||
// For controlled components, check value prop
|
||||
if (value !== undefined) {
|
||||
return String(value ?? '').trim().length > 0
|
||||
return String(value ?? "").trim().length > 0
|
||||
}
|
||||
// For uncontrolled, check ref
|
||||
return String(inputRef.current?.value ?? '').trim().length > 0
|
||||
return String(inputRef.current?.value ?? "").trim().length > 0
|
||||
})
|
||||
|
||||
// Sync with controlled value changes
|
||||
useEffect(() => {
|
||||
if (value !== undefined) {
|
||||
setHasValue(String(value ?? '').trim().length > 0)
|
||||
setHasValue(String(value ?? "").trim().length > 0)
|
||||
}
|
||||
}, [value])
|
||||
|
||||
@@ -102,17 +102,17 @@ export function useInputHasValue(
|
||||
setTimeout(() => {
|
||||
const target = (event?.target as HTMLInputElement) || inputRef.current
|
||||
if (target) {
|
||||
setHasValue((target.value ?? '').trim().length > 0)
|
||||
setHasValue((target.value ?? "").trim().length > 0)
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
|
||||
input.addEventListener('input', updateHasValue)
|
||||
input.addEventListener("input", updateHasValue)
|
||||
// Also listen to change event as a fallback
|
||||
input.addEventListener('change', updateHasValue)
|
||||
input.addEventListener("change", updateHasValue)
|
||||
return () => {
|
||||
input.removeEventListener('input', updateHasValue)
|
||||
input.removeEventListener('change', updateHasValue)
|
||||
input.removeEventListener("input", updateHasValue)
|
||||
input.removeEventListener("change", updateHasValue)
|
||||
}
|
||||
}, [value, inputRef])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user