From 8d864143d8edea6fd9f8fbfe2960aca677a0eebf Mon Sep 17 00:00:00 2001 From: Lukas Spirig Date: Wed, 11 Dec 2024 09:50:30 +0100 Subject: [PATCH] feat(date-input): create sbb-date-input as a native text input --- src/elements/core/datetime/date-adapter.ts | 9 +- .../core/datetime/native-date-adapter.ts | 10 +- src/elements/core/mixins.ts | 1 + .../mixins/form-associated-input-mixin.ts | 263 ++++++++++++++++++ src/elements/date-input.ts | 1 + .../date-input.snapshot.spec.snap.js | 60 ++++ src/elements/date-input/date-input.scss | 13 + .../date-input/date-input.snapshot.spec.ts | 27 ++ src/elements/date-input/date-input.spec.ts | 18 ++ .../date-input/date-input.ssr.spec.ts | 20 ++ src/elements/date-input/date-input.stories.ts | 128 +++++++++ src/elements/date-input/date-input.ts | 124 +++++++++ .../date-input/date-input.visual.spec.ts | 28 ++ src/elements/date-input/readme.md | 64 +++++ .../form-field/form-field/form-field.scss | 2 +- .../form-field/form-field/form-field.ts | 2 + 16 files changed, 765 insertions(+), 5 deletions(-) create mode 100644 src/elements/core/mixins/form-associated-input-mixin.ts create mode 100644 src/elements/date-input.ts create mode 100644 src/elements/date-input/__snapshots__/date-input.snapshot.spec.snap.js create mode 100644 src/elements/date-input/date-input.scss create mode 100644 src/elements/date-input/date-input.snapshot.spec.ts create mode 100644 src/elements/date-input/date-input.spec.ts create mode 100644 src/elements/date-input/date-input.ssr.spec.ts create mode 100644 src/elements/date-input/date-input.stories.ts create mode 100644 src/elements/date-input/date-input.ts create mode 100644 src/elements/date-input/date-input.visual.spec.ts create mode 100644 src/elements/date-input/readme.md diff --git a/src/elements/core/datetime/date-adapter.ts b/src/elements/core/datetime/date-adapter.ts index 8be5ef8bed7..528a8371ee2 100644 --- a/src/elements/core/datetime/date-adapter.ts +++ b/src/elements/core/datetime/date-adapter.ts @@ -4,6 +4,7 @@ export const YEARS_PER_ROW: number = 4; export const YEARS_PER_PAGE: number = 24; export const FORMAT_DATE = /(^0?[1-9]?|[12]?[0-9]?|3?[01]?)[.,\\/\-\s](0?[1-9]?|1?[0-2]?)?[.,\\/\-\s](\d{1,4}$)?/; +export const ISO8601_FORMAT_DATE = /^(\d{4})-(\d{2})-(\d{2})$/; /** * Abstract date functionality. @@ -137,7 +138,7 @@ export abstract class DateAdapter { * @param value The date in the format DD.MM.YYYY. * @param now The current date as Date. */ - public abstract parse(value: string | null | undefined, now: T): T | null; + public abstract parse(value: string | null | undefined, now?: T): T | null; /** * Format the given date as string. @@ -146,7 +147,7 @@ export abstract class DateAdapter { */ public format( date: T | null | undefined, - options?: { weekdayStyle?: 'long' | 'short' | 'narrow' }, + options?: { weekdayStyle?: 'long' | 'short' | 'narrow' | 'none' }, ): string { if (!this.isValid(date)) { return ''; @@ -159,6 +160,10 @@ export abstract class DateAdapter { year: 'numeric', }); + if (options?.weekdayStyle === 'none') { + return dateFormatter.format(value); + } + const weekdayStyle = options?.weekdayStyle ?? 'short'; let weekday = this.getDayOfWeekNames(weekdayStyle)[this.getDayOfWeek(date!)]; weekday = weekday.charAt(0).toUpperCase() + weekday.substring(1); diff --git a/src/elements/core/datetime/native-date-adapter.ts b/src/elements/core/datetime/native-date-adapter.ts index 401a33d4a4c..434b43ae501 100644 --- a/src/elements/core/datetime/native-date-adapter.ts +++ b/src/elements/core/datetime/native-date-adapter.ts @@ -1,7 +1,7 @@ import { SbbLanguageController } from '../controllers.js'; import type { SbbDateLike } from '../interfaces.js'; -import { DateAdapter, FORMAT_DATE } from './date-adapter.js'; +import { DateAdapter, FORMAT_DATE, ISO8601_FORMAT_DATE } from './date-adapter.js'; /** * Matches strings that have the form of a valid RFC 3339 string @@ -173,11 +173,17 @@ export class NativeDateAdapter extends DateAdapter { } /** Returns the right format for the `valueAsDate` property. */ - public parse(value: string | null | undefined, now: Date): Date | null { + public parse(value: string | null | undefined, now: Date = this.today()): Date | null { if (!value) { return null; } + const isoMatch = value.match(ISO8601_FORMAT_DATE); + const date = isoMatch ? this.createDate(+isoMatch[1], +isoMatch[2], +isoMatch[3]) : null; + if (this.isValid(date)) { + return date; + } + const strippedValue = value.replace(/\D/g, ' ').trim(); const match: RegExpMatchArray | null | undefined = strippedValue?.match(FORMAT_DATE); diff --git a/src/elements/core/mixins.ts b/src/elements/core/mixins.ts index 3524825a79d..883ece434f5 100644 --- a/src/elements/core/mixins.ts +++ b/src/elements/core/mixins.ts @@ -1,6 +1,7 @@ export * from './mixins/constructor.js'; export * from './mixins/disabled-mixin.js'; export * from './mixins/form-associated-checkbox-mixin.js'; +export * from './mixins/form-associated-input-mixin.js'; export * from './mixins/form-associated-mixin.js'; export * from './mixins/form-associated-radio-button-mixin.js'; export * from './mixins/hydration-mixin.js'; diff --git a/src/elements/core/mixins/form-associated-input-mixin.ts b/src/elements/core/mixins/form-associated-input-mixin.ts new file mode 100644 index 00000000000..4faf2eccdf7 --- /dev/null +++ b/src/elements/core/mixins/form-associated-input-mixin.ts @@ -0,0 +1,263 @@ +import { html, type LitElement } from 'lit'; +import { eventOptions, property } from 'lit/decorators.js'; + +import type { Constructor } from './constructor.js'; +import { + type FormRestoreReason, + type FormRestoreState, + SbbFormAssociatedMixin, + type SbbFormAssociatedMixinType, +} from './form-associated-mixin.js'; +import { SbbRequiredMixin, type SbbRequiredMixinType } from './required-mixin.js'; + +export declare abstract class SbbFormAssociatedInputMixinType + extends SbbFormAssociatedMixinType + implements Partial +{ + public set disabled(value: boolean); + public get disabled(): boolean; + + public set readOnly(value: boolean); + public get readOnly(): boolean; + + public set required(value: boolean); + public get required(): boolean; + + public formResetCallback(): void; + public formStateRestoreCallback(state: FormRestoreState | null, reason: FormRestoreReason): void; + + protected withUserInteraction?(): void; + protected updateFormValue(): void; +} + +/** + * The FormAssociatedCheckboxMixin enables native form support for checkbox controls. + * + * Inherited classes MUST implement the ariaChecked state (ElementInternals) themselves. + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export const SbbFormAssociatedInputMixin = >( + superClass: T, +): Constructor & T => { + abstract class SbbFormAssociatedInputElement + extends SbbRequiredMixin(SbbFormAssociatedMixin(superClass)) + implements Partial + { + /** + * The native text input changes the value property when the value attribute is + * changed under the condition that no input event has occured since creation + * or the last form reset. + */ + private _interacted = false; + /** + * An element with contenteditable will not emit a change event. To achieve parity + * with a native text input, we need to track whether a change event should be + * emitted. + */ + private _shouldEmitChange = false; + /** + * A native text input attempts to submit the form when pressing Enter. + * This can be prevented by calling preventDefault on the keydown event. + * We track whether to request submit, which should occur before the keyup + * event. + */ + private _shouldTriggerSubmit = false; + + /** + * Form type of element. + * @default 'text' + */ + public override get type(): string { + return 'text'; + } + + /** + * The text value of the input element. + */ + public override set value(value: string) { + this.textContent = this._cleanText(value); + } + public override get value(): string { + return this.textContent ?? ''; + } + + /** + * Whether the component is readonly. + * @attr readonly + * @default false + */ + @property({ attribute: false }) + public set readOnly(value: boolean) { + this.toggleAttribute('readonly', !!value); + } + public get readOnly(): boolean { + return this.hasAttribute('readonly'); + } + + /** + * Whether the component is disabled. + * @attr disabled + * @default false + */ + @property({ attribute: false }) + public set disabled(value: boolean) { + this.toggleAttribute('disabled', !!value); + if (this.isConnected) { + this.setAttribute('contenteditable', `${!value}`); + } + } + public get disabled(): boolean { + return this.hasAttribute('disabled'); + } + + protected constructor() { + super(); + /** @internal */ + this.internals.role = 'textbox'; + // We primarily use capture event listeners, as we want + // our listeners to occur before consumer event listeners. + this.addEventListener?.( + 'input', + () => { + this._interacted = true; + this._shouldEmitChange = true; + this.updateFormValue(); + }, + { capture: true }, + ); + this.addEventListener?.( + 'keydown', + (event) => { + if ((event.key === 'Enter' || event.key === '\n') && event.isTrusted) { + event.preventDefault(); + event.stopImmediatePropagation(); + // We prevent recursive events by checking the original event for isTrusted + // which is false for manually dispatched events. + this._shouldTriggerSubmit = this.dispatchEvent(new KeyboardEvent('keydown', event)); + } else if (this.readOnly) { + event.preventDefault(); + } + }, + { capture: true }, + ); + this.addEventListener?.( + 'keyup', + (event) => { + if (event.key === 'Enter' || event.key === '\n') { + this._emitChangeIfNecessary(); + if (this._shouldTriggerSubmit) { + this._shouldTriggerSubmit = false; + this.form?.requestSubmit(); + } + } + }, + { capture: true }, + ); + // contenteditable allows pasting rich content into its host. + // We prevent this by listening to the paste event and + // extracting the plain text from the pasted content + // and inserting it into the selected range (cursor position + // or selection). + this.addEventListener?.('paste', (e) => { + e.preventDefault(); + const text = e.clipboardData?.getData('text/plain'); + const selectedRange = window.getSelection()?.getRangeAt(0); + if (!selectedRange || !text) { + return; + } + + selectedRange.deleteContents(); + selectedRange.insertNode(document.createTextNode(text)); + selectedRange.setStart(selectedRange.endContainer, selectedRange.endOffset); + this.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true })); + }); + // On blur the native text input scrolls the text to the start of the text. + // We mimick that by resetting the scroll position. + this.addEventListener?.( + 'blur', + () => { + this._emitChangeIfNecessary(); + this.scrollLeft = 0; + }, + { capture: true }, + ); + } + + public override connectedCallback(): void { + super.connectedCallback(); + this.setAttribute('contenteditable', `${!this.disabled}`); + // We want to replace any content by just the text content. + this.innerHTML = this.value; + } + + public override attributeChangedCallback( + name: string, + old: string | null, + value: string | null, + ): void { + if (name !== 'value' || !this._interacted) { + super.attributeChangedCallback(name, old, value); + } + } + + /** + * Is called whenever the form is being reset. + * + * @internal + */ + public override formResetCallback(): void { + this._interacted = false; + this.value = this.getAttribute('value') ?? ''; + } + + /** + * Called when the browser is trying to restore element’s state to state in which case + * reason is “restore”, or when the browser is trying to fulfill autofill on behalf of + * user in which case reason is “autocomplete”. + * In the case of “restore”, state is a string, File, or FormData object + * previously set as the second argument to setFormValue. + * + * @internal + */ + public override formStateRestoreCallback( + state: FormRestoreState | null, + _reason: FormRestoreReason, + ): void { + if (state && typeof state === 'string') { + this.value = state; + } + } + + protected override updateFormValue(): void { + this.internals.setFormValue(this.value, this.value); + } + + private _cleanText(value: string): string { + // The native text input removes all newline characters if passed to the value property + return `${value}`.replace(/[\n\r]+/g, ''); + } + + @eventOptions({ passive: true }) + private _cleanChildren(): void { + if (this.childElementCount) { + for (const element of this.children) { + element.remove(); + } + } + } + + private _emitChangeIfNecessary(): void { + if (this._shouldEmitChange) { + this._shouldEmitChange = false; + this.dispatchEvent(new Event('change', { bubbles: true })); + } + } + + protected override render(): unknown { + return html``; + } + } + + return SbbFormAssociatedInputElement as unknown as Constructor & + T; +}; diff --git a/src/elements/date-input.ts b/src/elements/date-input.ts new file mode 100644 index 00000000000..2f7535f8d18 --- /dev/null +++ b/src/elements/date-input.ts @@ -0,0 +1 @@ +export * from './date-input/date-input.js'; diff --git a/src/elements/date-input/__snapshots__/date-input.snapshot.spec.snap.js b/src/elements/date-input/__snapshots__/date-input.snapshot.spec.snap.js new file mode 100644 index 00000000000..5e1032f32a3 --- /dev/null +++ b/src/elements/date-input/__snapshots__/date-input.snapshot.spec.snap.js @@ -0,0 +1,60 @@ +/* @web/test-runner snapshot v1 */ +export const snapshots = {}; + +snapshots["sbb-date-input renders DOM"] = +` + We, 11.12.2024 + +`; +/* end snapshot sbb-date-input renders DOM */ + +snapshots["sbb-date-input renders Shadow DOM"] = +` + +`; +/* end snapshot sbb-date-input renders Shadow DOM */ + +snapshots["sbb-date-input renders A11y tree Chrome"] = +`

+ { + "role": "WebArea", + "name": "", + "children": [ + { + "role": "textbox", + "name": "", + "multiline": true, + "children": [ + { + "role": "text", + "name": "We, 11.12.2024" + } + ], + "value": "We, 11.12.2024" + } + ] +} +

+`; +/* end snapshot sbb-date-input renders A11y tree Chrome */ + +snapshots["sbb-date-input renders A11y tree Firefox"] = +`

+ { + "role": "document", + "name": "", + "children": [ + { + "role": "textbox", + "name": "", + "value": "We, 11.12.2024" + } + ] +} +

+`; +/* end snapshot sbb-date-input renders A11y tree Firefox */ + diff --git a/src/elements/date-input/date-input.scss b/src/elements/date-input/date-input.scss new file mode 100644 index 00000000000..b02ecace55c --- /dev/null +++ b/src/elements/date-input/date-input.scss @@ -0,0 +1,13 @@ +:host { + display: inline-block; + max-width: 100%; + cursor: text; +} + +:host([disabled]) { + cursor: default; +} + +:host(:focus) { + text-overflow: initial !important; +} diff --git a/src/elements/date-input/date-input.snapshot.spec.ts b/src/elements/date-input/date-input.snapshot.spec.ts new file mode 100644 index 00000000000..59c8dec04cc --- /dev/null +++ b/src/elements/date-input/date-input.snapshot.spec.ts @@ -0,0 +1,27 @@ +import { expect } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; + +import { fixture, testA11yTreeSnapshot } from '../core/testing/private.js'; + +import type { SbbDateInputElement } from './date-input.js'; +import './date-input.js'; + +describe(`sbb-date-input`, () => { + describe('renders', () => { + let element: SbbDateInputElement; + + beforeEach(async () => { + element = await fixture(html``); + }); + + it('DOM', async () => { + await expect(element).dom.to.be.equalSnapshot(); + }); + + it('Shadow DOM', async () => { + await expect(element).shadowDom.to.be.equalSnapshot(); + }); + + testA11yTreeSnapshot(); + }); +}); diff --git a/src/elements/date-input/date-input.spec.ts b/src/elements/date-input/date-input.spec.ts new file mode 100644 index 00000000000..193c85f237d --- /dev/null +++ b/src/elements/date-input/date-input.spec.ts @@ -0,0 +1,18 @@ +import { assert } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; + +import { fixture } from '../core/testing/private.js'; + +import { SbbDateInputElement } from './date-input.js'; + +describe('sbb-date-input', () => { + let element: SbbDateInputElement; + + beforeEach(async () => { + element = await fixture(html``); + }); + + it('renders', async () => { + assert.instanceOf(element, SbbDateInputElement); + }); +}); diff --git a/src/elements/date-input/date-input.ssr.spec.ts b/src/elements/date-input/date-input.ssr.spec.ts new file mode 100644 index 00000000000..66b8ea9124d --- /dev/null +++ b/src/elements/date-input/date-input.ssr.spec.ts @@ -0,0 +1,20 @@ +import { assert } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; + +import { ssrHydratedFixture } from '../core/testing/private.js'; + +import { SbbDateInputElement } from './date-input.js'; + +describe(`sbb-date-input ssr`, () => { + let root: SbbDateInputElement; + + beforeEach(async () => { + root = await ssrHydratedFixture(html``, { + modules: ['./date-input.js'], + }); + }); + + it('renders', () => { + assert.instanceOf(root, SbbDateInputElement); + }); +}); diff --git a/src/elements/date-input/date-input.stories.ts b/src/elements/date-input/date-input.stories.ts new file mode 100644 index 00000000000..dcb29a5f4ca --- /dev/null +++ b/src/elements/date-input/date-input.stories.ts @@ -0,0 +1,128 @@ +import { withActions } from '@storybook/addon-actions/decorator'; +import type { InputType } from '@storybook/types'; +import type { + Args, + ArgTypes, + Decorator, + Meta, + StoryContext, + StoryObj, +} from '@storybook/web-components'; +import type { TemplateResult } from 'lit'; +import { html } from 'lit'; +import { ref } from 'lit/directives/ref.js'; + +import { defaultDateAdapter } from '../core/datetime.js'; + +import { SbbDateInputElement } from './date-input.js'; +import readme from './readme.md?raw'; + +import '../form-field.js'; +import '../title.js'; + +const negative: InputType = { + control: { + type: 'boolean', + }, + table: { + category: 'Date Input', + }, +}; + +const disabled: InputType = { + control: { + type: 'boolean', + }, + table: { + category: 'Date Input', + }, +}; + +const readonly: InputType = { + control: { + type: 'boolean', + }, + table: { + category: 'Date Input', + }, +}; + +const value: InputType = { + control: { + type: 'text', + }, + table: { + category: 'Date Input', + }, +}; + +const defaultArgTypes: ArgTypes = { + negative, + disabled, + readonly, + value, +}; + +const defaultArgs: Args = { + negative: false, + disabled: false, + readonly: false, + value: '2024-12-11', +}; + +const Template = ({ value, disabled, readonly }: Args): TemplateResult => + html` + + { + if (!dateInput || !(dateInput instanceof SbbDateInputElement)) { + return; + } + const [valueOutput, isoOutput] = Array.from( + (dateInput.parentElement!.nextElementSibling as HTMLElement)!.querySelectorAll( + 'output', + )!, + ); + const updateOutputs = (): void => { + valueOutput.value = String(dateInput.valueAsDate); + isoOutput.value = dateInput.valueAsDate + ? defaultDateAdapter.toIso8601(dateInput.valueAsDate) + : 'null'; + }; + updateOutputs(); + dateInput.addEventListener('input', updateOutputs); + })} + > + +

+ valueAsDate + + ISO 8601 Date + +

`; + +export const Default: StoryObj = { + render: Template, + argTypes: defaultArgTypes, + args: { ...defaultArgs }, +}; + +const meta: Meta = { + decorators: [withActions as Decorator], + parameters: { + actions: { + handles: ['input', 'change', 'keydown', 'keyup', 'keypressed', 'focus', 'blur'], + }, + backgroundColor: (context: StoryContext) => + context.args.negative ? 'var(--sbb-color-black)' : 'var(--sbb-color-white)', + docs: { + extractComponentDescription: () => readme, + }, + }, + title: 'elements/sbb-date-input', +}; + +export default meta; diff --git a/src/elements/date-input/date-input.ts b/src/elements/date-input/date-input.ts new file mode 100644 index 00000000000..9fed8b0c7eb --- /dev/null +++ b/src/elements/date-input/date-input.ts @@ -0,0 +1,124 @@ +import { LitElement, type CSSResultGroup } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; + +import { readConfig } from '../core/config.js'; +import { type DateAdapter, defaultDateAdapter } from '../core/datetime.js'; +import { + SbbFormAssociatedInputMixin, + type FormRestoreReason, + type FormRestoreState, +} from '../core/mixins.js'; + +import style from './date-input.scss?lit&inline'; + +/** + * Custom input for a date. + */ +export +@customElement('sbb-date-input') +class SbbDateInputElement extends SbbFormAssociatedInputMixin(LitElement) { + public static override styles: CSSResultGroup = style; + + private _dateAdapter: DateAdapter = readConfig().datetime?.dateAdapter ?? defaultDateAdapter; + + /** + * The value of the date input. Reflects the current text value + * of this input. + * @attr Accepts ISO8601 formatted values, which will be + * formatted according to the current locale. + */ + public override set value(value: string) { + this._tryParseValue(value); + super.value = this.valueAsDate !== null ? this._formatDate() : value; + } + public override get value(): string { + return super.value ?? ''; + } + + @property({ attribute: false }) + public set valueAsDate(value: T | null) { + if (!this._dateAdapter.isDateInstance(value) || !this._dateAdapter.isValid(value)) { + this._valueAsDate = null; + this._valueCache = ['', null]; + this.value = ''; + } else if ( + !this._dateAdapter.isDateInstance(this._valueCache[1]) || + !this._dateAdapter.compareDate(this._valueCache[1]!, value!) + ) { + // Align with the native date input, as it copies the value of + // the given date and does not retain the original instance. + this._valueAsDate = this._dateAdapter.clone(value!); + const stringValue = this._formatDate(); + this._valueCache = [stringValue, this._valueAsDate]; + this.value = stringValue; + } + } + public get valueAsDate(): T | null { + return this._valueAsDate ?? null; + } + private _valueAsDate?: T | null; + + @property({ attribute: 'weekday-style' }) + public accessor weekdayStyle: 'long' | 'short' | 'narrow' | 'none' = 'short'; + + private _valueCache: [string, T | null] = ['', null]; + + public constructor() { + super(); + this.addEventListener?.( + 'change', + () => { + if (this.valueAsDate) { + const formattedDate = this._formatDate(); + if (this.value !== formattedDate) { + this.value = formattedDate; + } + } + }, + { capture: true }, + ); + } + + /** + * Called when the browser is trying to restore element’s state to state in which case + * reason is “restore”, or when the browser is trying to fulfill autofill on behalf of + * user in which case reason is “autocomplete”. + * In the case of “restore”, state is a string, File, or FormData object + * previously set as the second argument to setFormValue. + * + * @internal + */ + public override formStateRestoreCallback( + state: FormRestoreState | null, + _reason: FormRestoreReason, + ): void { + if (state && typeof state === 'string') { + this.value = state; + } + } + + protected override updateFormValue(): void { + this._tryParseValue(); + const formValue = + this.valueAsDate !== null ? this._dateAdapter.toIso8601(this.valueAsDate) : null; + this.internals.setFormValue(formValue, this.value); + } + + private _tryParseValue(value = this.value): void { + if (this._valueCache[0] !== value) { + this._valueAsDate = this._dateAdapter.parse(value); + this._valueCache = [value, this._valueAsDate]; + } + } + + private _formatDate(): string { + return this._dateAdapter.format(this.valueAsDate, { weekdayStyle: this.weekdayStyle }); + } +} + +declare global { + interface HTMLElementTagNameMap { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'sbb-date-input': SbbDateInputElement; + } +} diff --git a/src/elements/date-input/date-input.visual.spec.ts b/src/elements/date-input/date-input.visual.spec.ts new file mode 100644 index 00000000000..bf1a234d9a3 --- /dev/null +++ b/src/elements/date-input/date-input.visual.spec.ts @@ -0,0 +1,28 @@ +import { html } from 'lit'; + +import { describeViewports, visualDiffStandardStates } from '../core/testing/private.js'; + +import './date-input.js'; +import '../form-field/form-field.js'; + +describe('sbb-date-input', () => { + /** + * Add the `viewports` param to test only specific viewport; + * add the `viewportHeight` param to set a fixed height for the browser. + */ + describeViewports({ viewports: ['zero', 'medium'] }, () => { + for (const state of visualDiffStandardStates) { + it( + state.name, + state.with(async (setup) => { + await setup.withFixture(html` + + + + `); + setup.withStateElement(setup.snapshotElement.querySelector('sbb-date-input')!); + }), + ); + } + }); +}); diff --git a/src/elements/date-input/readme.md b/src/elements/date-input/readme.md new file mode 100644 index 00000000000..18876162e42 --- /dev/null +++ b/src/elements/date-input/readme.md @@ -0,0 +1,64 @@ +> Explain the use and the purpose of the component; add minor details if needed and provide a basic example.
+> If you reference other components, link their documentation at least once (the path must start from _/docs/..._ ).
+> For the examples, use triple backticks with file extension (` ```html ``` `).
+> The following list of paragraphs is only suggested; remove, create and adapt as needed. + +The `sbb-date-input` is a component . . . + +```html + +``` + +## Slots + +> Describe slot naming and usage and provide an example of slotted content. + +## States + +> Describe the component states (`disabled`, `readonly`, etc.) and provide examples. + +## Style + +> Describe the properties which change the component visualization (`size`, `negative`, etc.) and provide examples. + +## Interactions + +> Describe how it's possible to interact with the component (open and close a `sbb-dialog`, dismiss a `sbb-alert`, etc.) and provide examples. + +## Events + +> Describe events triggered by the component and possibly how to get information from the payload. + +## Keyboard interaction + +> If the component has logic for keyboard navigation (as the `sbb-calendar` or the `sbb-select`) describe it. + +| Keyboard | Action | +| -------------- | ------------- | +| Key | What it does. | + +## Accessibility + +> Describe how accessibility is implemented and if there are issues or suggested best-practice for the consumers. + + + +## Properties + +| Name | Attribute | Privacy | Type | Default | Description | +| -------------- | --------------- | ------- | ----------------------------------------- | --------- | --------------------------------------------------------------------------- | +| `disabled` | `disabled` | public | `boolean` | `false` | Whether the component is disabled. | +| `form` | - | public | `HTMLFormElement \| null` | | Returns the form owner of the internals of the target element. | +| `name` | `name` | public | `string` | | Name of the form element. Will be read from name attribute. | +| `readOnly` | `readonly` | public | `boolean` | `false` | Whether the component is readonly. | +| `required` | `required` | public | `boolean` | `false` | Whether the component is required. | +| `type` | - | public | `string` | `'text'` | Form type of element. | +| `value` | `Accepts` | public | `string \| null` | `null` | The value of the date input. Reflects the current text value of this input. | +| `valueAsDate` | - | public | `T \| null` | | | +| `weekdayStyle` | `weekday-style` | public | `'long' \| 'short' \| 'narrow' \| 'none'` | `'short'` | | + +## Events + +| Name | Type | Description | Inherited From | +| -------- | ------- | ----------- | --------------------------- | +| `change` | `Event` | | SbbFormAssociatedInputMixin | diff --git a/src/elements/form-field/form-field/form-field.scss b/src/elements/form-field/form-field/form-field.scss index c58469fdd1f..b1ed6f26792 100644 --- a/src/elements/form-field/form-field/form-field.scss +++ b/src/elements/form-field/form-field/form-field.scss @@ -405,7 +405,7 @@ // Input -.sbb-form-field__input ::slotted(:where(input, select, textarea, sbb-select)) { +.sbb-form-field__input ::slotted(:where(input, select, textarea, sbb-select, sbb-date-input)) { @include sbb.text-m--regular; @include sbb.ellipsis; @include sbb.input-reset; diff --git a/src/elements/form-field/form-field/form-field.ts b/src/elements/form-field/form-field/form-field.ts index c1d3762236b..4cdefc1b1a5 100644 --- a/src/elements/form-field/form-field/form-field.ts +++ b/src/elements/form-field/form-field/form-field.ts @@ -46,6 +46,7 @@ class SbbFormFieldElement extends SbbNegativeMixin(SbbHydrationMixin(LitElement) // List of supported element selectors in unnamed slot private readonly _supportedInputElements = [ ...this._supportedNativeInputElements, + 'sbb-date-input', 'sbb-select', 'sbb-slider', ]; @@ -55,6 +56,7 @@ class SbbFormFieldElement extends SbbNegativeMixin(SbbHydrationMixin(LitElement) private readonly _floatingLabelSupportedInputElements = [ 'input', 'select', + 'sbb-date-input', 'sbb-select', 'textarea', ];