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

feat: create new Form package #2925

Merged
merged 11 commits into from
Jan 10, 2023
6 changes: 6 additions & 0 deletions .changeset/nervous-crabs-relax.md
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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'});
shleewhite marked this conversation as resolved.
Show resolved Hide resolved
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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)}>
TheSisb marked this conversation as resolved.
Show resolved Hide resolved
{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
Original file line number Diff line number Diff line change
@@ -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"
shleewhite marked this conversation as resolved.
Show resolved Hide resolved
columnGap="space50"
ref={ref}
element={element}
{...safelySpreadBoxProps(props)}
>
{children}
</Box>
)
);

FormControlTwoColumn.displayName = 'FormControlTwoColumn';

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