-
Notifications
You must be signed in to change notification settings - Fork 2
feat: Input primitive #54
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
Merged
Merged
Changes from 4 commits
Commits
Show all changes
30 commits
Select commit
Hold shift + click to select a range
d2dce7f
feat: add input package, stories, and basic input
amarques-godaddy 2b19009
feat: add input tests and basic form example
amarques-godaddy 1151797
feat: add input to control, add tests, update examples
amarques-godaddy 92fe7c5
fix: remove unnecessary comments for test
amarques-godaddy b7364f9
fix: merge changes from main branch in
amarques-godaddy 9ffb66a
fix: update package lock
amarques-godaddy 5fb97bf
fix: add changeset and remove unused import from input test
amarques-godaddy 12d76cb
fix: remove unnecessary example prop
amarques-godaddy 5d9e2cb
fix: inline data attribute values
amarques-godaddy 4650ee7
fix: remove duplicate render prop types and undo linter fixes
amarques-godaddy 06c418c
fix: add full form test and remove unnecessary tests
amarques-godaddy 2ba86ad
fix: use container and text in examples, update readme
amarques-godaddy ed70dc9
Merge branch 'main' into input-primitive
amarques-godaddy 73c3a01
fix: Simplify InputProps types and readme
amarques-godaddy de7b503
Merge branch 'main' into input-primitive
amarques-godaddy 5f0b454
fix: Replace native input in checkbox and radio, use forward package
amarques-godaddy b6dbd55
fix: simplify isTextInput to mirror React Aria implementation
amarques-godaddy 62f1659
Merge branch 'main' into input-primitive
amarques-godaddy 145b57b
fix: remove withForwardRef from Input
amarques-godaddy 082df50
fix: merge main into branch
amarques-godaddy ee098a2
fix: add input imports back
amarques-godaddy 3ce14f4
fix: switch labels and pre with container, remove mergeProps spread
amarques-godaddy 5ab95aa
fix: Assign types to the args array
3rd-Eden 9f73b60
fix: update storybook config, make file paths more specific
amarques-godaddy e21fcac
fix: flatten args for use in useProps, add mergedProps, ref still not…
amarques-godaddy f9f6e5b
fix: update useProps to latest to use ref from destructured object
amarques-godaddy 8230052
fix: correct package references
3rd-Eden 33e0433
fix: use props.ref instead of destructured ref
amarques-godaddy 7c1089e
fix: add noops and remove story alert to reduce test warnings
amarques-godaddy 59200fa
fix: remake changeset, remove as any from checkbox ref
amarques-godaddy File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| # @bento/input |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| The MIT License (MIT) | ||
|
|
||
| Copyright (c) 2025 GoDaddy Operating Company, LLC. | ||
|
|
||
| Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: | ||
|
|
||
| The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. | ||
|
|
||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,171 @@ | ||
| import { | ||
| Meta, | ||
| Story, | ||
| ArgTypes, | ||
| Controls, | ||
| Source, | ||
| } from '@storybook/addon-docs/blocks'; | ||
| import * as Stories from './input.stories.tsx'; | ||
|
|
||
| import SourceControlled from './examples/controlled.tsx?raw'; | ||
| import SourceForm from './examples/basic-form.tsx?raw'; | ||
| import SourceUncontrolled from './examples/uncontrolled.tsx?raw'; | ||
|
|
||
| <Meta of={Stories} name="Overview" /> | ||
|
|
||
| # Input | ||
|
|
||
| The `@bento/input` package provides a universal input primitive component that renders an `<input>` element with React Aria interactions. It supports all HTML input types with proper accessibility, hover, and focus management. | ||
|
|
||
| This primitive is built with accessibility and flexibility in mind, providing state-based render props, comprehensive data attributes, and integration with React Aria's focus and hover hooks. It can be used for text inputs, checkboxes, radio buttons, file uploads, and any other input type supported by HTML. | ||
|
|
||
| ## Installation | ||
|
|
||
| ```shell | ||
| npm install --save @bento/input | ||
| ``` | ||
|
|
||
| ## Props | ||
|
|
||
| The `@bento/input` package exports the `Input` component: | ||
|
|
||
| ```jsx | ||
| import { Input } from '@bento/input'; | ||
|
|
||
| <Input type="text" placeholder="Enter text" /> | ||
| ``` | ||
|
|
||
| The following properties are available to be used on the `Input` component: | ||
|
|
||
| <ArgTypes of={Stories.Props} /> | ||
|
|
||
| For all other properties specified on the `Input` component, the component | ||
| will pass them down to the underlying `<input>` element. This includes properties | ||
| such as `id`, `data-*` attributes, or additional ARIA attributes that you might | ||
| need for specialized use cases. | ||
|
|
||
| ## Examples | ||
|
|
||
| ### Controlled Input | ||
|
|
||
| The most common pattern for the `Input` component is to use it as a controlled component, where the value is managed by React state. This allows you to easily read and update the input value. | ||
|
|
||
| <Source language='tsx' code={SourceControlled} /> | ||
| <Story of={Stories.Controlled} inline /> | ||
| <Controls of={Stories.Controlled} /> | ||
|
|
||
| In this example, the input value is stored in state and updated via the `onChange` handler. This is the recommended pattern for most use cases where you need to read or validate the input value. | ||
|
|
||
| ### Uncontrolled Input | ||
|
|
||
| For simpler use cases where you don't need to track the input value in React state, you can use an uncontrolled input with a `defaultValue`. The DOM will manage the input's value internally. | ||
|
|
||
| <Source language='tsx' code={SourceUncontrolled} /> | ||
| <Story of={Stories.Uncontrolled} inline /> | ||
| <Controls of={Stories.Uncontrolled} /> | ||
|
|
||
| Uncontrolled inputs are useful when you only need to read the value on form submission, or when integrating with form libraries that manage values through refs. | ||
|
|
||
| ### Form Example | ||
|
|
||
| The `Input` component supports all HTML input types, making it versatile for building complete forms. This example demonstrates multiple input types working together in a single form. | ||
|
|
||
| <Source language='tsx' code={SourceForm} /> | ||
| <Story of={Stories.BasicForm} inline collapsed /> | ||
| <Controls of={Stories.BasicForm} /> | ||
|
|
||
| The component intelligently handles type-specific props, ensuring that only relevant attributes are applied to each input type. For example, `min` and `max` are only applied to number and range inputs, while `checked` is only applied to checkbox and radio inputs. | ||
|
|
||
| ## Usage Guidelines | ||
|
|
||
| The `Input` component is a low-level primitive that provides the foundation for building input fields. Understanding when and how to use it will help you create accessible, user-friendly forms. | ||
|
|
||
| ### Supported Input Types | ||
|
|
||
| The `Input` component supports all HTML input types: | ||
|
|
||
| **Text-based inputs:** `text`, `email`, `password`, `url`, `tel`, `search` | ||
| **Numeric inputs:** `number`, `range` | ||
| **Date/time inputs:** `date`, `datetime-local`, `time`, `month`, `week` | ||
| **Choice inputs:** `checkbox`, `radio` | ||
| **File input:** `file` | ||
| **Color input:** `color` | ||
| **Hidden input:** `hidden` | ||
|
|
||
| Each input type receives appropriate type-specific props automatically. You don't need to worry about which props are valid for which types—the component handles this for you. | ||
|
|
||
| ### Accessibility Best Practices | ||
|
|
||
| Always provide labels for your inputs using the `<label>` element with a matching `htmlFor` attribute, or by using `aria-label` or `aria-labelledby`. Never rely solely on `placeholder` text for labeling, as it disappears when the user starts typing. | ||
|
|
||
| For invalid inputs, set `aria-invalid` to provide semantic meaning to assistive technologies. Combine this with `aria-describedby` to reference error messages that explain what went wrong. | ||
|
|
||
| Use appropriate input types to enable better mobile keyboards and native browser features. For example, `type="email"` shows an email-optimized keyboard on mobile devices, while `type="date"` shows a native date picker. | ||
|
|
||
| ## Accessibility | ||
|
|
||
| The `Input` component is built with accessibility as a core requirement. It provides comprehensive keyboard support, ARIA attributes, and integration with React Aria's focus management system. | ||
|
|
||
| **Keyboard Navigation** | ||
|
|
||
| All input types are fully keyboard accessible. Text inputs can be focused with Tab and navigated with arrow keys. Checkboxes and radio buttons can be toggled with Space. The component integrates with React Aria's `useFocusRing` hook to provide intelligent focus indicators that only appear during keyboard navigation, not mouse clicks. | ||
|
|
||
| **Focus Management** | ||
|
|
||
| The component distinguishes between mouse focus and keyboard focus through the `isFocused` and `isFocusVisible` states. This allows you to style focus rings appropriately—showing them only when the user is navigating with a keyboard, which reduces visual noise for mouse users while maintaining accessibility for keyboard users. | ||
|
|
||
| **ARIA Support** | ||
|
|
||
| The component passes through all ARIA attributes you provide, including `aria-label`, `aria-labelledby`, `aria-describedby`, and `aria-invalid`. These attributes are essential for screen reader users to understand the purpose and state of form fields. | ||
|
|
||
| **State Communication** | ||
|
|
||
| The component provides comprehensive data attributes that communicate its state to both CSS selectors and assistive technologies. These include `data-focused`, `data-focus-visible`, `data-hovered`, `data-disabled`, `data-invalid`, `data-readonly`, `data-required`, `data-empty`, and `data-checked`. These attributes make it easy to style inputs based on their state and ensure consistent visual feedback. | ||
|
amarques-godaddy marked this conversation as resolved.
Outdated
|
||
|
|
||
| ## Customization | ||
|
|
||
| The `Input` component is built using the `@bento/slots` package, allowing you to customize styling based on component state through render props and data attributes. | ||
|
|
||
| ### Render Props | ||
|
|
||
| The `className` and `style` props can accept functions that receive render props describing the current state of the input: | ||
|
amarques-godaddy marked this conversation as resolved.
Outdated
|
||
|
|
||
| ```jsx | ||
| <Input | ||
| type="text" | ||
| className={(state) => | ||
| state.isFocusVisible ? 'input-focused' : 'input-default' | ||
| } | ||
| style={(state) => ({ | ||
| borderColor: state.isInvalid ? 'red' : state.isFocused ? 'blue' : 'gray' | ||
| })} | ||
| /> | ||
| ``` | ||
|
|
||
| The render props object includes: | ||
|
amarques-godaddy marked this conversation as resolved.
Outdated
|
||
| - `isHovered`: Whether the input is currently hovered with a mouse | ||
| - `isFocused`: Whether the input is focused (via mouse or keyboard) | ||
| - `isFocusVisible`: Whether the input is keyboard focused | ||
| - `isDisabled`: Whether the input is disabled | ||
| - `isInvalid`: Whether the input has `aria-invalid` set | ||
|
|
||
| ### Data Attributes | ||
|
|
||
| The component automatically applies data attributes that correspond to its state, allowing you to style with CSS selectors: | ||
|
|
||
| | Attribute | Description | Example Values | | ||
| | ---------------------- | ---------------------------------------------- | ----------------- | | ||
| | `data-focused` | Whether the input is focused | "true" / "false" | | ||
| | `data-focus-visible` | Whether keyboard focus ring should be visible | "true" / "false" | | ||
| | `data-hovered` | Whether the input is hovered | "true" / "false" | | ||
| | `data-disabled` | Whether the input is disabled | "true" / "false" | | ||
| | `data-invalid` | Whether the input is invalid | "true" / "false" | | ||
| | `data-readonly` | Whether the input is read-only | "true" / "false" | | ||
| | `data-required` | Whether the input is required | "true" / "false" | | ||
| | `data-empty` | Whether the input has no value | "true" / "false" | | ||
| | `data-checked` | Whether checkbox/radio is checked | "true" / "false" | | ||
|
|
||
| ### Slots | ||
|
|
||
| The component is registered as `BentoInput` in the slots system. While the base Input component doesn't introduce additional slots, it can be extended with slot-based composition for building higher-level components. | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| /* v8 ignore next */ | ||
| import React, { useState } from 'react'; | ||
| import { Input } from '../src/index.tsx'; | ||
|
|
||
| export function BasicFormExample() { | ||
| const [formData, setFormData] = useState({ | ||
| name: '', | ||
| email: '' | ||
| }); | ||
|
|
||
| function handleChange(field: string) { | ||
| function onInputChange(e: React.ChangeEvent<HTMLInputElement>) { | ||
| setFormData((prev) => ({ ...prev, [field]: e.target.value })); | ||
| } | ||
| return onInputChange; | ||
| } | ||
|
|
||
| return ( | ||
| <form> | ||
| <h2>Basic Form</h2> | ||
|
|
||
| <div> | ||
| <label htmlFor="name">Name</label> | ||
|
amarques-godaddy marked this conversation as resolved.
Outdated
|
||
| <Input | ||
| id="name" | ||
| type="text" | ||
| value={formData.name} | ||
| onChange={handleChange('name')} | ||
| placeholder="Enter your name" | ||
| /> | ||
| </div> | ||
|
|
||
| <div> | ||
| <label htmlFor="email">Email</label> | ||
| <Input | ||
| id="email" | ||
| type="email" | ||
| value={formData.email} | ||
| onChange={handleChange('email')} | ||
| placeholder="you@example.com" | ||
| required | ||
| /> | ||
| </div> | ||
|
|
||
| <div> | ||
| <Input | ||
| type="submit" | ||
| onClick={function HandleClick(e) { | ||
| e.preventDefault(); | ||
| }} | ||
| value="Submit" | ||
| /> | ||
| </div> | ||
|
|
||
| <div> | ||
| <h3>Form Data (live preview):</h3> | ||
| <pre>{JSON.stringify(formData, null, 2)}</pre> | ||
| </div> | ||
| </form> | ||
| ); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| /* v8 ignore next */ | ||
| import React, { useState } from 'react'; | ||
| import { Input } from '@bento/input'; | ||
|
|
||
| export function ControlledInput() { | ||
| const [value, setValue] = useState(''); | ||
| return ( | ||
| <Input | ||
| value={value} | ||
| onChange={function ChangeEvent(e) { | ||
| setValue(e.target.value); | ||
| }} | ||
| type="text" | ||
| step={1} | ||
|
amarques-godaddy marked this conversation as resolved.
Outdated
|
||
| /> | ||
| ); | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.