diff --git a/.changeset/neat-sloths-march.md b/.changeset/neat-sloths-march.md
new file mode 100644
index 0000000000..9d65b65396
--- /dev/null
+++ b/.changeset/neat-sloths-march.md
@@ -0,0 +1,7 @@
+---
+"@primer/css": patch
+---
+
+- Updates stories to reflect markup for Rails
+- Clean up FormControl classes
+- Add Radio and Checkbox custom styles
diff --git a/docs/.storybook/main.js b/docs/.storybook/main.js
index 0aca5a6fd1..bbb2b0925a 100644
--- a/docs/.storybook/main.js
+++ b/docs/.storybook/main.js
@@ -7,7 +7,8 @@ module.exports = {
'@storybook/preset-scss',
'@whitespace/storybook-addon-html',
'storybook-addon-designs',
- 'storybook-color-picker'
+ 'storybook-color-picker',
+ 'storybook-addon-variants/preset.js'
],
framework: '@storybook/react',
core: {
diff --git a/docs/package.json b/docs/package.json
index 32607096b8..c09da46036 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -59,6 +59,7 @@
"@whitespace/storybook-addon-html": "^5.0.0",
"babel-loader": "^8.2.5",
"storybook-addon-designs": "6.2.1",
+ "storybook-addon-variants": "^0.0.5",
"storybook-color-picker": "2.3.1"
}
}
diff --git a/docs/src/stories/rails-form-framework/Checkbox.stories.jsx b/docs/src/stories/rails-form-framework/Checkbox.stories.jsx
new file mode 100644
index 0000000000..8e5e2f4d3e
--- /dev/null
+++ b/docs/src/stories/rails-form-framework/Checkbox.stories.jsx
@@ -0,0 +1,117 @@
+import React from 'react'
+import clsx from 'clsx'
+
+export default {
+ title: 'Rails Forms/Checkbox',
+ parameters: {
+ layout: 'padded'
+ },
+ decorators: [
+ Story => (
+
+ )
+ ],
+ excludeStories: ['InputTemplate'],
+ argTypes: {
+ disabled: {
+ description: 'disabled field',
+ control: {type: 'boolean'},
+ table: {
+ category: 'CSS'
+ }
+ },
+ visuallyHidden: {
+ description: 'visually hide label',
+ control: {type: 'boolean'},
+ table: {
+ category: 'CSS'
+ }
+ },
+ label: {
+ type: 'string',
+ name: 'label',
+ description: 'string',
+ table: {
+ category: 'HTML'
+ }
+ },
+ caption: {
+ name: 'caption',
+ type: 'string',
+ description: 'caption',
+ table: {
+ category: 'HTML'
+ }
+ },
+ focusElement: {
+ control: {type: 'boolean'},
+ description: 'set focus on element',
+ table: {
+ category: 'Interactive'
+ }
+ },
+ checked: {
+ control: {type: 'boolean'},
+ description: 'checked',
+ table: {
+ category: 'Interactive'
+ }
+ },
+ indeterminate: {
+ control: {type: 'boolean'},
+ description: 'indeterminate',
+ table: {
+ category: 'Interactive'
+ }
+ }
+ }
+}
+
+const focusMethod = function getFocus() {
+ // find the focusable element
+ var input = document.getElementsByTagName('input')[0]
+ // set focus on element
+ input.focus()
+}
+
+export const InputTemplate = ({label, disabled, visuallyHidden, focusElement, caption, checked, indeterminate}) => (
+ <>
+
+
+ {focusElement && focusMethod()}
+ >
+)
+
+export const Playground = InputTemplate.bind({})
+Playground.args = {
+ label: 'Select an option',
+ disabled: false,
+ focusElement: false,
+ caption: 'Caption',
+ invalid: false,
+ visuallyHidden: false,
+ checked: false,
+ indeterminate: false
+}
diff --git a/docs/src/stories/rails-form-framework/Radio.stories.jsx b/docs/src/stories/rails-form-framework/Radio.stories.jsx
new file mode 100644
index 0000000000..9d6142fe12
--- /dev/null
+++ b/docs/src/stories/rails-form-framework/Radio.stories.jsx
@@ -0,0 +1,127 @@
+import React from 'react'
+import clsx from 'clsx'
+
+export default {
+ title: 'Rails Forms/Radio',
+ parameters: {
+ layout: 'padded'
+ },
+ decorators: [
+ Story => (
+
+ )
+ ],
+ excludeStories: ['RadioTemplate'],
+ argTypes: {
+ disabled: {
+ description: 'disabled field',
+ control: {type: 'boolean'},
+ table: {
+ category: 'CSS'
+ }
+ },
+ visuallyHidden: {
+ description: 'visually hide label',
+ control: {type: 'boolean'},
+ table: {
+ category: 'CSS'
+ }
+ },
+ label: {
+ type: 'string',
+ name: 'label',
+ description: 'string',
+ table: {
+ category: 'HTML'
+ }
+ },
+ caption: {
+ name: 'caption',
+ type: 'string',
+ description: 'caption',
+ table: {
+ category: 'HTML'
+ }
+ },
+ id: {
+ name: 'id',
+ type: 'string',
+ description: 'id',
+ table: {
+ category: 'Radio'
+ }
+ },
+ focusElement: {
+ control: {type: 'boolean'},
+ description: 'set focus on element',
+ table: {
+ category: 'Interactive'
+ }
+ },
+ checked: {
+ control: {type: 'boolean'},
+ description: 'checked',
+ table: {
+ category: 'Interactive'
+ }
+ },
+ indeterminate: {
+ control: {type: 'boolean'},
+ description: 'indeterminate',
+ table: {
+ category: 'Interactive'
+ }
+ }
+ }
+}
+
+const focusMethod = function getFocus() {
+ // find the focusable element
+ var input = document.getElementsByTagName('input')[0]
+ // set focus on element
+ input.focus()
+}
+
+export const RadioTemplate = ({label, disabled, visuallyHidden, focusElement, caption, checked, indeterminate, id}) => (
+ <>
+
+
+ {focusElement && focusMethod()}
+ >
+)
+
+export const Playground = RadioTemplate.bind({})
+Playground.args = {
+ id: 'some-id',
+ label: 'Select an option',
+ disabled: false,
+ focusElement: false,
+ caption: 'Caption',
+ invalid: false,
+ visuallyHidden: false,
+ checked: false,
+ indeterminate: false
+}
diff --git a/docs/src/stories/rails-form-framework/RadioFeatures.stories.jsx b/docs/src/stories/rails-form-framework/RadioFeatures.stories.jsx
new file mode 100644
index 0000000000..2dcef6821b
--- /dev/null
+++ b/docs/src/stories/rails-form-framework/RadioFeatures.stories.jsx
@@ -0,0 +1,14 @@
+import React from 'react'
+import {RadioTemplate} from './Radio.stories.jsx'
+
+export default {
+ title: 'Rails Forms/Radio/Features'
+}
+
+export const MultiRadios = ({}) => (
+
+)
diff --git a/docs/src/stories/rails-form-framework/Select.stories.jsx b/docs/src/stories/rails-form-framework/Select.stories.jsx
new file mode 100644
index 0000000000..35d8053d05
--- /dev/null
+++ b/docs/src/stories/rails-form-framework/Select.stories.jsx
@@ -0,0 +1,210 @@
+import React from 'react'
+import clsx from 'clsx'
+
+export default {
+ title: 'Rails Forms/Select',
+ parameters: {
+ layout: 'padded'
+ },
+ decorators: [
+ Story => (
+
+ )
+ ],
+ excludeStories: ['InputTemplate'],
+ argTypes: {
+ size: {
+ options: [0, 1, 2], // iterator
+ mapping: ['FormControl-small', 'FormControl-medium', 'FormControl-large'], // values
+ control: {
+ type: 'inline-radio',
+ labels: ['small', 'medium', 'large']
+ },
+ table: {
+ category: 'Input'
+ }
+ },
+ validationStatus: {
+ options: [0, 1, 2, 3], // iterator
+ mapping: ['', 'FormControl-error', 'FormControl-success', 'FormControl-warning'], // values
+ control: {
+ type: 'inline-radio',
+ labels: ['undefined', 'error', 'success', 'warning']
+ },
+ table: {
+ category: 'Validation'
+ }
+ },
+ fullWidth: {
+ description: 'formerly called Block',
+ control: {type: 'boolean'},
+ table: {
+ category: 'Input'
+ }
+ },
+ disabled: {
+ description: 'disabled field',
+ control: {type: 'boolean'},
+ table: {
+ category: 'Input'
+ }
+ },
+ required: {
+ description: 'required field',
+ control: {type: 'boolean'},
+ table: {
+ category: 'Input'
+ }
+ },
+ invalid: {
+ description: 'invalid field',
+ control: {type: 'boolean'},
+ table: {
+ category: 'Validation'
+ }
+ },
+ visuallyHidden: {
+ description: 'visually hide label',
+ control: {type: 'boolean'},
+ table: {
+ category: 'Label'
+ }
+ },
+ label: {
+ type: 'string',
+ name: 'label',
+ description: 'string',
+ table: {
+ category: 'Label'
+ }
+ },
+ caption: {
+ name: 'caption',
+ type: 'string',
+ description: 'caption',
+ table: {
+ category: 'Caption'
+ }
+ },
+ validation: {
+ type: 'string',
+ name: 'label',
+ description: 'string',
+ table: {
+ category: 'Validation'
+ }
+ },
+ focusElement: {
+ control: {type: 'boolean'},
+ description: 'set focus on element',
+ table: {
+ category: 'Interactive'
+ }
+ },
+ monospace: {
+ description: 'monospace text',
+ control: {type: 'boolean'},
+ table: {
+ category: 'Input'
+ }
+ },
+ inset: {
+ description: 'formerly called Contrast',
+ control: {type: 'boolean'},
+ table: {
+ category: 'Input'
+ }
+ }
+ }
+}
+
+const focusMethod = function getFocus() {
+ // find the focusable element
+ var input = document.getElementsByTagName('input')[0]
+ // set focus on element
+ input.focus()
+}
+
+export const InputTemplate = ({
+ label,
+ size,
+ fullWidth,
+ placeholder,
+ inset,
+ disabled,
+ visuallyHidden,
+ monospace,
+ focusElement,
+ invalid,
+ caption,
+ validationStatus
+}) => (
+ <>
+
+
+
+
+
+ {invalid && (
+
+
+ {validation}
+
+ )}
+ {caption && (
+
+ {caption}
+
+ )}
+
+ {focusElement && focusMethod()}
+ >
+)
+
+export const Playground = InputTemplate.bind({})
+Playground.args = {
+ type: 'email',
+ placeholder: 'Email address',
+ label: 'Enter email address',
+ fullWidth: false,
+ monospace: false,
+ inset: false,
+ disabled: false,
+ focusElement: false,
+ size: 1,
+ caption: 'Caption',
+ invalid: false,
+ visuallyHidden: false,
+ validationStatus: 0
+}
diff --git a/docs/src/stories/rails-form-framework/TextInput.stories.jsx b/docs/src/stories/rails-form-framework/TextInput.stories.jsx
new file mode 100644
index 0000000000..4412823fd4
--- /dev/null
+++ b/docs/src/stories/rails-form-framework/TextInput.stories.jsx
@@ -0,0 +1,311 @@
+import React from 'react'
+import clsx from 'clsx'
+
+export default {
+ title: 'Rails Forms/TextInput',
+ parameters: {
+ layout: 'padded'
+ },
+ decorators: [
+ Story => (
+
+ )
+ ],
+ excludeStories: ['InputTemplate'],
+ argTypes: {
+ size: {
+ options: [0, 1, 2], // iterator
+ mapping: ['FormControl-small', 'FormControl-medium', 'FormControl-large'], // values
+ control: {
+ type: 'inline-radio',
+ labels: ['small', 'medium', 'large']
+ },
+ table: {
+ category: 'Input'
+ }
+ },
+ validationStatus: {
+ options: [0, 1, 2, 3], // iterator
+ mapping: ['', 'FormControl-error', 'FormControl-success', 'FormControl-warning'], // values
+ control: {
+ type: 'inline-radio',
+ labels: ['undefined', 'error', 'success', 'warning']
+ },
+ table: {
+ category: 'Validation'
+ }
+ },
+ fullWidth: {
+ description: 'formerly called Block',
+ control: {type: 'boolean'},
+ table: {
+ category: 'Input'
+ }
+ },
+ showClearButton: {
+ description: 'show clear button',
+ control: {type: 'boolean'},
+ table: {
+ category: 'Input'
+ }
+ },
+ trailingActionDivider: {
+ description: 'divider between input and trailing action',
+ control: {type: 'boolean'},
+ table: {
+ category: 'Input'
+ }
+ },
+ monospace: {
+ description: 'monospace text',
+ control: {type: 'boolean'},
+ table: {
+ category: 'Input'
+ }
+ },
+ inset: {
+ description: 'formerly called Contrast',
+ control: {type: 'boolean'},
+ table: {
+ category: 'Input'
+ }
+ },
+ disabled: {
+ description: 'disabled field',
+ control: {type: 'boolean'},
+ table: {
+ category: 'Input'
+ }
+ },
+ required: {
+ description: 'required field',
+ control: {type: 'boolean'},
+ table: {
+ category: 'Input'
+ }
+ },
+ invalid: {
+ description: 'invalid field',
+ control: {type: 'boolean'},
+ table: {
+ category: 'Validation'
+ }
+ },
+ visuallyHidden: {
+ description: 'visually hide label',
+ control: {type: 'boolean'},
+ table: {
+ category: 'Label'
+ }
+ },
+ placeholder: {
+ type: 'string',
+ name: 'placeholder',
+ description: 'string',
+ table: {
+ category: 'Input'
+ }
+ },
+ label: {
+ type: 'string',
+ name: 'label',
+ description: 'string',
+ table: {
+ category: 'Label'
+ }
+ },
+ caption: {
+ name: 'caption',
+ type: 'string',
+ description: 'caption',
+ table: {
+ category: 'Caption'
+ }
+ },
+ validation: {
+ type: 'string',
+ name: 'label',
+ description: 'string',
+ table: {
+ category: 'Validation'
+ }
+ },
+ focusElement: {
+ control: {type: 'boolean'},
+ description: 'set focus on element',
+ table: {
+ category: 'Interactive'
+ }
+ },
+ leadingVisual: {
+ name: 'leadingVisual',
+ type: 'boolean',
+ description: 'octicon',
+ table: {
+ category: 'Input'
+ }
+ }
+ }
+}
+
+const focusMethod = function getFocus() {
+ // find the focusable element
+ var input = document.getElementsByTagName('input')[0]
+ // set focus on element
+ input.focus()
+}
+
+export const InputTemplate = ({
+ label,
+ size,
+ fullWidth,
+ placeholder,
+ inset,
+ disabled,
+ visuallyHidden,
+ monospace,
+ focusElement,
+ showClearButton,
+ leadingVisual,
+ invalid,
+ caption,
+ validation,
+ trailingActionDivider,
+ validationStatus
+}) => (
+ <>
+
+
+ {showClearButton || leadingVisual ? (
+
+ {leadingVisual && (
+
+
+
+ )}
+
+ {showClearButton && (
+
+ )}
+
+ ) : (
+
+ )}
+ {invalid && (
+
+
+ {validation}
+
+ )}
+ {caption && (
+
+ {caption}
+
+ )}
+
+ {focusElement && focusMethod()}
+ >
+)
+
+export const Playground = InputTemplate.bind({})
+Playground.args = {
+ placeholder: 'Email address',
+ label: 'Enter email address',
+ fullWidth: false,
+ monospace: false,
+ inset: false,
+ disabled: false,
+ focusElement: false,
+ leadingVisual: false,
+ size: 1,
+ caption: 'Caption',
+ showClearButton: false,
+ invalid: false,
+ visuallyHidden: false,
+ validation: '',
+ trailingActionDivider: false,
+ validationStatus: 0
+}
diff --git a/docs/src/stories/rails-form-framework/Textarea.stories.jsx b/docs/src/stories/rails-form-framework/Textarea.stories.jsx
new file mode 100644
index 0000000000..6a7a31bffc
--- /dev/null
+++ b/docs/src/stories/rails-form-framework/Textarea.stories.jsx
@@ -0,0 +1,199 @@
+import React from 'react'
+import clsx from 'clsx'
+
+export default {
+ title: 'Rails Forms/Textarea',
+ parameters: {
+ layout: 'padded'
+ },
+ decorators: [
+ Story => (
+
+ )
+ ],
+ excludeStories: ['InputTemplate'],
+ argTypes: {
+ validationStatus: {
+ options: [0, 1, 2, 3], // iterator
+ mapping: ['', 'FormControl-error', 'FormControl-success', 'FormControl-warning'], // values
+ control: {
+ type: 'inline-radio',
+ labels: ['undefined', 'error', 'success', 'warning']
+ },
+ table: {
+ category: 'Validation'
+ }
+ },
+ fullWidth: {
+ description: 'formerly called Block',
+ control: {type: 'boolean'},
+ table: {
+ category: 'Input'
+ }
+ },
+ monospace: {
+ description: 'monospace text',
+ control: {type: 'boolean'},
+ table: {
+ category: 'Input'
+ }
+ },
+ inset: {
+ description: 'formerly called Contrast',
+ control: {type: 'boolean'},
+ table: {
+ category: 'Input'
+ }
+ },
+ disabled: {
+ description: 'disabled field',
+ control: {type: 'boolean'},
+ table: {
+ category: 'Input'
+ }
+ },
+ required: {
+ description: 'required field',
+ control: {type: 'boolean'},
+ table: {
+ category: 'Input'
+ }
+ },
+ invalid: {
+ description: 'invalid field',
+ control: {type: 'boolean'},
+ table: {
+ category: 'Validation'
+ }
+ },
+ visuallyHidden: {
+ description: 'visually hide label',
+ control: {type: 'boolean'},
+ table: {
+ category: 'Label'
+ }
+ },
+ placeholder: {
+ type: 'string',
+ name: 'placeholder',
+ description: 'string',
+ table: {
+ category: 'Input'
+ }
+ },
+ label: {
+ type: 'string',
+ name: 'label',
+ description: 'string',
+ table: {
+ category: 'Label'
+ }
+ },
+ caption: {
+ name: 'caption',
+ type: 'string',
+ description: 'caption',
+ table: {
+ category: 'Caption'
+ }
+ },
+ validation: {
+ type: 'string',
+ name: 'label',
+ description: 'string',
+ table: {
+ category: 'Validation'
+ }
+ },
+ focusElement: {
+ control: {type: 'boolean'},
+ description: 'set focus on element',
+ table: {
+ category: 'Interactive'
+ }
+ }
+ }
+}
+
+const focusMethod = function getFocus() {
+ // find the focusable element
+ var input = document.getElementsByTagName('input')[0]
+ // set focus on element
+ input.focus()
+}
+
+export const InputTemplate = ({
+ label,
+ fullWidth,
+ placeholder,
+ inset,
+ disabled,
+ visuallyHidden,
+ monospace,
+ focusElement,
+ invalid,
+ caption,
+ validation,
+ validationStatus
+}) => (
+ <>
+
+
+
+ {invalid && (
+
+
+ {validation}
+
+ )}
+ {caption && (
+
+ {caption}
+
+ )}
+
+ {focusElement && focusMethod()}
+ >
+)
+
+export const Playground = InputTemplate.bind({})
+Playground.args = {
+ placeholder: 'Email address',
+ label: 'Enter email address',
+ fullWidth: false,
+ monospace: false,
+ inset: false,
+ disabled: false,
+ focusElement: false,
+ caption: 'Caption',
+ invalid: false,
+ visuallyHidden: false,
+ validation: '',
+ validationStatus: 0
+}
diff --git a/docs/yarn.lock b/docs/yarn.lock
index a522f5203a..f4fd7c6623 100644
--- a/docs/yarn.lock
+++ b/docs/yarn.lock
@@ -6304,6 +6304,13 @@ capture-exit@^2.0.0:
dependencies:
rsvp "^4.8.4"
+cartesian@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/cartesian/-/cartesian-1.0.1.tgz#ae3fc8a63e2ba7e2c4989ce696207457bcae65af"
+ integrity sha512-tR3qKRYpRJ6FXEGuoBwpuCYcwydrk1N2rduy7eWg1Msepi3i5fCxheryw4VBlCqjCbk3Vhjh3eg+IGHtl5H74A==
+ dependencies:
+ xtend "^4.0.1"
+
case-sensitive-paths-webpack-plugin@^2.3.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz#db64066c6422eed2e08cc14b986ca43796dbc6d4"
@@ -18592,6 +18599,13 @@ storybook-addon-designs@6.2.1:
dependencies:
"@figspec/react" "^1.0.0"
+storybook-addon-variants@^0.0.5:
+ version "0.0.5"
+ resolved "https://registry.yarnpkg.com/storybook-addon-variants/-/storybook-addon-variants-0.0.5.tgz#cd2b221999d0044b3a39a150180f1e738bb504ad"
+ integrity sha512-MSQeVcliKCx/w5MiA7mcW5d7GqeL6kN4oSYKbULo4UldXkNUt3AjmjjuMMfVVcF7TdwWjAgmsHAXPo/vBIXyiQ==
+ dependencies:
+ cartesian "^1.0.1"
+
storybook-color-picker@2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/storybook-color-picker/-/storybook-color-picker-2.3.1.tgz#d1b6e577708747d2599d8af99e125bea0d96982e"
diff --git a/package.json b/package.json
index a1efaab191..aee7e6ec26 100644
--- a/package.json
+++ b/package.json
@@ -41,7 +41,7 @@
"storybook": "cd docs && yarn && yarn storybook"
},
"dependencies": {
- "@primer/primitives": "^7.8.3"
+ "@primer/primitives": "^7.8.4"
},
"devDependencies": {
"@changesets/changelog-github": "0.4.4",
diff --git a/src/base/base.scss b/src/base/base.scss
index 7778f4b5d3..bdd4b39f82 100644
--- a/src/base/base.scss
+++ b/src/base/base.scss
@@ -96,8 +96,6 @@ button,
[role='button'],
input[type='radio'],
input[type='checkbox'] {
- transition: 80ms cubic-bezier(0.33, 1, 0.68, 1);
- transition-property: color, background-color, box-shadow, border-color;
// fallback :focus state
&:focus {
@include focusOutline;
diff --git a/src/forms/FormControl.scss b/src/forms/FormControl.scss
index d5c767c670..914079638a 100644
--- a/src/forms/FormControl.scss
+++ b/src/forms/FormControl.scss
@@ -1,27 +1,23 @@
-// stylelint-disable primer/typography, primer/borders, primer/spacing, primer/box-shadow, max-nesting-depth
+// stylelint-disable primer/typography, primer/borders, primer/spacing, selector-max-type, selector-max-specificity, selector-no-qualifying-type, max-nesting-depth
-// group label, field, caption and error message
-.FormGroup {
+// groups label, field, caption and inline error message
+.FormControl {
display: inline-flex;
flex-direction: column;
gap: var(--base-size-4, 4px);
}
// fill container
-.FormGroup--fullWidth {
+.FormControl--fullWidth {
display: flex;
-
- // stretch field to fill container
- .FormControl-fieldWrap {
- flex-grow: 1;
- }
}
//