-
Notifications
You must be signed in to change notification settings - Fork 471
feat(headless): add input primitive #9647
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| --- | ||
| --- | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| # Input | ||
|
|
||
| An unstyled native input primitive for standalone fields and compound controls. It supports the shared `render` escape hatch and reflects native state through `data-disabled`, `data-invalid`, and `data-readonly` attributes for styling. | ||
|
|
||
| ```tsx | ||
| import { Input } from '@clerk/headless/input'; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win Use the local primitive import in this README. Import Based on learnings: primitive README examples should use internal 🤖 Prompt for AI AgentsSource: Learnings |
||
|
|
||
| <Input | ||
| aria-label='Domain' | ||
| placeholder='example.com' | ||
| />; | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { Input, type InputProps } from './input'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| import { cleanup, render, screen } from '@testing-library/react'; | ||
| import React from 'react'; | ||
| import { afterEach, describe, expect, it } from 'vitest'; | ||
|
|
||
| import { Input } from './input'; | ||
|
|
||
| afterEach(() => cleanup()); | ||
|
|
||
| describe('Input', () => { | ||
| it('renders a native input and forwards its props and ref', () => { | ||
| const ref = React.createRef<HTMLInputElement>(); | ||
| render( | ||
| <Input | ||
| ref={ref} | ||
| aria-label='Name' | ||
| name='name' | ||
| />, | ||
| ); | ||
|
|
||
| const input = screen.getByRole('textbox', { name: 'Name' }); | ||
| expect(input.tagName).toBe('INPUT'); | ||
| expect(input).toHaveAttribute('name', 'name'); | ||
| expect(ref.current).toBe(input); | ||
| }); | ||
|
|
||
| it('reflects native state for styling', () => { | ||
| render( | ||
| <Input | ||
| aria-label='Name' | ||
| aria-invalid='true' | ||
| disabled | ||
|
Comment on lines
+30
to
+31
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Cover both
As per coding guidelines: unit tests are required for all new functionality and must verify edge cases. 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| readOnly | ||
| />, | ||
| ); | ||
|
|
||
| const input = screen.getByRole('textbox', { name: 'Name' }); | ||
| expect(input).toHaveAttribute('data-disabled', ''); | ||
| expect(input).toHaveAttribute('data-invalid', ''); | ||
| expect(input).toHaveAttribute('data-readonly', ''); | ||
| }); | ||
|
|
||
| it('supports the headless render escape hatch', () => { | ||
| render( | ||
| <Input | ||
| render={<textarea aria-label='Biography' />} | ||
| data-input='grouped' | ||
| />, | ||
| ); | ||
|
|
||
| const input = screen.getByRole('textbox', { name: 'Biography' }); | ||
| expect(input.tagName).toBe('TEXTAREA'); | ||
| expect(input).toHaveAttribute('data-input', 'grouped'); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| 'use client'; | ||
|
|
||
| import React from 'react'; | ||
|
|
||
| import { type ComponentProps, useRender } from '../../utils'; | ||
|
|
||
| /** Props for the unstyled input primitive. */ | ||
| export type InputProps = ComponentProps<'input'>; | ||
|
|
||
| /** | ||
| * An unstyled native input with render-prop support and reflected state attributes. | ||
| * Styled layers can use it for standalone fields or place it inside compound controls. | ||
| */ | ||
| export const Input = React.forwardRef<HTMLInputElement, InputProps>(function Input( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift Make the forwarded ref match the rendered element.
Based on 🤖 Prompt for AI Agents |
||
| { render, disabled = false, readOnly = false, 'aria-invalid': ariaInvalid, ...otherProps }, | ||
| ref, | ||
| ) { | ||
| const invalid = ariaInvalid === true || ariaInvalid === 'true'; | ||
|
|
||
| return useRender({ | ||
| defaultTagName: 'input', | ||
| render, | ||
| ref, | ||
| state: { disabled, invalid, readOnly }, | ||
| stateAttributesMapping: { | ||
| disabled: value => (value ? { 'data-disabled': '' } : null), | ||
| invalid: value => (value ? { 'data-invalid': '' } : null), | ||
| readOnly: value => (value ? { 'data-readonly': '' } : null), | ||
| }, | ||
| props: { | ||
| disabled, | ||
| readOnly, | ||
| 'aria-invalid': ariaInvalid, | ||
| ...otherProps, | ||
| }, | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| import * as InputStories from './input.primitive.stories'; | ||
|
|
||
| # Input | ||
|
|
||
| An unstyled native input from `@clerk/headless`. It provides the shared `render` escape hatch and reflects native disabled, invalid, and read-only state as `data-*` attributes, but ships no styles. | ||
|
|
||
| ## Example | ||
|
|
||
| The demo renders the raw primitive with only the browser's native appearance. | ||
|
|
||
| <Story | ||
| name='Default' | ||
| storyModule={InputStories} | ||
| /> | ||
|
|
||
| ## Usage | ||
|
Comment on lines
+7
to
+16
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Use the required primitive documentation section order. This page omits As per coding guidelines: “Playground / Props / Usage are mandatory and always in this order.” Also applies to: 49-58 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
|
|
||
| ```tsx | ||
| import { Input } from '@clerk/headless/input'; | ||
|
|
||
| <label> | ||
| Email address | ||
| <Input | ||
| type='email' | ||
| name='email' | ||
| autoComplete='email' | ||
| placeholder='you@example.com' | ||
| /> | ||
| </label>; | ||
| ``` | ||
|
|
||
| Use `render` when a styled layer or compound control needs to supply another input-like element: | ||
|
|
||
| ```tsx | ||
| <Input | ||
| render={<textarea />} | ||
| aria-label='Biography' | ||
| /> | ||
| ``` | ||
|
|
||
| ## Parts | ||
|
|
||
| | Part | Default Element | Description | | ||
| | ------- | --------------- | ---------------------------------------------------------------- | | ||
| | `Input` | `<input>` | Native input with polymorphic rendering and reflected state data | | ||
|
|
||
| `Input` accepts the shared `render` prop and all standard attributes for its default element. | ||
|
|
||
| ## Props | ||
|
|
||
| | Prop | Type | Default | Description | | ||
| | -------------- | -------------------------------- | ----------- | -------------------------------------------------------- | | ||
| | `render` | `ReactElement \| RenderFunction` | `<input>` | Replaces the default element while merging props and ref | | ||
| | `disabled` | `boolean` | `false` | Disables the native control | | ||
| | `readOnly` | `boolean` | `false` | Makes the native control read-only | | ||
| | `aria-invalid` | `boolean \| 'true' \| 'false'` | `undefined` | Exposes the control's validation state | | ||
|
|
||
| All other native input attributes pass through unchanged. | ||
|
|
||
| ## Styling | ||
|
|
||
| The primitive exposes state through attributes that any styling system can target: | ||
|
|
||
| | Attribute | Description | | ||
| | --------------- | ------------------------------------- | | ||
| | `data-disabled` | Present when `disabled` is `true` | | ||
| | `data-invalid` | Present when `aria-invalid` is `true` | | ||
| | `data-readonly` | Present when `readOnly` is `true` | | ||
|
|
||
| ```css | ||
| input[data-invalid] { | ||
| border-color: red; | ||
| } | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import { Input } from '@clerk/headless/input'; | ||
|
|
||
| import type { StoryMeta } from '@/lib/types'; | ||
|
|
||
| export const meta: StoryMeta = { | ||
| group: 'Primitives', | ||
| title: 'Input', | ||
| source: 'packages/headless/src/primitives/input/index.ts', | ||
| }; | ||
|
|
||
| export function Default() { | ||
| return ( | ||
| <div> | ||
| <label htmlFor='headless-input-demo'>Email address</label> | ||
| <Input | ||
| id='headless-input-demo' | ||
| type='email' | ||
| name='email' | ||
| autoComplete='email' | ||
| placeholder='you@example.com' | ||
| /> | ||
| </div> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Add a package release entry to this changeset.
The file declares no package or version bump. The new
@clerk/headless/inputexport can therefore be omitted from the release plan. Add the appropriate minor bump and summary, or remove this file if no release is intended.As per coding guidelines: “Use Changesets for managing releases.”
🤖 Prompt for AI Agents
Source: Coding guidelines