Skip to content

PD-1669 Add size variants to text input #1072

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 16 commits into from
Dec 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fifty-zoos-wait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@leafygreen-ui/text-input': minor
---

Add sizeVariant prop
34 changes: 18 additions & 16 deletions packages/text-input/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,22 +68,24 @@ return (

## Properties

| Prop | Type | Description | Default |
| -------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | -------- |
| `id` | `string` | id associated with the TextInput component. | |
| `label` | `string` | Text shown in bold above the input element. | |
| `description` | `string` | Text that gives more detail about the requirements for the input. | |
| `optional` | `boolean` | Marks the input as optional | `false` |
| `disabled` | `boolean` | Disabled the input | `false` |
| `onChange` | `function` | The event handler function for the 'onchange' event. Accepts the change event object as its argument and returns nothing. | |
| `placeholder` | `string` | The placeholder text shown in the input field before the user begins typing. | |
| `errorMessage` | `string` | Text that gives more detail about the requirements for the input. | |
| `state` | `'none'`, `'valid'`, `'error'` | Describes the state of the TextInput element before and after the input has been validated | `'none'` |
| `value` | `string` | Sets the HTML `value` attribute. | `''` |
| `className` | `string` | Adds a className to the class attribute. | `''` |
| `type` | `'email'`, `'password'`, `'search'`, `'text'`, `'url'`, `'tel'`, `'number'` | Sets type for TextInput | `'text'` |
| `darkMode` | `boolean` | Determines whether or not the component will appear in dark mode. | `false` |
| ... | native `input` attributes | Any other props will be spread on the root `input` element | |
| Prop | Type | Description | Default |
| -------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | --------- |
| `id` | `string` | id associated with the TextInput component. | |
| `label` | `string` | Text shown in bold above the input element. | |
| `description` | `string` | Text that gives more detail about the requirements for the input. | |
| `optional` | `boolean` | Marks the input as optional | `false` |
| `disabled` | `boolean` | Disabled the input | `false` |
| `onChange` | `function` | The event handler function for the 'onchange' event. Accepts the change event object as its argument and returns nothing. | |
| `placeholder` | `string` | The placeholder text shown in the input field before the user begins typing. | |
| `errorMessage` | `string` | Text that gives more detail about the requirements for the input. | |
| `state` | `'none'`, `'valid'`, `'error'` | Describes the state of the TextInput element before and after the input has been validated | `'none'` |
| `value` | `string` | Sets the HTML `value` attribute. | `''` |
| `className` | `string` | Adds a className to the class attribute. | `''` |
| `type` | `'email'`, `'password'`, `'search'`, `'text'`, `'url'`, `'tel'`, `'number'` | Sets type for TextInput | `'text'` |
| `darkMode` | `boolean` | Determines whether or not the component will appear in dark mode. | `false` |
| `sizeVariant` | `'xsmall'`, `'small'`, `'default'`, `'large'`, | Sets the side padding, text size, and input height. | `default` |
| `baseFontSize` | `14`, `16` | Determines the base font-size of the component if the sizeVariant prop is set to default | `14` |
| ... | native `input` attributes | Any other props will be spread on the root `input` element | |

### Special Case: Aria Labels

Expand Down
17 changes: 16 additions & 1 deletion packages/text-input/src/TextInput.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { axe } from 'jest-axe';
import TextInput, { State } from './TextInput';
import TextInput, { State, SizeVariant } from './TextInput';

const error = 'This is the error message';
const validEmail = '[email protected]';
Expand Down Expand Up @@ -222,6 +222,21 @@ describe('packages/text-input', () => {
});
});

describe('when the "sizeVariant" is "large"', () => {
test('check if font-size is 18px', () => {
const { label } = renderTextInput({
value: validEmail,
sizeVariant: SizeVariant.Large,
optional: true,
...defaultProps,
});

expect(label).toHaveStyle({
fontSize: '18px',
});
});
});

Comment on lines +225 to +239
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works for now, but this type of test is better suited to something like Chromatic or Percy

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could even consider using this package https://www.npmjs.com/package/jest-image-snapshot

/* eslint-disable jest/expect-expect, jest/no-disabled-tests */
describe.skip('types behave as expected', () => {
test('TextInput throws error when neither aria-labelledby or label is supplied', () => {
Expand Down
6 changes: 6 additions & 0 deletions packages/text-input/src/TextInput.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,15 @@ storiesOf('TextInput', module)
Object.values(TextInputType),
TextInputType.Text,
)}
sizeVariant={select(
'Size Variant',
['xsmall', 'small', 'default', 'large'],
'default',
)}
errorMessage={text('Error Message', 'This is an error message')}
darkMode={darkMode}
handleValidation={value => console.log(`handleValidation ${value}`)}
baseFontSize={select('Base Font Size', [14, 16], 14)}
/>
</div>
</LeafyGreenProvider>
Expand Down
109 changes: 105 additions & 4 deletions packages/text-input/src/TextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,19 @@ const Mode = {

type Mode = typeof Mode[keyof typeof Mode];

export const SizeVariant = {
XSmall: 'xsmall',
Small: 'small',
Default: 'default',
Large: 'large',
} as const;

export type SizeVariant = typeof SizeVariant[keyof typeof SizeVariant];

export const BaseFontSize = 14 | 16;

export type BaseFontSize = typeof BaseFontSize;

interface TextInputProps extends HTMLElementProps<'input', HTMLInputElement> {
/**
* id associated with the TextInput component.
Expand Down Expand Up @@ -107,6 +120,18 @@ interface TextInputProps extends HTMLElementProps<'input', HTMLInputElement> {
handleValidation?: (value: string) => void;

['aria-labelledby']?: string;

/**
* determines the font size and padding.
*/

sizeVariant?: SizeVariant;

/**
* determines the base font size if sizeVariant is set to default.
*/

baseFontSize?: BaseFontSize;
}

type AriaLabels = 'label' | 'aria-labelledby';
Expand Down Expand Up @@ -229,16 +254,26 @@ const interactionRingColor: Record<Mode, Record<'valid' | 'error', string>> = {
},
};

interface SizeSet {
inputHeight: number;
inputText: number;
text: number;
lineHeight: number;
padding: number;
}

function getStatefulInputStyles({
state,
optional,
mode,
disabled,
sizeSet,
}: {
state: State;
optional: boolean;
mode: Mode;
disabled: boolean;
sizeSet: SizeSet;
}) {
switch (state) {
case State.Valid: {
Expand All @@ -264,13 +299,47 @@ function getStatefulInputStyles({

default: {
return css`
padding-right: ${optional ? 60 : 12}px;
padding-right: ${optional ? 60 : sizeSet.padding}px;
border-color: ${colorSets[mode].defaultBorder};
`;
}
}
}

function getSizeSets(baseFontSize: BaseFontSize, sizeVariant: SizeVariant) {
const sizeSets: Record<SizeVariant, SizeSet> = {
[SizeVariant.XSmall]: {
inputHeight: 22,
inputText: 12,
text: 14,
lineHeight: 20,
padding: 10,
},
[SizeVariant.Small]: {
inputHeight: 28,
inputText: 14,
text: 14,
lineHeight: 20,
padding: 10,
},
[SizeVariant.Default]: {
inputHeight: 36,
inputText: baseFontSize,
text: baseFontSize,
lineHeight: 20,
padding: 12,
},
[SizeVariant.Large]: {
inputHeight: 48,
inputText: 18,
text: 18,
lineHeight: 22,
padding: 16,
},
};
return sizeSets[sizeVariant];
}

/**
* # TextInput
*
Expand All @@ -291,6 +360,7 @@ function getStatefulInputStyles({
* @param props.value The current value of the input field. If a value is passed to this prop, component will be controlled by consumer.
* @param props.className className supplied to the TextInput container.
* @param props.darkMode determines whether or not the component appears in dark mode.
* @param props.sizeVariant determines the size of the text and the height of the input.
*/
const TextInput: React.ComponentType<
React.PropsWithRef<AccessibleTextInputProps>
Expand All @@ -310,8 +380,10 @@ const TextInput: React.ComponentType<
value: controlledValue,
className,
darkMode = false,
sizeVariant = SizeVariant.Default,
'aria-labelledby': ariaLabelledby,
handleValidation,
baseFontSize = 14,
...rest
}: AccessibleTextInputProps,
forwardRef: React.Ref<HTMLInputElement>,
Expand All @@ -321,6 +393,7 @@ const TextInput: React.ComponentType<
const [uncontrolledValue, setValue] = useState('');
const value = isControlled ? controlledValue : uncontrolledValue;
const id = useIdAllocator({ prefix: 'textinput', id: propsId });
const sizeSet = getSizeSets(baseFontSize, sizeVariant);

// Validation
const validation = useValidation<HTMLInputElement>(handleValidation);
Expand Down Expand Up @@ -356,12 +429,27 @@ const TextInput: React.ComponentType<
return (
<div className={cx(textInputStyle, className)}>
{label && (
<Label darkMode={darkMode} htmlFor={id} disabled={disabled}>
<Label
darkMode={darkMode}
htmlFor={id}
disabled={disabled}
className={cx(css`
font-size: ${sizeSet.text}px;
`)}
>
{label}
</Label>
)}
{description && (
<Description darkMode={darkMode}>{description}</Description>
<Description
darkMode={darkMode}
className={cx(css`
font-size: ${sizeSet.text}px;
line-height: ${sizeSet.lineHeight}px;
`)}
>
{description}
</Description>
)}
<div className={inputContainerStyle}>
<InteractionRing
Expand All @@ -386,6 +474,9 @@ const TextInput: React.ComponentType<
css`
color: ${colorSets[mode].inputColor};
background-color: ${colorSets[mode].inputBackgroundColor};
font-size: ${sizeSet.inputText}px;
height: ${sizeSet.inputHeight}px;
padding-left: ${sizeSet.padding}px;

&:focus {
border: 1px solid ${colorSets[mode].inputBackgroundColor};
Expand All @@ -410,7 +501,13 @@ const TextInput: React.ComponentType<
}
}
`,
getStatefulInputStyles({ state, optional, mode, disabled }),
getStatefulInputStyles({
state,
optional,
mode,
disabled,
sizeSet,
}),
)}
value={value}
required={!optional}
Expand Down Expand Up @@ -462,6 +559,8 @@ const TextInput: React.ComponentType<
errorMessageStyle,
css`
color: ${colorSets[mode].errorMessage};
font-size: ${sizeSet.text}px;
line-height: ${sizeSet.lineHeight}px;
`,
)}
>
Expand All @@ -487,6 +586,8 @@ TextInput.propTypes = {
state: PropTypes.oneOf(Object.values(State)),
value: PropTypes.string,
className: PropTypes.string,
sizeVariant: PropTypes.oneOf(Object.values(SizeVariant)),
baseFontSize: PropTypes.oneOf([14, 16]),
};

export default TextInput;
9 changes: 7 additions & 2 deletions packages/text-input/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import TextInput, { TextInputType, State } from './TextInput';
export { TextInputType, State };
import TextInput, {
TextInputType,
State,
SizeVariant,
BaseFontSize,
} from './TextInput';
export { TextInputType, State, SizeVariant, BaseFontSize };
export default TextInput;
21 changes: 20 additions & 1 deletion website/pages/component/text-input/example.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import React from 'react';
import TextInput, { State, TextInputType } from '@leafygreen-ui/text-input';
import TextInput, {
State,
TextInputType,
SizeVariant,
BaseFontSize,
} from '@leafygreen-ui/text-input';
import LiveExample, { KnobsConfigInterface } from 'components/live-example';

const knobsConfig: KnobsConfigInterface<{
Expand All @@ -12,6 +17,8 @@ const knobsConfig: KnobsConfigInterface<{
type: TextInputType;
darkMode: boolean;
errorMessage: string;
sizeVariant: SizeVariant;
baseFontSize: BaseFontSize;
}> = {
label: {
type: 'text',
Expand Down Expand Up @@ -50,6 +57,12 @@ const knobsConfig: KnobsConfigInterface<{
default: TextInputType.Text,
label: 'Type',
},
sizeVariant: {
type: 'select',
options: Object.values(SizeVariant),
default: 'default',
label: 'Size Variant',
},
darkMode: {
type: 'boolean',
default: false,
Expand All @@ -61,6 +74,12 @@ const knobsConfig: KnobsConfigInterface<{
'The team name that you entered is not unique, please pick another',
label: 'Error Message',
},
baseFontSize: {
type: 'select',
options: [14, 16],
default: 14,
label: 'Base Font Size',
},
};

export default function TextInputLiveExample() {
Expand Down