diff --git a/cypress/integration/sitemap-vrt/constants.ts b/cypress/integration/sitemap-vrt/constants.ts index 09aac8de56..e33e22196d 100644 --- a/cypress/integration/sitemap-vrt/constants.ts +++ b/cypress/integration/sitemap-vrt/constants.ts @@ -321,6 +321,7 @@ export const SITEMAP = [ "/patterns/object-status/", "/primitives/combobox-primitive/", "/patterns/create/", + "/patterns/form/", "/primitives/", "/primitives/menu-primitive/", "/primitives/disclosure-primitive/", diff --git a/packages/paste-website/src/assets/images/patterns/form-ingredients.png b/packages/paste-website/src/assets/images/patterns/form-ingredients.png new file mode 100644 index 0000000000..57808135d3 Binary files /dev/null and b/packages/paste-website/src/assets/images/patterns/form-ingredients.png differ diff --git a/packages/paste-website/src/component-examples/FormExamples.ts b/packages/paste-website/src/component-examples/FormExamples.ts index 1a7cdd14a3..315569858c 100644 --- a/packages/paste-website/src/component-examples/FormExamples.ts +++ b/packages/paste-website/src/component-examples/FormExamples.ts @@ -430,3 +430,580 @@ render( ) `.trim(); + +export const InterruptiveFormsExample = ` +const FormExample = () => { + const [isOpen, setIsOpen] = React.useState(false); + const handleOpen = () => setIsOpen(true); + const handleClose = () => setIsOpen(false); + const modalHeadingID = useUID(); + const name = useUID(); + const email = useUID(); + const phone = useUID(); + const birthday = useUID(); + return ( + + + + + + Add new contact + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + +
+
+ ); +}; + +render( + +)`.trim(); + +export const InlineFormsExample = ` +const FormExample = () => { + const popoverID = useUID(); + const phone = useUID(); + const country = useUID(); + return ( + + + Edit phone number + +
+ + Edit phone number + + + + + + + + + + + + + +
+ + + + + + +
+
+
+ ); +}; + +render( + +)`.trim(); + +export const ConditionalFormsExample = ` +const FormExample = () => { + const [selectedValue, setSelectedValue] = React.useState(undefined); + const tax = useUID(); + return ( + + + + Tax information + + + Based on your jurisdiction, Twilio may need to collect tax on the services sold, in order to abide by local + laws. Please provide your tax number so that we can apply this correctly to your invoice + + + + + Yes, I can provide a tax number + + {selectedValue === "yes" ? ( + + + + + ) : null} + + No, I cannot provide a tax number + + + + + + + + + + ); +}; + +render( + +)`.trim(); + +export const InlineValidationFormsExample = ` +const FormExample = () => { + const [selectedValue, setSelectedValue] = React.useState(undefined); + const password = useUID(); + const isFourCharacter = selectedValue?.length >= 4; + const hasNumber = /\d/.test(selectedValue || ""); + const hasUppercase = /[A-Z]/.test(selectedValue || ""); + return ( + + + + setSelectedValue(e.target.value)} + /> + + Atleast 4 character + Atleast 1 number + Atleast 1 uppercase + + ); +}; + +render( + +)`.trim(); + +export const ErrorStateFormExample = ` +type FormValues = { + country: string; + address: string; + city: string; + state: string; + zip: string; +}; +const InputWithError = React.memo( + ({ + id, + label, + registerName, + registerOptions, + insertAfter, + errors, + register, + }: { + id: string; + label: string; + registerName: keyof FormValues; + registerOptions: any; + insertAfter?: React.ReactNode; + errors: Record; + register: UseFormRegister; + }) => { + return ( + + + + ( + + {message} + + )} + /> + + ); + }, +); +const FormExample = () => { +const { control, handleSubmit, register, setFocus } = useForm({ + defaultValues: { + country: "", + address: "", + city: "", + state: "", + zip: "", + }, + }); + const { errors } = useFormState({ control }); + const seed = useUIDSeed(); + const fieldList = { + country: "Country", + address: "Address line 1", + city: "City", + state: "State", + zip: "Zip code", + }; + return ( + + + Main address + + + This information will be used for taxation purposes. For US customers, this is your service address. For + international customers, this is your permanent place of establishment (e.g. head office.) + + {Object.keys(errors).length > 0 ? ( + + Missing Values + Enter values for all required fields + + {Object.keys(errors).map((errorKey) => ( + + + + ))} + + + ) : null} + +
+ } + errors={errors} + register={register} + /> + + + + + + + + +
+
+ ); +}; + +render( + +)`.trim(); + +export const ValidationOnSubmitFormsExample = ` +const FormExample = () => { + const [selectedValue, setSelectedValue] = React.useState(""); + const [formState, setFormState] = React.useState<"default" | "loading" | "error" | "success">("default"); + const previousFormState = React.useRef<"default" | "error" | "success">("default"); + const vatID = useUID(); + function handleSubmit(event: React.FormEvent) { + event.preventDefault(); + previousFormState.current = formState === "loading" ? previousFormState.current : formState; + setFormState("loading"); + setTimeout(() => { + const vatRegex = /^[A-Z]{2}\\d{9}$/; + if (vatRegex.test(selectedValue || "")) { + setFormState("success"); + } else { + setFormState("error"); + } + }, 1000); + } + const helpTextContent = { + default: "Use the following format: IEXXXXXXXXX", + error: "Enter VAT in this format: IEXXXXXXXXX", + success: "VAT number validated", + }; + return ( + +
+ + + + setSelectedValue(e.target.value)} + /> + + {formState === "loading" ? helpTextContent[previousFormState.current] : helpTextContent[formState]} + + + +
+ + + +
+ ); +}; + +render( + +)`.trim(); + +export const ConfirmationFormsExample = ` +const FormExample = () => { + const TableComponent = () => { + return ( + + + + Field name + Answer + Action + + + + + Content + Content + + Change + + + + Content + Content + + Change + + + + Content + Content + + Change + + + + Content + Content + + Change + + + + + ); + }; + return ( + + + + + + Label + + Label + + Label + + + + Confirmation + Check and confirm your answers + + See all your selected answers below. You can also change them if you need to. + + + + + +
+ + + + 1 + + + + + Step 1 + Paragraph + + + + + + + + 2 + + + + + Step 2 + Paragraph + + + + +
+
+ + + + + + + + + + +
+
+
+ ); +}; + +render( + +)`.trim(); diff --git a/packages/paste-website/src/component-examples/page-templates/WizardExamples.ts b/packages/paste-website/src/component-examples/page-templates/WizardExamples.ts index bb6a250e98..faee7862bb 100644 --- a/packages/paste-website/src/component-examples/page-templates/WizardExamples.ts +++ b/packages/paste-website/src/component-examples/page-templates/WizardExamples.ts @@ -276,6 +276,23 @@ export const DefaultWizardOrderedListExample = ` `.trim(); +export const DefaultWizardOrderedListExampleRender = ` +const App = () => { + const input1 = useUID(); + const input2 = useUID(); + const input3 = useUID(); + const input4 = useUID(); + const input5 = useUID(); + return ( + ${DefaultWizardOrderedListExample} + ); +} + +render( + +) +`.trim(); + export const WizardFooterOptionsExample = ` { + const navigationData = await getNavigationData(); + const feature = await getFeature('Form'); + return { + props: { + data: { + ...feature, + }, + navigationData, + }, + }; +}; + + + + + + + + + + + + + +```jsx +// import the components for form pattern as required + +import { Form, FormActions, FormControl, FormSection, FormSectionDescription, FormSectionHeading } from '@twilio-paste/core/form'; +import { Callout, CalloutHeading, CalloutList, CalloutListItem, CalloutText } from '@twilio-paste/core/callout'; +import { Select, Option } from '@twilio-paste/core/select'; +import { Separator } from '@twilio-paste/core/separator'; +import { useUID, useUIDSeed } from '@twilio-paste/core/uid-library'; +import { Label } from '@twilio-paste/core/label'; +import { Input } from '@twilio-paste/core/input'; +import { Paragraph } from '@twilio-paste/core/paragraph'; +import { HelpText } from '@twilio-paste/core/help-text'; +import { Button } from '@twilio-paste/core/button'; +import { ButtonGroup } from '@twilio-paste/core/button-group'; +import { Text } from '@twilio-paste/core/text'; +import { Anchor } from '@twilio-paste/core/anchor'; +import { Box } from '@twilio-paste/core/box'; +import { Modal, ModalBody, ModalFooter, ModalFooterActions, ModalHeader, ModalHeading } from "@twilio-paste/core/modal"; +import { ErrorMessage } from '@hookform/error-message'; +import { useForm, useFormState, UseFormRegister } from 'react-hook-form'; +``` + +## Usage + +### General + +Use forms to present input fields, selection options, and actions in a clear, accessible, and logical flow, ensuring users can easily understand, complete, and submit their information. + +A form can be: + + + + Simple and user-oriented like survey forms, login form, or a contact form. + + + A complex and feature-driven form like settings, and multi-step forms spread across multiple screens. + + + +Before designing a form you should: + + + + Think about the user’s relationship with the product. + + + Understand the user’s goals associated with the form. + + + Identify essential data needed to achieve your goals. Then, consider the optional data that would be helpful if available but are not critical to the form's success. + + + Communicate the outcome of completing the form so users know what to expect once they submit it. + + + +### Accessibility + + + Don't ask for the same information twice in the same session. Reference: WCAG + Provide accessible error identification and messaging. Reference: WCAG + Group related fields using fieldsets and legends with `FormSection` and `FormSectionHeading` from the Form layout component, which render an HTML fieldset and legend. Reference: WebAIM + The order in which form elements are presented on a webpage should be logical and meaningful. Reference: WCAG + + + + + Check accessibility for each form element component—for example, follow Time Picker accessibility guidelines. You can also refer to the WCAG Forms Tutorial for more details. + + + +## Ingredients + +### What’s in a form? + +The different parts of a form are: + + + + Form layout component: A wrapper layout component that sets layout and spacing between form elements. + + + Form element: Any UI component in a form. Examples: Input, Button, Callout. + + + Form header: Includes form elements like Heading and Paragraph that describe the purpose of the form. + + + Form field: A UI component in a form where users enter information, including their associated Label and Help Text. + Examples: Checkbox, Input, Time Picker—with their Label and Help Text. + + + Form actions: Includes form elements like Button or Button Group for submitting or navigating a form. + + + + + +### General form elements + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ComponentWhen to use
Form layoutArranges a layout of form elements with preset spacing.
Heading and ParagraphExplains the purpose of the form or form section.
LabelProvides a visible and accessible name to a form field.
Help TextHelps users prevent an error and describe what makes the form field successful. It also includes error, warning, and success variants to communicate different states on a field.
Button or Button GroupTriggers actions and submits forms.
CalloutSummarizes errors or other important information in the header.
+ +
+ +### User input fields + + + + + + + + + + + + + + + + + + + + + + + + + + +
ComponentWhen to use
Input or TextareaUse Input for single-line text input and Textarea for multi-line text input.
Checkbox or SwitchUse a Checkbox to enable binary choices that require a submit action.

Use a Switch for immediate binary on/off choices that don’t require a submit action and applies the user's choice instantly.
File PickerAllows the user to upload only one file at a time.
File UploaderAllows the user to upload multiple files.
+ +
+ +### Single selection fields + + + + + + + + + + + + + + + + + + + + + + +
ComponentWhen to use
Radio Group or{" "} + Radio Button Group or{" "} + Visual Picker RadioUse for up to 6 fixed list items and when only one selection is allowed.
Select or Singleselect ComboboxUse for 6+ fixed list items and when only one selection is allowed. By default, use Select.
Singleselect Combobox - AutocompleteUse for lists with 15+ options or when users need to search a database to select one option.
+ +
+ +### Multiple selection fields + + + + + + + + + + + + + + + + + + + + + + +
ComponentWhen to use
Checkbox Group or{" "} + Visual Picker CheckboxUse for up to 6 fixed list items and when multiple selection is allowed.
Multiselect ComboboxUse for 6+ fixed list items and when multiple selection is allowed.
Form Pill GroupUse when you’re editing a collection of data within a form.
+ +
+ +### Numeric fields + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ComponentWhen to use
SliderUse it when the exact value doesn’t matter.
Input with number functionalityUse it when the exact numeric value matters.
Date PickerUse it for selecting specific dates.
Date Range PickerUse it for selecting date ranges.
Time PickerUse it for selecting specific times.
Time Range PickerUse it for selecting time ranges.
+ +
+ +## Variations + +### Single step or page forms + +A single-step form presents all elements on one page, allowing users to fill out and submit the form in one continuous action. + +Use single-step forms for: + + + + Tasks that users can complete in one go without interruptions or needing external references. + + + Low cognitive load forms that require a maximum of 8 fields. For longer forms, use a Multi-step form or for settings, use the Settings template and divide the fields with In Page Navigation or Tabs. + + + + + {sectionsForm} + + +### Multi-step forms + +A multi-step form divides the input process into stages, allowing users to complete one section of fields at a time. + +Use multi-step forms when: + + + + Tasks require a lot of information (example: account creation, application forms, or surveys.) + + + Some fields depend on previous answers. + + + The data being collected can be grouped into distinct sections (example: personal information, preferences, payment details) + + + Helping users focus on one task at a time. + + + +Use the [Wizard template](/page-templates/wizard#about-wizard) for designing multi-step forms. + + + + Adding a rough time estimate in multi-step form can be helpful as it sets clear expectations and reduces uncertainty for long forms. + + + + + {DefaultWizardOrderedListExampleRender} + + +### Interruptive forms + +Interruptive forms use a [Modal](/components/modal), [Alert Dialog](/components/alert-dialog), or [Side Modal](/components/side-modal) as a page overlay that collects user input and blocks interaction with the page until the form is submitted or the modal is dismissed. + +Use interruptive forms: + + + + For brief interruptions where user input is required, but the interaction is short enough that the user retains context from their previous task. + + + When users need to confirm an action before proceeding, such as deleting an account or submitting a payment. + + + When the form doesn’t require leaving the page or referencing background content. + + + +Avoid long/multi-step forms inside modals unless absolutely necessary. If a multi-step form inside a modal is needed: + + + + Avoid using progress indicators. + + + Ensure that it’s a form that needs to be shown across multiple user flows. For example, phone number provisioning flows are in Modals because they can be launched from multiple touchpoints. + + + Plan for scalability and consider how the form may evolve over time. If the form is likely to get more complex over time, put the form on the page instead. + + + + + + Users may lose progress by clicking outside or pressing "Esc." Use the Confirmation pattern before closing the form. + + + + + {InterruptiveFormsExample} + + +### Contextual forms + +Contextual forms use our [Side Panel](/components/side-panel) component and are primarily used on pages using the [Filter pattern](/patterns/filter) when there are too many filter options to display on the page. +Use Contextual forms if the form is something users might interact with while still needing visibility of the background content +(e.g., adjusting settings, applying filters, or configuring options without losing context). + + { + const [selectedFilters, setSelectedFilters] = React.useState({}); + const pillState = useFormPillState(); + const [filteredTableData, setFilteredTableData] = React.useState(data); + return ( + <> + + + + + More filters + + + + + + {filterList.map((filter) => { + return ( + + + + {filter.label} + {selectedCount ? ( + + Selected {filter.type === "status" ? 1 : selectedCount} + + ) : null} + + + + + + + ); + })} + + + + + + + + + + + // Filter components from other examples + + + + ) +}`} +/> + +### Inline forms + +Inline forms use our [Popover](/components/popover) component. Use Inline forms when the input is optional and concise. Popovers work best for lightweight interactions that don’t disrupt the user’s primary workflow. + + + {InlineFormsExample} + + +## Composition + +Designing a good form requires making decisions about composition, sequence, form elements, copy, and feedback. +Use the [Form layout component](/components/form) to arrange a layout of form elements with preset spacing. + +### Number of form elements + +The process of completing forms should be as simple as possible. Take the time to evaluate every field you add to your forms. Before adding form elements, ask yourself: + + + + Do you really need to ask for this information? + + + Is it information that you can get automatically? + + + Can we prepopulate the field from data we already have from a user? + + + Is there a better time or place to get an answer from our users? + + + +
+ + Each question in a Web form has a cost…If there are too many questions in the form, you’ll lose users…if there are questions in the form that users consider impertinent or irrelevant—or, even worse, you’ll get made-up data. + + +
+ +Don’t make the user repeat information. For example, if the shipping and billing addresses are the same, provide an option to reuse the shipping address instead of requiring duplicate input. + +### Form order + +#### Order form fields logically + +When ordering your form, place fields in a logical order from a user’s perspective, not the application or database’s logic. For example, it’s unusual to ask for someone’s address before their name. + +
+ + The order of questions in your form should, as closely as possible, match the way things flow in the user’s mind. + + +
+ + +#### Organization: When to break into sections or new pages + +After determining the number of form fields, decide how to group them into sections, using `FormSection` from the [Form layout component](/components/form). If a form naturally breaks down into short sections, a [single step form](https://docs.google.com/document/d/1sYcP_i_YTwYWWdjKDQfkIdMj1NqyZV5c_8k4BxrEYc0/edit?pli=1&tab=t.0#heading=h.3vpocpbk0gl7) is likely to be a good way to organize the form. When a form becomes long and has more than 8 questions that are only related by a few topics, [multi-step form](https://docs.google.com/document/d/1sYcP_i_YTwYWWdjKDQfkIdMj1NqyZV5c_8k4BxrEYc0/edit?pli=1&tab=t.0#heading=h.yi4naptd6s8d) would be a better way to organize the form. + +### Form field composition + +#### Alignment + +Keep forms left-aligned to improve scannability and help users quickly understand the required information. This also makes it easier for keyboard users to tab through fields. + +#### Field length + +Ensure that field lengths provide both: + + + + Meaningful affordances that help people answer questions. + + + Enough room for inputs across multiple languages. + + + +For example, a “Zip code” field can be `size-20` in width because it’s not expected to maintain a small content length across languages. However a “Destination name” field should stretch to the max width you’ve defined on your content area. Refer to [page templates](/page-templates) for suggested max widths based on the type of page you’re building. + +#### Optional vs. Required + +Make required and optional fields distinguishable in the Label. Try to avoid optional input fields in forms. + + + + If most of the inputs on a form are required, indicate only the few that are optional. + + + If most of the inputs on a form are optional, indicate only the few that are required. + + + +If you use the `required` prop to indicate required fields, you'll see a "Required" title on the symbol. If you're building for other languages, use the `i18nLabel` prop to translate the "Required" title. + +#### Using Help Text + +Use [Help Text](/components/help-text) to help users prevent an error and describe what makes the form field successful. Include examples to show the expected format (example: "Example: 123-456-7890" for phone numbers) + +Avoid placeholder text. It is not broadly supported by assistive technologies, does not display in older browsers, and disappears from the field when a user enters text. + +### Friendly name or non-PII fields + +Non-PII (Non-Personally Identifiable Information) fields, especially Friendly name Inputs, require us to inform users when they might accidentally input sensitive information. These fields are marked as “Not PII” in the Twilio API, and require a visible representation in the UI. + +Check out to the [Privacy pattern](/patterns/privacy) for more information. + +{PrivacyExample} + +### Conditional fields + +Conditional fields in forms appear to users based on their previous interactions or choices. + +When using conditional fields: + + + + Limit nesting to one level deep. If a conditional field reveals another conditional field (and so on), it can make a form unpredictable and harder to complete. + + + If more than 3 fields need to be conditionally displayed, consider moving them to the next step of the multi-step form. + + + Ensure users understand why new fields appear. + + + Ensure that the revealed conditional field remains on the same page for user reference to previously selected fields and within the recommended field limit for a single page. + + + + + {ConditionalFormsExample} + + +### Form actions + + + + Use primary Buttons for key actions like Submit, Save, or Next, helping users complete the form and secondary Buttons for actions like Back or Cancel. + + + When a user is submitting a form, use the format “[Verb] [Object]”. For example, “Add funds” or “Update credit card”. + + + +#### Actions placement + +The primary Button placement depends on the form type: + + + + For Single Step Forms, Side Modals, Side Panels, Minimizable Dialogs and Popover forms, keep the primary button left-aligned, and placed before secondary buttons for better scannability of the content and its related actions. + + + For Multi-step Forms and Wizards, keep the primary button right-aligned, placed after secondary buttons to indicate the direction of the form. Refer to our Wizards action footer guidelines for additional guidance. + + + +## Behavior + +### Validation + +#### Validation on submit + +Most form validation should occur after a user presses the submit button. + + + + Don't prevent form submission by disabling the submit button. Assistive technologies cannot detect disabled buttons. Use error messages to explain what information is missing or incorrect. + + + Disable the primary action button only if you need to prevent duplicate form submissions, or you clearly explain what a user needs to do before submitting the form. + + + If it’s going to take a while to process a form, communicate this to the user with feedback messages and progress indicators (For example, use a loading Button). + + + + + + {ValidationOnSubmitFormsExample} + + +#### Inline validation + +Inline validation, either on blur or while typing, should be used rarely. It can be frustrating to users who tab through controls to navigate a page, and to screen reader users, who might not be able to detect that an error occurred `on-blur`. + +If you absolutely need to validate inline: + + + + Use inline validation only where immediate feedback helps, like creating passwords. + + + If validation fails, do not display an error message while the user is typing. Keep the form field in a neutral state and consider using a checklist-style inline validation that marks rules as complete as the user types. + + + Use positive inline validation once the user has entered the data again. + + + + + {InlineValidationFormsExample} + + +### Errors + +Use an [error Callout](/components/callout#error-callout) to communicate errors for a particular section of a page, or that applies to the whole page. Within the Callout, list errored fields as anchors and shift focus to the first errored field in the form. For additional guidance on how to compose error messages, refer to the [error state pattern](/patterns/error-state). + + + {ErrorStateFormExample} + + +### Confirmation and deletion + + + + Use the Confirmation pattern for critical, infrequent, and possibly destructive actions. For frequent actions, use the after deletion pattern and offer a way to undo the action in the success message instead. + + + If a user needs to agree to Terms of Service or similar, use Checkbox Disclaimer + + + If the user needs to review multiple answers, show a full page with the question asked, answer they gave, and allow them to change their answer, if they need to. + + + + + {ConfirmationFormsExample} + + +### Success + +Clearly communicate when a form has been completed successfully and what happened as a result using success messages with the [Toast](/components/toast) component. + +Refer to our [Notifications and feedback pattern](/patterns/notifications-and-feedback) for additional guidance on processing states of different complexity and emotional levels in forms. + +## Related patterns and templates + + + + Confirmation + + + Create + + + Error state + + + Notifications and feedback + + + Privacy + + + Settings + + + Wizard + + + +## References + +Further reading on forms best practices. + + + + Kathryn Whitenton, Website Form Usability: Top 10 Recommendations (Nielsen Norman Group) + + + Katie Sherwin, Placeholders in Form Fields are Harmful (Nielsen Norman Group, 2014) + + + Andrew Coyle, Design Better Forms (UX Collective, 2016) + + + Adam Silver, Form Design: From zero to hero all in one blog post (2019) + + + Luke Wroblewski, Web Form Design: Filling in the blanks (2008) + + +
+ +
diff --git a/packages/paste-website/stories/Form.stories.tsx b/packages/paste-website/stories/Form.stories.tsx new file mode 100644 index 0000000000..def8ba9062 --- /dev/null +++ b/packages/paste-website/stories/Form.stories.tsx @@ -0,0 +1,631 @@ +import { ErrorMessage } from "@hookform/error-message"; +import { Anchor } from "@twilio-paste/anchor"; +import { Box } from "@twilio-paste/box"; +import { Button } from "@twilio-paste/button"; +import { ButtonGroup } from "@twilio-paste/button-group"; +import { Callout, CalloutHeading, CalloutList, CalloutListItem, CalloutText } from "@twilio-paste/callout"; +import { + DataGrid, + DataGridBody, + DataGridCell, + DataGridHead, + DataGridHeader, + DataGridRow, +} from "@twilio-paste/data-grid"; +import { DatePicker } from "@twilio-paste/date-picker"; +import { Form, FormControl, FormSection, FormSectionDescription, FormSectionHeading } from "@twilio-paste/form"; +import { Heading } from "@twilio-paste/heading"; +import { HelpText } from "@twilio-paste/help-text"; +import { ArrowBackIcon } from "@twilio-paste/icons/esm/ArrowBackIcon"; +import { ArrowForwardIcon } from "@twilio-paste/icons/esm/ArrowForwardIcon"; +import { SearchIcon } from "@twilio-paste/icons/esm/SearchIcon"; +import { Input } from "@twilio-paste/input"; +import { Label } from "@twilio-paste/label"; +import { Modal, ModalBody, ModalFooter, ModalFooterActions, ModalHeader, ModalHeading } from "@twilio-paste/modal"; +import { + PageHeader, + PageHeaderDetails, + PageHeaderHeading, + PageHeaderKeyword, + PageHeaderParagraph, + PageHeaderSetting, +} from "@twilio-paste/page-header"; +import { Paragraph } from "@twilio-paste/paragraph"; +import { Popover, PopoverButton, PopoverContainer } from "@twilio-paste/popover"; +import { + ProgressStepComplete, + ProgressStepCurrent, + ProgressStepSeparator, + ProgressSteps, +} from "@twilio-paste/progress-steps"; +import { Radio, RadioGroup } from "@twilio-paste/radio-group"; +import { Option, Select } from "@twilio-paste/select"; +import { Separator } from "@twilio-paste/separator"; +import { Text } from "@twilio-paste/text"; +import { useUID, useUIDSeed } from "@twilio-paste/uid-library"; +import type { JSX } from "react"; +import * as React from "react"; +import { type UseFormRegister, useForm, useFormState } from "react-hook-form"; + +export default { + title: "Website/FormExamples", +}; + +export const InterruptiveForms = (): JSX.Element => { + const [isOpen, setIsOpen] = React.useState(false); + const handleOpen: () => void = () => setIsOpen(true); + const handleClose: () => void = () => setIsOpen(false); + const modalHeadingID = useUID(); + const name = useUID(); + const email = useUID(); + const phone = useUID(); + const birthday = useUID(); + return ( + + + + + + Add new contact + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + +
+
+ ); +}; + +InterruptiveForms.parameters = { + padding: false, +}; + +export const InlineForms = (): JSX.Element => { + const popoverID = useUID(); + const phone = useUID(); + const country = useUID(); + return ( + + + Edit phone number + +
+ + Edit phone number + + + + + + + + + + + + + +
+ + + + + + +
+
+
+ ); +}; + +InlineForms.parameters = { + padding: false, +}; + +export const ConditionalForm = (): JSX.Element => { + const [selectedValue, setSelectedValue] = React.useState(undefined); + const tax = useUID(); + return ( + + + + Tax information + + + Based on your jurisdiction, Twilio may need to collect tax on the services sold, in order to abide by local + laws. Please provide your tax number so that we can apply this correctly to your invoice + + + + + + Yes, I can provide a tax number + + {selectedValue === "yes" ? ( + + + + + ) : null} + + No, I cannot provide a tax number + + + + + + + + + + + ); +}; + +ConditionalForm.parameters = { + padding: false, +}; + +export const InlineValidationForm = (): JSX.Element => { + const [selectedValue, setSelectedValue] = React.useState(undefined); + const password = useUID(); + const isFourCharacter = (selectedValue?.length as number) >= 4; + const hasNumber = /\d/.test(selectedValue || ""); + const hasUppercase = /[A-Z]/.test(selectedValue || ""); + return ( + + + + setSelectedValue(e.target.value)} + /> + + Atleast 4 character + Atleast 1 number + Atleast 1 uppercase + + ); +}; + +InlineValidationForm.parameters = { + padding: false, +}; + +type FormValues = { + country: string; + address: string; + city: string; + state: string; + zip: string; +}; +const InputWithError = React.memo( + ({ + id, + label, + registerName, + registerOptions, + insertAfter, + errors, + register, + }: { + id: string; + label: string; + registerName: keyof FormValues; + registerOptions: any; + insertAfter?: React.ReactNode; + errors: Record; + register: UseFormRegister; + }) => { + return ( + + + + ( + + {message} + + )} + /> + + ); + }, +); + +export const ErrorState = (): JSX.Element => { + const { control, handleSubmit, register, setFocus } = useForm({ + defaultValues: { + country: "", + address: "", + city: "", + state: "", + zip: "", + }, + }); + const { errors } = useFormState({ control }); + const seed = useUIDSeed(); + + const fieldList = { + country: "Country", + address: "Address line 1", + city: "City", + state: "State", + zip: "Zip code", + }; + + return ( + + + Main address + + + This information will be used for taxation purposes. For US customers, this is your service address. For + international customers, this is your permanent place of establishment (e.g. head office.) + + {Object.keys(errors).length > 0 ? ( + + Missing values + Enter values for all required fields + + {Object.keys(errors).map((errorKey) => ( + + + + ))} + + + ) : null} + +
+ } + errors={errors} + register={register} + /> + + + + + + + + + +
+
+ ); +}; + +ErrorState.parameters = { + padding: false, +}; + +export const ValidationOnSubmit = (): JSX.Element => { + const [selectedValue, setSelectedValue] = React.useState(""); + const [formState, setFormState] = React.useState<"default" | "loading" | "error" | "success">("default"); + const previousFormState = React.useRef<"default" | "error" | "success">("default"); + const vatID = useUID(); + + function handleSubmit(event: React.FormEvent): void { + event.preventDefault(); + previousFormState.current = formState === "loading" ? previousFormState.current : formState; + setFormState("loading"); + + setTimeout(() => { + const vatRegex = /^[A-Z]{2}\d{9}$/; + if (vatRegex.test(selectedValue || "")) { + setFormState("success"); + } else { + setFormState("error"); + } + }, 1000); + } + + const helpTextContent = { + default: "Use the following format: IEXXXXXXXXX", + error: "Enter VAT in this format: IEXXXXXXXXX", + success: "VAT number validated", + }; + + return ( + +
+ + + + setSelectedValue(e.target.value)} + /> + + {formState === "loading" ? helpTextContent[previousFormState.current] : helpTextContent[formState]} + + + +
+ + + +
+ ); +}; + +ValidationOnSubmit.parameters = { + padding: false, +}; + +export const ConfirmationForm = (): JSX.Element => { + const TableComponent = (): JSX.Element => { + return ( + + + + Field name + Answer + Action + + + + + Content + Content + + Change + + + + Content + Content + + Change + + + + Content + Content + + Change + + + + Content + Content + + Change + + + + + ); + }; + return ( + + + + + + Label + + Label + + Label + + + + Confirmation + Check and confirm your answers + + See all your selected answers below. You can also change them if you need to. + + + + + +
+ + + + 1 + + + + + Step 1 + Paragraph + + + + + + + + 2 + + + + + Step 2 + Paragraph + + + + +
+
+ + + + + + + + + + +
+
+
+ ); +}; +ConfirmationForm.parameters = { + padding: false, +};