Skip to content

Commit

Permalink
Add form
Browse files Browse the repository at this point in the history
  • Loading branch information
mikeldking committed Feb 24, 2022
1 parent 4934c2f commit 6733319
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 95 deletions.
74 changes: 0 additions & 74 deletions src/Form.tsx

This file was deleted.

2 changes: 2 additions & 0 deletions src/field/Field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import React, {
ReactNode,
ReactElement,
} from 'react';
import { useFormProps } from '../form';

export interface FieldProps
extends FieldLabelProps,
Expand All @@ -23,6 +24,7 @@ export interface FieldProps
}

function Field(props: FieldProps, ref: RefObject<HTMLElement>) {
props = useFormProps(props);
let {
label,
labelExtra,
Expand Down
119 changes: 119 additions & 0 deletions src/form/Form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import {
Alignment,
DOMRef,
LabelPosition,
LabelableProps,
DOMProps,
AriaLabelingProps,
ValidationState,
} from '../types';
import { classNames, useDOMRef } from '../utils';
import { filterDOMProps } from '@react-aria/utils';
import React, { useContext, ReactElement, FormEventHandler } from 'react';
import { css } from '@emotion/core';
import theme from '../theme';

let FormContext = React.createContext<LabelableProps>({});

export function useFormProps<T extends LabelableProps>(props: T): T {
let ctx = useContext(FormContext);
return { ...ctx, ...props };
}

const formPropNames = new Set([
'action',
'autoComplete',
'encType',
'method',
'target',
'onSubmit',
]);

export interface FormProps extends DOMProps, AriaLabelingProps, LabelableProps {
/** The contents of the Form. */
children: ReactElement<LabelableProps> | ReactElement<LabelableProps>[];

/** Whether the Form elements are disabled. */
isDisabled?: boolean;
/** Whether user input is required on each of the Form elements before Form submission. */
isRequired?: boolean;
/** Whether the Form elements can be selected but not changed by the user. */
isReadOnly?: boolean;
/**
* Whether the Form elements should display their "valid" or "invalid" visual styling.
* @default 'valid'
*/
validationState?: ValidationState;
/**
* Where to send the form-data when the form is submitted.
*/
action?: string;
/**
* The enctype attribute specifies how the form-data should be encoded when submitting it to the server.
*/
encType?:
| 'application/x-www-form-urlencoded'
| 'multipart/form-data'
| 'text/plain';
/**
* The form-data can be sent as URL variables (with method="get") or as HTTP post transaction (with method="post").
*/
method?: 'get' | 'post';
/**
* The target attribute specifies a name or a keyword that indicates where to display the response that is received after submitting the form.
*/
target?: '_blank' | '_self' | '_parent' | '_top';
/**
* Fired on form submission.
*/
onSubmit?: FormEventHandler;
}

function Form(props: FormProps, ref: DOMRef<HTMLFormElement>) {
let {
children,
labelPosition = 'top' as LabelPosition,
labelAlign = 'start' as Alignment,
isRequired,
necessityIndicator,
isDisabled,
isReadOnly,
validationState,
...otherProps
} = props;
let domRef = useDOMRef(ref);

let ctx = {
labelPosition,
labelAlign,
necessityIndicator,
};

return (
<form
{...filterDOMProps(otherProps, {
labelable: true,
propNames: formPropNames,
})}
noValidate
ref={domRef}
className={classNames('ac-form', {
'ac-form--positionSide': labelPosition === 'side',
'ac-form--positionTop': labelPosition === 'top',
})}
css={css`
& > *:not(:last-child):not(.ac-field--hasHelpText) {
margin-bottom: ${theme.spacing.margin24}px;
}
`}
>
<FormContext.Provider value={ctx}>{children}</FormContext.Provider>
</form>
);
}

/**
* Forms allow users to enter data that can be submitted while providing alignment and styling for form fields.
*/
const _Form = React.forwardRef(Form);
export { _Form as Form };
1 change: 1 addition & 0 deletions src/form/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Form';
1 change: 1 addition & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export * from './listbox';
export * from './picker';
export * from './notification';
export * from './textfield';
export * from './form';
// export interface Props extends HTMLAttributes<HTMLDivElement> {
// /** custom content, defaults to 'the snozzberries taste like snozzberries' */
// children?: ReactChild;
Expand Down
13 changes: 12 additions & 1 deletion src/textfield/TextFieldBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ function TextFieldBase(props: TextFieldBaseProps, ref: Ref<TextFieldRef>) {
label,
validationState,
isDisabled,
isReadOnly,
multiLine,
autoFocus,
inputClassName,
Expand Down Expand Up @@ -164,6 +165,8 @@ function TextFieldBase(props: TextFieldBaseProps, ref: Ref<TextFieldRef>) {
'ac-textfield--multiline': multiLine,
'is-hovered': isHovered,
'is-focused': isFocused,
'is-disabled': isDisabled,
'is-readonly': isReadOnly,
})}
css={css`
display: flex;
Expand All @@ -175,14 +178,22 @@ function TextFieldBase(props: TextFieldBaseProps, ref: Ref<TextFieldRef>) {
background-color: ${theme.components.textField.backgroundColor};
transition: all 0.2s ease-in-out;
overflow: hidden;
&.is-hovered {
&.is-hovered[:not(.is-disabled)] {
border: 1px solid ${theme.components.textField.hoverBorderColor};
background-color: ${theme.components.textField.activeBackgroundColor};
}
&.is-focused {
border: 1px solid ${theme.components.textField.activeBorderColor};
background-color: ${theme.components.textField.activeBackgroundColor};
}
&.is-disabled,
&.is-readonly {
border: 1px solid ${theme.components.textField.backgroundColor};
background-color: ${theme.components.textField.backgroundColor};
.ac-textfield__input {
color: ${theme.textColors.white70};
}
}
.ac-textfield__input::placeholder {
color: ${theme.textColors.white50};
font-style: italic;
Expand Down
51 changes: 36 additions & 15 deletions stories/Form.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import { Meta, Story } from '@storybook/react';
import Form, { FormProps } from '../src/Form';
import { Form, FormProps, TextField } from '../src';
import InfoTip from './components/InfoTip';

const meta: Meta = {
title: 'Form',
Expand All @@ -19,20 +20,40 @@ const meta: Meta = {

export default meta;

const Template: Story<FormProps> = args => (
<Form {...args}>
<Form.RequiredFieldsDisclosure />
<Form.FormLabel text="Label Text" required>
<input type="text" />
</Form.FormLabel>
<Form.FormLabel text="Label Text" helperText="Limit 10">
<input type="text" />
</Form.FormLabel>
<Form.FormLabel text="Label Text" required helperText="Limit 10">
<input type="text" />
</Form.FormLabel>
</Form>
);
const Template: Story<FormProps> = args => {
const [name, setName] = React.useState('');
const [email, setEmail] = React.useState('');
return (
<Form {...args}>
<TextField
label="Name"
placeholder="enter your name"
isRequired
errorMessage={!name ? 'This field is required' : null}
value={name}
validationState={!name ? 'invalid' : null}
onChange={v => setName(v)}
/>
<TextField
label="Email"
placeholder="enter your email address"
isRequired
errorMessage={!email ? 'This field is required' : null}
value={email}
onChange={v => setEmail(v)}
/>
<TextField
label="Charge"
labelExtra={<InfoTip>The amount you will be charged</InfoTip>}
placeholder="enter your amount"
isRequired
validationState={'invalid'}
addonBefore="$"
errorMessage="This field is required"
/>
</Form>
);
};

// By passing using the Args format for exported stories, you can control the props for a component for reuse in a test
// https://storybook.js.org/docs/react/workflows/unit-testing
Expand Down
18 changes: 13 additions & 5 deletions stories/TextField.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ const meta: Meta = {
decorators: [withDesign],
parameters: {
controls: { expanded: true },
// design: {
// type: 'figma',
// url:
// 'https://www.figma.com/file/5mMInYH9JdJY389s8iBVQm/Component-Library?node-id=76%3A505',
// },
design: {
type: 'figma',
url:
'https://www.figma.com/file/5mMInYH9JdJY389s8iBVQm/Component-Library?node-id=76%3A505',
},
},
};

Expand Down Expand Up @@ -54,6 +54,14 @@ export const Gallery = () => (
addonBefore="$"
errorMessage="This field is required"
/>
<TextField
label="Charge"
labelExtra={<InfoTip>The amount you will be charged</InfoTip>}
placeholder="enter your amount"
isDisabled
addonBefore="$"
value="100"
/>
</div>
);

Expand Down

0 comments on commit 6733319

Please sign in to comment.