diff --git a/packages/styleguide/src/lib/Meta/About.mdx b/packages/styleguide/src/lib/Meta/About.mdx
index 5fadd6a3420..77487f9b7a3 100644
--- a/packages/styleguide/src/lib/Meta/About.mdx
+++ b/packages/styleguide/src/lib/Meta/About.mdx
@@ -9,6 +9,7 @@ import {
import { parameters as bestPracticesParameters } from './Best Practices.mdx';
import { parameters as brandParameters } from './Brand.mdx';
import { parameters as contributingParameters } from './Contributing.mdx';
+import { parameters as deepControlsParameters } from './Deep Controls Add-On.mdx';
import { parameters as faqsParameters } from './FAQs.mdx';
import { parameters as installationParameters } from './Installation.mdx';
import { parameters as storiesParameters } from './Stories.mdx';
@@ -29,6 +30,7 @@ export const parameters = {
links={addParentPath(parameters.id, [
bestPracticesParameters,
contributingParameters,
+ deepControlsParameters,
faqsParameters,
storiesParameters,
brandParameters,
diff --git a/packages/styleguide/src/lib/Meta/Deep Controls Add-On.mdx b/packages/styleguide/src/lib/Meta/Deep Controls Add-On.mdx
index 8a14af90765..04dc424ba16 100644
--- a/packages/styleguide/src/lib/Meta/Deep Controls Add-On.mdx
+++ b/packages/styleguide/src/lib/Meta/Deep Controls Add-On.mdx
@@ -2,6 +2,13 @@ import { Meta } from '@storybook/blocks';
import { Callout, ImageWrapper, LinkTo } from '~styleguide/blocks';
+export const parameters = {
+ id: 'Deep Controls Add-On',
+ title: 'Deep Controls Add-On',
+ subtitle: `Enables Storybook controls for nested component properties, allowing you to interactively modify deeply nested props directly from the Controls panel without having to manually edit complex object structures.`,
+ status: 'static',
+};
+
# Deep Controls add-on
diff --git a/packages/styleguide/src/lib/Organisms/About.mdx b/packages/styleguide/src/lib/Organisms/About.mdx
index 90429136a41..09876d6032b 100644
--- a/packages/styleguide/src/lib/Organisms/About.mdx
+++ b/packages/styleguide/src/lib/Organisms/About.mdx
@@ -7,7 +7,7 @@ import {
} from '~styleguide/blocks';
import { parameters as connectedFormParameters } from './ConnectedForm/About.mdx';
-import { parameters as gridFormParameters } from './GridForm/GridForm.mdx';
+import { parameters as gridFormParameters } from './GridForm/About.mdx';
import { parameters as listsTablesParameters } from './Lists & Tables/About.mdx';
import { parameters as markdownParameters } from './Markdown/Markdown.mdx';
diff --git a/packages/styleguide/src/lib/Organisms/GridForm/About.mdx b/packages/styleguide/src/lib/Organisms/GridForm/About.mdx
new file mode 100644
index 00000000000..b2e5a1cfb1b
--- /dev/null
+++ b/packages/styleguide/src/lib/Organisms/GridForm/About.mdx
@@ -0,0 +1,42 @@
+import { Meta } from '@storybook/blocks';
+
+import {
+ AboutHeader,
+ addParentPath,
+ LinkTo,
+ TableOfContents,
+} from '~styleguide/blocks';
+
+import { parameters as buttonsParameters } from './Buttons.mdx';
+import { parameters as fieldsParameters } from './Fields.mdx';
+import { parameters as layoutParameters } from './Layout.mdx';
+import { parameters as statesParameters } from './States.mdx';
+import { parameters as usageParameters } from './Usage.mdx';
+import { parameters as validationParameters } from './Validation.mdx';
+
+export const parameters = {
+ id: 'Organisms/GridForm',
+ title: 'GridForm',
+ subtitle: 'An efficient way to build and design forms on a grid.',
+};
+
+
+
+
+
+## Getting started
+
+If you're new to `GridForm`, start with Usage to see an overview of how `GridForm` works and interact with a live playground.
+
+All other pages below provide guidance on specific parts of `GridForm`.
+
+
diff --git a/packages/styleguide/src/lib/Organisms/GridForm/Buttons.mdx b/packages/styleguide/src/lib/Organisms/GridForm/Buttons.mdx
new file mode 100644
index 00000000000..0480ca7dc8c
--- /dev/null
+++ b/packages/styleguide/src/lib/Organisms/GridForm/Buttons.mdx
@@ -0,0 +1,69 @@
+import { Canvas, Meta } from '@storybook/blocks';
+
+import { ComponentHeader, LinkTo } from '~styleguide/blocks';
+
+import * as ButtonsStories from './Buttons.stories';
+
+export const parameters = {
+ title: 'Buttons',
+ subtitle: 'Configure submit and cancel buttons for your forms.',
+ status: 'static',
+};
+
+
+
+
+
+## Submit button position
+
+We can position the submit button by passing the `position` prop within `submit` with a
+value of `'left'`, `'center'`, `'right'`, or `'stretch'`. The default is `'left'`.
+
+
+
+
+
+
+
+
+
+## Submit button type
+
+We can specify the type submit button by passing the `type` prop within `submit`. We can choose between
+the `FillButton` or `CTAButton`. The default is `'fill'`.
+
+
+
+
+
+## Inline submit button
+
+We can make the Submit button inline with an input by setting the column
+sizes so they fit on the same row (e.g size 6 for an input and size 4 for
+the submit).
+
+We can additionally remove the label from text inputs and checkbox inputs.
+Use the `hideLabel` prop to remove the label, allowing the submit button to
+align nicely with the input. **However**, if using `hideLabel` to remove the default label, you should provide an `aria-label` and/or include another label to the right/left of the input to ensure the input is accessible.
+
+
+
+## Cancel button
+
+Optionally, include a cancel button.
+
+
+
+## Button states
+
+### Loading
+
+We can set the state of the submit button to `loading` as `true` to show a loading spinner. This is useful when you need to show the user that the form is submitting.
+
+
+
+### Disabled
+
+You can also set `disabled` to `true` to disable submission.
+
+
diff --git a/packages/styleguide/src/lib/Organisms/GridForm/Buttons.stories.tsx b/packages/styleguide/src/lib/Organisms/GridForm/Buttons.stories.tsx
new file mode 100644
index 00000000000..cfea05f18f2
--- /dev/null
+++ b/packages/styleguide/src/lib/Organisms/GridForm/Buttons.stories.tsx
@@ -0,0 +1,137 @@
+import { GridForm } from '@codecademy/gamut';
+import { action } from '@storybook/addon-actions';
+import type { Meta, StoryObj } from '@storybook/react';
+
+const meta: Meta = {
+ component: GridForm,
+ args: {
+ onSubmit: (values) => {
+ action('Form Submitted')(values);
+ // eslint-disable-next-line no-console
+ console.log('Form Submitted', values);
+ },
+ fields: [
+ {
+ label: 'Simple text',
+ name: 'simple-text',
+ type: 'text',
+ size: 12,
+ },
+ ],
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const SubmitButtonRight: Story = {
+ args: {
+ submit: {
+ contents: 'Right Submit!?',
+ position: 'right',
+ size: 12,
+ },
+ },
+};
+
+export const SubmitButtonLeft: Story = {
+ args: {
+ submit: {
+ contents: 'Left Submit!?',
+ position: 'left',
+ size: 12,
+ },
+ },
+};
+
+export const SubmitButtonCenter: Story = {
+ args: {
+ submit: {
+ contents: 'Center Submit!?',
+ position: 'center',
+ size: 12,
+ },
+ },
+};
+
+export const SubmitButtonStretch: Story = {
+ args: {
+ submit: {
+ contents: 'Stretch Submit!?',
+ position: 'stretch',
+ size: 12,
+ },
+ },
+};
+
+export const SubmitButtonFill: Story = {
+ args: {
+ submit: {
+ contents: 'Fill Submit!?',
+ type: 'fill',
+ size: 12,
+ },
+ },
+};
+
+export const SubmitButtonCTA: Story = {
+ args: {
+ submit: {
+ contents: 'CTA Submit!?',
+ type: 'cta',
+ size: 12,
+ },
+ },
+};
+
+export const SubmitButtonInline: Story = {
+ args: {
+ fields: [
+ {
+ label: 'Simple text',
+ name: 'simple-text',
+ type: 'text',
+ size: 6,
+ },
+ ],
+ submit: {
+ contents: 'Inline Submit!?',
+ size: 4,
+ position: 'right',
+ },
+ },
+};
+
+export const CancelButton: Story = {
+ args: {
+ cancel: {
+ children: 'Cancel',
+ onClick: () => {},
+ },
+ submit: {
+ contents: 'Submit!?',
+ position: 'right',
+ size: 12,
+ },
+ },
+};
+
+export const Loading: Story = {
+ args: {
+ submit: {
+ contents: 'Loading Submit!?',
+ loading: true,
+ size: 12,
+ },
+ },
+};
+
+export const Disabled: Story = {
+ args: {
+ submit: {
+ contents: 'Disabled Submit!?',
+ disabled: true,
+ size: 12,
+ },
+ },
+};
diff --git a/packages/styleguide/src/lib/Organisms/GridForm/Fields.mdx b/packages/styleguide/src/lib/Organisms/GridForm/Fields.mdx
new file mode 100644
index 00000000000..84f408b073c
--- /dev/null
+++ b/packages/styleguide/src/lib/Organisms/GridForm/Fields.mdx
@@ -0,0 +1,97 @@
+import { Canvas, Meta } from '@storybook/blocks';
+
+import { ComponentHeader } from '~styleguide/blocks';
+
+import * as FieldsStories from './Fields.stories';
+
+export const parameters = {
+ title: 'Fields',
+ subtitle: 'Comprehensive GridForm field types to cover various input needs.',
+ status: 'static',
+};
+
+
+
+
+
+## Text inputs
+
+Text inputs support various HTML input types including `text`, `email`, `password`, `number`, `tel`, `url`, `search`, `date`, `time`, and more. All text inputs share the same basic properties but may have different validation patterns.
+
+
+
+### Default value
+
+
+
+### Placeholder text
+
+Text inputs are allowed to have traditional `placeholder` text.
+This is a somewhat dangerous behavior for accessibility, as browsers
+generally don't render placeholder text with high enough color contrast
+for AA standards. If you do need to use placeholder text, such as on
+landing page forms that have been shown to have higher completion rates
+with the text, please make sure the placeholder text doesn't add any new
+information to the form — it should really only rephrase the text label.
+
+See [this article](https://www.nngroup.com/articles/form-design-placeholders/) for
+more details on why using placeholders is often bad.
+
+
+
+## Textarea input
+
+
+
+## Select input
+
+
+
+## Radio group input
+
+
+
+## File upload input
+
+File upload fields allow users to select and upload files. You can add custom validation to restrict file types and sizes.
+
+
+
+## Checkbox input
+
+
+
+### Spacing
+
+Checkboxes can use tight spacing when you need them to fit in smaller areas:
+
+
+
+## Nested checkboxes input
+
+Nested checkboxes allow for hierarchical selection with parent-child relationships between options. Infinite levels of nesting are supported. Clicking a parent checkbox toggles all its children accordingly. Individual child checkboxes can be toggled independently. Checkbox states are `checked` if all children are checked, `indeterminate` if some children are checked, or `unchecked` if no children are checked. The values returned by the form on submit or update are an array of all selected values, including all children.
+
+
+
+## Custom inputs
+
+Some forms, such as the checkout flows that use Recurly, need to define
+their own inputs. We can specify a `'custom'` field type along with a [`render` prop](https://reactjs.org/docs/render-props.html).
+
+We also have a `'custom-group'` type for when you are passing in a custom `FormGroup` - including a label. If you do not want `GridForm` to surface errors for your field, you should likely use a `'custom-group'`. If you chose to go this route, please be aware of [accessibility best practices](https://www.deque.com/blog/anatomy-of-accessible-forms-best-practices/) for forms.
+
+
+
+## Hidden input
+
+Hidden inputs can be used to include data that users can't see or modify with the submission. For this implementation you can set the `defaultValue` in the object and it will be submitted with the regular form data.
+
+
+
+## Sweet container input
+
+"Sweet container" ([honeypot]()) inputs can be used to detect bots by providing a field that would not generally be clicked by human users, but might be triggered automatically by bots.
+
+We call it a "sweet container" so that bots do not immediately detect it as a honeypot input.
+
+
diff --git a/packages/styleguide/src/lib/Organisms/GridForm/Fields.stories.tsx b/packages/styleguide/src/lib/Organisms/GridForm/Fields.stories.tsx
new file mode 100644
index 00000000000..40ecc134b0d
--- /dev/null
+++ b/packages/styleguide/src/lib/Organisms/GridForm/Fields.stories.tsx
@@ -0,0 +1,330 @@
+import { FormGroup, GridForm, Input } from '@codecademy/gamut';
+import { action } from '@storybook/addon-actions';
+import type { Meta, StoryObj } from '@storybook/react';
+
+const meta: Meta = {
+ component: GridForm,
+ args: {
+ onSubmit: (values) => {
+ action('Form Submitted')(values);
+ // eslint-disable-next-line no-console
+ console.log('Form Submitted', values);
+ },
+ submit: {
+ contents: 'Submit',
+ size: 4,
+ position: 'left',
+ },
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const TextField: Story = {
+ args: {
+ fields: [
+ {
+ label: 'Simple text',
+ name: 'simple-text',
+ size: 9,
+ type: 'text',
+ },
+ ],
+ },
+};
+
+export const DefaultTextField: Story = {
+ args: {
+ fields: [
+ {
+ defaultValue: 'yeet',
+ label: 'Text with default value',
+ name: 'text-with-default',
+ size: 9,
+ type: 'text',
+ },
+ ],
+ },
+};
+
+export const PlaceholderTextField: Story = {
+ args: {
+ fields: [
+ {
+ label: 'Text with placeholder',
+ placeholder: 'Your email',
+ name: 'placeholder',
+ size: 9,
+ type: 'email',
+ },
+ ],
+ },
+};
+
+export const TextareaField: Story = {
+ args: {
+ fields: [
+ {
+ label: 'Write a paragraph about penguins',
+ name: 'textarea-input',
+ size: 9,
+ type: 'textarea',
+ },
+ ],
+ },
+};
+
+export const SelectField: Story = {
+ args: {
+ fields: [
+ {
+ label: 'Simple select',
+ name: 'simple-select',
+ options: ['', 'One fish', 'Two fish', 'Red fish', 'Blue fish'],
+ size: 9,
+ type: 'select',
+ },
+ ],
+ },
+};
+
+export const RadioGroupField: Story = {
+ args: {
+ fields: [
+ {
+ label: 'Preferred Modern Artist',
+ name: 'artist',
+ options: [
+ {
+ label: 'Cardi B',
+ value: 'cardi',
+ },
+ {
+ label: 'Nicki Minaj',
+ value: 'nicki',
+ },
+ ],
+ size: 9,
+ type: 'radio-group',
+ },
+ ],
+ },
+};
+
+export const FileUploadField: Story = {
+ args: {
+ fields: [
+ {
+ label: 'Upload a cat image (we support pdf, jpeg, or png files)',
+ name: 'file-input',
+ size: 9,
+ type: 'file',
+ validation: {
+ required: true,
+ validate: (files) => {
+ const { type } = files.item(0);
+ const allowedTypes = ['application/pdf', 'image/jpeg', 'image/png'];
+ if (!allowedTypes.includes(type))
+ return 'Please upload a pdf, jpeg, or png file.';
+ return true;
+ },
+ },
+ },
+ ],
+ },
+};
+
+export const CheckboxField: Story = {
+ args: {
+ fields: [
+ {
+ description: 'I agree to the terms',
+ label: 'Terms',
+ name: 'terms',
+ size: 6,
+ type: 'checkbox',
+ id: 'my-super-cool-id',
+ validation: {
+ required: true,
+ },
+ },
+ {
+ description: 'I agree to the conditions',
+ label: 'Conditions',
+ name: 'conditions',
+ size: 6,
+ type: 'checkbox',
+ id: 'my-super-cool-id2',
+ validation: {
+ required: true,
+ },
+ },
+ ],
+ },
+};
+
+export const CheckboxSpacing: Story = {
+ args: {
+ fields: [
+ {
+ description: 'I agree to the terms',
+ label: 'Terms',
+ name: 'terms',
+ size: 6,
+ type: 'checkbox',
+ id: 'terms-id',
+ spacing: 'tight',
+ validation: {
+ required: true,
+ },
+ },
+ {
+ description: 'I agree to the conditions',
+ label: 'Conditions',
+ name: 'conditions',
+ size: 6,
+ type: 'checkbox',
+ id: 'conditions-id',
+ spacing: 'tight',
+ validation: {
+ required: true,
+ },
+ },
+ ],
+ },
+};
+
+export const NestedCheckboxesField: Story = {
+ args: {
+ fields: [
+ {
+ label: 'Nested checkboxes',
+ name: 'nested-checkboxes',
+ type: 'nested-checkboxes',
+ defaultValue: ['backend', 'react', 'vue'],
+ options: [
+ {
+ value: 'frontend',
+ label: 'Frontend Technologies',
+ options: [
+ {
+ value: 'react',
+ label: 'React',
+ options: [
+ { value: 'nextjs', label: 'Next.js' },
+ { value: 'typescript', label: 'TypeScript' },
+ ],
+ },
+ {
+ value: 'vue',
+ label: 'Vue.js',
+ },
+ { value: 'angular', label: 'Angular' },
+ ],
+ },
+ {
+ value: 'backend',
+ label: 'Backend Technologies',
+ options: [
+ { value: 'node', label: 'Node.js' },
+ { value: 'python', label: 'Python' },
+ { value: 'java', label: 'Java' },
+ ],
+ },
+ ],
+ size: 12,
+ },
+ ],
+ },
+};
+
+export const CustomInputs: Story = {
+ args: {
+ fields: [
+ {
+ render: ({ error, setValue }) => (
+ <>
+ setValue(event.target.value)}
+ />
+ 🕺
+ >
+ ),
+ label: 'Gimme two more swags',
+ name: 'custom-input',
+ size: 12,
+ validation: {
+ required: true,
+ pattern: {
+ value: /swag(.*)swag/,
+ message: 'Still not enough swag, what are you doing... 💢',
+ },
+ },
+ type: 'custom',
+ },
+ {
+ render: ({ error, setValue }) => (
+
+ setValue(event.target.value)}
+ />
+
+ ),
+ size: 12,
+ label: 'Gimme two more swags',
+ name: 'custom-input-group',
+ validation: {
+ required: true,
+ pattern: {
+ value: /swag(.*)swag/,
+ message: 'Still not enough swag, what are you doing... 💢',
+ },
+ },
+ type: 'custom-group',
+ },
+ ],
+ },
+};
+
+export const HiddenInput: Story = {
+ args: {
+ fields: [
+ {
+ type: 'hidden',
+ name: 'secret-stuff',
+ defaultValue: "I'm invisible!",
+ },
+ {
+ label: "There's more than one field here!",
+ name: 'custom-hidden-input',
+ type: 'email',
+ size: 12,
+ },
+ ],
+ },
+};
+
+export const SweetContainer: Story = {
+ args: {
+ fields: [
+ {
+ label: 'This is our sticky sweet label',
+ name: 'sweet-container',
+ type: 'sweet-container',
+ },
+ {
+ label: "There's something sticky and sweet here!",
+ name: 'custom-input',
+ type: 'email',
+ size: 12,
+ },
+ ],
+ },
+};
diff --git a/packages/styleguide/src/lib/Organisms/GridForm/GridForm.mdx b/packages/styleguide/src/lib/Organisms/GridForm/GridForm.mdx
deleted file mode 100644
index f9a7a708453..00000000000
--- a/packages/styleguide/src/lib/Organisms/GridForm/GridForm.mdx
+++ /dev/null
@@ -1,234 +0,0 @@
-import { Canvas, Controls, Meta } from '@storybook/blocks';
-
-import { ComponentHeader, LinkTo } from '~styleguide/blocks';
-
-import * as GridFormStories from './GridForm.stories';
-
-export const parameters = {
- title: 'GridForm',
- subtitle: `GridForm an efficient way to build and design forms on a grid.`,
- design: {
- type: 'figma',
- url: 'https://www.figma.com/file/ReGfRNillGABAj5SlITalN/%F0%9F%93%90-Gamut?node-id=1689%3A3910',
- },
- status: 'current',
- source: {
- repo: 'gamut',
- githubLink:
- 'https://github.com/Codecademy/gamut/blob/main/packages/gamut/src/GridForm/GridForm.tsx',
- },
-};
-
-
-
-
-
-## Usage
-
-The GridForm organism provides an easy, out-of-the-box way to implement forms from a list of fields. When provided a list of fields, GridForm strings together the appropriate Form Elements inside a LayoutGrid.
-
-GridForm provides the following benefits:
-
-1. **Simplicity**: This organism takes in plain JSON-like props and uses them to string together a validated form
-2. **Accessibility**: All GridForms handle accessibility styling and behaviors, passing tests out-of-the-box
-3. **Functionality**: Validation and submission logic is handled by the [react-hook-form](https://react-hook-form.com) library
-4. **Visual Consistency**: Aligns all input elements with the correct vertical rhythms and grid spacing
-
-## Designing with GridForm
-
-All [Form Input](https://www.figma.com/file/ReGfRNillGABAj5SlITalN/%F0%9F%93%90-Gamut?node-id=1189%3A0) components in the Figma library are consistent with their implementations in code. By setting the form inputs within the component's layout grid, we can design forms that are compatible with Gamut.
-
-The [GridForm page in Gamut](https://www.figma.com/file/ReGfRNillGABAj5SlITalN/%F0%9F%93%90-Gamut?node-id=1689%3A3910) also contains several starter templates for incorporating this organism in your designs.
-
-- **Starter**: Contains sample components to begin creating your own form
-- **Sections**: Contains the headers and dividers that are rendered in the optional Sections modules
-- **Inline Submit**: A form incorporating the Inline Submit Button style of GridForm
-- **Instructions**: Contains suggestions for modifying the Figma component for your own designs
-
-### Figma component instructions
-
-- Enable Layout Grid (^G)
-- Select a `❖ GridForm` variant as a template
- - Starter, Sections, Inline Submit
-- Detatch the component to modify the `📐 LayoutGrid`
-- Add, remove, and edit `⬦ Form Inputs`
- - Input Field, TextArea, Checkbox, Radio Button, Select
-- Customize `🚥 GridFormButtons`
- - Submit button style (Fill/CTA), position, cancel button
-
-## Customizations
-
-### Disabled inputs
-
-If an input is not meant to be usable, such as when a portion of a form is disabled pending user action, you can make it visually disabled with a `disabled` field member.
-
-
-
-### GridForm-atting
-
-We can use the `size` and `rowspan` props (borrowed from LayoutGrid) to customize the layouts of our GridForms. You can also customize the `FormRequiredText` using the `requiredTextProps`. `FormRequiredText` should be spaced apart from or be stylistically different from the text element above it.
-
-
-
-### Submit button position
-
-We can position the submit button by passing the `position` prop with a
-value of `'left'`, `'center'`, `'right'`, or `'stretch'`.
-
-
-
-### Submit button options
-
-We can specify the version of our button by passing the type prop. We can choose between
-the `FillButton` or `CTAButton`.
-
-
-
-### Inline submit button
-
-We can make the Submit button inline with an input by setting the column
-sizes so they fit on the same row (e.g size 8 for an input and size 4 for
-the submit).
-
-We can additionally remove the label from text inputs and checkbox inputs.
-Use the `hideLabel` prop to remove the label, allowing the submit button to
-align nicely with the input. **However**, if using `hideLabel` to remove the default label, you should provide an `aria-label` and/or include another label to the right/left of the input to ensure the input is accessible.
-
-
-
-### Cancel button
-
-Optionally, include a cancel button.
-
-
-
-### Custom inputs
-
-Some forms, such as the checkout flows that use Recurly, need to define
-their own inputs. We can specify a 'custom' field type to with a [render prop](https://reactjs.org/docs/render-props.html).
-
-We also have a 'custom-group' type for when you are passing in a custom FormGroup - including a label. If you do not want GridForm to surface errors for your field, you should likely use a 'custom-group'. If you chose to go this route, please be aware of [accessibility best practices](https://www.deque.com/blog/anatomy-of-accessible-forms-best-practices/) for forms.
-
-
-
-### Placeholder text
-
-Text inputs are allowed to have traditional `placeholder` text.
-This is a somewhat dangerous behavior for accessibility, as browsers
-generally don't render placeholder text with high enough color contrast
-for AA standards. If you do need to use placeholder text, such as on
-landing page forms that have been shown to have higher completion rates
-with the text, please make sure the placeholder text doesn't add any new
-information to the form -- it should really only rephrase the text label.
-
-See [this article](https://www.nngroup.com/articles/form-design-placeholders/) or
-more details on why using placeholders is often bad.
-
-
-
-### On field update
-
-A field can take an onUpdate callback. This callback will fire when the
-field's value changes. This could be useful if you need to use the
-field's value in a parent component before onSubmit gets triggered.
-
-
-
-### InfoTip + GridForm
-
-A field can include our existing `InfoTip`. The position of the infotip on each field is always set to the bottom-right.
-
-See the `Radio` story for an example of how to add a infotip to a radio option.
-
-
-
-### Sections
-
-Our `GridForm`s optionally take an array of sections that have left and center-aligned variants.
-
-Each sections should have a title that correctly follows [heading ranks](https://usability.yale.edu/web-accessibility/articles/headings). You can set the text type of this title though the `as` prop on that section -- the default is `h2`. You can only set the `as` prop to a heading.
-
-You can set the Text variant prop for the section title the same way. Only title variants are reccomended, but if you need more granular control of the Text component, you can pass them into `titleWrapperProps`.
-
-When using the left-aligned layout, please note that the `title` takes up 3 columns, so any field size over 9 may cause inconsistent behavior!
-
-
-
-### Custom error
-
-A field can take a custom error in addition to validation errors. The validation error will always take precedence to the custom error.
-
-
-
-### Hidden input
-
-Hidden inputs can be used to include data that users can't see or modify with the submission. For this implementation you can set the `defaultValue` in the object and it will be submitted with the regular form data.
-
-
-
-### Sweet container input
-
-"Sweet container" ([honeypot]()) inputs can be used to detect bots by providing a field that would not generally be clicked by human users, but might be triggered automatically by bots.
-
-We call it a "sweet container" so that bots do not immediately detect it as a honeypot input.
-
-
-
-### Markdown errors
-
-GridForm renders errors through our Markdown component so we can optionally add markdown to our validation messages.
-
-
-
-### ``
-
-By toggling dark mode you can see all the colors map to a new color that is accessible for the mode by default. Please use the ColorMode control on the top of this page and navigating to the Playground example to check it out!
-
-**Note**: you **cannot** use the deprecated 'business' button type with `ColorMode`.
-
-### Checkbox spacing
-
-If you need to checkboxes to fit into a smaller space, you can use our our `tight` spacing prop for checkboxes that are a bit closer together.
-
-
-
-### Loading and disabled states
-
-You can set the state of the submit button to `loading` as `true` to show a loading spinner. This is useful when you need to show the user that the form is submitting.
-You can also set `disabled` to `true` to disable submission.
-
-
-
-### Disabled fields on submit
-
-`disableFieldsOnSubmit` will disable all form fields once the form has been successfully submitted. If you have any server-side validation that needs to happen, we recommend using the `wasSubmitSuccessful` prop, but submission will also fail if a promise is rejected within your `onSubmit` or if a field does not pass validation.
-
-
-
-### Reset form on submit
-
-`resetOnSubmit` will reset the form once the GridForm has been successfully submitted. If you have any server validation that needs to happen, we recommend using the `wasSubmitSuccessful` prop, but submission will also fail if a promise is rejected within your `onSubmit` or if a field does not pass validation.
-
-
-
-We can combine these together to create some pretty cool forms which have a loading state, disable their fields while submitting, and reset the form when the submit was successful.
-
-
-
-### `hideRequiredText`
-
-`hideRequiredText` will hide the '\* Required' text that appears at the top of our forms. This should only be hidden if the form has no required fields.
-
-
-
-### Solo field form
-
-Solo field form should always have their solo input be required. They should automagically not have the required/optional text - if you have a custom rendered hidden input, you may have to use the `hasSoloField` prop.
-
-
-
-## Playground
-
-
-
-
diff --git a/packages/styleguide/src/lib/Organisms/GridForm/GridForm.stories.tsx b/packages/styleguide/src/lib/Organisms/GridForm/GridForm.stories.tsx
deleted file mode 100644
index 0d3713ba3d8..00000000000
--- a/packages/styleguide/src/lib/Organisms/GridForm/GridForm.stories.tsx
+++ /dev/null
@@ -1,1419 +0,0 @@
-import {
- Column,
- FormGroup,
- GridForm,
- Input,
- LayoutGrid,
- Markdown,
-} from '@codecademy/gamut';
-import { Background } from '@codecademy/gamut-styles';
-import { action } from '@storybook/addon-actions';
-import type { Meta, StoryObj } from '@storybook/react';
-import { ComponentProps, useState } from 'react';
-import type { TypeWithDeepControls } from 'storybook-addon-deep-controls';
-
-const meta: TypeWithDeepControls> = {
- component: GridForm,
- args: {
- fields: [
- {
- label: 'Simple text',
- name: 'simple-text',
- size: 3,
- type: 'text',
- },
- {
- defaultValue: 'yeet',
- label: 'Text with default value',
- name: 'text-with-default',
- size: 4,
- type: 'text',
- },
- {
- label: 'Simple select',
- name: 'simple-select',
- options: ['', 'One fish', 'Two fish', 'Red fish', 'Blue fish'],
- size: 5,
- type: 'select',
- validation: {
- required: 'Please select an option',
- },
- },
- {
- label: 'Upload a cat image (we support pdf, jpeg, or png files)',
- name: 'file-input',
- size: 4,
- type: 'file',
- validation: {
- required: true,
- validate: (files) => {
- const { type } = files.item(0);
- const allowedTypes = ['application/pdf', 'image/jpeg', 'image/png'];
- if (!allowedTypes.includes(type))
- return 'Please upload a pdf, jpeg, or png file.';
- return true;
- },
- },
- },
- {
- label: 'Write a paragraph about penguins',
- name: 'textarea-input',
- size: 6,
- type: 'textarea',
- validation: {
- required: 'Please write something about penguins!',
- },
- },
- {
- label:
- "Validated, required text that must contain the word 'swag' twice",
- name: 'validated-required-text',
- size: 5,
- type: 'text',
- validation: {
- required: true,
- pattern: {
- value: /swag(.*)swag/,
- message: 'Not enough swag',
- },
- },
- },
- {
- description: 'I have swag',
- label: 'Swag levels',
- name: 'enough-swag',
- size: 3,
- type: 'checkbox',
- id: 'my-super-cool-id',
- defaultValue: true,
- },
- {
- label: 'Preferred Modern Artist',
- name: 'artist',
- options: [
- {
- label: 'Cardi B',
- value: 'cardi',
- infotip: { info: 'This is super important.' },
- },
- {
- label: 'Nicki Minaj',
- value: 'nicki',
- },
- ],
- size: 4,
- type: 'radio-group',
- validation: {
- required: 'You gotta pick one!',
- },
- },
- {
- label: 'End User License Agreement',
- description: 'I accept the terms and conditions (required or else!!!)',
- name: 'eula-checkbox-required',
- type: 'checkbox',
- validation: {
- required: 'Please check the box to agree to the terms.',
- },
- size: 4,
- },
- {
- label: 'Nested checkboxes',
- name: 'nested-checkboxes',
- type: 'nested-checkboxes',
- defaultValue: ['backend', 'react', 'vue'],
- options: [
- {
- value: 'frontend',
- label: 'Frontend Technologies',
- options: [
- {
- value: 'react',
- label: 'React',
- options: [
- { value: 'nextjs', label: 'Next.js' },
- { value: 'typescript', label: 'TypeScript' },
- ],
- },
- {
- value: 'vue',
- label: 'Vue.js',
- },
- { value: 'angular', label: 'Angular' },
- ],
- },
- {
- value: 'backend',
- label: 'Backend Technologies',
- options: [
- { value: 'node', label: 'Node.js' },
- { value: 'python', label: 'Python' },
- { value: 'java', label: 'Java' },
- ],
- },
- ],
- size: 12,
- },
- ],
- submit: {
- contents: 'Submit Me!?',
- size: 4,
- position: 'left',
- disabled: false,
- loading: false,
- type: 'fill',
- },
- onSubmit: (values) => {
- action('Form Submitted')(values);
- // eslint-disable-next-line no-console
- console.log('Form Submitted', values);
- },
- validation: 'onSubmit',
- resetOnSubmit: true,
- },
- argTypes: {
- 'submit.type': {
- control: 'radio',
- options: ['fill', 'cta'],
- table: {
- defaultValue: { summary: 'fill' },
- type: { summary: 'fill | cta' },
- },
- description: 'The type of the submit button.',
- },
- 'submit.position': {
- control: 'radio',
- options: ['left', 'center', 'right', 'stretch'],
- table: {
- defaultValue: { summary: 'left' },
- type: { summary: 'left | center | right | stretch' },
- },
- description: 'The position of the submit button.',
- },
- 'submit.size': {
- control: {
- type: 'number',
- min: 1,
- max: 12,
- step: 1,
- },
- description: 'The column size of the submit button.',
- },
- 'submit.contents': {
- control: 'text',
- table: {
- type: { summary: 'string' },
- },
- description: 'The text of the submit button.',
- },
- cancel: {
- table: {
- disable: true,
- },
- },
- 'cancel.children': {
- control: 'text',
- table: {
- type: { summary: 'string' },
- },
- description: 'The text of the cancel button.',
- },
- 'cancel.onClick': {
- table: {
- type: { summary: 'function' },
- },
- },
- 'cancel.href': {
- control: 'text',
- table: {
- type: { summary: 'string' },
- },
- },
- },
-};
-
-export default meta;
-type Story = StoryObj;
-
-export const Default: React.FC> = (args) => {
- return ;
-};
-
-const DisabledInputsExample = () => {
- return (
- {
- action('Form Submitted')(values);
- }}
- />
- );
-};
-
-export const DisabledInputs: Story = {
- render: () => ,
-};
-
-const FormattedExample = () => {
- return (
- {
- action('Form Submitted')(values);
- }}
- />
- );
-};
-
-export const Formatted: Story = {
- render: () => ,
-};
-
-const SubmitButtonPositionExample = () => {
- return (
-
-
- {
- action('Form Submitted')(values);
- }}
- />
-
-
- {
- action('Form Submitted')(values);
- }}
- />
-
-
- {
- action('Form Submitted')(values);
- }}
- />
-
-
- {
- action('Form Submitted')(values);
- }}
- />
-
-
- );
-};
-
-export const SubmitButtonPosition: Story = {
- render: () => ,
-};
-
-const SubmitButtonOptionsExample = () => {
- return (
- <>
- {
- action('Form Submitted')(values);
- }}
- />
- {
- action('Form Submitted')(values);
- }}
- />
- {
- action('Form Submitted')(values);
- }}
- />
- {
- action('Form Submitted')(values);
- }}
- />
- >
- );
-};
-
-export const SubmitButtonOptions: Story = {
- render: () => ,
-};
-
-const InlineExample = () => {
- return (
- {
- action('Form Submitted')(values);
- }}
- />
- );
-};
-
-export const InlineSubmit: Story = {
- render: () => ,
-};
-
-const CancelButtonExample = () => {
- return (
- {},
- }}
- fields={[
- {
- label: 'Simple text',
- name: 'right-sub-simple-text',
- type: 'text',
- size: 12,
- },
- ]}
- hideRequiredText
- submit={{
- contents: 'Right Submit!?',
- position: 'right',
- size: 12,
- }}
- onSubmit={(values) => {
- action('Form Submitted')(values);
- }}
- />
- );
-};
-
-export const CancelButton: Story = {
- render: () => ,
-};
-
-const CustomInputsExample = () => {
- return (
- (
- <>
- setValue(event.target.value)}
- />
- 🕺
- >
- ),
- label: 'Gimme two more swags',
- name: 'custom-input',
- size: 12,
- validation: {
- required: true,
- pattern: {
- value: /swag(.*)swag/,
- message: 'Still not enough swag, what are you doing... 💢',
- },
- },
- type: 'custom',
- },
- {
- render: ({ error, setValue }) => (
-
- setValue(event.target.value)}
- />
-
- ),
- size: 12,
- label: 'Gimme two more swags',
- name: 'custom-input-group',
- validation: {
- required: true,
- pattern: {
- value: /swag(.*)swag/,
- message: 'Still not enough swag, what are you doing... 💢',
- },
- },
- type: 'custom-group',
- },
- ]}
- submit={{
- contents: 'Submit Me!?',
- size: 12,
- }}
- onSubmit={(values) => {
- action('Form Submitted')(values);
- }}
- />
- );
-};
-
-export const CustomInputs: Story = {
- render: () => ,
-};
-
-const PlaceholderTextExample = () => {
- return (
- {
- action('Form Submitted')(values);
- }}
- />
- );
-};
-
-export const PlaceholderText: Story = {
- render: () => ,
-};
-
-const OnFieldUpdateExample = () => {
- const [text, setText] = useState('');
- return (
- <>
- <>The text value is: {text}>
- {
- action('Form Submitted')(values);
- }}
- />
- >
- );
-};
-
-export const OnFieldUpdate: Story = {
- render: () => ,
-};
-
-const InfoTipExample = () => {
- return (
- <>
-
- ),
- alignment: 'bottom-right',
- },
- label: 'Select with infotip',
- options: ['', 'Water', 'Earth', 'Fire', 'Air', 'Boomerang'],
- size: 3,
- type: 'select',
- validation: {
- required: 'Please select an option',
- },
- name: 'select-field',
- },
- {
- infotip: {
- info: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
- },
- label: 'Write a paragraph about infotips',
- name: 'textarea-field',
- size: 6,
- type: 'textarea',
- rows: 6,
- placeholder: 'Check out my infotip',
- validation: {
- required: 'Please write something about infotips!',
- },
- },
- {
- infotip: {
- info: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
- },
- label: 'Preferred Modern Artist',
- name: 'modern-artist',
- options: [
- {
- label: 'Taylor Swift',
- value: 'taylor-swift',
- },
- {
- label: 'Beyonce',
- value: 'beyonce',
- },
- ],
- size: 3,
- type: 'radio-group',
- validation: {
- required: 'You gotta pick one!',
- },
- },
- {
- infotip: {
- info: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
- alignment: 'bottom-right',
- },
- label: 'End User License Agreement',
- description: 'I promise that I read it',
- name: 'eula-checkbox-required-agreement',
- size: 4,
- type: 'checkbox',
- validation: {
- required: 'Please check the box to agree to the terms.',
- },
- },
- ]}
- submit={{
- contents: 'Submit',
- size: 12,
- }}
- onSubmit={(values) => {
- action('Form Submitted')(values);
- }}
- />
- >
- );
-};
-
-export const InfoTip: Story = {
- render: () => ,
-};
-
-const SectionsExample = () => {
- return (
- {
- action('Form Submitted')(values);
- }}
- />
- );
-};
-
-export const Sections: Story = {
- render: () => ,
-};
-
-const CustomErrorExample = () => {
- return (
- {
- action('Form Submitted')(values);
- }}
- />
- );
-};
-
-export const CustomError: Story = {
- render: () => ,
-};
-
-const HiddenInputExample = () => {
- return (
- {
- action('Form Submitted')(values);
- }}
- />
- );
-};
-
-export const HiddenInput: Story = {
- render: () => ,
-};
-
-const SweetContainerExample = () => {
- return (
- {
- action('Form Submitted')(values);
- }}
- />
- );
-};
-
-export const SweetContainer: Story = {
- render: () => ,
-};
-
-const MarkdownErrorsExample = () => {
- return (
- {
- action('Form Submitted')(values);
- }}
- />
- );
-};
-
-export const MarkdownErrors: Story = {
- render: () => ,
-};
-
-const CheckboxSpacingExample = () => {
- return (
- {
- action('Form Submitted')(values);
- }}
- />
- );
-};
-
-export const CheckboxSpacing: Story = {
- render: () => ,
-};
-
-const LoadingAndDisabledExample = () => {
- return (
- <>
-
- {
- action('Form Submitted')(values);
- }}
- />
-
-
- {
- action('Form Submitted')(values);
- }}
- />
-
- >
- );
-};
-
-export const LoadingAndDisabled: Story = {
- render: () => ,
-};
-
-const DisabledFieldsOnSubmitExample = () => {
- return (
- <>
-
- {
- action('Form Submitted')(values);
- }}
- />
-
-
- {
- action('Form Submitted')(values);
- }}
- />
-
-
-
- false || "It's never gonna work out between us",
- },
- },
- size: 12,
- },
- ]}
- hideRequiredText
- submit={{
- contents: 'Submit Me 🤷🏻',
- size: 12,
- }}
- onSubmit={(values) => {
- action('Form Submitted')(values);
- }}
- />
-
- >
- );
-};
-
-export const DisabledFieldsOnSubmit: Story = {
- render: () => ,
-};
-
-const ResetOnSubmitExample = () => {
- return (
- <>
-
- {
- action('Form Submitted')(values);
- }}
- />
-
- >
- );
-};
-
-export const ResetOnSubmit: Story = {
- render: () => ,
-};
-
-export const FormLoadingExample = () => {
- const [loading, setLoading] = useState(false);
-
- const wait = (ms: number) =>
- new Promise((resolve) => setTimeout(resolve, ms));
-
- const onSubmit = async () => {
- setLoading(true);
- await wait(2000);
- setLoading(false);
- };
-
- return (
-
-
-
- );
-};
-
-export const FormLoading: Story = {
- render: () => ,
-};
-
-const HideRequiredTextExample = () => {
- return (
- <>
-
- {
- action('Form Submitted')(values);
- }}
- />
-
- >
- );
-};
-
-export const HideRequiredText: Story = {
- render: () => ,
-};
-
-const SoloFieldExample = () => {
- return (
- <>
-
- {
- action('Form Submitted')(values);
- }}
- />
-
- >
- );
-};
-
-export const SoloField: Story = {
- render: () => ,
-};
diff --git a/packages/styleguide/src/lib/Organisms/GridForm/Layout.mdx b/packages/styleguide/src/lib/Organisms/GridForm/Layout.mdx
new file mode 100644
index 00000000000..0c1ac337c07
--- /dev/null
+++ b/packages/styleguide/src/lib/Organisms/GridForm/Layout.mdx
@@ -0,0 +1,53 @@
+import { Canvas, Meta } from '@storybook/blocks';
+
+import { ComponentHeader, LinkTo } from '~styleguide/blocks';
+
+import * as LayoutStories from './Layout.stories';
+
+export const parameters = {
+ title: 'Layout',
+ subtitle: 'Customize the visual layout and styling of your forms.',
+ status: 'static',
+};
+
+
+
+
+
+## GridForm-atting
+
+We can use the `size` and `rowspan` props (borrowed from LayoutGrid) to customize the layouts of our GridForms. You can also customize the `FormRequiredText` using the `requiredTextProps`. `FormRequiredText` should be spaced apart from or be stylistically different from the text element above it.
+
+
+
+## `hideRequiredText`
+
+`hideRequiredText` will hide the '\* Required' text that appears at the top of our forms. This should only be hidden if the form has no required fields.
+
+
+
+## Solo field form
+
+Solo field form should always have their solo input be required. They should automagically not have the required/optional text - if you have a custom rendered hidden input, you may have to use the `hasSoloField` prop.
+
+
+
+## InfoTips
+
+A field can include our existing `InfoTip`. See the InfoTip story for more information on what props are available.
+
+See the Radio story for an example of how to add a infotip to a radio option.
+
+
+
+## Sections
+
+`GridForm` can optionally take an array of sections that have left and center-aligned variants.
+
+Each sections should have a title that correctly follows [heading ranks](https://usability.yale.edu/web-accessibility/articles/headings). You can set the text type of this title though the `as` prop on that section -- the default is `h2`. You can only set the `as` prop to a heading.
+
+You can set the Text variant prop for the section title the same way. Only title variants are recommended, but if you need more granular control of the `Text` component, you can pass them into `titleWrapperProps`.
+
+When using the left-aligned layout, please note that the `title` takes up 3 columns, so any field size over 9 may cause inconsistent behavior!
+
+
diff --git a/packages/styleguide/src/lib/Organisms/GridForm/Layout.stories.tsx b/packages/styleguide/src/lib/Organisms/GridForm/Layout.stories.tsx
new file mode 100644
index 00000000000..407191e62fb
--- /dev/null
+++ b/packages/styleguide/src/lib/Organisms/GridForm/Layout.stories.tsx
@@ -0,0 +1,284 @@
+import { GridForm, Markdown } from '@codecademy/gamut';
+import { action } from '@storybook/addon-actions';
+import type { Meta, StoryObj } from '@storybook/react';
+
+const meta: Meta = {
+ component: GridForm,
+ args: {
+ onSubmit: (values) => {
+ action('Form Submitted')(values);
+ // eslint-disable-next-line no-console
+ console.log('Form Submitted', values);
+ },
+ submit: {
+ contents: 'Submit',
+ size: 4,
+ position: 'left',
+ },
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Formatted: Story = {
+ args: {
+ fields: [
+ {
+ label: 'Fave Gamut Component',
+ name: 'rowspan-radiogroup',
+ options: [
+ {
+ label: 'FlexBox',
+ value: 'flex',
+ },
+ {
+ label: 'GridForm',
+ value: 'grid',
+ },
+ {
+ label: 'Text',
+ value: 'text',
+ },
+ ],
+ size: 3,
+ rowspan: 3,
+ type: 'radio-group',
+ validation: {
+ required: 'You gotta pick one!',
+ },
+ },
+ {
+ label: 'Simple text',
+ name: 'rowspan-simple-text',
+ size: 3,
+ type: 'text',
+ },
+ {
+ defaultValue: 'yeet',
+ label: 'Text with default value',
+ name: 'text-with-default-formatting',
+ size: 4,
+ type: 'text',
+ },
+ {
+ label: 'Simple select (required)',
+ name: 'simple-select-formatting',
+ options: ['', 'One fish', 'Two fish', 'Red fish', 'Blue fish'],
+ size: 5,
+ type: 'select',
+ validation: {
+ required: 'Please select an option',
+ },
+ },
+ ],
+ requiredTextProps: { color: 'danger', variant: 'title-xs' },
+ },
+};
+
+export const HideRequiredText: Story = {
+ args: {
+ fields: [
+ {
+ label: 'A field',
+ placeholder: 'I am very optional',
+ name: 'very-optional',
+ type: 'text',
+ size: 12,
+ },
+ {
+ label: 'A field',
+ placeholder: 'I am very optional',
+ name: 'very-optional',
+ type: 'text',
+ size: 12,
+ },
+ ],
+ hideRequiredText: true,
+ },
+};
+
+export const SoloField: Story = {
+ args: {
+ fields: [
+ {
+ label: 'A field',
+ placeholder: 'I am a required solo field',
+ name: 'so-required',
+ type: 'text',
+ size: 12,
+ validation: {
+ required: 'I am required',
+ },
+ },
+ ],
+ },
+};
+
+export const InfoTip: Story = {
+ args: {
+ fields: [
+ {
+ infotip: {
+ info: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
+ alignment: 'bottom-left',
+ },
+ label: 'Tool input',
+ name: 'input-field',
+ size: 6,
+ type: 'text',
+ },
+ {
+ infotip: {
+ info: ,
+ alignment: 'bottom-right',
+ },
+ label: 'Select with infotip',
+ options: ['', 'Water', 'Earth', 'Fire', 'Air', 'Boomerang'],
+ size: 3,
+ type: 'select',
+ validation: {
+ required: 'Please select an option',
+ },
+ name: 'select-field',
+ },
+ {
+ infotip: {
+ info: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
+ },
+ label: 'Write a paragraph about infotips',
+ name: 'textarea-field',
+ size: 6,
+ type: 'textarea',
+ rows: 6,
+ placeholder: 'Check out my infotip',
+ validation: {
+ required: 'Please write something about infotips!',
+ },
+ },
+ {
+ infotip: {
+ info: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
+ },
+ label: 'Preferred Modern Artist',
+ name: 'modern-artist',
+ options: [
+ {
+ label: 'Taylor Swift',
+ value: 'taylor-swift',
+ },
+ {
+ label: 'Beyonce',
+ value: 'beyonce',
+ },
+ ],
+ size: 3,
+ type: 'radio-group',
+ validation: {
+ required: 'You gotta pick one!',
+ },
+ },
+ {
+ infotip: {
+ info: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
+ alignment: 'bottom-right',
+ },
+ label: 'End User License Agreement',
+ description: 'I promise that I read it',
+ name: 'eula-checkbox-required-agreement',
+ size: 4,
+ type: 'checkbox',
+ validation: {
+ required: 'Please check the box to agree to the terms.',
+ },
+ },
+ ],
+ },
+};
+
+export const Sections: Story = {
+ args: {
+ fields: [
+ {
+ title: 'first section',
+ layout: 'left',
+ variant: 'title-xs',
+ titleWrapperProps: {
+ color: 'danger',
+ },
+ fields: [
+ {
+ label: 'hi?',
+ name: 'text01-left-section',
+ size: 4,
+ type: 'text',
+ validation: {
+ required: true,
+ },
+ },
+ {
+ label: 'hello?',
+ name: 'text02-left-section',
+ size: 4,
+ type: 'text',
+ validation: {
+ required: true,
+ },
+ },
+ {
+ label: 'Write a paragraph.',
+ name: 'paragraph01-left-section',
+ size: 8,
+ type: 'textarea',
+ validation: {
+ required: 'Please write something about penguins!',
+ },
+ },
+ {
+ label: 'howdy?',
+ name: 'text03-left-section',
+ size: 4,
+ type: 'text',
+ validation: {
+ required: true,
+ },
+ },
+ {
+ label: 'whats up?',
+ name: 'text04--left-section',
+ size: 4,
+ type: 'text',
+ validation: {
+ required: true,
+ },
+ },
+ {
+ label: 'Write another long paragraph',
+ name: 'paragraph02-left-section',
+ size: 8,
+ type: 'textarea',
+ validation: {
+ required: 'Please write something about penguins!',
+ },
+ },
+ ],
+ },
+ {
+ title: 'hi there... again',
+ as: 'h3',
+ fields: [
+ {
+ label: 'hello....',
+ name: 'text01-center-section',
+ size: 5,
+ type: 'text',
+ validation: {
+ required: true,
+ },
+ },
+ ],
+ },
+ ],
+ requiredTextProps: { color: 'primary', fontStyle: 'italic' },
+ },
+};
diff --git a/packages/styleguide/src/lib/Organisms/GridForm/States.mdx b/packages/styleguide/src/lib/Organisms/GridForm/States.mdx
new file mode 100644
index 00000000000..7a7d865956d
--- /dev/null
+++ b/packages/styleguide/src/lib/Organisms/GridForm/States.mdx
@@ -0,0 +1,59 @@
+import { Canvas, Meta } from '@storybook/blocks';
+
+import { ComponentHeader, LinkTo } from '~styleguide/blocks';
+
+import * as StatesStories from './States.stories';
+
+export const parameters = {
+ title: 'States',
+ subtitle: 'Manage field behavior and form state.',
+ status: 'static',
+};
+
+
+
+
+
+## Disabled inputs
+
+If an input is not meant to be usable, such as when a portion of a form is disabled pending user action, you can make it visually disabled with a `disabled` field member.
+
+
+
+## On field update
+
+A field can take an `onUpdate` callback. This callback will fire when the
+field's value changes. This could be useful if you need to use the
+field's value in a parent component before `onSubmit` gets triggered.
+
+
+
+## Disabled fields on submit
+
+`disableFieldsOnSubmit` will disable all form fields once the form has been successfully submitted. If you have any server-side validation that needs to happen, we recommend using the `wasSubmitSuccessful` prop, but submission will also fail if a promise is rejected within your `onSubmit` or if a field does not pass validation.
+
+
+
+## Reset form on submit
+
+`resetOnSubmit` will reset the form once the GridForm has been successfully submitted. If you have any server validation that needs to happen, we recommend using the `wasSubmitSuccessful` prop, but submission will also fail if a promise is rejected within your `onSubmit` or if a field does not pass validation.
+
+
+
+## Putting it all together
+
+We can combine these together to create some pretty cool forms which have a loading state, disable their fields while submitting, and reset the form when the submit was successful.
+
+
+
+## Custom error
+
+A field can take a custom error in addition to validation errors. The validation error will always take precedence to the custom error.
+
+
+
+## Markdown errors
+
+GridForm renders errors through our Markdown component so we can optionally add markdown to our validation messages.
+
+
diff --git a/packages/styleguide/src/lib/Organisms/GridForm/States.stories.tsx b/packages/styleguide/src/lib/Organisms/GridForm/States.stories.tsx
new file mode 100644
index 00000000000..dcc3dde1e17
--- /dev/null
+++ b/packages/styleguide/src/lib/Organisms/GridForm/States.stories.tsx
@@ -0,0 +1,197 @@
+import { GridForm } from '@codecademy/gamut';
+import { action } from '@storybook/addon-actions';
+import type { Meta, StoryObj } from '@storybook/react';
+import { useState } from 'react';
+
+const meta: Meta = {
+ component: GridForm,
+ args: {
+ onSubmit: (values) => {
+ action('Form Submitted')(values);
+ // eslint-disable-next-line no-console
+ console.log('Form Submitted', values);
+ },
+ submit: {
+ contents: 'Submit',
+ size: 4,
+ position: 'left',
+ },
+ hideRequiredText: true,
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const DisabledInputs: Story = {
+ args: {
+ fields: [
+ {
+ disabled: true,
+ label: 'Disabled text',
+ name: 'disabled-text',
+ type: 'text',
+ size: 6,
+ },
+ {
+ label: 'Enabled text',
+ name: 'enabled-text',
+ type: 'text',
+ size: 6,
+ },
+ ],
+ },
+};
+
+export const OnFieldUpdate: Story = {
+ render: function OnFieldUpdate(args) {
+ const [text, setText] = useState('');
+ return (
+ <>
+ <>The text value is: {text}>
+
+ >
+ );
+ },
+};
+
+export const DisabledFieldsOnSubmit: Story = {
+ args: {
+ disableFieldsOnSubmit: true,
+ fields: [
+ {
+ label: 'Email',
+ placeholder: 'i will disable on correct submission!',
+ name: 'disabled-fields-on-submit',
+ type: 'email',
+ validation: {
+ required: 'pls fill this out',
+ pattern: {
+ value: /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,})+$/,
+ message: 'that is not an email 😔',
+ },
+ },
+ size: 9,
+ },
+ ],
+ },
+};
+
+export const ResetOnSubmit: Story = {
+ args: {
+ fields: [
+ {
+ label: 'Email',
+ placeholder: 'i will reset on correct submission!',
+ name: 'reset-on-submit',
+ type: 'email',
+ validation: {
+ required: 'pls fill this out',
+ pattern: {
+ value: /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,})+$/,
+ message: 'that is not an email 😔',
+ },
+ },
+ size: 9,
+ },
+ ],
+ resetOnSubmit: true,
+ },
+};
+
+export const FormLoading: Story = {
+ args: {
+ disableFieldsOnSubmit: true,
+ fields: [
+ {
+ label: 'Email',
+ placeholder:
+ 'i will disable form fields on loading and reset on correct submission!',
+ name: 'im-new',
+ type: 'email',
+ validation: {
+ required: 'pls fill this out',
+ pattern: {
+ value:
+ /^(?:[a-zA-Z0-9_]+(?:[.-]?[a-zA-Z0-9_]+)*)@(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/,
+ message: 'that is not an email 😔',
+ },
+ },
+ size: 9,
+ },
+ ],
+ resetOnSubmit: true,
+ },
+ render: function FormLoading(args) {
+ const [loading, setLoading] = useState(false);
+
+ const wait = (ms: number) =>
+ new Promise((resolve) => setTimeout(resolve, ms));
+
+ const onSubmit = async () => {
+ setLoading(true);
+ await wait(2000);
+ setLoading(false);
+ };
+
+ return (
+
+ );
+ },
+};
+
+export const CustomError: Story = {
+ args: {
+ fields: [
+ {
+ label: 'Who is the best at bending?',
+ name: 'custom-error',
+ size: 9,
+ type: 'text',
+ customError: 'NOT Flexo.',
+ validation: {
+ required: true,
+ pattern: {
+ value: /Bender/,
+ message: 'Just type Bender...',
+ },
+ },
+ },
+ ],
+ },
+};
+
+export const MarkdownErrors: Story = {
+ args: {
+ fields: [
+ {
+ label: 'there is a markdown error here!',
+ name: 'markdown-error',
+ type: 'email',
+ validation: {
+ required:
+ 'This is [an example](https://www.youtube.com/watch?v=5IuRzJRrRpQ) error link.',
+ },
+ size: 9,
+ },
+ ],
+ },
+};
diff --git a/packages/styleguide/src/lib/Organisms/GridForm/Usage.mdx b/packages/styleguide/src/lib/Organisms/GridForm/Usage.mdx
new file mode 100644
index 00000000000..46e87f7cfc0
--- /dev/null
+++ b/packages/styleguide/src/lib/Organisms/GridForm/Usage.mdx
@@ -0,0 +1,63 @@
+import { Canvas, Controls, Meta } from '@storybook/blocks';
+
+import { ComponentHeader, LinkTo } from '~styleguide/blocks';
+
+import * as GridFormStories from './Usage.stories';
+
+export const parameters = {
+ title: 'Usage',
+ subtitle: 'How to use and design with GridForm.',
+ design: {
+ type: 'figma',
+ url: 'https://www.figma.com/file/ReGfRNillGABAj5SlITalN/%F0%9F%93%90-Gamut?node-id=1689%3A3910',
+ },
+ status: 'current',
+ source: {
+ repo: 'gamut',
+ githubLink:
+ 'https://github.com/Codecademy/gamut/blob/main/packages/gamut/src/GridForm/GridForm.tsx',
+ },
+};
+
+
+
+
+
+## Usage
+
+The GridForm organism provides an easy, out-of-the-box way to implement forms from a list of fields. When provided a list of fields, GridForm strings together the appropriate Form Elements inside a LayoutGrid.
+
+GridForm provides the following benefits:
+
+1. **Simplicity**: This organism takes in plain JSON-like props and uses them to string together a validated form
+2. **Accessibility**: All GridForms handle accessibility styling and behaviors, passing tests out-of-the-box
+3. **Functionality**: Validation and submission logic is handled by the [react-hook-form](https://react-hook-form.com) library
+4. **Visual Consistency**: Aligns all input elements with the correct vertical rhythms and grid spacing
+
+## Designing with GridForm
+
+All [Form Input](https://www.figma.com/file/ReGfRNillGABAj5SlITalN/%F0%9F%93%90-Gamut?node-id=1189%3A0) components in the Figma library are consistent with their implementations in code. By setting the form inputs within the component's layout grid, we can design forms that are compatible with Gamut.
+
+The [GridForm page in Gamut](https://www.figma.com/file/ReGfRNillGABAj5SlITalN/%F0%9F%93%90-Gamut?node-id=1689%3A3910) also contains several starter templates for incorporating this organism in your designs.
+
+- **Starter**: Contains sample components to begin creating your own form
+- **Sections**: Contains the headers and dividers that are rendered in the optional Sections modules
+- **Inline Submit**: A form incorporating the Inline Submit Button style of GridForm
+- **Instructions**: Contains suggestions for modifying the Figma component for your own designs
+
+### Figma component instructions
+
+- Enable Layout Grid (^G)
+- Select a `❖ GridForm` variant as a template
+ - Starter, Sections, Inline Submit
+- Detatch the component to modify the `📐 LayoutGrid`
+- Add, remove, and edit `⬦ Form Inputs`
+ - Input Field, TextArea, Checkbox, Radio Button, Select
+- Customize `🚥 GridFormButtons`
+ - Submit button style (Fill/CTA), position, cancel button
+
+## Playground
+
+
+
+
diff --git a/packages/styleguide/src/lib/Organisms/GridForm/Usage.stories.tsx b/packages/styleguide/src/lib/Organisms/GridForm/Usage.stories.tsx
new file mode 100644
index 00000000000..bb4fce3b884
--- /dev/null
+++ b/packages/styleguide/src/lib/Organisms/GridForm/Usage.stories.tsx
@@ -0,0 +1,229 @@
+import { GridForm } from '@codecademy/gamut';
+import { action } from '@storybook/addon-actions';
+import type { Meta, StoryObj } from '@storybook/react';
+import type { TypeWithDeepControls } from 'storybook-addon-deep-controls';
+
+const meta: TypeWithDeepControls> = {
+ component: GridForm,
+ args: {
+ fields: [
+ {
+ label: 'Simple text',
+ name: 'simple-text',
+ size: 3,
+ type: 'text',
+ },
+ {
+ defaultValue: 'yeet',
+ label: 'Text with default value',
+ name: 'text-with-default',
+ size: 4,
+ type: 'text',
+ },
+ {
+ label: 'Simple select',
+ name: 'simple-select',
+ options: ['', 'One fish', 'Two fish', 'Red fish', 'Blue fish'],
+ size: 5,
+ type: 'select',
+ validation: {
+ required: 'Please select an option',
+ },
+ },
+ {
+ label: 'Upload a cat image (we support pdf, jpeg, or png files)',
+ name: 'file-input',
+ size: 4,
+ type: 'file',
+ validation: {
+ required: true,
+ validate: (files) => {
+ const { type } = files.item(0);
+ const allowedTypes = ['application/pdf', 'image/jpeg', 'image/png'];
+ if (!allowedTypes.includes(type))
+ return 'Please upload a pdf, jpeg, or png file.';
+ return true;
+ },
+ },
+ },
+ {
+ label: 'Write a paragraph about penguins',
+ name: 'textarea-input',
+ size: 6,
+ type: 'textarea',
+ validation: {
+ required: 'Please write something about penguins!',
+ },
+ },
+ {
+ label:
+ "Validated, required text that must contain the word 'swag' twice",
+ name: 'validated-required-text',
+ size: 5,
+ type: 'text',
+ validation: {
+ required: true,
+ pattern: {
+ value: /swag(.*)swag/,
+ message: 'Not enough swag',
+ },
+ },
+ },
+ {
+ description: 'I have swag',
+ label: 'Swag levels',
+ name: 'enough-swag',
+ size: 3,
+ type: 'checkbox',
+ id: 'my-super-cool-id',
+ defaultValue: true,
+ },
+ {
+ label: 'Preferred Modern Artist',
+ name: 'artist',
+ options: [
+ {
+ label: 'Cardi B',
+ value: 'cardi',
+ infotip: { info: 'This is super important.' },
+ },
+ {
+ label: 'Nicki Minaj',
+ value: 'nicki',
+ },
+ ],
+ size: 4,
+ type: 'radio-group',
+ validation: {
+ required: 'You gotta pick one!',
+ },
+ },
+ {
+ label: 'End User License Agreement',
+ description: 'I accept the terms and conditions (required or else!!!)',
+ name: 'eula-checkbox-required',
+ type: 'checkbox',
+ validation: {
+ required: 'Please check the box to agree to the terms.',
+ },
+ size: 4,
+ },
+ {
+ label: 'Nested checkboxes',
+ name: 'nested-checkboxes',
+ type: 'nested-checkboxes',
+ defaultValue: ['backend', 'react', 'vue'],
+ options: [
+ {
+ value: 'frontend',
+ label: 'Frontend Technologies',
+ options: [
+ {
+ value: 'react',
+ label: 'React',
+ options: [
+ { value: 'nextjs', label: 'Next.js' },
+ { value: 'typescript', label: 'TypeScript' },
+ ],
+ },
+ {
+ value: 'vue',
+ label: 'Vue.js',
+ },
+ { value: 'angular', label: 'Angular' },
+ ],
+ },
+ {
+ value: 'backend',
+ label: 'Backend Technologies',
+ options: [
+ { value: 'node', label: 'Node.js' },
+ { value: 'python', label: 'Python' },
+ { value: 'java', label: 'Java' },
+ ],
+ },
+ ],
+ size: 12,
+ },
+ ],
+ submit: {
+ contents: 'Submit Me!?',
+ size: 4,
+ position: 'left',
+ disabled: false,
+ loading: false,
+ type: 'fill',
+ },
+ onSubmit: (values) => {
+ action('Form Submitted')(values);
+ // eslint-disable-next-line no-console
+ console.log('Form Submitted', values);
+ },
+ validation: 'onSubmit',
+ resetOnSubmit: true,
+ },
+ argTypes: {
+ 'submit.type': {
+ control: 'radio',
+ options: ['fill', 'cta'],
+ table: {
+ defaultValue: { summary: 'fill' },
+ type: { summary: 'fill | cta' },
+ },
+ description: 'The type of the submit button.',
+ },
+ 'submit.position': {
+ control: 'radio',
+ options: ['left', 'center', 'right', 'stretch'],
+ table: {
+ defaultValue: { summary: 'left' },
+ type: { summary: 'left | center | right | stretch' },
+ },
+ description: 'The position of the submit button.',
+ },
+ 'submit.size': {
+ control: {
+ type: 'number',
+ min: 1,
+ max: 12,
+ step: 1,
+ },
+ description: 'The column size of the submit button.',
+ },
+ 'submit.contents': {
+ control: 'text',
+ table: {
+ type: { summary: 'string' },
+ },
+ description: 'The text of the submit button.',
+ },
+ cancel: {
+ table: {
+ disable: true,
+ },
+ },
+ 'cancel.children': {
+ control: 'text',
+ table: {
+ type: { summary: 'string' },
+ },
+ description: 'The text of the cancel button.',
+ },
+ 'cancel.onClick': {
+ table: {
+ type: { summary: 'function' },
+ },
+ },
+ 'cancel.href': {
+ control: 'text',
+ table: {
+ type: { summary: 'string' },
+ },
+ },
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/packages/styleguide/src/lib/Organisms/GridForm/Validation.mdx b/packages/styleguide/src/lib/Organisms/GridForm/Validation.mdx
new file mode 100644
index 00000000000..6b430df07ea
--- /dev/null
+++ b/packages/styleguide/src/lib/Organisms/GridForm/Validation.mdx
@@ -0,0 +1,110 @@
+import { Meta } from '@storybook/blocks';
+
+import { ComponentHeader } from '~styleguide/blocks';
+
+export const parameters = {
+ title: 'Validation',
+ subtitle: 'Robust form validation using react-hook-form.',
+ status: 'static',
+};
+
+
+
+
+
+## Validation
+
+GridForm uses [react-hook-form](https://react-hook-form.com) for validation, providing a robust and flexible validation system. All validation rules are applied to individual fields through the `validation` property.
+
+You can control when validation occurs by setting the `validation` prop on the GridForm:
+
+- `'onSubmit'` (default) - Validate only when the form is submitted
+- `'onChange'` - Validate on every change, submit button is disabled until all required fields are valid
+- `'onTouched'` - Validate when a field loses focus
+
+## Required Fields
+
+The most common validation is making fields required. You can specify required validation as a boolean or with a custom error message:
+
+```tsx
+// Simple required validation
+validation: {
+ required: true;
+}
+
+// Required with custom error message
+validation: {
+ required: 'Please enter your email address';
+}
+```
+
+## Pattern Validation
+
+Use regular expressions (regex) to validate field formats like email addresses, phone numbers, or custom patterns:
+
+```tsx
+validation: {
+ required: 'Email is required',
+ pattern: {
+ value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
+ message: 'Please enter a valid email address'
+ }
+}
+```
+
+## Custom Validation
+
+For complex validation logic, use custom validation functions. These functions receive the field value and can return `true` for valid input or an error message string:
+
+```tsx
+validation: {
+ required: 'Please select a file',
+ validate: (files) => {
+ const file = files.item(0);
+ const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
+ if (!allowedTypes.includes(file.type)) {
+ return 'Please upload a JPEG, PNG, or GIF image';
+ }
+ if (file.size > 2 * 1024 * 1024) {
+ return 'File size must be less than 2MB';
+ }
+ return true;
+ }
+}
+```
+
+## String Length Validation
+
+Validate minimum and maximum string lengths:
+
+```tsx
+validation: {
+ required: 'Message is required',
+ minLength: {
+ value: 10,
+ message: 'Message must be at least 10 characters'
+ },
+ maxLength: {
+ value: 500,
+ message: 'Message cannot exceed 500 characters'
+ }
+}
+```
+
+## Number Range Validation
+
+For number inputs, validate minimum and maximum values:
+
+```tsx
+validation: {
+ required: 'Age is required',
+ min: {
+ value: 18,
+ message: 'Must be at least 18 years old'
+ },
+ max: {
+ value: 99,
+ message: 'Age cannot exceed 99'
+ }
+}
+```