Skip to content

Commit

Permalink
Merge pull request #389 from bcgov/feature/textfield
Browse files Browse the repository at this point in the history
Add TextField component
  • Loading branch information
mkernohanbc authored Jul 17, 2024
2 parents d31b776 + eec554a commit b3cf196
Show file tree
Hide file tree
Showing 10 changed files with 531 additions and 2 deletions.
9 changes: 8 additions & 1 deletion packages/react-components/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ import "@bcgov/bc-sans/css/BC_Sans.css";

import { Button, Footer, FooterLinks, Header } from "@/components";
import useWindowDimensions from "@/hooks/useWindowDimensions";
import { ButtonPage, SelectPage, TagGroupPage, TooltipPage } from "@/pages";
import {
ButtonPage,
SelectPage,
TagGroupPage,
TooltipPage,
TextFieldPage,
} from "@/pages";

// This icon is available as a plain SVG at src/assets/icon-menu.svg
function SvgMenuIcon() {
Expand Down Expand Up @@ -142,6 +148,7 @@ function App() {
<SelectPage />
<TagGroupPage />
<TooltipPage />
<TextFieldPage />
</main>
<Footer />
<Footer
Expand Down
125 changes: 125 additions & 0 deletions packages/react-components/src/components/TextField/TextField.css
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 packages/react-components/src/components/TextField/TextField.tsx
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>
);
}
2 changes: 2 additions & 0 deletions packages/react-components/src/components/TextField/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default } from "./TextField";
export type { TextFieldProps } from "./TextField";
1 change: 1 addition & 0 deletions packages/react-components/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export { default as SvgBcLogo } from "./SvgBcLogo";
export { default as Tag } from "./Tag";
export { default as TagGroup } from "./TagGroup";
export { default as TagList } from "./TagList";
export { default as TextField } from "./TextField";
export { default as Tooltip, TooltipTrigger } from "./Tooltip";
10 changes: 10 additions & 0 deletions packages/react-components/src/pages/TextField/TextField.tsx
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 />
</>
);
}
3 changes: 3 additions & 0 deletions packages/react-components/src/pages/TextField/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import TextFieldPage from "./TextField";

export default TextFieldPage;
3 changes: 2 additions & 1 deletion packages/react-components/src/pages/index.ts
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 };
98 changes: 98 additions & 0 deletions packages/react-components/src/stories/TextField.mdx
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} />
Loading

0 comments on commit b3cf196

Please sign in to comment.