From a1a73b753f351f27674e8f35e4d1b95531ef3535 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Sch=C3=BCrch?= Date: Wed, 24 Apr 2024 13:29:06 +0200 Subject: [PATCH] chore(documentation): update CSS card-control styles and merge docs pages with the one from the web-component (#2847) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Loïc Fürhoff <12294151+imagoiq@users.noreply.github.com> --- .changeset/sixty-dancers-ring.md | 8 + .../src/app/routes/home/home.component.html | 10 +- .../post-card-control/post-card-control.scss | 152 +++++++++- ...cy.ts => card-control-standard-html.cy.ts} | 2 +- .../forms/card-control/card-control.docs.mdx | 178 +++++++---- .../card-control/card-control.styles.scss | 5 - .../card-control-firefox-fallback.sample.ts | 12 + .../card-control.snapshot.stories.ts} | 16 +- .../standard-html/card-control.stories.ts | 264 +++++++++++++++++ .../card-control-methods.sample.ts | 0 .../card-control.snapshot.stories.ts | 2 +- .../card-control.stories.ts | 70 ++++- .../web-component/card-control.styles.scss | 9 + .../forms/choice-card/checkbox-card.docs.mdx | 66 ----- .../choice-card/checkbox-card.stories.ts | 38 --- .../forms/choice-card/choice-card.ts | 249 ---------------- .../forms/choice-card/firefox-fallback.ts | 18 -- .../choice-card/radiobutton-card.docs.mdx | 66 ----- .../choice-card/radiobutton-card.stories.ts | 33 --- packages/documentation/tsconfig.json | 3 + packages/documentation/vite.config.js | 4 + packages/styles/src/components/_index.scss | 4 +- .../styles/src/components/card-control.scss | 280 ++++++++++++++++++ .../src/components/choice-control-card.scss | 148 --------- 24 files changed, 921 insertions(+), 716 deletions(-) create mode 100644 .changeset/sixty-dancers-ring.md rename packages/documentation/cypress/e2e/components/{choice-card.cy.ts => card-control-standard-html.cy.ts} (85%) delete mode 100644 packages/documentation/src/stories/components/forms/card-control/card-control.styles.scss create mode 100644 packages/documentation/src/stories/components/forms/card-control/standard-html/card-control-firefox-fallback.sample.ts rename packages/documentation/src/stories/components/forms/{choice-card/choice-card.snapshot.stories.ts => card-control/standard-html/card-control.snapshot.stories.ts} (65%) create mode 100644 packages/documentation/src/stories/components/forms/card-control/standard-html/card-control.stories.ts rename packages/documentation/src/stories/components/forms/card-control/{ => web-component}/card-control-methods.sample.ts (100%) rename packages/documentation/src/stories/components/forms/card-control/{ => web-component}/card-control.snapshot.stories.ts (97%) rename packages/documentation/src/stories/components/forms/card-control/{ => web-component}/card-control.stories.ts (83%) create mode 100644 packages/documentation/src/stories/components/forms/card-control/web-component/card-control.styles.scss delete mode 100644 packages/documentation/src/stories/components/forms/choice-card/checkbox-card.docs.mdx delete mode 100644 packages/documentation/src/stories/components/forms/choice-card/checkbox-card.stories.ts delete mode 100644 packages/documentation/src/stories/components/forms/choice-card/choice-card.ts delete mode 100644 packages/documentation/src/stories/components/forms/choice-card/firefox-fallback.ts delete mode 100644 packages/documentation/src/stories/components/forms/choice-card/radiobutton-card.docs.mdx delete mode 100644 packages/documentation/src/stories/components/forms/choice-card/radiobutton-card.stories.ts create mode 100644 packages/styles/src/components/card-control.scss delete mode 100644 packages/styles/src/components/choice-control-card.scss diff --git a/.changeset/sixty-dancers-ring.md b/.changeset/sixty-dancers-ring.md new file mode 100644 index 0000000000..e4f406de13 --- /dev/null +++ b/.changeset/sixty-dancers-ring.md @@ -0,0 +1,8 @@ +--- +'@swisspost/design-system-components-angular-workspace': patch +'@swisspost/design-system-documentation': patch +'@swisspost/design-system-components': patch +'@swisspost/design-system-styles': patch +--- + +Merged the card-control, checkbox-card and radio button card docs pages and updated the choice-card-control styles. diff --git a/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.html b/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.html index cd2d915695..58327c6fe3 100644 --- a/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.html +++ b/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.html @@ -40,11 +40,6 @@

Post Icon

-
-

Post Rating

- -
-

Post Popover

+
+

Post Rating

+ +
+

Post Tabs

diff --git a/packages/components/src/components/post-card-control/post-card-control.scss b/packages/components/src/components/post-card-control/post-card-control.scss index bbc8a3c734..8f260c85b6 100644 --- a/packages/components/src/components/post-card-control/post-card-control.scss +++ b/packages/components/src/components/post-card-control/post-card-control.scss @@ -30,9 +30,7 @@ border: post.$size-line solid var(--post-card-control-border-color); border-radius: post.$border-radius; color: var(--post-card-control-color); - cursor: pointer; - transition: background-color 100ms linear, border-color 100ms linear; .card-control--input { grid-area: input; @@ -41,12 +39,19 @@ border-color: var(--post-card-control-input-border-color) !important; color: var(--post-card-control-input-border-color) !important; cursor: inherit; - transition: border-color 100ms ease-in-out; + transition: none; &:focus, &:focus-visible { box-shadow: none; } + + @include post.high-contrast-mode() { + &::after { + forced-color-adjust: none; + border-color: transparent; + } + } } .card-control--label { @@ -88,6 +93,20 @@ &.is-checked { --post-card-control-border-color: #{post.$black}; --post-card-control-bg: #{post.$yellow}; + + @include post.high-contrast-mode() { + --post-card-control-border-color: FieldText; + --post-card-control-bg: SelectedItem; + --post-card-control-color: SelectedItemText; + --post-card-control-input-border-color: SelectedItemText; + --post-card-control-input-bg: SelectedItem; + + .card-control--input { + &::after { + background-color: var(--post-card-control-color) !important; + } + } + } } &.is-invalid { @@ -101,6 +120,20 @@ --post-card-control-bg: #{post.$gray-60}; --post-card-control-color: #{post.$white}; --post-card-control-input-border-color: #{post.$black}; + + @include post.high-contrast-mode() { + --post-card-control-border-color: Highlight; + --post-card-control-bg: Field; + --post-card-control-color: FieldText; + --post-card-control-input-border-color: Highlight; + --post-card-control-input-bg: Field; + + .card-control--input { + &::after { + color: FieldText; + } + } + } } } @@ -126,6 +159,35 @@ .card-control--input { border-style: dashed; } + + @include post.high-contrast-mode() { + --post-card-control-border-color: GrayText; + --post-card-control-color: GrayText; + --post-card-control-input-border-color: GrayText; + + &.is-checked { + --post-card-control-input-bg: field; + + position: relative; + + &::before { + display: block; + content: ''; + position: absolute; + inset: post.$size-line; + border-radius: post.$size-line * 0.5; + background-color: SelectedItem; + } + + > * { + position: relative; + } + + .card-control--input { + outline: post.$size-line solid Field; + } + } + } } :host-context(:is(#{post.$dark-backgrounds})) & { @@ -146,8 +208,12 @@ --post-card-control-input-border-color: #{post.$gray-80}; --post-card-control-input-bg: #{post.$white}; - &.is-invalid { - --post-card-control-bg: #{post.$yellow}; + @include post.high-contrast-mode() { + --post-card-control-border-color: FieldText; + --post-card-control-bg: SelectedItem; + --post-card-control-color: SelectedItemText; + --post-card-control-input-border-color: SelectedItemText; + --post-card-control-input-bg: SelectedItem; } } @@ -157,6 +223,18 @@ --post-card-control-color: #{post.$error}; --post-card-control-input-border-color: #{post.$error}; --post-card-control-input-bg: #{post.$white}; + + &.is-checked { + --post-card-control-bg: #{post.$yellow}; + + @include post.high-contrast-mode() { + --post-card-control-border-color: FieldText; + --post-card-control-bg: SelectedItem; + --post-card-control-color: SelectedItemText; + --post-card-control-input-border-color: SelectedItemText; + --post-card-control-input-bg: SelectedItem; + } + } } &:hover { @@ -165,6 +243,14 @@ --post-card-control-color: #{post.$black}; --post-card-control-input-border-color: #{post.$black}; --post-card-control-input-bg: #{post.$white}; + + @include post.high-contrast-mode() { + --post-card-control-border-color: Highlight; + --post-card-control-bg: Field; + --post-card-control-color: FieldText; + --post-card-control-input-border-color: Highlight; + --post-card-control-input-bg: Field; + } } } @@ -175,13 +261,22 @@ } } - // TODO: update white alpha colors with design-system alpha colors, once they are defined &.is-disabled { - --post-card-control-border-color: #{#{rgba(post.$white, 0.8)}}; + --post-card-control-border-color: post.$white-alpha-80; --post-card-control-bg: transparent; - --post-card-control-color: #{#{rgba(post.$white, 0.8)}}; - --post-card-control-input-border-color: #{#{rgba(post.$white, 0.8)}}; + --post-card-control-color: post.$white-alpha-80; + --post-card-control-input-border-color: post.$white-alpha-80; --post-card-control-input-bg: transparent; + + @include post.high-contrast-mode() { + --post-card-control-border-color: GrayText; + --post-card-control-color: GrayText; + --post-card-control-input-border-color: GrayText; + + &.is-checked { + --post-card-control-input-bg: Field; + } + } } } } @@ -213,8 +308,12 @@ --post-card-control-input-border-color: #{post.$gray-80}; --post-card-control-input-bg: #{post.$white}; - &.is-invalid { - --post-card-control-bg: #{post.$yellow}; + @include post.high-contrast-mode() { + --post-card-control-border-color: FieldText; + --post-card-control-bg: SelectedItem; + --post-card-control-color: SelectedItemText; + --post-card-control-input-border-color: SelectedItemText; + --post-card-control-input-bg: SelectedItem; } } @@ -224,6 +323,18 @@ --post-card-control-color: #{post.$error}; --post-card-control-input-border-color: #{post.$error}; --post-card-control-input-bg: #{post.$white}; + + &.is-checked { + --post-card-control-bg: #{post.$yellow}; + + @include post.high-contrast-mode() { + --post-card-control-border-color: FieldText; + --post-card-control-bg: SelectedItem; + --post-card-control-color: SelectedItemText; + --post-card-control-input-border-color: SelectedItemText; + --post-card-control-input-bg: SelectedItem; + } + } } &:hover { @@ -232,6 +343,14 @@ --post-card-control-color: #{post.$black}; --post-card-control-input-border-color: #{post.$black}; --post-card-control-input-bg: #{post.$white}; + + @include post.high-contrast-mode() { + --post-card-control-border-color: Highlight; + --post-card-control-bg: Field; + --post-card-control-color: FieldText; + --post-card-control-input-border-color: Highlight; + --post-card-control-input-bg: Field; + } } } @@ -242,13 +361,22 @@ } } - // TODO: update white alpha colors with design-system alpha colors, once they are defined &.is-disabled { --post-card-control-border-color: post.$white-alpha-80; --post-card-control-bg: transparent; --post-card-control-color: post.$white-alpha-80; --post-card-control-input-border-color: post.$white-alpha-80; --post-card-control-input-bg: transparent; + + @include post.high-contrast-mode() { + --post-card-control-border-color: GrayText; + --post-card-control-color: GrayText; + --post-card-control-input-border-color: GrayText; + + &.is-checked { + --post-card-control-input-bg: Field; + } + } } } } diff --git a/packages/documentation/cypress/e2e/components/choice-card.cy.ts b/packages/documentation/cypress/e2e/components/card-control-standard-html.cy.ts similarity index 85% rename from packages/documentation/cypress/e2e/components/choice-card.cy.ts rename to packages/documentation/cypress/e2e/components/card-control-standard-html.cy.ts index 08f79b7d50..038338820c 100644 --- a/packages/documentation/cypress/e2e/components/choice-card.cy.ts +++ b/packages/documentation/cypress/e2e/components/card-control-standard-html.cy.ts @@ -1,7 +1,7 @@ describe('Choice-card', () => { describe('Accessibility', () => { beforeEach(() => { - cy.visit('/iframe.html?id=snapshots--choice-card'); + cy.visit('/iframe.html?id=snapshots--card-control-standard-html'); cy.get('.radio-button-card', { timeout: 30000 }).should('be.visible'); cy.injectAxe(); }); diff --git a/packages/documentation/src/stories/components/forms/card-control/card-control.docs.mdx b/packages/documentation/src/stories/components/forms/card-control/card-control.docs.mdx index d06a421b31..9671ac1919 100644 --- a/packages/documentation/src/stories/components/forms/card-control/card-control.docs.mdx +++ b/packages/documentation/src/stories/components/forms/card-control/card-control.docs.mdx @@ -1,6 +1,17 @@ import { Canvas, Controls, Meta, Source } from '@storybook/blocks'; -import * as CardControlStories from './card-control.stories'; -import SampleCardControlMethods from './card-control-methods.sample?raw'; +import { + PostAlert, + PostTabs, + PostTabHeader, + PostTabPanel, +} from '@swisspost/design-system-components-react'; +import StylesPackageImport from '@/shared/styles-package-import.mdx'; + +import * as CardControlStandardHTMLStories from './standard-html/card-control.stories'; +import firefoxFallbackSnippet from './standard-html/card-control-firefox-fallback.sample.ts?raw'; + +import * as CardControlStories from './web-component/card-control.stories'; +import SampleCardControlMethods from './web-component/card-control-methods.sample?raw'; @@ -12,78 +23,143 @@ import SampleCardControlMethods from './card-control-methods.sample?raw';
-

For a more specialized visualization of checkbox and/or radio elements.

- - - - -## Installation +

For a more specialized visualization of checkbox and radio elements.

+ + +

There are various methods to integrate this component into your project.

+

We advise opting for the "Standard HTML" approach for when you need full access to the input field and using the "Web Component" method in every other case.

+
+ + + Standard HTML + + +
+ +
+ + ## Examples + + ### Dark backgrounds + +
+ +
+ + ### Grouping + + Checkbox cards can be grouped together. Use a `fieldset`/`legend` combination to label the group. If there is an error, link the legend with the error message through the `aria-describedby` attribute on the `legend`, pointing to the `id` of the error message. + + +
+ +
+ + ### Firefox fallback + +
+ Firefox currently does not support{' '} + + the new CSS :has pseudo-selector + + . As a fallback, the following states have to be mirrored on the top level element in the form of + classes (see below for a snippet): +
    +
  • + checked +
  • +
  • + disabled +
  • +
  • + is-invalid +
  • +
+ Check caniuse :has() to check if you still need the + fallback. +
+ + This snippet adds a global event listener to mirror the `focused` and `checked` states to the parent of the input. This fallback has to be applied as long as Firefox does not support the `:has` selector. + + + + +
+ + Webcomponent + + + + + ## Installation The `` element is part of the `@swisspost/design-system-components` package. For more information, read the getting started with components guide. -## Examples + ## Examples -### Dark backgrounds + ### Dark backgrounds - + -
- -
+
+ +
-### Custom icon + ### Custom icon -You can use our built-in icons by just adding the `icon` property with the name of the desired icon.
-If this is not enough, you can also use the named `icon` slot and add your very own custom icon. + You can use our built-in icons by just adding the `icon` property with the name of the desired icon.
+ If this is not enough, you can also use the named `icon` slot and add your very own custom icon. -
Make sure you remove all the `width` and `height` attributes from the `img` or `svg` tag. Otherwise we can not ensure, our styles will work properly.
+
Make sure you remove all the `width` and `height` attributes from the `img` or `svg` tag. Otherwise we can not ensure, our styles will work properly.
- + -### Custom content + ### Custom content -If you need to add other content to the component, you can use the default slot to do so. + If you need to add other content to the component, you can use the default slot to do so. -
-

Even if it is generally possible, we do not recommend using interactive elements in this slot because the background of the card control is clickable.

-

This can lead to confusion when the hit box of nested interactive controls is not clearly separated from the background, is invalid HTML and click events bubbling up to the card control will unexpectedly toggle it if they're not captured. More info: https://accessibilityinsights.io/info-examples/web/nested-interactive/

-
+
+

Even if it is generally possible, we do not recommend using interactive elements in this slot because the background of the card control is clickable.

+

This can lead to confusion when the hit box of nested interactive controls is not clearly separated from the background, is invalid HTML and click events bubbling up to the card control will unexpectedly toggle it if they're not captured. More info: https://accessibilityinsights.io/info-examples/web/nested-interactive/

+
- + -### Form Integration + ### Form Integration -You can use the component directly in your forms, the control value will be available in the `FormData` of the surrounding `
` element, just as you are used to from native input elements. + You can use the component directly in your forms, the control value will be available in the `FormData` of the surrounding `` element, just as you are used to from native input elements. -

Update the control and submit or reset the form to see how its FormData value changes.

- -
- -
+

Update the control and submit or reset the form to see how its FormData value changes.

+ +
+ +
-### Lined up + ### Lined up -Change the `width` of a `` component, by putting it (for example) in a grid. -If you like to stretch all `` components within a row to the same `height`, simply add the class `.h-100` to them. + Change the `width` of a `` component, by putting it (for example) in a grid. + If you like to stretch all `` components within a row to the same `height`, simply add the class `.h-100` to them. - -
- -
+ +
+ +
-### Radio button group + ### Radio button group -As you can create radio button groups with native `` elements, you can do the same with our `` component as well.
-Just add the same `name` attribute value to multiple `` components. + As you can create radio button groups with native `` elements, you can do the same with our `` component as well.
+ Just add the same `name` attribute value to multiple `` components. - + -### Custom Trigger -The `` offers a `reset` method to reset it to the initial state (`validity` and `checked` state). -The method can be called directly on the element itself. + ### Custom Trigger + The `` offers a `reset` method to reset it to the initial state (`validity` and `checked` state). + The method can be called directly on the element itself. - + + + \ No newline at end of file diff --git a/packages/documentation/src/stories/components/forms/card-control/card-control.styles.scss b/packages/documentation/src/stories/components/forms/card-control/card-control.styles.scss deleted file mode 100644 index dfff789c5a..0000000000 --- a/packages/documentation/src/stories/components/forms/card-control/card-control.styles.scss +++ /dev/null @@ -1,5 +0,0 @@ -#story--886fabcf-148b-4054-a2ec-4869668294fb--lined-up { - post-card-control { - visibility: visible; - } -} diff --git a/packages/documentation/src/stories/components/forms/card-control/standard-html/card-control-firefox-fallback.sample.ts b/packages/documentation/src/stories/components/forms/card-control/standard-html/card-control-firefox-fallback.sample.ts new file mode 100644 index 0000000000..5778b60ecf --- /dev/null +++ b/packages/documentation/src/stories/components/forms/card-control/standard-html/card-control-firefox-fallback.sample.ts @@ -0,0 +1,12 @@ +document.addEventListener('input', e => { + if (!(e.target instanceof Element) || e.target.nodeName !== 'input') return; + + const parent = e.target.parentElement; + + if ( + parent?.classList.contains('checkbox-button-card') || + parent?.classList.contains('radio-button-card') + ) { + parent.classList.toggle('checked', (e.target as HTMLInputElement).checked); + } +}); diff --git a/packages/documentation/src/stories/components/forms/choice-card/choice-card.snapshot.stories.ts b/packages/documentation/src/stories/components/forms/card-control/standard-html/card-control.snapshot.stories.ts similarity index 65% rename from packages/documentation/src/stories/components/forms/choice-card/choice-card.snapshot.stories.ts rename to packages/documentation/src/stories/components/forms/card-control/standard-html/card-control.snapshot.stories.ts index 111f02b2a5..8c3828cbdc 100644 --- a/packages/documentation/src/stories/components/forms/choice-card/choice-card.snapshot.stories.ts +++ b/packages/documentation/src/stories/components/forms/card-control/standard-html/card-control.snapshot.stories.ts @@ -1,9 +1,9 @@ -import { bombArgs } from '../../../../utils'; -import { choiceCardDefault, choiceCardMeta } from './choice-card'; import { StoryObj } from '@storybook/web-components'; import { html } from 'lit'; +import { bombArgs } from '../../../../../utils'; +import meta, { Default } from './card-control.stories'; -const { id, ...metaWithoutId } = choiceCardMeta; +const { id, ...metaWithoutId } = meta; export default { ...metaWithoutId, @@ -11,8 +11,8 @@ export default { }; const bombedArgs = bombArgs({ - type: choiceCardMeta.argTypes!.type!.options, - validation: choiceCardMeta.argTypes!.validation!.options, + type: meta.argTypes!.type!.options, + validation: meta.argTypes!.validation!.options, checked: [false, true], disabled: [false, true], label: ['Card check text', 'Really long running choice card text that wraps to two lines'], @@ -23,16 +23,14 @@ const bombedArgs = bombArgs({ // Filter out disabled and invalid combinations .filter(args => !(args.disabled && args.validation === 'is-invalid')); -export const ChoiceCard: StoryObj = { +export const CardControlStandardHTML: StoryObj = { render: () => { return html`
${['bg-white', 'bg-dark'].map( bg => html`
- ${bombedArgs.map( - args => html`
${choiceCardDefault(args)}
`, - )} + ${bombedArgs.map(args => html`
${Default.render(args)}
`)}
`, )} diff --git a/packages/documentation/src/stories/components/forms/card-control/standard-html/card-control.stories.ts b/packages/documentation/src/stories/components/forms/card-control/standard-html/card-control.stories.ts new file mode 100644 index 0000000000..8cd8a8d277 --- /dev/null +++ b/packages/documentation/src/stories/components/forms/card-control/standard-html/card-control.stories.ts @@ -0,0 +1,264 @@ +import type { Args, StoryContext, StoryFn } from '@storybook/web-components'; +import { useArgs, useState } from '@storybook/preview-api'; +import { nothing } from 'lit'; +import { html } from 'lit/static-html.js'; +import { classMap } from 'lit/directives/class-map.js'; +import { MetaComponent } from '../../../../../../types'; +import { parse } from '../../../../../utils/sass-export'; +import scss from '../card-control.module.scss'; + +const SCSS_VARIABLES: { [key: string]: string } = parse(scss); + +const meta: MetaComponent = { + id: '047501dd-a185-4835-be91-09130fa3dad9', + title: 'Components/Forms/Card-Control', + tags: ['package:HTML'], + parameters: { + badges: [], + design: { + type: 'figma', + url: 'https://www.figma.com/file/xZ0IW0MJO0vnFicmrHiKaY/Components-Post?type=design&node-id=22630-6854&mode=design&t=3lniLiZhl7q9Gqgn-4', + }, + }, + args: { + type: 'checkbox', + label: 'Label', + description: '', + icon: 'none', + checked: false, + disabled: false, + validation: 'null', + groupValidation: 'null', + }, + argTypes: { + type: { + name: 'Type', + control: { + type: 'radio', + labels: { + checkbox: 'Checkbox', + radio: 'Radio', + }, + }, + options: ['checkbox', 'radio'], + table: { + category: 'General', + }, + }, + label: { + name: 'Label', + type: 'string', + description: 'The main label of the input', + table: { + category: 'General', + }, + }, + description: { + name: 'Description', + type: 'string', + description: 'A short additional description', + table: { + category: 'General', + }, + }, + icon: { + name: 'Icon', + control: { + type: 'select', + }, + options: ['none', '1000', '1001', '2000'], + table: { + category: 'General', + }, + }, + checked: { + name: 'Checked', + type: 'boolean', + description: 'When set to `true`, places the component in the checked state.', + table: { + category: 'States', + }, + }, + disabled: { + name: 'Disabled', + description: + 'When set to `true`, disables the component\'s functionality and places it in a disabled state.There are accessibility concerns with the disabled state.
Please read our disabled state accessibility guide.
', + control: { + type: 'boolean', + }, + table: { + category: 'States', + }, + }, + validation: { + name: 'Validation', + description: "Controls the display of the component's validation state.", + control: { + type: 'radio', + labels: { + 'null': 'Default', + 'is-invalid': 'Invalid', + }, + }, + options: ['null', 'is-invalid'], + table: { + category: 'States', + }, + }, + groupValidation: { + name: 'Group Validation', + description: 'Set validation status for the whole group of choice cards', + control: { + type: 'radio', + labels: { + 'null': 'Default', + 'is-invalid': 'Invalid', + }, + }, + options: ['null', 'is-invalid'], + table: { + category: 'General', + }, + }, + }, +}; + +export default meta; + +let cardControlId = 0; +const CONTROL_LABELS = ['One', 'Two', 'Three', 'Four', 'Five', 'Six']; + +// Firefox fallback for the :has selector +function inputHandler(e: InputEvent, updateArgs: Function) { + const target = e.target as HTMLInputElement; + updateArgs({ checked: target.checked }); + + // Fix input events not fired on "deselected" radio buttons + target + .closest('fieldset') + ?.querySelectorAll('.radio-button-card') + .forEach(e => e?.classList.remove('checked')); + target.parentElement?.classList.toggle('checked', target.checked); +} + +export const Default = { + parameters: { + controls: { + exclude: ['Group Validation'], + }, + }, + render: (args: Args) => { + const [id] = useState(args.id ?? cardControlId++); + const [_, updateArgs] = useArgs(); + + // Conditional classes + const cardClasses = classMap({ + 'checked': args.checked, + 'disabled': args.disabled, + 'is-invalid': args.validation === 'is-invalid', + 'checkbox-button-card': args.type === 'checkbox', + 'radio-button-card': args.type === 'radio', + }); + const inputClasses = classMap({ + 'form-check-input': true, + 'is-invalid': args.validation === 'is-invalid', + }); + + // Child components + const controlId = `CardControl_${id}`; + const description = html`${args.description}`; + const icon = html` `; + + return html` +
+ + + ${args.icon !== 'none' ? icon : nothing} +
+ `; + }, +}; + +export const DarkBackground = { + parameters: { + docs: { + controls: { + include: ['Background-Color', 'Type', 'Checked', 'Disabled', 'Validation'], + }, + }, + }, + decorators: [ + (story: StoryFn, context: StoryContext) => + html`
+ ${story(context.args, context)} +
`, + ], + args: { + background: 'dark', + icon: '1001', + }, + argTypes: { + background: { + name: 'Background-Color', + description: 'The background color of a surrounding wrapper element.', + control: { + type: 'select', + }, + options: [...Object.keys(SCSS_VARIABLES.dark)], + }, + }, + render: Default.render, +}; + +function col(label: string, args: Args, useState: Function) { + const [id] = useState(cardControlId++); + + return html` +
+ ${Default.render({ + ...args, + id, + label, + inputName: args.type === 'radio' ? 'group' : `control-${id}`, + checked: false, + validation: args.groupValidation, + })} +
+ `; +} + +export const Group = { + parameters: { + controls: { + include: ['Type', 'Group Validation'], + }, + }, + render: (args: Args) => { + const error = html` +

Invalid choice

+ `; + + return html` +
+ Legend +
${CONTROL_LABELS.map(n => col(n, args, useState))}
+ ${args.groupValidation === 'is-invalid' ? error : null} +
+ `; + }, +}; diff --git a/packages/documentation/src/stories/components/forms/card-control/card-control-methods.sample.ts b/packages/documentation/src/stories/components/forms/card-control/web-component/card-control-methods.sample.ts similarity index 100% rename from packages/documentation/src/stories/components/forms/card-control/card-control-methods.sample.ts rename to packages/documentation/src/stories/components/forms/card-control/web-component/card-control-methods.sample.ts diff --git a/packages/documentation/src/stories/components/forms/card-control/card-control.snapshot.stories.ts b/packages/documentation/src/stories/components/forms/card-control/web-component/card-control.snapshot.stories.ts similarity index 97% rename from packages/documentation/src/stories/components/forms/card-control/card-control.snapshot.stories.ts rename to packages/documentation/src/stories/components/forms/card-control/web-component/card-control.snapshot.stories.ts index f12749d7f2..4344ea418f 100644 --- a/packages/documentation/src/stories/components/forms/card-control/card-control.snapshot.stories.ts +++ b/packages/documentation/src/stories/components/forms/card-control/web-component/card-control.snapshot.stories.ts @@ -1,7 +1,7 @@ import type { Args, StoryContext, StoryObj } from '@storybook/web-components'; import meta, { Default } from './card-control.stories'; import { html } from 'lit'; -import { bombArgs } from '../../../../utils'; +import { bombArgs } from '../../../../../utils'; const { id, ...metaWithoutId } = meta; diff --git a/packages/documentation/src/stories/components/forms/card-control/card-control.stories.ts b/packages/documentation/src/stories/components/forms/card-control/web-component/card-control.stories.ts similarity index 83% rename from packages/documentation/src/stories/components/forms/card-control/card-control.stories.ts rename to packages/documentation/src/stories/components/forms/card-control/web-component/card-control.stories.ts index df5baa0227..da2be22907 100644 --- a/packages/documentation/src/stories/components/forms/card-control/card-control.stories.ts +++ b/packages/documentation/src/stories/components/forms/card-control/web-component/card-control.stories.ts @@ -1,12 +1,12 @@ -import { useArgs } from '@storybook/preview-api'; import { Args, StoryContext, StoryObj } from '@storybook/web-components'; -import { MetaComponent } from '../../../../../types'; +import { useArgs } from '@storybook/preview-api'; +import { MetaComponent } from '../../../../../../types'; import { html, nothing } from 'lit'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; -import { parse } from '../../../../utils/sass-export'; +import { parse } from '../../../../../utils/sass-export'; import './card-control.styles.scss'; -import scss from './card-control.module.scss'; -import { coloredBackground } from '../../../../shared/decorators/dark-background'; +import scss from '../card-control.module.scss'; +import { coloredBackground } from '../../../../../shared/decorators/dark-background'; const SCSS_VARIABLES: any = parse(scss); @@ -53,11 +53,10 @@ const meta: MetaComponent = { type: 'radio', labels: { null: 'Default', - true: 'Valid', false: 'Invalid', }, }, - options: ['null', 'true', 'false'], + options: ['null', 'false'], table: { type: { summary: 'null | boolean', @@ -168,9 +167,11 @@ export const FormIntegration: Story = { args: { name: 'checkbox', checkboxFieldset: false, + validity: 'null', radioValue: '', radioDisabled: '', radioFieldset: false, + radioValidity: 'null', }, argTypes: { value: { @@ -195,6 +196,11 @@ export const FormIntegration: Story = { category: 'Checkbox', }, }, + validity: { + table: { + category: 'Checkbox', + }, + }, radioValue: { name: 'value', description: 'Set the value **prefix** of the `radio` cards.', @@ -225,6 +231,25 @@ export const FormIntegration: Story = { category: 'Radio', }, }, + radioValidity: { + name: 'validity', + description: + 'Defines the validation `validity` of the control. To reset validity to an undefiend state, simply remove the attribute from the control.', + control: { + type: 'radio', + labels: { + null: 'Default', + false: 'Invalid', + }, + }, + options: ['null', 'false'], + table: { + category: 'Radio', + type: { + summary: 'null | boolean', + }, + }, + }, }, decorators: [ story => html` @@ -237,13 +262,26 @@ export const FormIntegration: Story = { `, ], render: (args: Args, context: StoryContext) => { - return html` + const [_, updateArgs] = useArgs(); + + const invalidFeedback = html`

+ Invalid feedback +

`; + + return html`
Legend ${Default.render?.(args, context)}
- Legend + Legend ${[1, 2, 3].map( n => html``, )} + ${args.radioValidity === 'false' ? invalidFeedback : nothing}
@@ -263,17 +303,23 @@ export const FormIntegration: Story = { }, }; -function formHandler(e: any) { +function formHandler(e: any, updateArgs: Function) { if (e.type === 'submit') e.preventDefault(); setTimeout(() => { const formOutput = document.querySelector('#AssociatedFormOutput'); - const formData = Array.from(new FormData(e.target).entries()).reduce( + const formData: { [key: string]: string } = Array.from(new FormData(e.target).entries()).reduce( (acc, [k, v]) => Object.assign(acc, { [k]: v }), {}, ); - if (formOutput) formOutput.innerHTML = JSON.stringify(formData, null, 2); + if (formOutput) { + updateArgs({ + validity: e.type === 'reset' ? 'null' : (formData.checkbox !== undefined).toString(), + radioValidity: e.type === 'reset' ? 'null' : (formData.radio !== undefined).toString(), + }); + formOutput.innerHTML = JSON.stringify(formData, null, 2); + } }); } diff --git a/packages/documentation/src/stories/components/forms/card-control/web-component/card-control.styles.scss b/packages/documentation/src/stories/components/forms/card-control/web-component/card-control.styles.scss new file mode 100644 index 0000000000..e82562cf2b --- /dev/null +++ b/packages/documentation/src/stories/components/forms/card-control/web-component/card-control.styles.scss @@ -0,0 +1,9 @@ +// This is necessary, because we overrite the css classes by a custom control +// which removes the hydrated class from the componentn, +// which then causes the component to be visibly hidden + +#story--886fabcf-148b-4054-a2ec-4869668294fb--lined-up { + post-card-control { + visibility: visible; + } +} diff --git a/packages/documentation/src/stories/components/forms/choice-card/checkbox-card.docs.mdx b/packages/documentation/src/stories/components/forms/choice-card/checkbox-card.docs.mdx deleted file mode 100644 index 83c674bbe2..0000000000 --- a/packages/documentation/src/stories/components/forms/choice-card/checkbox-card.docs.mdx +++ /dev/null @@ -1,66 +0,0 @@ -import { Canvas, Controls, Meta, Source } from '@storybook/blocks'; -import * as CheckboxCardStories from './checkbox-card.stories'; -import StylesPackageImport from '../../../../shared/styles-package-import.mdx'; -import firefoxFallbackSnippet from './firefox-fallback.ts?raw'; - - - -
- # Checkbox card - - -
- -
- A larger, more consumer oriented, touch friendly variant of the checkbox used for e.g. selecting - services. -
- - -
- -
- -## Firefox fallback - -
- Firefox currently does not support{' '} - - the new CSS :has pseudo-selector - - . As a fallback, the following states have to be mirrored on the top level element in the form of - classes (see below for a snippet): -
    -
  • - checked -
  • -
  • - focused -
  • -
  • - disabled -
  • -
  • - is-invalid -
  • -
- Check caniuse :has() to check if you still need the - fallback. -
- -This snippet adds a global event listener to mirror the `focused` and `checked` states to the parent of the input. This fallback has to be applied as long as Firefox does not support the `:has` selector. - - - -## Grouping - -Checkbox cards can be grouped together. Use a `fieldset`/`legend` combination to label the group. If there is an error, link the legend with the error message through the `aria-describedby` attribute on the `legend`, pointing to the `id` of the error message. - - -
- -
- - diff --git a/packages/documentation/src/stories/components/forms/choice-card/checkbox-card.stories.ts b/packages/documentation/src/stories/components/forms/choice-card/checkbox-card.stories.ts deleted file mode 100644 index 6d54e3ce02..0000000000 --- a/packages/documentation/src/stories/components/forms/choice-card/checkbox-card.stories.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { choiceCardDefault, choiceCardGroup, choiceCardMeta } from './choice-card'; -import { MetaComponent } from '../../../../../types'; - -const meta: MetaComponent = { - ...choiceCardMeta, - id: '9637bbae-0533-4522-89d4-c2732431c69b', - title: 'Components/Forms/Checkbox Card', - tags: ['package:HTML'], - parameters: { - badges: [], - design: { - type: 'figma', - url: 'https://www.figma.com/file/xZ0IW0MJO0vnFicmrHiKaY/Components-Post?type=design&node-id=22630-6854&mode=design&t=3lniLiZhl7q9Gqgn-4', - }, - }, -}; - -export default meta; - -export const Default = { - render: choiceCardDefault, - args: { ...choiceCardMeta.args, type: 'checkbox' }, - parameters: { - controls: { - exclude: ['Group Validation'], - }, - }, -}; - -export const Group = { - render: choiceCardGroup, - args: { ...choiceCardMeta.args, type: 'checkbox' }, - parameters: { - controls: { - include: ['Group Validation'], - }, - }, -}; diff --git a/packages/documentation/src/stories/components/forms/choice-card/choice-card.ts b/packages/documentation/src/stories/components/forms/choice-card/choice-card.ts deleted file mode 100644 index 29942999c9..0000000000 --- a/packages/documentation/src/stories/components/forms/choice-card/choice-card.ts +++ /dev/null @@ -1,249 +0,0 @@ -import type { Args } from '@storybook/web-components'; -import { html } from 'lit/static-html.js'; -import { classMap } from 'lit/directives/class-map.js'; -import { BADGE } from '../../../../../.storybook/constants'; -import { nothing } from 'lit'; -import { useArgs } from '@storybook/preview-api'; -import { MetaExtended } from '../../../../../types'; - -export const choiceCardMeta: MetaExtended = { - parameters: { - badges: [BADGE.NEEDS_REVISION], - controls: { - exclude: ['Type'], - }, - }, - args: { - label: 'Card check text', - type: 'radio', - checked: false, - disabled: false, - focused: false, - validation: 'null', - showDescription: false, - description: 'A small description', - icon: 1000, - showIcon: false, - groupValidation: 'null', - }, - argTypes: { - label: { - name: 'Label', - type: 'string', - description: 'The main label of the input', - table: { - category: 'General', - }, - }, - type: { - name: 'Type', - control: { - type: 'radio', - labels: { - radio: 'Radio button', - checkbox: 'Checkbox', - }, - }, - options: ['radio', 'checkbox'], - table: { - // Hide it in the controls because there are two pages - disable: true, - }, - }, - checked: { - name: 'Checked', - type: 'boolean', - description: 'When set to `true`, places the component in the checked state.', - table: { - category: 'States', - }, - }, - disabled: { - name: 'Disabled', - description: - 'When set to `true`, disables the component\'s functionality and places it in a disabled state.There are accessibility concerns with the disabled state.
Please read our disabled state accessibility guide.
', - control: { - type: 'boolean', - }, - table: { - category: 'States', - }, - }, - focused: { - name: 'Focused', - description: 'Render the component in a focused state', - control: { type: 'boolean' }, - table: { - category: 'States', - }, - }, - validation: { - name: 'Validation', - description: "Controls the display of the component's validation state.", - control: { - type: 'radio', - labels: { - 'null': 'Default', - 'is-invalid': 'Invalid', - }, - }, - options: ['null', 'is-invalid'], - table: { - category: 'States', - }, - }, - showDescription: { - name: 'Show description', - type: 'boolean', - description: 'Toggles an additional description', - table: { - category: 'Description', - }, - }, - description: { - name: 'Description', - type: 'string', - description: 'A short additional description', - table: { - category: 'Description', - }, - }, - showIcon: { - name: 'Show icon', - type: 'boolean', - description: 'Show or hide icon', - table: { - category: 'Icon', - }, - }, - icon: { - name: 'Icon', - control: { - type: 'select', - }, - options: [1000, 1001, 2000], - table: { - category: 'Icon', - }, - }, - groupValidation: { - name: 'Group Validation', - description: 'Set validation status for the whole group of choice cards', - control: { - type: 'radio', - labels: { - 'null': 'Default', - 'is-invalid': 'Invalid', - }, - }, - options: ['null', 'is-invalid'], - }, - }, -}; - -let id_ct = 1; - -export const choiceCardDefault = (args: Args) => { - const [_, updateArgs] = useArgs(); - - // Conditional classes - const inputClasses = classMap({ - 'form-check-input': true, - 'is-invalid': args.validation === 'is-invalid', - }); - const cardClassMap = classMap({ - 'checked': args.checked, - 'disabled': args.disabled, - 'is-invalid': args.validation === 'is-invalid', - 'checkbox-button-card': args.type === 'checkbox', - 'radio-button-card': args.type === 'radio', - }); - - // Child components - const id = `control-${id_ct++}`; - const description = html` -
- ${args.description} - `; - const icon = html` `; - - // Firefox fallback for the :has selector - const _handleInput = (e: InputEvent) => { - const target = e.target as HTMLInputElement; - updateArgs({ checked: target.checked }); - - // Fix input events not fired on "deselected" radio buttons - target - .closest('fieldset') - ?.querySelectorAll('.radio-button-card') - .forEach(e => e?.classList.remove('checked')); - target.parentElement?.classList.toggle('checked', target.checked); - }; - - // Firefox fallback for the :has selector - const _handleFocus = (e: InputEvent) => { - updateArgs({ focused: true }); - (e.target as HTMLInputElement).parentElement?.classList.add('focused'); - }; - - // Firefox fallback for the :has selector - const _handleBlur = (e: InputEvent) => { - updateArgs({ focused: false }); - (e.target as HTMLInputElement).parentElement?.classList.remove('focused'); - }; - - return html` -
- - - ${args.showIcon ? icon : nothing} -
- `; -}; - -export const choiceCardGroup = (args: Args) => { - const loop = ['One', 'Two', 'Three', 'Four', 'Five', 'Six']; - - const col = (label: string) => html` -
- ${choiceCardDefault({ - ...args, - label, - checked: false, - focused: false, - validation: args.groupValidation, - inputName: 'group', - })} -
- `; - - const error = html` -

Invalid choice

- `; - - return html` -
- - Legend - -
${loop.map(n => col(n))}
- ${args.groupValidation === 'is-invalid' ? error : null} -
- `; -}; diff --git a/packages/documentation/src/stories/components/forms/choice-card/firefox-fallback.ts b/packages/documentation/src/stories/components/forms/choice-card/firefox-fallback.ts deleted file mode 100644 index 94640a0b61..0000000000 --- a/packages/documentation/src/stories/components/forms/choice-card/firefox-fallback.ts +++ /dev/null @@ -1,18 +0,0 @@ -['focusin', 'focusout', 'input'].forEach(t => - document.addEventListener(t, e => { - if (!(e.target instanceof Element) || e.target.nodeName !== 'input') return; - const parent = e.target.parentElement; - if (!parent?.classList.contains('radio-button-card')) return; - switch (e.type) { - case 'focusin': - parent.classList.add('focused'); - break; - case 'focusout': - parent.classList.remove('focused'); - break; - case 'input': - parent.classList.toggle('checked', (e.target as HTMLInputElement).checked); - break; - } - }), -); diff --git a/packages/documentation/src/stories/components/forms/choice-card/radiobutton-card.docs.mdx b/packages/documentation/src/stories/components/forms/choice-card/radiobutton-card.docs.mdx deleted file mode 100644 index 3c7690e702..0000000000 --- a/packages/documentation/src/stories/components/forms/choice-card/radiobutton-card.docs.mdx +++ /dev/null @@ -1,66 +0,0 @@ -import { Canvas, Controls, Meta, Source } from '@storybook/blocks'; -import * as RadiobuttonCardStories from './radiobutton-card.stories'; -import StylesPackageImport from '../../../../shared/styles-package-import.mdx'; -import firefoxFallbackSnippet from './firefox-fallback.ts?raw'; - - - -
- # Radio button card - - -
- -
- A larger, more consumer oriented, touch friendly variant of the radio-button used for e.g. - selecting services. -
- - -
- -
- -## Firefox fallback - -
- Firefox currently does not support{' '} - - the new CSS :has pseudo-selector - - . As a fallback, the following states have to be mirrored on the top level element in the form of - classes: -
    -
  • - checked -
  • -
  • - focused -
  • -
  • - disabled -
  • -
  • - is-invalid -
  • -
- Check caniuse :has() to check if you still need the - fallback. -
- -This snippet adds a global event listener to mirror the `focused` and `checked` states to the parent of the input. This fallback has to be applied as long as Firefox does not support the `:has` selector. - - - -## Grouping - -Radiobutton cards can be grouped together. Use a `fieldset`/`legend` combination to label the group. If there is an error, link the legend with the error message through the `aria-describedby` attribute on the `legend`, pointing to the `id` of the error message. - - -
- -
- - diff --git a/packages/documentation/src/stories/components/forms/choice-card/radiobutton-card.stories.ts b/packages/documentation/src/stories/components/forms/choice-card/radiobutton-card.stories.ts deleted file mode 100644 index b518d2c551..0000000000 --- a/packages/documentation/src/stories/components/forms/choice-card/radiobutton-card.stories.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { choiceCardDefault, choiceCardGroup, choiceCardMeta } from './choice-card'; -import { MetaComponent } from '../../../../../types'; - -const meta: MetaComponent = { - ...choiceCardMeta, - title: 'Components/Forms/Radio Button Card', - tags: ['package:HTML'], - parameters: { - badges: [], - design: { - type: 'figma', - url: 'https://www.figma.com/file/xZ0IW0MJO0vnFicmrHiKaY/Components-Post?type=design&node-id=24497-16195&mode=design&t=3lniLiZhl7q9Gqgn-4', - }, - }, -}; - -export default meta; - -export const Default = { - render: choiceCardDefault, - args: { ...choiceCardMeta.args, type: 'radio' }, - parameters: { - controls: { - exclude: ['Group Validation'], - }, - }, -}; - -export const Group = { - render: choiceCardGroup, - args: { ...choiceCardMeta.args, type: 'radio' }, - parameters: { controls: { include: ['Group Validation'] } }, -}; diff --git a/packages/documentation/tsconfig.json b/packages/documentation/tsconfig.json index e2d8655ef1..65eda9d0b2 100644 --- a/packages/documentation/tsconfig.json +++ b/packages/documentation/tsconfig.json @@ -1,5 +1,8 @@ { "compilerOptions": { + "paths": { + "@/*": ["./src/*"] + }, "target": "ESNext", "useDefineForClassFields": false, "lib": ["DOM", "DOM.Iterable", "ESNext"], diff --git a/packages/documentation/vite.config.js b/packages/documentation/vite.config.js index e5c4c09df2..774072d041 100644 --- a/packages/documentation/vite.config.js +++ b/packages/documentation/vite.config.js @@ -1,7 +1,11 @@ +import { fileURLToPath, URL } from 'url'; // https://vitejs.dev/config/ /** @type {import('vite').UserConfig} */ export default { + resolve: { + alias: [{ find: '@', replacement: fileURLToPath(new URL('./src', import.meta.url)) }], + }, optimizeDeps: { include: [ '@pxtrn/storybook-addon-docs-stencil', diff --git a/packages/styles/src/components/_index.scss b/packages/styles/src/components/_index.scss index 287e24d725..f003b7e37f 100644 --- a/packages/styles/src/components/_index.scss +++ b/packages/styles/src/components/_index.scss @@ -9,9 +9,9 @@ @use 'button'; @use 'button-group'; @use 'card'; -@use 'chip'; -@use 'choice-control-card'; +@use 'card-control'; @use 'carousel'; +@use 'chip'; @use 'close'; @use 'elevation'; @use 'error-container'; diff --git a/packages/styles/src/components/card-control.scss b/packages/styles/src/components/card-control.scss new file mode 100644 index 0000000000..20732b63a8 --- /dev/null +++ b/packages/styles/src/components/card-control.scss @@ -0,0 +1,280 @@ +@use '../variables/color'; +@use '../variables/commons'; +@use '../variables/spacing'; +@use '../variables/components/forms'; +@use '../mixins/utilities'; + +fieldset { + .radio-button-card, + .checkbox-button-card { + &:not(:last-child) { + margin-bottom: spacing.$size-regular; + } + } +} + +.radio-button-card, +.checkbox-button-card { + --post-card-control-border-color: #{color.$gray-60}; + --post-card-control-bg: #{color.$white}; + --post-card-control-color: #{color.$gray-80}; + --post-card-control-input-border-color: #{color.$gray-80}; + --post-card-control-input-bg: #{color.$white}; + + display: grid; + grid-template: 'input label icon' 'input content icon' / min-content auto min-content; + gap: 0 spacing.$size-mini; + position: relative; + padding: spacing.$size-regular; + width: 100%; + background-color: var(--post-card-control-bg); + border: spacing.$size-line solid var(--post-card-control-border-color); + border-radius: commons.$border-radius; + color: var(--post-card-control-color); + cursor: pointer; + + input.form-check-input { + grid-area: input; + position: relative; + z-index: 2; + margin: spacing.$size-micro 0; + background-color: var(--post-card-control-input-bg); + border-color: var(--post-card-control-input-border-color) !important; + color: var(--post-card-control-input-border-color) !important; + cursor: inherit; + transition: none; + + &:focus, + &:focus-visible { + box-shadow: none; + } + + ~ label.form-check-label { + color: inherit !important; + } + + @include utilities.high-contrast-mode() { + &::after { + forced-color-adjust: none; + border-color: transparent; + } + } + } + + label.form-check-label { + grid-area: label; + margin: spacing.$size-micro 0; + padding: 0; + border-radius: inherit; + transition-duration: 100ms; + + &::before { + display: block; + content: ''; + position: absolute; + inset: spacing.$size-line * -1; + border-radius: inherit; + } + + > * { + display: block; + // Lift content above the :before element to make it selectable + position: relative; + z-index: 2; + } + } + + post-icon { + grid-area: icon; + width: spacing.$size-big; + height: spacing.$size-big; + pointer-events: none; + } + + .card-control--content { + grid-area: content; + } + + &:where(:not(:has(input:disabled)), :not(.disabled)) { + &:where(:has(input:checked), .checked) { + --post-card-control-border-color: #{color.$black}; + --post-card-control-bg: #{color.$yellow}; + + @include utilities.high-contrast-mode() { + --post-card-control-border-color: FieldText; + --post-card-control-bg: SelectedItem; + --post-card-control-color: SelectedItemText; + --post-card-control-input-border-color: SelectedItemText; + --post-card-control-input-bg: SelectedItem; + + input.form-check-input { + &::after { + background-color: var(--post-card-control-color) !important; + } + } + } + } + + &:where(:has(input:invalid), :has(input[aria-invalid]), :has(input.is-invalid), .is-invalid) { + --post-card-control-border-color: #{color.$error}; + --post-card-control-color: #{color.$error}; + --post-card-control-input-border-color: #{color.$error}; + } + + &:where(:hover) { + --post-card-control-border-color: #{color.$gray-80}; + --post-card-control-bg: #{color.$gray-60}; + --post-card-control-color: #{color.$white}; + --post-card-control-input-border-color: #{color.$black}; + + @include utilities.high-contrast-mode() { + --post-card-control-border-color: Highlight; + --post-card-control-bg: Field; + --post-card-control-color: FieldText; + --post-card-control-input-border-color: Highlight; + --post-card-control-input-bg: Field; + + input.form-check-input { + &::after { + color: FieldText; + } + } + } + } + + input:focus-visible ~ label::before { + outline-offset: forms.$input-focus-outline-thickness; + outline: forms.$input-focus-outline-thickness solid commons.$outline-color; + } + } + + &:where(:has(input:disabled), .disabled) { + --post-card-control-border-color: #{color.$gray-60}; + --post-card-control-bg: transparent; + --post-card-control-color: #{color.$gray-60}; + --post-card-control-input-border-color: #{color.$gray-60}; + --post-card-control-input-bg: transparent; + + border-style: dashed; + text-decoration: line-through; + cursor: default; + + input.form-check-input { + border-style: dashed; + } + + @include utilities.high-contrast-mode() { + --post-card-control-border-color: GrayText; + --post-card-control-color: GrayText; + --post-card-control-input-border-color: GrayText; + + &:where(:has(input:checked), .checked) { + --post-card-control-input-bg: Field; + + input.form-check-input { + outline: spacing.$size-line solid Field; + } + + label.form-check-label::before { + inset: spacing.$size-line; + border-radius: spacing.$size-line; + background-color: SelectedItem; + } + } + } + } +} + +@each $bg in color.$dark-backgrounds { + #{$bg} { + .radio-button-card, + .checkbox-button-card { + --post-card-control-border-color: #{color.$white}; + --post-card-control-bg: transparent; + --post-card-control-color: #{color.$white}; + --post-card-control-input-border-color: #{color.$white}; + --post-card-control-input-bg: transparent; + + &:where(:not(:has(input:disabled)), :not(.disabled)) { + &:where(:has(input:checked), .checked) { + --post-card-control-border-color: #{color.$yellow}; + --post-card-control-bg: #{color.$yellow}; + --post-card-control-color: #{color.$gray-80}; + --post-card-control-input-border-color: #{color.$gray-80}; + --post-card-control-input-bg: #{color.$white}; + + @include utilities.high-contrast-mode() { + --post-card-control-border-color: FieldText; + --post-card-control-bg: SelectedItem; + --post-card-control-color: SelectedItemText; + --post-card-control-input-border-color: SelectedItemText; + --post-card-control-input-bg: SelectedItem; + } + } + + &:where( + :has(input:invalid), + :has(input[aria-invalid]), + :has(input.is-invalid), + .is-invalid + ) { + --post-card-control-border-color: #{color.$error}; + --post-card-control-bg: #{color.$error-background}; + --post-card-control-color: #{color.$error}; + --post-card-control-input-border-color: #{color.$error}; + --post-card-control-input-bg: #{color.$white}; + + &:where(:has(input:checked), .checked) { + --post-card-control-bg: #{color.$yellow}; + + @include utilities.high-contrast-mode() { + --post-card-control-border-color: FieldText; + --post-card-control-bg: SelectedItem; + --post-card-control-color: SelectedItemText; + --post-card-control-input-border-color: SelectedItemText; + --post-card-control-input-bg: SelectedItem; + } + } + } + + &:where(:hover) { + --post-card-control-border-color: #{color.$black}; + --post-card-control-bg: #{color.$gray-20}; + --post-card-control-color: #{color.$black}; + --post-card-control-input-border-color: #{color.$black}; + --post-card-control-input-bg: #{color.$white}; + + @include utilities.high-contrast-mode() { + --post-card-control-border-color: Highlight; + --post-card-control-bg: Field; + --post-card-control-color: FieldText; + --post-card-control-input-border-color: Highlight; + --post-card-control-input-bg: Field; + } + } + + input:focus-visible ~ label::before { + outline-color: color.$white; + } + } + + &:where(:has(input:disabled), .disabled) { + --post-card-control-border-color: post.$white-alpha-80; + --post-card-control-bg: transparent; + --post-card-control-color: post.$white-alpha-80; + --post-card-control-input-border-color: post.$white-alpha-80; + --post-card-control-input-bg: transparent; + + @include utilities.high-contrast-mode() { + --post-card-control-border-color: GrayText; + --post-card-control-color: GrayText; + --post-card-control-input-border-color: GrayText; + + &:where(:has(input:checked), .checked) { + --post-card-control-input-bg: Field; + } + } + } + } + } +} diff --git a/packages/styles/src/components/choice-control-card.scss b/packages/styles/src/components/choice-control-card.scss deleted file mode 100644 index 27f4dca3f3..0000000000 --- a/packages/styles/src/components/choice-control-card.scss +++ /dev/null @@ -1,148 +0,0 @@ -@use '../variables/color'; -@use '../variables/commons'; -@use '../variables/spacing'; -@use '../mixins/utilities'; - -.radio-button-card, -.checkbox-button-card { - --post-card-select--hover-bg: #{color.$gray-10}; - - position: relative; - display: flex; - gap: 0 spacing.$size-mini; - - width: 100%; - - color: color.$gray-80; - background-color: color.$white; - border: 2px solid color.$gray-60; - border-radius: commons.$border-radius; - padding: spacing.$size-regular; - transition: - color 100ms ease-in-out, - background-color 100ms ease-in-out, - border-color 100ms ease-in-out; - - // Checked - &:where(:has(input:checked), .checked) { - --post-card-select--hover-bg: #{color.$yellow}; - background-color: color.$yellow; - border-color: color.$gray-60; - - input { - background-color: color.$white !important; - } - } - - // Focus - &:where(:has(input:focus-visible), .focused) { - background-color: var(--post-card-select--hover-bg); - outline: 2px solid color.$black; - outline-offset: 2px; - border-color: color.$black; - - input { - border-color: color.$black; - } - } - - // Hover - &:where(:hover:not(:has(input:disabled)), :hover:not(.disabled)) { - border-color: color.$black; - color: color.$black; - background-color: var(--post-card-select--hover-bg); - - input { - border-color: color.$black; - } - - @include utilities.high-contrast-mode() { - border-color: Highlight; - transition: none; - } - } - - // Disabled - &:has(input:disabled), - &.disabled { - border-color: color.$gray-20; - color: color.$gray-40 !important; - - > * { - cursor: default; - } - } - - &:has(input:disabled:checked), - &.disabled.checked { - background-color: color.$gray-10; - } - - // Error - &:has(input[aria-invalid]), - &:has(input.is-invalid), - &.is-invalid { - color: color.$error; - border-color: color.$error; - - ~ .invalid-feedback { - display: block; - } - } - - &:last-child { - margin-bottom: 0; - } - - > * { - cursor: pointer; - } - - post-icon { - width: 2em; - height: 2em; - margin-left: spacing.$size-mini; - pointer-events: none; - } - - input { - border-color: color.$gray-80; - background-color: color.$white; - transition: border-color 100ms ease-in-out; - grid-column: 1 / 2; - - &:focus { - box-shadow: none !important; - } - } - - .form-check-label { - padding-left: 0; - - &::before { - // This spans up the click area of the label to the whole - // card in order to make it clickable natively. - content: ''; - position: absolute; - inset: -2px; - } - - span { - // Lift spans above the before element to make them selectable - position: relative; - } - } - - input, - .form-check-label { - margin-block: 0.25rem; - } - - // Align input and label if there is an icon - &:has(post-icon) { - input, - .form-check-label { - margin-top: 0.25rem; - } - } -}