Skip to content

Commit

Permalink
Feat(web): Introduce UNSTABLE Toggle component #DS-1345
Browse files Browse the repository at this point in the history
  • Loading branch information
crishpeen committed Jul 16, 2024
1 parent bd48607 commit 8a409b8
Show file tree
Hide file tree
Showing 10 changed files with 540 additions and 1 deletion.
193 changes: 193 additions & 0 deletions packages/web/src/scss/components/UNSTABLE_Toggle/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
# UNSTABLE Toggle

> ⚠️ This component is UNSTABLE. It may significantly change at any point in the future.
> Please use it with caution.
Toggle is a form control that allows users to switch between two states.

## Basic Usage

The Toggle component implements the HTML [checkbox input][mdn-checkbox] element. It uses
the native input element and styles it to look like a toggle switch.

```html
<label for="toggle-default" class="UNSTABLE_Toggle">
<span class="UNSTABLE_Toggle__text">
<span class="UNSTABLE_Toggle__label">Toggle Label</span>
</span>
<input type="checkbox" id="toggle-default" class="UNSTABLE_Toggle__input" name="default" />
</label>
```

## Indicators

If you need to indicate the state of the toggle, you can add the `UNSTABLE_Toggle__input--indicators`
modifier class to the input. This will add a visual indicators to the toggle switch.

```html
<label for="toggle-indicators" class="UNSTABLE_Toggle">
<span class="UNSTABLE_Toggle__text">
<span class="UNSTABLE_Toggle__label">Toggle Label</span>
</span>
<input
type="checkbox"
id="toggle-indicators"
class="UNSTABLE_Toggle__input UNSTABLE_Toggle__input--indicators"
name="default"
/>
</label>
```

## Required

Add the `required` attribute to the input to mark it as required and add the
`UNSTABLE_Toggle__label--required` modifier class to the label to indicate the state.

```html
<label for="toggle-required" class="UNSTABLE_Toggle">
<span class="UNSTABLE_Toggle__text">
<span class="UNSTABLE_Toggle__label UNSTABLE_Toggle__label--required">Toggle Label</span>
</span>
<input type="checkbox" id="toggle-required" class="UNSTABLE_Toggle__input" name="required" required />
</label>
```

## Hidden Label

```html
<label for="toggle-hidden-label" class="UNSTABLE_Toggle">
<span class="UNSTABLE_Toggle__text">
<span class="UNSTABLE_Toggle__label UNSTABLE_Toggle__label--hidden">Toggle Label</span>
</span>
<input type="checkbox" id="toggle-hidden-label" class="UNSTABLE_Toggle__input" name="hidden-label" />
</label>
```

## Fluid

```html
<label for="toggle-fluid" class="UNSTABLE_Toggle UNSTABLE_Toggle--fluid">
<span class="UNSTABLE_Toggle__text">
<span class="UNSTABLE_Toggle__label">Toggle Label</span>
</span>
<input type="checkbox" id="toggle-fluid" class="UNSTABLE_Toggle__input" name="fluid" />
</label>
```

## Helper Text

```html
<label for="toggle-helper-text" class="UNSTABLE_Toggle">
<span class="UNSTABLE_Toggle__text">
<span class="UNSTABLE_Toggle__label">Toggle Label</span>
<span class="UNSTABLE_Toggle__helperText" id="toggle-helper-text-helper-text">Helper text</span>
</span>
<input
type="checkbox"
id="toggle-helper-text"
class="UNSTABLE_Toggle__input"
name="helper-text"
aria-describedby="toggle-helper-text-helper-text"
/>
</label>
```

## Validation States

Validation states can be presented either by adding a CSS modifier class
(`UNSTABLE_Toggle--success`, `UNSTABLE_Toggle--warning`, `UNSTABLE_Toggle--danger`), or by adding
a JS interaction class when controlled by JavaScript (`has-success`,
`has-warning`, `has-danger`). See Validation state [dictionary][dictionary-validation].

```html
<label for="toggle-success" class="UNSTABLE_Toggle UNSTABLE_Toggle--success">
<span class="UNSTABLE_Toggle__text">
<span class="UNSTABLE_Toggle__label">Toggle Label</span>
</span>
<input type="checkbox" id="toggle-success" class="UNSTABLE_Toggle__input" name="default" />
</label>

<label for="toggle-warning" class="UNSTABLE_Toggle UNSTABLE_Toggle--warning">
<span class="UNSTABLE_Toggle__text">
<span class="UNSTABLE_Toggle__label">Toggle Label</span>
<span class="UNSTABLE_Toggle__validationText" id="toggle-warning-validation-text">Validation text</span>
</span>
<input
type="checkbox"
id="toggle-warning"
class="UNSTABLE_Toggle__input"
name="default"
aria-describedby="toggle-warning-validation-text"
checked
/>
</label>

<div class="UNSTABLE_Toggle UNSTABLE_Toggle--danger">
<div class="UNSTABLE_Toggle__text">
<label for="toggle-danger" class="UNSTABLE_Toggle__label">Toggle Label</label>
<ul class="UNSTABLE_Toggle__validationText" id="toggle-danger-validation-text">
<li>First validation text</li>
<li>Second validation text</li>
</ul>
</div>
<input
type="checkbox"
id="toggle-danger"
class="UNSTABLE_Toggle__input"
name="default"
aria-describedby="toggle-danger-validation-text"
/>
</div>
```

### JavaScript-Controlled Validation Text

When implementing client-side form validation, use JS interaction state classes
(`has-success`, `has-warning`, `has-danger`) on the wrapping `<div>` element and
render validation texts in a `<div>` or `<ul>` with `data-spirit-element="validation_text"`
attribute. This way your JS remains disconnected from CSS that may or may not be
[prefixed][prefixed].

**Remember this approach is only valid for vanilla JS implementation. React
components mix CSS with JS by design and handle prefixes their own way.**

```html
<label for="toggle-success" class="UNSTABLE_Toggle has-success">
<span class="UNSTABLE_Toggle__text">
<span class="UNSTABLE_Toggle__label">Toggle Label</span>
<div
class="UNSTABLE_Toggle__validationText"
id="toggle-success-validation-text"
data-spirit-element="validation_text"
>
Validation text
</div>
</span>
<input
type="checkbox"
id="toggle-success"
class="UNSTABLE_Toggle__input"
name="default"
aria-describedby="toggle-success-validation-text"
/>
</label>
```

## Disabled State

On top of adding the `disabled` attribute to the input, disabled Toggle needs to
be marked by adding `UNSTABLE_Toggle--disabled` modifier class, or with `is-disabled`
JS interaction class when controlled by JavaScript:

```html
<label for="toggle-disabled" class="UNSTABLE_Toggle UNSTABLE_Toggle--disabled">
<span class="UNSTABLE_Toggle__text">
<span class="UNSTABLE_Toggle__label">Toggle Label</span>
</span>
<input type="checkbox" id="toggle-disabled" class="UNSTABLE_Toggle__input" name="default" disabled />
</label>
```

[dictionary-validation]: https://github.com/lmc-eu/spirit-design-system/blob/main/docs/DICTIONARIES.md#validation
[mdn-checkbox]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox
[prefixed]: https://github.com/lmc-eu/spirit-design-system/blob/main/packages/web/README.md#prefixing-css-class-names
128 changes: 128 additions & 0 deletions packages/web/src/scss/components/UNSTABLE_Toggle/_UNSTABLE_Toggle.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
@use 'theme';
@use '../../settings/cursors';
@use '../../theme/form-fields' as form-fields-theme;
@use '../../tools/form-fields' as form-fields-tools;
@use '../../tools/svg';

$_field-name: 'UNSTABLE_Toggle';

.UNSTABLE_Toggle {
display: flex;
align-items: start;
justify-content: space-between;
max-width: theme.$max-width;
margin-block: form-fields-theme.$gap;
cursor: cursors.$form-fields;
}

.UNSTABLE_Toggle--fluid {
max-width: none;
}

.UNSTABLE_Toggle__text {
margin-right: form-fields-theme.$gap;
}

.UNSTABLE_Toggle__label {
@include form-fields-tools.inline-field-label();
}

.UNSTABLE_Toggle__label--hidden {
@include form-fields-tools.label-hidden();
}

.UNSTABLE_Toggle__label--required::after {
@include form-fields-tools.label-required();
}

.UNSTABLE_Toggle__input {
@include form-fields-tools.inline-field-input();

flex-shrink: 0;
width: theme.$input-width;
height: theme.$input-height;
border: theme.$input-border-width solid form-fields-theme.$inline-field-input-color-unchecked;
border-radius: theme.$input-border-radius;
background-image: svg.escape(theme.$input-mark);
background-position-x: theme.$input-mark-position-x;
background-position-y: theme.$input-mark-position-y;
background-size: theme.$input-mark-width theme.$input-mark-height;
background-repeat: no-repeat;
background-color: form-fields-theme.$inline-field-input-color-unchecked;

@media (prefers-reduced-motion: no-preference) {
transition-property: border-color, background-color, background-position;
transition-duration: theme.$input-transition-duration;
transition-timing-function: theme.$input-transition-timing;
}

&:focus {
background-color: form-fields-theme.$inline-field-input-color-unchecked-active;
box-shadow: theme.$input-focus-shadow;
}

&:checked {
border-color: form-fields-theme.$inline-field-input-color-checked;
background-position-x: theme.$input-mark-position-x-checked;
background-color: form-fields-theme.$inline-field-input-color-checked;
}

@media (hover: hover) {
&:hover {
background-color: form-fields-theme.$inline-field-input-color-unchecked-hover;
}

&:checked:hover {
background-color: form-fields-theme.$inline-field-input-color-checked-hover;
}
}
}

.UNSTABLE_Toggle__input--indicators {
background-image: svg.escape(theme.$input-mark-indicators);
}

.UNSTABLE_Toggle__validationText,
.UNSTABLE_Toggle > .UNSTABLE_Toggle__text > [data-spirit-element='validation_text'] {
@include form-fields-tools.validation-text();
}

.UNSTABLE_Toggle__helperText {
@include form-fields-tools.helper-text();
}

@include form-fields-tools.input-field-validation-states($_field-name);

.UNSTABLE_Toggle--disabled {
@include form-fields-tools.inline-field-root-disabled();
}

.UNSTABLE_Toggle--disabled .UNSTABLE_Toggle__label {
@include form-fields-tools.label-disabled();
}

.UNSTABLE_Toggle--disabled > .UNSTABLE_Toggle__input,
.UNSTABLE_Toggle > .UNSTABLE_Toggle__input:disabled {
@include form-fields-tools.input-disabled();

border-color: theme.$input-border-color-disabled;
background-image: svg.escape(theme.$input-mark-disabled);
background-color: transparent;
}

.UNSTABLE_Toggle__input:checked:focus {
background-color: form-fields-theme.$inline-field-input-color-checked-active;
}

.UNSTABLE_Toggle--disabled > .UNSTABLE_Toggle__input:checked,
.UNSTABLE_Toggle > .UNSTABLE_Toggle__input:disabled:checked {
background-image: svg.escape(theme.$input-mark-disabled-checked);
}

.UNSTABLE_Toggle--disabled .UNSTABLE_Toggle__validationText {
@include form-fields-tools.validation-text-disabled();
}

.UNSTABLE_Toggle--disabled .UNSTABLE_Toggle__helperText {
@include form-fields-tools.helper-text-disabled();
}
21 changes: 21 additions & 0 deletions packages/web/src/scss/components/UNSTABLE_Toggle/_theme.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
@use '@tokens' as tokens;
@use '../../settings/transitions';

$max-width: 21rem;
$input-border-width: tokens.$border-width-100;
$input-border-radius: 12px;
$input-width: 44px;
$input-height: 24px;
$input-mark-height: 44px;
$input-mark-width: 52px;
$input-mark-position-y: -7px; // Position of the mark is offset because it has a shadow
$input-mark-position-x: -15px; // Position of the mark in non-checked state
$input-mark-position-x-checked: $input-mark-position-x + 20px; // Position of the mark in checked state
$input-mark: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="#{$input-mark-width}" height="#{$input-mark-height}" viewBox="0 0 52 44" fill="none"><g xmlns="http://www.w3.org/2000/svg" filter="url(#a)"><circle xmlns="http://www.w3.org/2000/svg" cx="26" cy="18" r="10" fill="#{tokens.$background-basic}"/></g><defs><filter id="a" width="44" height="44" x="4" y="0" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="4"/><feGaussianBlur stdDeviation="6"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"/><feBlend in2="BackgroundImageFix" result="effect1_dropShadow_1_81"/><feBlend in="SourceGraphic" in2="effect1_dropShadow_1_81" result="shape"/></filter></defs></svg>');
$input-mark-indicators: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="#{$input-mark-width}" height="#{$input-mark-height}" viewBox="0 0 52 44" fill="none"><path fill="#{tokens.$background-basic}" d="M48.675 14.33a.58.58 0 0 0-.822 0L45 17.178l-2.852-2.852a.58.58 0 1 0-.823.823L44.177 18l-2.852 2.852a.58.58 0 1 0 .822.823L45 18.823l2.852 2.852a.58.58 0 1 0 .823-.823L45.823 18l2.852-2.852a.585.585 0 0 0 0-.817ZM5.25 20.651 3.208 18.61a.574.574 0 0 0-.816 0 .574.574 0 0 0 0 .817l2.444 2.444a.58.58 0 0 0 .822 0l6.184-6.177a.574.574 0 0 0 0-.817.574.574 0 0 0-.817 0L5.25 20.651Z"/><g xmlns="http://www.w3.org/2000/svg" filter="url(#a)"><circle xmlns="http://www.w3.org/2000/svg" cx="26" cy="18" r="10" fill="#{tokens.$background-basic}"/></g><defs><filter id="a" width="44" height="44" x="4" y="0" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="4"/><feGaussianBlur stdDeviation="6"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"/><feBlend in2="BackgroundImageFix" result="effect1_dropShadow_1_81"/><feBlend in="SourceGraphic" in2="effect1_dropShadow_1_81" result="shape"/></filter></defs></svg>');
$input-mark-disabled: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="#{$input-mark-width}" height="#{$input-mark-height}" viewBox="0 0 52 44" fill="none"><circle xmlns="http://www.w3.org/2000/svg" cx="26" cy="18" r="10" fill="#{tokens.$action-unselected-disabled}"/></svg>');
$input-mark-disabled-checked: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="#{$input-mark-width}" height="#{$input-mark-height}" viewBox="0 0 52 44" fill="none"><circle xmlns="http://www.w3.org/2000/svg" cx="26" cy="18" r="10" fill="#{tokens.$action-selected-disabled}"/></svg>');
$input-transition-duration: transitions.$duration-100;
$input-transition-timing: transitions.$timing-eased-in-out;
$input-border-color-disabled: tokens.$border-primary-disabled;
$input-focus-shadow: tokens.$focus;
Loading

0 comments on commit 8a409b8

Please sign in to comment.