Skip to content

Commit

Permalink
feat: create new Form package (#2925)
Browse files Browse the repository at this point in the history
  • Loading branch information
shleewhite committed Jan 10, 2023
1 parent 6e8b78f commit a4b9a58
Show file tree
Hide file tree
Showing 24 changed files with 1,012 additions and 241 deletions.
6 changes: 6 additions & 0 deletions .changeset/nervous-crabs-relax.md
@@ -0,0 +1,6 @@
---
'@twilio-paste/form': major
'@twilio-paste/core': minor
---

[Form] Create a set of components to handle form layouts.
1 change: 1 addition & 0 deletions .codesandbox/ci.json
Expand Up @@ -40,6 +40,7 @@
"/packages/paste-core/components/file-picker",
"/packages/paste-core/components/file-uploader",
"/packages/paste-core/layout/flex",
"/packages/paste-core/components/form",
"/packages/paste-core/components/form-pill-group",
"/packages/paste-core/layout/grid",
"/packages/paste-core/components/heading",
Expand Down
7 changes: 7 additions & 0 deletions packages/paste-codemods/tools/.cache/mappings.json
Expand Up @@ -94,6 +94,13 @@
"FormPill": "@twilio-paste/core/form-pill-group",
"FormPillGroup": "@twilio-paste/core/form-pill-group",
"useFormPillState": "@twilio-paste/core/form-pill-group",
"Form": "@twilio-paste/core/form",
"FormActions": "@twilio-paste/core/form",
"FormControl": "@twilio-paste/core/form",
"FormControlTwoColumn": "@twilio-paste/core/form",
"FormSection": "@twilio-paste/core/form",
"FormSectionDescription": "@twilio-paste/core/form",
"FormSectionHeading": "@twilio-paste/core/form",
"Heading": "@twilio-paste/core/heading",
"HeadingPropTypes": "@twilio-paste/core/heading",
"HelpText": "@twilio-paste/core/help-text",
Expand Down
Empty file.
179 changes: 179 additions & 0 deletions packages/paste-core/components/form/__tests__/index.spec.tsx
@@ -0,0 +1,179 @@
import * as React from 'react';
import {render, screen} from '@testing-library/react';
import {CustomizationProvider} from '@twilio-paste/customization';

import {
Form,
FormActions,
FormControl,
FormControlTwoColumn,
FormSection,
FormSectionDescription,
FormSectionHeading,
} from '../src';

describe('Form', () => {
it('should render correctly', () => {
render(
<Form aria-label="My Form">
<FormSection id="foo">
<FormSectionHeading data-testid="section-heading">Settings</FormSectionHeading>
<FormSectionDescription data-testid="section-desc">These are the settings</FormSectionDescription>
</FormSection>
<FormControlTwoColumn>Two column content</FormControlTwoColumn>
<FormControl>Control content</FormControl>
<FormActions>Actions content</FormActions>
</Form>
);

const form = screen.getByRole('form');
expect(form).toBeDefined();
expect(form).toHaveAttribute('aria-label', 'My Form');

const description = screen.getByTestId('section-desc');
expect(description.id).toBe('foo-section-description');

const section = screen.getByRole('group', {name: 'Settings'});
expect(section).toBeDefined();
expect(section).toHaveAttribute('aria-describedby', description.id);

expect(screen.getByTestId('section-heading').nodeName).toBe('LEGEND');
expect(screen.getByText('Two column content')).toBeDefined();
expect(screen.getByText('Control content')).toBeDefined();
expect(screen.getByText('Actions content')).toBeDefined();
});

it('should set the description id if no id prop is passed to the form section', () => {
render(
<FormSection>
<FormSectionHeading>Section</FormSectionHeading>
<FormSectionDescription data-testid="section-desc">Description</FormSectionDescription>
</FormSection>
);

expect(screen.getByRole('group', {name: 'Section'})).toHaveAttribute(
'aria-describedby',
screen.getByTestId('section-desc').id
);
});
});

describe('Form customization', () => {
it('should set an element data attribute', () => {
render(
<Form aria-label="My Form">
<FormSection id="foo">
<FormSectionHeading data-testid="section-heading">Settings</FormSectionHeading>
<FormSectionDescription data-testid="section-desc">These are the settings</FormSectionDescription>
</FormSection>
<FormControlTwoColumn>Two column content</FormControlTwoColumn>
<FormControl>Control content</FormControl>
<FormActions>Actions content</FormActions>
</Form>
);

expect(screen.getByRole('form').dataset.pasteElement).toBe('FORM');
expect(screen.getByRole('group', {name: 'Settings'}).dataset.pasteElement).toBe('FORM_SECTION');
expect(screen.getByTestId('section-heading').dataset.pasteElement).toBe('FORM_SECTION_HEADING');
expect(screen.getByText('Two column content').dataset.pasteElement).toBe('FORM_CONTROL_TWO_COLUMN');
expect(screen.getByText('Control content').dataset.pasteElement).toBe('FORM_CONTROL');
expect(screen.getByText('Actions content').dataset.pasteElement).toBe('FORM_ACTIONS');
});

it('should set a custom element data attribute', () => {
render(
<Form aria-label="My Form" element="MY_FORM">
<FormSection id="foo" element="MY_FORM_SECTION">
<FormSectionHeading data-testid="section-heading" element="MY_FORM_SECTION_HEADING">
Settings
</FormSectionHeading>
<FormSectionDescription data-testid="section-desc" element="MY_FORM_SECTION_DESCRIPTION">
These are the settings
</FormSectionDescription>
</FormSection>
<FormControlTwoColumn element="MY_FORM_CONTROL_TWO_COLUMN">Two column content</FormControlTwoColumn>
<FormControl element="MY_FORM_CONTROL">Control content</FormControl>
<FormActions element="MY_FORM_ACTIONS">Actions content</FormActions>
</Form>
);

expect(screen.getByRole('form').dataset.pasteElement).toBe('MY_FORM');
expect(screen.getByRole('group', {name: 'Settings'}).dataset.pasteElement).toBe('MY_FORM_SECTION');
expect(screen.getByTestId('section-heading').dataset.pasteElement).toBe('MY_FORM_SECTION_HEADING');
expect(screen.getByText('Two column content').dataset.pasteElement).toBe('MY_FORM_CONTROL_TWO_COLUMN');
expect(screen.getByText('Control content').dataset.pasteElement).toBe('MY_FORM_CONTROL');
expect(screen.getByText('Actions content').dataset.pasteElement).toBe('MY_FORM_ACTIONS');
});

it('should add custom styling', () => {
render(
<CustomizationProvider
theme={TestTheme}
elements={{
FORM: {rowGap: 'space20'},
FORM_ACTIONS: {justifyContent: 'center'},
FORM_CONTROL: {flexGrow: 'unset'},
FORM_CONTROL_TWO_COLUMN: {columnGap: 'space20'},
FORM_SECTION: {borderWidth: 'borderWidth10', borderStyle: 'solid', borderColor: 'colorBorder'},
FORM_SECTION_DESCRIPTION: {fontWeight: 'fontWeightBold'},
FORM_SECTION_HEADING: {backgroundColor: 'colorBackgroundErrorWeakest'},
}}
>
<Form aria-label="My Form">
<FormSection id="foo">
<FormSectionHeading data-testid="section-heading">Settings</FormSectionHeading>
<FormSectionDescription data-testid="section-desc">These are the settings</FormSectionDescription>
</FormSection>
<FormControlTwoColumn data-testid="two-col">Two column content</FormControlTwoColumn>
<FormControl>Control content</FormControl>
<FormActions>Actions content</FormActions>
</Form>
</CustomizationProvider>
);

expect(screen.getByRole('form')).toHaveStyleRule('row-gap', '0.25rem');
expect(screen.getByRole('group', {name: 'Settings'})).toHaveStyleRule('border-style', 'solid');
expect(screen.getByTestId('section-heading')).toHaveStyleRule('background-color', 'rgb(254, 236, 236)');
expect(screen.getByText('Two column content')).toHaveStyleRule('column-gap', '0.25rem');
expect(screen.getByText('Control content')).toHaveStyleRule('flex-grow', 'unset');
expect(screen.getByText('Actions content')).toHaveStyleRule('justify-content', 'center');
});

it('should add custom styling to a custom named form', () => {
render(
<CustomizationProvider
theme={TestTheme}
elements={{
MY_FORM: {rowGap: 'space20'},
MY_FORM_ACTIONS: {justifyContent: 'center'},
MY_FORM_CONTROL: {flexGrow: 'unset'},
MY_FORM_CONTROL_TWO_COLUMN: {columnGap: 'space20'},
MY_FORM_SECTION: {borderWidth: 'borderWidth10', borderStyle: 'solid', borderColor: 'colorBorder'},
MY_FORM_SECTION_DESCRIPTION: {fontWeight: 'fontWeightBold'},
MY_FORM_SECTION_HEADING: {backgroundColor: 'colorBackgroundErrorWeakest'},
}}
>
<Form aria-label="My Form" element="MY_FORM">
<FormSection id="foo" element="MY_FORM_SECTION">
<FormSectionHeading data-testid="section-heading" element="MY_FORM_SECTION_HEADING">
Settings
</FormSectionHeading>
<FormSectionDescription data-testid="section-desc" element="MY_FORM_SECTION_DESCRIPTION">
These are the settings
</FormSectionDescription>
</FormSection>
<FormControlTwoColumn element="MY_FORM_CONTROL_TWO_COLUMN">Two column content</FormControlTwoColumn>
<FormControl element="MY_FORM_CONTROL">Control content</FormControl>
<FormActions element="MY_FORM_ACTIONS">Actions content</FormActions>
</Form>
</CustomizationProvider>
);

expect(screen.getByRole('form')).toHaveStyleRule('row-gap', '0.25rem');
expect(screen.getByRole('group', {name: 'Settings'})).toHaveStyleRule('border-style', 'solid');
expect(screen.getByTestId('section-heading')).toHaveStyleRule('background-color', 'rgb(254, 236, 236)');
expect(screen.getByText('Two column content')).toHaveStyleRule('column-gap', '0.25rem');
expect(screen.getByText('Control content')).toHaveStyleRule('flex-grow', 'unset');
expect(screen.getByText('Actions content')).toHaveStyleRule('justify-content', 'center');
});
});
3 changes: 3 additions & 0 deletions packages/paste-core/components/form/build.js
@@ -0,0 +1,3 @@
const {build} = require('../../../../tools/build/esbuild');

build(require('./package.json'));
58 changes: 58 additions & 0 deletions packages/paste-core/components/form/package.json
@@ -0,0 +1,58 @@
{
"name": "@twilio-paste/form",
"version": "0.0.0",
"category": "layout",
"status": "production",
"description": "A form groups related form elements that allow users to input information or configure options.",
"author": "Twilio Inc.",
"license": "MIT",
"main:dev": "src/index.tsx",
"main": "dist/index.js",
"module": "dist/index.es.js",
"types": "dist/index.d.ts",
"sideEffects": false,
"publishConfig": {
"access": "public"
},
"files": [
"dist"
],
"scripts": {
"build": "yarn clean && NODE_ENV=production node build.js && tsc",
"build:js": "NODE_ENV=development node build.js",
"build:props": "typedoc --tsconfig ./tsconfig.json --json ./dist/prop-types.json",
"clean": "rm -rf ./dist",
"tsc": "tsc"
},
"peerDependencies": {
"@twilio-paste/animation-library": "^0.3.2",
"@twilio-paste/box": "^7.0.0",
"@twilio-paste/customization": "^5.0.0",
"@twilio-paste/design-tokens": "^8.0.0",
"@twilio-paste/heading": "^8.0.0",
"@twilio-paste/style-props": "^6.0.0",
"@twilio-paste/styling-library": "^1.0.0",
"@twilio-paste/theme": "^8.0.0",
"@twilio-paste/types": "^3.1.1",
"@twilio-paste/uid-library": "^0.2.6",
"prop-types": "^15.7.2",
"react": "^16.8.6 || ^17.0.2",
"react-dom": "^16.8.6 || ^17.0.2"
},
"devDependencies": {
"@twilio-paste/animation-library": "^0.3.2",
"@twilio-paste/box": "^7.0.0",
"@twilio-paste/customization": "^5.0.0",
"@twilio-paste/design-tokens": "^8.0.0",
"@twilio-paste/heading": "^8.0.0",
"@twilio-paste/style-props": "^6.0.0",
"@twilio-paste/styling-library": "^1.0.0",
"@twilio-paste/theme": "^8.0.0",
"@twilio-paste/types": "^3.1.1",
"@twilio-paste/uid-library": "^0.2.6",
"prop-types": "^15.7.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"typescript": "^4.9.4"
}
}
36 changes: 36 additions & 0 deletions packages/paste-core/components/form/src/Form.tsx
@@ -0,0 +1,36 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import {Box, safelySpreadBoxProps} from '@twilio-paste/box';
import type {BoxProps, BoxStyleProps} from '@twilio-paste/box';
import {isMaxWidthTokenProp} from '@twilio-paste/style-props';

export interface FormProps extends Omit<React.ComponentPropsWithRef<'form'>, 'children'> {
element?: BoxProps['element'];
children: React.ReactNode;
maxWidth?: BoxStyleProps['maxWidth'];
}

export const Form = React.forwardRef<HTMLDivElement, FormProps>(
({element = 'FORM', maxWidth, children, ...props}, ref) => (
<Box
as="form"
ref={ref}
element={element}
maxWidth={maxWidth}
display="flex"
flexDirection="column"
rowGap="space80"
{...safelySpreadBoxProps(props)}
>
{children}
</Box>
)
);

Form.displayName = 'Form';

Form.propTypes = {
children: PropTypes.node,
element: PropTypes.string,
maxWidth: isMaxWidthTokenProp,
};
32 changes: 32 additions & 0 deletions packages/paste-core/components/form/src/FormActions.tsx
@@ -0,0 +1,32 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import {Box, safelySpreadBoxProps} from '@twilio-paste/box';
import type {BoxProps} from '@twilio-paste/box';

export interface FormActionsProps extends Omit<React.ComponentPropsWithRef<'div'>, 'children'> {
element?: BoxProps['element'];
children: React.ReactNode;
}

export const FormActions = React.forwardRef<HTMLDivElement, FormActionsProps>(
({children, element = 'FORM_ACTIONS', ...props}, ref) => (
<Box
ref={ref}
element={element}
{...safelySpreadBoxProps(props)}
display="flex"
flexDirection="row"
columnGap="space40"
marginTop="space60"
>
{children}
</Box>
)
);

FormActions.displayName = 'FormActions';

FormActions.propTypes = {
children: PropTypes.node,
element: PropTypes.string,
};
24 changes: 24 additions & 0 deletions packages/paste-core/components/form/src/FormControl.tsx
@@ -0,0 +1,24 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import {Box, safelySpreadBoxProps} from '@twilio-paste/box';
import type {BoxProps} from '@twilio-paste/box';

export interface FormControlProps extends Omit<React.ComponentPropsWithRef<'div'>, 'children'> {
element?: BoxProps['element'];
children: React.ReactNode;
}

export const FormControl = React.forwardRef<HTMLDivElement, FormControlProps>(
({children, element = 'FORM_CONTROL', ...props}, ref) => (
<Box ref={ref} flexGrow={1} element={element} {...safelySpreadBoxProps(props)}>
{children}
</Box>
)
);

FormControl.displayName = 'FormControl';

FormControl.propTypes = {
children: PropTypes.node,
element: PropTypes.string,
};
31 changes: 31 additions & 0 deletions packages/paste-core/components/form/src/FormControlTwoColumn.tsx
@@ -0,0 +1,31 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import {Box, safelySpreadBoxProps} from '@twilio-paste/box';
import type {BoxProps} from '@twilio-paste/box';

export interface FormControlTwoColumnProps extends Omit<React.ComponentPropsWithRef<'div'>, 'children'> {
element?: BoxProps['element'];
children: React.ReactNode;
}

export const FormControlTwoColumn = React.forwardRef<HTMLLegendElement, FormControlTwoColumnProps>(
({children, element = 'FORM_CONTROL_TWO_COLUMN', ...props}, ref) => (
<Box
display="grid"
gridTemplateColumns="1fr 1fr"
columnGap="space50"
ref={ref}
element={element}
{...safelySpreadBoxProps(props)}
>
{children}
</Box>
)
);

FormControlTwoColumn.displayName = 'FormControlTwoColumn';

FormControlTwoColumn.propTypes = {
children: PropTypes.node,
element: PropTypes.string,
};

0 comments on commit a4b9a58

Please sign in to comment.