Skip to content

Commit

Permalink
Design System: Add suffix to input component (#6355)
Browse files Browse the repository at this point in the history
  • Loading branch information
samwhale authored Feb 17, 2021
1 parent 166a4d7 commit e6b68a8
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 38 deletions.
115 changes: 92 additions & 23 deletions assets/src/design-system/components/input/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,20 @@
* External dependencies
*/
import PropTypes from 'prop-types';
import { useMemo } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import styled, { css } from 'styled-components';
import { v4 as uuidv4 } from 'uuid';
/**
* Internal dependencies
*/
import { Text } from '../typography';
import { themeHelpers, THEME_CONSTANTS } from '../../theme';
import { focusCSS } from '../../theme/helpers';

const Container = styled.div`
position: relative;
width: 100%;
min-width: 150px;
`;

const Label = styled(Text)`
Expand All @@ -37,15 +40,59 @@ const Label = styled(Text)`

const Hint = styled(Text)`
margin-top: 12px;
color: ${({ hasError, theme }) =>
theme.colors.fg[hasError ? 'negative' : 'tertiary']};
`;

const StyledInput = styled.input(
({ hasError, theme }) => css`
const Suffix = styled(Text)`
background: transparent;
color: ${({ theme }) => theme.colors.fg.tertiary};
white-space: nowrap;
`;

const InputContainer = styled.div(
({ focused, hasError, theme }) => css`
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
height: 36px;
padding: 4px 12px;
background-color: ${theme.colors.bg.primary};
border: 1px solid
${theme.colors.border[hasError ? 'negativeNormal' : 'defaultNormal']};
border-radius: ${theme.borders.radius.small};
overflow: hidden;
${focused &&
!hasError &&
css`
border-color: ${theme.colors.border.defaultActive};
`};
${focused &&
css`
${Suffix} {
color: ${theme.colors.fg.primary};
}
`};
:focus-within {
${focusCSS(theme.colors.border.focus)};
}
`
);

const StyledInput = styled.input(
({ theme }) => css`
height: 100%;
width: 100%;
padding: 8px 12px;
${themeHelpers.focusableOutlineCSS(theme.colors.border.focus)};
padding: 0 8px 0 0;
background-color: inherit;
border: none;
outline: none;
color: ${theme.colors.fg.primary};
${themeHelpers.expandPresetStyles({
preset: {
...theme.typography.presets.paragraph[
Expand All @@ -54,23 +101,18 @@ const StyledInput = styled.input(
},
theme,
})};
background-color: ${theme.colors.bg.primary};
border: 1px solid
${theme.colors.border[hasError ? 'negativeNormal' : 'defaultNormal']};
border-radius: ${theme.borders.radius.small};
color: ${theme.colors.fg.primary};
:active {
border-color: ${theme.colors.border.defaultActive};
}
:disabled {
color: ${theme.colors.fg.disable};
border-color: ${theme.colors.border.disable};
& ~ ${Suffix} {
color: ${theme.colors.fg.disable};
}
}
& + p {
color: ${theme.colors.fg[hasError ? 'negative' : 'tertiary']};
:active:enabled {
color: ${theme.colors.fg.primary};
}
`
);
Expand All @@ -82,23 +124,49 @@ export const Input = ({
hint,
id,
label,
suffix,
...props
}) => {
const inputId = useMemo(() => id || uuidv4(), [id]);
const inputRef = useRef(null);
const [focused, setFocused] = useState(false);

useEffect(() => {
if (focused && inputRef.current) {
inputRef.current.select();
}
}, [focused]);

const handleFocus = useCallback(() => setFocused(true), []);
const handleBlur = useCallback(() => setFocused(false), []);

return (
<Container className={className}>
{label && (
<Label htmlFor={inputId} as="label" disabled={disabled}>
<Label htmlFor={inputId} forwardedAs="label" disabled={disabled}>
{label}
</Label>
)}
<StyledInput
id={inputId}
disabled={disabled}
hasError={hasError}
{...props}
/>
<InputContainer focused={focused} hasError={hasError}>
<StyledInput
id={inputId}
disabled={disabled}
ref={inputRef}
onBlur={handleBlur}
onFocus={handleFocus}
{...props}
/>
{suffix && (
<Suffix
hasLabel={Boolean(label)}
forwardedAs="span"
size={THEME_CONSTANTS.TYPOGRAPHY.PRESET_SIZES.SMALL}
onClick={handleFocus}
>
{suffix}
</Suffix>
)}
</InputContainer>
{hint && <Hint hasError={hasError}>{hint}</Hint>}
</Container>
);
Expand Down Expand Up @@ -147,4 +215,5 @@ Input.propTypes = {
hint: PropTypes.string,
id: PropTypes.string,
label: labelAccessibilityValidator,
suffix: PropTypes.node,
};
63 changes: 52 additions & 11 deletions assets/src/design-system/components/input/stories/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { text } from '@storybook/addon-knobs';
import { Input } from '..';
import { DarkThemeProvider } from '../../../storybookUtils';
import { Headline } from '../../..';
import { AlignCenter } from '../../../icons';

export default {
title: 'DesignSystem/Components/Input',
Expand All @@ -44,7 +45,7 @@ const Container = styled.div`

const Row = styled.div`
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
grid-column: 1 / -1;
grid-column-gap: 60px;
Expand All @@ -54,14 +55,22 @@ const Row = styled.div`
}
`;

const IconContainer = styled.div`
height: 32px;
width: 32px;
margin-right: -8px;
`;

export const _default = () => {
const [inputState, setInputState] = useState({
oneLight: 'Text',
twoLight: 'we have an error',
threeLight: 'disabled',
fourLight: 'disabled',
oneDark: 'Dark mode text',
twoDark: '',
threeDark: '',
fourDark: '',
});

const handleChange = (event) => {
Expand Down Expand Up @@ -90,27 +99,41 @@ export const _default = () => {
label={text('Input 1 Label', 'Normal')}
hint={text('Hint', 'Hint')}
placeholder="placeholder"
suffix={text('Suffix')}
/>
<Input
aria-label="input-two"
id="two-light"
name="twoLight"
value={inputState.twoLight}
onChange={handleChange}
label={text('Input 2 Label', 'Error')}
label={text('Input 2 Label', 'Suffix')}
hint={text('Hint', 'Hint')}
placeholder="placeholder"
hasError
suffix={text('Suffix', 'Duration')}
/>
<Input
aria-label="disabled-input-one"
aria-label="input-three"
id="three-light"
name="threeLight"
value={inputState.threeLight}
onChange={handleChange}
label={text('Input 3 Label', 'Disabled')}
label={text('Input 3 Label', 'Error')}
hint={text('Hint', 'Hint')}
placeholder="placeholder"
suffix={text('Suffix')}
hasError
/>
<Input
aria-label="disabled-input-one"
id="four-light"
name="fourLight"
value={inputState.fourLight}
onChange={handleChange}
label={text('Input 4 Label', 'Disabled')}
hint={text('Hint', 'Hint')}
placeholder="placeholder"
suffix={text('Suffix')}
disabled
/>
</Row>
Expand All @@ -119,35 +142,53 @@ export const _default = () => {
<Container darkMode>
<Row>
<Input
aria-label="input-three"
aria-label="input-four"
id="one-dark"
name="oneDark"
value={inputState.oneDark}
onChange={handleChange}
label={text('Input 1 Label', 'Normal')}
hint={text('Hint', 'Hint')}
placeholder="placeholder"
suffix={text('Suffix')}
/>
<Input
aria-label="input-four"
aria-label="input-five"
id="two-dark"
name="twoDark"
value={inputState.twoDark}
onChange={handleChange}
label={text('Input 2 Label', 'Error')}
label={text('Input 2 Label', 'Suffix')}
hint={text('Hint', 'Hint')}
placeholder="placeholder"
hasError
suffix={
<IconContainer>
<AlignCenter />
</IconContainer>
}
/>
<Input
aria-label="disabled-input-two"
aria-label="input-six"
id="three-dark"
name="threeDark"
value={inputState.threeDark}
onChange={handleChange}
label={text('Input 3 Label', 'Disabled')}
label={text('Input 3 Label', 'Error')}
hint={text('Hint', 'Hint')}
placeholder="placeholder"
suffix={text('Suffix')}
hasError
/>
<Input
aria-label="disabled-input-two"
id="four-dark"
name="fourDark"
value={inputState.fourDark}
onChange={handleChange}
label={text('Input 4 Label', 'Disabled')}
hint={text('Hint', 'Hint')}
placeholder="placeholder"
suffix={text('Suffix')}
disabled
/>
</Row>
Expand Down
14 changes: 10 additions & 4 deletions assets/src/design-system/components/input/test/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@
* limitations under the License.
*/

/**
* External dependencies
*/

/**
* Internal dependencies
*/
Expand Down Expand Up @@ -48,6 +44,16 @@ describe('Input', () => {

expect(getByText('This is my input hint')).toBeInTheDocument();
});

it('should render a suffix', () => {
const { getByText } = renderWithProviders(
<Input aria-label="test" suffix="suffix" />
);

const suffixElement = getByText('suffix');

expect(suffixElement).toBeInTheDocument();
});
});

describe('labelAccessibilityValidator', () => {
Expand Down

0 comments on commit e6b68a8

Please sign in to comment.