Skip to content
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

Input styling changes, Form component addition #216

Merged
merged 43 commits into from
Nov 20, 2020
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
7fdc05e
Update Input styling to match new designs for errors, passwords
kylesuss Oct 22, 2020
1734e1f
Setup form component for managing common form state
kylesuss Oct 23, 2020
4c0a145
Add form state management example to the stories
kylesuss Oct 23, 2020
c17f8a7
Fix some console errors/warnings
kylesuss Oct 23, 2020
76e346e
Fix Pill input story
kylesuss Oct 23, 2020
c415557
Remove unused onChange func
kylesuss Oct 23, 2020
9b6ba82
Allow control of tabIndex on WithTooltip
kylesuss Oct 23, 2020
2caa871
Document error tooltip focus state logic
kylesuss Oct 23, 2020
bdd70a3
Rename Form -> FormErrorState, use func as children pattern
kylesuss Oct 26, 2020
105ff7e
Reuse types
kylesuss Oct 26, 2020
cc32a49
convert Link to TS
winkerVSbecks Oct 19, 2020
a0dcef8
better typing for LinkProps
winkerVSbecks Oct 21, 2020
1105a5e
disable react/prop-types for TS files
winkerVSbecks Oct 23, 2020
1fabb5f
Add LinkComponentPicker to avoid passing LinkProps to LinkWrapper
winkerVSbecks Oct 23, 2020
29a72c6
Change story component name
kylesuss Oct 26, 2020
56f67ab
Export FormErrorState
kylesuss Oct 26, 2020
7773289
Add didAttemptSubmission prop to FormErrorState
kylesuss Oct 26, 2020
4ee99da
Share logic for error visibility with other handlers
kylesuss Oct 26, 2020
d0306d2
Fix typing
kylesuss Oct 26, 2020
8e31216
Add stack level styling
kylesuss Oct 26, 2020
c1efe6d
Fix icon & password action styling
kylesuss Oct 26, 2020
2f81e8d
Stop propagation on password form button
kylesuss Oct 27, 2020
9ff4bc9
Add type=button to password input button
kylesuss Oct 27, 2020
d273b85
Pull didAttemptSubmission logic over
kylesuss Oct 27, 2020
2e1b4ec
Add pill specific icon styling
kylesuss Oct 27, 2020
00b0d24
Fix bug in WithModal
kylesuss Oct 28, 2020
21d5920
Remove tooltip space when closed
kylesuss Nov 17, 2020
ff255f4
Use display none vs hidden visibility
kylesuss Nov 17, 2020
30c7941
New technique -- hide tooltip element when missing
kylesuss Nov 17, 2020
a404b7c
Revert to old icon size
kylesuss Nov 17, 2020
b01a975
Change input borders to color.border
kylesuss Nov 17, 2020
d1295d6
Update form icon to 14px
kylesuss Nov 18, 2020
7a134c2
Remove bold styling for input placeholders
kylesuss Nov 18, 2020
2661d34
Final tweaks on input icon spacing
kylesuss Nov 18, 2020
5b2593b
Refactor primaryField logic, add cases for when not blurred
kylesuss Nov 19, 2020
694f226
Fix pill icon styling
kylesuss Nov 19, 2020
84e2695
Account for hover in lastXFieldId
kylesuss Nov 19, 2020
9bb4b76
Clean up error tracking a bit, allow manually hiding of tooltips
kylesuss Nov 19, 2020
5bd1f17
Remove hidden error message logic. Doesnt work where I need it
kylesuss Nov 19, 2020
67420b1
Fix bug with tabbing backward, clean up callbacks
kylesuss Nov 19, 2020
348dd80
Ignore form typing
kylesuss Nov 20, 2020
e662c34
Make sure wasFieldTouched and isErrorVisible use useCallback
kylesuss Nov 20, 2020
dc8f4e7
Make sure onFocus uses callbackRegenValues
kylesuss Nov 20, 2020
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
2 changes: 1 addition & 1 deletion .storybook/main.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module.exports = {
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.js'],
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.js', '../src/**/*.stories.tsx'],
addons: [
'@storybook/addon-essentials',
'@storybook/addon-storysource',
Expand Down
102 changes: 102 additions & 0 deletions src/components/Form.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import React, { useState } from 'react';
import styled from 'styled-components';
import { action, actions as makeActions } from '@storybook/addon-actions';

import { Form, PureForm, FormProps, PureFormProps } from './Form';
// @ts-ignore
import { Button } from './Button';
// @ts-ignore
import { Input as UnstyledInput } from './Input';

export default {
title: 'Design System/forms/Form',
component: Form,
};

const FormWrapper = styled.div`
padding: 3em 12em;
`;

const Input = styled(UnstyledInput)`
padding-bottom: 1em;
`;

const onSubmit = (e: Event) => {
e.preventDefault();
action('onClick')(e);
};

const Submit = () => (
<Button appearance="secondary" onClick={onSubmit}>
Submit
</Button>
);

const fields = [
{
id: 'input-1',
validationError: (value: string) =>
!value && `There is an error with this field with value: "${value}"`,
Component: (props: any) => {
const [value, setValue] = useState('');
return (
<Input
startFocused
appearance="secondary"
label="Label"
hideLabel
value={value}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
action('change')(e.target.value);
setValue(e.target.value);
}}
{...props}
/>
);
},
},
{
id: 'input-2',
validationError: (value: string) => `There is an error with this field with value: "${value}"`,
Component: (props: any) => {
const [value, setValue] = useState('prefilled');
return (
<Input
appearance="secondary"
label="Label"
hideLabel
value={value}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
action('change')(e.target.value);
setValue(e.target.value);
}}
{...props}
/>
);
},
},
];

export const Default = (args: FormProps) => <Form {...args} />;
Default.decorators = [(storyFn: any) => <FormWrapper>{storyFn()}</FormWrapper>];
Default.args = {
children: <Submit />,
fields,
};

export const Pure = (args: PureFormProps) => <PureForm {...args} />;
Pure.decorators = [(storyFn: any) => <FormWrapper>{storyFn()}</FormWrapper>];
Pure.args = {
...makeActions('onFocus', 'onBlur', 'onMouseEnter', 'onMouseLeave'),
primaryFieldId: 'input-1',
blurredFieldIds: new Set(),
children: <Submit />,
fields,
};

export const PureMultipleErrors = Pure.bind({});
PureMultipleErrors.decorators = [(storyFn: any) => <FormWrapper>{storyFn()}</FormWrapper>];
PureMultipleErrors.args = {
...Pure.args,
blurredFieldIds: new Set(Pure.args.fields.map((field) => field.id)),
};
91 changes: 91 additions & 0 deletions src/components/Form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React, { useEffect, useState } from 'react';

interface Field {
id: string;
validationError: (value: string) => string | void;
Component: (props: any) => JSX.Element;
}

export interface FormProps {
fields: [Field?];
}

export interface PureFormProps extends FormProps {
children?: React.ReactChildren;
primaryFieldId: string;
onMouseEnter: (id: string) => void;
onMouseLeave: (id: string) => void;
onFocus: (id: string) => void;
onBlur: (id: string) => void;
blurredFieldIds: Set<string | unknown>;
}

export const PureForm = ({
children,
fields = [],
onMouseEnter,
onMouseLeave,
onBlur,
onFocus,
primaryFieldId,
blurredFieldIds,
...rest
}: PureFormProps) => {
return (
<form {...rest}>
{fields.map(({ id, validationError, Component }: Field) => (
<Component
key={id}
id={id}
onMouseEnter={() => onMouseEnter(id)}
onMouseLeave={() => onMouseLeave(id)}
onFocus={() => onFocus(id)}
onBlur={() => onBlur(id)}
error={(value: string) => blurredFieldIds.has(id) && validationError(value)}
suppressErrorMessage={!primaryFieldId || primaryFieldId !== id}
/>
))}
{children}
</form>
);
};

export const Form = (props: FormProps) => {
const [focusedFieldId, setFocusedFieldId] = useState(undefined);
const [hoveredFieldId, setHoveredFieldId] = useState(undefined);
// The primary field is the field that's visual cues take precedence over any
// others given the entire form's focused & hover states.
// Use this to control things like error messaging priority.
const [primaryFieldId, setPrimaryFieldId] = useState(undefined);
const [blurredFieldIds, setBlurredFieldIds] = useState(new Set());

useEffect(() => {
if (hoveredFieldId) setPrimaryFieldId(hoveredFieldId);
else if (focusedFieldId) setPrimaryFieldId(focusedFieldId);
else setPrimaryFieldId(undefined);
}, [focusedFieldId, hoveredFieldId, setPrimaryFieldId]);

return (
<PureForm
{...props}
{...{
blurredFieldIds,
onFocus: (id) => setFocusedFieldId(id),
onBlur: (id) => {
setBlurredFieldIds(blurredFieldIds.add(id));
setFocusedFieldId(undefined);
},
onMouseEnter: (id) => {
// We only care about the hover state of previously blurred fields.
// We don't want to show error tooltips for fields that haven't been
// visited yet.
if (blurredFieldIds.has(id)) setHoveredFieldId(id);
},
onMouseLeave: (id) => {
if (blurredFieldIds.has(id)) setHoveredFieldId(undefined);
},
primaryFieldId,
}}
/>
);
};
Loading