Skip to content

Commit

Permalink
feat(TextInputFloating): new component (#874)
Browse files Browse the repository at this point in the history
* wip

* wip

* lint

* Create TextInputFloating.test.tsx

* style lint

* update prefix and suffix styles

* div wrapper

* safari workaround

* Update FormControls.stories.mdx

* Update FormTheming.stories.mdx

* Create TextInputFloating.Playground.stories.tsx

* visual tests
  • Loading branch information
nathanyoung committed Feb 29, 2024
1 parent 20c3fbb commit a19211b
Show file tree
Hide file tree
Showing 9 changed files with 1,700 additions and 76 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
import { useState } from 'react';
import { action } from '@storybook/addon-actions';
import { Meta, Story, Canvas, ArgsTable } from '@storybook/addon-docs';
import { TextInputFloating } from './TextInputFloating';
import { Box } from '../Box/Box';
import { Icon } from '../Icon/Icon';

<Meta
title="Components/TextInputFloating/Overview"
component={TextInputFloating}
parameters={{
controls: { hideNoControlsWarning: true },
}}
/>

# TextInputFloating

Use `TextInputFloating` to create a floating label input field. It is a controlled component, which means that you need to provide a value and an `onChange` handler to update the value.

<Canvas withSource="open">
<Story name="Default">
{() => {
const [value, setValue] = useState('');
const [valuePw, setValuePw] = useState('');
return (
<Box gap="md">
<TextInputFloating
id="emailInput"
value={value}
label="Email"
placeholder="Enter your email address"
onChange={event => setValue(event.target.value)}
/>
<TextInputFloating
id="passwordInput"
value={valuePw}
label="Password"
type="password"
placeholder="Enter your password"
onChange={event => setValuePw(event.target.value)}
helpText="Password must be at least 8 characters long"
/>
</Box>
);
}}
</Story>
</Canvas>

## Props

<ArgsTable of={TextInputFloating} />

### Required

Use the `isRequired` prop to set the `required` and `aria-required` on the underlying input element.

<Canvas>
<Story name="Required">
{() => {
const [value, setValue] = useState('');
return (
<TextInputFloating
id="requiredInput"
value={value}
label="Required Input"
placeholder="Enter your email address"
onChange={event => setValue(event.target.value)}
isRequired
/>
);
}}
</Story>
</Canvas>

Customize the required indicator with the `requiredIndicator` prop.

<Canvas>
<Story name="Custom Required Indicator">
{() => {
const [value, setValue] = useState('');
return (
<TextInputFloating
id="customRequiredInput"
value={value}
label="Required Input"
onChange={event => setValue(event.target.value)}
isRequired
requiredIndicator=" (required)"
/>
);
}}
</Story>
</Canvas>

### Help Text

Use `helpText` to provide additional information about the input field.

<Canvas>
<Story name="Help Text">
{() => {
const [value, setValue] = useState('');
return (
<TextInputFloating
id="helpTextStory"
value={value}
label="Inverter Manufacturer"
onChange={event => setValue(event.target.value)}
helpText="Must be from the approved vendor list"
/>
);
}}
</Story>
</Canvas>

### Validation Error

Use the `error` prop to mark the input as invalid. `error` accepts a `boolean`, `string`, or `node`.

<Canvas>
<Story name="Error">
{() => {
const [value, setValue] = useState('');
const [value2, setValue2] = useState('Invalid Value');
return (
<Box gap="md">
<TextInputFloating
id="error1"
value={value}
label="email"
error="Required Input"
onChange={event => setValue(event.target.value)}
isRequired
/>
<TextInputFloating
id="error2"
value={value2}
label="email"
error="Must be a valid email address"
helpText="e.g. name@email.com"
onChange={event => setValue2(event.target.value)}
/>
</Box>
);
}}
</Story>
</Canvas>

### Disabled

Add `isDisabled`to give it a grayed out appearance, remove pointer events, and prevent focusing.

<Canvas>
<Story name="Disabled">
{() => {
const [value, setValue] = useState('');
const [value2, setValue2] = useState('Value');
return (
<Box gap="md">
<TextInputFloating
id="disabled"
value={value}
label="email"
onChange={event => setValue(event.target.value)}
isDisabled
/>
<TextInputFloating
id="disabledValue"
value={value2}
label="Disabled with Value"
onChange={event => setValue2(event.target.value)}
isDisabled
/>
</Box>
);
}}
</Story>
</Canvas>

### Clearable

Use the `onClear` prop to display a clear icon (x) when the input has a value. `onClear` will fire
a callback function when the clear icon is clicked, which can then be handled to clear the value.

<Canvas>
<Story name="Clearable">
{() => {
const [value, setValue] = useState('clear me');
return (
<TextInputFloating
id="clearableTextInput"
value={value}
label="Clearable Input"
onChange={event => setValue(event.target.value)}
onClear={event => setValue('')}
isRequired
/>
);
}}
</Story>
</Canvas>

### Prefix and Suffix

All that is required to render a basic version of the TextInput is a unique `id`, `label`, `value`, and an onchange event handler passed to the `onChange` prop.

<Canvas>
<Story name="With Prefix and Suffix">
{() => {
const [prefixValue0, setPrefixValue0] = useState('');
const [prefixValue1, setPrefixValue1] = useState('palmettosolar');
const [prefixValue2, setPrefixValue2] = useState('2.51');
const [prefixValue3, setPrefixValue3] = useState('');
const [prefixValue4, setPrefixValue4] = useState('Pre-populated Value');
return (
<Box gap="md">
<TextInputFloating
id="prefixSuffix0"
value={prefixValue0}
label="website address"
onChange={event => setPrefixValue0(event.target.value)}
prefix="https://"
placeholder="Enter your website address"
/>
<TextInputFloating
id="prefixSuffix1"
value={prefixValue1}
label="Social Media Handle"
onChange={event => setPrefixValue1(event.target.value)}
prefix="@"
/>
<TextInputFloating
id="prefixSuffix2"
value={prefixValue2}
label="Price Per Watt"
onChange={event => setPrefixValue2(event.target.value)}
prefix="$"
suffix="/watt"
/>
<TextInputFloating
id="prefixSuffix3"
value={prefixValue3}
label="Suffix Without Value"
onChange={event => setPrefixValue3(event.target.value)}
suffix={<Icon name="book" />}
/>
<TextInputFloating
id="prefixSuffix4"
value={prefixValue4}
label="Suffix with Clear"
placeholder="Contact name"
onChange={event => setPrefixValue4(event.target.value)}
onClear={event => setPrefixValue4('')}
suffix={<Icon name="search" />}
/>
</Box>
);
}}
</Story>
</Canvas>

### Sizes

There are only two sizes for TextInputFloating, `md` and `lg`, with `md` being the default.

<Canvas>
<Story name="Sizes">
{() => {
const [value1, setValue1] = useState('');
const [value2, setValue2] = useState('');
return (
<Box gap="md">
<TextInputFloating
id="mdTextInput"
value={value1}
label="Medium Input"
size="md"
onChange={event => setValue1(event.target.value)}
/>
<TextInputFloating
id="lgTextInput"
value={value2}
label="Large Input"
size="lg"
onChange={event => setValue2(event.target.value)}
/>
</Box>
);
}}
</Story>
</Canvas>

## Component Design Tokens

This component shares component design tokens with all form controls. For a complete list of tokens, see the [Theming Form Controls documentation](/docs/theming-form-controls--custom-theme-form).
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import React from 'react';
import { Meta, Story } from '@storybook/react/types-6-0';
import { TextInputFloating, TextInputFloatingProps } from './TextInputFloating';

export default {
title: 'Components/TextInputFloating/Playground',
component: TextInputFloating,
argTypes: {
autoComplete: {
control: 'boolean',
},
id: {
control: 'text',
},
label: {
control: 'text',
},
name: {
control: 'text',
},
value: {
control: 'text',
},
autoFocus: {
control: 'boolean',
},
error: {
control: 'text',
},
helpText: {
control: 'text',
},
hideLabel: {
control: 'boolean',
},
isClearable: {
control: 'boolean',
},
isDisabled: {
control: 'boolean',
},
isRequired: {
control: 'boolean',
},
className: {
control: 'text',
},
placeholder: {
control: 'text',
},
prefix: {
control: 'text',
},
suffix: {
control: 'text',
},
maxLength: {
control: 'number',
},
size: {
control: {
type: 'radio',
options: ['md', 'lg'],
},
},
requiredIndicator: {
control: 'text',
},
type: {
control: {
type: 'select',
options: ['text', 'password', 'email', 'tel', 'url', 'search'],
},
},
},
} as Meta;

const Template: Story<TextInputFloatingProps> = ({ ...args }) => (
<TextInputFloating {...args} />
);

export const Playground = Template.bind({});
Playground.args = {
id: 'playgroundTextInputFloating ',
label: 'Playground TextInputFloating ',
helpText: 'Helpful text',
name: 'playgroundTextInputFloating ',
};
Loading

0 comments on commit a19211b

Please sign in to comment.