1
1
mirror of https://github.com/primer/css.git synced 2024-09-11 16:36:07 +03:00

FormControl refactor/cleanup (#2114)

* checkbox animation adjustment

* radio styles

* disabled state

* pull styles from app header

* cleanup spacing/alignment

* add checkbox focus styles

* stories + radio focus styles

* convert to css check

* refactor to pure *CSS* :)

* configure multi radio story

* lint

* Create neat-sloths-march.md

* snappier animations

* adjust mask size for better scailing

* checkbox mask size

* lint

* Stylelint auto-fixes

* rename classes

* how many times will she refactor?

* slight refactor based on original api

* delete component file test

* Stylelint auto-fixes

* add comments around padding

* Stylelint auto-fixes

* undo changes to old forms

* increase touch target :)

* remove unused grid + cursor pointer

* Stylelint auto-fixes

* adjust disabled checked styles

* bump primitives

* Stylelint auto-fixes

* Update neat-sloths-march.md

* temp add prefix for testing

* test windows high contrast

* cleanup prefix for final build

Co-authored-by: Actions Auto Build <actions@github.com>
This commit is contained in:
Katie Langerman 2022-06-30 14:37:27 -07:00 committed by GitHub
parent 8984684470
commit facb96823a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 1534 additions and 241 deletions

View File

@ -0,0 +1,7 @@
---
"@primer/css": patch
---
- Updates stories to reflect markup for Rails
- Clean up FormControl classes
- Add Radio and Checkbox custom styles

View File

@ -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: {

View File

@ -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"
}
}

View File

@ -0,0 +1,117 @@
import React from 'react'
import clsx from 'clsx'
export default {
title: 'Rails Forms/Checkbox',
parameters: {
layout: 'padded'
},
decorators: [
Story => (
<form>
<Story />
</form>
)
],
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}) => (
<>
<div data-view-component="true" class="FormControl-checkbox-wrap">
<input
id="input-id"
type="checkbox"
disabled={disabled}
class="FormControl-checkbox"
checked={checked ? 'true' : undefined}
indeterminate={indeterminate ? 'true' : undefined}
ariaDescribedBy={caption ? 'caption-ebb67985' : undefined}
/>
<span class="FormControl-checkbox-labelWrap">
<label htmlFor="input-id" className={clsx('FormControl-label', visuallyHidden && 'sr-only')}>
{label}
</label>
{caption && (
<p className="FormControl-caption" id="caption-ebb67985">
{caption}
</p>
)}
</span>
</div>
{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
}

View File

@ -0,0 +1,127 @@
import React from 'react'
import clsx from 'clsx'
export default {
title: 'Rails Forms/Radio',
parameters: {
layout: 'padded'
},
decorators: [
Story => (
<form>
<Story />
</form>
)
],
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}) => (
<>
<div class="FormControl-radio-wrap">
<input
id={id}
name="radio"
type="radio"
disabled={disabled}
class="FormControl-radio"
checked={checked ? 'true' : undefined}
indeterminate={indeterminate ? 'true' : undefined}
ariaDescribedBy={caption ? 'caption-ebb67985' : undefined}
/>
<span class="FormControl-radio-labelWrap">
<label htmlFor={id} className={clsx('FormControl-label', visuallyHidden && 'sr-only')}>
{label}
</label>
{caption && (
<p className="FormControl-caption" id="caption-ebb67985">
{caption}
</p>
)}
</span>
</div>
{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
}

View File

@ -0,0 +1,14 @@
import React from 'react'
import {RadioTemplate} from './Radio.stories.jsx'
export default {
title: 'Rails Forms/Radio/Features'
}
export const MultiRadios = ({}) => (
<form style={{display: 'grid', gap: '.5rem'}}>
<RadioTemplate label="First radio item" caption="This one has a caption!" id="first" />
<RadioTemplate label="Second radio item" id="second" />
<RadioTemplate label="Third radio item" id="third" />
</form>
)

View File

@ -0,0 +1,210 @@
import React from 'react'
import clsx from 'clsx'
export default {
title: 'Rails Forms/Select',
parameters: {
layout: 'padded'
},
decorators: [
Story => (
<form>
<Story />
</form>
)
],
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
}) => (
<>
<div className={clsx('FormControl', fullWidth && 'FormControl--fullWidth')}>
<label htmlFor="input-id" className={clsx('FormControl-label', visuallyHidden && 'sr-only')}>
{label}
</label>
<div className={clsx('FormControl-select-wrap', size && `${size}`)}>
<select
placeholder={placeholder}
id="input-id"
name="input-id"
className={clsx(
'FormControl-select',
size && `${size}`,
validationStatus && `${validationStatus}`,
inset && 'FormControl--inset',
monospace && 'FormControl--monospace'
)}
disabled={disabled ? 'true' : undefined}
invalid={invalid ? 'true' : undefined}
>
<option label="First item" data-view-component="true"></option>
</select>
</div>
{invalid && (
<span className="FormControl-inlineValidation">
<svg
aria-hidden="true"
height="12"
viewBox="0 0 12 12"
version="1.1"
width="12"
className="octicon octicon-alert-fill"
>
<path
fillRule="evenodd"
d="M4.855.708c.5-.896 1.79-.896 2.29 0l4.675 8.351a1.312 1.312 0 01-1.146 1.954H1.33A1.312 1.312 0 01.183 9.058L4.855.708zM7 7V3H5v4h2zm-1 3a1 1 0 100-2 1 1 0 000 2z"
></path>
</svg>
<p id="validation-5e6e1c8a">{validation}</p>
</span>
)}
{caption && (
<p className="FormControl-caption" id="caption-ebb67985">
{caption}
</p>
)}
</div>
{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
}

View File

@ -0,0 +1,311 @@
import React from 'react'
import clsx from 'clsx'
export default {
title: 'Rails Forms/TextInput',
parameters: {
layout: 'padded'
},
decorators: [
Story => (
<form>
<Story />
</form>
)
],
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
}) => (
<>
<div className={clsx('FormControl', fullWidth && 'FormControl--fullWidth')}>
<label htmlFor="input-id" className={clsx('FormControl-label', visuallyHidden && 'sr-only')}>
{label}
</label>
{showClearButton || leadingVisual ? (
<div
className={clsx(
'FormControl-input-wrap',
showClearButton && 'FormControl-input-wrap--trailingAction',
trailingActionDivider && 'FormControl-input-wrap-trailingAction--divider',
size && `${size}`,
leadingVisual && 'FormControl-input-wrap--leadingVisual'
)}
>
{leadingVisual && (
<span class="FormControl-input-leadingVisualWrap">
<svg
aria-hidden="true"
height="16"
viewBox="0 0 16 16"
version="1.1"
width="16"
data-view-component="true"
class="octicon octicon-search FormControl-input-leadingVisual"
>
<path
fill-rule="evenodd"
d="M11.5 7a4.499 4.499 0 11-8.998 0A4.499 4.499 0 0111.5 7zm-.82 4.74a6 6 0 111.06-1.06l3.04 3.04a.75.75 0 11-1.06 1.06l-3.04-3.04z"
></path>
</svg>
</span>
)}
<input
placeholder={placeholder}
id="input-id"
type="text"
className={clsx(
'FormControl-input',
size && `${size}`,
inset && 'FormControl--inset',
monospace && 'FormControl--monospace'
)}
disabled={disabled ? 'true' : undefined}
invalid={invalid ? 'true' : undefined}
/>
{showClearButton && (
<button
id="input-id-clear"
className={clsx(
'FormControl-input-trailingAction',
trailingActionDivider && 'FormControl-input-trailingAction--divider'
)}
aria-label="Clear"
>
<svg
aria-hidden="true"
height="16"
viewBox="0 0 16 16"
version="1.1"
width="16"
data-view-component="true"
class="octicon octicon-x"
>
<path
fill-rule="evenodd"
d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z"
></path>
</svg>
</button>
)}
</div>
) : (
<input
placeholder={placeholder}
id="input-id"
type="text"
disabled={disabled ? 'true' : undefined}
className={clsx(
'FormControl-input',
size && `${size}`,
validationStatus && `${validationStatus}`,
inset && 'FormControl-inset',
monospace && 'FormControl-monospace'
)}
/>
)}
{invalid && (
<span className="FormControl-inlineValidation">
<svg
aria-hidden="true"
height="12"
viewBox="0 0 12 12"
version="1.1"
width="12"
className="octicon octicon-alert-fill"
>
<path
fillRule="evenodd"
d="M4.855.708c.5-.896 1.79-.896 2.29 0l4.675 8.351a1.312 1.312 0 01-1.146 1.954H1.33A1.312 1.312 0 01.183 9.058L4.855.708zM7 7V3H5v4h2zm-1 3a1 1 0 100-2 1 1 0 000 2z"
></path>
</svg>
<p id="validation-5e6e1c8a">{validation}</p>
</span>
)}
{caption && (
<p className="FormControl-caption" id="caption-ebb67985">
{caption}
</p>
)}
</div>
{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
}

View File

@ -0,0 +1,199 @@
import React from 'react'
import clsx from 'clsx'
export default {
title: 'Rails Forms/Textarea',
parameters: {
layout: 'padded'
},
decorators: [
Story => (
<form>
<Story />
</form>
)
],
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
}) => (
<>
<div className={clsx('FormControl', fullWidth && 'FormControl--fullWidth')}>
<label htmlFor="input-id" className={clsx('FormControl-label', visuallyHidden && 'sr-only')}>
{label}
</label>
<textarea
placeholder={placeholder}
id="input-id"
disabled={disabled ? 'true' : undefined}
className={clsx(
'FormControl-textarea',
inset && 'FormControl-inset',
monospace && 'FormControl-monospace',
validationStatus && `${validationStatus}`
)}
/>
{invalid && (
<span className="FormControl-inlineValidation">
<svg
aria-hidden="true"
height="12"
viewBox="0 0 12 12"
version="1.1"
width="12"
className="octicon octicon-alert-fill"
>
<path
fillRule="evenodd"
d="M4.855.708c.5-.896 1.79-.896 2.29 0l4.675 8.351a1.312 1.312 0 01-1.146 1.954H1.33A1.312 1.312 0 01.183 9.058L4.855.708zM7 7V3H5v4h2zm-1 3a1 1 0 100-2 1 1 0 000 2z"
></path>
</svg>
<p id="validation-5e6e1c8a">{validation}</p>
</span>
)}
{caption && (
<p className="FormControl-caption" id="caption-ebb67985">
{caption}
</p>
)}
</div>
{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
}

View File

@ -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"

View File

@ -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",

View File

@ -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;

View File

@ -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;
}
}
// <label>
.FormControl-label {
font-size: var(--primer-text-body-size-medium, 14px);
font-weight: var(--base-text-weight-semibold, 600);
line-height: var(--primer-text-body-lineHeight-medium, 20px);
line-height: var(--primer-text-body-lineHeight-medium, calc(20 / 14));
color: var(--color-fg-default);
user-select: none;
}
@ -30,11 +26,12 @@
margin-bottom: 0;
font-size: var(--primer-text-caption-size, 12px);
font-weight: var(--primer-text-caption-weight, 400);
line-height: var(--primer-text-caption-lineHeight, 16px);
line-height: var(--primer-text-caption-lineHeight, calc(16 / 12));
color: var(--color-fg-muted);
}
.FormControl-validation {
// inline validation message
.FormControl-inlineValidation {
display: flex;
font-size: var(--primer-text-caption-size, 12px);
font-weight: var(--base-text-weight-semibold, 600);
@ -44,339 +41,636 @@
align-items: center;
gap: var(--base-size-4, 4px);
// stylelint-disable-next-line selector-max-type
p {
margin-bottom: 0;
}
}
// shared among all form control components (input, select, textarea, checkbox, radio)
.FormControl {
@mixin Field {
background-color: var(--color-canvas-default);
border: solid var(--primer-borderWidth-thin, 1px) var(--color-border-default);
&[disabled] {
color: var(--color-primer-fg-disabled);
cursor: not-allowed;
background-color: var(--color-input-disabled-bg);
border-color: var(--color-border-default);
-webkit-text-fill-color: var(--color-primer-fg-disabled);
opacity: 1;
-webkit-text-fill-color: var(--color-primer-fg-disabled);
}
&:not(:focus)[invalid] {
border-color: var(--color-danger-emphasis);
}
// <select> and <input>
&.FormControl--input,
&.FormControl--select {
height: var(--primer-control-medium-size, 32px);
font-size: var(--primer-text-body-size-medium, 14px);
line-height: var(--primer-text-body-lineHeight-medium, 20px);
border-radius: var(--primer-borderRadius-medium, 6px);
padding-inline: var(--primer-control-medium-paddingInline-condensed, 8px);
transition: 80ms cubic-bezier(0.33, 1, 0.68, 1);
transition-property: color, background-color, box-shadow, border-color;
&:not([type='checkbox']):not([type='radio']):focus {
@include focusBoxShadowInset;
&[disabled] {
&::placeholder {
color: var(--color-primer-fg-disabled);
}
}
// remove fallback :focus if :focus-visible is supported
&:not(:focus-visible) {
border-color: transparent;
::placeholder {
color: var(--color-fg-subtle);
opacity: 1;
@include focusBoxShadowInset(1px, transparent);
}
}
// default focus state
&:focus-visible {
@include focusBoxShadowInset;
}
}
// TextInput structure
// ===================
//
// .FormControl
// .FormControl-label
// .FormControl-input-wrap
// .FormControl-input-leadingVisualWrap
// .FormControl-input-leadingVisual
// .FormControl-input
// .FormControl-input-trailingAction
// .FormControl-inlineValidation
// .FormControl-caption
// Select structure
// ===================
//
// .FormControl
// .FormControl-label
// .FormControl-select-wrap
// .FormControl-select
// .FormControl-inlineValidation
// .FormControl-caption
// Textarea structure
// ===================
//
// .FormControl
// .FormControl-label
// .FormControl-textarea
// .FormControl-inlineValidation
// .FormControl-caption
.FormControl-input,
.FormControl-select,
.FormControl-textarea {
@include Field;
width: 100%;
font-size: var(--primer-text-body-size-medium, 14px);
line-height: var(--primer-text-body-lineHeight-medium, calc(20 / 14));
border-radius: var(--primer-borderRadius-medium, 6px);
transition: 80ms cubic-bezier(0.33, 1, 0.68, 1);
transition-property: color, background-color, box-shadow, border-color;
padding-inline: var(--primer-control-medium-paddingInline-condensed, 8px);
padding-block: calc(var(--primer-control-medium-paddingBlock, 6px) - var(--primer-borderWidth-thin, 1px));
&[disabled] {
&::placeholder {
color: var(--color-primer-fg-disabled);
}
}
::placeholder {
color: var(--color-fg-subtle);
opacity: 1;
}
// sizes
&.FormControl--small {
&.FormControl-small {
height: var(--primer-control-small-size, 28px);
padding-inline: var(--primer-control-small-paddingInline-normal, 8px);
padding-block: var(--primer-control-small-paddingBlock, 4px);
font-size: var(--primer-text-body-size-small, 12px);
}
&.FormControl--medium {
&.FormControl-medium {
height: var(--primer-control-medium-size, 32px);
}
&.FormControl--large {
&.FormControl-large {
height: var(--primer-control-large-size, 40px);
padding-inline: var(--primer-control-large-paddingInline-normal, 12px);
padding-block: var(--primer-control-large-paddingBlock, 10px);
}
// variants
&.FormControl-inset {
background-color: var(--color-canvas-inset);
}
&.FormControl-monospace {
font-family:
var(
--primer-fontStack-monospace,
'ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace'
);
}
// validation states
&.FormControl-error {
border-color: var(--color-danger-emphasis);
}
&.FormControl-success {
border-color: var(--color-success-emphasis);
}
&.FormControl-warning {
border-color: var(--color-attention-emphasis);
}
}
// pseudo input styles to allow for visual and action slots
// set input styles on -fieldWrap, remove styles from <input>
.FormControl-fieldWrap {
display: inline-grid;
height: var(--primer-control-medium-size, 32px);
background-color: var(--color-canvas-default);
border-radius: var(--primer-borderRadius-medium, 6px);
box-shadow: var(--primer-borderInset-thin, inset 0 0 0 max(1px, 0.0625rem)) var(--color-border-default);
grid-template-rows: auto;
gap: var(--primer-controlStack-medium-gap-condensed, 8px);
align-items: center;
align-content: center;
padding-inline: var(--primer-control-medium-paddingInline-condensed, 8px);
// positioning for leading/trailing items for TextInput
.FormControl-input-wrap {
position: relative;
display: grid;
.FormControl {
padding: unset;
background-color: transparent;
border: 0;
.FormControl-input-leadingVisualWrap {
position: absolute;
top: var(--base-size-8, 8px);
left: var(--base-size-8, 8px);
display: block;
width: var(--base-size-16, 16px);
height: var(--base-size-16, 16px);
color: var(--color-fg-muted);
pointer-events: none;
&:focus-visible,
&:focus {
outline: none;
box-shadow: none;
// octicon
.FormControl-input-leadingVisual {
display: block;
user-select: none;
}
}
&:focus-within {
@include focusBoxShadowInset(2px, var(--color-accent-fg));
}
// TODO: replace with new Button component
.FormControl-input-trailingAction {
position: absolute;
top: var(--base-size-4, 4px);
right: var(--base-size-4, 4px);
z-index: 4;
display: grid;
width: var(--primer-control-xsmall-size, 24px);
height: var(--primer-control-xsmall-size, 24px);
padding: 0;
color: var(--color-fg-muted);
cursor: pointer;
background: transparent;
border: 0;
border-radius: var(--primer-borderRadius-small);
transition: 0.2s cubic-bezier(0.3, 0, 0.5, 1);
transition-property: color, background-color, border-color;
align-items: center;
justify-content: center;
&.FormControl-fieldWrap--disabled {
color: var(--color-primer-fg-disabled);
background-color: var(--color-input-disabled-bg);
}
svg {
user-select: none;
}
&.FormControl-fieldWrap--invalid:not(:focus-within) {
box-shadow: var(--primer-borderInset-thin, inset 0 0 0 max(1px, 0.0625rem)) var(--color-danger-emphasis);
&[disabled] {
color: var(--color-primer-fg-disabled);
pointer-events: none;
}
&:hover {
background: var(--color-action-list-item-default-hover-bg);
}
&:active {
background: var(--color-action-list-item-default-active-bg);
}
// show vertical divider line between field and button
&.FormControl-input-trailingAction--divider {
&::before {
position: absolute;
top: calc((var(--primer-control-xsmall-size) - var(--base-size-16)) / 2);
left: calc(var(--base-size-4, 4px) * -1);
display: block;
width: var(--primer-borderWidth-thin);
height: var(--base-size-16);
content: '';
background: var(--color-border-default);
}
}
&::after {
@include minTouchTarget(var(--primer-control-medium-size, 32px), var(--primer-control-medium-size, 32px));
@media (pointer: coarse) {
min-width: var(--primer-control-minTarget-coarse, 44px);
min-height: var(--primer-control-minTarget-coarse, 44px);
}
}
}
// if leadingVisual is present
&.FormControl-fieldWrap--input-leadingVisual {
grid-template-columns: var(--base-size-16, 16px) minmax(0, auto);
.FormControl--input {
padding-inline-start: calc(var(--base-size-16, 16px) + var(--primer-control-medium-paddingInline-condensed, 8px));
}
// if leadingVisual and trailingAction are present
&.FormControl-fieldWrap--input-trailingAction {
grid-template-columns: var(--base-size-16, 16px) minmax(0, auto) min-content;
.FormControl--input-trailingAction {
grid-column: 3 / 4;
}
/*
32px
16px 16px
8px
*/
&.FormControl-input-wrap--leadingVisual {
.FormControl-input {
padding-inline-start:
calc(
var(--primer-control-medium-paddingInline-condensed, 8px) + var(--base-size-16, 16px) +
var(--primer-control-medium-gap, 8px)
); /* 32px */
}
}
/*
32px
24px 24px
*/
// if trailingAction is present
&.FormControl-fieldWrap--input-trailingAction {
grid-template-columns: minmax(0, auto) min-content;
&.FormControl-input-wrap--trailingAction {
.FormControl-input {
padding-inline-end:
calc(
var(--primer-control-medium-paddingInline-condensed, 8px) + var(--base-size-16, 16px) +
var(--primer-control-medium-gap, 8px)
); /* 32px */
}
/*
32px + 1px border
33px
24px 24px
*/
// if trailingAction divider is present, add 1px padding to accomodate input field text
// can be refactored to has(.FormControl-input-trailingAction--divider)
&.FormControl-input-wrap-trailingAction--divider {
.FormControl-input {
padding-inline-end:
calc(
var(--primer-control-medium-paddingInline-condensed, 8px) + var(--base-size-16, 16px) +
var(--primer-control-medium-gap, 8px) + var(--primer-borderWidth-thin, 1px)
); /* 33px */
}
}
}
&.FormControl-fieldWrap--input {
grid-template-rows: max-content;
.FormControl--input-leadingVisual {
grid-column: 1 / 2;
grid-row: 1;
// size modifications can be refactored with :has() - FormControl-input-wrap:has(.FormControl-large)
// sizes
&.FormControl-small {
.FormControl-input-leadingVisualWrap {
top: calc(var(--primer-control-medium-paddingInline-condensed, 8px) - 0.125rem); /* 6px */
left: calc(var(--primer-control-medium-paddingInline-condensed, 8px) - 0.125rem); /* 6px */
}
// <input> spans entire grid
.FormControl--input {
grid-column: 1 / -1;
grid-row: 1;
}
// TODO: replace with new Button component
// trailingAction will auto fill last grid slot
.FormControl--input-trailingAction {
display: grid;
width: calc(var(--primer-control-medium-size, 32px) - var(--base-size-4, 4px));
height: calc(var(--primer-control-medium-size, 32px) - var(--base-size-4, 4px));
padding: 0;
// optically align the icon with field padding
margin-right: calc(calc(var(--primer-control-medium-paddingInline-condensed, 8px) - 0.125rem) * -1);
color: var(--color-fg-muted);
cursor: pointer;
user-select: none;
background-color: transparent;
border: solid var(--primer-borderWidth-thin, 1px) transparent;
border-radius: var(--primer-borderRadius-medium, 6px);
transition: 0.2s cubic-bezier(0.3, 0, 0.5, 1);
transition-property: color, background-color, border-color;
grid-row: 1;
place-content: center;
place-self: center end;
grid-column: 2 / 3;
&:hover {
background-color: var(--color-btn-hover-bg);
border: solid var(--primer-borderWidth-thin, 1px) var(--color-btn-hover-bg);
/*
28px
20px 20px
*/
&.FormControl-input-wrap--trailingAction {
.FormControl-input.FormControl-small {
padding-inline-end:
calc(
var(--primer-control-small-paddingInline-condensed, 8px) + var(--base-size-16, 16px) +
var(--primer-control-small-gap, 8px)
); /* 28px */
}
&[disabled] {
color: var(--color-primer-fg-disabled);
pointer-events: none;
/*
28px + 1px border
29px
20px 20px
*/
&.FormControl-input-wrap-trailingAction--divider {
.FormControl-input.FormControl-small {
padding-inline-end:
calc(
var(--primer-control-small-paddingInline-condensed, 8px) + var(--base-size-16, 16px) +
var(--primer-control-small-gap, 8px) + var(--primer-borderWidth-thin, 1px)
); /* 29px */
}
}
}
// sizes
.FormControl-input-trailingAction {
width: calc(var(--primer-control-small-size, 28px) - var(--base-size-8, 8px));
height: calc(var(--primer-control-small-size, 28px) - var(--base-size-8, 8px));
// these selectors can be refactored to use :has()
&.FormControl--small {
&::before {
top: calc((var(--primer-control-xsmall-size) - var(--base-size-16)) / 4); /* 2px */
}
}
}
&.FormControl-large {
.FormControl-input-leadingVisualWrap {
top: var(--primer-control-medium-paddingInline-normal, 12px);
left: var(--primer-control-medium-paddingInline-normal, 12px);
}
/*
36px12px padding
16px 16px
12px8px
*/
&.FormControl-input-wrap--leadingVisual {
.FormControl-input.FormControl-large {
padding-inline-start:
calc(
var(--primer-control-large-paddingInline-normal, 12px) + var(--base-size-16, 16px) +
var(--primer-control-large-gap, 8px)
); /* 36px */
}
}
/*
36px
28px 28px
*/
&.FormControl-input-wrap--trailingAction {
.FormControl-input.FormControl-large {
padding-inline-end:
calc(
var(--primer-control-large-paddingInline-normal, 12px) + var(--base-size-16, 16px) +
var(--primer-control-large-gap, 8px)
); /* 36px */
}
/*
37px
28px 28px
*/
&.FormControl-input-wrap-trailingAction--divider {
.FormControl-input.FormControl-large {
padding-inline-end:
calc(
var(--primer-control-large-paddingInline-normal, 12px) + var(--base-size-16, 16px) +
var(--primer-control-large-gap, 8px) + var(--primer-borderWidth-thin, 1px)
); /* 37px */
}
}
}
.FormControl-input-trailingAction {
top: calc(var(--primer-control-medium-paddingInline-condensed, 8px) - 0.125rem); /* 6px */
right: calc(var(--primer-control-medium-paddingInline-condensed, 8px) - 0.125rem); /* 6px */
width: var(--primer-control-small-size, 28px);
height: var(--primer-control-small-size, 28px);
font-size: var(--primer-text-body-size-small, 12px);
.FormControl--input-trailingAction {
width: calc(var(--primer-control-small-size, 28px) - var(--base-size-8, 8px));
height: calc(var(--primer-control-small-size, 28px) - var(--base-size-8, 8px));
}
}
&.FormControl--medium {
height: var(--primer-control-medium-size, 32px);
.FormControl--input-trailingAction {
width: calc(var(--primer-control-medium-size, 32px) - var(--base-size-8, 8px));
height: calc(var(--primer-control-medium-size, 32px) - var(--base-size-8, 8px));
}
}
&.FormControl--large {
height: var(--primer-control-large-size, 40px);
padding-inline: var(--primer-control-large-paddingInline-normal, 12px);
.FormControl--input-trailingAction {
width: calc(var(--primer-control-large-size, 40px) - var(--base-size-8, 8px));
height: calc(var(--primer-control-large-size, 40px) - var(--base-size-8, 8px));
margin-right: calc(calc(var(--primer-control-medium-paddingInline-normal, 12px) - 0.125rem) * -1);
&::before {
top: unset;
height: var(--base-size-20);
}
}
}
}
.FormControl-fieldWrap--select {
.FormControl-select-wrap {
display: grid;
grid-template-columns: minmax(0, auto) var(--base-size-16, 16px);
padding-inline-end: var(--primer-control-medium-paddingInline-condensed, 8px);
// mask allows for background-color to respect themes
&::after {
width: var(--base-size-16, 16px);
height: var(--base-size-16, 16px);
padding-right: var(--base-size-4, 4px);
pointer-events: none;
content: '';
background-color: var(--color-fg-muted);
mask: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0iIzU4NjA2OSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNNC40MjcgOS40MjdsMy4zOTYgMy4zOTZhLjI1MS4yNTEgMCAwMC4zNTQgMGwzLjM5Ni0zLjM5NkEuMjUuMjUgMCAwMDExLjM5NiA5SDQuNjA0YS4yNS4yNSAwIDAwLS4xNzcuNDI3ek00LjQyMyA2LjQ3TDcuODIgMy4wNzJhLjI1LjI1IDAgMDEuMzU0IDBMMTEuNTcgNi40N2EuMjUuMjUgMCAwMS0uMTc3LjQyN0g0LjZhLjI1LjI1IDAgMDEtLjE3Ny0uNDI3eiIgLz48L3N2Zz4=');
mask-size: cover;
mask-size: contain;
mask-repeat: no-repeat;
grid-column: 2;
grid-row: 1;
place-self: center end;
}
// spans entire grid below mask
.FormControl {
.FormControl-select {
grid-column: 1/-1;
grid-row: 1;
appearance: none;
padding-right: var(--base-size-16, 16px);
}
}
.FormGroup--checkbox {
display: inline-grid;
grid-template-areas: 'field label' '. caption';
gap: var(--base-size-8, 8px);
.FormControl-label {
grid-area: label;
}
.FormControl-caption {
grid-area: caption;
padding-right: var(--base-size-20, 20px);
}
}
// checkbox + radio specific styles
.FormControl--checkbox,
.FormControl--radio {
position: absolute;
// Checkbox + Radio structure
// ===================
//
// .FormControl-radio-wrap
// .FormControl-radio
// .FormControl-radio-labelWrap
// .FormControl-label
// .FormControl-caption
.FormControl-checkbox-wrap,
.FormControl-radio-wrap {
display: inline-grid;
grid-template-columns: min-content auto;
gap: var(--base-size-8, 8px);
.FormControl-checkbox-labelWrap,
.FormControl-radio-labelWrap {
display: flex;
flex-direction: column;
gap: var(--base-size-4, 4px);
}
.FormControl-label {
cursor: pointer;
}
}
// these selectors are temporary to override base.scss
// once Field styles are widely adopted, we can adjust this and the global base styles
input[type='checkbox'].FormControl-checkbox,
input[type='radio'].FormControl-radio {
@include Field;
position: relative;
display: grid;
width: var(--base-size-16, 16px);
height: var(--base-size-16, 16px);
opacity: 0;
margin: 0;
margin-top: 0.125rem; // 2px to center align with label (20px line-height)
cursor: pointer;
border: solid var(--primer-borderWidth-thin, 1px) var(--color-border-default);
border-radius: var(--primer-borderRadius-small, 3px);
transition: background-color, border-color 80ms cubic-bezier(0.33, 1, 0.68, 1); // checked -> unchecked - add 120ms delay to fully see animation-out
appearance: none;
place-content: center;
&::before {
width: var(--base-size-16, 16px);
height: var(--base-size-16, 16px);
visibility: hidden;
content: '';
background-color: var(--color-fg-on-emphasis);
transition: visibility 0s linear 230ms;
clip-path: inset(var(--base-size-16, 16px) 0 0 0);
mask-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iOSIgdmlld0JveD0iMCAwIDEyIDkiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTEuNzgwMyAwLjIxOTYyNUMxMS45MjEgMC4zNjA0MjcgMTIgMC41NTEzMDUgMTIgMC43NTAzMTNDMTIgMC45NDkzMjEgMTEuOTIxIDEuMTQwMTkgMTEuNzgwMyAxLjI4MUw0LjUxODYgOC41NDA0MkM0LjM3Nzc1IDguNjgxIDQuMTg2ODIgOC43NiAzLjk4Nzc0IDguNzZDMy43ODg2NyA4Ljc2IDMuNTk3NzMgOC42ODEgMy40NTY4OSA4LjU0MDQyTDAuMjAxNjIyIDUuMjg2MkMwLjA2ODkyNzcgNS4xNDM4MyAtMC4wMDMzMDkwNSA0Ljk1NTU1IDAuMDAwMTE2NDkzIDQuNzYwOThDMC4wMDM1NTIwNSA0LjU2NjQzIDAuMDgyMzg5NCA0LjM4MDgxIDAuMjIwMDMyIDQuMjQzMjFDMC4zNTc2NjUgNC4xMDU2MiAwLjU0MzM1NSA0LjAyNjgxIDAuNzM3OTcgNC4wMjMzOEMwLjkzMjU4NCA0LjAxOTk0IDEuMTIwOTMgNC4wOTIxNyAxLjI2MzM0IDQuMjI0ODJMMy45ODc3NCA2Ljk0ODM1TDEwLjcxODYgMC4yMTk2MjVDMTAuODU5NSAwLjA3ODk5MjMgMTEuMDUwNCAwIDExLjI0OTUgMEMxMS40NDg1IDAgMTEuNjM5NSAwLjA3ODk5MjMgMTEuNzgwMyAwLjIxOTYyNVoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo='); // octicon checkmark image
mask-size: 75%;
mask-repeat: no-repeat;
mask-position: center;
@media screen and (prefers-reduced-motion: no-preference) {
animation: checkmarkOut 80ms cubic-bezier(0.65, 0, 0.35, 1) forwards; // slightly snappier animation out
}
}
// extend touch target
&::after {
@include minTouchTarget(var(--primer-control-medium-size, 32px), var(--primer-control-medium-size, 32px));
}
&[disabled] {
~ .FormControl-checkbox-labelWrap,
~ .FormControl-radio-labelWrap {
.FormControl-label {
color: var(--color-primer-fg-disabled);
cursor: not-allowed;
}
}
}
&:checked {
+ .FormControl--checkbox-svg {
.FormControl--checkbox-background {
fill: var(--color-accent-fg);
stroke: var(--color-accent-fg);
background: var(--color-accent-fg);
border-color: var(--color-accent-fg);
transition: background-color, border-color 80ms cubic-bezier(0.32, 0, 0.67, 0) 0ms; // unchecked -> checked
&::before {
visibility: visible;
transition: visibility 0s linear 0s;
@media screen and (prefers-reduced-motion: no-preference) {
animation: checkmarkIn 80ms cubic-bezier(0.65, 0, 0.35, 1) forwards 80ms;
}
}
.FormControl--checkbox-check {
fill: var(--color-fg-on-emphasis);
visibility: visible;
&:disabled {
cursor: not-allowed;
background-color: var(--color-primer-fg-disabled);
border-color: var(--color-primer-fg-disabled);
opacity: 1;
@media screen and (prefers-reduced-motion: no-preference) {
animation: checkmarkIn 200ms cubic-bezier(0.11, 0, 0.5, 0) forwards;
}
&::before {
background-color: var(--color-fg-on-emphasis);
}
}
// Windows High Contrast mode
// stylelint-disable primer/colors
@media (forced-colors: active) {
background-color: CanvasText;
border-color: CanvasText;
}
// stylelint-enable primer/colors
}
&:focus,
&:focus-visible {
outline-offset: 2px;
}
&:indeterminate {
&::before {
mask-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iMiIgdmlld0JveD0iMCAwIDEwIDIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMCAxQzAgMC40NDc3MTUgMC40NDc3MTUgMCAxIDBIOUM5LjU1MjI5IDAgMTAgMC40NDc3MTUgMTAgMUMxMCAxLjU1MjI4IDkuNTUyMjkgMiA5IDJIMUMwLjQ0NzcxNSAyIDAgMS41NTIyOCAwIDFaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K');
visibility: visible;
}
}
}
input[type='radio'].FormControl-radio {
border-radius: var(--primer-borderRadius-full, 100vh);
&::before {
clip-path: circle(0%);
mask-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iMTAiIHZpZXdCb3g9IjAgMCAxMCAxMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTUgOS4zNzVDNy40MTYyMiA5LjM3NSA5LjM3NSA3LjQxNjIyIDkuMzc1IDVDOS4zNzUgMi41ODM3NiA3LjQxNjIyIDAuNjI1IDUgMC42MjVDMi41ODM3NiAwLjYyNSAwLjYyNSAyLjU4Mzc2IDAuNjI1IDVDMC42MjUgNy40MTYyMiAyLjU4Mzc2IDkuMzc1IDUgOS4zNzVaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K'); // checked circle image
mask-size: 65%;
@media screen and (prefers-reduced-motion: no-preference) {
animation: radioOut 80ms cubic-bezier(0.65, 0, 0.35, 1) forwards; // slightly snappier animation out
}
}
&:checked {
&::before {
@media screen and (prefers-reduced-motion: no-preference) {
animation: radioIn 80ms cubic-bezier(0.65, 0, 0.35, 1) forwards 80ms;
}
}
}
&:indeterminate {
+ .FormControl--checkbox-svg {
.FormControl--checkbox-background {
fill: var(--color-accent-fg);
stroke: var(--color-accent-fg);
}
.FormControl--checkbox-indeterminate {
fill: var(--color-fg-on-emphasis);
visibility: visible;
}
}
}
@keyframes checkmarkIn {
from {
clip-path: inset(16px 0 0 0);
}
to {
clip-path: inset(0 0 0 0);
}
}
@keyframes checkmarkOut {
from {
clip-path: inset(0 0 0 0);
}
to {
clip-path: inset(16px 0 0 0);
&::before {
visibility: hidden;
}
}
}
.FormControl--checkbox-svg {
width: var(--base-size-16, 16px);
height: var(--base-size-16, 16px);
margin-top: 0.125rem;
cursor: pointer;
.FormControl--checkbox-background {
fill: var(--color-canvas-default);
stroke: var(--color-border-default);
@keyframes checkmarkIn {
from {
clip-path: inset(var(--base-size-16, 16px) 0 0 0);
}
.FormControl--checkbox-check {
visibility: hidden;
transition: visibility 0s linear 200ms;
clip-path: inset(16px 0 0 0);
@media screen and (prefers-reduced-motion: no-preference) {
animation: checkmarkOut 200ms cubic-bezier(0.11, 0, 0.5, 0) forwards;
}
}
.FormControl--checkbox-indeterminate {
fill: transparent;
visibility: hidden;
to {
clip-path: inset(0 0 0 0);
}
}
@keyframes checkmarkOut {
from {
clip-path: inset(0 0 0 0);
}
to {
clip-path: inset(var(--base-size-16, 16px) 0 0 0);
}
}
@keyframes radioIn {
from {
clip-path: circle(0%);
}
to {
clip-path: circle(100%);
}
}
@keyframes radioOut {
from {
clip-path: circle(100%);
}
to {
clip-path: circle(0%);
}
}

View File

@ -1076,10 +1076,10 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@primer/primitives@^7.8.3":
version "7.8.3"
resolved "https://registry.yarnpkg.com/@primer/primitives/-/primitives-7.8.3.tgz#de7e03492cf977e99f2417490d76421db9715e9f"
integrity sha512-04ZwfJhpZ0QFwDrJfCuLX6iOh0BflWDTvuoRA80lQH9xk0RtIg16INbruwwtnbSgnKKXXRSykRRJ5BbxnqufRA==
"@primer/primitives@^7.8.4":
version "7.8.4"
resolved "https://registry.yarnpkg.com/@primer/primitives/-/primitives-7.8.4.tgz#484486ee47050f18b2e82c33e9df247a5886c82a"
integrity sha512-cXmnhKBvrwbP3FYR9oxNYx3s8y2svsQLbDNZuoGcsZJLQ6RD3HfQ9ZtXgbyTbTYTyfPvkyd0pkQLI7tRJSc5kg==
"@primer/stylelint-config@^12.4.0":
version "12.6.1"