From 0eaf476c6b2c98f734ec1c596e21ac0ece325d9a Mon Sep 17 00:00:00 2001 From: Davide Mininni Date: Thu, 23 May 2024 18:25:28 +0200 Subject: [PATCH] feat: add now mixin --- src/components/calendar/calendar.ts | 28 +++++++------- src/components/calendar/readme.md | 4 +- src/components/clock/clock.stories.ts | 6 +-- src/components/clock/clock.ts | 24 ++++-------- src/components/clock/readme.md | 8 ++-- src/components/core/mixins.ts | 1 + src/components/core/mixins/now-mixin.ts | 37 +++++++++++++++++++ .../datepicker/common/datepicker-button.ts | 4 +- .../datepicker-toggle/datepicker-toggle.ts | 4 +- .../datepicker/datepicker/datepicker.ts | 19 ++++------ .../datepicker/datepicker/readme.md | 4 +- .../journey-summary/journey-summary.ts | 12 ++---- src/components/journey-summary/readme.md | 4 +- .../pearl-chain-time/pearl-chain-time.ts | 12 ++---- src/components/pearl-chain-time/readme.md | 4 +- src/components/pearl-chain/pearl-chain.ts | 16 +++----- src/components/pearl-chain/readme.md | 4 +- src/components/timetable-row/readme.md | 4 +- src/components/timetable-row/timetable-row.ts | 12 ++---- 19 files changed, 103 insertions(+), 104 deletions(-) create mode 100644 src/components/core/mixins/now-mixin.ts diff --git a/src/components/calendar/calendar.ts b/src/components/calendar/calendar.ts index 3028e8c2c7..e9e8c72b3e 100644 --- a/src/components/calendar/calendar.ts +++ b/src/components/calendar/calendar.ts @@ -33,6 +33,7 @@ import { i18nYearMonthSelection, } from '../core/i18n.js'; import type { SbbDateLike } from '../core/interfaces.js'; +import { SbbNowMixin } from '../core/mixins.js'; import style from './calendar.scss?lit&inline'; @@ -90,7 +91,7 @@ export type CalendarView = 'day' | 'month' | 'year'; * @event {CustomEvent} dateSelected - Event emitted on date selection. */ @customElement('sbb-calendar') -export class SbbCalendarElement extends LitElement { +export class SbbCalendarElement extends SbbNowMixin(LitElement) { public static override styles: CSSResultGroup = style; public static readonly events = { dateSelected: 'dateSelected', @@ -141,9 +142,6 @@ export class SbbCalendarElement extends LitElement { /** A function used to filter out dates. */ @property({ attribute: 'date-filter' }) public dateFilter?: (date: T | null) => boolean; - /** A specific date for the current datetime (timestamp in milliseconds). */ - @property({ attribute: 'now' }) public dataNow?: number; - private _dateAdapter: DateAdapter = defaultDateAdapter as unknown as DateAdapter; /** Event emitted on date selection. */ @@ -153,7 +151,7 @@ export class SbbCalendarElement extends LitElement { ); /** The currently active date. */ - @state() private _activeDate: T = this._now(); + @state() private _activeDate: T = this._getNow(); /** The selected date as ISOString. */ @state() private _selected?: string; @@ -237,7 +235,7 @@ export class SbbCalendarElement extends LitElement { if (this._calendarView !== 'day') { this._resetToDayView(); } - this._activeDate = this.selected ?? this._now(); + this._activeDate = this.selected ?? this._getNow(); this._init(); } @@ -609,7 +607,7 @@ export class SbbCalendarElement extends LitElement { } private _getFirstFocusable(): HTMLButtonElement { - const active = this._selected ? this._dateAdapter.deserialize(this._selected)! : this._now(); + const active = this._selected ? this._dateAdapter.deserialize(this._selected)! : this._getNow(); let firstFocusable = this.shadowRoot!.querySelector('.sbb-calendar__selected') ?? this.shadowRoot!.querySelector( @@ -798,9 +796,9 @@ export class SbbCalendarElement extends LitElement { : this._findNext(days, nextIndex, -verticalOffset); } - private _now(): T { - if (this.dataNow) { - const today = new Date(+this.dataNow); + private _getNow(): T { + if (this.now) { + const today = new Date(+this.now); if (defaultDateAdapter.isValid(today)) { return this._dateAdapter.createDate( today.getFullYear(), @@ -814,7 +812,7 @@ export class SbbCalendarElement extends LitElement { private _resetToDayView(): void { this._resetFocus = true; - this._activeDate = this.selected ?? this._now(); + this._activeDate = this.selected ?? this._getNow(); this._chosenYear = undefined; this._chosenMonth = undefined; this._nextCalendarView = 'day'; @@ -926,7 +924,7 @@ export class SbbCalendarElement extends LitElement { /** Creates the table body with the day cells. For the first row, it also considers the possible day's offset. */ private _createDayTableBody(weeks: Day[][]): TemplateResult[] { - const today: string = this._dateAdapter.toIso8601(this._now()); + const today: string = this._dateAdapter.toIso8601(this._getNow()); return weeks.map((week: Day[], rowIndex: number) => { const firstRowOffset: number = DAYS_PER_ROW - week.length; if (rowIndex === 0 && firstRowOffset) { @@ -1061,8 +1059,8 @@ export class SbbCalendarElement extends LitElement { !!this._selected && year === selectedYear && month.monthValue === selectedMonth; const isCurrentMonth = - year === this._dateAdapter.getYear(this._now()) && - this._dateAdapter.getMonth(this._now()) === month.monthValue; + year === this._dateAdapter.getYear(this._getNow()) && + this._dateAdapter.getMonth(this._getNow()) === month.monthValue; return html` extends LitElement { /** Creates the table for the year selection view. */ private _createYearTable(years: number[][], shiftRight = false): TemplateResult { - const now = this._now(); + const now = this._getNow(); return html` this._tableAnimationEnd(e)} diff --git a/src/components/calendar/readme.md b/src/components/calendar/readme.md index a4aa8e3b7e..f30532ce1c 100644 --- a/src/components/calendar/readme.md +++ b/src/components/calendar/readme.md @@ -20,7 +20,7 @@ It's recommended to set the time to 00:00:00. ``` -To specify a specific date for the current datetime, you can use the `now` property (timestamp in milliseconds). +To simulate the current datetime, you can use the `now` property (timestamp in milliseconds). This is helpful if you need a specific state of the component. ## Style @@ -64,10 +64,10 @@ For accessibility purposes, the component is rendered as a native table element | Name | Attribute | Privacy | Type | Default | Description | | ------------ | ------------- | ------- | ------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------ | -| `dataNow` | `now` | public | `number \| undefined` | | A specific date for the current datetime (timestamp in milliseconds). | | `dateFilter` | `date-filter` | public | `(date: T \| null) => boolean \| undefined` | | A function used to filter out dates. | | `max` | `max` | public | `T \| null` | | The maximum valid date. Takes T Object, ISOString, and Unix Timestamp (number of seconds since Jan 1, 1970). | | `min` | `min` | public | `T \| null` | | The minimum valid date. Takes T Object, ISOString, and Unix Timestamp (number of seconds since Jan 1, 1970). | +| `now` | `now` | public | `number \| undefined` | | A specific date for the current datetime (timestamp in milliseconds). | | `selected` | `selected` | public | `T \| null` | | The selected date. Takes T Object, ISOString, and Unix Timestamp (number of seconds since Jan 1, 1970). | | `wide` | `wide` | public | `boolean` | `false` | If set to true, two months are displayed | diff --git a/src/components/clock/clock.stories.ts b/src/components/clock/clock.stories.ts index 54fc460d59..db8298c669 100644 --- a/src/components/clock/clock.stories.ts +++ b/src/components/clock/clock.stories.ts @@ -11,7 +11,7 @@ import readme from './readme.md?raw'; import './clock.js'; -const dataNow: InputType = { +const now: InputType = { control: { type: 'date', }, @@ -21,13 +21,13 @@ const Template = (args: Args): TemplateResult => html` { if (document.visibilityState === 'hidden') { this._stopClock(); - } else if (!this.dataNow) { + } else if (!this.now) { await this._startClock(); } } @@ -119,7 +118,7 @@ export class SbbClockElement extends LitElement { /** Given the current date, calculates the hh/mm/ss values and the hh/mm/ss left to the next midnight. */ private _assignCurrentTime(): void { - const date = this._now(); + const date = new Date(this.dateNow); this._hours = date.getHours() % 12; this._minutes = date.getMinutes(); this._seconds = date.getSeconds(); @@ -219,7 +218,7 @@ export class SbbClockElement extends LitElement { private _stopClock(): void { clearInterval(this._handMovement); - if (this.dataNow) { + if (this.now) { this._setHandsStartingPosition(); this._clockHandSeconds?.classList.add('sbb-clock__hand-seconds--initial-minute'); this._clockHandHours?.classList.add('sbb-clock__hand-hours--initial-hour'); @@ -254,19 +253,12 @@ export class SbbClockElement extends LitElement { ); } - private _now(): Date { - if (this.dataNow) { - return new Date(+this.dataNow); - } - return new Date(); - } - protected override async firstUpdated(changedProperties: PropertyValues): Promise { super.firstUpdated(changedProperties); this._addEventListeners(); - if (this.dataNow) { + if (this.now) { this._stopClock(); } else { await this._startClock(); diff --git a/src/components/clock/readme.md b/src/components/clock/readme.md index bb2d33b9c1..bf4917b7ba 100644 --- a/src/components/clock/readme.md +++ b/src/components/clock/readme.md @@ -7,7 +7,7 @@ then it briefly pauses at the clock top before starting a new rotation. ``` -To specify a specific date for the current datetime, you can use the `now` property (timestamp in milliseconds). +To simulate the current datetime, you can use the `now` property (timestamp in milliseconds). This is helpful if you need a specific state of the component. ```html @@ -18,6 +18,6 @@ This is helpful if you need a specific state of the component. ## Properties -| Name | Attribute | Privacy | Type | Default | Description | -| --------- | --------- | ------- | --------------------- | ------- | --------------------------------------------------------------------- | -| `dataNow` | `now` | public | `number \| undefined` | | A specific date for the current datetime (timestamp in milliseconds). | +| Name | Attribute | Privacy | Type | Default | Description | +| ----- | --------- | ------- | --------------------- | ------- | --------------------------------------------------------------------- | +| `now` | `now` | public | `number \| undefined` | | A specific date for the current datetime (timestamp in milliseconds). | diff --git a/src/components/core/mixins.ts b/src/components/core/mixins.ts index 2038021e61..0f9fbc9303 100644 --- a/src/components/core/mixins.ts +++ b/src/components/core/mixins.ts @@ -5,5 +5,6 @@ export * from './mixins/form-associated-mixin.js'; export * from './mixins/hydration-mixin.js'; export * from './mixins/named-slot-list-mixin.js'; export * from './mixins/negative-mixin.js'; +export * from './mixins/now-mixin.js'; export * from './mixins/required-mixin.js'; export * from './mixins/update-scheduler-mixin.js'; diff --git a/src/components/core/mixins/now-mixin.ts b/src/components/core/mixins/now-mixin.ts new file mode 100644 index 0000000000..1c757e48c4 --- /dev/null +++ b/src/components/core/mixins/now-mixin.ts @@ -0,0 +1,37 @@ +import type { LitElement } from 'lit'; +import { property } from 'lit/decorators.js'; + +import type { AbstractConstructor } from './constructor.js'; + +export declare class SbbNowMixinType { + public set now(value: number | string); + public get now(): number; + protected get dateNow(): number; +} + +/** + * Enhance your component with a `now` property. + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export const SbbNowMixin = >( + superClass: T, +): AbstractConstructor & T => { + abstract class SbbNowElement extends superClass implements Partial { + /** A specific date for the current datetime (timestamp in milliseconds). */ + @property({ type: Number }) + public get now(): number | undefined { + return this._now; + } + public set now(value: number | string) { + this._now = +value; + } + private _now?: number; + + /** Returns the `_now` value if available, otherwise the current datetime (as timestamp in millisecond). */ + protected get dateNow(): number { + return this._now ?? Date.now(); + } + } + + return SbbNowElement as unknown as AbstractConstructor & T; +}; diff --git a/src/components/datepicker/common/datepicker-button.ts b/src/components/datepicker/common/datepicker-button.ts index 070ce9d4a0..c5d08381dc 100644 --- a/src/components/datepicker/common/datepicker-button.ts +++ b/src/components/datepicker/common/datepicker-button.ts @@ -90,7 +90,7 @@ export abstract class SbbDatepickerButton extends SbbNegativeMixin(SbbButtonBase return; } const startingDate: Date = - this.datePickerElement.getValueAsDate() ?? this.datePickerElement.now(); + this.datePickerElement.getValueAsDate() ?? this.datePickerElement.getNow(); const date: Date = this.findAvailableDate( startingDate, this.datePickerElement.dateFilter, @@ -175,7 +175,7 @@ export abstract class SbbDatepickerButton extends SbbNegativeMixin(SbbButtonBase } const currentDateString = - this.datePickerElement?.now().toDateString() === currentDate.toDateString() + this.datePickerElement?.getNow().toDateString() === currentDate.toDateString() ? i18nToday[this._language.current].toLowerCase() : this._dateAdapter.getAccessibilityFormatDate(currentDate); diff --git a/src/components/datepicker/datepicker-toggle/datepicker-toggle.ts b/src/components/datepicker/datepicker-toggle/datepicker-toggle.ts index fd4f5e530c..b253e0ebd9 100644 --- a/src/components/datepicker/datepicker-toggle/datepicker-toggle.ts +++ b/src/components/datepicker/datepicker-toggle/datepicker-toggle.ts @@ -156,8 +156,8 @@ export class SbbDatepickerToggleElement extends SbbNegativeMixin(LitElement) { } private _now(): Date | undefined { - if (this._datePickerElement?.dataNow) { - const today = new Date(+this._datePickerElement?.dataNow); + if (this._datePickerElement?.now) { + const today = new Date(+this._datePickerElement?.now); today.setHours(0, 0, 0, 0); return today; } diff --git a/src/components/datepicker/datepicker/datepicker.ts b/src/components/datepicker/datepicker/datepicker.ts index c96a956899..68466f7eca 100644 --- a/src/components/datepicker/datepicker/datepicker.ts +++ b/src/components/datepicker/datepicker/datepicker.ts @@ -10,6 +10,7 @@ import { findInput, findReferencedElement } from '../../core/dom.js'; import { EventEmitter } from '../../core/eventing.js'; import { i18nDateChangedTo, i18nDatePickerPlaceholder } from '../../core/i18n.js'; import type { SbbDateLike, SbbValidationChangeEvent } from '../../core/interfaces.js'; +import { SbbNowMixin } from '../../core/mixins.js'; import { AgnosticMutationObserver } from '../../core/observers.js'; import type { SbbDatepickerButton } from '../common.js'; import type { SbbDatepickerToggleElement } from '../datepicker-toggle.js'; @@ -165,7 +166,7 @@ export const datepickerControlRegisteredEventFactory = (): CustomEvent => * @event {CustomEvent} validationChange - Emits whenever the internal validation state changes. */ @customElement('sbb-datepicker') -export class SbbDatepickerElement extends LitElement { +export class SbbDatepickerElement extends SbbNowMixin(LitElement) { public static override styles: CSSResultGroup = style; public static readonly events = { didChange: 'didChange', @@ -191,9 +192,6 @@ export class SbbDatepickerElement extends LitElement { /** Reference of the native input connected to the datepicker. */ @property() public input?: string | HTMLElement; - /** A specific date for the current datetime (timestamp in milliseconds). */ - @property({ attribute: 'now' }) public dataNow?: number; - /** * @deprecated only used for React. Will probably be removed once React 19 is available. */ @@ -462,17 +460,14 @@ export class SbbDatepickerElement extends LitElement { * @internal * Returns current date or configured date. */ - public now(): Date { - if (this.dataNow) { - const today = new Date(+this.dataNow); - today.setHours(0, 0, 0, 0); - return today; - } - return this._dateAdapter.today(); + public getNow(): Date { + const today = new Date(this.dateNow); + today.setHours(0, 0, 0, 0); + return today; } private _parse(value: string): Date | undefined { - return this.dateParser ? this.dateParser(value) : this._dateAdapter.parse(value, this.now()); + return this.dateParser ? this.dateParser(value) : this._dateAdapter.parse(value, this.getNow()); } private _format(date: Date): string { diff --git a/src/components/datepicker/datepicker/readme.md b/src/components/datepicker/datepicker/readme.md index cd5a911d1e..e38516b58e 100644 --- a/src/components/datepicker/datepicker/readme.md +++ b/src/components/datepicker/datepicker/readme.md @@ -57,7 +57,7 @@ a `blur` event is fired on the input to ensure compatibility with any framework ## Custom date formats -To specify a specific date for the current datetime, you can use the `now` property (timestamp in milliseconds). +To simulate the current datetime, you can use the `now` property (timestamp in milliseconds). This is helpful if you need a specific state of the component. Using a combination of the `dateParser` and `format` properties, it's possible to configure the datepicker @@ -104,11 +104,11 @@ Whenever the validation state changes (e.g., a valid value becomes invalid or vi | Name | Attribute | Privacy | Type | Default | Description | | ------------ | ------------- | ------- | --------------------------------------------------- | ------- | --------------------------------------------------------------------- | -| `dataNow` | `now` | public | `number \| undefined` | | A specific date for the current datetime (timestamp in milliseconds). | | `dateFilter` | `date-filter` | public | `(date: Date \| null) => boolean` | | A function used to filter out dates. | | `dateParser` | `date-parser` | public | `(value: string) => Date \| undefined \| undefined` | | A function used to parse string value into dates. | | `format` | `format` | public | `(date: Date) => string \| undefined` | | A function used to format dates into the preferred string format. | | `input` | `input` | public | `string \| HTMLElement \| undefined` | | Reference of the native input connected to the datepicker. | +| `now` | `now` | public | `number \| undefined` | | A specific date for the current datetime (timestamp in milliseconds). | | `wide` | `wide` | public | `boolean` | `false` | If set to true, two months are displayed. | ## Methods diff --git a/src/components/journey-summary/journey-summary.ts b/src/components/journey-summary/journey-summary.ts index 883096acd3..a8425f8611 100644 --- a/src/components/journey-summary/journey-summary.ts +++ b/src/components/journey-summary/journey-summary.ts @@ -10,6 +10,7 @@ import { removeTimezoneFromISOTimeString, } from '../core/datetime.js'; import { i18nTripDuration } from '../core/i18n.js'; +import { SbbNowMixin } from '../core/mixins.js'; import type { Leg } from '../core/timetable.js'; import type { SbbTitleLevel } from '../title.js'; @@ -38,7 +39,7 @@ export interface InterfaceSbbJourneySummaryAttributes { * @slot content - Use this slot to add `sbb-button`s or other interactive elements. */ @customElement('sbb-journey-summary') -export class SbbJourneySummaryElement extends LitElement { +export class SbbJourneySummaryElement extends SbbNowMixin(LitElement) { public static override styles: CSSResultGroup = style; /** The trip prop */ @@ -62,9 +63,6 @@ export class SbbJourneySummaryElement extends LitElement { */ @property({ attribute: 'disable-animation', type: Boolean }) public disableAnimation?: boolean; - /** A specific date for the current datetime (timestamp in milliseconds). */ - @property({ attribute: 'now' }) public dataNow?: number; - private _hasContentSlot: boolean = false; private _language = new SbbLanguageController(this); @@ -73,10 +71,6 @@ export class SbbJourneySummaryElement extends LitElement { this._hasContentSlot = Boolean(this.querySelector?.('[slot="content"]')); } - private _now(): number { - return this.dataNow ?? Date.now(); - } - /** renders the date of the journey or if it is the current or next day */ private _renderJourneyStart( departureTime: Date | undefined, @@ -137,7 +131,7 @@ export class SbbJourneySummaryElement extends LitElement { .arrivalWalk=${arrivalWalk} .legs=${legs} .disableAnimation=${this.disableAnimation} - now=${this._now()} + now=${this.dateNow} > `; diff --git a/src/components/journey-summary/readme.md b/src/components/journey-summary/readme.md index 81e88f4bd4..aa045b4432 100644 --- a/src/components/journey-summary/readme.md +++ b/src/components/journey-summary/readme.md @@ -16,7 +16,7 @@ If the tripBack prop is passed to the component a second journey-summary, withou ``` -To specify a specific date for the current datetime, you can use the `now` property (timestamp in milliseconds). +To simulate the current datetime, you can use the `now` property (timestamp in milliseconds). This is helpful if you need a specific state of the component. @@ -25,9 +25,9 @@ This is helpful if you need a specific state of the component. | Name | Attribute | Privacy | Type | Default | Description | | ------------------ | ------------------- | ------- | --------------------------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------- | -| `dataNow` | `now` | public | `number \| undefined` | | A specific date for the current datetime (timestamp in milliseconds). | | `disableAnimation` | `disable-animation` | public | `boolean \| undefined` | | Per default, the current location has a pulsating animation. You can disable the animation with this property. | | `headerLevel` | `header-level` | public | `SbbTitleLevel` | `'3'` | Heading level of the journey header element (e.g. h1-h6). | +| `now` | `now` | public | `number \| undefined` | | A specific date for the current datetime (timestamp in milliseconds). | | `roundTrip` | `round-trip` | public | `boolean \| undefined` | | The RoundTrip prop. This prop controls if one or two arrows are displayed in the header. | | `trip` | `trip` | public | `InterfaceSbbJourneySummaryAttributes` | | The trip prop | | `tripBack` | `trip-back` | public | `InterfaceSbbJourneySummaryAttributes \| undefined` | | The tripBack prop | diff --git a/src/components/pearl-chain-time/pearl-chain-time.ts b/src/components/pearl-chain-time/pearl-chain-time.ts index 99676e6871..190eab01da 100644 --- a/src/components/pearl-chain-time/pearl-chain-time.ts +++ b/src/components/pearl-chain-time/pearl-chain-time.ts @@ -6,6 +6,7 @@ import { customElement, property } from 'lit/decorators.js'; import { SbbLanguageController } from '../core/controllers.js'; import { removeTimezoneFromISOTimeString } from '../core/datetime.js'; import { i18nArrival, i18nDeparture, i18nTransferProcedures } from '../core/i18n.js'; +import { SbbNowMixin } from '../core/mixins.js'; import type { Leg, PtRideLeg } from '../core/timetable.js'; import { getDepartureArrivalTimeAttribute, isRideLeg } from '../core/timetable.js'; @@ -17,7 +18,7 @@ import '../pearl-chain.js'; * Combined with `sbb-pearl-chain`, it displays walk time information. */ @customElement('sbb-pearl-chain-time') -export class SbbPearlChainTimeElement extends LitElement { +export class SbbPearlChainTimeElement extends SbbNowMixin(LitElement) { public static override styles: CSSResultGroup = style; /** @@ -48,15 +49,8 @@ export class SbbPearlChainTimeElement extends LitElement { */ @property({ attribute: 'disable-animation', type: Boolean }) public disableAnimation?: boolean; - /** A specific date for the current datetime (timestamp in milliseconds). */ - @property({ attribute: 'now' }) public dataNow?: number; - private _language = new SbbLanguageController(this); - private _now(): number { - return this.dataNow ?? Date.now(); - } - protected override render(): TemplateResult { const departure: Date | undefined = this.departureTime ? removeTimezoneFromISOTimeString(this.departureTime) @@ -94,7 +88,7 @@ export class SbbPearlChainTimeElement extends LitElement { class="sbb-pearl-chain__time-chain" .legs=${this.legs} .disableAnimation=${this.disableAnimation} - now=${this._now()} + now=${this.dateNow} > ${arrival ? html`