Skip to content
Draft
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
7 changes: 7 additions & 0 deletions .storybook/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ const preview: Preview = {
date: /Date$/i,
},
},

a11y: {
// 'todo' - show a11y violations in the test UI only
// 'error' - fail CI on a11y violations
// 'off' - skip a11y checks entirely
test: 'todo',
},
},
decorators: [
withThemeByClassName({
Expand Down
3 changes: 2 additions & 1 deletion .storybook/vitest.setup.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import * as a11yAddonAnnotations from '@storybook/addon-a11y/preview'
import { beforeAll } from 'vitest'
import { setProjectAnnotations } from '@storybook/react-vite'
import * as projectAnnotations from './preview'

// This is an important step to apply the right configuration when testing your stories.
// More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations
const project = setProjectAnnotations([projectAnnotations])
const project = setProjectAnnotations([a11yAddonAnnotations, projectAnnotations])

beforeAll(project.beforeAll)
18 changes: 9 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@
"packageManager": "[email protected]",
"devDependencies": {
"@eslint/js": "^9.26.0",
"@storybook/addon-a11y": "^9.0.0-rc.2",
"@storybook/addon-designs": "^10.0.0",
"@storybook/addon-docs": "^9.0.0-rc.2",
"@storybook/addon-themes": "^9.0.0-rc.2",
"@storybook/addon-vitest": "9.0.0-rc.2",
"@storybook/react-vite": "^9.0.0-rc.2",
"@storybook/test-runner": "^0.22.0",
"@storybook/addon-a11y": "^10.1.11",
"@storybook/addon-designs": "^11.1.1",
"@storybook/addon-docs": "^10.1.11",
"@storybook/addon-themes": "^10.1.11",
"@storybook/addon-vitest": "10.1.11",
"@storybook/react-vite": "^10.1.11",
"@storybook/test-runner": "^0.24.2",
"@types/node": "^22.15.3",
"@types/react": "^18.2.0",
"@vitejs/plugin-react": "^4.4.1",
Expand All @@ -37,13 +37,13 @@
"eslint": "^9.26.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"eslint-plugin-storybook": "^9.0.0-rc.2",
"eslint-plugin-storybook": "^10.1.11",
"globals": "^16.0.0",
"markdownlint-cli": "^0.45.0",
"playwright": "^1.56.1",
"prettier": "^3.5.3",
"shadcn": "2.5.0",
"storybook": "^9.0.0-rc.2",
"storybook": "^10.1.11",
"typescript": "~5.7.2",
"typescript-eslint": "^8.31.1",
"vite": "^6.3.1",
Expand Down
143 changes: 131 additions & 12 deletions src/components/ui/input.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,139 @@
import * as React from 'react'

import { cn } from '../../lib/utils'
import { useEffect, useRef, useState } from 'react'

interface OwnProps {
// Something that needs to appear before the start, outside the field
beforeStartDecoration?: React.ReactNode

// Something that needs to appear at the start, inside the field
startDecoration?: React.ReactNode

// Something that needs to appear at the end, inside the field
endDecoration?: React.ReactNode

// Something that needs to appear after the end, outside the field
afterEndDecoration?: React.ReactNode
}

function Input({
className,
type,
beforeStartDecoration,
startDecoration,
endDecoration,
afterEndDecoration,
...props
}: React.ComponentProps<'input'> & OwnProps) {
const beforeStartDecorationRef = useRef<HTMLSpanElement>(null)
const [beforeStartDecorationWidth, setBeforeStartDecorationWidth] = useState<number>(0)
const startDecorationRef = useRef<HTMLSpanElement>(null)
const [startDecorationWidth, setStartDecorationWidth] = useState<number>(0)
const endDecorationRef = useRef<HTMLSpanElement>(null)
const [endDecorationWidth, setEndDecorationWidth] = useState<number>(0)
const afterEndDecorationRef = useRef<HTMLSpanElement>(null)
const [afterEndDecorationWidth, setAfterEndDecorationWidth] = useState<number>(0)

useEffect(() => {
if (!!beforeStartDecoration && !!beforeStartDecorationRef.current) {
const rect = beforeStartDecorationRef.current.getBoundingClientRect()
setBeforeStartDecorationWidth(rect.width)
} else {
setBeforeStartDecorationWidth(0)
}
}, [beforeStartDecoration, beforeStartDecorationRef.current])

Check warning on line 45 in src/components/ui/input.tsx

View workflow job for this annotation

GitHub Actions / lint

React Hook useEffect has an unnecessary dependency: 'beforeStartDecorationRef.current'. Either exclude it or remove the dependency array. Mutable values like 'beforeStartDecorationRef.current' aren't valid dependencies because mutating them doesn't re-render the component

useEffect(() => {
if (!!startDecoration && !!startDecorationRef.current) {
const rect = startDecorationRef.current.getBoundingClientRect()
setStartDecorationWidth(rect.width)
} else {
setStartDecorationWidth(0)
}
}, [startDecoration, startDecorationRef.current])

Check warning on line 54 in src/components/ui/input.tsx

View workflow job for this annotation

GitHub Actions / lint

React Hook useEffect has an unnecessary dependency: 'startDecorationRef.current'. Either exclude it or remove the dependency array. Mutable values like 'startDecorationRef.current' aren't valid dependencies because mutating them doesn't re-render the component

useEffect(() => {
if (!!endDecoration && !!endDecorationRef.current) {
const rect = endDecorationRef.current.getBoundingClientRect()
setEndDecorationWidth(rect.width)
} else {
setEndDecorationWidth(0)
}
}, [endDecoration, endDecorationRef.current]) // Re-run if content changes

Check warning on line 63 in src/components/ui/input.tsx

View workflow job for this annotation

GitHub Actions / lint

React Hook useEffect has an unnecessary dependency: 'endDecorationRef.current'. Either exclude it or remove the dependency array. Mutable values like 'endDecorationRef.current' aren't valid dependencies because mutating them doesn't re-render the component

useEffect(() => {
if (!!afterEndDecoration && !!afterEndDecorationRef.current) {
const rect = afterEndDecorationRef.current.getBoundingClientRect()
setAfterEndDecorationWidth(rect.width)
} else {
setAfterEndDecorationWidth(0)
}
}, [afterEndDecoration, afterEndDecorationRef.current]) // Re-run if content changes

Check warning on line 72 in src/components/ui/input.tsx

View workflow job for this annotation

GitHub Actions / lint

React Hook useEffect has an unnecessary dependency: 'afterEndDecorationRef.current'. Either exclude it or remove the dependency array. Mutable values like 'afterEndDecorationRef.current' aren't valid dependencies because mutating them doesn't re-render the component

const startDecoratorStyle = beforeStartDecorationWidth
? { paddingLeft: `${8 + beforeStartDecorationWidth}px` }
: {}

const startStyle =
!!startDecorationWidth || beforeStartDecorationWidth
? { paddingLeft: `${16 + beforeStartDecorationWidth + startDecorationWidth}px` }
: {}

const endStyle =
!!endDecorationWidth || !afterEndDecorationWidth
? { paddingRight: `${16 + endDecorationWidth + afterEndDecorationWidth}px` }
: {}

const endDecoratorStyle = !!afterEndDecorationWidth

Check failure on line 88 in src/components/ui/input.tsx

View workflow job for this annotation

GitHub Actions / lint

Redundant double negation
? { paddingRight: `${8 + afterEndDecorationWidth}px` }
: {}

function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
return (
<input
type={type}
data-slot="input"
className={cn(
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
className
)}
{...props}
/>
<>
<div className={'relative'}>
{!!beforeStartDecoration && (
<span
className={'absolute left-2.5 top-2.5 pr-2.5'}
style={{ borderRight: '2px solid black' }}
ref={beforeStartDecorationRef}
>
{beforeStartDecoration}
</span>
)}
{!!startDecoration && (
<span className={'absolute left-2.5 top-2.5'} style={startDecoratorStyle} ref={startDecorationRef}>
{startDecoration}
</span>
)}
<input
type={type}
data-slot="input"
className={cn(
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
className
)}
style={{ ...startStyle, ...endStyle }}
{...props}
/>
{!!endDecoration && (
<span className={'absolute right-2.5 top-2.5'} style={endDecoratorStyle} ref={endDecorationRef}>
{endDecoration}
</span>
)}
{!!afterEndDecoration && (
<span
className={'absolute right-2.5 top-2.5 pl-2.5'}
style={{ borderLeft: '2px solid black' }}
ref={afterEndDecorationRef}
>
{afterEndDecoration}
</span>
)}
</div>
</>
)
}

Expand Down
46 changes: 42 additions & 4 deletions src/stories/Input/Input.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'
import { Input } from '../../components'
import { Label } from '../../components'
import { expect, within, userEvent } from 'storybook/test'
import { SearchIcon } from 'lucide-react'
import { Rocket as LaunchIcon, Settings as SettingsIcon } from 'lucide-react'
import { SearchInput as SearchInputCmp } from '../../components/input'
import { useState } from 'react'

Expand Down Expand Up @@ -48,11 +48,49 @@ export const Default: Story = {
},
}

export const WithIcon: Story = {
export const WithManualIcon: Story = {
render: () => (
<div className="relative w-[300px]">
<SearchIcon className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input type="search" placeholder="Search..." className="pl-8" />
<SettingsIcon className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input placeholder="Search..." className="pl-8" />
</div>
),
}

export const WithInternalStartAndEndDecoration: Story = {
render: () => (
<div className="w-[300px]">
<Input
placeholder="Search..."
startDecoration={<SettingsIcon className="h-4 w-4 text-muted-foreground" />}
endDecoration={<LaunchIcon className="h-4 w-4 text-muted-foreground" />}
/>
</div>
),
}

export const WithExternalStartAndEndDecoration: Story = {
render: () => (
<div className="w-[300px]">
<Input
placeholder="Search..."
beforeStartDecoration={<SettingsIcon className="h-4 w-4 text-muted-foreground" />}
afterEndDecoration={<LaunchIcon className="h-4 w-4 text-muted-foreground" />}
/>
</div>
),
}

export const WithDoubleStartAndEndDecoration: Story = {
render: () => (
<div className="w-[300px]">
<Input
placeholder="Search..."
beforeStartDecoration={<SettingsIcon className="h-4 w-4 text-muted-foreground" />}
startDecoration={<SettingsIcon className="h-4 w-4" />}
endDecoration={<LaunchIcon className="h-4 w-4" />}
afterEndDecoration={<LaunchIcon className="h-4 w-4 text-muted-foreground" />}
/>
</div>
),
}
Expand Down
2 changes: 1 addition & 1 deletion vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import tailwindcss from '@tailwindcss/vite'
import path from 'node:path'
import { defineConfig, UserConfig } from 'vite'
import react from '@vitejs/plugin-react'
import pkg from './package.json' assert { type: 'json' }
import pkg from './package.json' with { type: 'json' }

export default defineConfig({
base: './',
Expand Down
Loading
Loading