-
Notifications
You must be signed in to change notification settings - Fork 40
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #389 from bcgov/feature/textfield
Add TextField component
- Loading branch information
Showing
10 changed files
with
531 additions
and
2 deletions.
There are no files selected for viewing
This file contains 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
125 changes: 125 additions & 0 deletions
125
packages/react-components/src/components/TextField/TextField.css
This file contains 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,125 @@ | ||
.bcds-react-aria-TextField { | ||
display: inline-flex; | ||
flex-direction: column; | ||
/* Hacks for `stretch`: https://caniuse.com/mdn-css_properties_max-width_stretch */ | ||
align-self: stretch; | ||
max-width: -moz-available; | ||
max-width: -webkit-fill-available; | ||
} | ||
|
||
/* Styles for the text label above the input field */ | ||
.bcds-react-aria-TextField--Label { | ||
font: var(--typography-regular-small-body); | ||
color: var(--typography-color-primary); | ||
padding: var(--layout-margin-hair) 0px; | ||
} | ||
|
||
.bcds-react-aria-TextField--Label .required { | ||
color: var(--typography-color-secondary); | ||
padding: var(--layout-padding-none) var(--layout-padding-xsmall); | ||
} | ||
|
||
/* Styles for the text description below the input field */ | ||
.bcds-react-aria-TextField--Description { | ||
font: var(--typography-regular-small-body); | ||
color: var(--typography-color-secondary); | ||
} | ||
|
||
/* Styles for the input field container */ | ||
.bcds-react-aria-TextField--container { | ||
color: var(--typography-color-primary); | ||
display: flex; | ||
flex-direction: row; | ||
align-items: center; | ||
gap: var(--layout-margin-small); | ||
background: var(--surface-color-forms-default); | ||
box-sizing: border-box; | ||
border: var(--layout-border-width-small) solid | ||
var(--surface-color-border-default); | ||
border-radius: var(--layout-border-radius-medium); | ||
margin: var(--layout-margin-small) var(--layout-margin-none); | ||
padding: var(--layout-padding-small) 12px; | ||
} | ||
|
||
/* Sizes */ | ||
.bcds-react-aria-TextField--container.small { | ||
/* using margin token is kludgy, consider adding component-specific token for input sizing */ | ||
height: var(--layout-margin-xlarge); | ||
min-height: var(--layout-margin-xlarge); | ||
} | ||
|
||
.bcds-react-aria-TextField--container.medium { | ||
/* using margin token is kludgy, consider adding component-specific token for input sizing */ | ||
height: var(--layout-margin-xxlarge); | ||
min-height: var(--layout-margin-xxlarge); | ||
} | ||
|
||
/* Text input field */ | ||
.bcds-react-aria-TextField--Input { | ||
font: var(--typography-regular-body); | ||
padding: var(--layout-padding-none); | ||
color: var(--typography-color-primary); | ||
border: none; | ||
flex-grow: 1; | ||
} | ||
|
||
.bcds-react-aria-TextField--Input::placeholder { | ||
color: var(--typography-color-placeholder); | ||
} | ||
|
||
/* Hover and focus states */ | ||
|
||
.bcds-react-aria-TextField--container | ||
> .bcds-react-aria-TextField--Input[data-focused] { | ||
outline: none; | ||
} | ||
|
||
.bcds-react-aria-TextField--container:focus-within { | ||
border-radius: var(--layout-border-radius-large); | ||
border: var(--layout-border-width-small) solid | ||
var(--surface-color-border-active); | ||
outline: solid var(--layout-border-width-medium) | ||
var(--surface-color-border-active); | ||
outline-offset: var(--layout-margin-hair); | ||
} | ||
|
||
.bcds-react-aria-TextField--container:hover { | ||
border-color: var(--surface-color-border-dark); | ||
} | ||
|
||
/* Disabled and invalid states */ | ||
|
||
.bcds-react-aria-TextField[data-disabled] | ||
> .bcds-react-aria-TextField--container { | ||
background: var(--surface-color-forms-disabled); | ||
cursor: not-allowed; | ||
} | ||
|
||
.bcds-react-aria-TextField--Input[data-disabled] { | ||
color: var(--typography-color-placeholder); | ||
cursor: not-allowed; | ||
} | ||
|
||
.bcds-react-aria-TextField[data-invalid] | ||
> .bcds-react-aria-TextField--container { | ||
border-radius: var(--layout-border-radius-medium); | ||
border: 1px solid var(--support-border-color-danger); | ||
background: var(--surface-color-forms-default); | ||
} | ||
|
||
.bcds-react-aria-TextField[data-readonly] | ||
> .bcds-react-aria-TextField--container { | ||
background: var(--surface-color-forms-disabled); | ||
} | ||
|
||
.bcds-react-aria-TextField[data-readonly] | ||
> .bcds-react-aria-TextField--container | ||
> .bcds-react-aria-TextField--Input { | ||
background: var(--surface-color-forms-disabled); | ||
} | ||
|
||
/* Styles for the error message slot */ | ||
.bcds-react-aria-TextField--Error { | ||
font: var(--typography-regular-small-body); | ||
color: var(--typography-color-danger); | ||
} |
92 changes: 92 additions & 0 deletions
92
packages/react-components/src/components/TextField/TextField.tsx
This file contains 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,92 @@ | ||
import { | ||
TextField as ReactAriaTextField, | ||
TextFieldProps as ReactAriaTextFieldProps, | ||
Input, | ||
Label, | ||
FieldError, | ||
Text, | ||
ValidationResult, | ||
} from "react-aria-components"; | ||
|
||
import "./TextField.css"; | ||
|
||
export interface TextFieldProps extends ReactAriaTextFieldProps { | ||
/* Sets size of text input field */ | ||
size?: "medium" | "small"; | ||
/* Sets text label above text input field */ | ||
label?: string; | ||
/* Sets optional description text below text input field */ | ||
description?: string; | ||
/* Used for data validation and error handling */ | ||
errorMessage?: string | ((validation: ValidationResult) => string); | ||
/* Icon slot to left of text input field */ | ||
iconLeft?: React.ReactElement; | ||
/* Icon slot to right of text input field */ | ||
iconRight?: React.ReactElement; | ||
} | ||
|
||
/* Icon displayed when input is in invalid state */ | ||
const iconError = ( | ||
<svg | ||
width="20" | ||
height="20" | ||
viewBox="0 0 20 20" | ||
xmlns="http://www.w3.org/2000/svg" | ||
> | ||
<g id="20px/Error icon"> | ||
<path | ||
id="Icon" | ||
d="M17.835 15.0312C18.335 15.9062 17.71 17 16.6787 17H3.33499C2.30374 17 1.67874 15.9062 2.14749 15.0312L8.83499 3.65625C9.11624 3.21875 9.55373 3 10.0225 3C10.46 3 10.8975 3.21875 11.1787 3.65625L17.835 15.0312ZM3.64749 15.5H16.3663L9.99123 4.65625L3.64749 15.5ZM10.0225 12.5625C10.5537 12.5625 10.9912 13 10.9912 13.5312C10.9912 14.0625 10.5537 14.5 10.0225 14.5C9.45998 14.5 9.02249 14.0625 9.02249 13.5312C9.02249 13 9.45998 12.5625 10.0225 12.5625ZM9.27249 7.75C9.27249 7.34375 9.58498 7 10.0225 7C10.4287 7 10.7725 7.34375 10.7725 7.75V10.75C10.7725 11.1875 10.4287 11.5 10.0225 11.5C9.58498 11.5 9.27249 11.1875 9.27249 10.75V7.75Z" | ||
fill="var(--icons-color-danger)" | ||
/> | ||
</g> | ||
</svg> | ||
); | ||
|
||
export default function TextField({ | ||
size, | ||
label, | ||
description, | ||
errorMessage, | ||
iconLeft, | ||
iconRight, | ||
...props | ||
}: TextFieldProps) { | ||
return ( | ||
<ReactAriaTextField className="bcds-react-aria-TextField" {...props}> | ||
{({ isRequired, isInvalid }) => ( | ||
<> | ||
{label && ( | ||
<Label className="bcds-react-aria-TextField--Label"> | ||
{label} | ||
{isRequired && ( | ||
<span className="bcds-react-aria-TextField--Label required"> | ||
(required) | ||
</span> | ||
)} | ||
</Label> | ||
)} | ||
<div | ||
className={`bcds-react-aria-TextField--container ${size === "small" ? "small" : "medium"}`} | ||
> | ||
{iconLeft} | ||
<Input className="bcds-react-aria-TextField--Input" /> | ||
{isInvalid && iconError} | ||
{iconRight} | ||
</div> | ||
{description && ( | ||
<Text | ||
slot="description" | ||
className={`bcds-react-aria-TextField--Description`} | ||
> | ||
{description} | ||
</Text> | ||
)} | ||
<FieldError className="bcds-react-aria-TextField--Error"> | ||
{errorMessage} | ||
</FieldError> | ||
</> | ||
)} | ||
</ReactAriaTextField> | ||
); | ||
} |
This file contains 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,2 @@ | ||
export { default } from "./TextField"; | ||
export type { TextFieldProps } from "./TextField"; |
This file contains 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
10 changes: 10 additions & 0 deletions
10
packages/react-components/src/pages/TextField/TextField.tsx
This file contains 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,10 @@ | ||
import { TextField } from "@/components"; | ||
|
||
export default function TextFieldPage() { | ||
return ( | ||
<> | ||
<h2>TextField</h2> | ||
<TextField /> | ||
</> | ||
); | ||
} |
This file contains 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,3 @@ | ||
import TextFieldPage from "./TextField"; | ||
|
||
export default TextFieldPage; |
This file contains 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 |
---|---|---|
@@ -1,6 +1,7 @@ | ||
import ButtonPage from "./Button"; | ||
import SelectPage from "./Select"; | ||
import TagGroupPage from "./TagGroup"; | ||
import TextFieldPage from "./TextField"; | ||
import TooltipPage from "./Tooltip"; | ||
|
||
export { ButtonPage, SelectPage, TagGroupPage, TooltipPage }; | ||
export { ButtonPage, SelectPage, TagGroupPage, TextFieldPage, TooltipPage }; |
This file contains 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,98 @@ | ||
{/* TextField.mdx */} | ||
|
||
import { | ||
Canvas, | ||
Controls, | ||
Meta, | ||
Primary, | ||
Source, | ||
Story, | ||
Subtitle, | ||
} from "@storybook/blocks"; | ||
|
||
import * as TextFieldStories from "./TextField.stories"; | ||
|
||
<Meta of={TextFieldStories} /> | ||
|
||
# Text Field | ||
|
||
<Subtitle> | ||
The text field component enables a user to enter text into an interface or | ||
form. | ||
</Subtitle> | ||
|
||
<Source | ||
code={`import { TextField } from "@bcgov/design-system-react-components"; `} | ||
language="typescript" | ||
/> | ||
|
||
## Usage and resources | ||
|
||
Learn more about working with the text field component: | ||
|
||
- [Usage and best practice guidance](https://www2.gov.bc.ca/gov/content/digital/design-system/components/text-field) | ||
- [View the text field component in Figma](#) | ||
|
||
This component is based on [React Aria TextField](https://react-spectrum.adobe.com/react-aria/TextField.html). Consult the React Aria documentation for additional technical information and a full list of supported props. | ||
|
||
### Validation | ||
|
||
Default and custom data validation support is built in, using the [React Aria FieldError subcomponent](https://react-spectrum.adobe.com/react-aria/forms.html#validation). | ||
|
||
### Placeholder text | ||
|
||
As a general rule, the `placeholder` attribute should not be used on text fields. While `placeholder` is technically supported, the use of 'ghost text' has [serious accessibility and usability issues](https://developer.mozilla.org/en-US/docs/Web/CSS/::placeholder#accessibility_concerns). | ||
|
||
## Controls | ||
|
||
<Primary of={TextFieldStories} /> | ||
<Controls of={TextFieldStories.TextFieldTemplate} /> | ||
|
||
## Configuration | ||
|
||
### Size | ||
|
||
The `size` prop enables you to choose between medium (default) and small (reduced height) versions: | ||
|
||
<Canvas of={TextFieldStories.MediumTextField} /> | ||
<Canvas of={TextFieldStories.SmallTextField} /> | ||
|
||
### Required | ||
|
||
By default, input fields are marked as optional. Use the `isRequired` prop to mark an input as required, and display a text label: | ||
|
||
<Canvas of={TextFieldStories.RequiredTextField} /> | ||
|
||
### Icons | ||
|
||
You can display icons in a text field by passing an image to either or both of the `iconLeft` and `iconRight` slots: | ||
|
||
<Canvas of={TextFieldStories.TextFieldWithIcons} /> | ||
|
||
Icons can be used to provide an additional visual indicator of a text field's function, like a search field: | ||
|
||
<Canvas of={TextFieldStories.SearchField} /> | ||
|
||
### Input types | ||
|
||
Use the `type` prop to define the type of input a text field expects. If no `type` prop is provided, the attribute defaults to `text`. | ||
|
||
For example, setting the type to `password` automatically obscures input: | ||
|
||
<Canvas of={TextFieldStories.PasswordField} /> | ||
|
||
Types like `url`, `tel` or `email` are used for data validation. Consult the MDN documentation for [a full list of valid input type attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#input_types). | ||
|
||
### States | ||
|
||
Use `isDisabled` to disable a text field. A disabled field cannot be focused or interacted with: | ||
|
||
<Canvas of={TextFieldStories.DisabledTextField} /> | ||
|
||
Use `isReadOnly` to lock a field to its current value. A read-only input can be focused and copied, but cannot be edited: | ||
|
||
<Canvas of={TextFieldStories.ReadOnlyTextField} /> | ||
|
||
The `isInvalid` prop should be set programmatically when an input is invalid. It renders an error icon in the input field, changes the border colour and displays an `errorMessage` (using the [FieldError subcomponent](https://react-spectrum.adobe.com/react-aria/TextField.html#validation)): | ||
|
||
<Canvas of={TextFieldStories.TextFieldError} /> |
Oops, something went wrong.