diff --git a/packages/components/src/_components/checkbox/checkbox.stories.ts b/packages/components/src/_components/checkbox/checkbox.stories.ts deleted file mode 100644 index 8ef5c86d0..000000000 --- a/packages/components/src/_components/checkbox/checkbox.stories.ts +++ /dev/null @@ -1,26 +0,0 @@ -import '../../solid-components'; -import { storybookDefaults, storybookTemplate } from '../../../scripts/storybook/helper'; -import { withActions } from '@storybook/addon-actions/decorator'; - -const { argTypes, args, parameters } = storybookDefaults('sd-checkbox'); -const { generateTemplate } = storybookTemplate('sd-checkbox'); - -export default { - title: 'Components/sd-checkbox', - component: 'sd-checkbox', - args, - argTypes, - parameters: {...parameters}, - decorators: [withActions] as any -}; - - -/** - * Default: This shows sd-checkbox in its default state. - */ - -export const Default = { - render: (args: any) => { - return generateTemplate({ args }); - } -}; diff --git a/packages/components/src/_components/checkbox/checkbox.styles.ts b/packages/components/src/_components/checkbox/checkbox.styles.ts deleted file mode 100644 index 2a59e6b43..000000000 --- a/packages/components/src/_components/checkbox/checkbox.styles.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { css } from 'lit'; -import componentStyles from '../../styles/component.styles'; - -export default css` - ${componentStyles} - - :host { - display: inline-block; - } - - .checkbox { - display: inline-flex; - align-items: top; - font-family: var(--sd-input-font-family); - font-weight: var(--sd-input-font-weight); - color: var(--sd-input-label-color); - vertical-align: middle; - cursor: pointer; - } - - .checkbox--small { - --toggle-size: var(--sd-toggle-size-small); - font-size: var(--sd-input-font-size-small); - } - - .checkbox--medium { - --toggle-size: var(--sd-toggle-size-medium); - font-size: var(--sd-input-font-size-medium); - } - - .checkbox--large { - --toggle-size: var(--sd-toggle-size-large); - font-size: var(--sd-input-font-size-large); - } - - .checkbox__control { - flex: 0 0 auto; - position: relative; - display: inline-flex; - align-items: center; - justify-content: center; - width: var(--toggle-size); - height: var(--toggle-size); - border: solid var(--sd-input-border-width) var(--sd-input-border-color); - border-radius: 2px; - background-color: var(--sd-input-background-color); - color: var(--sd-color-neutral-0); - transition: var(--sd-transition-fast) border-color, var(--sd-transition-fast) background-color, - var(--sd-transition-fast) color, var(--sd-transition-fast) box-shadow; - } - - .checkbox__input { - position: absolute; - opacity: 0; - padding: 0; - margin: 0; - pointer-events: none; - } - - .checkbox__checked-icon, - .checkbox__indeterminate-icon { - display: inline-flex; - width: var(--toggle-size); - height: var(--toggle-size); - } - - /* Hover */ - .checkbox:not(.checkbox--checked):not(.checkbox--disabled) .checkbox__control:hover { - border-color: var(--sd-input-border-color-hover); - background-color: var(--sd-input-background-color-hover); - } - - /* Focus */ - .checkbox:not(.checkbox--checked):not(.checkbox--disabled) .checkbox__input:focus-visible ~ .checkbox__control { - outline: var(--sd-focus-ring); - outline-offset: var(--sd-focus-ring-offset); - } - - /* Checked/indeterminate */ - .checkbox--checked .checkbox__control, - .checkbox--indeterminate .checkbox__control { - border-color: var(--sd-color-primary-600); - background-color: var(--sd-color-primary-600); - } - - /* Checked/indeterminate + hover */ - .checkbox.checkbox--checked:not(.checkbox--disabled) .checkbox__control:hover, - .checkbox.checkbox--indeterminate:not(.checkbox--disabled) .checkbox__control:hover { - border-color: var(--sd-color-primary-500); - background-color: var(--sd-color-primary-500); - } - - /* Checked/indeterminate + focus */ - .checkbox.checkbox--checked:not(.checkbox--disabled) .checkbox__input:focus-visible ~ .checkbox__control, - .checkbox.checkbox--indeterminate:not(.checkbox--disabled) .checkbox__input:focus-visible ~ .checkbox__control { - outline: var(--sd-focus-ring); - outline-offset: var(--sd-focus-ring-offset); - } - - /* Disabled */ - .checkbox--disabled { - opacity: 0.5; - cursor: not-allowed; - } - - .checkbox__label { - display: inline-block; - color: var(--sd-input-label-color); - line-height: var(--toggle-size); - margin-inline-start: 0.5em; - user-select: none; - } - - :host([required]) .checkbox__label::after { - content: var(--sd-input-required-content); - margin-inline-start: var(--sd-input-required-content-offset); - } -`; diff --git a/packages/components/src/components/checkbox-group/checkbox-group.stories.ts b/packages/components/src/components/checkbox-group/checkbox-group.stories.ts new file mode 100644 index 000000000..4398174c7 --- /dev/null +++ b/packages/components/src/components/checkbox-group/checkbox-group.stories.ts @@ -0,0 +1,141 @@ +import '../../solid-components'; +import { html } from 'lit-html'; +import { storybookDefaults, storybookHelpers, storybookTemplate } from '../../../scripts/storybook/helper'; +import { userEvent } from '@storybook/testing-library'; +import { waitUntil } from '@open-wc/testing-helpers'; + +const { argTypes, parameters } = storybookDefaults('sd-checkbox-group'); +const { generateTemplate } = storybookTemplate('sd-checkbox-group'); +const { overrideArgs } = storybookHelpers('sd-checkbox-group'); + +export default { + title: 'Components/sd-checkbox-group', + component: 'sd-checkbox-group', + args: overrideArgs([ + { type: 'slot', name: 'label', value: `` }, + { + type: 'slot', + name: 'default', + value: `Checkbox 1Checkbox 2Checkbox 3` + } + ]), + argTypes, + parameters: { + ...parameters, + design: { + type: 'figma', + url: 'https://www.figma.com/file/Q7E9GTBET7Gs2HyH1kbpu5/Checkbox-%2F-Checkbox-Group?type=design&node-id=0-1&mode=design&t=DV2yJRUqqYBrskyb-0' + } + } +}; + +/** + * Default: This shows sd-checkbox-group in its default state. + */ + +export const Default = { + render: (args: any) => { + return generateTemplate({ args }); + } +}; + +/** + * The sd-checkbox in all possible combinations of `orientation` and `size`. + */ + +export const Orientation = { + parameters: { controls: { exclude: ['orientation', 'size', 'default'] } }, + render: (args: any) => { + return generateTemplate({ + axis: { + x: { type: 'attribute', name: 'orientation' }, + y: { type: 'attribute', name: 'size' } + }, + args + }); + } +}; + +/** + * Use the disabled attribute to disable an input checkbox. Clicks will be suppressed until the disabled state is removed + */ + +export const Disabled = { + name: 'Disabled x Size', + parameters: { controls: { exclude: ['size', 'default'] } }, + render: (args: any) => { + return generateTemplate({ + axis: { + x: [ + { + type: 'slot', + name: 'default', + title: 'disabled', + values: [ + { + value: + 'Option 1Option 2Option 3', + title: 'true' + }, + { + value: + 'Option 1Option 2Option 3', + title: 'false' + } + ] + } + ], + y: { type: 'attribute', name: 'size' } + }, + args + }); + } +}; + +/** + * Use the `form-control`, `form-control-label` and `form-control-input` part selectors to customize the checkbox-group. + */ +export const Parts = { + parameters: { + controls: { exclude: ['form-control', 'form-control-label', 'form-control-input'] } + }, + render: (args: any) => { + return generateTemplate({ + axis: { + y: { + type: 'template', + name: 'sd-checkbox-group::part(...){outline: solid 2px red}', + values: ['form-control', 'form-control-label', 'form-control-input'].map(part => { + return { + title: part, + value: `
%TEMPLATE%
` + }; + }) + } + }, + constants: [{ type: 'template', name: 'width', value: '
%TEMPLATE%
' }], + args + }); + } +}; + +/** + * sd-checkbox-group is fully accessibile via keyboard. + */ +export const Mouseless = { + render: (args: any) => { + return html`
${generateTemplate({ args })}
`; + }, + + play: async ({ canvasElement }: { canvasElement: HTMLUnknownElement }) => { + const el = canvasElement.querySelector('.mouseless sd-checkbox-group'); + await waitUntil(() => el?.shadowRoot?.querySelector('label')); + + if (el?.shadowRoot) { + const label = el.shadowRoot.querySelector('label'); + if (label) { + await userEvent.type(label, '{space}', { pointerEventsCheck: 0 }); + } + } + } +}; diff --git a/packages/components/src/components/checkbox-group/checkbox-group.test.ts b/packages/components/src/components/checkbox-group/checkbox-group.test.ts new file mode 100644 index 000000000..98106a2e1 --- /dev/null +++ b/packages/components/src/components/checkbox-group/checkbox-group.test.ts @@ -0,0 +1,52 @@ +import { expect, fixture, html, waitUntil } from '@open-wc/testing'; +import sinon from 'sinon'; + +describe('when submitting a form', () => { + it('should submit the correct value when a value is provided', async () => { + const form = await fixture(html` +
+ + + + + + Submit +
+ `); + const button = form.querySelector('sd-button')!; + const checkbox = form.querySelectorAll('sd-checkbox')[1]!; + const checkbox2 = form.querySelectorAll('sd-checkbox')[2]!; + const submitHandler = sinon.spy((event: SubmitEvent) => { + formData = new FormData(form); + + event.preventDefault(); + }); + let formData: FormData; + + form.addEventListener('submit', submitHandler); + checkbox2.click(); + checkbox.click(); + button.click(); + await waitUntil(() => submitHandler.calledOnce); + expect(formData!.getAll('a')).to.eql(['2', '3']); + }); + + it('should be present in form data when using the form attribute and located outside of a
', async () => { + const el = await fixture(html` +
+ + Submit + + + + + + +
+ `); + const form = el.querySelector('form')!; + const formData = new FormData(form); + + expect(formData.getAll('a')).to.eql(['1', '3']); + }); +}); diff --git a/packages/components/src/components/checkbox-group/checkbox-group.ts b/packages/components/src/components/checkbox-group/checkbox-group.ts new file mode 100644 index 000000000..36faa0184 --- /dev/null +++ b/packages/components/src/components/checkbox-group/checkbox-group.ts @@ -0,0 +1,164 @@ +import '../icon/icon'; +import { css, html } from 'lit'; +import { customElement } from '../../../src/internal/register-custom-element'; +import { HasSlotController } from '../../internal/slot'; +import { property } from 'lit/decorators.js'; +import { watch } from '../../internal/watch'; +import componentStyles from '../../styles/component.styles'; +import cx from 'classix'; +import SolidElement from '../../internal/solid-element'; +import type SdCheckbox from '../checkbox/checkbox'; + +/** + * @summary Checkbox groups are used to group multiple [checkbox](/components/checkbox). It provides only presentational functionality. + * @documentation https://solid.union-investment.com/[storybook-link]/checkbox-group + * @status stable + * @since 1.18.0 + * + * @slot - The default slot where `` elements are placed. + * @slot label - The checkbox group's label. Required for proper accessibility. Alternatively, you can use the `label` + * attribute. + **/ + +@customElement('sd-checkbox-group') +export default class SdCheckboxGroup extends SolidElement { + private readonly hasSlotController = new HasSlotController(this, 'label'); + + /** + * The checkbox group's label. Required for proper accessibility. If you need to display HTML, use the `label` slot + * instead. + */ + @property() label = ''; + + /** The checkbox group's size. This size will be applied to the label, all child checkboxes. */ + @property({ reflect: true }) size: 'lg' | 'sm' = 'lg'; + + /** + * The orientation property determines the alignment of the component's content or elements. It accepts two possible + * values: 'horizontal' and 'vertical'. The default value is 'vertical'. + * This property allows you to control the visual layout and arrangement of elements within the component, providing + * flexibility in how the component is displayed based on your specific design needs. + */ + @property({ reflect: true }) orientation: 'horizontal' | 'vertical' = 'vertical'; + + private getAllCheckboxes() { + return [...this.querySelectorAll('sd-checkbox')]; + } + + private async syncCheckboxElements() { + const checkboxes = this.getAllCheckboxes(); + + await Promise.all( + // Sync the checked state and size + checkboxes.map(async checkbox => { + await checkbox.updateComplete; + + checkbox.size = this.size; + }) + ); + + if (!checkboxes.some(checkbox => checkbox.checked)) { + checkboxes[0].tabIndex = 0; + } + } + + private syncCheckboxes() { + if (customElements.get('sd-checkbox')) { + this.syncCheckboxElements(); + } else { + customElements.whenDefined('sd-checkbox').then(() => this.syncCheckboxes()); + } + } + + @watch('size', { waitUntilFirstUpdate: true }) + handleSizeChange() { + this.syncCheckboxes(); + } + @watch('invalid', { waitUntilFirstUpdate: true }) + handleInvalid() { + this.syncCheckboxes(); + } + + render() { + const hasLabelSlot = this.hasSlotController.test('label'); + const hasLabel = this.label ? true : hasLabelSlot; + const defaultSlot = html` `; + + return html` +
+ + +
+ ${defaultSlot} +
+
+ `; + } + + /** + * Inherits Tailwind classes and includes additional styling. + */ + static styles = [ + componentStyles, + SolidElement.styles, + css` + :host { + display: block; + } + + :host([orientation='vertical']) ::slotted(sd-checkbox) { + margin-bottom: 8px; + display: flex; + } + + :host([orientation='vertical']) ::slotted(sd-checkbox:last-of-type) { + margin-bottom: 0; + } + + :host([orientation='horizontal']) ::slotted(sd-checkbox) { + margin-right: 24px; + } + + :host([size='sm']):host([orientation='horizontal']) ::slotted(sd-checkbox) { + margin-right: 16px; + } + + :host([orientation='horizontal']) ::slotted(sd-checkbox:last-of-type) { + margin-right: 0; + } + ` + ]; +} + +declare global { + interface HTMLElementTagNameMap { + 'sd-checkbox-group': SdCheckboxGroup; + } +} diff --git a/packages/components/src/components/checkbox/checkbox.stories.ts b/packages/components/src/components/checkbox/checkbox.stories.ts new file mode 100644 index 000000000..4ea1080d5 --- /dev/null +++ b/packages/components/src/components/checkbox/checkbox.stories.ts @@ -0,0 +1,289 @@ +import '../../solid-components'; +import { html } from 'lit-html'; +import { storybookDefaults, storybookHelpers, storybookTemplate } from '../../../scripts/storybook/helper'; +import { userEvent } from '@storybook/testing-library'; +import { waitUntil } from '@open-wc/testing-helpers'; +const { argTypes, parameters } = storybookDefaults('sd-checkbox'); +const { generateTemplate } = storybookTemplate('sd-checkbox'); +const { overrideArgs } = storybookHelpers('sd-checkbox'); + +export default { + title: 'Components/sd-checkbox', + component: 'sd-checkbox', + args: overrideArgs([{ type: 'slot', name: 'default', value: 'Default Slot' }]), + argTypes, + parameters: { + ...parameters, + design: { + type: 'figma', + url: 'https://www.figma.com/file/Q7E9GTBET7Gs2HyH1kbpu5/Checkbox-%2F-Checkbox-Group?type=design&node-id=0-1&mode=design&t=DV2yJRUqqYBrskyb-0' + } + } +}; + +/** + * Default: This shows sd-checkbox in its default state. + */ + +export const Default = { + render: (args: any) => { + return generateTemplate({ args }); + } +}; + +/** + * Use the disabled attribute to disable an input checkbox. Clicks will be suppressed until the disabled state is removed + */ + +export const DisabledAndSize = { + name: 'Disabled × Size', + parameters: { controls: { exclude: ['disabled', 'size', 'default'] } }, + render: (args: any) => { + return generateTemplate({ + axis: { + x: { + type: 'attribute', + name: 'disabled', + values: [false, true] + }, + y: [ + { + type: 'attribute', + name: 'size', + values: ['lg', 'sm'] + } + ] + }, + constants: { type: 'attribute', name: 'disabled', value: true }, + args + }); + } +}; + +/** + * Use the `size` attribute to change the size of the input checkbox. This attribute affects the font-size within the element, while the element itself remains the same size. + */ + +export const Size = { + parameters: { controls: { exclude: ['size'] } }, + render: (args: any) => { + return generateTemplate({ + axis: { + x: { + type: 'attribute', + name: 'size', + values: ['lg', 'sm'] + } + }, + args + }); + } +}; + +export const MultipleLines = { + parameters: { controls: { exclude: ['size'] } }, + render: () => { + return generateTemplate({ + axis: { + x: { + type: 'attribute', + name: 'size', + values: ['lg', 'sm'] + } + }, + args: overrideArgs([{ type: 'slot', name: 'default', value: 'Default Slot
Second Line' }]) + }); + } +}; + +/** + * Use the `required` attribute to mark the element as required. This can be used for form validation purposes. + */ + +export const Required = { + parameters: { controls: { exclude: ['required'] } }, + render: (args: any) => { + return generateTemplate({ + axis: { + x: [{ type: 'attribute', name: 'size', values: ['lg', 'sm'] }], + y: { type: 'attribute', name: 'required' } + }, + args + }); + } +}; + +export const Checked = { + parameters: { controls: { exclude: ['checked'] } }, + render: (args: any) => { + return generateTemplate({ + axis: { + x: { + type: 'attribute', + name: 'disabled', + values: [false, true] + }, + y: [ + { + type: 'attribute', + name: 'size', + values: ['lg', 'sm'] + } + ] + }, + constants: { type: 'attribute', name: 'checked', value: true }, + args + }); + } +}; + +export const Indeterminate = { + parameters: { controls: { exclude: ['indeterminate'] } }, + render: (args: any) => { + return generateTemplate({ + axis: { + x: { + type: 'attribute', + name: 'disabled', + values: [false, true] + }, + y: [ + { + type: 'attribute', + name: 'size', + values: ['lg', 'sm'] + } + ] + }, + constants: { type: 'attribute', name: 'indeterminate', value: true }, + args + }); + } +}; + +/** + * Test invalid state inside a form. + */ + +export const Invalid = { + parameters: { controls: { exclude: ['required'] } }, + render: (args: any) => { + return html` +
+ ${generateTemplate({ + args, + constants: [{ type: 'attribute', name: 'required', value: true }] + })} + Submit +
+ `; + }, + play: async ({ canvasElement }: { canvasElement: HTMLUnknownElement }) => { + const el = canvasElement.querySelector('sd-button'); + await waitUntil(() => el?.shadowRoot?.querySelector('button')); + await userEvent.type(el!.shadowRoot!.querySelector('button')!, '{return}', { pointerEventsCheck: 0 }); + } +}; + +export const IndeterminateInvalid = { + parameters: { controls: { exclude: ['required', 'indeterminate'] } }, + render: (args: any) => { + return html` +
+ ${generateTemplate({ + args, + constants: [ + { type: 'attribute', name: 'required', value: true }, + { type: 'attribute', name: 'indeterminate', value: true } + ] + })} + Submit +
+ `; + }, + play: async ({ canvasElement }: { canvasElement: HTMLUnknownElement }) => { + const el = canvasElement.querySelector('sd-button'); + await waitUntil(() => el?.shadowRoot?.querySelector('button')); + await userEvent.type(el!.shadowRoot!.querySelector('button')!, '{return}', { pointerEventsCheck: 0 }); + } +}; + +/** + * Use the `base`, `control--unchecked`, `control--checked`, `checked` and `label` part selectors to customize the checkbox. + */ +export const Parts = { + parameters: { + controls: { + exclude: [ + 'base', + 'control', + 'control--unchecked', + 'control--checked', + 'checked-icon', + 'control--indeterminate', + 'indeterminate-icon', + 'label', + 'title', + 'name', + 'value', + 'size', + 'disabled', + 'checked', + 'indeterminate', + 'form', + 'required', + 'default' + ] + } + }, + render: (args: any) => { + return generateTemplate({ + axis: { + y: { + type: 'template', + name: 'sd-checkbox::part(...){outline: solid 2px red}', + values: [ + 'base', + 'control', + 'control--unchecked', + 'control--checked', + 'checked-icon', + 'control--indeterminate', + 'indeterminate-icon', + 'label' + ].map(part => { + return { + title: part, + value: ` + +
${checkboxTemplate(part)}
+ + ` + }; + }) + } + }, + args + }); + } +}; + +const checkboxTemplate = (part: string) => { + switch (part) { + case 'control--checked': + return `Default Slot`; + case 'checked-icon': + return `Default Slot`; + case 'control--indeterminate': + return `Default Slot`; + case 'indeterminate-icon': + return `Default Slot`; + case 'form-control-error-text': + return `Default Slot`; + default: + return `Default Slot`; + } +}; diff --git a/packages/components/src/_components/checkbox/checkbox.test.ts b/packages/components/src/components/checkbox/checkbox.test.ts similarity index 97% rename from packages/components/src/_components/checkbox/checkbox.test.ts rename to packages/components/src/components/checkbox/checkbox.test.ts index 2df92e938..ee4546dd5 100644 --- a/packages/components/src/_components/checkbox/checkbox.test.ts +++ b/packages/components/src/components/checkbox/checkbox.test.ts @@ -1,4 +1,3 @@ -import { clickOnElement } from '../../internal/test'; import { expect, fixture, html, oneEvent, waitUntil } from '@open-wc/testing'; import { sendKeys } from '@web/test-runner-commands'; import sinon from 'sinon'; @@ -10,9 +9,13 @@ describe('', () => { await expect(el).to.be.accessible(); }); + it('should not be checked by default', async () => { + const radio = await fixture(html``); + expect(radio.checked).to.be.false; + }); + it('default properties', async () => { const el = await fixture(html` `); - expect(el.name).to.equal(''); expect(el.value).to.be.undefined; expect(el.title).to.equal(''); @@ -153,7 +156,7 @@ describe('', () => { expect(checkbox.hasAttribute('data-user-invalid')).to.be.false; expect(checkbox.hasAttribute('data-user-valid')).to.be.false; - await clickOnElement(checkbox); + checkbox.click(); await checkbox.updateComplete; expect(checkbox.hasAttribute('data-user-invalid')).to.be.true; @@ -201,7 +204,7 @@ describe('', () => { await checkbox.updateComplete; setTimeout(() => button.click()); - await oneEvent(form, 'reset'); + await oneEvent(form, 'reset', false); await checkbox.updateComplete; expect(checkbox.checked).to.true; @@ -209,7 +212,7 @@ describe('', () => { checkbox.defaultChecked = false; setTimeout(() => button.click()); - await oneEvent(form, 'reset'); + await oneEvent(form, 'reset', false); await checkbox.updateComplete; expect(checkbox.checked).to.false; diff --git a/packages/components/src/_components/checkbox/checkbox.ts b/packages/components/src/components/checkbox/checkbox.ts similarity index 65% rename from packages/components/src/_components/checkbox/checkbox.ts rename to packages/components/src/components/checkbox/checkbox.ts index b0593cf02..23bfd53b7 100644 --- a/packages/components/src/_components/checkbox/checkbox.ts +++ b/packages/components/src/components/checkbox/checkbox.ts @@ -1,16 +1,14 @@ import '../icon/icon'; -import { classMap } from 'lit/directives/class-map.js'; +import { css, html } from 'lit'; import { customElement } from '../../../src/internal/register-custom-element'; -import {property, query, state } from 'lit/decorators.js'; import { defaultValue } from '../../internal/default-value'; import { FormControlController } from '../../internal/form'; -import { html } from 'lit'; import { ifDefined } from 'lit/directives/if-defined.js'; import { live } from 'lit/directives/live.js'; +import { property, query } from 'lit/decorators.js'; import { watch } from '../../internal/watch'; +import cx from 'classix'; import SolidElement from '../../internal/solid-element'; -import styles from './checkbox.styles'; -import type { CSSResultGroup } from 'lit'; import type { SolidFormControl } from '../../internal/solid-element'; /** @@ -38,8 +36,6 @@ import type { SolidFormControl } from '../../internal/solid-element'; */ @customElement('sd-checkbox') export default class SdCheckbox extends SolidElement implements SolidFormControl { - static styles: CSSResultGroup = styles; - private readonly formControlController = new FormControlController(this, { value: (control: SdCheckbox) => (control.checked ? control.value || 'on' : undefined), defaultValue: (control: SdCheckbox) => control.defaultChecked, @@ -48,8 +44,6 @@ export default class SdCheckbox extends SolidElement implements SolidFormControl @query('input[type="checkbox"]') input: HTMLInputElement; - @state() private hasFocus = false; - @property() title = ''; // make reactive to pass through /** The name of the checkbox, submitted as a name/value pair with form data. */ @@ -59,7 +53,7 @@ export default class SdCheckbox extends SolidElement implements SolidFormControl @property() value: string; /** The checkbox's size. */ - @property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium'; + @property({ reflect: true }) size: 'sm' | 'lg' = 'lg'; /** Disables the checkbox. */ @property({ type: Boolean, reflect: true }) disabled = false; @@ -86,6 +80,10 @@ export default class SdCheckbox extends SolidElement implements SolidFormControl /** Makes the checkbox a required field. */ @property({ type: Boolean, reflect: true }) required = false; + /** Gets the validity state object */ + get validity() { + return this.input.validity; + } firstUpdated() { this.formControlController.updateValidity(); } @@ -97,7 +95,6 @@ export default class SdCheckbox extends SolidElement implements SolidFormControl } private handleBlur() { - this.hasFocus = false; this.emit('sd-blur'); } @@ -105,13 +102,18 @@ export default class SdCheckbox extends SolidElement implements SolidFormControl this.emit('sd-input'); } + private handleInvalid(event: Event) { + this.formControlController.setValidity(false); + this.formControlController.emitInvalidEvent(event); + } + private handleFocus() { - this.hasFocus = true; this.emit('sd-focus'); } @watch('disabled', { waitUntilFirstUpdate: true }) handleDisabledChange() { + this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false'); // Disabled form controls are always valid this.formControlController.setValidity(this.disabled); } @@ -143,6 +145,11 @@ export default class SdCheckbox extends SolidElement implements SolidFormControl return this.input.checkValidity(); } + /** Gets the associated form, if one exists. */ + getForm(): HTMLFormElement | null { + return this.formControlController.getForm(); + } + /** Checks for validity and shows a validation message if the control is invalid. */ reportValidity() { return this.input.reportValidity(); @@ -161,19 +168,18 @@ export default class SdCheckbox extends SolidElement implements SolidFormControl return html` `; } + + /** + * Inherits Tailwind classes and includes additional styling. + */ + static styles = [ + SolidElement.styles, + css` + :host { + display: block; + } + + :host(:focus-visible) { + outline: 0; + } + + :host([required]) #label::after { + content: ' *'; + } + + :host([data-user-invalid]) #label { + color: rgb(var(--sd-color-error, 204 25 55)); + } + + :host([data-user-invalid]) #control { + border-color: rgb(var(--sd-color-error, 204 25 55)); + } + + :host([data-user-invalid]):host([indeterminate]) #control { + background-color: rgb(var(--sd-color-error, 204 25 55)); + } + ` + ]; } declare global { diff --git a/packages/components/src/components/icon/library.system.ts b/packages/components/src/components/icon/library.system.ts index b71bbf362..89e16864e 100644 --- a/packages/components/src/components/icon/library.system.ts +++ b/packages/components/src/components/icon/library.system.ts @@ -22,7 +22,17 @@ export const icons = { -`, + `, + 'status-hook': ` + + + + `, + 'status-minus': ` + + + + `, start: `