From ac55cec7eea39876c735f1359056c4bc5871af20 Mon Sep 17 00:00:00 2001 From: "Schuerch Oliver, IT16.12" Date: Fri, 6 Oct 2023 09:35:29 +0200 Subject: [PATCH 01/82] feat(components): add post-card-control (unfinished) --- packages/components/src/components.d.ts | 41 +++++++++++++ .../post-card-control/post-card-control.scss | 59 +++++++++++++++++++ .../post-card-control/post-card-control.tsx | 58 ++++++++++++++++++ .../components/post-card-control/readme.md | 31 ++++++++++ .../src/components/post-icon/readme.md | 2 + 5 files changed, 191 insertions(+) create mode 100644 packages/components/src/components/post-card-control/post-card-control.scss create mode 100644 packages/components/src/components/post-card-control/post-card-control.tsx create mode 100644 packages/components/src/components/post-card-control/readme.md diff --git a/packages/components/src/components.d.ts b/packages/components/src/components.d.ts index dafbed4386..15e72529c8 100644 --- a/packages/components/src/components.d.ts +++ b/packages/components/src/components.d.ts @@ -38,6 +38,19 @@ export namespace Components { */ "type": AlertType; } + /** + * @class PostCardControl - representing a stencil component + */ + interface PostCardControl { + /** + * Defines the `id` of the input inside the card. + */ + "inputid": string; + /** + * Defines the `type` of the input inside the card. + */ + "type": string; + } interface PostCollapsible { /** * If `true`, the element is initially collapsed otherwise it is displayed. @@ -148,6 +161,15 @@ declare global { prototype: HTMLPostAlertElement; new (): HTMLPostAlertElement; }; + /** + * @class PostCardControl - representing a stencil component + */ + interface HTMLPostCardControlElement extends Components.PostCardControl, HTMLStencilElement { + } + var HTMLPostCardControlElement: { + prototype: HTMLPostCardControlElement; + new (): HTMLPostCardControlElement; + }; interface HTMLPostCollapsibleElement extends Components.PostCollapsible, HTMLStencilElement { } var HTMLPostCollapsibleElement: { @@ -189,6 +211,7 @@ declare global { }; interface HTMLElementTagNameMap { "post-alert": HTMLPostAlertElement; + "post-card-control": HTMLPostCardControlElement; "post-collapsible": HTMLPostCollapsibleElement; "post-icon": HTMLPostIconElement; "post-tab-header": HTMLPostTabHeaderElement; @@ -224,6 +247,19 @@ declare namespace LocalJSX { */ "type"?: AlertType; } + /** + * @class PostCardControl - representing a stencil component + */ + interface PostCardControl { + /** + * Defines the `id` of the input inside the card. + */ + "inputid"?: string; + /** + * Defines the `type` of the input inside the card. + */ + "type"?: string; + } interface PostCollapsible { /** * If `true`, the element is initially collapsed otherwise it is displayed. @@ -301,6 +337,7 @@ declare namespace LocalJSX { } interface IntrinsicElements { "post-alert": PostAlert; + "post-card-control": PostCardControl; "post-collapsible": PostCollapsible; "post-icon": PostIcon; "post-tab-header": PostTabHeader; @@ -314,6 +351,10 @@ declare module "@stencil/core" { export namespace JSX { interface IntrinsicElements { "post-alert": LocalJSX.PostAlert & JSXBase.HTMLAttributes; + /** + * @class PostCardControl - representing a stencil component + */ + "post-card-control": LocalJSX.PostCardControl & JSXBase.HTMLAttributes; "post-collapsible": LocalJSX.PostCollapsible & JSXBase.HTMLAttributes; /** * @class PostIcon - representing a stencil component 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 new file mode 100644 index 0000000000..47ec65b85f --- /dev/null +++ b/packages/components/src/components/post-card-control/post-card-control.scss @@ -0,0 +1,59 @@ +@use '@swisspost/design-system-styles/core' as post; +@use '@swisspost/design-system-styles/components/form-check'; + +:host { + --post-card-control-bg: #{post.$white}; + --post-card-control-border-color: #{post.$gray-60}; + --post-card-control-color: #{post.$gray-80}; + + display: block; + padding: post.$size-regular; + background-color: var(--post-card-control-bg); + 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: color 100ms ease-in-out, background-color 100ms ease-in-out, + border-color 100ms ease-in-out; +} + +.card-control--header { + display: flex; + gap: 0 post.$size-mini; +} + +.header--input { + margin: post.$size-micro 0; + background-color: post.$white; + border-color: var(--post-card-control-border-color); + transition: border-color 100ms ease-in-out; +} + +.header--label { + flex-grow: 2; + margin: post.$size-micro 0; +} + +.header--description { + font-size: 0.75rem; +} + +.header--icon { + width: post.$size-big; + height: post.$size-big; + pointer-events: none; +} + +.card-control--content { + margin-left: post.$form-check-input-size + post.$size-mini; +} + +:host(:hover:not(.checked, .disabled)) { + --post-card-control-bg: #{post.$gray-10}; + --post-card-control-border-color: #{post.$black}; + --post-card-control-color: #{post.$black}; +} + +:host(.checked) { + --post-card-control-bg: #{post.$yellow}; +} diff --git a/packages/components/src/components/post-card-control/post-card-control.tsx b/packages/components/src/components/post-card-control/post-card-control.tsx new file mode 100644 index 0000000000..1320cdc842 --- /dev/null +++ b/packages/components/src/components/post-card-control/post-card-control.tsx @@ -0,0 +1,58 @@ +import { Component, Element, h, Host, Prop } from '@stencil/core'; + +import { version } from '../../../package.json'; + +/** + * @class PostCardControl - representing a stencil component + */ +@Component({ + tag: 'post-card-control', + styleUrl: 'post-card-control.scss', + shadow: true, +}) +export class PostCardControl { + @Element() host: HTMLPostCardControlElement; + + /** + * Defines the `type` of the input inside the card. + */ + @Prop() readonly type: string = 'checkbox'; + + /** + * Defines the `id` of the input inside the card. + */ + @Prop() readonly inputid: string = ''; + + onChange(e: Event) { + const action = (e.target as HTMLInputElement).checked ? 'add' : 'remove'; + this.host.classList[action]('checked'); + } + + render() { + return ( + +
+ + + + + +
+ +
+ +
+
+ ); + } +} diff --git a/packages/components/src/components/post-card-control/readme.md b/packages/components/src/components/post-card-control/readme.md new file mode 100644 index 0000000000..295747e6b4 --- /dev/null +++ b/packages/components/src/components/post-card-control/readme.md @@ -0,0 +1,31 @@ +# post-card-control + + + + + + +## Properties + +| Property | Attribute | Description | Type | Default | +| --------- | --------- | ------------------------------------------------ | -------- | ------------ | +| `inputid` | `inputid` | Defines the `id` of the input inside the card. | `string` | `''` | +| `type` | `type` | Defines the `type` of the input inside the card. | `string` | `'checkbox'` | + + +## Dependencies + +### Depends on + +- [post-icon](../post-icon) + +### Graph +```mermaid +graph TD; + post-card-control --> post-icon + style post-card-control fill:#f9f,stroke:#333,stroke-width:4px +``` + +---------------------------------------------- + +*Built with [StencilJS](https://stenciljs.com/)* diff --git a/packages/components/src/components/post-icon/readme.md b/packages/components/src/components/post-icon/readme.md index 48f295e9c8..7a58de2cb6 100644 --- a/packages/components/src/components/post-icon/readme.md +++ b/packages/components/src/components/post-icon/readme.md @@ -23,11 +23,13 @@ some content ### Used by - [post-alert](../post-alert) + - [post-card-control](../post-card-control) ### Graph ```mermaid graph TD; post-alert --> post-icon + post-card-control --> post-icon style post-icon fill:#f9f,stroke:#333,stroke-width:4px ``` From f0477666b1dc90fd54e3c526d39fd5335cd5b881 Mon Sep 17 00:00:00 2001 From: "Schuerch Oliver, IT16.12" Date: Fri, 6 Oct 2023 09:35:53 +0200 Subject: [PATCH 02/82] feat(documentation): add card-control stories (unfinished) --- .../card-control/card-control.docs.mdx | 7 +++ .../card-control/card-control.stories.ts | 49 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 packages/documentation/src/stories/components/card-control/card-control.docs.mdx create mode 100644 packages/documentation/src/stories/components/card-control/card-control.stories.ts diff --git a/packages/documentation/src/stories/components/card-control/card-control.docs.mdx b/packages/documentation/src/stories/components/card-control/card-control.docs.mdx new file mode 100644 index 0000000000..e81f9a0a98 --- /dev/null +++ b/packages/documentation/src/stories/components/card-control/card-control.docs.mdx @@ -0,0 +1,7 @@ +import { Canvas, Controls, Meta } from '@storybook/blocks'; +import * as CardControlStories from './card-control.stories'; + + + + + diff --git a/packages/documentation/src/stories/components/card-control/card-control.stories.ts b/packages/documentation/src/stories/components/card-control/card-control.stories.ts new file mode 100644 index 0000000000..d9aa478e4a --- /dev/null +++ b/packages/documentation/src/stories/components/card-control/card-control.stories.ts @@ -0,0 +1,49 @@ +import { Args, Meta, StoryObj } from '@storybook/web-components'; +import { BADGE } from '../../../../.storybook/constants'; +import { html } from 'lit'; + +const meta: Meta = { + title: 'Components/Card-Control', + component: 'post-card-control', + parameters: { + badges: [BADGE.NEEDS_REVISION], + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + render: (args: Args) => html` + + Label + Description + +
+ +
+
+
+ + Label + Description + +
+ +
+
+ `, +}; From 1e049749a9227ebe1b043b52fb867b5791262843 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Fri, 10 Nov 2023 17:10:21 +0100 Subject: [PATCH 03/82] feat(components): add new component card-control (unfinished) --- packages/components/src/components.d.ts | 92 ++++- .../post-card-control/post-card-control.scss | 127 +++++-- .../post-card-control/post-card-control.tsx | 281 +++++++++++++-- .../components/post-card-control/readme.md | 18 +- .../card-control/card-control.stories.ts | 323 ++++++++++++++++-- 5 files changed, 747 insertions(+), 94 deletions(-) diff --git a/packages/components/src/components.d.ts b/packages/components/src/components.d.ts index 032b4e415a..252562be90 100644 --- a/packages/components/src/components.d.ts +++ b/packages/components/src/components.d.ts @@ -63,13 +63,53 @@ export namespace Components { */ interface PostCardControl { /** - * Defines the `id` of the input inside the card. + * Defines the `checked` attribute of the control. */ - "inputid": string; + "checked"?: boolean; /** - * Defines the `type` of the input inside the card. + * Defines the `id` attribute of the control. + */ + "controlId": string; + /** + * Defines the description in the control-label. + */ + "description"?: string; + /** + * Defines the `disabled` attribute of the control. + */ + "disabled"?: boolean; + /** + * Defines the `form` attribute of the control. + */ + "form"?: string; + /** + * Defines the icon `name` inside of the card. If not set the icon will not show up. + */ + "icon"?: string; + /** + * Defines the text in the control-label. + */ + "label": string; + /** + * Defines the `name` attribute of the control. + */ + "name"?: string; + /** + * Defines the `required` attribute of the control. + */ + "required"?: boolean; + /** + * Defines the validation `state` of the control. + */ + "state": boolean; + /** + * Defines the `type` attribute of the control. */ "type": string; + /** + * Defines the `value` attribute of the control. + */ + "value"?: string; } interface PostCollapsible { /** @@ -322,13 +362,53 @@ declare namespace LocalJSX { */ interface PostCardControl { /** - * Defines the `id` of the input inside the card. + * Defines the `checked` attribute of the control. */ - "inputid"?: string; + "checked"?: boolean; /** - * Defines the `type` of the input inside the card. + * Defines the `id` attribute of the control. + */ + "controlId": string; + /** + * Defines the description in the control-label. + */ + "description"?: string; + /** + * Defines the `disabled` attribute of the control. + */ + "disabled"?: boolean; + /** + * Defines the `form` attribute of the control. + */ + "form"?: string; + /** + * Defines the icon `name` inside of the card. If not set the icon will not show up. + */ + "icon"?: string; + /** + * Defines the text in the control-label. + */ + "label": string; + /** + * Defines the `name` attribute of the control. + */ + "name"?: string; + /** + * Defines the `required` attribute of the control. + */ + "required"?: boolean; + /** + * Defines the validation `state` of the control. + */ + "state"?: boolean; + /** + * Defines the `type` attribute of the control. */ "type"?: string; + /** + * Defines the `value` attribute of the control. + */ + "value"?: string; } interface PostCollapsible { /** 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 47ec65b85f..dc04c738ac 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 @@ -2,6 +2,10 @@ @use '@swisspost/design-system-styles/components/form-check'; :host { + display: block; +} + +.card-control { --post-card-control-bg: #{post.$white}; --post-card-control-border-color: #{post.$gray-60}; --post-card-control-color: #{post.$gray-80}; @@ -11,49 +15,114 @@ background-color: var(--post-card-control-bg); 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: color 100ms ease-in-out, background-color 100ms ease-in-out, border-color 100ms ease-in-out; + + &:not(.is-disabled) { + &:hover { + --post-card-control-bg: #{post.$gray-10}; + --post-card-control-border-color: #{post.$black}; + --post-card-control-color: #{post.$black}; + } + + &.is-checked { + --post-card-control-bg: #{post.$yellow}; + + .card-control--content { + display: block; + } + } + + &.is-invalid { + --post-card-control-border-color: #{post.$danger}; + --post-card-control-color: #{post.$danger}; + + &:hover { + --post-card-control-border-color: #{post.$danger}; + --post-card-control-color: #{post.$danger}; + } + } + } + + &.is-focused { + // needed for Firefox until the :has selector is implemented correctly + // downside: outline is drawn also when clicking with the mouse on the checkbox element directly + @supports not selector(:has(.header--input:focus-visible)) { + --post-card-control-border-color: #{post.$black}; + --post-card-control-color: #{post.$black}; + + outline-offset: post.$input-focus-outline-thickness; + outline: post.$input-focus-outline-thickness solid post.$focus; + } + + &:where(:has(.header--input:focus-visible)) { + --post-card-control-border-color: #{post.$black}; + --post-card-control-color: #{post.$black}; + + outline-offset: post.$input-focus-outline-thickness; + outline: post.$input-focus-outline-thickness solid post.$focus; + } + } + + &.is-disabled { + --post-card-control-border-color: #{post.$gray-40}; + --post-card-control-color: #{post.$gray-40}; + cursor: default; + + &.is-checked { + --post-card-control-bg: #{post.$gray-10}; + border-color: var(--post-card-control-bg); + } + } } .card-control--header { display: flex; gap: 0 post.$size-mini; -} + color: var(--post-card-control-color); -.header--input { - margin: post.$size-micro 0; - background-color: post.$white; - border-color: var(--post-card-control-border-color); - transition: border-color 100ms ease-in-out; -} + .header--input { + flex: 0 0 auto; + margin: post.$size-micro 0; + background-color: post.$white; + border-color: var(--post-card-control-border-color) !important; + color: inherit !important; + cursor: inherit; + transition: border-color 100ms ease-in-out; -.header--label { - flex-grow: 2; - margin: post.$size-micro 0; -} + &:focus, + &:focus-visible { + box-shadow: none; + } -.header--description { - font-size: 0.75rem; -} + ~ .header--label { + flex-grow: 2; + margin: post.$size-micro 0; + color: inherit !important; + pointer-events: none; + } + } -.header--icon { - width: post.$size-big; - height: post.$size-big; - pointer-events: none; -} + .header--description { + font-size: 0.75rem; + } -.card-control--content { - margin-left: post.$form-check-input-size + post.$size-mini; -} + .header--icon { + flex: 0 0 auto; + width: post.$size-big; + height: post.$size-big; + pointer-events: none; -:host(:hover:not(.checked, .disabled)) { - --post-card-control-bg: #{post.$gray-10}; - --post-card-control-border-color: #{post.$black}; - --post-card-control-color: #{post.$black}; + > slot > * { + width: 100%; + height: 100%; + } + } } -:host(.checked) { - --post-card-control-bg: #{post.$yellow}; +.card-control--content { + display: none; + margin-left: post.$form-check-input-size + post.$size-mini; } diff --git a/packages/components/src/components/post-card-control/post-card-control.tsx b/packages/components/src/components/post-card-control/post-card-control.tsx index 1320cdc842..7a01f4a9d0 100644 --- a/packages/components/src/components/post-card-control/post-card-control.tsx +++ b/packages/components/src/components/post-card-control/post-card-control.tsx @@ -1,5 +1,5 @@ -import { Component, Element, h, Host, Prop } from '@stencil/core'; - +import { AttachInternals, Component, Element, h, Host, Prop, State, Watch } from '@stencil/core'; +import { checkOneOf } from '../../utils'; import { version } from '../../../package.json'; /** @@ -9,50 +9,279 @@ import { version } from '../../../package.json'; tag: 'post-card-control', styleUrl: 'post-card-control.scss', shadow: true, + formAssociated: true, }) export class PostCardControl { + private readonly GROUPEVENT: string; + + private readonly KEYCODES = { + SPACE: 'Space', + LEFT: 'ArrowLeft', + UP: 'ArrowUp', + RIGHT: 'ArrowRight', + DOWN: 'ArrowDown', + }; + + private group = { + members: [], + first: null, + last: null, + checked: null, + focusable: null, + }; + + private control: HTMLInputElement; + @Element() host: HTMLPostCardControlElement; /** - * Defines the `type` of the input inside the card. + * Defines the text in the control-label. + */ + @Prop() readonly label!: string; + + /** + * Defines the description in the control-label. + */ + @Prop() readonly description?: string; + + /** + * Defines the `id` attribute of the control. + */ + @Prop() readonly controlId!: string; + + /** + * Defines the `type` attribute of the control. */ @Prop() readonly type: string = 'checkbox'; /** - * Defines the `id` of the input inside the card. + * Defines the `form` attribute of the control. + */ + @Prop() readonly form?: string; + + /** + * Defines the `name` attribute of the control. + */ + @Prop() readonly name?: string; + + /** + * Defines the `value` attribute of the control. + */ + @Prop() readonly value?: string; + + /** + * Defines the `checked` attribute of the control. + */ + @Prop({ reflect: true, mutable: true }) checked?: boolean = false; + + /** + * Defines the `required` attribute of the control. + */ + @Prop() readonly required?: boolean = false; + + /** + * Defines the `disabled` attribute of the control. + */ + @Prop() readonly disabled?: boolean = false; + + /** + * Defines the validation `state` of the control. */ - @Prop() readonly inputid: string = ''; + @Prop() readonly state: boolean = null; - onChange(e: Event) { - const action = (e.target as HTMLInputElement).checked ? 'add' : 'remove'; - this.host.classList[action]('checked'); + /** + * Defines the icon `name` inside of the card. + * If not set the icon will not show up. + */ + @Prop() readonly icon?: string; + + @State() focused = false; + + @AttachInternals() private internals: ElementInternals; + + constructor() { + this.GROUPEVENT = `PostCardControlGroup:${this.name}:change`; + + this.cardClickHandler = this.cardClickHandler.bind(this); + this.controlClickHandler = this.controlClickHandler.bind(this); + this.controlChangeHandler = this.controlChangeHandler.bind(this); + this.controlFocusHandler = this.controlFocusHandler.bind(this); + this.controlKeyDownHandler = this.controlKeyDownHandler.bind(this); + + this.contentClickHandler = this.contentClickHandler.bind(this); + + this.groupEventHandler = this.groupEventHandler.bind(this); + + window.addEventListener(this.GROUPEVENT, this.groupEventHandler); + } + + @Watch('type') + validateControlType(type = this.type) { + checkOneOf( + type, + ['checkbox', 'radio'], + 'The "post-card-control" element requires an "controlType"" of either "checkbox" (default) or "radio".', + ); + } + + private cardClickHandler(e: Event) { + if (e.target !== this.control) this.control.click(); + } + + private controlClickHandler(e: Event) { + if (this.disabled) e.preventDefault(); + } + + private controlChangeHandler() { + if (!this.disabled) { + this.checked = this.control.checked; + this.internals.setFormValue(this.control.value); + + if (this.group.members.length > 0 && this.control.checked) { + this.groupSetCheckedMember(this.control); + } + } + } + + private controlFocusHandler() { + this.focused = this.host === document.activeElement; + } + + private contentClickHandler(e: Event) { + e.stopPropagation(); + } + + // https://googlechromelabs.github.io/howto-components/howto-radio-group/ + private controlKeyDownHandler(e: KeyboardEvent) { + if (this.group.members.length > 0) { + switch (e.code) { + case this.KEYCODES.UP: + case this.KEYCODES.LEFT: + e.preventDefault(); + this.groupSetCheckedMember(this.groupGetPrevMember()); + break; + + case this.KEYCODES.DOWN: + case this.KEYCODES.RIGHT: + e.preventDefault(); + this.groupSetCheckedMember(this.groupGetNextMember()); + break; + + case this.KEYCODES.SPACE: + e.preventDefault(); + this.groupSetCheckedMember(this.control); + break; + + default: + break; + } + } + } + + private groupCollectMembers() { + if (this.type === 'radio' && this.name) { + const groupHosts = document.querySelectorAll( + `post-card-control[type="radio"][name="${this.name}"]`, + ); + + this.group.members = Array.from(groupHosts) + .map(m => m.shadowRoot.querySelector('input[type="radio"]:not([aria-disabled])')) + .filter(m => m !== null); + + if (this.group.members.length > 0) { + this.group.first = this.group.members[0]; + this.group.last = this.group.members[this.group.members.length - 1]; + this.group.checked = this.group.members.find(m => m.checked) ?? null; + this.group.focusable = this.group.checked ?? this.group.first; + + if (!this.disabled) this.control.tabIndex = this.control === this.group.focusable ? 0 : -1; + } + } + } + + private groupGetPrevMember() { + const focusableIndex = this.group.members.findIndex(m => m.id === this.group.focusable.id); + return this.group.members.find((_m, i) => i === focusableIndex - 1) ?? this.group.last; + } + + private groupGetNextMember() { + const focusableIndex = this.group.members.findIndex(m => m.id === this.group.focusable.id); + return this.group.members.find((_m, i) => i === focusableIndex + 1) ?? this.group.first; + } + + private groupSetCheckedMember(newCheckedMember: HTMLInputElement) { + window.dispatchEvent(new CustomEvent(this.GROUPEVENT, { detail: newCheckedMember })); + } + + private groupEventHandler(e: CustomEvent) { + if (!this.disabled) { + this.checked = this.control == e.detail; + if (this.checked) this.control.focus(); + setTimeout(this.groupCollectMembers, 250); + } + } + + componentWillLoad() { + this.validateControlType(); } render() { return ( -
- - - +
+
+ (this.control = el as HTMLInputElement)} + id={this.controlId} + class="header--input form-check-input" + type={this.type} + form={this.form} + name={this.name} + value={this.value} + checked={this.checked} + required={this.required} + aria-disabled={this.disabled} + onClick={this.controlClickHandler} + onChange={this.controlChangeHandler} + onFocus={this.controlFocusHandler} + onBlur={this.controlFocusHandler} + onKeyDown={this.controlKeyDownHandler} + /> - -
+ + +
+ {this.icon ? : null} +
+
-
- +
+ +
); } + + componentDidRender() { + this.groupCollectMembers(); + } + + disconnectedCallback() { + window.removeEventListener(this.GROUPEVENT, this.groupEventHandler); + } } diff --git a/packages/components/src/components/post-card-control/readme.md b/packages/components/src/components/post-card-control/readme.md index 295747e6b4..3a3ad67829 100644 --- a/packages/components/src/components/post-card-control/readme.md +++ b/packages/components/src/components/post-card-control/readme.md @@ -7,10 +7,20 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| --------- | --------- | ------------------------------------------------ | -------- | ------------ | -| `inputid` | `inputid` | Defines the `id` of the input inside the card. | `string` | `''` | -| `type` | `type` | Defines the `type` of the input inside the card. | `string` | `'checkbox'` | +| Property | Attribute | Description | Type | Default | +| ------------------------ | ------------- | --------------------------------------------------------------------------------- | --------- | ------------ | +| `checked` | `checked` | Defines the `checked` attribute of the control. | `boolean` | `false` | +| `controlId` _(required)_ | `control-id` | Defines the `id` attribute of the control. | `string` | `undefined` | +| `description` | `description` | Defines the description in the control-label. | `string` | `undefined` | +| `disabled` | `disabled` | Defines the `disabled` attribute of the control. | `boolean` | `false` | +| `form` | `form` | Defines the `form` attribute of the control. | `string` | `undefined` | +| `icon` | `icon` | Defines the icon `name` inside of the card. If not set the icon will not show up. | `string` | `undefined` | +| `label` _(required)_ | `label` | Defines the text in the control-label. | `string` | `undefined` | +| `name` | `name` | Defines the `name` attribute of the control. | `string` | `undefined` | +| `required` | `required` | Defines the `required` attribute of the control. | `boolean` | `false` | +| `state` | `state` | Defines the validation `state` of the control. | `boolean` | `null` | +| `type` | `type` | Defines the `type` attribute of the control. | `string` | `'checkbox'` | +| `value` | `value` | Defines the `value` attribute of the control. | `string` | `undefined` | ## Dependencies diff --git a/packages/documentation/src/stories/components/card-control/card-control.stories.ts b/packages/documentation/src/stories/components/card-control/card-control.stories.ts index d9aa478e4a..38292b663e 100644 --- a/packages/documentation/src/stories/components/card-control/card-control.stories.ts +++ b/packages/documentation/src/stories/components/card-control/card-control.stories.ts @@ -16,34 +16,299 @@ type Story = StoryObj; export const Default: Story = { render: (args: Args) => html` - - Label - Description - -
- -
-
-
- - Label - Description - -
- -
-
+ + + + + +
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ + + +
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ + + +
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ + + +
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
`, }; From 1d819ebe689aef5a30af41137529923b9db2c53a Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Fri, 10 Nov 2023 17:10:40 +0100 Subject: [PATCH 04/82] feat(styles): add new custom color variable $focus --- packages/styles/src/variables/_color.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/styles/src/variables/_color.scss b/packages/styles/src/variables/_color.scss index a12aff0679..6ef4b52a15 100644 --- a/packages/styles/src/variables/_color.scss +++ b/packages/styles/src/variables/_color.scss @@ -9,6 +9,9 @@ // The one and only $yellow: #fc0; +// Custom colors +$focus: #1976c8; + // Grayscale $gray-background: #f4f3f1; $gray-background-light: #faf9f8; From e5a9343bc3d80ed233648277c8a354e9ce7a6e01 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Fri, 10 Nov 2023 17:16:29 +0100 Subject: [PATCH 05/82] chore: add todo --- .../src/components/post-card-control/post-card-control.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/components/src/components/post-card-control/post-card-control.tsx b/packages/components/src/components/post-card-control/post-card-control.tsx index 7a01f4a9d0..520b8c8e19 100644 --- a/packages/components/src/components/post-card-control/post-card-control.tsx +++ b/packages/components/src/components/post-card-control/post-card-control.tsx @@ -191,6 +191,7 @@ export class PostCardControl { if (this.group.members.length > 0) { this.group.first = this.group.members[0]; this.group.last = this.group.members[this.group.members.length - 1]; + // TODO: fix checked, which is sometimes not set correctly after groupEventHandler, when changing the checked radio group element with keyboard this.group.checked = this.group.members.find(m => m.checked) ?? null; this.group.focusable = this.group.checked ?? this.group.first; @@ -217,7 +218,7 @@ export class PostCardControl { if (!this.disabled) { this.checked = this.control == e.detail; if (this.checked) this.control.focus(); - setTimeout(this.groupCollectMembers, 250); + this.groupCollectMembers(); } } From c53e70188cf73ebb45670493e47bf74d1c193be1 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Tue, 19 Dec 2023 13:00:16 +0100 Subject: [PATCH 06/82] fix(components): fix post-card-control group behaviour of --- .../post-card-control/post-card-control.tsx | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/components/src/components/post-card-control/post-card-control.tsx b/packages/components/src/components/post-card-control/post-card-control.tsx index 520b8c8e19..423784acef 100644 --- a/packages/components/src/components/post-card-control/post-card-control.tsx +++ b/packages/components/src/components/post-card-control/post-card-control.tsx @@ -137,7 +137,7 @@ export class PostCardControl { this.checked = this.control.checked; this.internals.setFormValue(this.control.value); - if (this.group.members.length > 0 && this.control.checked) { + if (this.group.members.length > 1 && this.control.checked) { this.groupSetCheckedMember(this.control); } } @@ -158,18 +158,18 @@ export class PostCardControl { case this.KEYCODES.UP: case this.KEYCODES.LEFT: e.preventDefault(); - this.groupSetCheckedMember(this.groupGetPrevMember()); + this.groupSetCheckedMember(this.groupGetPrevMember(), true); break; case this.KEYCODES.DOWN: case this.KEYCODES.RIGHT: e.preventDefault(); - this.groupSetCheckedMember(this.groupGetNextMember()); + this.groupSetCheckedMember(this.groupGetNextMember(), true); break; case this.KEYCODES.SPACE: e.preventDefault(); - this.groupSetCheckedMember(this.control); + this.groupSetCheckedMember(this.control, true); break; default: @@ -191,7 +191,6 @@ export class PostCardControl { if (this.group.members.length > 0) { this.group.first = this.group.members[0]; this.group.last = this.group.members[this.group.members.length - 1]; - // TODO: fix checked, which is sometimes not set correctly after groupEventHandler, when changing the checked radio group element with keyboard this.group.checked = this.group.members.find(m => m.checked) ?? null; this.group.focusable = this.group.checked ?? this.group.first; @@ -210,14 +209,19 @@ export class PostCardControl { return this.group.members.find((_m, i) => i === focusableIndex + 1) ?? this.group.first; } - private groupSetCheckedMember(newCheckedMember: HTMLInputElement) { - window.dispatchEvent(new CustomEvent(this.GROUPEVENT, { detail: newCheckedMember })); + private groupSetCheckedMember(newCheckedMember: HTMLInputElement, triggeredByKeyboard?: boolean) { + window.dispatchEvent( + new CustomEvent(this.GROUPEVENT, { + detail: { control: newCheckedMember, triggeredByKeyboard }, + }), + ); } private groupEventHandler(e: CustomEvent) { if (!this.disabled) { - this.checked = this.control == e.detail; - if (this.checked) this.control.focus(); + this.control.checked = this.checked = this.control == e.detail.control; + + if (this.checked && e.detail.triggeredByKeyboard) this.control.focus(); this.groupCollectMembers(); } } From 4b29ccbaf715628d1e197c662d1e62dbc4fe8238 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Wed, 20 Dec 2023 11:49:17 +0100 Subject: [PATCH 07/82] chore(components): update props and add controlChange event --- packages/components/src/components.d.ts | 47 +++++++++------ .../post-card-control/post-card-control.tsx | 58 ++++++++++++------- .../components/post-card-control/readme.md | 34 ++++++----- 3 files changed, 86 insertions(+), 53 deletions(-) diff --git a/packages/components/src/components.d.ts b/packages/components/src/components.d.ts index 72dacb86ed..9e3e54d164 100644 --- a/packages/components/src/components.d.ts +++ b/packages/components/src/components.d.ts @@ -65,25 +65,25 @@ export namespace Components { */ "checked"?: boolean; /** - * Defines the `id` attribute of the control. + * Defines the `id` attribute of the control. Make sure, the `id` is unique in the entire document. */ "controlId": string; /** * Defines the description in the control-label. */ - "description"?: string; + "description": string; /** * Defines the `disabled` attribute of the control. */ - "disabled"?: boolean; + "disabled": boolean; /** * Defines the `form` attribute of the control. */ - "form"?: string; + "form": string; /** - * Defines the icon `name` inside of the card. If not set the icon will not show up. + * Defines the icon `name` inside of the card. If not set the icon will not show up. */ - "icon"?: string; + "icon": string; /** * Defines the text in the control-label. */ @@ -91,11 +91,7 @@ export namespace Components { /** * Defines the `name` attribute of the control. */ - "name"?: string; - /** - * Defines the `required` attribute of the control. - */ - "required"?: boolean; + "name": string; /** * Defines the validation `state` of the control. */ @@ -103,11 +99,11 @@ export namespace Components { /** * Defines the `type` attribute of the control. */ - "type": string; + "type": 'checkbox' | 'radio'; /** * Defines the `value` attribute of the control. */ - "value"?: string; + "value": string; } interface PostCollapsible { /** @@ -258,6 +254,10 @@ export interface PostAlertCustomEvent extends CustomEvent { detail: T; target: HTMLPostAlertElement; } +export interface PostCardControlCustomEvent extends CustomEvent { + detail: T; + target: HTMLPostCardControlElement; +} export interface PostCollapsibleCustomEvent extends CustomEvent { detail: T; target: HTMLPostCollapsibleElement; @@ -294,10 +294,21 @@ declare global { prototype: HTMLPostAlertElement; new (): HTMLPostAlertElement; }; + interface HTMLPostCardControlElementEventMap { + "controlChange": boolean; + } /** * @class PostCardControl - representing a stencil component */ interface HTMLPostCardControlElement extends Components.PostCardControl, HTMLStencilElement { + addEventListener(type: K, listener: (this: HTMLPostCardControlElement, ev: PostCardControlCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; + removeEventListener(type: K, listener: (this: HTMLPostCardControlElement, ev: PostCardControlCustomEvent) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; } var HTMLPostCardControlElement: { prototype: HTMLPostCardControlElement; @@ -443,7 +454,7 @@ declare namespace LocalJSX { */ "checked"?: boolean; /** - * Defines the `id` attribute of the control. + * Defines the `id` attribute of the control. Make sure, the `id` is unique in the entire document. */ "controlId": string; /** @@ -459,7 +470,7 @@ declare namespace LocalJSX { */ "form"?: string; /** - * Defines the icon `name` inside of the card. If not set the icon will not show up. + * Defines the icon `name` inside of the card. If not set the icon will not show up. */ "icon"?: string; /** @@ -471,9 +482,9 @@ declare namespace LocalJSX { */ "name"?: string; /** - * Defines the `required` attribute of the control. + * An event emitted whenever the control value changes. The payload contains the current checked state under `event.details`. */ - "required"?: boolean; + "onControlChange"?: (event: PostCardControlCustomEvent) => void; /** * Defines the validation `state` of the control. */ @@ -481,7 +492,7 @@ declare namespace LocalJSX { /** * Defines the `type` attribute of the control. */ - "type"?: string; + "type": 'checkbox' | 'radio'; /** * Defines the `value` attribute of the control. */ diff --git a/packages/components/src/components/post-card-control/post-card-control.tsx b/packages/components/src/components/post-card-control/post-card-control.tsx index 423784acef..911f893373 100644 --- a/packages/components/src/components/post-card-control/post-card-control.tsx +++ b/packages/components/src/components/post-card-control/post-card-control.tsx @@ -1,4 +1,15 @@ -import { AttachInternals, Component, Element, h, Host, Prop, State, Watch } from '@stencil/core'; +import { + AttachInternals, + Component, + Element, + Event, + EventEmitter, + h, + Host, + Prop, + State, + Watch, +} from '@stencil/core'; import { checkOneOf } from '../../utils'; import { version } from '../../../package.json'; @@ -34,6 +45,10 @@ export class PostCardControl { @Element() host: HTMLPostCardControlElement; + @State() focused = false; + + @AttachInternals() private internals: ElementInternals; + /** * Defines the text in the control-label. */ @@ -42,47 +57,43 @@ export class PostCardControl { /** * Defines the description in the control-label. */ - @Prop() readonly description?: string; + @Prop() readonly description: string = null; /** * Defines the `id` attribute of the control. + * Make sure, the `id` is unique in the entire document. */ @Prop() readonly controlId!: string; /** * Defines the `type` attribute of the control. */ - @Prop() readonly type: string = 'checkbox'; + @Prop() readonly type!: 'checkbox' | 'radio'; /** * Defines the `form` attribute of the control. */ - @Prop() readonly form?: string; + @Prop() readonly form: string = null; /** * Defines the `name` attribute of the control. */ - @Prop() readonly name?: string; + @Prop() readonly name: string = null; /** * Defines the `value` attribute of the control. */ - @Prop() readonly value?: string; + @Prop() readonly value: string = null; /** * Defines the `checked` attribute of the control. */ @Prop({ reflect: true, mutable: true }) checked?: boolean = false; - /** - * Defines the `required` attribute of the control. - */ - @Prop() readonly required?: boolean = false; - /** * Defines the `disabled` attribute of the control. */ - @Prop() readonly disabled?: boolean = false; + @Prop() readonly disabled: boolean = false; /** * Defines the validation `state` of the control. @@ -91,13 +102,15 @@ export class PostCardControl { /** * Defines the icon `name` inside of the card. - * If not set the icon will not show up. + * If not set the icon will not show up. */ - @Prop() readonly icon?: string; + @Prop() readonly icon: string = null; - @State() focused = false; - - @AttachInternals() private internals: ElementInternals; + /** + * An event emitted whenever the control value changes. + * The payload contains the current checked state under `event.details`. + */ + @Event() controlChange: EventEmitter; constructor() { this.GROUPEVENT = `PostCardControlGroup:${this.name}:change`; @@ -140,6 +153,8 @@ export class PostCardControl { if (this.group.members.length > 1 && this.control.checked) { this.groupSetCheckedMember(this.control); } + + this.controlChange.emit(this.checked); } } @@ -220,9 +235,12 @@ export class PostCardControl { private groupEventHandler(e: CustomEvent) { if (!this.disabled) { this.control.checked = this.checked = this.control == e.detail.control; + this.internals.setFormValue(this.control.value); if (this.checked && e.detail.triggeredByKeyboard) this.control.focus(); this.groupCollectMembers(); + + this.controlChange.emit(this.checked); } } @@ -237,7 +255,6 @@ export class PostCardControl { class={{ 'card-control': true, 'is-checked': this.checked, - 'is-required': this.required, 'is-disabled': this.disabled, 'is-focused': this.focused, 'is-valid': this.state === true, @@ -248,14 +265,13 @@ export class PostCardControl {
(this.control = el as HTMLInputElement)} - id={this.controlId} + id={this.name} class="header--input form-check-input" type={this.type} form={this.form} name={this.name} value={this.value} checked={this.checked} - required={this.required} aria-disabled={this.disabled} onClick={this.controlClickHandler} onChange={this.controlChangeHandler} @@ -264,7 +280,7 @@ export class PostCardControl { onKeyDown={this.controlKeyDownHandler} /> -
-
+
diff --git a/packages/components/src/components/post-card-control/readme.md b/packages/components/src/components/post-card-control/readme.md index ae9c007f57..3ae2f996ea 100644 --- a/packages/components/src/components/post-card-control/readme.md +++ b/packages/components/src/components/post-card-control/readme.md @@ -7,19 +7,18 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| ------------------------ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------- | ----------- | -| `checked` | `checked` | Defines the `checked` attribute of the control. | `boolean` | `false` | -| `controlId` _(required)_ | `control-id` | Defines the `id` attribute of the control. Make sure, the `id` is unique in the entire document. | `string` | `undefined` | -| `description` | `description` | Defines the description in the control-label. | `string` | `null` | -| `disabled` | `disabled` | Defines the `disabled` attribute of the control. | `boolean` | `false` | -| `form` | `form` | Defines the `form` attribute of the control. | `string` | `null` | -| `icon` | `icon` | Defines the icon `name` inside of the card. If not set the icon will not show up. | `string` | `null` | -| `label` _(required)_ | `label` | Defines the text in the control-label. | `string` | `undefined` | -| `name` | `name` | Defines the `name` attribute of the control. | `string` | `null` | -| `state` | `state` | Defines the validation `state` of the control. | `boolean` | `null` | -| `type` _(required)_ | `type` | Defines the `type` attribute of the control. | `"checkbox" \| "radio"` | `undefined` | -| `value` | `value` | Defines the `value` attribute of the control. | `string` | `null` | +| Property | Attribute | Description | Type | Default | +| ------------------------ | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | ----------- | +| `checked` | `checked` | Defines the `checked` attribute of the control. If `true`, the control is selected. | `boolean` | `false` | +| `controlId` _(required)_ | `control-id` | Defines the `id` attribute of the control. Make sure, the `id` is unique in the entire document. | `string` | `undefined` | +| `description` | `description` | Defines the description in the control-label. | `string` | `null` | +| `disabled` | `disabled` | Defines the `disabled` attribute of the control. If `true`, the user can not interact with the control. | `boolean` | `false` | +| `icon` | `icon` | Defines the icon `name` inside of the card. If not set the icon will not show up. | `string` | `null` | +| `label` _(required)_ | `label` | Defines the text in the control-label. | `string` | `undefined` | +| `name` | `name` | Defines the `name` attribute of the control, which is submitted with the form data. | `string` | `null` | +| `state` | `state` | Defines the validation `state` of the control.
Only styles for the invalid state have been defined so far.
| `boolean` | `null` | +| `type` _(required)_ | `type` | Defines the `type` attribute of the control. | `"checkbox" \| "radio"` | `undefined` | +| `value` | `value` | Defines the `value` attribute of the control. This is only used, when the control participates in the native `form`. | `string` | `'on'` | ## Events diff --git a/packages/documentation/src/stories/components/card-control/card-control.docs.mdx b/packages/documentation/src/stories/components/card-control/card-control.docs.mdx index 47fc799d2c..a949e9a4c9 100644 --- a/packages/documentation/src/stories/components/card-control/card-control.docs.mdx +++ b/packages/documentation/src/stories/components/card-control/card-control.docs.mdx @@ -17,10 +17,33 @@ The `` element is part of the `@swisspost/design-system-compo ## Examples -### Grouped checkbox-cards +### Custom icon -TOTO: add example +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 `icon` slot and add your very own custom icon. -### Grouped radio-cards +
If you use the `icon` slot, 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.
-TOTO: add example + + +### With content + +The component accepts other HTML-elements as its content, this includes also other form elements. + + + +### Lined up + +Change the width of a `` component, by putting it (for example) in a grid. + + +
+ +
+ +### 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. + + diff --git a/packages/documentation/src/stories/components/card-control/card-control.stories.ts b/packages/documentation/src/stories/components/card-control/card-control.stories.ts index 15904e83a5..739ef354a2 100644 --- a/packages/documentation/src/stories/components/card-control/card-control.stories.ts +++ b/packages/documentation/src/stories/components/card-control/card-control.stories.ts @@ -1,5 +1,5 @@ import { useArgs } from '@storybook/preview-api'; -import { Args, Meta, StoryObj } from '@storybook/web-components'; +import { Args, Meta, StoryContext, StoryObj } from '@storybook/web-components'; import { BADGE } from '../../../../.storybook/constants'; import { html, nothing } from 'lit'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; @@ -12,31 +12,20 @@ const meta: Meta = { badges: [BADGE.NEEDS_REVISION], }, args: { - innerHTML: '', label: 'Label', description: '', - controlId: 'ExampleCardControl', + controlId: 'Default_CardControl', type: 'checkbox', - form: '', name: '', value: '', checked: '', disabled: '', state: 'null', icon: '', + slotDefault: '', + slotIcon: '', }, argTypes: { - innerHTML: { - name: 'Content', - description: - 'Defines the HTML markup contained in the card.
Content only shows up if the control is checked.

Markup accepted: block content.
This means, you can put everything in it, as long as there is enough space to render it.

', - table: { - category: 'General', - type: { - summary: 'string', - }, - }, - }, type: { control: { type: 'radio', @@ -58,6 +47,28 @@ const meta: Meta = { }, options: ['null', 'true', 'false'], }, + slotDefault: { + name: 'default', + description: + 'Content to place in the default slot.
Content only gets rendered if the control is checked and is not disabled.

Markup accepted: block content.
This means, you can put everything in it, as long as there is enough space to render it.

', + table: { + category: 'Slots', + type: { + summary: null, + }, + }, + }, + slotIcon: { + name: 'icon', + description: + 'Content to place in the named `icon` slot.

Markup accepted: inline content.
It is only meant for img or svg elements.

', + table: { + category: 'Slots', + type: { + summary: 'html', + }, + }, + }, }, }; @@ -85,8 +96,122 @@ export const Default: Story = { @controlChange="${(e: PostCardControlCustomEvent) => updateArgs({ checked: e.detail })}" > - ${unsafeHTML(args.innerHTML)} + ${unsafeHTML(args.slotDefault)} +
${unsafeHTML(args.slotIcon)}
`; }, }; + +export const CustomIcon: Story = { + args: { + controlId: 'CustomIcon_CardControl', + slotIcon: + '', + }, + render: Default.render, +}; + +export const Content: Story = { + args: { + slotDefault: `

Textus additione velit esse molestie consequat, vel + illum dolore eu feugiat nulla.

+
    +
  • Listus mixtus volare pagare la mare.
  • +
  • Elare volare cantare hendrerit in vulputate velit esse molestie consequat, vel + illum dolore eu feugiat.
  • +
+ +
+ + + + +
+ Hintus textus elare volare cantare hendrerit in vulputate velit esse molestie consequat, vel + illum dolore eu feugiat nulla facilisis. +
+
+ +
+ + +
`, + controlId: 'Content_CardControl', + checked: true, + }, + render: Default.render, +}; + +export const LinedUp: Story = { + parameters: { + controls: { + include: ['Columns'], + }, + }, + args: { + colCount: 2, + }, + argTypes: { + colCount: { + name: 'Columns', + description: 'Controls the amount of elements per row shown in the grid.', + control: { + type: 'inline-radio', + }, + options: [1, 2, 4], + table: { + category: 'General', + }, + }, + }, + render: (args: Args, context: StoryContext) => html` +
+ ${[1, 2, 3, 4, 5, 6].map( + i => html` +
+ ${Default.render?.( + { + label: `Checkbox${i}`, + description: i === 6 ? '20.- per year' : null, + controlId: `LindedUp_CardControl_${i}`, + type: args.type, + disabled: i === 3, + state: args.state, + }, + context, + )} +
+ `, + )} +
+ `, +}; + +export const RadioGroup: Story = { + render: (args: Args, context: StoryContext) => html` +
+ ${[1, 2, 3, 4, 5, 6].map( + i => html` +
+ ${Default.render?.( + { + label: `Radio${i}`, + controlId: `RadioGroup_CardControl_${i}`, + type: 'radio', + name: 'RadioGroup_Name', + disabled: i > 3 && i < 6 ? true : null, + state: args.state, + }, + context, + )} +
+ `, + )} +
+ `, +}; From a286ff682e88868e1ce93f7bfd8478e86199a8b3 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Fri, 22 Dec 2023 17:08:50 +0100 Subject: [PATCH 10/82] feat(styles): add default background-color for form-check elements --- packages/styles/src/components/form-check.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/styles/src/components/form-check.scss b/packages/styles/src/components/form-check.scss index 1c0c4a16cd..adee8a62b6 100644 --- a/packages/styles/src/components/form-check.scss +++ b/packages/styles/src/components/form-check.scss @@ -40,7 +40,7 @@ display: inline-flex; flex: 0 auto; appearance: none; - background: none; + background: color.$white; height: form-check.$form-check-input-size; width: form-check.$form-check-input-size; border: form-check.$form-check-input-border-width solid From 60bc278ada9855e557d5993d14d8935b8d02c726 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Fri, 22 Dec 2023 17:09:10 +0100 Subject: [PATCH 11/82] chore(documentation): add forms sort order in storybook navigation --- packages/documentation/.storybook/preview.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/documentation/.storybook/preview.ts b/packages/documentation/.storybook/preview.ts index 89e2a484fb..40b2212bd3 100644 --- a/packages/documentation/.storybook/preview.ts +++ b/packages/documentation/.storybook/preview.ts @@ -44,6 +44,17 @@ const preview: Preview = { 'Card', 'Collapsible', 'Forms', + [ + 'Input', + 'Select', + 'Textarea', + 'Checkbox', + 'Radio button', + 'Switch', + 'Card-Control', + 'Range', + 'Input Group', + ], 'Heading', 'Internet Header', ['Getting Started'], From 05690d2883772d2ffd8ee4fc215db97475162479 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Fri, 19 Jan 2024 17:11:23 +0100 Subject: [PATCH 12/82] feat(components): extend cypress getCompnent command to be able to input more options --- .../components/cypress/e2e/accordion.cy.ts | 2 +- packages/components/cypress/e2e/alert.cy.ts | 2 +- .../components/cypress/e2e/collapsible.cy.ts | 2 +- packages/components/cypress/e2e/tabs.cy.ts | 4 +-- packages/components/cypress/e2e/tooltip.cy.ts | 6 ++--- .../components/cypress/support/commands.ts | 25 ++++++++++++------- .../components/cypress/support/index.d.ts | 13 ++++++++-- 7 files changed, 35 insertions(+), 19 deletions(-) diff --git a/packages/components/cypress/e2e/accordion.cy.ts b/packages/components/cypress/e2e/accordion.cy.ts index 497ebc0e99..35c0d6dcf5 100644 --- a/packages/components/cypress/e2e/accordion.cy.ts +++ b/packages/components/cypress/e2e/accordion.cy.ts @@ -30,7 +30,7 @@ describe('accordion', () => { describe('multiple open panels', () => { beforeEach(() => { - cy.getComponent('accordion', 'multiple-open-panels'); + cy.getComponent('accordion', { story: 'multiple-open-panels' }); cy.get('@accordion').find('post-accordion-item').as('collapsibles'); }); diff --git a/packages/components/cypress/e2e/alert.cy.ts b/packages/components/cypress/e2e/alert.cy.ts index 80f945df5d..b948e66be2 100644 --- a/packages/components/cypress/e2e/alert.cy.ts +++ b/packages/components/cypress/e2e/alert.cy.ts @@ -15,7 +15,7 @@ describe('alert', () => { describe('dismissible', () => { beforeEach(() => { - cy.getComponent('post-alert', 'dismissible'); + cy.getComponent('post-alert', { story: 'dismissible' }); }); it('should have a close button', () => { diff --git a/packages/components/cypress/e2e/collapsible.cy.ts b/packages/components/cypress/e2e/collapsible.cy.ts index 22e1f5949b..688271632f 100644 --- a/packages/components/cypress/e2e/collapsible.cy.ts +++ b/packages/components/cypress/e2e/collapsible.cy.ts @@ -35,7 +35,7 @@ describe('collapsible', () => { describe('initially collapsed', () => { beforeEach(() => { - cy.getComponent('collapsible', 'initially-collapsed'); + cy.getComponent('collapsible', { story: 'initially-collapsed' }); cy.get('@collapsible').find('.collapse').as('collapse'); cy.get('#components-collapsible--initially-collapsed--button').as('toggler'); }); diff --git a/packages/components/cypress/e2e/tabs.cy.ts b/packages/components/cypress/e2e/tabs.cy.ts index 33048ff560..25abccab92 100644 --- a/packages/components/cypress/e2e/tabs.cy.ts +++ b/packages/components/cypress/e2e/tabs.cy.ts @@ -58,7 +58,7 @@ describe('tabs', () => { describe('active panel', () => { beforeEach(() => { - cy.getComponent('tabs', 'active-panel'); + cy.getComponent('tabs', { story: 'active-panel' }); cy.get('post-tab-header').as('headers'); cy.get('post-tab-panel:visible').as('panel'); }); @@ -91,7 +91,7 @@ describe('tabs', () => { describe('async', () => { beforeEach(() => { - cy.getComponent('tabs', 'async'); + cy.getComponent('tabs', { story: 'async' }); cy.get('post-tab-header').as('headers'); }); diff --git a/packages/components/cypress/e2e/tooltip.cy.ts b/packages/components/cypress/e2e/tooltip.cy.ts index d53df94c48..bc9d0c758b 100644 --- a/packages/components/cypress/e2e/tooltip.cy.ts +++ b/packages/components/cypress/e2e/tooltip.cy.ts @@ -1,7 +1,7 @@ describe('tooltips', () => { describe('default', () => { beforeEach(() => { - cy.getComponent('tooltip', 'multiple'); + cy.getComponent('tooltip', { story: 'multiple' }); cy.get('button[data-tooltip-target="tooltip-multiple"]:first-of-type').as('target1'); cy.get('button[data-tooltip-target="tooltip-multiple"]:last-of-type').as('target2'); cy.get('#tooltip-multiple').find('div[popover]').as('tooltip'); @@ -34,7 +34,7 @@ describe('tooltips', () => { describe('non-focusable element', () => { beforeEach(() => { - cy.getComponent('tooltip', 'non-focusable'); + cy.getComponent('tooltip', { story: 'non-focusable' }); cy.get('cite[data-tooltip-target="tooltip-non-focusable"]').as('target'); }); @@ -45,7 +45,7 @@ describe('tooltips', () => { describe('aria', () => { beforeEach(() => { - cy.getComponent('tooltip', 'multiple'); + cy.getComponent('tooltip', { story: 'multiple' }); cy.get('button[data-tooltip-target="tooltip-multiple"]:first-of-type').as('target1'); cy.get('@target1').invoke('attr', 'aria-describedby', 'existing-value'); }); diff --git a/packages/components/cypress/support/commands.ts b/packages/components/cypress/support/commands.ts index a209de01d7..48d43536b9 100644 --- a/packages/components/cypress/support/commands.ts +++ b/packages/components/cypress/support/commands.ts @@ -48,17 +48,24 @@ export const isInViewport = function (_chai: Chai.ChaiStatic) { chai.use(isInViewport); -Cypress.Commands.add('getComponent', (component: string, story = 'default') => { - cy.visit(`/iframe.html?id=components-${component}--${story}`); +Cypress.Commands.add('getComponent', (component: string, options = {}) => { + const componentPath = [] + .concat(options.group, component) + .filter(p => p) + .join('-'); + cy.visit(`/iframe.html?id=components-${componentPath}--${options.story ?? 'default'}`); const alias = component.replace(/^post-/, ''); cy.get(`post-${alias}`, { timeout: 30000 }).as(alias); }); -Cypress.Commands.add('checkAriaExpanded', (controlledElementSelector: string, isExpanded: 'true' | 'false') => { - cy.get(controlledElementSelector) - .invoke('attr', 'id') - .then(id => { - cy.get(`[aria-controls="${id}"]`).should('have.attr', 'aria-expanded', isExpanded); - }); -}); +Cypress.Commands.add( + 'checkAriaExpanded', + (controlledElementSelector: string, isExpanded: 'true' | 'false') => { + cy.get(controlledElementSelector) + .invoke('attr', 'id') + .then(id => { + cy.get(`[aria-controls="${id}"]`).should('have.attr', 'aria-expanded', isExpanded); + }); + }, +); diff --git a/packages/components/cypress/support/index.d.ts b/packages/components/cypress/support/index.d.ts index 3aa27be8e8..b9df4e1d0b 100644 --- a/packages/components/cypress/support/index.d.ts +++ b/packages/components/cypress/support/index.d.ts @@ -1,8 +1,17 @@ declare global { namespace Cypress { interface Chainable { - getComponent(component: string, story?: string): Chainable; - checkAriaExpanded(controlledElementSelector: string, isExpanded: 'true' | 'false'): Chainable; + getComponent( + component: string, + options: { + group?: string | string[]; + story?: string; + }, + ): Chainable; + checkAriaExpanded( + controlledElementSelector: string, + isExpanded: 'true' | 'false', + ): Chainable; } } } From 127025bab545f07f3025ef7923658b4714e116c0 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Fri, 19 Jan 2024 17:12:32 +0100 Subject: [PATCH 13/82] feat(components): remove controlId as prop (autocreation) --- .../components/post-card-control/post-card-control.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/components/src/components/post-card-control/post-card-control.tsx b/packages/components/src/components/post-card-control/post-card-control.tsx index 3cee0f1fb2..a66f85d708 100644 --- a/packages/components/src/components/post-card-control/post-card-control.tsx +++ b/packages/components/src/components/post-card-control/post-card-control.tsx @@ -13,6 +13,7 @@ import { import { checkOneOf } from '../../utils'; import { version } from '../../../package.json'; +let cardControlIds = 0; /** * @class PostCardControl - representing a stencil component */ @@ -43,6 +44,7 @@ export class PostCardControl { }; private control: HTMLInputElement; + private controlId = `PostCardControl_${cardControlIds++}`; @Element() host: HTMLPostCardControlElement; @@ -60,12 +62,6 @@ export class PostCardControl { */ @Prop() readonly description: string = null; - /** - * Defines the `id` attribute of the control. - * Make sure, the `id` is unique in the entire document. - */ - @Prop() readonly controlId!: string; - /** * Defines the `type` attribute of the control. */ From 62c9a94f692f504c2ff74cc8501dd7a027ac19bb Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Fri, 19 Jan 2024 17:15:07 +0100 Subject: [PATCH 14/82] feat(components): add errors when required props not set --- .../src/components/post-card-control/post-card-control.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/components/src/components/post-card-control/post-card-control.tsx b/packages/components/src/components/post-card-control/post-card-control.tsx index a66f85d708..523bf3fbb2 100644 --- a/packages/components/src/components/post-card-control/post-card-control.tsx +++ b/packages/components/src/components/post-card-control/post-card-control.tsx @@ -250,6 +250,9 @@ export class PostCardControl { } componentWillLoad() { + if (!this.label) throw new Error('No label set: must have a "label".'); + if (!this.type) throw new Error('No type set: must have a "type".'); + this.validateControlType(); } From cc7a7c75111241842a1fd003f15ad5b823e975f4 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Fri, 19 Jan 2024 17:15:51 +0100 Subject: [PATCH 15/82] refactor(components): remove default value 'on' for value prop --- .../src/components/post-card-control/post-card-control.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/components/src/components/post-card-control/post-card-control.tsx b/packages/components/src/components/post-card-control/post-card-control.tsx index 523bf3fbb2..ece71b4cf3 100644 --- a/packages/components/src/components/post-card-control/post-card-control.tsx +++ b/packages/components/src/components/post-card-control/post-card-control.tsx @@ -14,6 +14,7 @@ import { checkOneOf } from '../../utils'; import { version } from '../../../package.json'; let cardControlIds = 0; + /** * @class PostCardControl - representing a stencil component */ @@ -75,7 +76,7 @@ export class PostCardControl { /** * Defines the `value` attribute of the control. This is only used, when the control participates in the native `form`. */ - @Prop() readonly value: string = 'on'; + @Prop() readonly value: string = null; /** * Defines the `checked` attribute of the control. If `true`, the control is selected. From 7110b16bb5a6082d8b7b3d7f2f35873c6e31b59b Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Fri, 19 Jan 2024 17:16:51 +0100 Subject: [PATCH 16/82] refactor(components): state property to make it resettable --- .../src/components/post-card-control/post-card-control.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/src/components/post-card-control/post-card-control.tsx b/packages/components/src/components/post-card-control/post-card-control.tsx index ece71b4cf3..5f551d6ee7 100644 --- a/packages/components/src/components/post-card-control/post-card-control.tsx +++ b/packages/components/src/components/post-card-control/post-card-control.tsx @@ -91,7 +91,7 @@ export class PostCardControl { /** * Defines the validation `state` of the control.
Only styles for the invalid state have been defined so far.
*/ - @Prop() readonly state: boolean = null; + @Prop() readonly state: null | 'true' | 'false' = null; /** * Defines the icon `name` inside of the card. @@ -266,8 +266,8 @@ export class PostCardControl { 'is-checked': this.checked, 'is-disabled': this.disabled, 'is-focused': this.focused, - 'is-valid': this.state === true, - 'is-invalid': this.state === false, + 'is-valid': this.state !== null && this.state !== 'false', + 'is-invalid': this.state === 'false', }} onClick={this.cardClickHandler} > From f6fd5d9124c53f9a0dd76f95992212b329879505 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Fri, 19 Jan 2024 17:17:50 +0100 Subject: [PATCH 17/82] fix(components): move card click handler to listen on the host --- .../src/components/post-card-control/post-card-control.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/components/src/components/post-card-control/post-card-control.tsx b/packages/components/src/components/post-card-control/post-card-control.tsx index 5f551d6ee7..3492df3c53 100644 --- a/packages/components/src/components/post-card-control/post-card-control.tsx +++ b/packages/components/src/components/post-card-control/post-card-control.tsx @@ -259,7 +259,7 @@ export class PostCardControl { render() { return ( - +
Date: Fri, 19 Jan 2024 17:18:14 +0100 Subject: [PATCH 18/82] feat(components): improve clickable areas within content area --- .../post-card-control/post-card-control.tsx | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/components/src/components/post-card-control/post-card-control.tsx b/packages/components/src/components/post-card-control/post-card-control.tsx index 3492df3c53..2ef660ca51 100644 --- a/packages/components/src/components/post-card-control/post-card-control.tsx +++ b/packages/components/src/components/post-card-control/post-card-control.tsx @@ -44,6 +44,24 @@ export class PostCardControl { focused: null, }; + private INTERACTIVE_ELEMENT_SELECTORS = [ + 'a', + 'audio[controls]', + 'button', + 'details', + 'embed', + 'iframe', + 'img[usemap]', + 'input:not([type="hidden"])', + 'keygen', + 'label', + 'menu[type="toolbar"]', + 'object[usemap]', + 'select', + 'textarea', + 'video[controls]', + ]; + private control: HTMLInputElement; private controlId = `PostCardControl_${cardControlIds++}`; @@ -156,7 +174,14 @@ export class PostCardControl { } private contentClickHandler(e: Event) { - e.stopPropagation(); + const testWrapper = document.createElement('div'); + testWrapper.append((e.target as HTMLElement).cloneNode()); + + const isInteractive = this.INTERACTIVE_ELEMENT_SELECTORS.some(selector => + testWrapper.querySelector(selector), + ); + + if (isInteractive) e.stopPropagation(); } // https://googlechromelabs.github.io/howto-components/howto-radio-group/ From 2d2f981eb8e242683381c254fba32588e6e89404 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Fri, 19 Jan 2024 17:19:02 +0100 Subject: [PATCH 19/82] chore(components): update story and component definitions accordingly --- packages/components/src/components.d.ts | 12 ++-------- .../components/post-card-control/readme.md | 23 +++++++++---------- .../card-control/card-control.stories.ts | 20 ++++++++-------- 3 files changed, 24 insertions(+), 31 deletions(-) diff --git a/packages/components/src/components.d.ts b/packages/components/src/components.d.ts index 0b22079750..0b0fc9b154 100644 --- a/packages/components/src/components.d.ts +++ b/packages/components/src/components.d.ts @@ -78,10 +78,6 @@ export namespace Components { * Defines the `checked` attribute of the control. If `true`, the control is selected. */ "checked"?: boolean; - /** - * Defines the `id` attribute of the control. Make sure, the `id` is unique in the entire document. - */ - "controlId": string; /** * Defines the description in the control-label. */ @@ -105,7 +101,7 @@ export namespace Components { /** * Defines the validation `state` of the control.
Only styles for the invalid state have been defined so far.
*/ - "state": boolean; + "state": null | 'true' | 'false'; /** * Defines the `type` attribute of the control. */ @@ -480,10 +476,6 @@ declare namespace LocalJSX { * Defines the `checked` attribute of the control. If `true`, the control is selected. */ "checked"?: boolean; - /** - * Defines the `id` attribute of the control. Make sure, the `id` is unique in the entire document. - */ - "controlId": string; /** * Defines the description in the control-label. */ @@ -511,7 +503,7 @@ declare namespace LocalJSX { /** * Defines the validation `state` of the control.
Only styles for the invalid state have been defined so far.
*/ - "state"?: boolean; + "state"?: null | 'true' | 'false'; /** * Defines the `type` attribute of the control. */ diff --git a/packages/components/src/components/post-card-control/readme.md b/packages/components/src/components/post-card-control/readme.md index 3ae2f996ea..e86ea6fcf6 100644 --- a/packages/components/src/components/post-card-control/readme.md +++ b/packages/components/src/components/post-card-control/readme.md @@ -7,18 +7,17 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| ------------------------ | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | ----------- | -| `checked` | `checked` | Defines the `checked` attribute of the control. If `true`, the control is selected. | `boolean` | `false` | -| `controlId` _(required)_ | `control-id` | Defines the `id` attribute of the control. Make sure, the `id` is unique in the entire document. | `string` | `undefined` | -| `description` | `description` | Defines the description in the control-label. | `string` | `null` | -| `disabled` | `disabled` | Defines the `disabled` attribute of the control. If `true`, the user can not interact with the control. | `boolean` | `false` | -| `icon` | `icon` | Defines the icon `name` inside of the card. If not set the icon will not show up. | `string` | `null` | -| `label` _(required)_ | `label` | Defines the text in the control-label. | `string` | `undefined` | -| `name` | `name` | Defines the `name` attribute of the control, which is submitted with the form data. | `string` | `null` | -| `state` | `state` | Defines the validation `state` of the control.
Only styles for the invalid state have been defined so far.
| `boolean` | `null` | -| `type` _(required)_ | `type` | Defines the `type` attribute of the control. | `"checkbox" \| "radio"` | `undefined` | -| `value` | `value` | Defines the `value` attribute of the control. This is only used, when the control participates in the native `form`. | `string` | `'on'` | +| Property | Attribute | Description | Type | Default | +| -------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | ----------- | +| `checked` | `checked` | Defines the `checked` attribute of the control. If `true`, the control is selected. | `boolean` | `false` | +| `description` | `description` | Defines the description in the control-label. | `string` | `null` | +| `disabled` | `disabled` | Defines the `disabled` attribute of the control. If `true`, the user can not interact with the control. | `boolean` | `false` | +| `icon` | `icon` | Defines the icon `name` inside of the card. If not set the icon will not show up. | `string` | `null` | +| `label` _(required)_ | `label` | Defines the text in the control-label. | `string` | `undefined` | +| `name` | `name` | Defines the `name` attribute of the control, which is submitted with the form data. | `string` | `null` | +| `state` | `state` | Defines the validation `state` of the control.
Only styles for the invalid state have been defined so far.
| `"false" \| "true"` | `null` | +| `type` _(required)_ | `type` | Defines the `type` attribute of the control. | `"checkbox" \| "radio"` | `undefined` | +| `value` | `value` | Defines the `value` attribute of the control. This is only used, when the control participates in the native `form`. | `string` | `null` | ## Events diff --git a/packages/documentation/src/stories/components/card-control/card-control.stories.ts b/packages/documentation/src/stories/components/card-control/card-control.stories.ts index 739ef354a2..02fbc55072 100644 --- a/packages/documentation/src/stories/components/card-control/card-control.stories.ts +++ b/packages/documentation/src/stories/components/card-control/card-control.stories.ts @@ -14,7 +14,6 @@ const meta: Meta = { args: { label: 'Label', description: '', - controlId: 'Default_CardControl', type: 'checkbox', name: '', value: '', @@ -46,6 +45,11 @@ const meta: Meta = { }, }, options: ['null', 'true', 'false'], + table: { + type: { + summary: 'null | boolean', + }, + }, }, slotDefault: { name: 'default', @@ -61,7 +65,7 @@ const meta: Meta = { slotIcon: { name: 'icon', description: - 'Content to place in the named `icon` slot.

Markup accepted: inline content.
It is only meant for img or svg elements.

', + 'Content to place in the named `icon` slot.

Markup accepted: inline content.
It is only meant for img or svg elements and overrides the `icon` property.

', table: { category: 'Slots', type: { @@ -80,11 +84,14 @@ export const Default: Story = { render: (args: Args) => { const [, updateArgs] = useArgs(); + const icon = html` +
${unsafeHTML(args.slotIcon)}
+ `; + return html` - ${unsafeHTML(args.slotDefault)} -
${unsafeHTML(args.slotIcon)}
+ ${args.slotIcon ? icon : null} ${unsafeHTML(args.slotDefault)}
`; }, @@ -105,7 +111,6 @@ export const Default: Story = { export const CustomIcon: Story = { args: { - controlId: 'CustomIcon_CardControl', slotIcon: '', }, @@ -141,7 +146,6 @@ export const Content: Story = {
`, - controlId: 'Content_CardControl', checked: true, }, render: Default.render, @@ -178,7 +182,6 @@ export const LinedUp: Story = { { label: `Checkbox${i}`, description: i === 6 ? '20.- per year' : null, - controlId: `LindedUp_CardControl_${i}`, type: args.type, disabled: i === 3, state: args.state, @@ -201,7 +204,6 @@ export const RadioGroup: Story = { ${Default.render?.( { label: `Radio${i}`, - controlId: `RadioGroup_CardControl_${i}`, type: 'radio', name: 'RadioGroup_Name', disabled: i > 3 && i < 6 ? true : null, From f14797db86329bd8ed60326fd68c10ece68b1c52 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Fri, 19 Jan 2024 17:19:21 +0100 Subject: [PATCH 20/82] test(components): add basic testing --- .../components/cypress/e2e/control-card.cy.ts | 253 ++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 packages/components/cypress/e2e/control-card.cy.ts diff --git a/packages/components/cypress/e2e/control-card.cy.ts b/packages/components/cypress/e2e/control-card.cy.ts new file mode 100644 index 0000000000..4a98d80507 --- /dev/null +++ b/packages/components/cypress/e2e/control-card.cy.ts @@ -0,0 +1,253 @@ +const INTERACTIVE_ELEMENT_SELECTORS = [ + 'a', + 'audio[controls]', + 'button', + 'details', + 'embed', + 'iframe', + 'img[usemap]', + 'input:not([type="hidden"])', + 'keygen', + 'label', + 'menu[type="toolbar"]', + 'object[usemap]', + 'select', + 'textarea', + 'video[controls]', +]; + +describe('card-control', () => { + describe.skip('structure & props', () => { + beforeEach(() => { + cy.getComponent('card-control', { group: 'forms' }); + cy.window().then(win => { + cy.wrap(cy.spy(win.console, 'error')).as('consoleError'); + }); + + cy.get('@card-control').find('.card-control').as('wrapper'); + + cy.get('@card-control').find('input.header--input').as('input'); + + cy.get('@card-control').find('label.header--label').as('label'); + + cy.get('@card-control').find('.header--icon').as('icon'); + cy.get('@card-control').find('.header--icon slot[name="icon"]').as('slotIcon'); + + cy.get('@card-control').find('.card-control--content').as('content'); + cy.get('@card-control').find('.card-control--content slot').as('slotContent'); + }); + + it('should have no console errors', () => { + cy.get('@consoleError').should('not.be.called'); + }); + + it('should render mandatory elements', () => { + cy.get('@card-control').should('exist'); + + cy.get('@input').should('exist'); + + cy.get('@label').should('exist'); + + cy.get('@icon').should('exist'); + cy.get('@slotIcon').should('exist'); + + cy.get('@content').should('exist'); + cy.get('@slotContent').should('exist'); + }); + + it('should have mandatory attributes', () => { + cy.get('@card-control').should('have.attr', 'data-version'); + cy.get('@card-control').should('have.attr', 'label').and('eq', 'Label'); + cy.get('@card-control').should('have.attr', 'type').and('eq', 'checkbox'); + + cy.get('@input').then($input => { + const controlId = $input.attr('id'); + const contentId = `${controlId}_Content`; + + cy.get('@input') + .should('have.attr', 'id') + .and('contain', 'PostCardControl_') + .and('eq', controlId); + cy.get('@input').should('have.attr', 'type').and('eq', 'checkbox'); + cy.get('@input').should('have.attr', 'value').and('eq', 'on'); + cy.get('@input').should('have.attr', 'aria-controls').and('eq', contentId); + cy.get('@input').should('have.attr', 'aria-expanded').and('eq', 'false'); + + cy.get('@label').should('have.attr', 'for').and('eq', controlId); + + cy.get('@content').should('have.attr', 'id').and('eq', contentId); + }); + }); + + it('should have mandatory content', () => { + cy.get('@label').should('have.text', 'Label'); + cy.get('@content').should('not.be.visible'); + }); + + it('should set label text according to "label" prop', () => { + cy.get('@card-control').invoke('attr', 'label', 'Lorem ipsum'); + cy.get('@label').should('have.text', 'Lorem ipsum'); + }); + + it('should render label also with empty string', () => { + cy.get('@card-control').invoke('attr', 'label', ''); + cy.get('@label').should('exist').and('have.text', ''); + }); + + it('should set description text according to "description" prop', () => { + cy.get('@card-control').find('.header--description').should('not.exist'); + cy.get('@card-control') + .invoke('attr', 'description', 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr.') + .find('.header--description') + .should('exist') + .and('have.text', 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr.'); + }); + + it('should only render description, when "description" prop is not an empty string', () => { + cy.get('@card-control') + .invoke('attr', 'description', '') + .find('.header--description') + .should('not.exist'); + + cy.get('@card-control') + .invoke('removeAttr', 'description') + .should('not.have.attr', 'description'); + }); + + it('should set input "type" attr according to "type" prop', () => { + cy.get('@card-control').invoke('attr', 'type', 'radio'); + cy.get('@control').should('have.attr', 'type').and('eq', 'radio'); + cy.get('@card-control').invoke('attr', 'type', 'checkbox'); + cy.get('@control').should('have.attr', 'type').and('eq', 'checkbox'); + }); + + it('should set input "name" attr according to "name" prop', () => { + cy.get('@input').should('not.have.attr', 'name'); + cy.get('@card-control').invoke('attr', 'name', 'custom-name'); + cy.get('@input').should('have.attr', 'name').and('eq', 'custom-name'); + cy.get('@card-control').invoke('removeAttr', 'name'); + cy.get('@input').should('not.have.attr', 'name'); + }); + + it('should set input "value" attr according to "value" prop', () => { + cy.get('@input').should('not.have.attr', 'value'); + cy.get('@card-control').invoke('attr', 'value', 'custom-value'); + cy.get('@input').should('have.attr', 'value').and('eq', 'custom-value'); + cy.get('@card-control').invoke('removeAttr', 'value'); + cy.get('@input').should('not.have.attr', 'value'); + }); + + it('should set input "checked" prop according to "checked" prop', () => { + cy.get('@wrapper').should('not.have.class', 'is-checked'); + cy.get('@input').should('have.prop', 'checked').and('eq', false); + cy.get('@card-control').invoke('attr', 'checked', true); + cy.get('@wrapper').should('have.class', 'is-checked'); + cy.get('@input').should('have.prop', 'checked').and('eq', true); + cy.get('@card-control').invoke('removeAttr', 'checked'); + cy.get('@wrapper').should('not.have.class', 'is-checked'); + cy.get('@input').should('have.prop', 'checked').and('eq', false); + }); + + it('should set input "disabled" attr according to "disbled" prop', () => { + cy.get('@wrapper').should('not.have.class', 'is-disabled'); + cy.get('@input').should('not.have.attr', 'aria-disabled'); + cy.get('@card-control').invoke('attr', 'disabled', true); + cy.get('@wrapper').should('have.class', 'is-disabled'); + cy.get('@input').should('have.attr', 'aria-disabled'); + cy.get('@card-control').invoke('removeAttr', 'disabled'); + cy.get('@wrapper').should('not.have.class', 'is-disabled'); + cy.get('@input').should('not.have.attr', 'aria-disabled'); + }); + + it('should set validation state according to "state" prop', () => { + cy.get('@wrapper').should('not.have.class', 'is-valid').and('not.have.class', 'is-invalid'); + cy.get('@card-control').invoke('attr', 'state', ''); + cy.get('@wrapper').should('have.class', 'is-valid').and('not.have.class', 'is-invalid'); + cy.get('@card-control').invoke('attr', 'state', 'state'); + cy.get('@wrapper').should('have.class', 'is-valid').and('not.have.class', 'is-invalid'); + cy.get('@card-control').invoke('attr', 'state', true); + cy.get('@wrapper').should('have.class', 'is-valid').and('not.have.class', 'is-invalid'); + cy.get('@card-control').invoke('attr', 'state', false); + cy.get('@wrapper').should('not.have.class', 'is-valid').and('have.class', 'is-invalid'); + cy.get('@card-control').invoke('removeAttr', 'state'); + cy.get('@wrapper').should('not.have.class', 'is-valid').and('not.have.class', 'is-invalid'); + }); + + it('should set icon "name" attr according to "icon" prop', () => { + cy.get('@slotIcon').find('post-icon').should('not.exist'); + cy.get('@card-control').invoke('attr', 'icon', '1000'); + cy.get('@slotIcon') + .find('post-icon') + .should('exist') + .find('[style*="/1000.svg"]') + .should('exist'); + cy.get('@card-control').invoke('removeAttr', 'icon'); + cy.get('@slotIcon').find('post-icon').should('not.exist'); + }); + }); + + describe('events', () => { + beforeEach(() => { + cy.getComponent('card-control', { group: 'forms', story: 'content' }); + cy.get('@card-control').invoke('removeAttr', 'checked'); + + cy.get('@card-control').find('.card-control').as('wrapper'); + + cy.get('@card-control').find('input.header--input').as('input'); + + cy.get('@card-control').find('.card-control--content').as('content'); + cy.get('@card-control').find('.card-control--content slot').as('slotContent'); + }); + + it.skip('should toggle class "is-focused" when focused/blured', () => { + cy.get('@input') + .first() + .focus() + .then($input => { + cy.get('@wrapper').should('have.class', 'is-focused'); + $input[0].blur(); + cy.wrap($input).should('not.have.class', 'is-focused'); + }); + }); + + it.skip('should toggle content when clicked', () => { + cy.get('@content').should('not.be.visible'); + cy.get('@input').should('have.attr', 'aria-expanded').and('eq', 'false'); + cy.get('@card-control').click({ force: true }); + cy.get('@content').should('be.visible'); + cy.get('@input').should('have.attr', 'aria-expanded').and('eq', 'true'); + cy.get('@card-control').click({ force: true }); + cy.get('@content').should('not.be.visible'); + cy.get('@input').should('have.attr', 'aria-expanded').and('eq', 'false'); + }); + + it.skip('should not toggle content when clicking on interactive element in content', () => { + cy.get('@card-control').click(); + cy.get('@input').should('have.attr', 'aria-expanded').and('eq', 'true'); + cy.get('@content').should('be.visible'); + + cy.get('@card-control') + .find('> *') + .not('[slot]') + .find(INTERACTIVE_ELEMENT_SELECTORS.join(', ')) + .not('input[type="hidden"]') + .each($el => { + if ($el.is('select')) { + cy.wrap($el).select(0); + } else { + cy.wrap($el).click({ force: true }); + } + cy.get('@input').should('have.attr', 'aria-expanded').and('eq', 'true'); + cy.get('@content').should('be.visible'); + }); + }); + + it.skip('should toogle content when used with keyboard', () => { + // TODO when https://github.com/cypress-io/cypress/issues/311 is ready + }); + + // TODO: test change events + }); + + // TODO: test form association +}); From d79f1a710aa2b9f42301c16c877292a15a82e0fb Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Wed, 24 Jan 2024 11:59:20 +0100 Subject: [PATCH 21/82] fix(components): card-control e2e tests --- .../components/cypress/e2e/control-card.cy.ts | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/packages/components/cypress/e2e/control-card.cy.ts b/packages/components/cypress/e2e/control-card.cy.ts index 4a98d80507..29a6498434 100644 --- a/packages/components/cypress/e2e/control-card.cy.ts +++ b/packages/components/cypress/e2e/control-card.cy.ts @@ -17,7 +17,7 @@ const INTERACTIVE_ELEMENT_SELECTORS = [ ]; describe('card-control', () => { - describe.skip('structure & props', () => { + describe('structure & props', () => { beforeEach(() => { cy.getComponent('card-control', { group: 'forms' }); cy.window().then(win => { @@ -69,7 +69,6 @@ describe('card-control', () => { .and('contain', 'PostCardControl_') .and('eq', controlId); cy.get('@input').should('have.attr', 'type').and('eq', 'checkbox'); - cy.get('@input').should('have.attr', 'value').and('eq', 'on'); cy.get('@input').should('have.attr', 'aria-controls').and('eq', contentId); cy.get('@input').should('have.attr', 'aria-expanded').and('eq', 'false'); @@ -94,6 +93,8 @@ describe('card-control', () => { cy.get('@label').should('exist').and('have.text', ''); }); + // TODO: test error message when label not set + it('should set description text according to "description" prop', () => { cy.get('@card-control').find('.header--description').should('not.exist'); cy.get('@card-control') @@ -116,11 +117,13 @@ describe('card-control', () => { it('should set input "type" attr according to "type" prop', () => { cy.get('@card-control').invoke('attr', 'type', 'radio'); - cy.get('@control').should('have.attr', 'type').and('eq', 'radio'); + cy.get('@input').should('have.attr', 'type').and('eq', 'radio'); cy.get('@card-control').invoke('attr', 'type', 'checkbox'); - cy.get('@control').should('have.attr', 'type').and('eq', 'checkbox'); + cy.get('@input').should('have.attr', 'type').and('eq', 'checkbox'); }); + // TODO: test error message when type not set + it('should set input "name" attr according to "name" prop', () => { cy.get('@input').should('not.have.attr', 'name'); cy.get('@card-control').invoke('attr', 'name', 'custom-name'); @@ -199,18 +202,29 @@ describe('card-control', () => { cy.get('@card-control').find('.card-control--content slot').as('slotContent'); }); - it.skip('should toggle class "is-focused" when focused/blured', () => { + it('should toggle class "is-focused" when focused/blured', () => { + cy.get('@wrapper').should('not.have.class', 'is-focused'); + cy.get('@input') .first() .focus() - .then($input => { + // waiting for focus to be executed, because focus() is not implemented like other action commands, and does not follow the same rules of waiting for actionability. + .wait(300) + .then(() => { cy.get('@wrapper').should('have.class', 'is-focused'); - $input[0].blur(); - cy.wrap($input).should('not.have.class', 'is-focused'); + }); + + cy.get('@input') + .first() + .blur({ force: true }) + // waiting for blur to be executed, because blur() is not implemented like other action commands, and does not follow the same rules of waiting for actionability. + .wait(300) + .then(() => { + cy.get('@wrapper').should('not.have.class', 'is-focused'); }); }); - it.skip('should toggle content when clicked', () => { + it('should toggle content when clicked', () => { cy.get('@content').should('not.be.visible'); cy.get('@input').should('have.attr', 'aria-expanded').and('eq', 'false'); cy.get('@card-control').click({ force: true }); @@ -221,7 +235,7 @@ describe('card-control', () => { cy.get('@input').should('have.attr', 'aria-expanded').and('eq', 'false'); }); - it.skip('should not toggle content when clicking on interactive element in content', () => { + it('should not toggle content when clicking on interactive element in content', () => { cy.get('@card-control').click(); cy.get('@input').should('have.attr', 'aria-expanded').and('eq', 'true'); cy.get('@content').should('be.visible'); @@ -242,7 +256,7 @@ describe('card-control', () => { }); }); - it.skip('should toogle content when used with keyboard', () => { + it('should toogle content when used with keyboard', () => { // TODO when https://github.com/cypress-io/cypress/issues/311 is ready }); From b77ce942f250d5bdf38fbcae007d1964b1987aab Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Wed, 7 Feb 2024 14:13:12 +0100 Subject: [PATCH 22/82] chore(components): card-control add initialState state --- .../src/components/post-card-control/post-card-control.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/components/src/components/post-card-control/post-card-control.tsx b/packages/components/src/components/post-card-control/post-card-control.tsx index 2ef660ca51..169ac39264 100644 --- a/packages/components/src/components/post-card-control/post-card-control.tsx +++ b/packages/components/src/components/post-card-control/post-card-control.tsx @@ -67,6 +67,7 @@ export class PostCardControl { @Element() host: HTMLPostCardControlElement; + @State() initialState: boolean; @State() focused = false; @AttachInternals() private internals: ElementInternals; @@ -278,6 +279,9 @@ export class PostCardControl { componentWillLoad() { if (!this.label) throw new Error('No label set: must have a "label".'); if (!this.type) throw new Error('No type set: must have a "type".'); + connectedCallback() { + this.initialState = this.checked; + } this.validateControlType(); } From 422621c8f8c1e6e3c67303c70b120b7de9962266 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Wed, 7 Feb 2024 14:14:41 +0100 Subject: [PATCH 23/82] chore(components): card-control refactor required props checks --- .../post-card-control/post-card-control.tsx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/components/src/components/post-card-control/post-card-control.tsx b/packages/components/src/components/post-card-control/post-card-control.tsx index 169ac39264..9ab49f68bb 100644 --- a/packages/components/src/components/post-card-control/post-card-control.tsx +++ b/packages/components/src/components/post-card-control/post-card-control.tsx @@ -10,7 +10,7 @@ import { State, Watch, } from '@stencil/core'; -import { checkOneOf } from '../../utils'; +import { checkNonEmpty, checkOneOf } from '../../utils'; import { version } from '../../../package.json'; let cardControlIds = 0; @@ -140,12 +140,20 @@ export class PostCardControl { window.addEventListener(this.GROUPEVENT, this.groupEventHandler); } + @Watch('label') + validateControlLabel(label = this.label) { + checkNonEmpty( + label, + 'The "post-card-control" element requires its "label" property to be set.', + ); + } + @Watch('type') validateControlType(type = this.type) { checkOneOf( type, ['checkbox', 'radio'], - 'The "post-card-control" element requires an "controlType"" of either "checkbox" (default) or "radio".', + 'The "post-card-control" element requires its "type" prop to be one of either "checkbox" or "radio".', ); } @@ -276,13 +284,12 @@ export class PostCardControl { this.groupCollectMembers(); } - componentWillLoad() { - if (!this.label) throw new Error('No label set: must have a "label".'); - if (!this.type) throw new Error('No type set: must have a "type".'); connectedCallback() { this.initialState = this.checked; } + componentWillLoad() { + this.validateControlLabel(); this.validateControlType(); } From ed5b52ec58fa1fe573ce28e9acf3e4566c9d6161 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Wed, 7 Feb 2024 14:26:44 +0100 Subject: [PATCH 24/82] chore(components): card-control remove content area --- .../post-card-control/post-card-control.scss | 149 +++++++++--------- .../post-card-control/post-card-control.tsx | 93 ++++------- 2 files changed, 103 insertions(+), 139 deletions(-) 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 5147fd3bee..8edb92be0e 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 @@ -7,126 +7,129 @@ } .card-control { - --post-card-control-bg: #{post.$white}; --post-card-control-border-color: #{post.$gray-60}; + --post-card-control-bg: #{post.$white}; --post-card-control-color: #{post.$gray-80}; + --post-card-control-input-border-color: #{post.$gray-80}; + --post-card-control-input-bg: #{post.$white}; - display: block; + display: flex; + gap: 0 post.$size-mini; padding: post.$size-regular; - width: 100%; background-color: var(--post-card-control-bg); 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: color 100ms ease-in-out, background-color 100ms ease-in-out, - border-color 100ms ease-in-out; + transition: background-color 100ms linear, border-color 100ms linear; - &:not(.is-disabled) { - &:hover { - --post-card-control-bg: #{post.$gray-10}; - --post-card-control-border-color: #{post.$black}; - --post-card-control-color: #{post.$black}; + .card-control--input { + flex: 0 0 auto; + margin: post.$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: border-color 100ms ease-in-out; + + &:focus, + &:focus-visible { + box-shadow: none; } - &.is-checked { - --post-card-control-bg: #{post.$yellow}; + ~ .card-control--label { + flex-grow: 2; + margin: post.$size-micro 0; + color: inherit !important; + pointer-events: none; + } + } + + .card-control--description { + font-size: 0.75rem; + } + + .card-control--icon { + flex: 0 0 auto; + width: post.$size-big; + height: post.$size-big; + pointer-events: none; - .card-control--content { - display: block; + > slot { + > * { + width: 100%; + height: 100%; } } + } + + &:not(.is-disabled) { + // order matters! + + &.is-checked { + --post-card-control-border-color: #{post.$black}; + --post-card-control-bg: #{post.$yellow}; + } &.is-invalid { --post-card-control-border-color: #{post.$danger}; + --post-card-control-bg: #{post.$white}; --post-card-control-color: #{post.$danger}; + --post-card-control-input-border-color: #{post.$danger}; + } - &:hover { - --post-card-control-border-color: #{post.$danger}; - --post-card-control-color: #{post.$danger}; + &.is-focused { + &:where(:has(.card-control--input:focus-visible)) { + --post-card-control-border-color: #{post.$gray-60}; + --post-card-control-bg: #{post.$white}; + --post-card-control-color: #{post.$gray-80}; + --post-card-control-input-border-color: #{post.$gray-80}; } } - } - &.is-focused { - // needed for Firefox until the :has selector is implemented correctly - // downside: outline is drawn also when clicking with the mouse on the checkbox element directly - @supports not selector(:has(.header--input:focus-visible)) { - --post-card-control-border-color: #{post.$black}; - --post-card-control-color: #{post.$black}; - - outline-offset: post.$input-focus-outline-thickness; - outline: post.$input-focus-outline-thickness solid post.$focus; + &:hover { + --post-card-control-border-color: #{post.$gray-80}; + --post-card-control-bg: #{post.$gray-60}; + --post-card-control-color: #{post.$white}; + --post-card-control-input-border-color: #{post.$black}; } + } - &:where(:has(.header--input:focus-visible)) { - --post-card-control-border-color: #{post.$black}; - --post-card-control-color: #{post.$black}; - + // show focus even if is-disabled, because aria-disabled allows focus at any moment + &.is-focused { + &:where(:has(.card-control--input:focus-visible)) { outline-offset: post.$input-focus-outline-thickness; - outline: post.$input-focus-outline-thickness solid post.$focus; + outline: post.$input-focus-outline-thickness solid post.$outline-color; } } &.is-disabled { --post-card-control-border-color: #{post.$gray-40}; + --post-card-control-bg: transparent; --post-card-control-color: #{post.$gray-40}; + --post-card-control-input-border-color: #{post.$gray-40}; + --post-card-control-input-bg: transparent; + + border-style: dashed; + text-decoration: line-through; cursor: default; - &.is-checked { - --post-card-control-bg: #{post.$gray-10}; - border-color: var(--post-card-control-bg); + .card-control--input { + border-style: dashed; } } } -.card-control--header { - display: flex; - gap: 0 post.$size-mini; - color: var(--post-card-control-color); - .header--input { - flex: 0 0 auto; - margin: post.$size-micro 0; - background-color: post.$white; - border-color: var(--post-card-control-border-color) !important; - color: inherit !important; - cursor: inherit; - transition: border-color 100ms ease-in-out; - &:focus, - &:focus-visible { - box-shadow: none; - } - ~ .header--label { - flex-grow: 2; - margin: post.$size-micro 0; - color: inherit !important; - pointer-events: none; - } - } - .header--description { - font-size: 0.75rem; - } - .header--icon { - flex: 0 0 auto; - width: post.$size-big; - height: post.$size-big; - pointer-events: none; - > slot { - > * { - width: 100%; - height: 100%; } } } } -.card-control--content { - display: none; - margin-left: post.$form-check-input-size + post.$size-mini; } diff --git a/packages/components/src/components/post-card-control/post-card-control.tsx b/packages/components/src/components/post-card-control/post-card-control.tsx index 9ab49f68bb..419f9ad77f 100644 --- a/packages/components/src/components/post-card-control/post-card-control.tsx +++ b/packages/components/src/components/post-card-control/post-card-control.tsx @@ -44,24 +44,6 @@ export class PostCardControl { focused: null, }; - private INTERACTIVE_ELEMENT_SELECTORS = [ - 'a', - 'audio[controls]', - 'button', - 'details', - 'embed', - 'iframe', - 'img[usemap]', - 'input:not([type="hidden"])', - 'keygen', - 'label', - 'menu[type="toolbar"]', - 'object[usemap]', - 'select', - 'textarea', - 'video[controls]', - ]; - private control: HTMLInputElement; private controlId = `PostCardControl_${cardControlIds++}`; @@ -133,8 +115,6 @@ export class PostCardControl { this.controlFocusHandler = this.controlFocusHandler.bind(this); this.controlKeyDownHandler = this.controlKeyDownHandler.bind(this); - this.contentClickHandler = this.contentClickHandler.bind(this); - this.groupEventHandler = this.groupEventHandler.bind(this); window.addEventListener(this.GROUPEVENT, this.groupEventHandler); @@ -182,17 +162,6 @@ export class PostCardControl { this.focused = this.host === document.activeElement; } - private contentClickHandler(e: Event) { - const testWrapper = document.createElement('div'); - testWrapper.append((e.target as HTMLElement).cloneNode()); - - const isInteractive = this.INTERACTIVE_ELEMENT_SELECTORS.some(selector => - testWrapper.querySelector(selector), - ); - - if (isInteractive) e.stopPropagation(); - } - // https://googlechromelabs.github.io/howto-components/howto-radio-group/ private controlKeyDownHandler(e: KeyboardEvent) { if (this.group.members.length > 1) { @@ -306,41 +275,33 @@ export class PostCardControl { 'is-invalid': this.state === 'false', }} > -
- (this.control = el as HTMLInputElement)} - id={this.controlId} - class="header--input form-check-input" - type={this.type} - name={this.name} - value={this.value} - checked={this.checked} - aria-disabled={this.disabled} - aria-controls={`${this.controlId}_Content`} - aria-expanded={(this.checked && !this.disabled).toString()} - onClick={this.controlClickHandler} - onChange={this.controlChangeHandler} - onFocus={this.controlFocusHandler} - onBlur={this.controlFocusHandler} - onKeyDown={this.controlKeyDownHandler} - /> - - - -
- {this.icon ? : null} -
-
- -
- + (this.control = el as HTMLInputElement)} + id={this.controlId} + class="card-control--input form-check-input" + type={this.type} + name={this.name} + value={this.value} + checked={this.checked} + aria-readonly={this.readonly} + aria-disabled={this.disabled} + aria-invalid={this.validity === 'false'} + onClick={this.controlClickHandler} + onChange={this.controlChangeHandler} + onFocus={this.controlFocusHandler} + onBlur={this.controlFocusHandler} + onKeyDown={this.controlKeyDownHandler} + /> + + + +
+ {this.icon ? : null}
From 610565eeabe1482107d1cfeea076585a78283a01 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Wed, 7 Feb 2024 14:44:53 +0100 Subject: [PATCH 25/82] chore(components): card-control add slot docs --- .../src/components/post-card-control/post-card-control.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/components/src/components/post-card-control/post-card-control.tsx b/packages/components/src/components/post-card-control/post-card-control.tsx index 419f9ad77f..ea1931c7c8 100644 --- a/packages/components/src/components/post-card-control/post-card-control.tsx +++ b/packages/components/src/components/post-card-control/post-card-control.tsx @@ -17,6 +17,9 @@ let cardControlIds = 0; /** * @class PostCardControl - representing a stencil component + * + * @slot icon - Content to place in the named `icon` slot.

Markup accepted: inline content.
It is only meant for img or svg elements and overrides the `icon` property.

+ * @slot invalid-feedback - Content to place in the named `invalid-feedback` slot.

Markup accepted: inline content.

*/ @Component({ tag: 'post-card-control', From ada227e755511e50c0e1c30ba05fe0a5f1c8599a Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Wed, 7 Feb 2024 14:51:56 +0100 Subject: [PATCH 26/82] refactor(components): refactor card-control how the checked state is set --- .../post-card-control/post-card-control.tsx | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/packages/components/src/components/post-card-control/post-card-control.tsx b/packages/components/src/components/post-card-control/post-card-control.tsx index ea1931c7c8..8cf9ef85b8 100644 --- a/packages/components/src/components/post-card-control/post-card-control.tsx +++ b/packages/components/src/components/post-card-control/post-card-control.tsx @@ -146,19 +146,12 @@ export class PostCardControl { private controlClickHandler(e: Event) { if (this.disabled) e.preventDefault(); + e.stopPropagation(); } - private controlChangeHandler() { - if (!this.disabled) { - this.checked = this.control.checked; - this.internals.setFormValue(this.control.value); - - if (this.group.members.length > 1 && this.control.checked) { - this.groupSetSelectedMember(this.control); - } - - this.controlChange.emit(this.checked); - } + private controlChangeHandler(e: Event) { + this.controlSetChecked(this.control.checked, e); + if (this.group.members.length > 1) this.groupSetSelectedMember(this.control); } private controlFocusHandler() { @@ -192,6 +185,12 @@ export class PostCardControl { } } + private controlSetChecked(checked: boolean, e?: Event) { + if (this.disabled) return; + + this.checked = this.control.checked = checked; + this.internals.setFormValue(this.checked ? this.control.value : null); + } private groupCollectMembers() { if (this.type === 'radio' && this.name) { this.group.hosts = Array.from( @@ -247,12 +246,7 @@ export class PostCardControl { private groupEventHandler(e: CustomEvent) { if (e.detail.triggeredByKeyboard) e.detail.control.focus(); - if (!this.disabled) { - this.control.checked = this.checked = this.control === e.detail.control; - this.internals.setFormValue(this.control.value); - this.controlChange.emit(this.checked); - } - + this.controlSetChecked(this.control === e.detail.control); this.groupCollectMembers(); } From c2cf3e8e2123ad55c4db2193094a8d2402062805 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Wed, 7 Feb 2024 14:57:38 +0100 Subject: [PATCH 27/82] chore(components): card-control remove custom controlChange event --- .../post-card-control/post-card-control.tsx | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/packages/components/src/components/post-card-control/post-card-control.tsx b/packages/components/src/components/post-card-control/post-card-control.tsx index 8cf9ef85b8..8fccccdaee 100644 --- a/packages/components/src/components/post-card-control/post-card-control.tsx +++ b/packages/components/src/components/post-card-control/post-card-control.tsx @@ -1,15 +1,4 @@ -import { - AttachInternals, - Component, - Element, - Event, - EventEmitter, - h, - Host, - Prop, - State, - Watch, -} from '@stencil/core'; +import { AttachInternals, Component, Element, h, Host, Prop, State, Watch } from '@stencil/core'; import { checkNonEmpty, checkOneOf } from '../../utils'; import { version } from '../../../package.json'; @@ -103,11 +92,6 @@ export class PostCardControl { */ @Prop() readonly icon: string = null; - /** - * An event emitted whenever the control value changes. - * The payload contains the current checked state under `event.details`. - */ - @Event() controlChange: EventEmitter; constructor() { this.GROUPEVENT = `PostCardControlGroup:${this.name}:change`; From c6049ab269112c05662dce19e17c74749be76eeb Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Wed, 7 Feb 2024 15:00:27 +0100 Subject: [PATCH 28/82] chore(components): card-control update props and add readonly prop --- .../post-card-control/post-card-control.tsx | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/components/src/components/post-card-control/post-card-control.tsx b/packages/components/src/components/post-card-control/post-card-control.tsx index 8fccccdaee..972570f4ac 100644 --- a/packages/components/src/components/post-card-control/post-card-control.tsx +++ b/packages/components/src/components/post-card-control/post-card-control.tsx @@ -72,19 +72,23 @@ export class PostCardControl { @Prop() readonly value: string = null; /** - * Defines the `checked` attribute of the control. If `true`, the control is selected. + * Defines the `checked` attribute of the control. If `true`, the control is selected at its value will be included in the forms data. */ @Prop({ reflect: true, mutable: true }) checked?: boolean = false; /** - * Defines the `disabled` attribute of the control. If `true`, the user can not interact with the control. + * Defines the `readonly` attribute of the control. If `true`, the user can not interact with the control, but the controls value will be included in the forms data. */ - @Prop() readonly disabled: boolean = false; + @Prop() readonly readonly: boolean = false; + /** + * Defines the `disabled` attribute of the control. If `true`, the user can not interact with the control and the controls value will not be included in the forms data. + */ + @Prop({ mutable: true }) disabled: boolean = false; /** - * Defines the validation `state` of the control.
Only styles for the invalid state have been defined so far.
+ * Defines the validation `validity` of the control. */ - @Prop() readonly state: null | 'true' | 'false' = null; + @Prop({ mutable: true }) validity: null | 'true' | 'false' = null; /** * Defines the icon `name` inside of the card. @@ -129,7 +133,7 @@ export class PostCardControl { } private controlClickHandler(e: Event) { - if (this.disabled) e.preventDefault(); + if (this.readonly || this.disabled) e.preventDefault(); e.stopPropagation(); } @@ -170,7 +174,7 @@ export class PostCardControl { } private controlSetChecked(checked: boolean, e?: Event) { - if (this.disabled) return; + if (this.readonly || this.disabled) return; this.checked = this.control.checked = checked; this.internals.setFormValue(this.checked ? this.control.value : null); @@ -250,10 +254,11 @@ export class PostCardControl { class={{ 'card-control': true, 'is-checked': this.checked, + 'is-readonly': this.readonly, 'is-disabled': this.disabled, 'is-focused': this.focused, - 'is-valid': this.state !== null && this.state !== 'false', - 'is-invalid': this.state === 'false', + 'is-valid': this.validity !== null && this.validity !== 'false', + 'is-invalid': this.validity === 'false', }} > Date: Wed, 7 Feb 2024 16:54:41 +0100 Subject: [PATCH 29/82] feat(components): card-control add form associated callbacks and correctly implement update events --- .../post-card-control/post-card-control.tsx | 81 +++++++++++++++---- .../components/post-card-control/readme.md | 37 ++++----- 2 files changed, 85 insertions(+), 33 deletions(-) diff --git a/packages/components/src/components/post-card-control/post-card-control.tsx b/packages/components/src/components/post-card-control/post-card-control.tsx index 972570f4ac..ef63bbbcf6 100644 --- a/packages/components/src/components/post-card-control/post-card-control.tsx +++ b/packages/components/src/components/post-card-control/post-card-control.tsx @@ -41,7 +41,7 @@ export class PostCardControl { @Element() host: HTMLPostCardControlElement; - @State() initialState: boolean; + @State() initialChecked: boolean; @State() focused = false; @AttachInternals() private internals: ElementInternals; @@ -62,7 +62,7 @@ export class PostCardControl { @Prop() readonly type!: 'checkbox' | 'radio'; /** - * Defines the `name` attribute of the control, which is submitted with the form data. + * Defines the `name` attribute of the control. This name is used in a forms data to store the given value of the control. If no name is specified a form will never contain this controls value. */ @Prop() readonly name: string = null; @@ -74,16 +74,12 @@ export class PostCardControl { /** * Defines the `checked` attribute of the control. If `true`, the control is selected at its value will be included in the forms data. */ - @Prop({ reflect: true, mutable: true }) checked?: boolean = false; + @Prop({ mutable: true }) checked?: boolean = false; - /** - * Defines the `readonly` attribute of the control. If `true`, the user can not interact with the control, but the controls value will be included in the forms data. - */ - @Prop() readonly readonly: boolean = false; /** * Defines the `disabled` attribute of the control. If `true`, the user can not interact with the control and the controls value will not be included in the forms data. */ - @Prop({ mutable: true }) disabled: boolean = false; + @Prop({ mutable: true }) disabled = false; /** * Defines the validation `validity` of the control. @@ -128,12 +124,22 @@ export class PostCardControl { ); } + @Watch('checked') + updateControlChecked(checked = this.checked) { + this.controlSetChecked(checked); + } + + @Watch('disabled') + updateControlDisbled() { + this.controlSetChecked(this.checked); + } + private cardClickHandler(e: Event) { if (e.target !== this.control) this.control.click(); } private controlClickHandler(e: Event) { - if (this.readonly || this.disabled) e.preventDefault(); + if (this.disabled) e.preventDefault(); e.stopPropagation(); } @@ -174,11 +180,39 @@ export class PostCardControl { } private controlSetChecked(checked: boolean, e?: Event) { - if (this.readonly || this.disabled) return; + if (e && e.type === 'input') e.stopImmediatePropagation(); + + if (this.disabled) { + this.internals.setFormValue(null); + return; + } else { + this.checked = this.control.checked = checked; + this.internals.setFormValue(this.checked ? this.control.value : null); + this.controlEmitEvent(e); + } + } + + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox + private controlEmitEvent(e?: Event) { + if (!e) return; + + const event = new CustomEvent(e.type, { + detail: { + sourceEvent: e, + target: this.control, + state: this.checked, + }, + bubbles: e.bubbles, + cancelable: e.cancelable, + composed: true, + }); - this.checked = this.control.checked = checked; - this.internals.setFormValue(this.checked ? this.control.value : null); + const isCheckbox = this.type === 'checkbox'; + const isRadioAndChecked = this.type === 'radio' && this.checked; + + if (isCheckbox || isRadioAndChecked) this.host.dispatchEvent(event); } + private groupCollectMembers() { if (this.type === 'radio' && this.name) { this.group.hosts = Array.from( @@ -239,7 +273,7 @@ export class PostCardControl { } connectedCallback() { - this.initialState = this.checked; + this.initialChecked = this.checked; } componentWillLoad() { @@ -254,7 +288,6 @@ export class PostCardControl { class={{ 'card-control': true, 'is-checked': this.checked, - 'is-readonly': this.readonly, 'is-disabled': this.disabled, 'is-focused': this.focused, 'is-valid': this.validity !== null && this.validity !== 'false', @@ -269,10 +302,10 @@ export class PostCardControl { name={this.name} value={this.value} checked={this.checked} - aria-readonly={this.readonly} aria-disabled={this.disabled} aria-invalid={this.validity === 'false'} onClick={this.controlClickHandler} + onInput={this.controlChangeHandler} onChange={this.controlChangeHandler} onFocus={this.controlFocusHandler} onBlur={this.controlFocusHandler} @@ -298,6 +331,24 @@ export class PostCardControl { this.groupCollectMembers(); } + // https://stenciljs.com/docs/form-associated + formAssociatedCallback() { + this.controlSetChecked(this.checked); + } + + formDisabledCallback(disabled: boolean) { + this.disabled = disabled; + } + + formStateRestoreCallback(checked, _reason: 'restore' | 'autocomplete') { + this.controlSetChecked(checked); + } + + formResetCallback() { + this.validity = null; + this.controlSetChecked(this.initialChecked); + } + disconnectedCallback() { window.removeEventListener(this.GROUPEVENT, this.groupEventHandler); } diff --git a/packages/components/src/components/post-card-control/readme.md b/packages/components/src/components/post-card-control/readme.md index e86ea6fcf6..71bf3155d1 100644 --- a/packages/components/src/components/post-card-control/readme.md +++ b/packages/components/src/components/post-card-control/readme.md @@ -7,24 +7,25 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| -------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | ----------- | -| `checked` | `checked` | Defines the `checked` attribute of the control. If `true`, the control is selected. | `boolean` | `false` | -| `description` | `description` | Defines the description in the control-label. | `string` | `null` | -| `disabled` | `disabled` | Defines the `disabled` attribute of the control. If `true`, the user can not interact with the control. | `boolean` | `false` | -| `icon` | `icon` | Defines the icon `name` inside of the card. If not set the icon will not show up. | `string` | `null` | -| `label` _(required)_ | `label` | Defines the text in the control-label. | `string` | `undefined` | -| `name` | `name` | Defines the `name` attribute of the control, which is submitted with the form data. | `string` | `null` | -| `state` | `state` | Defines the validation `state` of the control.
Only styles for the invalid state have been defined so far.
| `"false" \| "true"` | `null` | -| `type` _(required)_ | `type` | Defines the `type` attribute of the control. | `"checkbox" \| "radio"` | `undefined` | -| `value` | `value` | Defines the `value` attribute of the control. This is only used, when the control participates in the native `form`. | `string` | `null` | - - -## Events - -| Event | Description | Type | -| --------------- | -------------------------------------------------------------------------------------------------------------------------- | ---------------------- | -| `controlChange` | An event emitted whenever the control value changes. The payload contains the current checked state under `event.details`. | `CustomEvent` | +| Property | Attribute | Description | Type | Default | +| -------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | ----------- | +| `checked` | `checked` | Defines the `checked` attribute of the control. If `true`, the control is selected at its value will be included in the forms data. | `boolean` | `false` | +| `description` | `description` | Defines the description in the control-label. | `string` | `null` | +| `disabled` | `disabled` | Defines the `disabled` attribute of the control. If `true`, the user can not interact with the control and the controls value will not be included in the forms data. | `boolean` | `false` | +| `icon` | `icon` | Defines the icon `name` inside of the card. If not set the icon will not show up. | `string` | `null` | +| `label` _(required)_ | `label` | Defines the text in the control-label. | `string` | `undefined` | +| `name` | `name` | Defines the `name` attribute of the control. This name is used in a forms data to store the given value of the control. If no name is specified a form will never contain this controls value. | `string` | `null` | +| `type` _(required)_ | `type` | Defines the `type` attribute of the control. | `"checkbox" \| "radio"` | `undefined` | +| `validity` | `validity` | Defines the validation `validity` of the control. | `"false" \| "true"` | `null` | +| `value` | `value` | Defines the `value` attribute of the control. This is only used, when the control participates in the native `form`. | `string` | `null` | + + +## Slots + +| Slot | Description | +| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `"icon"` | Content to place in the named `icon` slot.

Markup accepted: inline content.
It is only meant for img or svg elements and overrides the `icon` property.

| +| `"invalid-feedback"` | Content to place in the named `invalid-feedback` slot.

Markup accepted: inline content.

| ## Dependencies From f60ebd518c3993bb6ec84a1805c82315a375277d Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Wed, 7 Feb 2024 16:55:12 +0100 Subject: [PATCH 30/82] feat(components): card-control add dark-bg styles --- .../post-card-control/post-card-control.scss | 57 ++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) 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 8edb92be0e..41e086713f 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 @@ -119,16 +119,71 @@ border-style: dashed; } } -} + :host-context(:is(#{post.$dark-backgrounds})) & { + --post-card-control-border-color: #{post.$white}; + --post-card-control-bg: transparent; + --post-card-control-color: #{post.$white}; + --post-card-control-input-border-color: #{post.$white}; + --post-card-control-input-bg: transparent; + &:not(.is-disabled) { + // order matters! + &.is-checked { + --post-card-control-border-color: #{post.$yellow}; + --post-card-control-bg: #{post.$yellow}; + --post-card-control-color: #{post.$gray-80}; + --post-card-control-input-border-color: #{post.$gray-80}; + --post-card-control-input-bg: #{post.$white}; + &:hover { + --post-card-control-border-color: #{post.$black}; + --post-card-control-bg: #{post.$gray-20}; + } + } + &.is-invalid { + --post-card-control-border-color: #{post.$danger}; + --post-card-control-bg: #{post.$error-background}; + --post-card-control-color: #{post.$danger}; + --post-card-control-input-border-color: #{post.$danger}; + --post-card-control-input-bg: #{post.$white}; + } + &.is-focused { + &:where(:has(.card-control--input:focus-visible)) { + --post-card-control-border-color: #{post.$white}; + --post-card-control-bg: transparent; + --post-card-control-color: #{post.$white}; + --post-card-control-input-border-color: #{post.$white}; + --post-card-control-input-bg: transparent; + } + } + &:hover { + --post-card-control-border-color: #{post.$white}; + --post-card-control-bg: #{post.$yellow}; + --post-card-control-color: #{post.$black}; + --post-card-control-input-border-color: #{post.$black}; + --post-card-control-input-bg: #{post.$white}; } } + + // show focus even if is-disabled, because aria-disabled allows focus at any moment + &.is-focused { + &:where(:has(.card-control--input:focus-visible)) { + outline-color: post.$white; + } + } + + &.is-disabled { + --post-card-control-border-color: #{post.$gray-20}; + --post-card-control-bg: transparent; + --post-card-control-color: #{post.$gray-20}; + --post-card-control-input-border-color: #{post.$gray-20}; + --post-card-control-input-bg: transparent; + } } } From eb91b5681ab1df30982a09d82c3478d7056cf10f Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Wed, 7 Feb 2024 16:56:10 +0100 Subject: [PATCH 31/82] feat(documentation): card-control add stories --- .../card-control/card-control.docs.mdx | 25 ++- .../card-control/card-control.module.scss | 13 ++ .../card-control/card-control.stories.ts | 174 ++++++++++++------ 3 files changed, 147 insertions(+), 65 deletions(-) create mode 100644 packages/documentation/src/stories/components/card-control/card-control.module.scss diff --git a/packages/documentation/src/stories/components/card-control/card-control.docs.mdx b/packages/documentation/src/stories/components/card-control/card-control.docs.mdx index a949e9a4c9..bcf1c246b8 100644 --- a/packages/documentation/src/stories/components/card-control/card-control.docs.mdx +++ b/packages/documentation/src/stories/components/card-control/card-control.docs.mdx @@ -17,20 +17,37 @@ The `` element is part of the `@swisspost/design-system-compo ## Examples +### On dark background + + + +
+ +
+ ### 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 `icon` slot and add your very own custom icon. +If this is not enough, you can also use the named `icon` slot and add your very own custom icon.
If you use the `icon` slot, 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.
-### With content +### Invalid Feedback -The component accepts other HTML-elements as its content, this includes also other form elements. +If you need to give more information about an error occured on the ``, you can make use of the named `invalid-feedback` slot. - + + +### 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. + + +
+ +
### Lined up diff --git a/packages/documentation/src/stories/components/card-control/card-control.module.scss b/packages/documentation/src/stories/components/card-control/card-control.module.scss new file mode 100644 index 0000000000..2f116295ff --- /dev/null +++ b/packages/documentation/src/stories/components/card-control/card-control.module.scss @@ -0,0 +1,13 @@ +@use '@swisspost/design-system-styles/core' as post; + +:export { + @each $color, $value in post.$background-colors { + background_#{$color}: $value; + + @if (post.light-or-dark($value) == 'dark') { + dark_#{$color}: $value; + } @else { + light_#{$color}: $value; + } + } +} diff --git a/packages/documentation/src/stories/components/card-control/card-control.stories.ts b/packages/documentation/src/stories/components/card-control/card-control.stories.ts index 02fbc55072..48a780d25d 100644 --- a/packages/documentation/src/stories/components/card-control/card-control.stories.ts +++ b/packages/documentation/src/stories/components/card-control/card-control.stories.ts @@ -3,13 +3,16 @@ import { Args, Meta, StoryContext, StoryObj } from '@storybook/web-components'; import { BADGE } from '../../../../.storybook/constants'; import { html, nothing } from 'lit'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; -import { PostCardControlCustomEvent } from '@swisspost/design-system-components'; +import { parse } from '../../../utils/sass-export'; +import scss from './card-control.module.scss'; + +const SCSS_VARIABLES = parse(scss); const meta: Meta = { title: 'Components/Forms/Card-Control', component: 'post-card-control', parameters: { - badges: [BADGE.NEEDS_REVISION], + badges: [BADGE.NEEDS_REVISION, BADGE.SINCE_V1], }, args: { label: 'Label', @@ -19,10 +22,10 @@ const meta: Meta = { value: '', checked: '', disabled: '', - state: 'null', - icon: '', - slotDefault: '', + validity: 'null', + icon: '1001', slotIcon: '', + slotInvalidFeedback: '', }, argTypes: { type: { @@ -35,7 +38,7 @@ const meta: Meta = { }, options: ['checkbox', 'radio'], }, - state: { + validity: { control: { type: 'radio', labels: { @@ -51,26 +54,14 @@ const meta: Meta = { }, }, }, - slotDefault: { - name: 'default', - description: - 'Content to place in the default slot.
Content only gets rendered if the control is checked and is not disabled.

Markup accepted: block content.
This means, you can put everything in it, as long as there is enough space to render it.

', + slotIcon: { table: { - category: 'Slots', - type: { - summary: null, - }, + disable: true, }, }, - slotIcon: { - name: 'icon', - description: - 'Content to place in the named `icon` slot.

Markup accepted: inline content.
It is only meant for img or svg elements and overrides the `icon` property.

', + slotInvalidFeedback: { table: { - category: 'Slots', - type: { - summary: 'html', - }, + disable: true, }, }, }, @@ -84,31 +75,59 @@ export const Default: Story = { render: (args: Args) => { const [, updateArgs] = useArgs(); - const icon = html` -
${unsafeHTML(args.slotIcon)}
- `; + const icon = html`${unsafeHTML(args.slotIcon)} `; + const invalidFeedback = html` + ${unsafeHTML(args.slotInvalidFeedback)} + `; return html` - ${args.slotIcon ? icon : null} ${unsafeHTML(args.slotDefault)} + ${args.slotIcon ? icon : null} ${args.slotInvalidFeedback ? invalidFeedback : null} `; }, }; +export const DarkBackground: Story = { + parameters: { + docs: { + controls: { + include: ['Background-Color', 'checked', 'disabled', 'validity'], + }, + }, + }, + decorators: [ + (story, context) => html`
${story()}
`, + ], + 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, +}; + export const CustomIcon: Story = { args: { slotIcon: @@ -117,40 +136,73 @@ export const CustomIcon: Story = { render: Default.render, }; -export const Content: Story = { +export const InvalidFeedback: Story = { args: { - slotDefault: `

Textus additione velit esse molestie consequat, vel - illum dolore eu feugiat nulla.

-
    -
  • Listus mixtus volare pagare la mare.
  • -
  • Elare volare cantare hendrerit in vulputate velit esse molestie consequat, vel - illum dolore eu feugiat.
  • -
- -
- - - - -
- Hintus textus elare volare cantare hendrerit in vulputate velit esse molestie consequat, vel - illum dolore eu feugiat nulla facilisis. -
-
- -
- - -
`, - checked: true, + validity: 'false', + slotInvalidFeedback: 'Error Message', }, render: Default.render, }; +export const FormIntegration: Story = { + parameters: { + docs: { + controls: { + include: ['Fieldset disabled', 'checked', 'disabled'], + }, + }, + }, + args: { + fieldsetDisabled: false, + name: 'card-control', + }, + argTypes: { + fieldsetDisabled: { + name: 'Fieldset disabled', + description: + 'If a wrapping `fieldset` element is disabled, the `` will behave like disabled as well.', + control: { + type: 'boolean', + }, + }, + }, + decorators: [ + story => html` + ${story()} +
+

FormData

+
{}
+
+ `, + ], + render: (args: Args, context: StoryContext) => { + return html` +
+ Legend + ${Default.render?.(args, context)} +
+
+ + +
+ `; + }, +}; + +function formHandler(e: any) { + if (e.type === 'submit') e.preventDefault(); + + setTimeout(() => { + const formOutput = document.querySelector('#AssociatedFormOutput'); + const formData = 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); + }); +} + export const LinedUp: Story = { parameters: { controls: { @@ -184,7 +236,7 @@ export const LinedUp: Story = { description: i === 6 ? '20.- per year' : null, type: args.type, disabled: i === 3, - state: args.state, + validity: args.validity, }, context, )} @@ -207,7 +259,7 @@ export const RadioGroup: Story = { type: 'radio', name: 'RadioGroup_Name', disabled: i > 3 && i < 6 ? true : null, - state: args.state, + validity: args.validity, }, context, )} From 1bd387981f749bc9d97438c63bcc30da36b4a0c8 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Wed, 7 Feb 2024 17:03:35 +0100 Subject: [PATCH 32/82] chore(components): card-control remove stories which will be included in other tasks --- packages/components/src/components.d.ts | 47 ++++++------------- .../post-card-control/post-card-control.scss | 6 +-- .../post-card-control/post-card-control.tsx | 1 - .../card-control/card-control.docs.mdx | 6 --- .../card-control/card-control.stories.ts | 8 ---- 5 files changed, 16 insertions(+), 52 deletions(-) diff --git a/packages/components/src/components.d.ts b/packages/components/src/components.d.ts index 0b0fc9b154..9e178a68f3 100644 --- a/packages/components/src/components.d.ts +++ b/packages/components/src/components.d.ts @@ -75,7 +75,7 @@ export namespace Components { */ interface PostCardControl { /** - * Defines the `checked` attribute of the control. If `true`, the control is selected. + * Defines the `checked` attribute of the control. If `true`, the control is selected at its value will be included in the forms data. */ "checked"?: boolean; /** @@ -83,7 +83,7 @@ export namespace Components { */ "description": string; /** - * Defines the `disabled` attribute of the control. If `true`, the user can not interact with the control. + * Defines the `disabled` attribute of the control. If `true`, the user can not interact with the control and the controls value will not be included in the forms data. */ "disabled": boolean; /** @@ -95,17 +95,17 @@ export namespace Components { */ "label": string; /** - * Defines the `name` attribute of the control, which is submitted with the form data. + * Defines the `name` attribute of the control. This name is used in a forms data to store the given value of the control. If no name is specified a form will never contain this controls value. */ "name": string; - /** - * Defines the validation `state` of the control.
Only styles for the invalid state have been defined so far.
- */ - "state": null | 'true' | 'false'; /** * Defines the `type` attribute of the control. */ "type": 'checkbox' | 'radio'; + /** + * Defines the validation `validity` of the control. + */ + "validity": null | 'true' | 'false'; /** * Defines the `value` attribute of the control. This is only used, when the control participates in the native `form`. */ @@ -260,10 +260,6 @@ export interface PostAlertCustomEvent extends CustomEvent { detail: T; target: HTMLPostAlertElement; } -export interface PostCardControlCustomEvent extends CustomEvent { - detail: T; - target: HTMLPostCardControlElement; -} export interface PostCollapsibleCustomEvent extends CustomEvent { detail: T; target: HTMLPostCollapsibleElement; @@ -306,21 +302,10 @@ declare global { prototype: HTMLPostAlertElement; new (): HTMLPostAlertElement; }; - interface HTMLPostCardControlElementEventMap { - "controlChange": boolean; - } /** * @class PostCardControl - representing a stencil component */ interface HTMLPostCardControlElement extends Components.PostCardControl, HTMLStencilElement { - addEventListener(type: K, listener: (this: HTMLPostCardControlElement, ev: PostCardControlCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; - removeEventListener(type: K, listener: (this: HTMLPostCardControlElement, ev: PostCardControlCustomEvent) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; } var HTMLPostCardControlElement: { prototype: HTMLPostCardControlElement; @@ -473,7 +458,7 @@ declare namespace LocalJSX { */ interface PostCardControl { /** - * Defines the `checked` attribute of the control. If `true`, the control is selected. + * Defines the `checked` attribute of the control. If `true`, the control is selected at its value will be included in the forms data. */ "checked"?: boolean; /** @@ -481,7 +466,7 @@ declare namespace LocalJSX { */ "description"?: string; /** - * Defines the `disabled` attribute of the control. If `true`, the user can not interact with the control. + * Defines the `disabled` attribute of the control. If `true`, the user can not interact with the control and the controls value will not be included in the forms data. */ "disabled"?: boolean; /** @@ -493,21 +478,17 @@ declare namespace LocalJSX { */ "label": string; /** - * Defines the `name` attribute of the control, which is submitted with the form data. + * Defines the `name` attribute of the control. This name is used in a forms data to store the given value of the control. If no name is specified a form will never contain this controls value. */ "name"?: string; - /** - * An event emitted whenever the control value changes. The payload contains the current checked state under `event.details`. - */ - "onControlChange"?: (event: PostCardControlCustomEvent) => void; - /** - * Defines the validation `state` of the control.
Only styles for the invalid state have been defined so far.
- */ - "state"?: null | 'true' | 'false'; /** * Defines the `type` attribute of the control. */ "type": 'checkbox' | 'radio'; + /** + * Defines the validation `validity` of the control. + */ + "validity"?: null | 'true' | 'false'; /** * Defines the `value` attribute of the control. This is only used, when the control participates in the native `form`. */ 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 41e086713f..0985a26b99 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 @@ -2,8 +2,8 @@ @use '@swisspost/design-system-styles/components/form-check'; :host { - display: flex; - height: 100%; + display: block; + width: 100%; } .card-control { @@ -186,5 +186,3 @@ } } } - -} diff --git a/packages/components/src/components/post-card-control/post-card-control.tsx b/packages/components/src/components/post-card-control/post-card-control.tsx index ef63bbbcf6..2083d54ca8 100644 --- a/packages/components/src/components/post-card-control/post-card-control.tsx +++ b/packages/components/src/components/post-card-control/post-card-control.tsx @@ -92,7 +92,6 @@ export class PostCardControl { */ @Prop() readonly icon: string = null; - constructor() { this.GROUPEVENT = `PostCardControlGroup:${this.name}:change`; diff --git a/packages/documentation/src/stories/components/card-control/card-control.docs.mdx b/packages/documentation/src/stories/components/card-control/card-control.docs.mdx index bcf1c246b8..28362321ad 100644 --- a/packages/documentation/src/stories/components/card-control/card-control.docs.mdx +++ b/packages/documentation/src/stories/components/card-control/card-control.docs.mdx @@ -34,12 +34,6 @@ If this is not enough, you can also use the named `icon` slot and add your very -### Invalid Feedback - -If you need to give more information about an error occured on the ``, you can make use of the named `invalid-feedback` slot. - - - ### 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. diff --git a/packages/documentation/src/stories/components/card-control/card-control.stories.ts b/packages/documentation/src/stories/components/card-control/card-control.stories.ts index 48a780d25d..6ee5dda659 100644 --- a/packages/documentation/src/stories/components/card-control/card-control.stories.ts +++ b/packages/documentation/src/stories/components/card-control/card-control.stories.ts @@ -136,14 +136,6 @@ export const CustomIcon: Story = { render: Default.render, }; -export const InvalidFeedback: Story = { - args: { - validity: 'false', - slotInvalidFeedback: 'Error Message', - }, - render: Default.render, -}; - export const FormIntegration: Story = { parameters: { docs: { From 99397b5d8182622bb52334942886e9c03c6e2a23 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Wed, 7 Feb 2024 17:19:53 +0100 Subject: [PATCH 33/82] chore(components): card-component update property types --- packages/components/src/components.d.ts | 4 ++-- .../post-card-control/post-card-control.tsx | 8 +++---- .../components/post-card-control/readme.md | 23 ++++++++++--------- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/packages/components/src/components.d.ts b/packages/components/src/components.d.ts index 9e178a68f3..fe0692b899 100644 --- a/packages/components/src/components.d.ts +++ b/packages/components/src/components.d.ts @@ -105,7 +105,7 @@ export namespace Components { /** * Defines the validation `validity` of the control. */ - "validity": null | 'true' | 'false'; + "validity": null | true | false; /** * Defines the `value` attribute of the control. This is only used, when the control participates in the native `form`. */ @@ -488,7 +488,7 @@ declare namespace LocalJSX { /** * Defines the validation `validity` of the control. */ - "validity"?: null | 'true' | 'false'; + "validity"?: null | true | false; /** * Defines the `value` attribute of the control. This is only used, when the control participates in the native `form`. */ diff --git a/packages/components/src/components/post-card-control/post-card-control.tsx b/packages/components/src/components/post-card-control/post-card-control.tsx index 2083d54ca8..4e3718edef 100644 --- a/packages/components/src/components/post-card-control/post-card-control.tsx +++ b/packages/components/src/components/post-card-control/post-card-control.tsx @@ -84,7 +84,7 @@ export class PostCardControl { /** * Defines the validation `validity` of the control. */ - @Prop({ mutable: true }) validity: null | 'true' | 'false' = null; + @Prop({ mutable: true }) validity: null | true | false = null; /** * Defines the icon `name` inside of the card. @@ -289,8 +289,8 @@ export class PostCardControl { 'is-checked': this.checked, 'is-disabled': this.disabled, 'is-focused': this.focused, - 'is-valid': this.validity !== null && this.validity !== 'false', - 'is-invalid': this.validity === 'false', + 'is-valid': this.validity !== null && this.validity !== false, + 'is-invalid': this.validity === false, }} > If not set the icon will not show up. | `string` | `null` | -| `label` _(required)_ | `label` | Defines the text in the control-label. | `string` | `undefined` | -| `name` | `name` | Defines the `name` attribute of the control. This name is used in a forms data to store the given value of the control. If no name is specified a form will never contain this controls value. | `string` | `null` | -| `type` _(required)_ | `type` | Defines the `type` attribute of the control. | `"checkbox" \| "radio"` | `undefined` | -| `validity` | `validity` | Defines the validation `validity` of the control. | `"false" \| "true"` | `null` | -| `value` | `value` | Defines the `value` attribute of the control. This is only used, when the control participates in the native `form`. | `string` | `null` | +| Property | Attribute | Description | Type | Default | +| -------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | ----------- | +| `checked` | `checked` | Defines the `checked` attribute of the control. If `true`, the control is selected at its value will be included in the forms data. | `boolean` | `false` | +| `description` | `description` | Defines the description in the control-label. | `string` | `null` | +| `disabled` | `disabled` | Defines the `disabled` attribute of the control. If `true`, the user can not interact with the control and the controls value will not be included in the forms data. | `boolean` | `false` | +| `icon` | `icon` | Defines the icon `name` inside of the card. If not set the icon will not show up. | `string` | `null` | +| `label` _(required)_ | `label` | Defines the text in the control-label. | `string` | `undefined` | +| `name` | `name` | Defines the `name` attribute of the control. This name is used in a forms data to store the given value of the control. If no name is specified a form will never contain this controls value. | `string` | `null` | +| `type` _(required)_ | `type` | Defines the `type` attribute of the control. | `"checkbox" \| "radio"` | `undefined` | +| `validity` | `validity` | Defines the validation `validity` of the control. | `boolean` | `null` | +| `validityMessage` | `validity-message` | Defines the validation `validity-message` of the control. This parameter is only optional if validity is `true`. | `string` | `''` | +| `value` | `value` | Defines the `value` attribute of the control. This is only used, when the control participates in the native `form`. | `string` | `null` | ## Slots From 4532d6b215f34646da6badbb961e426f04a94840 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Thu, 8 Feb 2024 22:10:21 +0100 Subject: [PATCH 34/82] chore(components): card-component implement native like custom events (input and change) --- packages/components/src/components.d.ts | 24 ++++++++++++ .../post-card-control/post-card-control.tsx | 39 ++++++++++++------- .../components/post-card-control/readme.md | 31 +++++++++------ .../card-control/card-control.stories.ts | 2 +- 4 files changed, 70 insertions(+), 26 deletions(-) diff --git a/packages/components/src/components.d.ts b/packages/components/src/components.d.ts index fe0692b899..561969de19 100644 --- a/packages/components/src/components.d.ts +++ b/packages/components/src/components.d.ts @@ -260,6 +260,10 @@ export interface PostAlertCustomEvent extends CustomEvent { detail: T; target: HTMLPostAlertElement; } +export interface PostCardControlCustomEvent extends CustomEvent { + detail: T; + target: HTMLPostCardControlElement; +} export interface PostCollapsibleCustomEvent extends CustomEvent { detail: T; target: HTMLPostCollapsibleElement; @@ -302,10 +306,22 @@ declare global { prototype: HTMLPostAlertElement; new (): HTMLPostAlertElement; }; + interface HTMLPostCardControlElementEventMap { + "input": boolean; + "change": boolean; + } /** * @class PostCardControl - representing a stencil component */ interface HTMLPostCardControlElement extends Components.PostCardControl, HTMLStencilElement { + addEventListener(type: K, listener: (this: HTMLPostCardControlElement, ev: PostCardControlCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; + removeEventListener(type: K, listener: (this: HTMLPostCardControlElement, ev: PostCardControlCustomEvent) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; } var HTMLPostCardControlElement: { prototype: HTMLPostCardControlElement; @@ -481,6 +497,14 @@ declare namespace LocalJSX { * Defines the `name` attribute of the control. This name is used in a forms data to store the given value of the control. If no name is specified a form will never contain this controls value. */ "name"?: string; + /** + * An event emitted whenever the components checked state is toggled. The event payload (emitted under `event.detail.state`) is a boolean: `true` if the component is checked, `false` if it is unchecked. If the component is used with type `radio`, it will only emit this event, when the checked state is changing to `true`. + */ + "onChange"?: (event: PostCardControlCustomEvent) => void; + /** + * An event emitted whenever the components checked state is toggled. The event payload (emitted under `event.detail.state`) is a boolean: `true` if the component is checked, `false` if it is unchecked. + */ + "onInput"?: (event: PostCardControlCustomEvent) => void; /** * Defines the `type` attribute of the control. */ diff --git a/packages/components/src/components/post-card-control/post-card-control.tsx b/packages/components/src/components/post-card-control/post-card-control.tsx index 4e3718edef..3e70eb87de 100644 --- a/packages/components/src/components/post-card-control/post-card-control.tsx +++ b/packages/components/src/components/post-card-control/post-card-control.tsx @@ -1,4 +1,15 @@ -import { AttachInternals, Component, Element, h, Host, Prop, State, Watch } from '@stencil/core'; +import { + AttachInternals, + Component, + Element, + Event, + EventEmitter, + h, + Host, + Prop, + State, + Watch, +} from '@stencil/core'; import { checkNonEmpty, checkOneOf } from '../../utils'; import { version } from '../../../package.json'; @@ -92,6 +103,19 @@ export class PostCardControl { */ @Prop() readonly icon: string = null; + /** + * An event emitted whenever the components checked state is toggled. + * The event payload (emitted under `event.detail.state`) is a boolean: `true` if the component is checked, `false` if it is unchecked. + */ + @Event() input: EventEmitter; + + /** + * An event emitted whenever the components checked state is toggled. + * The event payload (emitted under `event.detail.state`) is a boolean: `true` if the component is checked, `false` if it is unchecked. + * If the component is used with type `radio`, it will only emit this event, when the checked state is changing to `true`. + */ + @Event() change: EventEmitter; + constructor() { this.GROUPEVENT = `PostCardControlGroup:${this.name}:change`; @@ -195,21 +219,10 @@ export class PostCardControl { private controlEmitEvent(e?: Event) { if (!e) return; - const event = new CustomEvent(e.type, { - detail: { - sourceEvent: e, - target: this.control, - state: this.checked, - }, - bubbles: e.bubbles, - cancelable: e.cancelable, - composed: true, - }); - const isCheckbox = this.type === 'checkbox'; const isRadioAndChecked = this.type === 'radio' && this.checked; - if (isCheckbox || isRadioAndChecked) this.host.dispatchEvent(event); + if (isCheckbox || isRadioAndChecked) this[e.type].emit({ state: this.checked }); } private groupCollectMembers() { diff --git a/packages/components/src/components/post-card-control/readme.md b/packages/components/src/components/post-card-control/readme.md index 4fcb9512a5..d9bc1c64bd 100644 --- a/packages/components/src/components/post-card-control/readme.md +++ b/packages/components/src/components/post-card-control/readme.md @@ -7,18 +7,25 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| -------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | ----------- | -| `checked` | `checked` | Defines the `checked` attribute of the control. If `true`, the control is selected at its value will be included in the forms data. | `boolean` | `false` | -| `description` | `description` | Defines the description in the control-label. | `string` | `null` | -| `disabled` | `disabled` | Defines the `disabled` attribute of the control. If `true`, the user can not interact with the control and the controls value will not be included in the forms data. | `boolean` | `false` | -| `icon` | `icon` | Defines the icon `name` inside of the card. If not set the icon will not show up. | `string` | `null` | -| `label` _(required)_ | `label` | Defines the text in the control-label. | `string` | `undefined` | -| `name` | `name` | Defines the `name` attribute of the control. This name is used in a forms data to store the given value of the control. If no name is specified a form will never contain this controls value. | `string` | `null` | -| `type` _(required)_ | `type` | Defines the `type` attribute of the control. | `"checkbox" \| "radio"` | `undefined` | -| `validity` | `validity` | Defines the validation `validity` of the control. | `boolean` | `null` | -| `validityMessage` | `validity-message` | Defines the validation `validity-message` of the control. This parameter is only optional if validity is `true`. | `string` | `''` | -| `value` | `value` | Defines the `value` attribute of the control. This is only used, when the control participates in the native `form`. | `string` | `null` | +| Property | Attribute | Description | Type | Default | +| -------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | ----------- | +| `checked` | `checked` | Defines the `checked` attribute of the control. If `true`, the control is selected at its value will be included in the forms data. | `boolean` | `false` | +| `description` | `description` | Defines the description in the control-label. | `string` | `null` | +| `disabled` | `disabled` | Defines the `disabled` attribute of the control. If `true`, the user can not interact with the control and the controls value will not be included in the forms data. | `boolean` | `false` | +| `icon` | `icon` | Defines the icon `name` inside of the card. If not set the icon will not show up. | `string` | `null` | +| `label` _(required)_ | `label` | Defines the text in the control-label. | `string` | `undefined` | +| `name` | `name` | Defines the `name` attribute of the control. This name is used in a forms data to store the given value of the control. If no name is specified a form will never contain this controls value. | `string` | `null` | +| `type` _(required)_ | `type` | Defines the `type` attribute of the control. | `"checkbox" \| "radio"` | `undefined` | +| `validity` | `validity` | Defines the validation `validity` of the control. | `boolean` | `null` | +| `value` | `value` | Defines the `value` attribute of the control. This is only used, when the control participates in the native `form`. | `string` | `null` | + + +## Events + +| Event | Description | Type | +| -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | +| `change` | An event emitted whenever the components checked state is toggled. The event payload (emitted under `event.detail.state`) is a boolean: `true` if the component is checked, `false` if it is unchecked. If the component is used with type `radio`, it will only emit this event, when the checked state is changing to `true`. | `CustomEvent` | +| `input` | An event emitted whenever the components checked state is toggled. The event payload (emitted under `event.detail.state`) is a boolean: `true` if the component is checked, `false` if it is unchecked. | `CustomEvent` | ## Slots diff --git a/packages/documentation/src/stories/components/card-control/card-control.stories.ts b/packages/documentation/src/stories/components/card-control/card-control.stories.ts index 6ee5dda659..04e03affad 100644 --- a/packages/documentation/src/stories/components/card-control/card-control.stories.ts +++ b/packages/documentation/src/stories/components/card-control/card-control.stories.ts @@ -23,7 +23,7 @@ const meta: Meta = { checked: '', disabled: '', validity: 'null', - icon: '1001', + icon: '', slotIcon: '', slotInvalidFeedback: '', }, From 9ab71007a32e1024f1f4b9d3340f4c06ff86cd51 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Thu, 8 Feb 2024 22:11:38 +0100 Subject: [PATCH 35/82] chore(styles): remove wrongly added $focus variable (instead $outline-color should be used) --- packages/styles/src/variables/_color.scss | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/styles/src/variables/_color.scss b/packages/styles/src/variables/_color.scss index 6fd9ddb21d..aeade15ddd 100644 --- a/packages/styles/src/variables/_color.scss +++ b/packages/styles/src/variables/_color.scss @@ -9,9 +9,6 @@ // The one and only $yellow: #fc0; -// Custom colors -$focus: #1976c8; - // Grayscale $gray-background: #f4f3f1; $gray-background-light: #faf9f8; From 167e0c7a8e7ba68d64db681f2710370775e1571c Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Thu, 8 Feb 2024 22:16:17 +0100 Subject: [PATCH 36/82] test(components): update card-control e2e tests --- ...{control-card.cy.ts => card-control.cy.ts} | 234 +++++++++--------- 1 file changed, 117 insertions(+), 117 deletions(-) rename packages/components/cypress/e2e/{control-card.cy.ts => card-control.cy.ts} (53%) diff --git a/packages/components/cypress/e2e/control-card.cy.ts b/packages/components/cypress/e2e/card-control.cy.ts similarity index 53% rename from packages/components/cypress/e2e/control-card.cy.ts rename to packages/components/cypress/e2e/card-control.cy.ts index 29a6498434..94a57db1da 100644 --- a/packages/components/cypress/e2e/control-card.cy.ts +++ b/packages/components/cypress/e2e/card-control.cy.ts @@ -1,42 +1,18 @@ -const INTERACTIVE_ELEMENT_SELECTORS = [ - 'a', - 'audio[controls]', - 'button', - 'details', - 'embed', - 'iframe', - 'img[usemap]', - 'input:not([type="hidden"])', - 'keygen', - 'label', - 'menu[type="toolbar"]', - 'object[usemap]', - 'select', - 'textarea', - 'video[controls]', -]; - describe('card-control', () => { - describe('structure & props', () => { - beforeEach(() => { - cy.getComponent('card-control', { group: 'forms' }); - cy.window().then(win => { - cy.wrap(cy.spy(win.console, 'error')).as('consoleError'); - }); - - cy.get('@card-control').find('.card-control').as('wrapper'); - - cy.get('@card-control').find('input.header--input').as('input'); - - cy.get('@card-control').find('label.header--label').as('label'); - - cy.get('@card-control').find('.header--icon').as('icon'); - cy.get('@card-control').find('.header--icon slot[name="icon"]').as('slotIcon'); - - cy.get('@card-control').find('.card-control--content').as('content'); - cy.get('@card-control').find('.card-control--content slot').as('slotContent'); + beforeEach(() => { + cy.getComponent('card-control', { group: 'forms' }); + cy.window().then(win => { + cy.wrap(cy.spy(win.console, 'error')).as('consoleError'); }); + cy.get('@card-control').find('.card-control').as('wrapper'); + cy.get('@card-control').find('input.card-control--input').as('input'); + cy.get('@card-control').find('label.card-control--label').as('label'); + cy.get('@card-control').find('.card-control--icon').as('icon'); + cy.get('@card-control').find('.card-control--icon slot[name="icon"]').as('slotIcon'); + }); + + describe.skip('structure & props', () => { it('should have no console errors', () => { cy.get('@consoleError').should('not.be.called'); }); @@ -50,9 +26,6 @@ describe('card-control', () => { cy.get('@icon').should('exist'); cy.get('@slotIcon').should('exist'); - - cy.get('@content').should('exist'); - cy.get('@slotContent').should('exist'); }); it('should have mandatory attributes', () => { @@ -62,44 +35,40 @@ describe('card-control', () => { cy.get('@input').then($input => { const controlId = $input.attr('id'); - const contentId = `${controlId}_Content`; cy.get('@input') .should('have.attr', 'id') .and('contain', 'PostCardControl_') .and('eq', controlId); cy.get('@input').should('have.attr', 'type').and('eq', 'checkbox'); - cy.get('@input').should('have.attr', 'aria-controls').and('eq', contentId); - cy.get('@input').should('have.attr', 'aria-expanded').and('eq', 'false'); cy.get('@label').should('have.attr', 'for').and('eq', controlId); - - cy.get('@content').should('have.attr', 'id').and('eq', contentId); }); }); - it('should have mandatory content', () => { - cy.get('@label').should('have.text', 'Label'); - cy.get('@content').should('not.be.visible'); - }); - it('should set label text according to "label" prop', () => { + cy.get('@label').should('have.text', 'Label'); cy.get('@card-control').invoke('attr', 'label', 'Lorem ipsum'); cy.get('@label').should('have.text', 'Lorem ipsum'); }); - it('should render label also with empty string', () => { + it('should render label also with empty string, but log an error', () => { cy.get('@card-control').invoke('attr', 'label', ''); cy.get('@label').should('exist').and('have.text', ''); + cy.get('@consoleError') + .invoke('getCalls') + .then(calls => { + expect(calls[0].args[0].message).to.eq( + 'The "post-card-control" element requires its "label" property to be set.', + ); + }); }); - // TODO: test error message when label not set - it('should set description text according to "description" prop', () => { - cy.get('@card-control').find('.header--description').should('not.exist'); + cy.get('@card-control').find('.card-control--description').should('not.exist'); cy.get('@card-control') .invoke('attr', 'description', 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr.') - .find('.header--description') + .find('.card-control--description') .should('exist') .and('have.text', 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr.'); }); @@ -107,12 +76,8 @@ describe('card-control', () => { it('should only render description, when "description" prop is not an empty string', () => { cy.get('@card-control') .invoke('attr', 'description', '') - .find('.header--description') + .find('.card-control--description') .should('not.exist'); - - cy.get('@card-control') - .invoke('removeAttr', 'description') - .should('not.have.attr', 'description'); }); it('should set input "type" attr according to "type" prop', () => { @@ -122,7 +87,16 @@ describe('card-control', () => { cy.get('@input').should('have.attr', 'type').and('eq', 'checkbox'); }); - // TODO: test error message when type not set + it('should log an error when "type" prop is not defined', () => { + cy.get('@card-control').invoke('attr', 'type', ''); + cy.get('@consoleError') + .invoke('getCalls') + .then(calls => { + expect(calls[0].args[0].message).to.eq( + 'The "post-card-control" element requires its "type" prop to be one of either "checkbox" or "radio".', + ); + }); + }); it('should set input "name" attr according to "name" prop', () => { cy.get('@input').should('not.have.attr', 'name'); @@ -162,17 +136,17 @@ describe('card-control', () => { cy.get('@input').should('not.have.attr', 'aria-disabled'); }); - it('should set validation state according to "state" prop', () => { + it('should set validation state according to "validity" prop', () => { cy.get('@wrapper').should('not.have.class', 'is-valid').and('not.have.class', 'is-invalid'); - cy.get('@card-control').invoke('attr', 'state', ''); + cy.get('@card-control').invoke('attr', 'validity', ''); cy.get('@wrapper').should('have.class', 'is-valid').and('not.have.class', 'is-invalid'); - cy.get('@card-control').invoke('attr', 'state', 'state'); + cy.get('@card-control').invoke('attr', 'validity', 'anything-but-false'); cy.get('@wrapper').should('have.class', 'is-valid').and('not.have.class', 'is-invalid'); - cy.get('@card-control').invoke('attr', 'state', true); + cy.get('@card-control').invoke('attr', 'validity', true); cy.get('@wrapper').should('have.class', 'is-valid').and('not.have.class', 'is-invalid'); - cy.get('@card-control').invoke('attr', 'state', false); + cy.get('@card-control').invoke('attr', 'validity', false); cy.get('@wrapper').should('not.have.class', 'is-valid').and('have.class', 'is-invalid'); - cy.get('@card-control').invoke('removeAttr', 'state'); + cy.get('@card-control').invoke('removeAttr', 'validity'); cy.get('@wrapper').should('not.have.class', 'is-valid').and('not.have.class', 'is-invalid'); }); @@ -190,77 +164,103 @@ describe('card-control', () => { }); describe('events', () => { - beforeEach(() => { - cy.getComponent('card-control', { group: 'forms', story: 'content' }); - cy.get('@card-control').invoke('removeAttr', 'checked'); + it.skip('should toggle when clicked or by typing {space}', () => { + cy.get('@input').should('not.be.checked'); + cy.get('@wrapper').should('not.have.class', 'is-checked').click(); + cy.get('@input').should('be.checked'); + cy.get('@wrapper').should('have.class', 'is-checked').click(); + cy.get('@input').should('not.be.checked'); + cy.get('@wrapper').should('not.have.class', 'is-checked'); - cy.get('@card-control').find('.card-control').as('wrapper'); + cy.get('@input').type(' ').should('be.checked'); + cy.get('@wrapper').should('have.class', 'is-checked'); + cy.get('@input').focus().type(' ').should('not.be.checked'); + cy.get('@wrapper').should('not.have.class', 'is-checked'); + }); - cy.get('@card-control').find('input.header--input').as('input'); + it.skip('should not toggle when disabled', () => { + cy.get('@card-control').invoke('attr', 'disabled', true); - cy.get('@card-control').find('.card-control--content').as('content'); - cy.get('@card-control').find('.card-control--content slot').as('slotContent'); + cy.get('@input').should('not.be.checked'); + cy.get('@wrapper').should('not.have.class', 'is-checked').click(); + cy.get('@input').should('not.be.checked'); + cy.get('@wrapper').should('not.have.class', 'is-checked'); + + cy.get('@input').type(' ').should('not.be.checked'); + cy.get('@wrapper').should('not.have.class', 'is-checked'); }); - it('should toggle class "is-focused" when focused/blured', () => { + it.skip('should toggle class "is-focused" when focused/blured', () => { cy.get('@wrapper').should('not.have.class', 'is-focused'); + cy.get('@input').should('not.have.focus').focus(); - cy.get('@input') - .first() - .focus() - // waiting for focus to be executed, because focus() is not implemented like other action commands, and does not follow the same rules of waiting for actionability. - .wait(300) + cy.get('@wrapper').should('have.class', 'is-focused'); + cy.get('@input').should('have.focus').blur({ force: true }); + + cy.get('@wrapper').should('not.have.class', 'is-focused'); + cy.get('@input').should('not.have.focus'); + }); + + it.skip('should emit input and change events when toggled', () => { + let inputEventCallCount = 0; + let changeEventCallCount = 0; + + cy.get('@card-control').then($cardControl => { + $cardControl.get(0).addEventListener('input', () => inputEventCallCount++); + $cardControl.get(0).addEventListener('change', () => changeEventCallCount++); + }); + + cy.get('@wrapper') + .click() + .then(() => { + expect(inputEventCallCount).to.eq(1); + expect(changeEventCallCount).to.eq(1); + }) + .click() .then(() => { - cy.get('@wrapper').should('have.class', 'is-focused'); + expect(inputEventCallCount).to.eq(2); + expect(changeEventCallCount).to.eq(2); }); cy.get('@input') - .first() - .blur({ force: true }) - // waiting for blur to be executed, because blur() is not implemented like other action commands, and does not follow the same rules of waiting for actionability. - .wait(300) + .type(' ') .then(() => { - cy.get('@wrapper').should('not.have.class', 'is-focused'); + expect(inputEventCallCount).to.eq(3); + expect(changeEventCallCount).to.eq(3); + }) + .focus() + .type(' ') + .then(() => { + expect(inputEventCallCount).to.eq(4); + expect(changeEventCallCount).to.eq(4); }); }); - it('should toggle content when clicked', () => { - cy.get('@content').should('not.be.visible'); - cy.get('@input').should('have.attr', 'aria-expanded').and('eq', 'false'); - cy.get('@card-control').click({ force: true }); - cy.get('@content').should('be.visible'); - cy.get('@input').should('have.attr', 'aria-expanded').and('eq', 'true'); - cy.get('@card-control').click({ force: true }); - cy.get('@content').should('not.be.visible'); - cy.get('@input').should('have.attr', 'aria-expanded').and('eq', 'false'); - }); + it('should not emit input and change events when disabled', () => { + cy.get('@card-control').invoke('attr', 'disabled', true); - it('should not toggle content when clicking on interactive element in content', () => { - cy.get('@card-control').click(); - cy.get('@input').should('have.attr', 'aria-expanded').and('eq', 'true'); - cy.get('@content').should('be.visible'); + let inputEventCallCount = 0; + let changeEventCallCount = 0; - cy.get('@card-control') - .find('> *') - .not('[slot]') - .find(INTERACTIVE_ELEMENT_SELECTORS.join(', ')) - .not('input[type="hidden"]') - .each($el => { - if ($el.is('select')) { - cy.wrap($el).select(0); - } else { - cy.wrap($el).click({ force: true }); - } - cy.get('@input').should('have.attr', 'aria-expanded').and('eq', 'true'); - cy.get('@content').should('be.visible'); + cy.get('@card-control').then($cardControl => { + $cardControl.get(0).addEventListener('input', () => inputEventCallCount++); + $cardControl.get(0).addEventListener('change', () => changeEventCallCount++); + }); + + cy.get('@wrapper') + .click() + .then(() => { + expect(inputEventCallCount).to.eq(0); + expect(changeEventCallCount).to.eq(0); }); - }); - it('should toogle content when used with keyboard', () => { - // TODO when https://github.com/cypress-io/cypress/issues/311 is ready + cy.get('@input') + .type(' ') + .then(() => { + expect(inputEventCallCount).to.eq(0); + expect(changeEventCallCount).to.eq(0); + }); }); - - // TODO: test change events }); // TODO: test form association From e61fae1804ed688556903e6018bbd1407f487eb0 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Fri, 9 Feb 2024 10:36:27 +0100 Subject: [PATCH 37/82] feat(components): add reset and clearvalidity methods in card-control --- packages/components/src/components.d.ts | 14 +++-- .../post-card-control/post-card-control.tsx | 29 ++++++++-- .../components/post-card-control/readme.md | 54 +++++++++++++------ .../card-control-methods.sample.ts | 3 ++ .../card-control/card-control.docs.mdx | 12 ++++- 5 files changed, 88 insertions(+), 24 deletions(-) create mode 100644 packages/documentation/src/stories/components/card-control/card-control-methods.sample.ts diff --git a/packages/components/src/components.d.ts b/packages/components/src/components.d.ts index 561969de19..e8157880a8 100644 --- a/packages/components/src/components.d.ts +++ b/packages/components/src/components.d.ts @@ -77,7 +77,11 @@ export namespace Components { /** * Defines the `checked` attribute of the control. If `true`, the control is selected at its value will be included in the forms data. */ - "checked"?: boolean; + "checked": boolean; + /** + * A public method to clear the controls `validity` state. The state is set to `null`, so it's neither valid nor invalid. + */ + "clearValidity": () => Promise; /** * Defines the description in the control-label. */ @@ -95,9 +99,13 @@ export namespace Components { */ "label": string; /** - * Defines the `name` attribute of the control. This name is used in a forms data to store the given value of the control. If no name is specified a form will never contain this controls value. + * Defines the `name` attribute of the control. The name is used in a forms `data` to store the given value of the control. If no name is specified, a form will never contain this controls value. */ "name": string; + /** + * A public method to reset the controls `checked` and `validity` state. The state is set to `null`, so it's neither valid nor invalid. + */ + "reset": () => Promise; /** * Defines the `type` attribute of the control. */ @@ -494,7 +502,7 @@ declare namespace LocalJSX { */ "label": string; /** - * Defines the `name` attribute of the control. This name is used in a forms data to store the given value of the control. If no name is specified a form will never contain this controls value. + * Defines the `name` attribute of the control. The name is used in a forms `data` to store the given value of the control. If no name is specified, a form will never contain this controls value. */ "name"?: string; /** diff --git a/packages/components/src/components/post-card-control/post-card-control.tsx b/packages/components/src/components/post-card-control/post-card-control.tsx index 3e70eb87de..5fea7d1882 100644 --- a/packages/components/src/components/post-card-control/post-card-control.tsx +++ b/packages/components/src/components/post-card-control/post-card-control.tsx @@ -6,6 +6,7 @@ import { EventEmitter, h, Host, + Method, Prop, State, Watch, @@ -19,7 +20,6 @@ let cardControlIds = 0; * @class PostCardControl - representing a stencil component * * @slot icon - Content to place in the named `icon` slot.

Markup accepted: inline content.
It is only meant for img or svg elements and overrides the `icon` property.

- * @slot invalid-feedback - Content to place in the named `invalid-feedback` slot.

Markup accepted: inline content.

*/ @Component({ tag: 'post-card-control', @@ -73,7 +73,8 @@ export class PostCardControl { @Prop() readonly type!: 'checkbox' | 'radio'; /** - * Defines the `name` attribute of the control. This name is used in a forms data to store the given value of the control. If no name is specified a form will never contain this controls value. + * Defines the `name` attribute of the control. + * The name is used in a forms `data` to store the given value of the control. If no name is specified, a form will never contain this controls value. */ @Prop() readonly name: string = null; @@ -85,7 +86,7 @@ export class PostCardControl { /** * Defines the `checked` attribute of the control. If `true`, the control is selected at its value will be included in the forms data. */ - @Prop({ mutable: true }) checked?: boolean = false; + @Prop({ mutable: true }) checked = false; /** * Defines the `disabled` attribute of the control. If `true`, the user can not interact with the control and the controls value will not be included in the forms data. @@ -116,6 +117,25 @@ export class PostCardControl { */ @Event() change: EventEmitter; + /** + * A public method to clear the controls `validity` state. + * The state is set to `null`, so it's neither valid nor invalid. + */ + @Method() + async clearValidity() { + this.validity = null; + } + + /** + * A public method to reset the controls `checked` and `validity` state. + * The state is set to `null`, so it's neither valid nor invalid. + */ + @Method() + async reset() { + this.validity = null; + this.controlSetChecked(this.initialChecked); + } + constructor() { this.GROUPEVENT = `PostCardControlGroup:${this.name}:change`; @@ -357,8 +377,7 @@ export class PostCardControl { } formResetCallback() { - this.validity = null; - this.controlSetChecked(this.initialChecked); + this.reset(); } disconnectedCallback() { diff --git a/packages/components/src/components/post-card-control/readme.md b/packages/components/src/components/post-card-control/readme.md index d9bc1c64bd..a29d487074 100644 --- a/packages/components/src/components/post-card-control/readme.md +++ b/packages/components/src/components/post-card-control/readme.md @@ -7,17 +7,17 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| -------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | ----------- | -| `checked` | `checked` | Defines the `checked` attribute of the control. If `true`, the control is selected at its value will be included in the forms data. | `boolean` | `false` | -| `description` | `description` | Defines the description in the control-label. | `string` | `null` | -| `disabled` | `disabled` | Defines the `disabled` attribute of the control. If `true`, the user can not interact with the control and the controls value will not be included in the forms data. | `boolean` | `false` | -| `icon` | `icon` | Defines the icon `name` inside of the card. If not set the icon will not show up. | `string` | `null` | -| `label` _(required)_ | `label` | Defines the text in the control-label. | `string` | `undefined` | -| `name` | `name` | Defines the `name` attribute of the control. This name is used in a forms data to store the given value of the control. If no name is specified a form will never contain this controls value. | `string` | `null` | -| `type` _(required)_ | `type` | Defines the `type` attribute of the control. | `"checkbox" \| "radio"` | `undefined` | -| `validity` | `validity` | Defines the validation `validity` of the control. | `boolean` | `null` | -| `value` | `value` | Defines the `value` attribute of the control. This is only used, when the control participates in the native `form`. | `string` | `null` | +| Property | Attribute | Description | Type | Default | +| -------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | ----------- | +| `checked` | `checked` | Defines the `checked` attribute of the control. If `true`, the control is selected at its value will be included in the forms data. | `boolean` | `false` | +| `description` | `description` | Defines the description in the control-label. | `string` | `null` | +| `disabled` | `disabled` | Defines the `disabled` attribute of the control. If `true`, the user can not interact with the control and the controls value will not be included in the forms data. | `boolean` | `false` | +| `icon` | `icon` | Defines the icon `name` inside of the card. If not set the icon will not show up. | `string` | `null` | +| `label` _(required)_ | `label` | Defines the text in the control-label. | `string` | `undefined` | +| `name` | `name` | Defines the `name` attribute of the control. The name is used in a forms `data` to store the given value of the control. If no name is specified, a form will never contain this controls value. | `string` | `null` | +| `type` _(required)_ | `type` | Defines the `type` attribute of the control. | `"checkbox" \| "radio"` | `undefined` | +| `validity` | `validity` | Defines the validation `validity` of the control. | `boolean` | `null` | +| `value` | `value` | Defines the `value` attribute of the control. This is only used, when the control participates in the native `form`. | `string` | `null` | ## Events @@ -28,12 +28,36 @@ | `input` | An event emitted whenever the components checked state is toggled. The event payload (emitted under `event.detail.state`) is a boolean: `true` if the component is checked, `false` if it is unchecked. | `CustomEvent` | +## Methods + +### `clearValidity() => Promise` + +A public method to clear the controls `validity` state. +The state is set to `null`, so it's neither valid nor invalid. + +#### Returns + +Type: `Promise` + + + +### `reset() => Promise` + +A public method to reset the controls `checked` and `validity` state. +The state is set to `null`, so it's neither valid nor invalid. + +#### Returns + +Type: `Promise` + + + + ## Slots -| Slot | Description | -| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `"icon"` | Content to place in the named `icon` slot.

Markup accepted: inline content.
It is only meant for img or svg elements and overrides the `icon` property.

| -| `"invalid-feedback"` | Content to place in the named `invalid-feedback` slot.

Markup accepted: inline content.

| +| Slot | Description | +| -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `"icon"` | Content to place in the named `icon` slot.

Markup accepted: inline content.
It is only meant for img or svg elements and overrides the `icon` property.

| ## Dependencies diff --git a/packages/documentation/src/stories/components/card-control/card-control-methods.sample.ts b/packages/documentation/src/stories/components/card-control/card-control-methods.sample.ts new file mode 100644 index 0000000000..9536314acd --- /dev/null +++ b/packages/documentation/src/stories/components/card-control/card-control-methods.sample.ts @@ -0,0 +1,3 @@ +const cardControl = document.querySelector('post-card-control') as HTMLPostCardControlElement; +cardControl.reset(); +cardControl.clearValidity(); diff --git a/packages/documentation/src/stories/components/card-control/card-control.docs.mdx b/packages/documentation/src/stories/components/card-control/card-control.docs.mdx index 28362321ad..c0833e81b5 100644 --- a/packages/documentation/src/stories/components/card-control/card-control.docs.mdx +++ b/packages/documentation/src/stories/components/card-control/card-control.docs.mdx @@ -1,6 +1,7 @@ -import { Canvas, Controls, Meta } from '@storybook/blocks'; +import { Canvas, Controls, Meta, Source } from '@storybook/blocks'; import * as CardControlStories from './card-control.stories'; import LinkTo from '@storybook/addon-links/react'; +import SampleCardControlMethods from './card-control-methods.sample?raw'; @@ -58,3 +59,12 @@ As you can create radio button groups with native `` elements, you can do Just add the same `name` attribute value to multiple `` components. + +### Custom Trigger +The `` offers several methods to manipulate its states. +They can be called directly on the element itself. + + From 5e265822288a81c22b749d21c08f96074b66599c Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Fri, 9 Feb 2024 10:37:39 +0100 Subject: [PATCH 38/82] test(components): add tests for methods and form-association --- .../components/cypress/e2e/card-control.cy.ts | 151 +++++++++++++++--- .../fixtures/post-card-control.test.html | 36 +++++ .../components/cypress/support/commands.ts | 8 + .../components/cypress/support/index.d.ts | 1 + 4 files changed, 177 insertions(+), 19 deletions(-) create mode 100644 packages/components/cypress/fixtures/post-card-control.test.html diff --git a/packages/components/cypress/e2e/card-control.cy.ts b/packages/components/cypress/e2e/card-control.cy.ts index 94a57db1da..b045cae6b9 100644 --- a/packages/components/cypress/e2e/card-control.cy.ts +++ b/packages/components/cypress/e2e/card-control.cy.ts @@ -1,18 +1,18 @@ describe('card-control', () => { - beforeEach(() => { - cy.getComponent('card-control', { group: 'forms' }); - cy.window().then(win => { - cy.wrap(cy.spy(win.console, 'error')).as('consoleError'); - }); + describe('structure & props', () => { + beforeEach(() => { + cy.getComponent('card-control', { group: 'forms' }); + cy.window().then(win => { + cy.wrap(cy.spy(win.console, 'error')).as('consoleError'); + }); - cy.get('@card-control').find('.card-control').as('wrapper'); - cy.get('@card-control').find('input.card-control--input').as('input'); - cy.get('@card-control').find('label.card-control--label').as('label'); - cy.get('@card-control').find('.card-control--icon').as('icon'); - cy.get('@card-control').find('.card-control--icon slot[name="icon"]').as('slotIcon'); - }); + cy.get('@card-control').find('.card-control').as('wrapper'); + cy.get('@card-control').find('input.card-control--input').as('input'); + cy.get('@card-control').find('label.card-control--label').as('label'); + cy.get('@card-control').find('.card-control--icon').as('icon'); + cy.get('@card-control').find('.card-control--icon slot[name="icon"]').as('slotIcon'); + }); - describe.skip('structure & props', () => { it('should have no console errors', () => { cy.get('@consoleError').should('not.be.called'); }); @@ -146,8 +146,6 @@ describe('card-control', () => { cy.get('@wrapper').should('have.class', 'is-valid').and('not.have.class', 'is-invalid'); cy.get('@card-control').invoke('attr', 'validity', false); cy.get('@wrapper').should('not.have.class', 'is-valid').and('have.class', 'is-invalid'); - cy.get('@card-control').invoke('removeAttr', 'validity'); - cy.get('@wrapper').should('not.have.class', 'is-valid').and('not.have.class', 'is-invalid'); }); it('should set icon "name" attr according to "icon" prop', () => { @@ -164,7 +162,14 @@ describe('card-control', () => { }); describe('events', () => { - it.skip('should toggle when clicked or by typing {space}', () => { + beforeEach(() => { + cy.getComponent('card-control', { group: 'forms' }); + + cy.get('@card-control').find('.card-control').as('wrapper'); + cy.get('@card-control').find('input.card-control--input').as('input'); + }); + + it('should toggle when clicked or by typing {space}', () => { cy.get('@input').should('not.be.checked'); cy.get('@wrapper').should('not.have.class', 'is-checked').click(); cy.get('@input').should('be.checked'); @@ -178,7 +183,7 @@ describe('card-control', () => { cy.get('@wrapper').should('not.have.class', 'is-checked'); }); - it.skip('should not toggle when disabled', () => { + it('should not toggle when disabled', () => { cy.get('@card-control').invoke('attr', 'disabled', true); cy.get('@input').should('not.be.checked'); @@ -190,7 +195,7 @@ describe('card-control', () => { cy.get('@wrapper').should('not.have.class', 'is-checked'); }); - it.skip('should toggle class "is-focused" when focused/blured', () => { + it('should toggle class "is-focused" when focused/blured', () => { cy.get('@wrapper').should('not.have.class', 'is-focused'); cy.get('@input').should('not.have.focus').focus(); @@ -201,7 +206,7 @@ describe('card-control', () => { cy.get('@input').should('not.have.focus'); }); - it.skip('should emit input and change events when toggled', () => { + it('should emit input and change events when toggled', () => { let inputEventCallCount = 0; let changeEventCallCount = 0; @@ -263,5 +268,113 @@ describe('card-control', () => { }); }); - // TODO: test form association + describe('methods', () => { + beforeEach(() => { + cy.getComponent('card-control', { group: 'forms' }); + + cy.get('@card-control').find('.card-control').as('wrapper'); + cy.get('@card-control').find('input.card-control--input').as('input'); + }); + + it('should reset the checked and validity state to its initial values, when calling public "reset" method', () => { + cy.get('@card-control').invoke('attr', 'checked', true).invoke('attr', 'validity', true); + cy.get('@wrapper') + .should('have.class', 'is-checked') + .and('have.class', 'is-valid') + .and('not.have.class', 'is-invalid'); + cy.get('@card-control').then($cardControl => { + ($cardControl.get(0) as HTMLPostCardControlElement).reset(); + cy.get('@wrapper') + .should('not.have.class', 'is-checked') + .and('not.have.class', 'is-valid') + .and('not.have.class', 'is-invalid'); + }); + }); + + it('should reset the validity state to its initial value, when calling public "clearValidity" method', () => { + cy.get('@card-control').invoke('attr', 'validity', true); + cy.get('@wrapper').and('have.class', 'is-valid').and('not.have.class', 'is-invalid'); + cy.get('@card-control').then($cardControl => { + ($cardControl.get(0) as HTMLPostCardControlElement).clearValidity(); + cy.get('@wrapper').and('not.have.class', 'is-valid').and('not.have.class', 'is-invalid'); + }); + }); + }); + + describe('form association', { baseUrl: null, includeShadowDom: true }, () => { + beforeEach(() => { + cy.visit('./cypress/fixtures/post-card-control.test.html'); + + cy.get('form#AssociatedForm').as('form'); + cy.get('@form').find('fieldset').as('fieldset'); + cy.get('@form').find('button[type="reset"]').as('reset'); + cy.get('@form').find('button[type="submit"]').as('submit'); + cy.get('@form').find('post-card-control').as('card-control'); + cy.get('@card-control').find('.card-control').as('wrapper'); + cy.get('@card-control').find('input.card-control--input').as('input'); + cy.get('@card-control').find('label.card-control--label').as('label'); + cy.get('@card-control').find('.card-control--icon').as('icon'); + cy.get('@card-control').find('.card-control--icon slot[name="icon"]').as('slotIcon'); + }); + + it('should update surrounding formdata when control is toggled', () => { + cy.get('@form').then($form => { + cy.get('@wrapper').click(); + cy.checkFormDataPropValue($form, 'CardControl', 'on'); + cy.get('@wrapper').click(); + cy.checkFormDataPropValue($form, 'CardControl', null); + + cy.get('@input').type(' '); + cy.checkFormDataPropValue($form, 'CardControl', 'on'); + cy.get('@input').focus().type(' '); + cy.checkFormDataPropValue($form, 'CardControl', null); + }); + }); + + it('should reset surrounding formdata and control itself when form is resetted', () => { + cy.get('@form').then($form => { + cy.get('@wrapper').click(); + cy.checkFormDataPropValue($form, 'CardControl', 'on'); + + cy.get('@reset').click(); + cy.get('@wrapper').should('not.have.class', 'is-checked'); + cy.get('@input').should('not.be.checked'); + cy.checkFormDataPropValue($form, 'CardControl', null); + }); + }); + + it('should not update surrounding formdata when control is disabled', () => { + cy.get('@form').then($form => { + cy.get('@card-control').invoke('attr', 'checked', true).invoke('attr', 'disabled', true); + + cy.get('@wrapper').should('have.class', 'is-checked').and('have.class', 'is-disabled'); + cy.get('@input').should('be.checked').and('have.attr', 'aria-disabled'); + cy.checkFormDataPropValue($form, 'CardControl', null); + }); + }); + + it('should disable control when surrounding fieldset element is disabled', () => { + cy.get('@form').then($form => { + cy.get('@fieldset').invoke('attr', 'disabled', true).should('have.attr', 'disabled'); + cy.get('@card-control').invoke('attr', 'checked', true); + + cy.get('@wrapper').should('have.class', 'is-checked').and('have.class', 'is-disabled'); + cy.get('@input').should('be.checked').and('have.attr', 'aria-disabled'); + cy.checkFormDataPropValue($form, 'CardControl', null); + }); + }); + + it('should not update surrounding formdata when control name is not set', () => { + cy.get('@form').then($form => { + cy.get('@card-control') + .invoke('attr', 'checked', true) + .invoke('removeAttr', 'name') + .should('not.have.attr', 'name'); + + cy.get('@wrapper').should('have.class', 'is-checked'); + cy.get('@input').should('be.checked'); + cy.checkFormDataPropValue($form, 'CardControl', null); + }); + }); + }); }); diff --git a/packages/components/cypress/fixtures/post-card-control.test.html b/packages/components/cypress/fixtures/post-card-control.test.html new file mode 100644 index 0000000000..701e72d977 --- /dev/null +++ b/packages/components/cypress/fixtures/post-card-control.test.html @@ -0,0 +1,36 @@ + + + + + + Document + + + + + +
+ Legend + +
+
+ + +
+ + + diff --git a/packages/components/cypress/support/commands.ts b/packages/components/cypress/support/commands.ts index 48d43536b9..eca9289989 100644 --- a/packages/components/cypress/support/commands.ts +++ b/packages/components/cypress/support/commands.ts @@ -69,3 +69,11 @@ Cypress.Commands.add( }); }, ); + +Cypress.Commands.add( + 'checkFormDataPropValue', + ($form: JQuery, key: string, value: any) => { + const formControlData = new FormData($form.get(0) as HTMLFormElement).get(key); + expect(formControlData).to.be.eq(value); + }, +); diff --git a/packages/components/cypress/support/index.d.ts b/packages/components/cypress/support/index.d.ts index b9df4e1d0b..f03552bbc2 100644 --- a/packages/components/cypress/support/index.d.ts +++ b/packages/components/cypress/support/index.d.ts @@ -12,6 +12,7 @@ declare global { controlledElementSelector: string, isExpanded: 'true' | 'false', ): Chainable; + checkFormDataPropValue($form: JQuery, key: string, value: any): Chainable; } } } From 5571ed067e7edfd22c1b5c8d1cff6ccb1764b0d1 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Fri, 9 Feb 2024 12:11:04 +0100 Subject: [PATCH 39/82] chore(components): remove public method clearValidity out ouf card-control and reset prop types to null | 'true' | 'false', to make it resettable --- .../components/cypress/e2e/card-control.cy.ts | 9 --------- packages/components/src/components.d.ts | 12 ++++-------- .../post-card-control/post-card-control.tsx | 18 +++++------------- .../src/components/post-card-control/readme.md | 13 +------------ .../card-control-methods.sample.ts | 1 - 5 files changed, 10 insertions(+), 43 deletions(-) diff --git a/packages/components/cypress/e2e/card-control.cy.ts b/packages/components/cypress/e2e/card-control.cy.ts index b045cae6b9..f8aa0e0d98 100644 --- a/packages/components/cypress/e2e/card-control.cy.ts +++ b/packages/components/cypress/e2e/card-control.cy.ts @@ -290,15 +290,6 @@ describe('card-control', () => { .and('not.have.class', 'is-invalid'); }); }); - - it('should reset the validity state to its initial value, when calling public "clearValidity" method', () => { - cy.get('@card-control').invoke('attr', 'validity', true); - cy.get('@wrapper').and('have.class', 'is-valid').and('not.have.class', 'is-invalid'); - cy.get('@card-control').then($cardControl => { - ($cardControl.get(0) as HTMLPostCardControlElement).clearValidity(); - cy.get('@wrapper').and('not.have.class', 'is-valid').and('not.have.class', 'is-invalid'); - }); - }); }); describe('form association', { baseUrl: null, includeShadowDom: true }, () => { diff --git a/packages/components/src/components.d.ts b/packages/components/src/components.d.ts index e8157880a8..cebfe885ca 100644 --- a/packages/components/src/components.d.ts +++ b/packages/components/src/components.d.ts @@ -78,10 +78,6 @@ export namespace Components { * Defines the `checked` attribute of the control. If `true`, the control is selected at its value will be included in the forms data. */ "checked": boolean; - /** - * A public method to clear the controls `validity` state. The state is set to `null`, so it's neither valid nor invalid. - */ - "clearValidity": () => Promise; /** * Defines the description in the control-label. */ @@ -111,9 +107,9 @@ export namespace Components { */ "type": 'checkbox' | 'radio'; /** - * Defines the validation `validity` of the control. + * Defines the validation `validity` of the control. To reset validity to an undefiend state, simply remove the attribute from the control. */ - "validity": null | true | false; + "validity": null | 'true' | 'false'; /** * Defines the `value` attribute of the control. This is only used, when the control participates in the native `form`. */ @@ -518,9 +514,9 @@ declare namespace LocalJSX { */ "type": 'checkbox' | 'radio'; /** - * Defines the validation `validity` of the control. + * Defines the validation `validity` of the control. To reset validity to an undefiend state, simply remove the attribute from the control. */ - "validity"?: null | true | false; + "validity"?: null | 'true' | 'false'; /** * Defines the `value` attribute of the control. This is only used, when the control participates in the native `form`. */ diff --git a/packages/components/src/components/post-card-control/post-card-control.tsx b/packages/components/src/components/post-card-control/post-card-control.tsx index 5fea7d1882..9d23b402e2 100644 --- a/packages/components/src/components/post-card-control/post-card-control.tsx +++ b/packages/components/src/components/post-card-control/post-card-control.tsx @@ -95,8 +95,9 @@ export class PostCardControl { /** * Defines the validation `validity` of the control. + * To reset validity to an undefiend state, simply remove the attribute from the control. */ - @Prop({ mutable: true }) validity: null | true | false = null; + @Prop({ mutable: true }) validity: null | 'true' | 'false' = null; /** * Defines the icon `name` inside of the card. @@ -117,15 +118,6 @@ export class PostCardControl { */ @Event() change: EventEmitter; - /** - * A public method to clear the controls `validity` state. - * The state is set to `null`, so it's neither valid nor invalid. - */ - @Method() - async clearValidity() { - this.validity = null; - } - /** * A public method to reset the controls `checked` and `validity` state. * The state is set to `null`, so it's neither valid nor invalid. @@ -322,8 +314,8 @@ export class PostCardControl { 'is-checked': this.checked, 'is-disabled': this.disabled, 'is-focused': this.focused, - 'is-valid': this.validity !== null && this.validity !== false, - 'is-invalid': this.validity === false, + 'is-valid': this.validity !== null && this.validity !== 'false', + 'is-invalid': this.validity === 'false', }} > The name is used in a forms `data` to store the given value of the control. If no name is specified, a form will never contain this controls value. | `string` | `null` | | `type` _(required)_ | `type` | Defines the `type` attribute of the control. | `"checkbox" \| "radio"` | `undefined` | -| `validity` | `validity` | Defines the validation `validity` of the control. | `boolean` | `null` | +| `validity` | `validity` | Defines the validation `validity` of the control. To reset validity to an undefiend state, simply remove the attribute from the control. | `"false" \| "true"` | `null` | | `value` | `value` | Defines the `value` attribute of the control. This is only used, when the control participates in the native `form`. | `string` | `null` | @@ -30,17 +30,6 @@ ## Methods -### `clearValidity() => Promise` - -A public method to clear the controls `validity` state. -The state is set to `null`, so it's neither valid nor invalid. - -#### Returns - -Type: `Promise` - - - ### `reset() => Promise` A public method to reset the controls `checked` and `validity` state. diff --git a/packages/documentation/src/stories/components/card-control/card-control-methods.sample.ts b/packages/documentation/src/stories/components/card-control/card-control-methods.sample.ts index 9536314acd..57f8b6d3fb 100644 --- a/packages/documentation/src/stories/components/card-control/card-control-methods.sample.ts +++ b/packages/documentation/src/stories/components/card-control/card-control-methods.sample.ts @@ -1,3 +1,2 @@ const cardControl = document.querySelector('post-card-control') as HTMLPostCardControlElement; cardControl.reset(); -cardControl.clearValidity(); From ef74c50e1f9c936a620b9c0308a8f975ef5b7c25 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Fri, 9 Feb 2024 12:11:27 +0100 Subject: [PATCH 40/82] chore(components): improve card-control background story --- .../components/card-control/card-control.docs.mdx | 2 +- .../components/card-control/card-control.stories.ts | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/documentation/src/stories/components/card-control/card-control.docs.mdx b/packages/documentation/src/stories/components/card-control/card-control.docs.mdx index c0833e81b5..495ef8056d 100644 --- a/packages/documentation/src/stories/components/card-control/card-control.docs.mdx +++ b/packages/documentation/src/stories/components/card-control/card-control.docs.mdx @@ -20,7 +20,7 @@ The `` element is part of the `@swisspost/design-system-compo ### On dark background - +
diff --git a/packages/documentation/src/stories/components/card-control/card-control.stories.ts b/packages/documentation/src/stories/components/card-control/card-control.stories.ts index 04e03affad..e8f65de38b 100644 --- a/packages/documentation/src/stories/components/card-control/card-control.stories.ts +++ b/packages/documentation/src/stories/components/card-control/card-control.stories.ts @@ -104,12 +104,18 @@ export const DarkBackground: Story = { parameters: { docs: { controls: { - include: ['Background-Color', 'checked', 'disabled', 'validity'], + include: ['Background-Color', 'type', 'checked', 'disabled', 'validity'], }, }, }, decorators: [ - (story, context) => html`
${story()}
`, + (story, context) => + html`
+ ${story()} +
`, ], args: { background: 'dark', From 8b40e3eafb062ba50e9dae72cd21b638102901e6 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Fri, 9 Feb 2024 12:12:39 +0100 Subject: [PATCH 41/82] chore(components): adapt newest color definitions from design --- .../post-card-control/post-card-control.scss | 42 +++++-------------- 1 file changed, 11 insertions(+), 31 deletions(-) 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 0985a26b99..5506af2c26 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 @@ -74,20 +74,10 @@ &.is-invalid { --post-card-control-border-color: #{post.$danger}; - --post-card-control-bg: #{post.$white}; --post-card-control-color: #{post.$danger}; --post-card-control-input-border-color: #{post.$danger}; } - &.is-focused { - &:where(:has(.card-control--input:focus-visible)) { - --post-card-control-border-color: #{post.$gray-60}; - --post-card-control-bg: #{post.$white}; - --post-card-control-color: #{post.$gray-80}; - --post-card-control-input-border-color: #{post.$gray-80}; - } - } - &:hover { --post-card-control-border-color: #{post.$gray-80}; --post-card-control-bg: #{post.$gray-60}; @@ -105,10 +95,10 @@ } &.is-disabled { - --post-card-control-border-color: #{post.$gray-40}; + --post-card-control-border-color: #{post.$gray-60}; --post-card-control-bg: transparent; - --post-card-control-color: #{post.$gray-40}; - --post-card-control-input-border-color: #{post.$gray-40}; + --post-card-control-color: #{post.$gray-60}; + --post-card-control-input-border-color: #{post.$gray-60}; --post-card-control-input-bg: transparent; border-style: dashed; @@ -137,9 +127,8 @@ --post-card-control-input-border-color: #{post.$gray-80}; --post-card-control-input-bg: #{post.$white}; - &:hover { - --post-card-control-border-color: #{post.$black}; - --post-card-control-bg: #{post.$gray-20}; + &.is-invalid { + --post-card-control-bg: #{post.$yellow}; } } @@ -151,19 +140,9 @@ --post-card-control-input-bg: #{post.$white}; } - &.is-focused { - &:where(:has(.card-control--input:focus-visible)) { - --post-card-control-border-color: #{post.$white}; - --post-card-control-bg: transparent; - --post-card-control-color: #{post.$white}; - --post-card-control-input-border-color: #{post.$white}; - --post-card-control-input-bg: transparent; - } - } - &:hover { - --post-card-control-border-color: #{post.$white}; - --post-card-control-bg: #{post.$yellow}; + --post-card-control-border-color: #{post.$black}; + --post-card-control-bg: #{post.$gray-20}; --post-card-control-color: #{post.$black}; --post-card-control-input-border-color: #{post.$black}; --post-card-control-input-bg: #{post.$white}; @@ -177,11 +156,12 @@ } } + // TODO: update white alpha colors with design-system alpha colors, once they are defined &.is-disabled { - --post-card-control-border-color: #{post.$gray-20}; + --post-card-control-border-color: #{#{rgba(post.$white, 0.8)}}; --post-card-control-bg: transparent; - --post-card-control-color: #{post.$gray-20}; - --post-card-control-input-border-color: #{post.$gray-20}; + --post-card-control-color: #{#{rgba(post.$white, 0.8)}}; + --post-card-control-input-border-color: #{#{rgba(post.$white, 0.8)}}; --post-card-control-input-bg: transparent; } } From 8e930ee42000e67132dfd0bce0fa9f2661a4d492 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Fri, 9 Feb 2024 14:35:37 +0100 Subject: [PATCH 42/82] test(components): add radio-group tests --- .../components/cypress/e2e/card-control.cy.ts | 140 +++++++++++++++++- ...t-card-control.form-association.test.html} | 0 .../post-card-control.radio-group.test.html | 45 ++++++ 3 files changed, 179 insertions(+), 6 deletions(-) rename packages/components/cypress/fixtures/{post-card-control.test.html => post-card-control.form-association.test.html} (100%) create mode 100644 packages/components/cypress/fixtures/post-card-control.radio-group.test.html diff --git a/packages/components/cypress/e2e/card-control.cy.ts b/packages/components/cypress/e2e/card-control.cy.ts index f8aa0e0d98..c75532342b 100644 --- a/packages/components/cypress/e2e/card-control.cy.ts +++ b/packages/components/cypress/e2e/card-control.cy.ts @@ -294,7 +294,7 @@ describe('card-control', () => { describe('form association', { baseUrl: null, includeShadowDom: true }, () => { beforeEach(() => { - cy.visit('./cypress/fixtures/post-card-control.test.html'); + cy.visit('./cypress/fixtures/post-card-control.form-association.test.html'); cy.get('form#AssociatedForm').as('form'); cy.get('@form').find('fieldset').as('fieldset'); @@ -308,7 +308,7 @@ describe('card-control', () => { cy.get('@card-control').find('.card-control--icon slot[name="icon"]').as('slotIcon'); }); - it('should update surrounding formdata when control is toggled', () => { + it('should update surrounding form when toggled', () => { cy.get('@form').then($form => { cy.get('@wrapper').click(); cy.checkFormDataPropValue($form, 'CardControl', 'on'); @@ -322,7 +322,7 @@ describe('card-control', () => { }); }); - it('should reset surrounding formdata and control itself when form is resetted', () => { + it('should reset surrounding form and itself when form is resetted', () => { cy.get('@form').then($form => { cy.get('@wrapper').click(); cy.checkFormDataPropValue($form, 'CardControl', 'on'); @@ -334,7 +334,7 @@ describe('card-control', () => { }); }); - it('should not update surrounding formdata when control is disabled', () => { + it('should not update surrounding form when disabled', () => { cy.get('@form').then($form => { cy.get('@card-control').invoke('attr', 'checked', true).invoke('attr', 'disabled', true); @@ -344,7 +344,7 @@ describe('card-control', () => { }); }); - it('should disable control when surrounding fieldset element is disabled', () => { + it('should be disable when surrounding fieldset element is disabled', () => { cy.get('@form').then($form => { cy.get('@fieldset').invoke('attr', 'disabled', true).should('have.attr', 'disabled'); cy.get('@card-control').invoke('attr', 'checked', true); @@ -355,7 +355,7 @@ describe('card-control', () => { }); }); - it('should not update surrounding formdata when control name is not set', () => { + it('should not update surrounding form when name is not set', () => { cy.get('@form').then($form => { cy.get('@card-control') .invoke('attr', 'checked', true) @@ -368,4 +368,132 @@ describe('card-control', () => { }); }); }); + + describe('form association', { baseUrl: null, includeShadowDom: true }, () => { + beforeEach(() => { + cy.visit('./cypress/fixtures/post-card-control.radio-group.test.html'); + + cy.get('form#AssociatedForm').as('form'); + cy.get('@form').find('post-card-control[name="CardControlGroup"]').as('card-control'); + cy.get('@card-control').find('.card-control').as('wrapper'); + cy.get('@wrapper').find('input.card-control--input').as('input'); + }); + + it('should only have its first group member focussable, when none is checked', () => { + cy.get('@wrapper').should('not.have.class', 'is-checked'); + cy.get('@input').should('not.be.checked'); + + cy.get('@input').each(($input, i) => { + cy.wrap($input) + .should('have.attr', 'tabindex') + .and('eq', i === 0 ? '0' : '-1'); + }); + }); + + it('should only have one checked group member', () => { + cy.get('@wrapper').should('not.have.class', 'is-checked'); + cy.get('@input').should('not.be.checked'); + + cy.get('@wrapper').each($wrapper => { + cy.wrap($wrapper).click(); + cy.get('@input').filter(':checked').its('length').should('eq', 1); + }); + }); + + it('should only have its checked group member focussable, when one is checked', () => { + cy.get('@wrapper').should('not.have.class', 'is-checked'); + cy.get('@input').should('not.be.checked').eq(1).type(' '); + + cy.get('@input').each(($input, i) => { + cy.wrap($input) + .should('have.attr', 'tabindex') + .and('eq', i === 1 ? '0' : '-1'); + }); + }); + + it('should be checked when focused + keydown {space}', () => { + cy.get('@wrapper').should('not.have.class', 'is-checked'); + cy.get('@input').should('not.be.checked'); + + cy.get('@input').each($input => { + cy.wrap($input).type(' ').should('be.checked'); + }); + }); + + it('should be checked when focused + keydown {space} the same control multiple times', () => { + cy.get('@input').each($input => { + cy.wrap($input).type(' ').should('be.checked'); + cy.wrap($input).type(' ').should('be.checked'); + cy.wrap($input).type(' ').should('be.checked'); + }); + }); + + it('should check next group member, when focused + keydown {downArrow | rightArrow}', () => { + cy.get('@input') + .its('length') + .then(length => { + cy.get('@input').each(($input, i) => { + cy.wrap($input).type('{downArrow}'); + cy.get('@input') + .eq(i + 1 < length ? i + 1 : 0) + .should('be.checked'); + }); + + cy.get('@input').each(($input, i) => { + cy.wrap($input).type('{rightArrow}'); + cy.get('@input') + .eq(i + 1 < length ? i + 1 : 0) + .should('be.checked'); + }); + }); + }); + + it('should check previous group member, when focused + keydown {upArrow | leftArrow}', () => { + cy.get('@input') + .its('length') + .then(length => { + let $input = cy.get('@input').eq(0); + + while (length > 0) { + $input.type('{upArrow}'); + $input = cy.get('@input').eq(--length).should('be.checked'); + } + }) + .then(length => { + let $input = cy.get('@input').eq(0); + + while (length > 0) { + $input.type('{leftArrow}'); + $input = cy.get('@input').eq(--length).should('be.checked'); + } + }); + }); + + it('should update surrounding form when selected', () => { + cy.get('@form').then($form => { + cy.get('@wrapper').each(($wrapper, i) => { + cy.wrap($wrapper).click(); + cy.checkFormDataPropValue($form, 'CardControlGroup', i.toString()); + }); + + cy.get('@input').each(($input, i) => { + cy.wrap($input).type(' '); + cy.checkFormDataPropValue($form, 'CardControlGroup', i.toString()); + }); + }); + }); + + it('should update surrounding form to not contain its value, when a disabled group member has been selected by keyboard', () => { + cy.get('@card-control').eq(1).invoke('attr', 'disabled', true); + cy.get('@wrapper').eq(1).should('have.class', 'is-disabled'); + cy.get('@input').eq(1).should('have.attr', 'aria-disabled'); + + cy.get('@form').then($form => { + cy.get('@input').each(($input, i) => { + cy.wrap($input).type(' '); + cy.checkFormDataPropValue($form, 'CardControlGroup', i !== 1 ? i.toString() : null); + }); + }); + }); + }); }); diff --git a/packages/components/cypress/fixtures/post-card-control.test.html b/packages/components/cypress/fixtures/post-card-control.form-association.test.html similarity index 100% rename from packages/components/cypress/fixtures/post-card-control.test.html rename to packages/components/cypress/fixtures/post-card-control.form-association.test.html diff --git a/packages/components/cypress/fixtures/post-card-control.radio-group.test.html b/packages/components/cypress/fixtures/post-card-control.radio-group.test.html new file mode 100644 index 0000000000..7fc84d356f --- /dev/null +++ b/packages/components/cypress/fixtures/post-card-control.radio-group.test.html @@ -0,0 +1,45 @@ + + + + + + Document + + + + +
+
+ Legend + + + + + + + + + + +
+
+ + +
+
+ + From 1dababe50b93e306b5e9fc4495044f35983fdd34 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Fri, 9 Feb 2024 14:36:28 +0100 Subject: [PATCH 43/82] chore(components): add todos and fix some docs texts --- packages/components/src/components.d.ts | 8 +++---- .../post-card-control/post-card-control.tsx | 7 ++++-- .../components/post-card-control/readme.md | 22 +++++++++---------- .../card-control/card-control.docs.mdx | 4 ++-- .../card-control/card-control.stories.ts | 2 ++ 5 files changed, 24 insertions(+), 19 deletions(-) diff --git a/packages/components/src/components.d.ts b/packages/components/src/components.d.ts index cebfe885ca..2c26ff0648 100644 --- a/packages/components/src/components.d.ts +++ b/packages/components/src/components.d.ts @@ -95,7 +95,7 @@ export namespace Components { */ "label": string; /** - * Defines the `name` attribute of the control. The name is used in a forms `data` to store the given value of the control. If no name is specified, a form will never contain this controls value. + * Defines the `name` attribute of the control. This is a required property, when the control should participate in a native `form`. If not specified, a native `form` will never contain this controls value. This is a required property, when the control is used with type `radio`. */ "name": string; /** @@ -111,7 +111,7 @@ export namespace Components { */ "validity": null | 'true' | 'false'; /** - * Defines the `value` attribute of the control. This is only used, when the control participates in the native `form`. + * Defines the `value` attribute of the control. This is a required property, when the control is used with type `radio`. */ "value": string; } @@ -498,7 +498,7 @@ declare namespace LocalJSX { */ "label": string; /** - * Defines the `name` attribute of the control. The name is used in a forms `data` to store the given value of the control. If no name is specified, a form will never contain this controls value. + * Defines the `name` attribute of the control. This is a required property, when the control should participate in a native `form`. If not specified, a native `form` will never contain this controls value. This is a required property, when the control is used with type `radio`. */ "name"?: string; /** @@ -518,7 +518,7 @@ declare namespace LocalJSX { */ "validity"?: null | 'true' | 'false'; /** - * Defines the `value` attribute of the control. This is only used, when the control participates in the native `form`. + * Defines the `value` attribute of the control. This is a required property, when the control is used with type `radio`. */ "value"?: string; } diff --git a/packages/components/src/components/post-card-control/post-card-control.tsx b/packages/components/src/components/post-card-control/post-card-control.tsx index 9d23b402e2..a887a2a456 100644 --- a/packages/components/src/components/post-card-control/post-card-control.tsx +++ b/packages/components/src/components/post-card-control/post-card-control.tsx @@ -16,6 +16,8 @@ import { version } from '../../../package.json'; let cardControlIds = 0; +// TODO: add integration for error message as soon as #2625 is implemented + /** * @class PostCardControl - representing a stencil component * @@ -74,12 +76,13 @@ export class PostCardControl { /** * Defines the `name` attribute of the control. - * The name is used in a forms `data` to store the given value of the control. If no name is specified, a form will never contain this controls value. + * This is a required property, when the control should participate in a native `form`. If not specified, a native `form` will never contain this controls value. + * This is a required property, when the control is used with type `radio`. */ @Prop() readonly name: string = null; /** - * Defines the `value` attribute of the control. This is only used, when the control participates in the native `form`. + * Defines the `value` attribute of the control. This is a required property, when the control is used with type `radio`. */ @Prop() readonly value: string = null; diff --git a/packages/components/src/components/post-card-control/readme.md b/packages/components/src/components/post-card-control/readme.md index fa21db9fb7..6853375dd7 100644 --- a/packages/components/src/components/post-card-control/readme.md +++ b/packages/components/src/components/post-card-control/readme.md @@ -7,17 +7,17 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| -------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | ----------- | -| `checked` | `checked` | Defines the `checked` attribute of the control. If `true`, the control is selected at its value will be included in the forms data. | `boolean` | `false` | -| `description` | `description` | Defines the description in the control-label. | `string` | `null` | -| `disabled` | `disabled` | Defines the `disabled` attribute of the control. If `true`, the user can not interact with the control and the controls value will not be included in the forms data. | `boolean` | `false` | -| `icon` | `icon` | Defines the icon `name` inside of the card. If not set the icon will not show up. | `string` | `null` | -| `label` _(required)_ | `label` | Defines the text in the control-label. | `string` | `undefined` | -| `name` | `name` | Defines the `name` attribute of the control. The name is used in a forms `data` to store the given value of the control. If no name is specified, a form will never contain this controls value. | `string` | `null` | -| `type` _(required)_ | `type` | Defines the `type` attribute of the control. | `"checkbox" \| "radio"` | `undefined` | -| `validity` | `validity` | Defines the validation `validity` of the control. To reset validity to an undefiend state, simply remove the attribute from the control. | `"false" \| "true"` | `null` | -| `value` | `value` | Defines the `value` attribute of the control. This is only used, when the control participates in the native `form`. | `string` | `null` | +| Property | Attribute | Description | Type | Default | +| -------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------- | ----------- | +| `checked` | `checked` | Defines the `checked` attribute of the control. If `true`, the control is selected at its value will be included in the forms data. | `boolean` | `false` | +| `description` | `description` | Defines the description in the control-label. | `string` | `null` | +| `disabled` | `disabled` | Defines the `disabled` attribute of the control. If `true`, the user can not interact with the control and the controls value will not be included in the forms data. | `boolean` | `false` | +| `icon` | `icon` | Defines the icon `name` inside of the card. If not set the icon will not show up. | `string` | `null` | +| `label` _(required)_ | `label` | Defines the text in the control-label. | `string` | `undefined` | +| `name` | `name` | Defines the `name` attribute of the control. This is a required property, when the control should participate in a native `form`. If not specified, a native `form` will never contain this controls value. This is a required property, when the control is used with type `radio`. | `string` | `null` | +| `type` _(required)_ | `type` | Defines the `type` attribute of the control. | `"checkbox" \| "radio"` | `undefined` | +| `validity` | `validity` | Defines the validation `validity` of the control. To reset validity to an undefiend state, simply remove the attribute from the control. | `"false" \| "true"` | `null` | +| `value` | `value` | Defines the `value` attribute of the control. This is a required property, when the control is used with type `radio`. | `string` | `null` | ## Events diff --git a/packages/documentation/src/stories/components/card-control/card-control.docs.mdx b/packages/documentation/src/stories/components/card-control/card-control.docs.mdx index 495ef8056d..5ffb5f72e6 100644 --- a/packages/documentation/src/stories/components/card-control/card-control.docs.mdx +++ b/packages/documentation/src/stories/components/card-control/card-control.docs.mdx @@ -61,8 +61,8 @@ Just add the same `name` attribute value to multiple `Update the control and submit or reset the form to see how its FormData value changes.

From 26193c2aa74dbeb8284ccbf0ebace230a1447f5b Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Fri, 16 Feb 2024 10:23:44 +0100 Subject: [PATCH 45/82] feat(documentation): add uuid for card-control story --- .../src/stories/components/card-control/card-control.stories.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/documentation/src/stories/components/card-control/card-control.stories.ts b/packages/documentation/src/stories/components/card-control/card-control.stories.ts index 405aee0a91..890c54c240 100644 --- a/packages/documentation/src/stories/components/card-control/card-control.stories.ts +++ b/packages/documentation/src/stories/components/card-control/card-control.stories.ts @@ -11,6 +11,7 @@ import scss from './card-control.module.scss'; const SCSS_VARIABLES = parse(scss); const meta: Meta = { + id: '886fabcf-148b-4054-a2ec-4869668294fb', title: 'Components/Forms/Card-Control', component: 'post-card-control', parameters: { From c9476c4265ec51d115d57d578ba5a0de45333aa2 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Fri, 16 Feb 2024 15:20:02 +0100 Subject: [PATCH 46/82] test(documentation): add card-control snapshot test --- .../components/card-control.snapshot.ts | 7 +++ .../card-control.snapshot.stories.ts | 51 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 packages/documentation/cypress/snapshots/components/card-control.snapshot.ts create mode 100644 packages/documentation/src/stories/components/card-control/card-control.snapshot.stories.ts diff --git a/packages/documentation/cypress/snapshots/components/card-control.snapshot.ts b/packages/documentation/cypress/snapshots/components/card-control.snapshot.ts new file mode 100644 index 0000000000..58bcc0fb6f --- /dev/null +++ b/packages/documentation/cypress/snapshots/components/card-control.snapshot.ts @@ -0,0 +1,7 @@ +describe('CardControl', () => { + it('default', () => { + cy.visit('/iframe.html?id=snapshots--card-control'); + cy.get('post-card-control.hydrated', { timeout: 30000 }).should('be.visible'); + cy.percySnapshot('CardControls', { widths: [1440] }); + }); +}); diff --git a/packages/documentation/src/stories/components/card-control/card-control.snapshot.stories.ts b/packages/documentation/src/stories/components/card-control/card-control.snapshot.stories.ts new file mode 100644 index 0000000000..9e664971b5 --- /dev/null +++ b/packages/documentation/src/stories/components/card-control/card-control.snapshot.stories.ts @@ -0,0 +1,51 @@ +import type { Args, StoryContext, StoryObj } from '@storybook/web-components'; +import meta, { Default } from './card-control.stories'; +import { html } from 'lit'; +import { bombArgs } from '../../../utils'; + +const { id, ...metaWithoutId } = meta; + +export default { + ...metaWithoutId, + title: 'Snapshots', +}; + +type Story = StoryObj; + +export const CardControl: Story = { + render: (_args: Args, context: StoryContext) => { + return html` +
+

CardControl

+ ${['bg-white', 'bg-dark'].map( + bg => html` +
+ ${context.argTypes.type.options.map( + (type: string) => html` +
+

type: ${type}

+ ${bombArgs({ + icon: ['1001'], + validity: ['null', true, false], + disabled: [false, true], + checked: [false, true], + type: [type], + label: ['Label'], + }).map((args: Args) => { + const description = Object.entries(args) + .filter(([key]) => !['label', 'type', 'icon'].includes(key)) + .map(([key, value]) => `${key}: ${value}`) + .join(', '); + + return Default.render?.({ ...context.args, ...args, description }, context); + })} +
+ `, + )} +
+ `, + )} +
+ `; + }, +}; From 29919ce9b3b98994c60ae32dac1378e937c11ece Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Fri, 16 Feb 2024 15:20:30 +0100 Subject: [PATCH 47/82] feat(documentation): add card-control fieldset example --- .../post-card-control/post-card-control.scss | 8 ++++++ .../card-control/card-control.stories.ts | 27 +++++++++---------- 2 files changed, 21 insertions(+), 14 deletions(-) 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 5506af2c26..e93a826bfb 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 @@ -6,6 +6,14 @@ width: 100%; } +:host-context(fieldset) { + &:host(:not(:last-child)) { + .card-control { + margin-bottom: post.$size-regular; + } + } +} + .card-control { --post-card-control-border-color: #{post.$gray-60}; --post-card-control-bg: #{post.$white}; diff --git a/packages/documentation/src/stories/components/card-control/card-control.stories.ts b/packages/documentation/src/stories/components/card-control/card-control.stories.ts index ee76e32ab9..9d01d4a2a3 100644 --- a/packages/documentation/src/stories/components/card-control/card-control.stories.ts +++ b/packages/documentation/src/stories/components/card-control/card-control.stories.ts @@ -250,23 +250,22 @@ export const LinedUp: Story = { export const RadioGroup: Story = { render: (args: Args, context: StoryContext) => html` -
+
+ Legend ${[1, 2, 3, 4, 5, 6].map( i => html` -
- ${Default.render?.( - { - label: `Radio${i}`, - type: 'radio', - name: 'RadioGroup_Name', - disabled: i > 3 && i < 6 ? true : null, - validity: args.validity, - }, - context, - )} -
+ ${Default.render?.( + { + label: `Radio${i}`, + type: 'radio', + name: 'RadioGroup_Name', + disabled: i > 3 && i < 6 ? true : null, + validity: args.validity, + }, + context, + )} `, )} -
+ `, }; From e71d908767d9b40d26a88f48115a51240141e8c4 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Fri, 16 Feb 2024 16:16:57 +0100 Subject: [PATCH 48/82] chore(documentation): update card-control lined up story --- .../card-control/card-control.docs.mdx | 5 +++-- .../card-control/card-control.stories.ts | 20 +++++++++++++++---- .../card-control/card-control.styles.scss | 5 +++++ 3 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 packages/documentation/src/stories/components/card-control/card-control.styles.scss diff --git a/packages/documentation/src/stories/components/card-control/card-control.docs.mdx b/packages/documentation/src/stories/components/card-control/card-control.docs.mdx index 1b05e411b9..b583834ffb 100644 --- a/packages/documentation/src/stories/components/card-control/card-control.docs.mdx +++ b/packages/documentation/src/stories/components/card-control/card-control.docs.mdx @@ -39,7 +39,7 @@ If this is not enough, you can also use the named `icon` slot and add your very 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.

@@ -47,7 +47,8 @@ You can use the component directly in your forms, the control value will be avai ### Lined up -Change the width of a `` component, by putting it (for example) in a grid. +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.
diff --git a/packages/documentation/src/stories/components/card-control/card-control.stories.ts b/packages/documentation/src/stories/components/card-control/card-control.stories.ts index 9d01d4a2a3..25428c7bd4 100644 --- a/packages/documentation/src/stories/components/card-control/card-control.stories.ts +++ b/packages/documentation/src/stories/components/card-control/card-control.stories.ts @@ -4,10 +4,9 @@ import { BADGE } from '../../../../.storybook/constants'; import { html, nothing } from 'lit'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; import { parse } from '../../../utils/sass-export'; +import './card-control.styles.scss'; import scss from './card-control.module.scss'; -// TODO: fieldset and legend example - const SCSS_VARIABLES = parse(scss); const meta: Meta = { @@ -85,6 +84,7 @@ export const Default: Story = { return html` ${Default.render?.( { + class: args.fullHeight ? 'h-100' : null, label: `Checkbox${i}`, description: i === 6 ? '20.- per year' : null, type: args.type, diff --git a/packages/documentation/src/stories/components/card-control/card-control.styles.scss b/packages/documentation/src/stories/components/card-control/card-control.styles.scss new file mode 100644 index 0000000000..dfff789c5a --- /dev/null +++ b/packages/documentation/src/stories/components/card-control/card-control.styles.scss @@ -0,0 +1,5 @@ +#story--886fabcf-148b-4054-a2ec-4869668294fb--lined-up { + post-card-control { + visibility: visible; + } +} From df7773c4fba2376356784d09be4401fa46582cd4 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Fri, 16 Feb 2024 16:17:45 +0100 Subject: [PATCH 49/82] feat(components): add the possibility to stretch all card-control elements in a grid row to the maximum height --- .../src/components/post-card-control/post-card-control.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 e93a826bfb..143484136e 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 @@ -2,7 +2,7 @@ @use '@swisspost/design-system-styles/components/form-check'; :host { - display: block; + display: flex; width: 100%; } @@ -21,6 +21,7 @@ --post-card-control-input-border-color: #{post.$gray-80}; --post-card-control-input-bg: #{post.$white}; + flex-basis: 100%; display: flex; gap: 0 post.$size-mini; padding: post.$size-regular; From 86194c61bf95df2216dc9179a1f630a99cca083e Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Tue, 20 Feb 2024 07:18:13 +0100 Subject: [PATCH 50/82] chore(components): fix codesmells reported by sonar --- .../components/cypress/e2e/card-control.cy.ts | 9 ++-- ...st-card-control.form-association.test.html | 18 +++++++- .../post-card-control.radio-group.test.html | 45 ------------------- .../post-card-control/post-card-control.tsx | 1 - 4 files changed, 21 insertions(+), 52 deletions(-) delete mode 100644 packages/components/cypress/fixtures/post-card-control.radio-group.test.html diff --git a/packages/components/cypress/e2e/card-control.cy.ts b/packages/components/cypress/e2e/card-control.cy.ts index 7f19ba8792..bdd28e58a6 100644 --- a/packages/components/cypress/e2e/card-control.cy.ts +++ b/packages/components/cypress/e2e/card-control.cy.ts @@ -299,10 +299,10 @@ describe('card-control', () => { cy.visit('./cypress/fixtures/post-card-control.form-association.test.html'); cy.get('form#AssociatedForm').as('form'); - cy.get('@form').find('fieldset').as('fieldset'); cy.get('@form').find('button[type="reset"]').as('reset'); cy.get('@form').find('button[type="submit"]').as('submit'); - cy.get('@form').find('post-card-control').as('card-control'); + cy.get('@form').find('fieldset#FieldsetCheckbox').as('fieldset'); + cy.get('@fieldset').find('post-card-control').as('card-control'); cy.get('@card-control').find('.card-control').as('wrapper'); cy.get('@card-control').find('input.card-control--input').as('input'); cy.get('@card-control').find('label.card-control--label').as('label'); @@ -373,10 +373,11 @@ describe('card-control', () => { describe('radio group', { baseUrl: null, includeShadowDom: true }, () => { beforeEach(() => { - cy.visit('./cypress/fixtures/post-card-control.radio-group.test.html'); + cy.visit('./cypress/fixtures/post-card-control.form-association.test.html'); cy.get('form#AssociatedForm').as('form'); - cy.get('@form').find('post-card-control[name="CardControlGroup"]').as('card-control'); + cy.get('@form').find('fieldset#FieldsetRadioGroup').as('fieldset'); + cy.get('@fieldset').find('post-card-control[name="CardControlGroup"]').as('card-control'); cy.get('@card-control').find('.card-control').as('wrapper'); cy.get('@wrapper').find('input.card-control--input').as('input'); }); diff --git a/packages/components/cypress/fixtures/post-card-control.form-association.test.html b/packages/components/cypress/fixtures/post-card-control.form-association.test.html index 701e72d977..8a06dfcfb3 100644 --- a/packages/components/cypress/fixtures/post-card-control.form-association.test.html +++ b/packages/components/cypress/fixtures/post-card-control.form-association.test.html @@ -23,10 +23,24 @@ -
- Legend +
+ Legend Checkbox
+ +
+ Legend RadioGroup + + + + + + + + + + +
diff --git a/packages/components/cypress/fixtures/post-card-control.radio-group.test.html b/packages/components/cypress/fixtures/post-card-control.radio-group.test.html deleted file mode 100644 index 7fc84d356f..0000000000 --- a/packages/components/cypress/fixtures/post-card-control.radio-group.test.html +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - Document - - - - - -
- Legend - - - - - - - - - - -
-
- - -
- - - diff --git a/packages/components/src/components/post-card-control/post-card-control.tsx b/packages/components/src/components/post-card-control/post-card-control.tsx index a887a2a456..0d9202bdbf 100644 --- a/packages/components/src/components/post-card-control/post-card-control.tsx +++ b/packages/components/src/components/post-card-control/post-card-control.tsx @@ -222,7 +222,6 @@ export class PostCardControl { if (this.disabled) { this.internals.setFormValue(null); - return; } else { this.checked = this.control.checked = checked; this.internals.setFormValue(this.checked ? this.control.value : null); From 79936b79da165f0c0478217cc6342e1eec8dc85c Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Tue, 20 Feb 2024 08:42:13 +0100 Subject: [PATCH 51/82] chore(components): add post-card-control to exported members --- packages/components/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 1f5c23ed4d..cc6c29d3f4 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -4,6 +4,7 @@ export { Components, JSX } from './components'; export { PostAccordion } from './components/post-accordion/post-accordion'; export { PostAccordionItem } from './components/post-accordion-item/post-accordion-item'; export { PostAlert } from './components/post-alert/post-alert'; +export { PostCardControl } from './components/post-card-control/post-card-control'; export { PostCollapsible } from './components/post-collapsible/post-collapsible'; export { PostIcon } from './components/post-icon/post-icon'; export { PostPopover } from './components/post-popover/post-popover'; From f9d880c851e851ec1892d2f85a69ed99a8947618 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Tue, 20 Feb 2024 10:44:36 +0100 Subject: [PATCH 52/82] chore(components): update eslint config --- packages/components/.eslintrc.js | 30 +------------------ .../post-card-control/post-card-control.tsx | 2 +- 2 files changed, 2 insertions(+), 30 deletions(-) diff --git a/packages/components/.eslintrc.js b/packages/components/.eslintrc.js index 11f03ada2b..6e6dd46738 100644 --- a/packages/components/.eslintrc.js +++ b/packages/components/.eslintrc.js @@ -29,6 +29,7 @@ module.exports = { ], 'quotes': ['error', 'single'], 'semi': ['error', 'always'], + 'react/jsx-no-bind': 'off', '@typescript-eslint/no-unused-vars': [ 'error', { @@ -39,40 +40,11 @@ module.exports = { ], '@stencil-community/strict-boolean-conditions': 'off', '@stencil-community/required-prefix': ['error', ['post-']], - '@stencil-community/async-methods': 'error', - '@stencil-community/decorators-context': 'error', - '@stencil-community/decorators-style': [ - 'error', - { - prop: 'inline', - state: 'inline', - element: 'inline', - event: 'inline', - method: 'multiline', - watch: 'multiline', - listen: 'multiline', - }, - ], '@stencil-community/class-pattern': [ 'error', { pattern: '^Post.*(?!Component)$', }, ], - '@stencil-community/element-type': 'error', - '@stencil-community/host-data-deprecated': 'error', - '@stencil-community/methods-must-be-public': 'error', - '@stencil-community/no-unused-watch': 'error', - '@stencil-community/own-methods-must-be-private': 'error', - '@stencil-community/own-props-must-be-private': 'error', - '@stencil-community/prefer-vdom-listener': 'error', - '@stencil-community/props-must-be-public': 'error', - '@stencil-community/props-must-be-readonly': 'error', - '@stencil-community/render-returns-host': 'error', - '@stencil-community/required-jsdoc': 'error', - '@stencil-community/reserved-member-names': 'error', - '@stencil-community/single-export': 'error', - '@stencil-community/strict-mutable': 'error', - 'react/jsx-no-bind': 'off', }, }; diff --git a/packages/components/src/components/post-card-control/post-card-control.tsx b/packages/components/src/components/post-card-control/post-card-control.tsx index 0d9202bdbf..1636ed3ce7 100644 --- a/packages/components/src/components/post-card-control/post-card-control.tsx +++ b/packages/components/src/components/post-card-control/post-card-control.tsx @@ -366,7 +366,7 @@ export class PostCardControl { this.disabled = disabled; } - formStateRestoreCallback(checked, _reason: 'restore' | 'autocomplete') { + formStateRestoreCallback(checked) { this.controlSetChecked(checked); } From 44f00025969461459d2b7598a9c079f93feab01e Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Tue, 20 Feb 2024 10:48:11 +0100 Subject: [PATCH 53/82] chore(components): remove todo comment reported by sonar --- .../src/components/post-card-control/post-card-control.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/components/src/components/post-card-control/post-card-control.tsx b/packages/components/src/components/post-card-control/post-card-control.tsx index 1636ed3ce7..9ba3527e96 100644 --- a/packages/components/src/components/post-card-control/post-card-control.tsx +++ b/packages/components/src/components/post-card-control/post-card-control.tsx @@ -16,8 +16,6 @@ import { version } from '../../../package.json'; let cardControlIds = 0; -// TODO: add integration for error message as soon as #2625 is implemented - /** * @class PostCardControl - representing a stencil component * From f020ab67cbcdce9c7d6990128bf4f07b4901a8fe Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Tue, 20 Feb 2024 13:31:42 +0100 Subject: [PATCH 54/82] chore: add changesets --- .changeset/fair-singers-return.md | 5 +++++ .changeset/shaggy-files-protect.md | 7 +++++++ 2 files changed, 12 insertions(+) create mode 100644 .changeset/fair-singers-return.md create mode 100644 .changeset/shaggy-files-protect.md diff --git a/.changeset/fair-singers-return.md b/.changeset/fair-singers-return.md new file mode 100644 index 0000000000..96e937a631 --- /dev/null +++ b/.changeset/fair-singers-return.md @@ -0,0 +1,5 @@ +--- +'@swisspost/design-system-styles': patch +--- + +Fixed the `.form-check-input` background-color, by setting it to white on none or light backgrounds. diff --git a/.changeset/shaggy-files-protect.md b/.changeset/shaggy-files-protect.md new file mode 100644 index 0000000000..4e3c6e9382 --- /dev/null +++ b/.changeset/shaggy-files-protect.md @@ -0,0 +1,7 @@ +--- +'@swisspost/design-system-components-angular-workspace': minor +'@swisspost/design-system-documentation': minor +'@swisspost/design-system-components': minor +--- + +Added a new web-component `post-card-control`, which works like a native `input[type="checkbox"]` or `input[type="radio"]` but with a custom visual design. From b5e884709589e213b32586a6c42fac3188158e60 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Tue, 20 Feb 2024 13:32:16 +0100 Subject: [PATCH 55/82] feat(components-angular): add `post-card-control` example to the consumer-app --- .../projects/consumer-app/src/app/app.component.html | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/components-angular/projects/consumer-app/src/app/app.component.html b/packages/components-angular/projects/consumer-app/src/app/app.component.html index 05ddaa8452..aad7477fa7 100644 --- a/packages/components-angular/projects/consumer-app/src/app/app.component.html +++ b/packages/components-angular/projects/consumer-app/src/app/app.component.html @@ -82,3 +82,8 @@

Post Tooltip

Hi there 👋
+ +
+

Post Card-Control

+ +
From 7f8ab0cef09a523deac337de5d09509dc3eb0ac8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Sch=C3=BCrch?= Date: Thu, 22 Feb 2024 07:46:34 +0100 Subject: [PATCH 56/82] Update packages/components/src/components.d.ts 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> --- packages/components/src/components.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/src/components.d.ts b/packages/components/src/components.d.ts index 2c26ff0648..17ab99da16 100644 --- a/packages/components/src/components.d.ts +++ b/packages/components/src/components.d.ts @@ -95,7 +95,7 @@ export namespace Components { */ "label": string; /** - * Defines the `name` attribute of the control. This is a required property, when the control should participate in a native `form`. If not specified, a native `form` will never contain this controls value. This is a required property, when the control is used with type `radio`. + * Defines the `name` attribute of the control. This is a required property, when the control is used in a native `form`. If not specified, a native `form` will never contain this controls value. This is a required property, when the control is used with type `radio`. */ "name": string; /** From 5698dcadeddba7062dc03d265340737c84f39186 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Sch=C3=BCrch?= Date: Thu, 22 Feb 2024 07:48:57 +0100 Subject: [PATCH 57/82] Update packages/documentation/src/stories/components/card-control/card-control.docs.mdx 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> --- .../src/stories/components/card-control/card-control.docs.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/documentation/src/stories/components/card-control/card-control.docs.mdx b/packages/documentation/src/stories/components/card-control/card-control.docs.mdx index b583834ffb..da71705101 100644 --- a/packages/documentation/src/stories/components/card-control/card-control.docs.mdx +++ b/packages/documentation/src/stories/components/card-control/card-control.docs.mdx @@ -31,7 +31,7 @@ The `` element is part of the `@swisspost/design-system-compo 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. -
If you use the `icon` slot, 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.
From 6615ff5b6ce12d2d0a5cfbe32f4651a89d53978a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Sch=C3=BCrch?= Date: Thu, 22 Feb 2024 07:49:51 +0100 Subject: [PATCH 58/82] Update packages/documentation/src/stories/components/card-control/card-control.stories.ts 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> --- .../src/stories/components/card-control/card-control.stories.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/documentation/src/stories/components/card-control/card-control.stories.ts b/packages/documentation/src/stories/components/card-control/card-control.stories.ts index 25428c7bd4..9817eab867 100644 --- a/packages/documentation/src/stories/components/card-control/card-control.stories.ts +++ b/packages/documentation/src/stories/components/card-control/card-control.stories.ts @@ -140,7 +140,7 @@ export const DarkBackground: Story = { export const CustomIcon: Story = { args: { slotIcon: - '', + '', }, render: Default.render, }; From d18ea7b1f39756f910146389dce2e4ad2ac33fb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20Sch=C3=BCrch?= Date: Thu, 22 Feb 2024 07:50:07 +0100 Subject: [PATCH 59/82] Update packages/documentation/src/stories/components/card-control/card-control.docs.mdx 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> --- .../src/stories/components/card-control/card-control.docs.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/documentation/src/stories/components/card-control/card-control.docs.mdx b/packages/documentation/src/stories/components/card-control/card-control.docs.mdx index da71705101..3e5470f7c3 100644 --- a/packages/documentation/src/stories/components/card-control/card-control.docs.mdx +++ b/packages/documentation/src/stories/components/card-control/card-control.docs.mdx @@ -28,7 +28,7 @@ The `` element is part of the `@swisspost/design-system-compo ### Custom icon -You can use our built in icons by just adding the `icon` property with the name of the desired 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.
From 95176a795e87c843fc3e89c7808b7bdcb4da81e6 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Thu, 22 Feb 2024 09:53:47 +0100 Subject: [PATCH 60/82] chore(documentation): remove `needs_revision` badge --- .../src/stories/components/card-control/card-control.stories.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/documentation/src/stories/components/card-control/card-control.stories.ts b/packages/documentation/src/stories/components/card-control/card-control.stories.ts index 9817eab867..4631c768b4 100644 --- a/packages/documentation/src/stories/components/card-control/card-control.stories.ts +++ b/packages/documentation/src/stories/components/card-control/card-control.stories.ts @@ -14,7 +14,7 @@ const meta: Meta = { title: 'Components/Forms/Card-Control', component: 'post-card-control', parameters: { - badges: [BADGE.NEEDS_REVISION, BADGE.SINCE_V1], + badges: [BADGE.BETA], }, args: { label: 'Label', From 538006982b360034649901ce294af33b81bc942b Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Thu, 22 Feb 2024 09:54:17 +0100 Subject: [PATCH 61/82] chore(components): add card-control comment for style order --- .../src/components/post-card-control/post-card-control.scss | 2 ++ 1 file changed, 2 insertions(+) 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 143484136e..ca2e5b0786 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 @@ -75,6 +75,7 @@ &:not(.is-disabled) { // order matters! + // because we only overwrite the props, which need to be different from one selector to the other. &.is-checked { --post-card-control-border-color: #{post.$black}; @@ -128,6 +129,7 @@ &:not(.is-disabled) { // order matters! + // because we only overwrite the props, which need to be different from one selector to the other. &.is-checked { --post-card-control-border-color: #{post.$yellow}; From e4ac0ee03affa71bdec64d2f28f7ef3ef3676d3e Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Thu, 22 Feb 2024 09:54:43 +0100 Subject: [PATCH 62/82] chore(components): update some generated files --- packages/components/src/components.d.ts | 2 +- pnpm-lock.yaml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/components/src/components.d.ts b/packages/components/src/components.d.ts index 17ab99da16..2c26ff0648 100644 --- a/packages/components/src/components.d.ts +++ b/packages/components/src/components.d.ts @@ -95,7 +95,7 @@ export namespace Components { */ "label": string; /** - * Defines the `name` attribute of the control. This is a required property, when the control is used in a native `form`. If not specified, a native `form` will never contain this controls value. This is a required property, when the control is used with type `radio`. + * Defines the `name` attribute of the control. This is a required property, when the control should participate in a native `form`. If not specified, a native `form` will never contain this controls value. This is a required property, when the control is used with type `radio`. */ "name": string; /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc52792cb2..cef9dd6f8c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16249,6 +16249,8 @@ packages: peerDependenciesMeta: webpack: optional: true + webpack-sources: + optional: true dependencies: webpack: 5.88.2(esbuild@0.18.17) webpack-sources: 3.2.3 From 4ecf48aed6042a949b517bdfb9f6608a4422fefb Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Thu, 22 Feb 2024 10:00:25 +0100 Subject: [PATCH 63/82] feat(components): add a111y test for card-control --- .../components/cypress/e2e/card-control.cy.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/components/cypress/e2e/card-control.cy.ts b/packages/components/cypress/e2e/card-control.cy.ts index bdd28e58a6..79dcc3510e 100644 --- a/packages/components/cypress/e2e/card-control.cy.ts +++ b/packages/components/cypress/e2e/card-control.cy.ts @@ -1,7 +1,7 @@ const CARDCONTROL_ID = '886fabcf-148b-4054-a2ec-4869668294fb'; -describe('card-control', () => { - describe('structure & props', () => { +describe('Card-Control', () => { + describe('Structure & Props', () => { beforeEach(() => { cy.getComponent('card-control', CARDCONTROL_ID); cy.window().then(win => { @@ -163,7 +163,7 @@ describe('card-control', () => { }); }); - describe('events', () => { + describe('Events', () => { beforeEach(() => { cy.getComponent('card-control', CARDCONTROL_ID); @@ -270,7 +270,7 @@ describe('card-control', () => { }); }); - describe('methods', () => { + describe('Methods', () => { beforeEach(() => { cy.getComponent('card-control', CARDCONTROL_ID); @@ -294,7 +294,7 @@ describe('card-control', () => { }); }); - describe('form association', { baseUrl: null, includeShadowDom: true }, () => { + describe('Form Association', { baseUrl: null, includeShadowDom: true }, () => { beforeEach(() => { cy.visit('./cypress/fixtures/post-card-control.form-association.test.html'); @@ -371,7 +371,7 @@ describe('card-control', () => { }); }); - describe('radio group', { baseUrl: null, includeShadowDom: true }, () => { + describe('Radio Group', { baseUrl: null, includeShadowDom: true }, () => { beforeEach(() => { cy.visit('./cypress/fixtures/post-card-control.form-association.test.html'); @@ -499,4 +499,11 @@ describe('card-control', () => { }); }); }); + + describe('Accessibility', () => { + it('Has no detectable a11y violations on load for all variants', () => { + cy.getSnapshots('card-control'); + cy.checkA11y('#root-inner'); + }); + }); }); From 657038351ec93b5128d90d564ffd8d899cb0f9e1 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Fri, 1 Mar 2024 15:49:06 +0100 Subject: [PATCH 64/82] feat(components): add css modules for components package --- packages/components/package.json | 1 + packages/components/stencil.config.ts | 15 + packages/components/tsconfig.json | 14 +- packages/components/types/css-modules.ts | 4 + pnpm-lock.yaml | 613 ++++++++++++++++++++++- 5 files changed, 632 insertions(+), 15 deletions(-) create mode 100644 packages/components/types/css-modules.ts diff --git a/packages/components/package.json b/packages/components/package.json index 3572ba9ecd..c02081e6a5 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -63,6 +63,7 @@ "eslint": "8.56.0", "eslint-plugin-react": "7.33.2", "rimraf": "5.0.5", + "rollup-plugin-postcss": "4.0.2", "sass": "1.70.0", "ts-jest": "29.1.2", "typescript": "4.9.5" diff --git a/packages/components/stencil.config.ts b/packages/components/stencil.config.ts index 2371a621f3..2f06439e84 100644 --- a/packages/components/stencil.config.ts +++ b/packages/components/stencil.config.ts @@ -1,5 +1,6 @@ import { Config } from '@stencil/core'; import { sass } from '@stencil/sass'; +import postcss from 'rollup-plugin-postcss'; import { reactOutputTarget } from '@stencil/react-output-target'; import { angularOutputTarget } from '@stencil/angular-output-target'; import { angularValueAccessorBindings } from './.config/bindings.angular'; @@ -50,6 +51,20 @@ export const config: Config = { includePaths: ['node_modules'], }), ], + rollupPlugins: { + before: [ + postcss({ + modules: true, + use: { + sass: { + includePaths: ['node_modules'], + }, + stylus: false, + less: false, + }, + }), + ], + }, testing: { testPathIgnorePatterns: [ '/dist/', diff --git a/packages/components/tsconfig.json b/packages/components/tsconfig.json index 46d74c1d44..1ddb3014e8 100644 --- a/packages/components/tsconfig.json +++ b/packages/components/tsconfig.json @@ -4,10 +4,7 @@ "allowUnreachableCode": false, "declaration": false, "experimentalDecorators": true, - "lib": [ - "dom", - "es2017" - ], + "lib": ["dom", "es2017"], "moduleResolution": "node", "module": "esnext", "target": "es2017", @@ -18,11 +15,6 @@ "resolveJsonModule": true, "skipLibCheck": true }, - "include": [ - "src" - ], - "exclude": [ - "node_modules", - "**/tests" - ] + "include": ["src", "types"], + "exclude": ["node_modules", "**/tests"] } diff --git a/packages/components/types/css-modules.ts b/packages/components/types/css-modules.ts new file mode 100644 index 0000000000..428dc96e0f --- /dev/null +++ b/packages/components/types/css-modules.ts @@ -0,0 +1,4 @@ +declare module '*.scss' { + const content: { [key: string]: string }; + export default content; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6392ee452a..b6dd29ffe6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -127,6 +127,9 @@ importers: rimraf: specifier: 5.0.5 version: 5.0.5 + rollup-plugin-postcss: + specifier: 4.0.2 + version: 4.0.2(postcss@8.4.33) sass: specifier: 1.70.0 version: 1.70.0 @@ -6641,8 +6644,8 @@ packages: rollup: 3.26.2 dev: true - /@rollup/plugin-node-resolve@15.0.2(rollup@3.26.2): - resolution: {integrity: sha512-Y35fRGUjC3FaurG722uhUuG8YHOJRJQbI6/CkbRkdPotSpDj9NtIN85z1zrcyDcCQIW4qp5mgG72U+gJ0TAFEg==} + /@rollup/plugin-node-resolve@15.0.0(rollup@3.26.2): + resolution: {integrity: sha512-iwJbzfTzlzDDQcGmkS7EkCKwe2kSkdBrjX87Fy/KrNjr6UNnLpod0t6X66e502LRe5JJCA4FFqrEscWPnZAkig==} engines: {node: '>=14.0.0'} peerDependencies: rollup: ^2.78.0||^3.0.0 @@ -6650,7 +6653,7 @@ packages: rollup: optional: true dependencies: - '@rollup/pluginutils': 5.0.2(rollup@3.26.2) + '@rollup/pluginutils': 4.2.1 '@types/resolve': 1.20.2 deepmerge: 4.3.1 is-builtin-module: 3.2.1 @@ -6659,6 +6662,14 @@ packages: rollup: 3.26.2 dev: true + /@rollup/pluginutils@4.2.1: + resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} + engines: {node: '>= 8.0.0'} + dependencies: + estree-walker: 2.0.2 + picomatch: 2.3.1 + dev: true + /@rollup/pluginutils@5.0.2(rollup@3.26.2): resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==} engines: {node: '>=14.0.0'} @@ -10247,6 +10258,15 @@ packages: engines: {node: '>=10'} dev: true + /caniuse-api@3.0.0: + resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} + dependencies: + browserslist: 4.22.2 + caniuse-lite: 1.0.30001580 + lodash.memoize: 4.1.2 + lodash.uniq: 4.5.0 + dev: true + /caniuse-lite@1.0.30001580: resolution: {integrity: sha512-mtj5ur2FFPZcCEpXFy8ADXbDACuNFXg6mxVDqp7tqooX6l3zwm+d8EPoeOSIFRDvHs8qu7/SLFOGniULkcH2iA==} @@ -10654,6 +10674,12 @@ packages: typedarray: 0.0.6 dev: true + /concat-with-sourcemaps@1.1.0: + resolution: {integrity: sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==} + dependencies: + source-map: 0.6.1 + dev: true + /connect-history-api-fallback@2.0.0: resolution: {integrity: sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==} engines: {node: '>=0.8'} @@ -10886,6 +10912,15 @@ packages: engines: {node: '>=8'} dev: true + /css-declaration-sorter@6.4.1(postcss@8.4.33): + resolution: {integrity: sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g==} + engines: {node: ^10 || ^12 || >=14} + peerDependencies: + postcss: '>=8.4.31' + dependencies: + postcss: 8.4.33 + dev: true + /css-functions-list@3.2.1: resolution: {integrity: sha512-Nj5YcaGgBtuUmn1D7oHqPW0c9iui7xsTsj5lIX8ZgevdfhmjFfKB3r8moHJtNJnctnYXJyYX5I1pp90HM4TPgQ==} engines: {node: '>=12 || >=16'} @@ -10908,6 +10943,16 @@ packages: webpack: 5.88.2(esbuild@0.18.17) dev: true + /css-select@4.3.0: + resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 4.3.1 + domutils: 2.8.0 + nth-check: 2.1.1 + dev: true + /css-select@5.1.0: resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} dependencies: @@ -10917,6 +10962,14 @@ packages: domutils: 3.1.0 nth-check: 2.1.1 + /css-tree@1.1.3: + resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==} + engines: {node: '>=8.0.0'} + dependencies: + mdn-data: 2.0.14 + source-map: 0.6.1 + dev: true + /css-tree@2.2.1: resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} @@ -10947,6 +11000,72 @@ packages: hasBin: true dev: true + /cssnano-preset-default@5.2.14(postcss@8.4.33): + resolution: {integrity: sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: '>=8.4.31' + dependencies: + css-declaration-sorter: 6.4.1(postcss@8.4.33) + cssnano-utils: 3.1.0(postcss@8.4.33) + postcss: 8.4.33 + postcss-calc: 8.2.4(postcss@8.4.33) + postcss-colormin: 5.3.1(postcss@8.4.33) + postcss-convert-values: 5.1.3(postcss@8.4.33) + postcss-discard-comments: 5.1.2(postcss@8.4.33) + postcss-discard-duplicates: 5.1.0(postcss@8.4.33) + postcss-discard-empty: 5.1.1(postcss@8.4.33) + postcss-discard-overridden: 5.1.0(postcss@8.4.33) + postcss-merge-longhand: 5.1.7(postcss@8.4.33) + postcss-merge-rules: 5.1.4(postcss@8.4.33) + postcss-minify-font-values: 5.1.0(postcss@8.4.33) + postcss-minify-gradients: 5.1.1(postcss@8.4.33) + postcss-minify-params: 5.1.4(postcss@8.4.33) + postcss-minify-selectors: 5.2.1(postcss@8.4.33) + postcss-normalize-charset: 5.1.0(postcss@8.4.33) + postcss-normalize-display-values: 5.1.0(postcss@8.4.33) + postcss-normalize-positions: 5.1.1(postcss@8.4.33) + postcss-normalize-repeat-style: 5.1.1(postcss@8.4.33) + postcss-normalize-string: 5.1.0(postcss@8.4.33) + postcss-normalize-timing-functions: 5.1.0(postcss@8.4.33) + postcss-normalize-unicode: 5.1.1(postcss@8.4.33) + postcss-normalize-url: 5.1.0(postcss@8.4.33) + postcss-normalize-whitespace: 5.1.1(postcss@8.4.33) + postcss-ordered-values: 5.1.3(postcss@8.4.33) + postcss-reduce-initial: 5.1.2(postcss@8.4.33) + postcss-reduce-transforms: 5.1.0(postcss@8.4.33) + postcss-svgo: 5.1.0(postcss@8.4.33) + postcss-unique-selectors: 5.1.1(postcss@8.4.33) + dev: true + + /cssnano-utils@3.1.0(postcss@8.4.33): + resolution: {integrity: sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: '>=8.4.31' + dependencies: + postcss: 8.4.33 + dev: true + + /cssnano@5.1.15(postcss@8.4.33): + resolution: {integrity: sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: '>=8.4.31' + dependencies: + cssnano-preset-default: 5.2.14(postcss@8.4.33) + lilconfig: 2.1.0 + postcss: 8.4.33 + yaml: 1.10.2 + dev: true + + /csso@4.2.0: + resolution: {integrity: sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==} + engines: {node: '>=8.0.0'} + dependencies: + css-tree: 1.1.3 + dev: true + /csso@5.0.5: resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} @@ -11477,6 +11596,14 @@ packages: void-elements: 2.0.1 dev: true + /dom-serializer@1.4.1: + resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + entities: 2.2.0 + dev: true + /dom-serializer@2.0.0: resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} dependencies: @@ -11507,12 +11634,27 @@ packages: webidl-conversions: 7.0.0 dev: true + /domhandler@4.3.1: + resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} + engines: {node: '>= 4'} + dependencies: + domelementtype: 2.3.0 + dev: true + /domhandler@5.0.3: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} dependencies: domelementtype: 2.3.0 + /domutils@2.8.0: + resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} + dependencies: + dom-serializer: 1.4.1 + domelementtype: 2.3.0 + domhandler: 4.3.1 + dev: true + /domutils@3.1.0: resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} dependencies: @@ -11660,6 +11802,10 @@ packages: resolution: {integrity: sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==} dev: true + /entities@2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + dev: true + /entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -13391,6 +13537,12 @@ packages: wide-align: 1.1.5 dev: true + /generic-names@4.0.0: + resolution: {integrity: sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A==} + dependencies: + loader-utils: 3.2.1 + dev: true + /gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -14291,6 +14443,10 @@ packages: safer-buffer: 2.1.2 dev: true + /icss-replace-symbols@1.1.0: + resolution: {integrity: sha512-chIaY3Vh2mh2Q3RGXttaDIzeiPvaVXJ+C4DAh/w3c37SKZ/U6PGMmuicR2EQQp9bKG8zLMCl7I+PtIoOOPp8Gg==} + dev: true + /icss-utils@5.1.0(postcss@8.4.33): resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} engines: {node: ^10 || ^12 || >= 14} @@ -14344,6 +14500,13 @@ packages: /immutable@4.3.0: resolution: {integrity: sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==} + /import-cwd@3.0.0: + resolution: {integrity: sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==} + engines: {node: '>=8'} + dependencies: + import-from: 3.0.0 + dev: true + /import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} @@ -14352,6 +14515,13 @@ packages: resolve-from: 4.0.0 dev: true + /import-from@3.0.0: + resolution: {integrity: sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==} + engines: {node: '>=8'} + dependencies: + resolve-from: 5.0.0 + dev: true + /import-local@3.1.0: resolution: {integrity: sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==} engines: {node: '>=8'} @@ -16265,6 +16435,8 @@ packages: peerDependenciesMeta: webpack: optional: true + webpack-sources: + optional: true dependencies: webpack: 5.88.2(esbuild@0.18.17) webpack-sources: 3.2.3 @@ -16439,6 +16611,10 @@ packages: p-locate: 6.0.0 dev: true + /lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + dev: true + /lodash.clonedeep@4.5.0: resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} dev: true @@ -16467,6 +16643,10 @@ packages: resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} dev: true + /lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + dev: true + /lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} dev: true @@ -16716,6 +16896,10 @@ packages: resolution: {integrity: sha512-jVU0Nr2B9X3MU4tSK7JP1CMkSvOj7X5l/GboG1tKRw52lLF1x2Ju92Ms9tNetCcbfX3hzlM73zYo2NKkWSfF/A==} dev: true + /mdn-data@2.0.14: + resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} + dev: true + /mdn-data@2.0.28: resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} dev: true @@ -17206,7 +17390,7 @@ packages: dependencies: '@angular/compiler-cli': 16.2.12(@angular/compiler@16.2.12)(typescript@4.9.5) '@rollup/plugin-json': 6.0.0(rollup@3.26.2) - '@rollup/plugin-node-resolve': 15.0.2(rollup@3.26.2) + '@rollup/plugin-node-resolve': 15.0.0(rollup@3.26.2) ajv: 8.12.0 ansi-colors: 4.1.3 autoprefixer: 10.4.16(postcss@8.4.31) @@ -17389,6 +17573,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /normalize-url@6.1.0: + resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} + engines: {node: '>=10'} + dev: true + /now-and-later@2.0.1: resolution: {integrity: sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ==} engines: {node: '>= 0.10'} @@ -17833,6 +18022,11 @@ packages: p-map: 2.1.0 dev: true + /p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + dev: true + /p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -17894,6 +18088,14 @@ packages: aggregate-error: 3.1.0 dev: true + /p-queue@6.6.2: + resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} + engines: {node: '>=8'} + dependencies: + eventemitter3: 4.0.7 + p-timeout: 3.2.0 + dev: true + /p-retry@4.6.2: resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} engines: {node: '>=8'} @@ -17902,6 +18104,13 @@ packages: retry: 0.13.1 dev: true + /p-timeout@3.2.0: + resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} + engines: {node: '>=8'} + dependencies: + p-finally: 1.0.0 + dev: true + /p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} @@ -18166,6 +18375,11 @@ packages: engines: {node: '>=6'} dev: true + /pify@5.0.0: + resolution: {integrity: sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==} + engines: {node: '>=10'} + dev: true + /pinkie-promise@2.0.1: resolution: {integrity: sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==} engines: {node: '>=0.10.0'} @@ -18287,6 +18501,76 @@ packages: engines: {node: '>=0.10.0'} dev: true + /postcss-calc@8.2.4(postcss@8.4.33): + resolution: {integrity: sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==} + peerDependencies: + postcss: '>=8.4.31' + dependencies: + postcss: 8.4.33 + postcss-selector-parser: 6.0.15 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-colormin@5.3.1(postcss@8.4.33): + resolution: {integrity: sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: '>=8.4.31' + dependencies: + browserslist: 4.22.2 + caniuse-api: 3.0.0 + colord: 2.9.3 + postcss: 8.4.33 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-convert-values@5.1.3(postcss@8.4.33): + resolution: {integrity: sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: '>=8.4.31' + dependencies: + browserslist: 4.22.2 + postcss: 8.4.33 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-discard-comments@5.1.2(postcss@8.4.33): + resolution: {integrity: sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: '>=8.4.31' + dependencies: + postcss: 8.4.33 + dev: true + + /postcss-discard-duplicates@5.1.0(postcss@8.4.33): + resolution: {integrity: sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: '>=8.4.31' + dependencies: + postcss: 8.4.33 + dev: true + + /postcss-discard-empty@5.1.1(postcss@8.4.33): + resolution: {integrity: sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: '>=8.4.31' + dependencies: + postcss: 8.4.33 + dev: true + + /postcss-discard-overridden@5.1.0(postcss@8.4.33): + resolution: {integrity: sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: '>=8.4.31' + dependencies: + postcss: 8.4.33 + dev: true + /postcss-import@15.1.0(postcss@8.4.33): resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} engines: {node: '>=14.0.0'} @@ -18309,6 +18593,23 @@ packages: postcss: 8.4.33 dev: true + /postcss-load-config@3.1.4(postcss@8.4.33): + resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} + engines: {node: '>= 10'} + peerDependencies: + postcss: '>=8.4.31' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + dependencies: + lilconfig: 2.1.0 + postcss: 8.4.33 + yaml: 1.10.2 + dev: true + /postcss-load-config@4.0.2(postcss@8.4.33): resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} engines: {node: '>= 14'} @@ -18361,6 +18662,74 @@ packages: resolution: {integrity: sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==} dev: true + /postcss-merge-longhand@5.1.7(postcss@8.4.33): + resolution: {integrity: sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: '>=8.4.31' + dependencies: + postcss: 8.4.33 + postcss-value-parser: 4.2.0 + stylehacks: 5.1.1(postcss@8.4.33) + dev: true + + /postcss-merge-rules@5.1.4(postcss@8.4.33): + resolution: {integrity: sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: '>=8.4.31' + dependencies: + browserslist: 4.22.2 + caniuse-api: 3.0.0 + cssnano-utils: 3.1.0(postcss@8.4.33) + postcss: 8.4.33 + postcss-selector-parser: 6.0.15 + dev: true + + /postcss-minify-font-values@5.1.0(postcss@8.4.33): + resolution: {integrity: sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: '>=8.4.31' + dependencies: + postcss: 8.4.33 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-minify-gradients@5.1.1(postcss@8.4.33): + resolution: {integrity: sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: '>=8.4.31' + dependencies: + colord: 2.9.3 + cssnano-utils: 3.1.0(postcss@8.4.33) + postcss: 8.4.33 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-minify-params@5.1.4(postcss@8.4.33): + resolution: {integrity: sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: '>=8.4.31' + dependencies: + browserslist: 4.22.2 + cssnano-utils: 3.1.0(postcss@8.4.33) + postcss: 8.4.33 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-minify-selectors@5.2.1(postcss@8.4.33): + resolution: {integrity: sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: '>=8.4.31' + dependencies: + postcss: 8.4.33 + postcss-selector-parser: 6.0.15 + dev: true + /postcss-modules-extract-imports@3.0.0(postcss@8.4.33): resolution: {integrity: sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==} engines: {node: ^10 || ^12 || >= 14} @@ -18402,6 +18771,22 @@ packages: postcss: 8.4.33 dev: true + /postcss-modules@4.3.1(postcss@8.4.33): + resolution: {integrity: sha512-ItUhSUxBBdNamkT3KzIZwYNNRFKmkJrofvC2nWab3CPKhYBQ1f27XXh1PAPE27Psx58jeelPsxWB/+og+KEH0Q==} + peerDependencies: + postcss: '>=8.4.31' + dependencies: + generic-names: 4.0.0 + icss-replace-symbols: 1.1.0 + lodash.camelcase: 4.3.0 + postcss: 8.4.33 + postcss-modules-extract-imports: 3.0.0(postcss@8.4.33) + postcss-modules-local-by-default: 4.0.3(postcss@8.4.33) + postcss-modules-scope: 3.0.0(postcss@8.4.33) + postcss-modules-values: 4.0.0(postcss@8.4.33) + string-hash: 1.1.3 + dev: true + /postcss-nested@6.0.1(postcss@8.4.33): resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==} engines: {node: '>=12.0'} @@ -18412,6 +18797,129 @@ packages: postcss-selector-parser: 6.0.15 dev: true + /postcss-normalize-charset@5.1.0(postcss@8.4.33): + resolution: {integrity: sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: '>=8.4.31' + dependencies: + postcss: 8.4.33 + dev: true + + /postcss-normalize-display-values@5.1.0(postcss@8.4.33): + resolution: {integrity: sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: '>=8.4.31' + dependencies: + postcss: 8.4.33 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-normalize-positions@5.1.1(postcss@8.4.33): + resolution: {integrity: sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: '>=8.4.31' + dependencies: + postcss: 8.4.33 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-normalize-repeat-style@5.1.1(postcss@8.4.33): + resolution: {integrity: sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: '>=8.4.31' + dependencies: + postcss: 8.4.33 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-normalize-string@5.1.0(postcss@8.4.33): + resolution: {integrity: sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: '>=8.4.31' + dependencies: + postcss: 8.4.33 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-normalize-timing-functions@5.1.0(postcss@8.4.33): + resolution: {integrity: sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: '>=8.4.31' + dependencies: + postcss: 8.4.33 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-normalize-unicode@5.1.1(postcss@8.4.33): + resolution: {integrity: sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: '>=8.4.31' + dependencies: + browserslist: 4.22.2 + postcss: 8.4.33 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-normalize-url@5.1.0(postcss@8.4.33): + resolution: {integrity: sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: '>=8.4.31' + dependencies: + normalize-url: 6.1.0 + postcss: 8.4.33 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-normalize-whitespace@5.1.1(postcss@8.4.33): + resolution: {integrity: sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: '>=8.4.31' + dependencies: + postcss: 8.4.33 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-ordered-values@5.1.3(postcss@8.4.33): + resolution: {integrity: sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: '>=8.4.31' + dependencies: + cssnano-utils: 3.1.0(postcss@8.4.33) + postcss: 8.4.33 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-reduce-initial@5.1.2(postcss@8.4.33): + resolution: {integrity: sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: '>=8.4.31' + dependencies: + browserslist: 4.22.2 + caniuse-api: 3.0.0 + postcss: 8.4.33 + dev: true + + /postcss-reduce-transforms@5.1.0(postcss@8.4.33): + resolution: {integrity: sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: '>=8.4.31' + dependencies: + postcss: 8.4.33 + postcss-value-parser: 4.2.0 + dev: true + /postcss-resolve-nested-selector@0.1.1: resolution: {integrity: sha512-HvExULSwLqHLgUy1rl3ANIqCsvMS0WHss2UOsXhXnQaZ9VCc2oBvIpXrl00IUFT5ZDITME0o6oiXeiHr2SAIfw==} dev: true @@ -18442,6 +18950,27 @@ packages: util-deprecate: 1.0.2 dev: true + /postcss-svgo@5.1.0(postcss@8.4.33): + resolution: {integrity: sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: '>=8.4.31' + dependencies: + postcss: 8.4.33 + postcss-value-parser: 4.2.0 + svgo: 2.8.0 + dev: true + + /postcss-unique-selectors@5.1.1(postcss@8.4.33): + resolution: {integrity: sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: '>=8.4.31' + dependencies: + postcss: 8.4.33 + postcss-selector-parser: 6.0.15 + dev: true + /postcss-url@10.1.3(postcss@8.4.31): resolution: {integrity: sha512-FUzyxfI5l2tKmXdYc6VTu3TWZsInayEKPbiyW+P6vmmIrrb4I6CGX0BFoewgYHLK+oIL5FECEK02REYRpBvUCw==} engines: {node: '>=10'} @@ -18582,6 +19111,11 @@ packages: retry: 0.12.0 dev: true + /promise.series@0.2.0: + resolution: {integrity: sha512-VWQJyU2bcDTgZw8kpfBpB/ejZASlCrzwz5f2hjb/zlujOEB4oeiAhHygAWq8ubsX2GVkD4kCU5V2dwOTaCY5EQ==} + engines: {node: '>=0.12'} + dev: true + /prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -19411,6 +19945,30 @@ packages: rollup-plugin-inject: 3.0.2 dev: true + /rollup-plugin-postcss@4.0.2(postcss@8.4.33): + resolution: {integrity: sha512-05EaY6zvZdmvPUDi3uCcAQoESDcYnv8ogJJQRp6V5kZ6J6P7uAVJlrTZcaaA20wTH527YTnKfkAoPxWI/jPp4w==} + engines: {node: '>=10'} + peerDependencies: + postcss: '>=8.4.31' + dependencies: + chalk: 4.1.2 + concat-with-sourcemaps: 1.1.0 + cssnano: 5.1.15(postcss@8.4.33) + import-cwd: 3.0.0 + p-queue: 6.6.2 + pify: 5.0.0 + postcss: 8.4.33 + postcss-load-config: 3.1.4(postcss@8.4.33) + postcss-modules: 4.3.1(postcss@8.4.33) + promise.series: 0.2.0 + resolve: 1.22.8 + rollup-pluginutils: 2.8.2 + safe-identifier: 0.4.2 + style-inject: 0.3.0 + transitivePeerDependencies: + - ts-node + dev: true + /rollup-plugin-scss@4.0.0: resolution: {integrity: sha512-wxasNXDYC2m+fDxCMgK00WebVWYmeFvShyNABmjvSJZ6D1/SepwqFeaMFMQromveI79gfvb64yJjiZZxSZxEIA==} dependencies: @@ -19503,6 +20061,10 @@ packages: /safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + /safe-identifier@0.4.2: + resolution: {integrity: sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==} + dev: true + /safe-regex-test@1.0.0: resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} dependencies: @@ -20161,6 +20723,11 @@ packages: minipass: 3.3.6 dev: true + /stable@0.1.8: + resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==} + deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility' + dev: true + /stack-trace@0.0.10: resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} dev: true @@ -20266,6 +20833,10 @@ packages: engines: {node: '>=10.0.0'} dev: false + /string-hash@1.1.3: + resolution: {integrity: sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==} + dev: true + /string-length@4.0.2: resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} engines: {node: '>=10'} @@ -20443,6 +21014,10 @@ packages: through: 2.3.8 dev: true + /style-inject@0.3.0: + resolution: {integrity: sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw==} + dev: true + /styled-jsx@5.1.1(react@18.2.0): resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} engines: {node: '>= 12.0.0'} @@ -20460,6 +21035,17 @@ packages: react: 18.2.0 dev: false + /stylehacks@5.1.1(postcss@8.4.33): + resolution: {integrity: sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: '>=8.4.31' + dependencies: + browserslist: 4.22.2 + postcss: 8.4.33 + postcss-selector-parser: 6.0.15 + dev: true + /stylelint-config-sass-guidelines@11.0.0(postcss@8.4.33)(stylelint@16.2.1): resolution: {integrity: sha512-ZFaIDq8Qd6SO1p7Cmg+TM7E2B8t3vDZgEIX+dribR2y+H3bJJ8Oh0poFJGSOIAVdbg6FiI7xQf//8riBZVhIhg==} engines: {node: '>=18.12.0'} @@ -20603,6 +21189,20 @@ packages: resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==} dev: true + /svgo@2.8.0: + resolution: {integrity: sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==} + engines: {node: '>=10.13.0'} + hasBin: true + dependencies: + '@trysound/sax': 0.2.0 + commander: 7.2.0 + css-select: 4.3.0 + css-tree: 1.1.3 + csso: 4.2.0 + picocolors: 1.0.0 + stable: 0.1.8 + dev: true + /svgo@3.2.0: resolution: {integrity: sha512-4PP6CMW/V7l/GmKRKzsLR8xxjdHTV4IMvhTnpuHwwBazSIlw5W/5SmPjN8Dwyt7lKbSJrRDgp4t9ph0HgChFBQ==} engines: {node: '>=14.0.0'} @@ -22701,6 +23301,11 @@ packages: /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + /yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + dev: true + /yaml@2.3.4: resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==} engines: {node: '>= 14'} From 1b3ab732b54ea905919e5a90c9ad46fecb6d4173 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Fri, 1 Mar 2024 15:49:42 +0100 Subject: [PATCH 65/82] feat(components): add sass-export util --- packages/components/src/utils/index.ts | 1 + packages/components/src/utils/sass-export.ts | 22 +++++++++++++++++++ .../documentation/src/utils/sass-export.ts | 2 +- 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 packages/components/src/utils/sass-export.ts diff --git a/packages/components/src/utils/index.ts b/packages/components/src/utils/index.ts index 8b8e9fc00e..6756aaf369 100644 --- a/packages/components/src/utils/index.ts +++ b/packages/components/src/utils/index.ts @@ -1,2 +1,3 @@ export * from './property-checkers'; export * from './is-motion-reduced'; +export * from './sass-export'; diff --git a/packages/components/src/utils/sass-export.ts b/packages/components/src/utils/sass-export.ts new file mode 100644 index 0000000000..903bddd17e --- /dev/null +++ b/packages/components/src/utils/sass-export.ts @@ -0,0 +1,22 @@ +export function parse(scss: object) { + const output: { [key: string]: any } = {}; + + return Object.entries(scss).reduce((object, [path, value]) => { + let temp: any = object; + + path.split('_').forEach((key: string, index: number, values: string[]) => { + const isJsonArray = typeof value === 'string' && /^\[.*\]$/.test(value); + const parsedValue = isJsonArray ? JSON.parse(value) : value; + const v = index === values.length - 1 ? parsedValue : temp[key] || {}; + + temp[key] = v; + temp = temp[key]; + }); + + return object; + }, output); +} + +export function formatAsMap(obj: object) { + return JSON.stringify(obj, null, 2).replace(/[{\[]/g, '(').replace(/[}\]]/g, ')'); +} diff --git a/packages/documentation/src/utils/sass-export.ts b/packages/documentation/src/utils/sass-export.ts index 76a59b9d25..903bddd17e 100644 --- a/packages/documentation/src/utils/sass-export.ts +++ b/packages/documentation/src/utils/sass-export.ts @@ -1,5 +1,5 @@ export function parse(scss: object) { - let output: { [key: string]: any } = {}; + const output: { [key: string]: any } = {}; return Object.entries(scss).reduce((object, [path, value]) => { let temp: any = object; From 2fee563efacd8c6a7c82d597728cb139ab732096 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Fri, 1 Mar 2024 15:51:22 +0100 Subject: [PATCH 66/82] chore(components): remove badges from card-control docs page --- .../stories/components/card-control/card-control.stories.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/documentation/src/stories/components/card-control/card-control.stories.ts b/packages/documentation/src/stories/components/card-control/card-control.stories.ts index 4631c768b4..b2dc84c45d 100644 --- a/packages/documentation/src/stories/components/card-control/card-control.stories.ts +++ b/packages/documentation/src/stories/components/card-control/card-control.stories.ts @@ -1,6 +1,5 @@ import { useArgs } from '@storybook/preview-api'; import { Args, Meta, StoryContext, StoryObj } from '@storybook/web-components'; -import { BADGE } from '../../../../.storybook/constants'; import { html, nothing } from 'lit'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; import { parse } from '../../../utils/sass-export'; @@ -13,9 +12,6 @@ const meta: Meta = { id: '886fabcf-148b-4054-a2ec-4869668294fb', title: 'Components/Forms/Card-Control', component: 'post-card-control', - parameters: { - badges: [BADGE.BETA], - }, args: { label: 'Label', description: '', From 1fb9e9a0eec1f7313005245f175b22ebe12bc42c Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Fri, 1 Mar 2024 15:52:55 +0100 Subject: [PATCH 67/82] fix(components): implement :host-context fallback for firefox and safari --- .../post-card-control.module.scss | 5 ++ .../post-card-control/post-card-control.scss | 67 +++++++++++++++++++ .../post-card-control/post-card-control.tsx | 40 +++++++++-- 3 files changed, 108 insertions(+), 4 deletions(-) create mode 100644 packages/components/src/components/post-card-control/post-card-control.module.scss diff --git a/packages/components/src/components/post-card-control/post-card-control.module.scss b/packages/components/src/components/post-card-control/post-card-control.module.scss new file mode 100644 index 0000000000..f843459b36 --- /dev/null +++ b/packages/components/src/components/post-card-control/post-card-control.module.scss @@ -0,0 +1,5 @@ +@use '@swisspost/design-system-styles/core' as post; + +:export { + dark-bg-selectors: [post.$dark-backgrounds]; +} 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 ca2e5b0786..e2ba443d2a 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 @@ -177,3 +177,70 @@ } } } + +// remove as soon as all browser support :host-context() +// https://caniuse.com/?search=%3Ahost-context() +:host(:not(:last-child)) { + .card-control[data-host-context*='fieldset'] { + margin-bottom: post.$size-regular; + } +} + +@each $bg in post.$dark-backgrounds { + .card-control[data-host-context*='#{$bg}'] { + --post-card-control-border-color: #{post.$white}; + --post-card-control-bg: transparent; + --post-card-control-color: #{post.$white}; + --post-card-control-input-border-color: #{post.$white}; + --post-card-control-input-bg: transparent; + + &:not(.is-disabled) { + // order matters! + // because we only overwrite the props, which need to be different from one selector to the other. + + &.is-checked { + --post-card-control-border-color: #{post.$yellow}; + --post-card-control-bg: #{post.$yellow}; + --post-card-control-color: #{post.$gray-80}; + --post-card-control-input-border-color: #{post.$gray-80}; + --post-card-control-input-bg: #{post.$white}; + + &.is-invalid { + --post-card-control-bg: #{post.$yellow}; + } + } + + &.is-invalid { + --post-card-control-border-color: #{post.$danger}; + --post-card-control-bg: #{post.$error-background}; + --post-card-control-color: #{post.$danger}; + --post-card-control-input-border-color: #{post.$danger}; + --post-card-control-input-bg: #{post.$white}; + } + + &:hover { + --post-card-control-border-color: #{post.$black}; + --post-card-control-bg: #{post.$gray-20}; + --post-card-control-color: #{post.$black}; + --post-card-control-input-border-color: #{post.$black}; + --post-card-control-input-bg: #{post.$white}; + } + } + + // show focus even if is-disabled, because aria-disabled allows focus at any moment + &.is-focused { + &:where(:has(.card-control--input:focus-visible)) { + outline-color: post.$white; + } + } + + // 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-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-input-bg: transparent; + } + } +} diff --git a/packages/components/src/components/post-card-control/post-card-control.tsx b/packages/components/src/components/post-card-control/post-card-control.tsx index 9ba3527e96..3e923f9194 100644 --- a/packages/components/src/components/post-card-control/post-card-control.tsx +++ b/packages/components/src/components/post-card-control/post-card-control.tsx @@ -14,6 +14,13 @@ import { import { checkNonEmpty, checkOneOf } from '../../utils'; import { version } from '../../../package.json'; +// remove as soon as all browser support :host-context() +// https://caniuse.com/?search=%3Ahost-context() +import scss from './post-card-control.module.scss'; +import { parse } from '../../utils/sass-export'; + +const SCSS_VARIABLES = parse(scss); + let cardControlIds = 0; /** @@ -49,10 +56,10 @@ export class PostCardControl { private control: HTMLInputElement; private controlId = `PostCardControl_${cardControlIds++}`; + private initialChecked: boolean; @Element() host: HTMLPostCardControlElement; - @State() initialChecked: boolean; @State() focused = false; @AttachInternals() private internals: ElementInternals; @@ -296,11 +303,34 @@ export class PostCardControl { this.groupCollectMembers(); } - connectedCallback() { - this.initialChecked = this.checked; + // remove as soon as all browser support the :host-context() selector + private readonly HOST_CONTEXT_FILTERS = ['fieldset', ...SCSS_VARIABLES['dark-bg-selectors']]; + private hostContext: string[]; + + private setHostContext() { + this.hostContext = []; + let element = this.host as HTMLElement; + + while (element) { + const localName = element.localName; + const id = element.id ? `#${element.id}` : ''; + const classes = + element.classList.length > 0 ? `.${Array.from(element.classList).join('.')}` : ''; + + this.hostContext.push(`${localName}${id}${classes}`); + element = element.parentElement; + } + + this.hostContext = this.hostContext.filter(ctx => + this.HOST_CONTEXT_FILTERS.find(f => ctx.includes(f)), + ); } - componentWillLoad() { + connectedCallback() { + // remove as soon as all browser support :host-context() + this.setHostContext(); + + this.initialChecked = this.checked; this.validateControlLabel(); this.validateControlType(); } @@ -317,6 +347,8 @@ export class PostCardControl { 'is-valid': this.validity !== null && this.validity !== 'false', 'is-invalid': this.validity === 'false', }} + // remove as soon as all browser support :host-context() + data-host-context={this.hostContext.join(' ')} > (this.control = el as HTMLInputElement)} From 46dd2888ac9e0fde5c4eb66539f4b81589f39fce Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Tue, 12 Mar 2024 17:31:28 +0100 Subject: [PATCH 68/82] refactor: post-card-control component --- .../components/src/lib/components.module.ts | 11 +- .../post-card-control-radio-value-accessor.ts | 53 +++++ .../projects/components/src/public-api.ts | 8 +- .../components/.config/bindings.angular.ts | 10 +- packages/components/src/components.d.ts | 14 +- .../post-card-control/post-card-control.tsx | 181 ++++++++++-------- .../components/post-card-control/readme.md | 20 +- 7 files changed, 198 insertions(+), 99 deletions(-) create mode 100644 packages/components-angular/projects/components/src/lib/custom/value-accessors/post-card-control-radio-value-accessor.ts diff --git a/packages/components-angular/projects/components/src/lib/components.module.ts b/packages/components-angular/projects/components/src/lib/components.module.ts index 2ca7874446..e7901d9999 100644 --- a/packages/components-angular/projects/components/src/lib/components.module.ts +++ b/packages/components-angular/projects/components/src/lib/components.module.ts @@ -1,9 +1,15 @@ import { APP_INITIALIZER, NgModule } from '@angular/core'; -import { DIRECTIVES } from './stencil-generated'; import { defineCustomElements } from '@swisspost/design-system-components/loader'; +import { DIRECTIVES } from './stencil-generated'; +import { BooleanValueAccessor } from './stencil-generated/boolean-value-accessor'; +import { PostCardControlValueAccessorDirective } from './custom/value-accessors/post-card-control-radio-value-accessor'; + +const DECLARATIONS = [...DIRECTIVES, BooleanValueAccessor, PostCardControlValueAccessorDirective]; + @NgModule({ - declarations: [...DIRECTIVES], + declarations: DECLARATIONS, + exports: DECLARATIONS, providers: [ { provide: APP_INITIALIZER, @@ -11,6 +17,5 @@ import { defineCustomElements } from '@swisspost/design-system-components/loader multi: true, }, ], - exports: [...DIRECTIVES], }) export class PostComponentsModule {} diff --git a/packages/components-angular/projects/components/src/lib/custom/value-accessors/post-card-control-radio-value-accessor.ts b/packages/components-angular/projects/components/src/lib/custom/value-accessors/post-card-control-radio-value-accessor.ts new file mode 100644 index 0000000000..5cc6110e50 --- /dev/null +++ b/packages/components-angular/projects/components/src/lib/custom/value-accessors/post-card-control-radio-value-accessor.ts @@ -0,0 +1,53 @@ +import { Directive, ElementRef, HostListener } from '@angular/core'; +import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms'; + +@Directive({ + /* eslint-disable-next-line @angular-eslint/directive-selector */ + selector: 'post-card-control[type="radio"]', + host: { + '(change)': 'handleChangeEvent($event.detail.value)', + }, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: PostCardControlValueAccessorDirective, + multi: true, + }, + ], +}) +export class PostCardControlValueAccessorDirective implements ControlValueAccessor { + private onChange: (value: any) => void = () => { + /**/ + }; + private onTouched: () => void = () => { + /**/ + }; + protected lastValue: any; + + constructor(protected el: ElementRef) {} + + writeValue(value: any) { + this.el.nativeElement.checked = this.lastValue = + this.el.nativeElement.value != value ? false : value; + } + + handleChangeEvent(value: any) { + this.onChange(value); + } + + @HostListener('focusout') + _handleBlurEvent() { + this.onTouched(); + } + + registerOnChange(fn: (value: any) => void) { + this.onChange = fn; + } + registerOnTouched(fn: () => void) { + this.onTouched = fn; + } + + setDisabledState(isDisabled: boolean) { + this.el.nativeElement.disabled = isDisabled; + } +} diff --git a/packages/components-angular/projects/components/src/public-api.ts b/packages/components-angular/projects/components/src/public-api.ts index 4e5ec8ff60..e60791988a 100644 --- a/packages/components-angular/projects/components/src/public-api.ts +++ b/packages/components-angular/projects/components/src/public-api.ts @@ -3,5 +3,11 @@ */ export * from './lib/components.module'; -export { DIRECTIVES } from './lib/stencil-generated'; export * from './lib/stencil-generated/components'; +export { DIRECTIVES } from './lib/stencil-generated'; + +// Export all custom made components & directives! +// Skipping this step will lead to Angular Ivy errors when building for production. + +export { BooleanValueAccessor } from './lib/stencil-generated/boolean-value-accessor'; +export { PostCardControlValueAccessorDirective } from './lib/custom/value-accessors/post-card-control-radio-value-accessor'; diff --git a/packages/components/.config/bindings.angular.ts b/packages/components/.config/bindings.angular.ts index 07665ff1dc..e86f44fbb7 100644 --- a/packages/components/.config/bindings.angular.ts +++ b/packages/components/.config/bindings.angular.ts @@ -1,4 +1,12 @@ import { ValueAccessorConfig } from '@stencil/angular-output-target'; // https://stenciljs.com/docs/v4/angular#valueaccessorconfigs -export const angularValueAccessorBindings: ValueAccessorConfig[] = []; +export const angularValueAccessorBindings: ValueAccessorConfig[] = [ + { + elementSelectors: ['post-card-control[type="checkbox"]'], + event: 'change', + targetAttr: 'checked', + type: 'boolean', + }, + // a custom post-card-control-value-accessor for post-card-control[type="radio"] has been implemented in the components-angular package +]; diff --git a/packages/components/src/components.d.ts b/packages/components/src/components.d.ts index 2c26ff0648..5933405f16 100644 --- a/packages/components/src/components.d.ts +++ b/packages/components/src/components.d.ts @@ -86,6 +86,10 @@ export namespace Components { * Defines the `disabled` attribute of the control. If `true`, the user can not interact with the control and the controls value will not be included in the forms data. */ "disabled": boolean; + /** + * A public method to reset the group controls `checked` state to `false`. + */ + "groupReset": () => Promise; /** * Defines the icon `name` inside of the card. If not set the icon will not show up. */ @@ -99,7 +103,7 @@ export namespace Components { */ "name": string; /** - * A public method to reset the controls `checked` and `validity` state. The state is set to `null`, so it's neither valid nor invalid. + * A public method to reset the controls `checked` and `validity` state. The validity state is set to `null`, so it's neither valid nor invalid. */ "reset": () => Promise; /** @@ -311,8 +315,8 @@ declare global { new (): HTMLPostAlertElement; }; interface HTMLPostCardControlElementEventMap { - "input": boolean; - "change": boolean; + "input": { state: boolean; value: string }; + "change": { state: boolean; value: string }; } /** * @class PostCardControl - representing a stencil component @@ -504,11 +508,11 @@ declare namespace LocalJSX { /** * An event emitted whenever the components checked state is toggled. The event payload (emitted under `event.detail.state`) is a boolean: `true` if the component is checked, `false` if it is unchecked. If the component is used with type `radio`, it will only emit this event, when the checked state is changing to `true`. */ - "onChange"?: (event: PostCardControlCustomEvent) => void; + "onChange"?: (event: PostCardControlCustomEvent<{ state: boolean; value: string }>) => void; /** * An event emitted whenever the components checked state is toggled. The event payload (emitted under `event.detail.state`) is a boolean: `true` if the component is checked, `false` if it is unchecked. */ - "onInput"?: (event: PostCardControlCustomEvent) => void; + "onInput"?: (event: PostCardControlCustomEvent<{ state: boolean; value: string }>) => void; /** * Defines the `type` attribute of the control. */ diff --git a/packages/components/src/components/post-card-control/post-card-control.tsx b/packages/components/src/components/post-card-control/post-card-control.tsx index 3e923f9194..57a53ac1e6 100644 --- a/packages/components/src/components/post-card-control/post-card-control.tsx +++ b/packages/components/src/components/post-card-control/post-card-control.tsx @@ -35,8 +35,6 @@ let cardControlIds = 0; formAssociated: true, }) export class PostCardControl { - private readonly GROUPEVENT: string; - private readonly KEYCODES = { SPACE: 'Space', LEFT: 'ArrowLeft', @@ -117,18 +115,18 @@ export class PostCardControl { * An event emitted whenever the components checked state is toggled. * The event payload (emitted under `event.detail.state`) is a boolean: `true` if the component is checked, `false` if it is unchecked. */ - @Event() input: EventEmitter; + @Event() input: EventEmitter<{ state: boolean; value: string }>; /** * An event emitted whenever the components checked state is toggled. * The event payload (emitted under `event.detail.state`) is a boolean: `true` if the component is checked, `false` if it is unchecked. * If the component is used with type `radio`, it will only emit this event, when the checked state is changing to `true`. */ - @Event() change: EventEmitter; + @Event() change: EventEmitter<{ state: boolean; value: string }>; /** * A public method to reset the controls `checked` and `validity` state. - * The state is set to `null`, so it's neither valid nor invalid. + * The validity state is set to `null`, so it's neither valid nor invalid. */ @Method() async reset() { @@ -136,18 +134,13 @@ export class PostCardControl { this.controlSetChecked(this.initialChecked); } - constructor() { - this.GROUPEVENT = `PostCardControlGroup:${this.name}:change`; - - this.cardClickHandler = this.cardClickHandler.bind(this); - this.controlClickHandler = this.controlClickHandler.bind(this); - this.controlChangeHandler = this.controlChangeHandler.bind(this); - this.controlFocusHandler = this.controlFocusHandler.bind(this); - this.controlKeyDownHandler = this.controlKeyDownHandler.bind(this); - - this.groupEventHandler = this.groupEventHandler.bind(this); - - window.addEventListener(this.GROUPEVENT, this.groupEventHandler); + /** + * A public method to reset the group controls `checked` state to `false`. + */ + @Method() + async groupReset() { + if (this.disabled) this.control.checked = this.checked = false; + this.controlSetChecked(false); } @Watch('label') @@ -177,18 +170,40 @@ export class PostCardControl { this.controlSetChecked(this.checked); } + constructor() { + this.cardClickHandler = this.cardClickHandler.bind(this); + this.controlClickHandler = this.controlClickHandler.bind(this); + this.controlChangeHandler = this.controlChangeHandler.bind(this); + this.controlFocusHandler = this.controlFocusHandler.bind(this); + this.controlKeyDownHandler = this.controlKeyDownHandler.bind(this); + } + private cardClickHandler(e: Event) { - if (e.target !== this.control) this.control.click(); + // trigger click on control to change it, if this was not the clicked element anyway + if (e.target !== this.control) { + e.stopPropagation(); + this.control.click(); + } } private controlClickHandler(e: Event) { - if (this.disabled) e.preventDefault(); - e.stopPropagation(); + // if control is disabled do nothing + // else control value will fire a change event, which is handled in the controlChangeHandler method + if (this.disabled) { + e.preventDefault(); + // this.change.emit({ state: this.checked, value: null }); + } } private controlChangeHandler(e: Event) { + // stop event from bubbling, because we will emit it manually + e.stopPropagation(); + // update group members + this.groupCollectMembers(); + // update checked state this.controlSetChecked(this.control.checked, e); - if (this.group.members.length > 1) this.groupSetSelectedMember(this.control); + // update selected group member + this.groupSetChecked(this.control, e); } private controlFocusHandler() { @@ -197,53 +212,46 @@ export class PostCardControl { // https://googlechromelabs.github.io/howto-components/howto-radio-group/ private controlKeyDownHandler(e: KeyboardEvent) { - if (this.group.members.length > 1) { - switch (e.code) { - case this.KEYCODES.UP: - case this.KEYCODES.LEFT: - e.preventDefault(); - this.groupSetSelectedMember(this.groupGetPrevMember(), true); - break; - - case this.KEYCODES.DOWN: - case this.KEYCODES.RIGHT: - e.preventDefault(); - this.groupSetSelectedMember(this.groupGetNextMember(), true); - break; - - case this.KEYCODES.SPACE: - e.preventDefault(); - this.groupSetSelectedMember(this.control, true); - break; - - default: - break; - } + // update group members + this.groupCollectMembers(); + + switch (e.code) { + case this.KEYCODES.UP: + case this.KEYCODES.LEFT: + this.groupSetChecked(this.groupGetPrev(), e); + break; + case this.KEYCODES.DOWN: + case this.KEYCODES.RIGHT: + this.groupSetChecked(this.groupGetNext(), e); + break; + case this.KEYCODES.SPACE: + this.groupSetChecked(this.control, e); + break; + default: + break; } } private controlSetChecked(checked: boolean, e?: Event) { - if (e && e.type === 'input') e.stopImmediatePropagation(); - if (this.disabled) { this.internals.setFormValue(null); } else { - this.checked = this.control.checked = checked; - this.internals.setFormValue(this.checked ? this.control.value : null); - this.controlEmitEvent(e); + this.control.checked = this.checked = checked; + this.internals.setFormValue(this.control.checked ? this.control.value : null); + + if (e) { + const isCheckbox = this.type === 'checkbox'; + const isRadioAndChecked = this.type === 'radio' && this.checked; + + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/radio + // if an event parameter is given and a native control would fire an event, emit the corresponding event to the light dom + if (isCheckbox || isRadioAndChecked) + this[e.type].emit({ state: this.checked, value: this.value }); + } } } - // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox - private controlEmitEvent(e?: Event) { - if (!e) return; - - const isCheckbox = this.type === 'checkbox'; - const isRadioAndChecked = this.type === 'radio' && this.checked; - - if (isCheckbox || isRadioAndChecked) this[e.type].emit({ state: this.checked }); - } - private groupCollectMembers() { if (this.type === 'radio' && this.name) { this.group.hosts = Array.from( @@ -262,45 +270,50 @@ export class PostCardControl { this.group.members.find(m => m.getRootNode().host === document.activeElement) ?? this.group.first; - this.groupUpdateTabIndexes(); + const focusableMember = this.group.checked || this.group.focused || this.group.first; + + this.group.members.forEach(m => { + m.tabIndex = m === focusableMember ? 0 : -1; + }); } } } - private groupUpdateTabIndexes() { - const focusableMember = this.group.checked || this.group.focused || this.group.first; - - this.group.members.forEach(m => { - m.tabIndex = m === focusableMember ? 0 : -1; - }); - } - - private groupGetPrevMember() { + private groupGetPrev() { const focusedIndex = this.group.members.findIndex(m => m.id === this.group.focused.id); return this.group.members.find((_m, i) => i === focusedIndex - 1) ?? this.group.last; } - private groupGetNextMember() { + private groupGetNext() { const focusedIndex = this.group.members.findIndex(m => m.id === this.group.focused.id); return this.group.members.find((_m, i) => i === focusedIndex + 1) ?? this.group.first; } - private groupSetSelectedMember( - newCheckedMember: HTMLInputElement, - triggeredByKeyboard?: boolean, - ) { - window.dispatchEvent( - new CustomEvent(this.GROUPEVENT, { - detail: { control: newCheckedMember, triggeredByKeyboard }, - }), - ); - } + private groupSetChecked(newChecked: HTMLInputElement, e: Event) { + if (this.group.members.length > 1) { + const isKeyboardEvent = e.type === 'keydown'; + const newIsAriaDisabled = newChecked.hasAttribute('aria-disabled'); + const newIndex = this.group.members.findIndex(m => m === newChecked); - private groupEventHandler(e: CustomEvent) { - if (e.detail.triggeredByKeyboard) e.detail.control.focus(); + if (isKeyboardEvent) { + e.preventDefault(); + newChecked.focus(); + } - this.controlSetChecked(this.control === e.detail.control); - this.groupCollectMembers(); + // if new is disabled, do not reset/set anything + if (!newIsAriaDisabled) { + // reset all group members but the newChecked + this.group.hosts + .filter((_h, i) => i !== newIndex) + .forEach(h => { + h.groupReset(); + }); + + // if method was called by keyboard event, select newChecked + // else this has already been done by clicking on the newChecked element already + if (isKeyboardEvent) newChecked.click(); + } + } } // remove as soon as all browser support the :host-context() selector @@ -405,6 +418,6 @@ export class PostCardControl { } disconnectedCallback() { - window.removeEventListener(this.GROUPEVENT, this.groupEventHandler); + // window.removeEventListener(this.GROUPEVENT, this.groupEventHandler); } } diff --git a/packages/components/src/components/post-card-control/readme.md b/packages/components/src/components/post-card-control/readme.md index 6853375dd7..972bb5e5a5 100644 --- a/packages/components/src/components/post-card-control/readme.md +++ b/packages/components/src/components/post-card-control/readme.md @@ -22,18 +22,28 @@ ## Events -| Event | Description | Type | -| -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | -| `change` | An event emitted whenever the components checked state is toggled. The event payload (emitted under `event.detail.state`) is a boolean: `true` if the component is checked, `false` if it is unchecked. If the component is used with type `radio`, it will only emit this event, when the checked state is changing to `true`. | `CustomEvent` | -| `input` | An event emitted whenever the components checked state is toggled. The event payload (emitted under `event.detail.state`) is a boolean: `true` if the component is checked, `false` if it is unchecked. | `CustomEvent` | +| Event | Description | Type | +| -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | +| `change` | An event emitted whenever the components checked state is toggled. The event payload (emitted under `event.detail.state`) is a boolean: `true` if the component is checked, `false` if it is unchecked. If the component is used with type `radio`, it will only emit this event, when the checked state is changing to `true`. | `CustomEvent<{ state: boolean; value: string; }>` | +| `input` | An event emitted whenever the components checked state is toggled. The event payload (emitted under `event.detail.state`) is a boolean: `true` if the component is checked, `false` if it is unchecked. | `CustomEvent<{ state: boolean; value: string; }>` | ## Methods +### `groupReset() => Promise` + +A public method to reset the group controls `checked` state to `false`. + +#### Returns + +Type: `Promise` + + + ### `reset() => Promise` A public method to reset the controls `checked` and `validity` state. -The state is set to `null`, so it's neither valid nor invalid. +The validity state is set to `null`, so it's neither valid nor invalid. #### Returns From 0fd63701606b74ac5f680aeb67a7f4c51fa58416 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Tue, 12 Mar 2024 17:32:13 +0100 Subject: [PATCH 69/82] feat(components-angular): add custom card-control-view --- .../src/app/app-routing.module.ts | 12 ++- .../consumer-app/src/app/app.component.html | 90 +------------------ .../src/app/app.component.spec.ts | 16 ++-- .../consumer-app/src/app/app.component.ts | 2 +- .../consumer-app/src/app/app.module.ts | 19 +++- .../card-control/card-control.component.html | 77 ++++++++++++++++ .../card-control/card-control.component.ts | 31 +++++++ .../src/app/routes/home/home.component.html | 87 ++++++++++++++++++ .../src/app/routes/home/home.component.ts | 7 ++ 9 files changed, 240 insertions(+), 101 deletions(-) create mode 100644 packages/components-angular/projects/consumer-app/src/app/routes/card-control/card-control.component.html create mode 100644 packages/components-angular/projects/consumer-app/src/app/routes/card-control/card-control.component.ts create mode 100644 packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.html create mode 100644 packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.ts diff --git a/packages/components-angular/projects/consumer-app/src/app/app-routing.module.ts b/packages/components-angular/projects/consumer-app/src/app/app-routing.module.ts index 02972627f8..11501ad4a8 100644 --- a/packages/components-angular/projects/consumer-app/src/app/app-routing.module.ts +++ b/packages/components-angular/projects/consumer-app/src/app/app-routing.module.ts @@ -1,10 +1,16 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; +import { HomeComponent } from './routes/home/home.component'; +import { CardControlComponent } from './routes/card-control/card-control.component'; -const routes: Routes = []; +const routes: Routes = [ + { path: '', redirectTo: 'home', pathMatch: 'full' }, + { path: 'home', component: HomeComponent }, + { path: 'card-control', component: CardControlComponent }, +]; @NgModule({ imports: [RouterModule.forRoot(routes)], - exports: [RouterModule] + exports: [RouterModule], }) -export class AppRoutingModule { } +export class AppRoutingModule {} diff --git a/packages/components-angular/projects/consumer-app/src/app/app.component.html b/packages/components-angular/projects/consumer-app/src/app/app.component.html index aad7477fa7..dd2257cca4 100644 --- a/packages/components-angular/projects/consumer-app/src/app/app.component.html +++ b/packages/components-angular/projects/consumer-app/src/app/app.component.html @@ -1,89 +1,5 @@

Hurray, it works!

-
-

Post Accordion

- - - Titulum 1 -

Contentus momentus vero siteos et accusam iretea et justo.

-
- - - Titulum 2 -

Contentus momentus vero siteos et accusam iretea et justo.

-
- - - Titulum 3 -

Contentus momentus vero siteos et accusam iretea et justo.

-
-
-
- -
-

Post Alert

-

Contentus momentus vero siteos et accusam iretea et justo.

-
- -
-

Post Collapsible

- -

Contentus momentus vero siteos et accusam iretea et justo.

-
-
- -
-

Post Popover

- - -

Optional title

- -

- A longer message that needs more time to read. - Links - are also possible. -

-
-
- -
-

Post Popovercontainer

- -
- -
-

Post Icon

- -
- -
-

Post Tabs

- - Unua langeto - Dua langeto - Tria langeto - - - Jen la enhavo de la unua langeto. Defaŭlte ĝi montriĝas komence. - - - Jen la enhavo de la dua langeto. Defaŭlte ĝi estas kaŝita komence. - - - Jen la enhavo de la tria langeto. Defaŭlte ĝi ankaŭ estas kaŝita komence. - - -
- -
-

Post Tooltip

- - Hi there 👋 -
- -
-

Post Card-Control

- -
+
+ +
diff --git a/packages/components-angular/projects/consumer-app/src/app/app.component.spec.ts b/packages/components-angular/projects/consumer-app/src/app/app.component.spec.ts index 127eda1957..427775cd67 100644 --- a/packages/components-angular/projects/consumer-app/src/app/app.component.spec.ts +++ b/packages/components-angular/projects/consumer-app/src/app/app.component.spec.ts @@ -2,11 +2,13 @@ import { TestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { AppComponent } from './app.component'; -describe('AppComponent', () => { - beforeEach(() => TestBed.configureTestingModule({ - imports: [RouterTestingModule], - declarations: [AppComponent] - })); +describe('App', () => { + beforeEach(() => + TestBed.configureTestingModule({ + imports: [RouterTestingModule], + declarations: [AppComponent], + }), + ); it('should create the app', () => { const fixture = TestBed.createComponent(AppComponent); @@ -24,6 +26,8 @@ describe('AppComponent', () => { const fixture = TestBed.createComponent(AppComponent); fixture.detectChanges(); const compiled = fixture.nativeElement as HTMLElement; - expect(compiled.querySelector('.content span')?.textContent).toContain('consumer-app app is running!'); + expect(compiled.querySelector('.content span')?.textContent).toContain( + 'consumer-app app is running!', + ); }); }); diff --git a/packages/components-angular/projects/consumer-app/src/app/app.component.ts b/packages/components-angular/projects/consumer-app/src/app/app.component.ts index aaa0cedc4a..d488e6ca0e 100644 --- a/packages/components-angular/projects/consumer-app/src/app/app.component.ts +++ b/packages/components-angular/projects/consumer-app/src/app/app.component.ts @@ -3,7 +3,7 @@ import { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', - styleUrls: ['./app.component.scss'] + styleUrls: ['./app.component.scss'], }) export class AppComponent { title = 'consumer-app'; diff --git a/packages/components-angular/projects/consumer-app/src/app/app.module.ts b/packages/components-angular/projects/consumer-app/src/app/app.module.ts index 00544eb082..063d25fddb 100644 --- a/packages/components-angular/projects/consumer-app/src/app/app.module.ts +++ b/packages/components-angular/projects/consumer-app/src/app/app.module.ts @@ -1,13 +1,24 @@ import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; import { BrowserModule } from '@angular/platform-browser'; - import { AppRoutingModule } from './app-routing.module'; -import { AppComponent } from './app.component'; +import { FormsModule } from '@angular/forms'; import { PostComponentsModule } from 'components'; +import { AppComponent } from './app.component'; +import { HomeComponent } from './routes/home/home.component'; +import { CardControlComponent } from './routes/card-control/card-control.component'; + @NgModule({ - declarations: [AppComponent], - imports: [BrowserModule, AppRoutingModule, PostComponentsModule], + imports: [ + CommonModule, + BrowserModule, + AppRoutingModule, + FormsModule, + PostComponentsModule, + CardControlComponent, + ], + declarations: [AppComponent, HomeComponent], providers: [], bootstrap: [AppComponent], }) diff --git a/packages/components-angular/projects/consumer-app/src/app/routes/card-control/card-control.component.html b/packages/components-angular/projects/consumer-app/src/app/routes/card-control/card-control.component.html new file mode 100644 index 0000000000..b47b55a173 --- /dev/null +++ b/packages/components-angular/projects/consumer-app/src/app/routes/card-control/card-control.component.html @@ -0,0 +1,77 @@ +

CardControl Forms

+ +

Form Builder Form

+
+
+
+
+ Post Card Control Checkbox + +
+ +
+ Native Checkbox +
+ + +
+
+
+
+ +
+
+
+ Post Card Control Radio Group + + + +
+ +
+ Native Checkbox +
+ + +
+
+
+
+ +

Form Status: {{ formBuilderForm.status }}

+ +
+ + +
+
+ +
+

Output

+
{{ formBuilderFormValue }}
+
diff --git a/packages/components-angular/projects/consumer-app/src/app/routes/card-control/card-control.component.ts b/packages/components-angular/projects/consumer-app/src/app/routes/card-control/card-control.component.ts new file mode 100644 index 0000000000..db9f7b78c7 --- /dev/null +++ b/packages/components-angular/projects/consumer-app/src/app/routes/card-control/card-control.component.ts @@ -0,0 +1,31 @@ +import { Component } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; +import { PostComponentsModule } from 'components'; + +@Component({ + standalone: true, + selector: 'card-control-page', + templateUrl: './card-control.component.html', + imports: [CommonModule, ReactiveFormsModule, PostComponentsModule], +}) +export class CardControlComponent { + public radioOptions = ['option_1', 'option_2', 'option_3', 'option_4']; + + formBuilderForm = this.formBuilder.group({ + checkbox: [null, Validators.requiredTrue], + nativeCheckbox: [null, Validators.requiredTrue], + radio: [null, Validators.required], + nativeRadio: [null, Validators.required], + }); + + constructor(private formBuilder: FormBuilder) {} + + get formBuilderFormValue() { + return JSON.stringify(this.formBuilderForm.value, null, 2); + } + + formBuilderFormOnSubmit(e: any) { + console.log(Array.from((new FormData(e.target) as any).entries())); + } +} 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 new file mode 100644 index 0000000000..10810e5fc0 --- /dev/null +++ b/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.html @@ -0,0 +1,87 @@ +
+

Post Accordion

+ + + Titulum 1 +

Contentus momentus vero siteos et accusam iretea et justo.

+
+ + + Titulum 2 +

Contentus momentus vero siteos et accusam iretea et justo.

+
+ + + Titulum 3 +

Contentus momentus vero siteos et accusam iretea et justo.

+
+
+
+ +
+

Post Alert

+

Contentus momentus vero siteos et accusam iretea et justo.

+
+ +
+

Post Collapsible

+ +

Contentus momentus vero siteos et accusam iretea et justo.

+
+
+ +
+

Post Popover

+ + +

Optional title

+ +

+ A longer message that needs more time to read. + Links + are also possible. +

+
+
+ +
+

Post Popovercontainer

+ +
+ +
+

Post Icon

+ +
+ +
+

Post Tabs

+ + Unua langeto + Dua langeto + Tria langeto + + + Jen la enhavo de la unua langeto. Defaŭlte ĝi montriĝas komence. + + + Jen la enhavo de la dua langeto. Defaŭlte ĝi estas kaŝita komence. + + + Jen la enhavo de la tria langeto. Defaŭlte ĝi ankaŭ estas kaŝita komence. + + +
+ +
+

Post Tooltip

+ + Hi there 👋 +
+ +
+

Post Card-Control

+ +
diff --git a/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.ts b/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.ts new file mode 100644 index 0000000000..b795b9fdf8 --- /dev/null +++ b/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'home-page', + templateUrl: './home.component.html', +}) +export class HomeComponent {} From 3a85acb1abf7a4a44c0b0bb6b1d0583e55a7337e Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Wed, 13 Mar 2024 10:39:22 +0100 Subject: [PATCH 70/82] test(components): update card-control tests --- .../components/cypress/e2e/card-control.cy.ts | 7 +++-- .../post-card-control/post-card-control.tsx | 30 ++++++++----------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/packages/components/cypress/e2e/card-control.cy.ts b/packages/components/cypress/e2e/card-control.cy.ts index 79dcc3510e..8fc28cd0d3 100644 --- a/packages/components/cypress/e2e/card-control.cy.ts +++ b/packages/components/cypress/e2e/card-control.cy.ts @@ -486,15 +486,18 @@ describe('Card-Control', () => { }); }); - it('should update surrounding form to not contain its value, when a disabled group member has been checked by keyboard', () => { + it('should not update the surrounding form value, when a disabled group member has been checked by keyboard', () => { cy.get('@card-control').eq(1).invoke('attr', 'disabled', true); cy.get('@wrapper').eq(1).should('have.class', 'is-disabled'); cy.get('@input').eq(1).should('have.attr', 'aria-disabled'); + let formValue = null; + cy.get('@form').then($form => { cy.get('@input').each(($input, i) => { + if (i !== 1) formValue = i.toString(); cy.wrap($input).type(' '); - cy.checkFormDataPropValue($form, 'CardControlGroup', i !== 1 ? i.toString() : null); + cy.checkFormDataPropValue($form, 'CardControlGroup', formValue); }); }); }); diff --git a/packages/components/src/components/post-card-control/post-card-control.tsx b/packages/components/src/components/post-card-control/post-card-control.tsx index 57a53ac1e6..450b619f4f 100644 --- a/packages/components/src/components/post-card-control/post-card-control.tsx +++ b/packages/components/src/components/post-card-control/post-card-control.tsx @@ -179,30 +179,25 @@ export class PostCardControl { } private cardClickHandler(e: Event) { - // trigger click on control to change it, if this was not the clicked element anyway - if (e.target !== this.control) { - e.stopPropagation(); - this.control.click(); - } + e.stopPropagation(); + + // if this was not the clicked element anyway, trigger click on control to change it + if (e.target !== this.control) this.control.click(); } private controlClickHandler(e: Event) { + e.stopPropagation(); + // if control is disabled do nothing // else control value will fire a change event, which is handled in the controlChangeHandler method - if (this.disabled) { - e.preventDefault(); - // this.change.emit({ state: this.checked, value: null }); - } + if (this.disabled) e.preventDefault(); } private controlChangeHandler(e: Event) { - // stop event from bubbling, because we will emit it manually e.stopPropagation(); - // update group members + this.groupCollectMembers(); - // update checked state this.controlSetChecked(this.control.checked, e); - // update selected group member this.groupSetChecked(this.control, e); } @@ -212,7 +207,9 @@ export class PostCardControl { // https://googlechromelabs.github.io/howto-components/howto-radio-group/ private controlKeyDownHandler(e: KeyboardEvent) { - // update group members + e.stopPropagation(); + if (Object.values(this.KEYCODES).includes(e.code)) e.preventDefault(); + this.groupCollectMembers(); switch (e.code) { @@ -295,10 +292,7 @@ export class PostCardControl { const newIsAriaDisabled = newChecked.hasAttribute('aria-disabled'); const newIndex = this.group.members.findIndex(m => m === newChecked); - if (isKeyboardEvent) { - e.preventDefault(); - newChecked.focus(); - } + if (isKeyboardEvent) newChecked.focus(); // if new is disabled, do not reset/set anything if (!newIsAriaDisabled) { From 9ff2620eed516c391475f08bd6dcbe32a468e2aa Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Wed, 13 Mar 2024 15:49:05 +0100 Subject: [PATCH 71/82] test(components-angular): add post-card-control e2e test --- package.json | 2 + .../projects/consumer-app/cypress.config.ts | 1 + .../cypress/e2e/card-control.cy.ts | 102 ++++++++++++++++++ .../consumer-app/cypress/support/commands.ts | 8 ++ .../consumer-app/cypress/support/component.ts | 8 +- .../consumer-app/cypress/support/e2e.ts | 2 +- .../consumer-app/cypress/support/index.d.ts | 5 + .../card-control/card-control.component.html | 11 +- .../card-control/card-control.component.ts | 2 +- 9 files changed, 130 insertions(+), 11 deletions(-) create mode 100644 packages/components-angular/projects/consumer-app/cypress/e2e/card-control.cy.ts create mode 100644 packages/components-angular/projects/consumer-app/cypress/support/index.d.ts diff --git a/package.json b/package.json index 10aa71b9c0..ba8d904573 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,8 @@ "components:snapshots": "start-server-and-test docs:headless 9001 'pnpm --filter design-system-components snapshots'", "components-angular:start": "pnpm --filter design-system-components-angular-workspace start", "components-angular:build": "pnpm --filter design-system-components-angular-workspace build", + "components-angular:e2e": "pnpm --filter design-system-components-angular-workspace e2e", + "components-angular:e2e:watch": "pnpm --filter design-system-components-angular-workspace e2e:watch", "header": "pnpm header:start", "header:start": "pnpm --filter internet-header dev", "header:build": "pnpm --filter internet-header build", diff --git a/packages/components-angular/projects/consumer-app/cypress.config.ts b/packages/components-angular/projects/consumer-app/cypress.config.ts index f01955f82b..94cece0eaa 100644 --- a/packages/components-angular/projects/consumer-app/cypress.config.ts +++ b/packages/components-angular/projects/consumer-app/cypress.config.ts @@ -3,6 +3,7 @@ import { defineConfig } from 'cypress'; export default defineConfig({ e2e: { baseUrl: 'http://localhost:4200', + includeShadowDom: true, }, component: { devServer: { diff --git a/packages/components-angular/projects/consumer-app/cypress/e2e/card-control.cy.ts b/packages/components-angular/projects/consumer-app/cypress/e2e/card-control.cy.ts new file mode 100644 index 0000000000..15c4b2b124 --- /dev/null +++ b/packages/components-angular/projects/consumer-app/cypress/e2e/card-control.cy.ts @@ -0,0 +1,102 @@ +describe('Card-Control', () => { + beforeEach(() => { + cy.visit('/card-control'); + + cy.get('post-card-control[type="checkbox"]').as('checkbox'); + cy.get('@checkbox').find('input.card-control--input').as('checkbox-input'); + cy.get('.form-check input.form-check-input[type="checkbox"]').as('native-checkbox'); + cy.get('post-card-control[type="radio"]').as('radio'); + cy.get('@radio').find('input.card-control--input').as('radio-input'); + cy.get('.form-check input.form-check-input[type="radio"]').as('native-radio'); + + cy.get('button[type="reset"]').as('reset'); + + cy.get('#validity').as('state'); + cy.get('#output').as('output'); + }); + + it('should exist', () => { + cy.get('@checkbox').should('exist'); + cy.get('@radio').should('exist'); + }); + + it('should update the surrounding form value, when checked', () => { + cy.get('@output').then($output => { + cy.checkOutputProps($output, { + checkbox: null, + radio: null, + }); + + cy.get('@checkbox').click(); + cy.checkOutputProps($output, { checkbox: true }); + cy.get('@checkbox').click(); + cy.checkOutputProps($output, { checkbox: false }); + cy.get('@checkbox-input').type(' '); + cy.checkOutputProps($output, { checkbox: true }); + + cy.get('@radio').each(($control, i) => { + cy.wrap($control).click(); + cy.checkOutputProps($output, { radio: `option_${i + 1}` }); + }); + + cy.get('@radio-input').each(($input, i) => { + cy.wrap($input).type(' '); + cy.checkOutputProps($output, { radio: `option_${i + 1}` }); + }); + }); + }); + + it('should not update the surrounding form value, when a disabled group member has been checked by keyboard', () => { + cy.get('@radio').eq(1).invoke('attr', 'disabled', true); + cy.get('@radio').eq(3).invoke('attr', 'disabled', true); + + cy.wait(0) + .get('@output') + .then($output => { + cy.checkOutputProps($output, { radio: null }); + + let option: null | undefined | string = null; + + cy.get('@radio-input') + .its('length') + .then(length => { + cy.get('@radio-input').each(($input, i) => { + cy.get('@radio-input') + .eq(i + 1 < length ? i + 1 : 0) + .then($next => { + const isDisabled = $next.attr('aria-disabled') !== undefined; + + if (!isDisabled) option = $next.val()?.toString(); + + cy.wrap($input).focus().type('{downArrow}'); + cy.checkOutputProps($output, { radio: option }); + }); + }); + }); + }); + }); + + it('should update the surrounding form validity, when checked', () => { + cy.get('@state').should('contain.text', 'INVALID'); + + cy.get('@native-checkbox').check(); + cy.get('@native-radio').eq(2).check(); + cy.get('@radio').eq(2).click(); + cy.get('@radio').eq(2).click(); + cy.get('@state').should('contain.text', 'INVALID'); + + cy.get('@checkbox').click(); + cy.get('@state').should('contain.text', 'VALID'); + cy.get('@checkbox').click(); + cy.get('@state').should('contain.text', 'INVALID'); + + cy.get('@reset').click(); + cy.get('@native-checkbox').check(); + cy.get('@native-radio').eq(2).check(); + cy.get('@checkbox').click(); + cy.get('@state').should('contain.text', 'INVALID'); + + cy.get('@radio').eq(2).click(); + cy.get('@state').should('contain.text', 'VALID'); + }); +}); diff --git a/packages/components-angular/projects/consumer-app/cypress/support/commands.ts b/packages/components-angular/projects/consumer-app/cypress/support/commands.ts index af1f44a0fc..1249d710fa 100644 --- a/packages/components-angular/projects/consumer-app/cypress/support/commands.ts +++ b/packages/components-angular/projects/consumer-app/cypress/support/commands.ts @@ -41,3 +41,11 @@ // // -- This will overwrite an existing command -- // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) + +Cypress.Commands.add('checkOutputProps', ($output, props) => { + const output = JSON.parse($output.text()); + + Object.entries(props).forEach(([key, value]) => { + expect(output[key]).to.be.eq(value); + }); +}); diff --git a/packages/components-angular/projects/consumer-app/cypress/support/component.ts b/packages/components-angular/projects/consumer-app/cypress/support/component.ts index 96e1d27983..a703d324a9 100644 --- a/packages/components-angular/projects/consumer-app/cypress/support/component.ts +++ b/packages/components-angular/projects/consumer-app/cypress/support/component.ts @@ -14,12 +14,12 @@ // *********************************************************** // Import commands.js using ES2015 syntax: -import './commands' +import './commands'; // Alternatively you can use CommonJS syntax: // require('./commands') -import { mount } from 'cypress/angular' +import { mount } from 'cypress/angular'; // Augment the Cypress namespace to include type definitions for // your custom command. @@ -28,12 +28,12 @@ import { mount } from 'cypress/angular' declare global { namespace Cypress { interface Chainable { - mount: typeof mount + mount: typeof mount; } } } -Cypress.Commands.add('mount', mount) +Cypress.Commands.add('mount', mount); // Example use: // cy.mount(MyComponent) diff --git a/packages/components-angular/projects/consumer-app/cypress/support/e2e.ts b/packages/components-angular/projects/consumer-app/cypress/support/e2e.ts index 55540ff7d9..959d46bc93 100644 --- a/packages/components-angular/projects/consumer-app/cypress/support/e2e.ts +++ b/packages/components-angular/projects/consumer-app/cypress/support/e2e.ts @@ -14,4 +14,4 @@ // *********************************************************** // When a command from ./commands is ready to use, import with `import './commands'` syntax -// import './commands'; +import './commands'; diff --git a/packages/components-angular/projects/consumer-app/cypress/support/index.d.ts b/packages/components-angular/projects/consumer-app/cypress/support/index.d.ts new file mode 100644 index 0000000000..1a314a1b78 --- /dev/null +++ b/packages/components-angular/projects/consumer-app/cypress/support/index.d.ts @@ -0,0 +1,5 @@ +declare namespace Cypress { + interface Chainable { + checkOutputProps($output: JQuery, props: {}): Chainable; + } +} diff --git a/packages/components-angular/projects/consumer-app/src/app/routes/card-control/card-control.component.html b/packages/components-angular/projects/consumer-app/src/app/routes/card-control/card-control.component.html index b47b55a173..5cd8b5b0a3 100644 --- a/packages/components-angular/projects/consumer-app/src/app/routes/card-control/card-control.component.html +++ b/packages/components-angular/projects/consumer-app/src/app/routes/card-control/card-control.component.html @@ -40,7 +40,6 @@

Form Builder Form

type="radio" name="radio" [value]="option" - [attr.disabled]="i === 1 || i === 2 ? true : null" formControlName="radio" >
@@ -54,7 +53,6 @@

Form Builder Form

class="form-check-input" type="radio" [value]="option" - [attr.disabled]="i === 1 || i === 2 ? true : null" formControlName="nativeRadio" /> @@ -63,15 +61,18 @@

Form Builder Form

-

Form Status: {{ formBuilderForm.status }}

-
+
+

Form Status:

+

{{ formBuilderForm.status }}

+
+

Output

-
{{ formBuilderFormValue }}
+
{{ formBuilderFormValue }}
diff --git a/packages/components-angular/projects/consumer-app/src/app/routes/card-control/card-control.component.ts b/packages/components-angular/projects/consumer-app/src/app/routes/card-control/card-control.component.ts index db9f7b78c7..fbd76304a3 100644 --- a/packages/components-angular/projects/consumer-app/src/app/routes/card-control/card-control.component.ts +++ b/packages/components-angular/projects/consumer-app/src/app/routes/card-control/card-control.component.ts @@ -10,7 +10,7 @@ import { PostComponentsModule } from 'components'; imports: [CommonModule, ReactiveFormsModule, PostComponentsModule], }) export class CardControlComponent { - public radioOptions = ['option_1', 'option_2', 'option_3', 'option_4']; + public radioOptions = ['option_1', 'option_2', 'option_3', 'option_4', 'option_5']; formBuilderForm = this.formBuilder.group({ checkbox: [null, Validators.requiredTrue], From ad97d2420a8c2b033c85fa593a75fb59f9a90b23 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Wed, 13 Mar 2024 16:51:13 +0100 Subject: [PATCH 72/82] chore: update card-control form associated story --- packages/components/src/components.d.ts | 2 +- .../post-card-control/post-card-control.tsx | 42 ++++----- .../components/post-card-control/readme.md | 2 +- .../card-control/card-control.stories.ts | 90 ++++++++++++++++--- 4 files changed, 102 insertions(+), 34 deletions(-) diff --git a/packages/components/src/components.d.ts b/packages/components/src/components.d.ts index 5933405f16..c747de41d3 100644 --- a/packages/components/src/components.d.ts +++ b/packages/components/src/components.d.ts @@ -87,7 +87,7 @@ export namespace Components { */ "disabled": boolean; /** - * A public method to reset the group controls `checked` state to `false`. + * A hidden public method to reset the group controls `checked` state to `false`. */ "groupReset": () => Promise; /** diff --git a/packages/components/src/components/post-card-control/post-card-control.tsx b/packages/components/src/components/post-card-control/post-card-control.tsx index 450b619f4f..385532d764 100644 --- a/packages/components/src/components/post-card-control/post-card-control.tsx +++ b/packages/components/src/components/post-card-control/post-card-control.tsx @@ -135,7 +135,7 @@ export class PostCardControl { } /** - * A public method to reset the group controls `checked` state to `false`. + * A hidden public method to reset the group controls `checked` state to `false`. */ @Method() async groupReset() { @@ -207,25 +207,27 @@ export class PostCardControl { // https://googlechromelabs.github.io/howto-components/howto-radio-group/ private controlKeyDownHandler(e: KeyboardEvent) { - e.stopPropagation(); - if (Object.values(this.KEYCODES).includes(e.code)) e.preventDefault(); - - this.groupCollectMembers(); - - switch (e.code) { - case this.KEYCODES.UP: - case this.KEYCODES.LEFT: - this.groupSetChecked(this.groupGetPrev(), e); - break; - case this.KEYCODES.DOWN: - case this.KEYCODES.RIGHT: - this.groupSetChecked(this.groupGetNext(), e); - break; - case this.KEYCODES.SPACE: - this.groupSetChecked(this.control, e); - break; - default: - break; + if (this.type === 'radio') { + e.stopPropagation(); + if (Object.values(this.KEYCODES).includes(e.code)) e.preventDefault(); + + this.groupCollectMembers(); + + switch (e.code) { + case this.KEYCODES.UP: + case this.KEYCODES.LEFT: + this.groupSetChecked(this.groupGetPrev(), e); + break; + case this.KEYCODES.DOWN: + case this.KEYCODES.RIGHT: + this.groupSetChecked(this.groupGetNext(), e); + break; + case this.KEYCODES.SPACE: + this.groupSetChecked(this.control, e); + break; + default: + break; + } } } diff --git a/packages/components/src/components/post-card-control/readme.md b/packages/components/src/components/post-card-control/readme.md index 972bb5e5a5..cd78493c3f 100644 --- a/packages/components/src/components/post-card-control/readme.md +++ b/packages/components/src/components/post-card-control/readme.md @@ -32,7 +32,7 @@ ### `groupReset() => Promise` -A public method to reset the group controls `checked` state to `false`. +A hidden public method to reset the group controls `checked` state to `false`. #### Returns diff --git a/packages/documentation/src/stories/components/card-control/card-control.stories.ts b/packages/documentation/src/stories/components/card-control/card-control.stories.ts index b2dc84c45d..788b42ada3 100644 --- a/packages/documentation/src/stories/components/card-control/card-control.stories.ts +++ b/packages/documentation/src/stories/components/card-control/card-control.stories.ts @@ -26,7 +26,7 @@ const meta: Meta = { slotInvalidFeedback: '', }, argTypes: { - type: { + 'type': { control: { type: 'radio', labels: { @@ -36,7 +36,7 @@ const meta: Meta = { }, options: ['checkbox', 'radio'], }, - validity: { + 'validity': { control: { type: 'radio', labels: { @@ -52,12 +52,17 @@ const meta: Meta = { }, }, }, - slotIcon: { + 'method-groupReset': { table: { disable: true, }, }, - slotInvalidFeedback: { + 'slotIcon': { + table: { + disable: true, + }, + }, + 'slotInvalidFeedback': { table: { disable: true, }, @@ -145,22 +150,69 @@ export const FormIntegration: Story = { parameters: { docs: { controls: { - include: ['Fieldset disabled', 'checked', 'disabled'], + include: ['disabled fieldset', 'value', 'disabled'], }, }, }, args: { - fieldsetDisabled: false, - name: 'card-control', + name: 'checkbox', + checkboxFieldset: false, + radioValue: '', + radioDisabled: '', + radioFieldset: false, }, argTypes: { - fieldsetDisabled: { - name: 'Fieldset disabled', - description: - 'If a wrapping `fieldset` element is disabled, the `` will behave like disabled as well.', + value: { + description: 'Set the value of the `checkbox` card.', + table: { + category: 'Checkbox', + }, + }, + disabled: { + description: 'Set the disabled state of the `checkbox` card.', + table: { + category: 'Checkbox', + }, + }, + checkboxFieldset: { + name: 'disabled fieldset', + description: 'Set the `disabled` attribute of the `fieldset` around the `checkbox` card.', control: { type: 'boolean', }, + table: { + category: 'Checkbox', + }, + }, + radioValue: { + name: 'value', + description: 'Set the value **prefix** of the `radio` cards.', + control: { + type: 'text', + }, + table: { + category: 'Radio', + }, + }, + radioDisabled: { + name: 'disabled', + description: 'Set the disabled state of the **second** `radio` card.', + control: { + type: 'boolean', + }, + table: { + category: 'Radio', + }, + }, + radioFieldset: { + name: 'disabled fieldset', + description: 'Set the `disabled` attribute of the `fieldset` around the `radio` cards.', + control: { + type: 'boolean', + }, + table: { + category: 'Radio', + }, }, }, decorators: [ @@ -168,16 +220,30 @@ export const FormIntegration: Story = { ${story()}

FormData

+

Submit or reset the form to see how the FormData will look like.

{}
`, ], render: (args: Args, context: StoryContext) => { return html`
-
+
Legend ${Default.render?.(args, context)}
+
+ Legend + ${[1, 2, 3].map( + n => + html``, + )} +
From 3be777f8314fddfb6b9462c89397c38b594ad1b1 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Wed, 13 Mar 2024 16:56:16 +0100 Subject: [PATCH 73/82] chore(components): remove unnecessary disconnectCallback in card-control --- .../src/components/post-card-control/post-card-control.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/components/src/components/post-card-control/post-card-control.tsx b/packages/components/src/components/post-card-control/post-card-control.tsx index 385532d764..426e588814 100644 --- a/packages/components/src/components/post-card-control/post-card-control.tsx +++ b/packages/components/src/components/post-card-control/post-card-control.tsx @@ -412,8 +412,4 @@ export class PostCardControl { formResetCallback() { this.reset(); } - - disconnectedCallback() { - // window.removeEventListener(this.GROUPEVENT, this.groupEventHandler); - } } From 6604d32bf6c7cbc4b1a88e0a9429f2a871583bf0 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Thu, 14 Mar 2024 08:05:04 +0100 Subject: [PATCH 74/82] fix(components-angular): fix issue in angular build process, by adding a noImplicitOverride flag to the tsconfig --- .../components-angular/projects/components/tsconfig.lib.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/components-angular/projects/components/tsconfig.lib.json b/packages/components-angular/projects/components/tsconfig.lib.json index 543fd474ab..b0d6d02847 100644 --- a/packages/components-angular/projects/components/tsconfig.lib.json +++ b/packages/components-angular/projects/components/tsconfig.lib.json @@ -6,9 +6,8 @@ "declaration": true, "declarationMap": true, "inlineSources": true, + "noImplicitOverride": false, "types": [] }, - "exclude": [ - "**/*.spec.ts" - ] + "exclude": ["**/*.spec.ts"] } From e1a20dfde3c100106e972f70bfba7b12d90f7f4a Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Thu, 14 Mar 2024 12:44:28 +0100 Subject: [PATCH 75/82] fix: lint errors --- .../post-card-control/post-card-control.tsx | 2 ++ packages/components/src/utils/sass-export.ts | 20 +++++++++---------- .../documentation/src/utils/sass-export.ts | 20 +++++++++---------- 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/packages/components/src/components/post-card-control/post-card-control.tsx b/packages/components/src/components/post-card-control/post-card-control.tsx index 426e588814..69ffc29c32 100644 --- a/packages/components/src/components/post-card-control/post-card-control.tsx +++ b/packages/components/src/components/post-card-control/post-card-control.tsx @@ -397,6 +397,7 @@ export class PostCardControl { } // https://stenciljs.com/docs/form-associated + /* eslint-disable @stencil-community/own-methods-must-be-private */ formAssociatedCallback() { this.controlSetChecked(this.checked); } @@ -412,4 +413,5 @@ export class PostCardControl { formResetCallback() { this.reset(); } + /* eslint-enable @stencil-community/own-methods-must-be-private */ } diff --git a/packages/components/src/utils/sass-export.ts b/packages/components/src/utils/sass-export.ts index 903bddd17e..e5a18ef1f0 100644 --- a/packages/components/src/utils/sass-export.ts +++ b/packages/components/src/utils/sass-export.ts @@ -1,22 +1,20 @@ export function parse(scss: object) { - const output: { [key: string]: any } = {}; - return Object.entries(scss).reduce((object, [path, value]) => { - let temp: any = object; + let output = object; - path.split('_').forEach((key: string, index: number, values: string[]) => { - const isJsonArray = typeof value === 'string' && /^\[.*\]$/.test(value); - const parsedValue = isJsonArray ? JSON.parse(value) : value; - const v = index === values.length - 1 ? parsedValue : temp[key] || {}; + path.split('_').forEach((key, i, values) => { + const pathKey = key as keyof typeof output; + const normalized = /^\[.*\]$/.test(value) ? JSON.parse(value) : value; + const pathValue = i >= values.length - 1 ? normalized : output[pathKey] || {}; - temp[key] = v; - temp = temp[key]; + output[pathKey] = pathValue as never; + output = output[pathKey]; }); return object; - }, output); + }, {}); } export function formatAsMap(obj: object) { - return JSON.stringify(obj, null, 2).replace(/[{\[]/g, '(').replace(/[}\]]/g, ')'); + return JSON.stringify(obj, null, 2).replace(/[{[]/g, '(').replace(/[}\]]/g, ')'); } diff --git a/packages/documentation/src/utils/sass-export.ts b/packages/documentation/src/utils/sass-export.ts index 903bddd17e..e5a18ef1f0 100644 --- a/packages/documentation/src/utils/sass-export.ts +++ b/packages/documentation/src/utils/sass-export.ts @@ -1,22 +1,20 @@ export function parse(scss: object) { - const output: { [key: string]: any } = {}; - return Object.entries(scss).reduce((object, [path, value]) => { - let temp: any = object; + let output = object; - path.split('_').forEach((key: string, index: number, values: string[]) => { - const isJsonArray = typeof value === 'string' && /^\[.*\]$/.test(value); - const parsedValue = isJsonArray ? JSON.parse(value) : value; - const v = index === values.length - 1 ? parsedValue : temp[key] || {}; + path.split('_').forEach((key, i, values) => { + const pathKey = key as keyof typeof output; + const normalized = /^\[.*\]$/.test(value) ? JSON.parse(value) : value; + const pathValue = i >= values.length - 1 ? normalized : output[pathKey] || {}; - temp[key] = v; - temp = temp[key]; + output[pathKey] = pathValue as never; + output = output[pathKey]; }); return object; - }, output); + }, {}); } export function formatAsMap(obj: object) { - return JSON.stringify(obj, null, 2).replace(/[{\[]/g, '(').replace(/[}\]]/g, ')'); + return JSON.stringify(obj, null, 2).replace(/[{[]/g, '(').replace(/[}\]]/g, ')'); } From d9443681eff587dce75641e6113d3e25afd8b0b0 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Thu, 14 Mar 2024 13:01:09 +0100 Subject: [PATCH 76/82] fix(components): add @HostListener istead of host property in directive decorator --- .../value-accessors/post-card-control-radio-value-accessor.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/components-angular/projects/components/src/lib/custom/value-accessors/post-card-control-radio-value-accessor.ts b/packages/components-angular/projects/components/src/lib/custom/value-accessors/post-card-control-radio-value-accessor.ts index 5cc6110e50..a08a58d2fe 100644 --- a/packages/components-angular/projects/components/src/lib/custom/value-accessors/post-card-control-radio-value-accessor.ts +++ b/packages/components-angular/projects/components/src/lib/custom/value-accessors/post-card-control-radio-value-accessor.ts @@ -4,9 +4,6 @@ import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms'; @Directive({ /* eslint-disable-next-line @angular-eslint/directive-selector */ selector: 'post-card-control[type="radio"]', - host: { - '(change)': 'handleChangeEvent($event.detail.value)', - }, providers: [ { provide: NG_VALUE_ACCESSOR, @@ -31,6 +28,7 @@ export class PostCardControlValueAccessorDirective implements ControlValueAccess this.el.nativeElement.value != value ? false : value; } + @HostListener('change', ['$event.detail.value']) handleChangeEvent(value: any) { this.onChange(value); } From 87c309c4e0e8a1cf92c0475af7aa97b4e4238569 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Thu, 14 Mar 2024 13:03:15 +0100 Subject: [PATCH 77/82] chore(components-angular): add post-rating to home view --- .../consumer-app/src/app/routes/home/home.component.html | 5 +++++ 1 file changed, 5 insertions(+) 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 10810e5fc0..4d8d0ffc36 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 @@ -51,6 +51,11 @@

Post Popovercontainer

+
+

Post Rating

+ +
+

Post Icon

From faff6bcbf69f1b75accd70765d87df27fdea7166 Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Thu, 14 Mar 2024 14:55:12 +0100 Subject: [PATCH 78/82] fix(components): prevent card-control from fiering errors, on startup --- .../post-card-control/post-card-control.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/components/src/components/post-card-control/post-card-control.tsx b/packages/components/src/components/post-card-control/post-card-control.tsx index 69ffc29c32..dffe7f05f0 100644 --- a/packages/components/src/components/post-card-control/post-card-control.tsx +++ b/packages/components/src/components/post-card-control/post-card-control.tsx @@ -232,11 +232,13 @@ export class PostCardControl { } private controlSetChecked(checked: boolean, e?: Event) { + if (!this.control) return; + if (this.disabled) { this.internals.setFormValue(null); } else { this.control.checked = this.checked = checked; - this.internals.setFormValue(this.control.checked ? this.control.value : null); + this.internals.setFormValue(this.checked ? this.control.value : null); if (e) { const isCheckbox = this.type === 'checkbox'; @@ -340,8 +342,6 @@ export class PostCardControl { this.setHostContext(); this.initialChecked = this.checked; - this.validateControlLabel(); - this.validateControlType(); } render() { @@ -396,6 +396,11 @@ export class PostCardControl { this.groupCollectMembers(); } + componentDidLoad() { + this.validateControlLabel(); + this.validateControlType(); + } + // https://stenciljs.com/docs/form-associated /* eslint-disable @stencil-community/own-methods-must-be-private */ formAssociatedCallback() { From 62a9957c2f41b76cafa0c258c79c8e55b945606b Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Thu, 14 Mar 2024 14:55:30 +0100 Subject: [PATCH 79/82] chore(components-angular): add a routerlink navigation --- .../consumer-app/src/app/app.component.html | 16 ++++++++++++++-- .../projects/consumer-app/src/index.html | 9 +-------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/components-angular/projects/consumer-app/src/app/app.component.html b/packages/components-angular/projects/consumer-app/src/app/app.component.html index dd2257cca4..fdc3e21be5 100644 --- a/packages/components-angular/projects/consumer-app/src/app/app.component.html +++ b/packages/components-angular/projects/consumer-app/src/app/app.component.html @@ -1,5 +1,17 @@ -

Hurray, it works!

+
+ +
-
+
+

Hurray, it works!

diff --git a/packages/components-angular/projects/consumer-app/src/index.html b/packages/components-angular/projects/consumer-app/src/index.html index 72a6331501..8c64c979ce 100644 --- a/packages/components-angular/projects/consumer-app/src/index.html +++ b/packages/components-angular/projects/consumer-app/src/index.html @@ -8,13 +8,6 @@ - - -
- - -
- - + From e555dc7ad314bb36102422bdf465ca4d36e3daed Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Thu, 14 Mar 2024 15:23:40 +0100 Subject: [PATCH 80/82] chore(components-angular): add generated first-level-routes navigation --- .../consumer-app/src/app/app-routing.module.ts | 4 ++-- .../projects/consumer-app/src/app/app.component.html | 7 ++----- .../projects/consumer-app/src/app/app.component.ts | 12 ++++++++++-- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/components-angular/projects/consumer-app/src/app/app-routing.module.ts b/packages/components-angular/projects/consumer-app/src/app/app-routing.module.ts index 11501ad4a8..5172e3b67c 100644 --- a/packages/components-angular/projects/consumer-app/src/app/app-routing.module.ts +++ b/packages/components-angular/projects/consumer-app/src/app/app-routing.module.ts @@ -5,8 +5,8 @@ import { CardControlComponent } from './routes/card-control/card-control.compone const routes: Routes = [ { path: '', redirectTo: 'home', pathMatch: 'full' }, - { path: 'home', component: HomeComponent }, - { path: 'card-control', component: CardControlComponent }, + { title: 'Home', path: 'home', component: HomeComponent }, + { title: 'Card-Control', path: 'card-control', component: CardControlComponent }, ]; @NgModule({ diff --git a/packages/components-angular/projects/consumer-app/src/app/app.component.html b/packages/components-angular/projects/consumer-app/src/app/app.component.html index fdc3e21be5..d67562cfc9 100644 --- a/packages/components-angular/projects/consumer-app/src/app/app.component.html +++ b/packages/components-angular/projects/consumer-app/src/app/app.component.html @@ -1,11 +1,8 @@
diff --git a/packages/components-angular/projects/consumer-app/src/app/app.component.ts b/packages/components-angular/projects/consumer-app/src/app/app.component.ts index d488e6ca0e..84eb2dd04b 100644 --- a/packages/components-angular/projects/consumer-app/src/app/app.component.ts +++ b/packages/components-angular/projects/consumer-app/src/app/app.component.ts @@ -1,10 +1,18 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; +import { Route, Router } from '@angular/router'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'], }) -export class AppComponent { +export class AppComponent implements OnInit { title = 'consumer-app'; + public navigationRoutes: Route[] = []; + + constructor(private router: Router) {} + + ngOnInit(): void { + this.navigationRoutes = this.router.config.filter(r => r.title); + } } From a480f5acb25b63b4532f89473c2d9c2c6b66642a Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Tue, 19 Mar 2024 15:26:18 +0100 Subject: [PATCH 81/82] chore(documentation): use MetaComponent type instead of Meta in card-control component --- .../card-control/card-control.stories.ts | 50 +++++++------------ 1 file changed, 19 insertions(+), 31 deletions(-) diff --git a/packages/documentation/src/stories/components/card-control/card-control.stories.ts b/packages/documentation/src/stories/components/card-control/card-control.stories.ts index 788b42ada3..e7fc8853db 100644 --- a/packages/documentation/src/stories/components/card-control/card-control.stories.ts +++ b/packages/documentation/src/stories/components/card-control/card-control.stories.ts @@ -1,29 +1,30 @@ import { useArgs } from '@storybook/preview-api'; -import { Args, Meta, StoryContext, StoryObj } from '@storybook/web-components'; +import { Args, StoryContext, StoryObj } from '@storybook/web-components'; +import { MetaComponent } from '../../../../types'; import { html, nothing } from 'lit'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; import { parse } from '../../../utils/sass-export'; import './card-control.styles.scss'; import scss from './card-control.module.scss'; -const SCSS_VARIABLES = parse(scss); +const SCSS_VARIABLES: { [key: string]: string } = parse(scss); -const meta: Meta = { +const meta: MetaComponent = { id: '886fabcf-148b-4054-a2ec-4869668294fb', title: 'Components/Forms/Card-Control', component: 'post-card-control', + tags: ['package:WebComponents'], args: { - label: 'Label', - description: '', - type: 'checkbox', - name: '', - value: '', - checked: '', - disabled: '', - validity: 'null', - icon: '', - slotIcon: '', - slotInvalidFeedback: '', + 'label': 'Label', + 'description': '', + 'type': 'checkbox', + 'name': '', + 'value': '', + 'checked': '', + 'disabled': '', + 'validity': 'null', + 'icon': '', + 'slots-icon': '', }, argTypes: { 'type': { @@ -57,16 +58,6 @@ const meta: Meta = { disable: true, }, }, - 'slotIcon': { - table: { - disable: true, - }, - }, - 'slotInvalidFeedback': { - table: { - disable: true, - }, - }, }, }; @@ -78,10 +69,7 @@ export const Default: Story = { render: (args: Args) => { const [, updateArgs] = useArgs(); - const icon = html`${unsafeHTML(args.slotIcon)} `; - const invalidFeedback = html` - ${unsafeHTML(args.slotInvalidFeedback)} - `; + const icon = html`${unsafeHTML(args['slots-icon'])} `; return html` - ${args.slotIcon ? icon : null} ${args.slotInvalidFeedback ? invalidFeedback : null} + ${args['slots-icon'] ? icon : null} `; }, @@ -140,7 +128,7 @@ export const DarkBackground: Story = { export const CustomIcon: Story = { args: { - slotIcon: + 'slots-icon': '', }, render: Default.render, From df973e44462ed597fdbb7144a7c256d3b16afd1f Mon Sep 17 00:00:00 2001 From: oliverschuerch Date: Wed, 20 Mar 2024 09:03:16 +0100 Subject: [PATCH 82/82] test(documentation): switch axe rule in card.cy.ts from prohibited to allowed aria attr rule --- packages/documentation/cypress/e2e/components/card.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/documentation/cypress/e2e/components/card.cy.ts b/packages/documentation/cypress/e2e/components/card.cy.ts index 10462d1dcf..4e6dfc474d 100644 --- a/packages/documentation/cypress/e2e/components/card.cy.ts +++ b/packages/documentation/cypress/e2e/components/card.cy.ts @@ -12,7 +12,7 @@ describe('Card', () => { 'heading-order': { enabled: false, }, - 'aria-prohibited-attr': { + 'aria-allowed-attr': { // aria-label attribute is used as a prop on post-icon enabled: false, },