diff --git a/src/elements-experimental/pearl-chain-legacy/pearl-chain-legacy.stories.ts b/src/elements-experimental/pearl-chain-legacy/pearl-chain-legacy.stories.ts index 7489665e77d..862086fc76b 100644 --- a/src/elements-experimental/pearl-chain-legacy/pearl-chain-legacy.stories.ts +++ b/src/elements-experimental/pearl-chain-legacy/pearl-chain-legacy.stories.ts @@ -160,7 +160,7 @@ const meta: Meta = { extractComponentDescription: () => readme, }, }, - title: 'experimental/sbb-pearl-chain', + title: 'experimental/sbb-pearl-chain-legacy', }; export default meta; diff --git a/src/elements-experimental/pearl-chain-legacy/pearl-chain-legacy.ts b/src/elements-experimental/pearl-chain-legacy/pearl-chain-legacy.ts index 3292920304b..21c4d2053f7 100644 --- a/src/elements-experimental/pearl-chain-legacy/pearl-chain-legacy.ts +++ b/src/elements-experimental/pearl-chain-legacy/pearl-chain-legacy.ts @@ -28,7 +28,7 @@ class SbbPearlChainLegacyElement extends LitElement { public static override styles: CSSResultGroup = style; /** - * Define the legs of the pearl-chain. + * Define the legs of the pearl-chain-legacy. * Format: * `{"legs": [{"duration": 25}, ...]}` * `duration` in minutes. Duration of the leg is relative diff --git a/src/elements-experimental/pearl-chain-legacy/readme.md b/src/elements-experimental/pearl-chain-legacy/readme.md index 24815662088..ca67533c20f 100644 --- a/src/elements-experimental/pearl-chain-legacy/readme.md +++ b/src/elements-experimental/pearl-chain-legacy/readme.md @@ -1,4 +1,4 @@ -The `sbb-pearl-chain` component displays all parts of a journey, including changes of trains or other kinds of transports. +The `sbb-pearl-chain-legacy` component displays all parts of a journey, including changes of trains or other kinds of transports. Also, it is possible to render the current position. The `legs` property is mandatory. @@ -54,8 +54,8 @@ This is helpful if you need a specific state of the component. ## Properties -| Name | Attribute | Privacy | Type | Default | Description | -| ------------------ | ------------------- | ------- | ---------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `disableAnimation` | `disable-animation` | public | `boolean` | `false` | Per default, the current location has a pulsating animation. You can disable the animation with this property. | -| `legs` | `legs` | public | `(Leg \| PtRideLeg)[]` | `[]` | Define the legs of the pearl-chain. Format: `{"legs": \[{"duration": 25}, ...]}` `duration` in minutes. Duration of the leg is relative to the total travel time. Example: departure 16:30, change at 16:40, arrival at 17:00. So the change should have a duration of 33.33%. | -| `now` | `now` | public | `Date \| null` | `null` | A configured date which acts as the current date instead of the real current date. Recommended for testing purposes. | +| Name | Attribute | Privacy | Type | Default | Description | +| ------------------ | ------------------- | ------- | ---------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `disableAnimation` | `disable-animation` | public | `boolean` | `false` | Per default, the current location has a pulsating animation. You can disable the animation with this property. | +| `legs` | `legs` | public | `(Leg \| PtRideLeg)[]` | `[]` | Define the legs of the pearl-chain-legacy. Format: `{"legs": \[{"duration": 25}, ...]}` `duration` in minutes. Duration of the leg is relative to the total travel time. Example: departure 16:30, change at 16:40, arrival at 17:00. So the change should have a duration of 33.33%. | +| `now` | `now` | public | `Date \| null` | `null` | A configured date which acts as the current date instead of the real current date. Recommended for testing purposes. | diff --git a/src/elements-experimental/pearl-chain-time/pearl-chain-time.ts b/src/elements-experimental/pearl-chain-time/pearl-chain-time.ts index da5edb41dbc..35efa0edc9e 100644 --- a/src/elements-experimental/pearl-chain-time/pearl-chain-time.ts +++ b/src/elements-experimental/pearl-chain-time/pearl-chain-time.ts @@ -21,7 +21,7 @@ import style from './pearl-chain-time.scss?lit&inline'; import '../pearl-chain-legacy.js'; /** - * Combined with `sbb-pearl-chain`, it displays walk time information. + * Combined with `sbb-pearl-chain-legacy`, it displays walk time information. */ export @customElement('sbb-pearl-chain-time') diff --git a/src/elements-experimental/pearl-chain-time/readme.md b/src/elements-experimental/pearl-chain-time/readme.md index fb688e25bd2..b6bb77a40e1 100644 --- a/src/elements-experimental/pearl-chain-time/readme.md +++ b/src/elements-experimental/pearl-chain-time/readme.md @@ -1,5 +1,5 @@ The `sbb-pearl-chain-time` component adds an optional walk icon and a duration in minutes -before and/or after the [sbb-pearl-chain](/docs/experimental-sbb-pearl-chain--docs). +before and/or after the [sbb-pearl-chain-legacy](/docs/experimental-sbb-pearl-chain-legacy--docs). The walk time indicates that the user has to walk to get to the destination, or to the station to begin the journey. diff --git a/src/elements/core/datetime.ts b/src/elements/core/datetime.ts index d9d7ebc4baf..19edfbc4d7b 100644 --- a/src/elements/core/datetime.ts +++ b/src/elements/core/datetime.ts @@ -1,2 +1,3 @@ export * from './datetime/date-adapter.js'; export * from './datetime/native-date-adapter.js'; +export * from './datetime/time-adapter.js'; diff --git a/src/elements/core/datetime/time-adapter.spec.ts b/src/elements/core/datetime/time-adapter.spec.ts new file mode 100644 index 00000000000..86de6a66988 --- /dev/null +++ b/src/elements/core/datetime/time-adapter.spec.ts @@ -0,0 +1,104 @@ +import { expect } from '@open-wc/testing'; + +import { TimeAdapter } from './time-adapter.js'; + +describe('TimeAdapter', () => { + let timeAdapter: TimeAdapter; + + beforeEach(() => { + timeAdapter = new TimeAdapter(); + }); + + it('addMilliseconds should return the right value', () => { + let date = new Date(2023, 4, 1, 20, 5, 20, 200); + + expect(date.toISOString()).to.be.equal('2023-05-01T18:05:20.200Z'); + + date = timeAdapter.addMilliseconds(date, 200); + expect(date.toISOString()).to.be.equal('2023-05-01T18:05:20.400Z'); + + date = timeAdapter.addMilliseconds(date, -300); + expect(date.toISOString()).to.be.equal('2023-05-01T18:05:20.100Z'); + }); + + it('addMinutes should return the right value', () => { + let date = new Date(2023, 4, 1, 20, 5); + + expect(date.toISOString()).to.be.equal('2023-05-01T18:05:00.000Z'); + + date = timeAdapter.addMinutes(date, 20); + expect(date.toISOString()).to.be.equal('2023-05-01T18:25:00.000Z'); + + date = timeAdapter.addMinutes(date, 150); + expect(date.toISOString()).to.be.equal('2023-05-01T20:55:00.000Z'); + }); + + it('differenceInMilliseconds should return the right value', () => { + let firstDate = new Date(2023, 4, 1, 18, 5); + let secondDate = new Date(2023, 4, 1, 20, 5); + + expect(timeAdapter.differenceInMilliseconds(firstDate, secondDate)).to.be.equal(-7200000); + + firstDate = new Date(2023, 4, 3, 16, 5); + secondDate = new Date(2023, 4, 3, 8, 5); + + expect(timeAdapter.differenceInMilliseconds(firstDate, secondDate)).to.be.equal(28800000); + }); + + it('differenceInMinutes should return the right value', () => { + let firstDate = new Date(2023, 4, 1, 18, 5); + let secondDate = new Date(2023, 4, 1, 20, 5); + + expect(timeAdapter.differenceInMinutes(firstDate, secondDate)).to.be.equal(-120); + + firstDate = new Date(2023, 4, 3, 16, 55); + secondDate = new Date(2023, 4, 3, 16, 5); + + expect(timeAdapter.differenceInMinutes(firstDate, secondDate)).to.be.equal(50); + }); + + it('isBefore should return the right value', () => { + let firstDate = new Date(2023, 4, 1, 18, 5); + let secondDate = new Date(2023, 4, 1, 20, 5); + + expect(timeAdapter.isBefore(firstDate, secondDate)).to.be.equal(true); + + firstDate = new Date(2023, 4, 3, 16, 55); + secondDate = new Date(2023, 4, 3, 16, 5); + + expect(timeAdapter.isBefore(firstDate, secondDate)).to.be.equal(false); + }); + + it('isAfter should return the right value', () => { + let firstDate = new Date(2023, 4, 1, 18, 5); + let secondDate = new Date(2023, 4, 1, 20, 5); + + expect(timeAdapter.isAfter(firstDate, secondDate)).to.be.equal(false); + + firstDate = new Date(2023, 4, 3, 16, 55); + secondDate = new Date(2023, 4, 3, 16, 5); + + expect(timeAdapter.isAfter(firstDate, secondDate)).to.be.equal(true); + }); + + it('isValid should return the right value', () => { + expect(timeAdapter.isValid(new Date(2023, 4, 1, 18, 5))).to.be.equal(true); + expect(timeAdapter.isValid(new Date(NaN))).to.be.equal(false); + }); + + it('deserialize should return the right value', () => { + expect(timeAdapter.deserialize(new Date(2023, 4, 1, 18, 5)).toISOString()).to.be.equal( + '2023-05-01T16:05:00.000Z', + ); + expect(timeAdapter.deserialize('2022-08-18T04:00').toISOString()).to.be.equal( + '2022-08-18T02:00:00.000Z', + ); + expect(timeAdapter.deserialize('1661788000').toISOString()).to.be.equal( + '2022-08-29T15:46:40.000Z', + ); + expect(timeAdapter.deserialize(1660628000).toISOString()).to.be.equal( + '2022-08-16T05:33:20.000Z', + ); + expect(timeAdapter.isValid(timeAdapter.deserialize('Invalid input'))).to.be.equal(false); + }); +}); diff --git a/src/elements/core/datetime/time-adapter.ts b/src/elements/core/datetime/time-adapter.ts new file mode 100644 index 00000000000..d5978d1a8d5 --- /dev/null +++ b/src/elements/core/datetime/time-adapter.ts @@ -0,0 +1,56 @@ +export class TimeAdapter { + public constructor() {} + + public addMilliseconds(date: Date, amount: number): Date { + const timestamp: number = date.getTime(); + return new Date(timestamp + amount); + } + + public addMinutes(date: Date, amount: number): Date { + return this.addMilliseconds(date, amount * 60000); + } + + public differenceInMilliseconds(firstDate: Date, secondDate: Date): number { + return firstDate.getTime() - secondDate.getTime(); + } + + public differenceInMinutes(firstDate: Date, secondDate: Date): number { + return this.differenceInMilliseconds(firstDate, secondDate) / 60000; + } + + public isBefore(firstDate: Date, secondDate: Date): boolean { + return this.differenceInMilliseconds(firstDate, secondDate) < 0; + } + + public isAfter(firstDate: Date, secondDate: Date): boolean { + return this.differenceInMilliseconds(firstDate, secondDate) > 0; + } + + /** Checks whether the given `date` is a valid Date. */ + public isValid(date: Date | null | undefined): boolean { + return !!date && !isNaN(date.valueOf()); + } + + /** Creates a Date from a valid input (Date, string or number in seconds). */ + public deserialize(date: Date | string | number | null | undefined): Date { + if (typeof date === 'object' && date instanceof Date) { + return date; + } else if (typeof date === 'string') { + if (!date) { + return new Date(NaN); + } else if (!Number.isNaN(+date)) { + return new Date(+date * 1000); + } else { + return new Date(date.includes('T') ? date : date + 'T00:00:00'); + } + } else if (typeof date === 'number') { + return new Date(date * 1000); + } + + return new Date(NaN); + } + + public invalid(): Date { + return new Date(NaN); + } +} diff --git a/src/elements/core/styles/mixins/pearl-chain-bullet.scss b/src/elements/core/styles/mixins/pearl-chain-bullet.scss index d0609901a79..c9db1beb1c2 100644 --- a/src/elements/core/styles/mixins/pearl-chain-bullet.scss +++ b/src/elements/core/styles/mixins/pearl-chain-bullet.scss @@ -10,7 +10,7 @@ --sbb-pearl-chain-bullet-size-stop: #{functions.px-to-rem-build(8)}; --sbb-pearl-chain-bullet-color: var(--sbb-color-charcoal); --sbb-pearl-chain-bullet-color-past: var(--sbb-color-metal); - --sbb-pearl-chain-bullet-color-irrelevant: var(--sbb-color-metal); + --sbb-pearl-chain-bullet-color-irrelzevant: var(--sbb-color-metal); --sbb-pearl-chain-bullet-color-disruption: var(--sbb-color-red); --sbb-pearl-chain-bullet-border-width: var(--sbb-border-width-2x); --sbb-pearl-chain-bullet-animation-duration: 1920ms; @@ -19,6 +19,7 @@ --sbb-pearl-chain-bullet-crossed-width: #{functions.px-to-rem-build(14.14)}; --sbb-pearl-chain-bullet-crossed-height: #{functions.px-to-rem-build(3.5)}; --sbb-pearl-chain-bullet-crossed-border-width: #{functions.px-to-rem-build(1.5)}; + --sbb-pearl-chain-bullet-background-color: Canvas; } @mixin pearl-chain-bullet { @@ -44,8 +45,8 @@ min-width: var(--sbb-pearl-chain-bullet-size-stop); height: var(--sbb-pearl-chain-bullet-size-stop); width: var(--sbb-pearl-chain-bullet-size-stop); - border: var(--sbb-pearl-chain-bullet-border-width) solid currentcolor; - background: Canvas; + border: var(--sbb-pearl-chain-bullet-border-width) solid var(--sbb-pearl-chain-bullet-color); + background: var(--sbb-pearl-chain-bullet-background-color); } @mixin pearl-chain-bullet-past { @@ -68,8 +69,8 @@ } @mixin pearl-chain-bullet-skipped { - border: var(--sbb-pearl-chain-bullet-border-width) solid currentcolor; - background: Canvas; + border: var(--sbb-pearl-chain-bullet-border-width) solid var(--sbb-pearl-chain-bullet-color); + background: var(--sbb-pearl-chain-bullet-background-color); &::before { content: ''; @@ -77,8 +78,9 @@ inset-block-start: 50%; inset-inline-start: 50%; transform: translate(-50%, -50%) rotate(45deg); - border-block-start: var(--sbb-pearl-chain-bullet-crossed-border-width) solid canvas; - background: var(--sbb-pearl-chain-bullet-color-disruption); + border-block-end: var(--sbb-pearl-chain-bullet-crossed-border-width) solid + var(--sbb-pearl-chain-bullet-color-disruption); + background: var(--sbb-pearl-chain-bullet-background-color); height: var(--sbb-pearl-chain-bullet-crossed-height); width: var(--sbb-pearl-chain-bullet-crossed-width); @@ -88,6 +90,21 @@ } } +@mixin pearl-chain-bullet-skipped-transparent { + mask: linear-gradient( + 45deg, + var(--sbb-color-black) 0%, + var(--sbb-color-black) 48%, + transparent 48%, + transparent 60%, + var(--sbb-color-black) 60%, + var(--sbb-color-black) 100% + ); + mask-clip: no-clip; + + @include pearl-chain-bullet-skipped; +} + @mixin pearl-chain-bullet-skipped-stop { --sbb-pearl-chain-bullet-crossed-width: #{functions.px-to-rem-build(11.31)}; --sbb-pearl-chain-bullet-crossed-height: #{functions.px-to-rem-build(3)}; diff --git a/src/elements/pearl-chain/pearl-chain-leg/pearl-chain-leg.scss b/src/elements/pearl-chain/pearl-chain-leg/pearl-chain-leg.scss index e86dd1b0249..73ea7f40f35 100644 --- a/src/elements/pearl-chain/pearl-chain-leg/pearl-chain-leg.scss +++ b/src/elements/pearl-chain/pearl-chain-leg/pearl-chain-leg.scss @@ -1,8 +1,14 @@ @use '../../core/styles' as sbb; @mixin sbb-pearl-chain-leg-dotted { + width: 100%; background-color: unset; - background-image: linear-gradient(to right, currentcolor 0%, currentcolor 50%, Canvas 50%); + background-image: linear-gradient( + to right, + var(--sbb-pearl-chain-leg-color-disruption) 0%, + var(--sbb-pearl-chain-leg-color-disruption) 50%, + transparent 50% + ); background-repeat: repeat-x; background-size: calc(2 * var(--sbb-pearl-chain-leg-spacing-small)) var(--sbb-pearl-chain-leg-height); @@ -21,60 +27,44 @@ :host { --sbb-pearl-chain-leg-height: var(--sbb-border-width-2x); --sbb-pearl-chain-leg-spacing-small: #{sbb.px-to-rem-build(2)}; + --sbb-pearl-chain-leg-color: var(--sbb-pearl-chain-bullet-color); --sbb-pearl-chain-leg-color-disruption: var(--sbb-pearl-chain-bullet-color-disruption); --sbb-pearl-chain-leg-color-past: var(--sbb-pearl-chain-bullet-color-past); - --sbb-pearl-chain-leg-width: 100%; + --sbb-pearl-chain-leg-offset: var( + --sbb-pearl-chain-bullet-size-stop, + var(--sbb-pearl-chain-last-leg-margin) + ); display: contents; @include sbb.pearl-chain-bullet-variables; + + --sbb-pearl-chain-bullet-background-color: transparent; } .sbb-pearl-chain__leg { flex: var(--sbb-pearl-chain-leg-weight, 1) var(--sbb-pearl-chain-leg-weight, 1); position: relative; height: var(--sbb-pearl-chain-leg-height); - border-inline-end: var( - --sbb-pearl-chain-last-leg-margin, - var(--sbb-pearl-chain-leg-spacing-small) - ) - solid Canvas; - background-color: currentcolor; - width: var(--sbb-pearl-chain-leg-width); + margin-inline-end: var( + --sbb-pearl-chain-last-leg-margin, + var(--sbb-pearl-chain-leg-spacing-small) + ); + background-color: var(--sbb-pearl-chain-leg-color); + width: 100%; display: flex; align-items: center; - @include sbb.if-forced-colors { - background-color: CanvasText; - - :host([past]) & { - background-color: GrayText; - } - } - &::after { inset-inline-end: var(--sbb-pearl-chain-last-leg-inset-inline-end, 0); } :host([past]) & { - color: var(--sbb-pearl-chain-leg-color-past); - - @include sbb.pearl-chain-bullet-past; - - @include sbb.if-forced-colors { - background-color: GrayText !important; - } + --sbb-pearl-chain-bullet-color: var(--sbb-pearl-chain-leg-color-past); } :host([disruption]) & { - color: var(--sbb-pearl-chain-leg-color-disruption); - - @include sbb.pearl-chain-bullet-disruption; - - @include sbb.if-forced-colors { - color: Highlight; - background: Highlight; - } + --sbb-pearl-chain-leg-color: transparent; &::after { @include sbb-pearl-chain-leg-dotted; @@ -82,7 +72,7 @@ } :host(:is([arrival-skipped], [departure-skipped])) & { - color: var(--sbb-pearl-chain-leg-color-disruption); + --sbb-pearl-chain-leg-color: transparent; &::after { @include sbb-pearl-chain-leg-dotted; @@ -90,8 +80,10 @@ } :host([data-progress]:not([arrival-skipped], [departure-skipped], [disruption])) & { + // --sbb-pearl-chain-status-position: defined in .ts file --sbb-pearl-chain-status-position-normalized: calc( - (100% - var(--sbb-pearl-chain-bullet-size-start-end)) * var(--sbb-pearl-chain-status-position) + (100% - (var(--sbb-pearl-chain-bullet-size-start-end) - var(--sbb-pearl-chain-leg-offset))) * + var(--sbb-pearl-chain-status-position) ); &::before { @@ -105,9 +97,8 @@ position: absolute; inset-block-start: -200%; z-index: 4; - - // --sbb-pearl-chain-status-position: defined in .ts file inset-inline-start: var(--sbb-pearl-chain-status-position-normalized); + translate: calc(-1 * var(--sbb-pearl-chain-leg-offset)); } } @@ -117,7 +108,7 @@ } & { - background-color: var(--sbb-pearl-chain-leg-color-past); + --sbb-pearl-chain-bullet-color: var(--sbb-pearl-chain-leg-color-past); // --sbb-pearl-chain-leg-status: defined in .ts file width: var(--sbb-pearl-chain-status-position-normalized); @@ -132,8 +123,7 @@ position: absolute; inset-block: 0; inset-inline-start: 0; - background-color: currentcolor; - border-radius: var(--sbb-pearl-chain-leg-height); + background-color: var(--sbb-pearl-chain-bullet-color); z-index: 1; @include sbb.if-forced-colors { @@ -149,6 +139,7 @@ display: var(--sbb-pearl-chain-leg-stop-display, unset); position: relative; z-index: 2; + left: calc(-1 * var(--sbb-pearl-chain-leg-offset)); @include sbb.pearl-chain-bullet; @@ -161,7 +152,7 @@ } :host(:is([departure-skipped], [data-skip-departure])) & { - @include sbb.pearl-chain-bullet-skipped; + @include sbb.pearl-chain-bullet-skipped-transparent; } :host([disruption]) & { diff --git a/src/elements/pearl-chain/pearl-chain-leg/pearl-chain-leg.stories.ts b/src/elements/pearl-chain/pearl-chain-leg/pearl-chain-leg.stories.ts index f28570b5ffa..50c0d206815 100644 --- a/src/elements/pearl-chain/pearl-chain-leg/pearl-chain-leg.stories.ts +++ b/src/elements/pearl-chain/pearl-chain-leg/pearl-chain-leg.stories.ts @@ -114,7 +114,7 @@ const meta: Meta = { extractComponentDescription: () => readme, }, }, - title: 'elements/pearl-chain/sbb-pearl-chain-leg', + title: 'elements/sbb-pearl-chain/sbb-pearl-chain-leg', }; export default meta; diff --git a/src/elements/pearl-chain/pearl-chain-leg/pearl-chain-leg.ts b/src/elements/pearl-chain/pearl-chain-leg/pearl-chain-leg.ts index b848760aba4..0faf4b72d62 100644 --- a/src/elements/pearl-chain/pearl-chain-leg/pearl-chain-leg.ts +++ b/src/elements/pearl-chain/pearl-chain-leg/pearl-chain-leg.ts @@ -2,37 +2,48 @@ import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit'; import { html, LitElement } from 'lit'; import { customElement, property } from 'lit/decorators.js'; +import { TimeAdapter } from '../../core/datetime.js'; import { forceType } from '../../core/decorators.js'; +import { EventEmitter } from '../../core/eventing/event-emitter.js'; import style from './pearl-chain-leg.scss?lit&inline'; /** * It displays a journey leg inside a `sbb-pearl-chain`. + * + * @event {CustomEvent} leg-updated - Update event emitter */ export @customElement('sbb-pearl-chain-leg') class SbbPearlChainLegElement extends LitElement { public static override styles: CSSResultGroup = style; + public static readonly events = { + legUpdated: 'leg-updated', + } as const; /** Departure time of the leg. */ - @property() - public set departure(value: Date | null) { - this._departure = value; + @property({ type: Date }) + public set departure(value: Date | string | number | null) { + this._departure = this._timeAdapter.deserialize(value); } - public get departure(): Date | null { - return this._departure; + + public get departure(): Date { + return this._departure ?? this._timeAdapter.invalid(); } - private _departure: Date | null = null; + + private _departure?: Date; /** Arrival time of the leg. */ - @property() - public set arrival(value: Date | null) { - this._arrival = value; + @property({ type: Date }) + public set arrival(value: Date | string | number | null) { + this._arrival = this._timeAdapter.deserialize(value); } - public get arrival(): Date | null { - return this._arrival; + + public get arrival(): Date { + return this._arrival ?? this._timeAdapter.invalid(); } - private _arrival: Date | null = null; + + private _arrival?: Date; /** Whether the leg is disrupted. */ @forceType() @@ -64,16 +75,28 @@ class SbbPearlChainLegElement extends LitElement { @property({ type: Number, attribute: 'arrival-delay' }) public accessor arrivalDelay: number = 0; + /** Input event emitter */ + private _legUpdated: EventEmitter = new EventEmitter( + this, + SbbPearlChainLegElement.events.legUpdated, + { + bubbles: true, + composed: false, + }, + ); + + private _timeAdapter: TimeAdapter = new TimeAdapter(); + protected override willUpdate(changedProperties: PropertyValues): void { super.willUpdate(changedProperties); - // We need to update parent pearl-chain so that following leg can be styled properly. - if (changedProperties.has('arrivalSkipped')) { - const parentPearlChain = this.closest?.('sbb-pearl-chain'); - if (!parentPearlChain) { - return; - } - parentPearlChain?.requestUpdate(); + if ( + this.hasUpdated && + (changedProperties.has('arrivalSkipped') || + changedProperties.has('arrival') || + changedProperties.has('departure')) + ) { + this._legUpdated.emit(); } } diff --git a/src/elements/pearl-chain/pearl-chain-leg/pearl-chain-leg.visual.spec.ts b/src/elements/pearl-chain/pearl-chain-leg/pearl-chain-leg.visual.spec.ts index 3ea77a89006..6ff0c2c6b44 100644 --- a/src/elements/pearl-chain/pearl-chain-leg/pearl-chain-leg.visual.spec.ts +++ b/src/elements/pearl-chain/pearl-chain-leg/pearl-chain-leg.visual.spec.ts @@ -5,70 +5,32 @@ import { describeViewports, visualDiffDefault } from '../../core/testing/private import './pearl-chain-leg.js'; describe('sbb-pearl-chain-leg', () => { - describeViewports({ viewports: ['medium'] }, () => { - it( - visualDiffDefault.name, - visualDiffDefault.with(async (setup) => { - await setup.withFixture( - html``, - ); - }), - ); - - it( - `past=true`, - visualDiffDefault.with(async (setup) => { - await setup.withFixture( - html``, - ); - }), - ); - - it( - `disruption=true`, - visualDiffDefault.with(async (setup) => { - await setup.withFixture( - html``, - ); - }), - ); + const cases = [ + {}, + { past: true }, + { disruption: true }, + { departureSkipped: true }, + { progress: true }, + ]; - it( - `departureSkipped=true`, - visualDiffDefault.with(async (setup) => { - await setup.withFixture( - html``, - ); - }), - ); - - it( - `progress`, - visualDiffDefault.with(async (setup) => { - await setup.withFixture( - html``, - ); - }), - ); + describeViewports({ viewports: ['medium'] }, () => { + for (const props of cases) { + it( + `past=${!!props?.past}-disruption=${!!props?.disruption}-departureSkipped=${!!props?.departureSkipped}-data-progress=${!!props?.progress}`, + visualDiffDefault.with(async (setup) => { + await setup.withFixture(html` + + `); + }), + ); + } }); }); diff --git a/src/elements/pearl-chain/pearl-chain-leg/readme.md b/src/elements/pearl-chain/pearl-chain-leg/readme.md index 921c9701349..97c6eadaf43 100644 --- a/src/elements/pearl-chain/pearl-chain-leg/readme.md +++ b/src/elements/pearl-chain/pearl-chain-leg/readme.md @@ -13,13 +13,19 @@ The `past`, `arrival-skipped`, `departure-skipped`, and `disruption` properties ## Properties -| Name | Attribute | Privacy | Type | Default | Description | -| ------------------ | ------------------- | ------- | --------------------- | ------- | -------------------------------------------- | -| `arrival` | `arrival` | public | `SbbDateLike \| null` | `null` | Arrival time of the leg. | -| `arrivalDelay` | `arrival-delay` | public | `number` | `0` | The number of minutes of delay on arrival. | -| `arrivalSkipped` | `arrival-skipped` | public | `boolean` | `false` | Whether the leg's arrival is skipped. | -| `departure` | `departure` | public | `SbbDateLike \| null` | `null` | Departure time of the leg. | -| `departureDelay` | `departure-delay` | public | `number` | `0` | The number of minutes of delay on departure. | -| `departureSkipped` | `departure-skipped` | public | `boolean` | `false` | Whether the leg's departure is skipped. | -| `disruption` | `disruption` | public | `boolean` | `false` | Whether the leg is disrupted. | -| `past` | `past` | public | `boolean` | `false` | Whether current time is past arrival time. | +| Name | Attribute | Privacy | Type | Default | Description | +| ------------------ | ------------------- | ------- | --------- | ------- | -------------------------------------------- | +| `arrival` | `arrival` | public | `Date` | | Arrival time of the leg. | +| `arrivalDelay` | `arrival-delay` | public | `number` | `0` | The number of minutes of delay on arrival. | +| `arrivalSkipped` | `arrival-skipped` | public | `boolean` | `false` | Whether the leg's arrival is skipped. | +| `departure` | `departure` | public | `Date` | | Departure time of the leg. | +| `departureDelay` | `departure-delay` | public | `number` | `0` | The number of minutes of delay on departure. | +| `departureSkipped` | `departure-skipped` | public | `boolean` | `false` | Whether the leg's departure is skipped. | +| `disruption` | `disruption` | public | `boolean` | `false` | Whether the leg is disrupted. | +| `past` | `past` | public | `boolean` | `false` | Whether current time is past arrival time. | + +## Events + +| Name | Type | Description | Inherited From | +| ------------- | ------------------- | -------------------- | -------------- | +| `leg-updated` | `CustomEvent` | Update event emitter | | diff --git a/src/elements/pearl-chain/pearl-chain/__snapshots__/pearl-chain.snapshot.spec.snap.js b/src/elements/pearl-chain/pearl-chain/__snapshots__/pearl-chain.snapshot.spec.snap.js index e004a87f2a5..a49b6ea20c3 100644 --- a/src/elements/pearl-chain/pearl-chain/__snapshots__/pearl-chain.snapshot.spec.snap.js +++ b/src/elements/pearl-chain/pearl-chain/__snapshots__/pearl-chain.snapshot.spec.snap.js @@ -5,10 +5,9 @@ snapshots["sbb-pearl-chain renders with one leg DOM"] = ` @@ -18,11 +17,17 @@ snapshots["sbb-pearl-chain renders with one leg DOM"] = snapshots["sbb-pearl-chain renders with one leg Shadow DOM"] = `
- + - +
@@ -33,16 +38,16 @@ snapshots["sbb-pearl-chain renders with two legs DOM"] = ` @@ -52,11 +57,17 @@ snapshots["sbb-pearl-chain renders with two legs DOM"] = snapshots["sbb-pearl-chain renders with two legs Shadow DOM"] = `
- + - +
@@ -67,17 +78,17 @@ snapshots["sbb-pearl-chain renders with departure stop skipped DOM"] = ` @@ -87,11 +98,17 @@ snapshots["sbb-pearl-chain renders with departure stop skipped DOM"] = snapshots["sbb-pearl-chain renders with departure stop skipped Shadow DOM"] = `
- + - +
@@ -102,17 +119,17 @@ snapshots["sbb-pearl-chain renders with arrival stop skipped DOM"] = ` @@ -122,11 +139,18 @@ snapshots["sbb-pearl-chain renders with arrival stop skipped DOM"] = snapshots["sbb-pearl-chain renders with arrival stop skipped Shadow DOM"] = `
- + - +
@@ -137,15 +161,13 @@ snapshots["sbb-pearl-chain renders with progress leg DOM"] = `
- + @@ -173,16 +198,14 @@ snapshots["sbb-pearl-chain renders with cancelled instead of progress leg DOM"] ` @@ -194,7 +217,11 @@ snapshots["sbb-pearl-chain renders with cancelled instead of progress leg DOM"] snapshots["sbb-pearl-chain renders with cancelled instead of progress leg Shadow DOM"] = `
- + @@ -205,3 +232,123 @@ snapshots["sbb-pearl-chain renders with cancelled instead of progress leg Shadow `; /* end snapshot sbb-pearl-chain renders with cancelled instead of progress leg Shadow DOM */ +snapshots["sbb-pearl-chain renders with one leg A11y tree Chrome"] = +`

+ { + "role": "WebArea", + "name": "" +} +

+`; +/* end snapshot sbb-pearl-chain renders with one leg A11y tree Chrome */ + +snapshots["sbb-pearl-chain renders with two legs A11y tree Chrome"] = +`

+ { + "role": "WebArea", + "name": "" +} +

+`; +/* end snapshot sbb-pearl-chain renders with two legs A11y tree Chrome */ + +snapshots["sbb-pearl-chain renders with departure stop skipped A11y tree Chrome"] = +`

+ { + "role": "WebArea", + "name": "" +} +

+`; +/* end snapshot sbb-pearl-chain renders with departure stop skipped A11y tree Chrome */ + +snapshots["sbb-pearl-chain renders with arrival stop skipped A11y tree Chrome"] = +`

+ { + "role": "WebArea", + "name": "" +} +

+`; +/* end snapshot sbb-pearl-chain renders with arrival stop skipped A11y tree Chrome */ + +snapshots["sbb-pearl-chain renders with progress leg A11y tree Chrome"] = +`

+ { + "role": "WebArea", + "name": "" +} +

+`; +/* end snapshot sbb-pearl-chain renders with progress leg A11y tree Chrome */ + +snapshots["sbb-pearl-chain renders with cancelled instead of progress leg A11y tree Chrome"] = +`

+ { + "role": "WebArea", + "name": "" +} +

+`; +/* end snapshot sbb-pearl-chain renders with cancelled instead of progress leg A11y tree Chrome */ + +snapshots["sbb-pearl-chain renders with one leg A11y tree Firefox"] = +`

+ { + "role": "document", + "name": "" +} +

+`; +/* end snapshot sbb-pearl-chain renders with one leg A11y tree Firefox */ + +snapshots["sbb-pearl-chain renders with two legs A11y tree Firefox"] = +`

+ { + "role": "document", + "name": "" +} +

+`; +/* end snapshot sbb-pearl-chain renders with two legs A11y tree Firefox */ + +snapshots["sbb-pearl-chain renders with departure stop skipped A11y tree Firefox"] = +`

+ { + "role": "document", + "name": "" +} +

+`; +/* end snapshot sbb-pearl-chain renders with departure stop skipped A11y tree Firefox */ + +snapshots["sbb-pearl-chain renders with arrival stop skipped A11y tree Firefox"] = +`

+ { + "role": "document", + "name": "" +} +

+`; +/* end snapshot sbb-pearl-chain renders with arrival stop skipped A11y tree Firefox */ + +snapshots["sbb-pearl-chain renders with progress leg A11y tree Firefox"] = +`

+ { + "role": "document", + "name": "" +} +

+`; +/* end snapshot sbb-pearl-chain renders with progress leg A11y tree Firefox */ + +snapshots["sbb-pearl-chain renders with cancelled instead of progress leg A11y tree Firefox"] = +`

+ { + "role": "document", + "name": "" +} +

+`; +/* end snapshot sbb-pearl-chain renders with cancelled instead of progress leg A11y tree Firefox */ + diff --git a/src/elements/pearl-chain/pearl-chain/pearl-chain.scss b/src/elements/pearl-chain/pearl-chain/pearl-chain.scss index be44aceeefb..641172493e1 100644 --- a/src/elements/pearl-chain/pearl-chain/pearl-chain.scss +++ b/src/elements/pearl-chain/pearl-chain/pearl-chain.scss @@ -14,6 +14,12 @@ display: block; @include sbb.pearl-chain-bullet-variables; + + --sbb-pearl-chain-bullet-background-color: transparent; + + @include sbb.if-forced-colors { + --sbb-pearl-chain-bullet-color: Highlight !important; + } } :host([marker='pulsing']) { @@ -43,12 +49,12 @@ display: flex; justify-content: space-between; flex-wrap: nowrap; - color: var(--sbb-pearl-chain-color); width: 100%; + gap: var(--sbb-pearl-chain-bullet-size-stop); padding-block: calc( (var(--sbb-pearl-chain-bullet-size-start-end) - var(--sbb-pearl-chain-leg-height)) / 2 ); - padding-inline-end: var(--sbb-pearl-chain-bullet-size-start-end); + padding-inline: var(--sbb-pearl-chain-bullet-size-start-end); } // First and last bullet @@ -59,7 +65,7 @@ & { content: ''; position: absolute; - background-color: currentcolor; + background-color: var(--sbb-pearl-chain-bullet-color); inset-block-start: 0; z-index: 3; } @@ -74,25 +80,13 @@ } .sbb-pearl-chain__bullet[data-past] { - color: var(--sbb-pearl-chain-leg-color-past); - @include sbb.pearl-chain-bullet-past; - - @include sbb.if-forced-colors { - background-color: GrayText !important; - } } .sbb-pearl-chain__bullet[data-disrupted] { - color: var(--sbb-pearl-chain-leg-color-disruption); - - @include sbb.if-forced-colors { - color: Highlight; - background: Highlight; - } + --sbb-pearl-chain-bullet-color: var(--sbb-pearl-chain-leg-color-disruption); } .sbb-pearl-chain__bullet[data-skipped] { - @include sbb.pearl-chain-bullet-start-end; - @include sbb.pearl-chain-bullet-skipped; + @include sbb.pearl-chain-bullet-skipped-transparent; } diff --git a/src/elements/pearl-chain/pearl-chain/pearl-chain.snapshot.spec.ts b/src/elements/pearl-chain/pearl-chain/pearl-chain.snapshot.spec.ts index 61866d2d560..b5b4f4b9dd1 100644 --- a/src/elements/pearl-chain/pearl-chain/pearl-chain.snapshot.spec.ts +++ b/src/elements/pearl-chain/pearl-chain/pearl-chain.snapshot.spec.ts @@ -1,7 +1,7 @@ import { expect } from '@open-wc/testing'; import { html } from 'lit/static-html.js'; -import { fixture } from '../../core/testing/private.js'; +import { fixture, testA11yTreeSnapshot } from '../../core/testing/private.js'; import './pearl-chain.js'; import type { SbbPearlChainElement } from './pearl-chain.js'; @@ -30,6 +30,8 @@ describe(`sbb-pearl-chain`, () => { it('Shadow DOM', async () => { await expect(element).shadowDom.to.be.equalSnapshot(); }); + + testA11yTreeSnapshot(); }); describe('renders with two legs', () => { @@ -55,6 +57,8 @@ describe(`sbb-pearl-chain`, () => { it('Shadow DOM', async () => { await expect(element).shadowDom.to.be.equalSnapshot(); }); + + testA11yTreeSnapshot(); }); describe('renders with departure stop skipped', () => { @@ -81,6 +85,8 @@ describe(`sbb-pearl-chain`, () => { it('Shadow DOM', async () => { await expect(element).shadowDom.to.be.equalSnapshot(); }); + + testA11yTreeSnapshot(); }); describe('renders with arrival stop skipped', () => { @@ -107,6 +113,8 @@ describe(`sbb-pearl-chain`, () => { it('Shadow DOM', async () => { await expect(element).shadowDom.to.be.equalSnapshot(); }); + + testA11yTreeSnapshot(); }); describe('renders with progress leg', () => { @@ -133,6 +141,8 @@ describe(`sbb-pearl-chain`, () => { it('Shadow DOM', async () => { await expect(element).shadowDom.to.be.equalSnapshot(); }); + + testA11yTreeSnapshot(); }); describe('renders with cancelled instead of progress leg', () => { @@ -159,5 +169,7 @@ describe(`sbb-pearl-chain`, () => { it('Shadow DOM', async () => { await expect(element).shadowDom.to.be.equalSnapshot(); }); + + testA11yTreeSnapshot(); }); }); diff --git a/src/elements/pearl-chain/pearl-chain/pearl-chain.spec.ts b/src/elements/pearl-chain/pearl-chain/pearl-chain.spec.ts index d91399faa11..c534546e87d 100644 --- a/src/elements/pearl-chain/pearl-chain/pearl-chain.spec.ts +++ b/src/elements/pearl-chain/pearl-chain/pearl-chain.spec.ts @@ -1,7 +1,8 @@ -import { assert } from '@open-wc/testing'; +import { assert, expect } from '@open-wc/testing'; import { html } from 'lit/static-html.js'; import { fixture } from '../../core/testing/private.js'; +import { waitForLitRender } from '../../core/testing/wait-for-render.js'; import { SbbPearlChainElement } from './pearl-chain.js'; @@ -10,20 +11,49 @@ import '../pearl-chain-leg.js'; describe(`sbb-pearl-chain`, () => { let element: SbbPearlChainElement; - it('renders', async () => { + beforeEach(async () => { element = await fixture( - html` + html` `, ); + }); + it('renders', async () => { assert.instanceOf(element, SbbPearlChainElement); }); + + it('weights legs properly', async () => { + const legs = Array.from(element.querySelectorAll('sbb-pearl-chain-leg')); + + expect(legs[0].style.getPropertyValue('--sbb-pearl-chain-leg-weight')).to.be.equal('0.5'); + expect(legs[1].style.getPropertyValue('--sbb-pearl-chain-leg-weight')).to.be.equal('0.5'); + + legs[0].departure = '2022-08-18T03:00'; + await waitForLitRender(element); + + expect(legs[0].style.getPropertyValue('--sbb-pearl-chain-leg-weight')).to.be.equal('0.75'); + expect(legs[1].style.getPropertyValue('--sbb-pearl-chain-leg-weight')).to.be.equal('0.25'); + }); + + it('places cursor correctly', async () => { + const legs = Array.from(element.querySelectorAll('sbb-pearl-chain-leg')); + + expect(legs[0]).not.to.have.attribute('data-progress'); + expect(legs[1]).not.to.have.attribute('data-progress'); + + element.now = '2022-08-18T04:15'; + await waitForLitRender(element); + + expect(legs[0]).to.have.attribute('data-progress'); + expect(legs[1]).not.to.have.attribute('data-progress'); + expect(legs[0].style.getPropertyValue('--sbb-pearl-chain-status-position')).to.be.equal('0.5'); + }); }); diff --git a/src/elements/pearl-chain/pearl-chain/pearl-chain.ssr.spec.ts b/src/elements/pearl-chain/pearl-chain/pearl-chain.ssr.spec.ts index 9aea1d9c631..35eb445b546 100644 --- a/src/elements/pearl-chain/pearl-chain/pearl-chain.ssr.spec.ts +++ b/src/elements/pearl-chain/pearl-chain/pearl-chain.ssr.spec.ts @@ -10,10 +10,18 @@ describe(`sbb-pearl-chain ssr`, () => { beforeEach(async () => { root = await ssrHydratedFixture( - html` + html` + + `, { - modules: ['./pearl-chain.js'], + modules: ['./pearl-chain.js', '../pearl-chain-leg.js'], }, ); }); diff --git a/src/elements/pearl-chain/pearl-chain/pearl-chain.stories.ts b/src/elements/pearl-chain/pearl-chain/pearl-chain.stories.ts index c7ebbe03fbb..8cc206a1314 100644 --- a/src/elements/pearl-chain/pearl-chain/pearl-chain.stories.ts +++ b/src/elements/pearl-chain/pearl-chain/pearl-chain.stories.ts @@ -44,7 +44,7 @@ const defaultArgTypes: ArgTypes = { const defaultArgs: Args = { marker: marker.options![0], - now: new Date('2024-12-05T12:11:00').valueOf(), + now: new Date('2024-01-05T12:11:00'), }; const TemplateSlotted = (legs: TemplateResult[], { now, ...args }: Args): TemplateResult => { @@ -132,7 +132,6 @@ export const ManyStops: StoryObj = { argTypes: defaultArgTypes, args: { ...defaultArgs, - now: new Date('2024-11-30T11:13:00').valueOf(), }, }; @@ -147,6 +146,9 @@ export const Cancelled: StoryObj = { export const CancelledManyStops: StoryObj = { render: TemplateManyCancelled, argTypes: defaultArgTypes, + args: { + ...defaultArgs, + }, }; export const WithPosition: StoryObj = { @@ -154,6 +156,7 @@ export const WithPosition: StoryObj = { argTypes: defaultArgTypes, args: { ...defaultArgs, + now: new Date('2024-12-05T12:11:00'), }, }; @@ -162,6 +165,7 @@ export const Past: StoryObj = { argTypes: defaultArgTypes, args: { ...defaultArgs, + now: new Date('2027-12-05T12:11:00'), }, }; @@ -170,6 +174,7 @@ export const DepartureStopSkipped: StoryObj = { argTypes: { ...defaultArgTypes, serviceAlteration }, args: { ...defaultArgs, + now: new Date('2024-12-05T12:11:00'), serviceAlteration: serviceAlteration.options![0], }, }; @@ -179,6 +184,7 @@ export const ArrivalStopSkipped: StoryObj = { argTypes: { ...defaultArgTypes, serviceAlteration }, args: { ...defaultArgs, + now: new Date('2024-12-05T12:11:00'), serviceAlteration: serviceAlteration.options![1], }, }; @@ -196,7 +202,6 @@ export const LastStopSkipped: StoryObj = { argTypes: defaultArgTypes, args: { ...defaultArgs, - now: new Date('2024-11-30T11:13:00').valueOf(), }, }; @@ -205,18 +210,19 @@ export const Mixed: StoryObj = { argTypes: { ...defaultArgTypes, serviceAlteration }, args: { ...defaultArgs, + now: new Date('2024-12-05T12:11:00'), serviceAlteration: serviceAlteration.options![2], }, }; const meta: Meta = { - decorators: [(story) => html`
${story()}
`], + decorators: [(story) => html`
${story()}
`], parameters: { docs: { extractComponentDescription: () => readme, }, }, - title: 'elements/pearl-chain/sbb-pearl-chain', + title: 'elements/sbb-pearl-chain/sbb-pearl-chain', }; export default meta; diff --git a/src/elements/pearl-chain/pearl-chain/pearl-chain.ts b/src/elements/pearl-chain/pearl-chain/pearl-chain.ts index 932acf18662..5b13c3ad4c0 100644 --- a/src/elements/pearl-chain/pearl-chain/pearl-chain.ts +++ b/src/elements/pearl-chain/pearl-chain/pearl-chain.ts @@ -1,11 +1,10 @@ -import { addMinutes, differenceInMinutes, isAfter, isBefore } from 'date-fns'; import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit'; import { html, LitElement } from 'lit'; import { customElement, property } from 'lit/decorators.js'; -import { type DateAdapter, defaultDateAdapter } from '../../core/datetime.js'; -import type { SbbDateLike } from '../../core/interfaces/types.js'; import '../pearl-chain-leg.js'; +import { TimeAdapter } from '../../core/datetime.js'; +import { SbbHydrationMixin } from '../../core/mixins/hydration-mixin.js'; import type { SbbPearlChainLegElement } from '../pearl-chain-leg.js'; import style from './pearl-chain.scss?lit&inline'; @@ -14,10 +13,12 @@ type Status = 'progress' | 'future' | 'past'; /** * It visually displays journey information. + * + * @slot - Use the unnamed slot to add `sbb-pearl-chain-leg`s to the pearl-chain. */ export @customElement('sbb-pearl-chain') -class SbbPearlChainElement extends LitElement { +class SbbPearlChainElement extends SbbHydrationMixin(LitElement) { public static override styles: CSSResultGroup = style; /** Whether the marker should be pulsing or static. */ @@ -25,10 +26,10 @@ class SbbPearlChainElement extends LitElement { public accessor marker: 'static' | 'pulsing' = 'static'; /** A configured date which acts as the current date instead of the real current date. Recommended for testing purposes. */ - @property() - public set now(value: Date) { - this._now = - this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(value)) ?? new Date(); + @property({ type: Date }) + public set now(value: Date | string | number | null) { + const valueAsDate = this._timeAdapter.deserialize(value); + this._now = this._timeAdapter.isValid(valueAsDate) ? valueAsDate : new Date(); } public get now(): Date { @@ -37,7 +38,16 @@ class SbbPearlChainElement extends LitElement { private _now: Date = new Date(); - private _dateAdapter: DateAdapter = defaultDateAdapter; + private _timeAdapter: TimeAdapter = new TimeAdapter(); + + public constructor() { + super(); + + this.addEventListener('leg-updated', (event) => { + event.stopPropagation(); + this._setUpComponent(); + }); + } private _legs(): SbbPearlChainLegElement[] { return Array.from(this.querySelectorAll?.('sbb-pearl-chain-leg') ?? []); @@ -45,85 +55,77 @@ class SbbPearlChainElement extends LitElement { private _totalDuration(legs: SbbPearlChainLegElement[]): number { return legs?.reduce((sum: number, leg) => { - const arrivalNoTz = this._dateAdapter.deserialize(leg.arrival); - const departureNoTz = this._dateAdapter.deserialize(leg.departure) as Date; + const arrivalNoTz = this._timeAdapter.deserialize(leg.arrival); + const departureNoTz = this._timeAdapter.deserialize(leg.departure); if (arrivalNoTz && departureNoTz) { - return sum + differenceInMinutes(arrivalNoTz, departureNoTz); + return sum + this._timeAdapter.differenceInMinutes(arrivalNoTz, departureNoTz); } return sum; }, 0); } private _getRelativeDuration(totalDuration: number, leg: SbbPearlChainLegElement): number { - const arrivalNoTz = this._dateAdapter.deserialize(leg.arrival); - const departureNoTz = this._dateAdapter.deserialize(leg.departure); - if (arrivalNoTz && departureNoTz) { - const duration = differenceInMinutes(arrivalNoTz, departureNoTz); + if (this._timeAdapter.isValid(leg.arrival) && this._timeAdapter.isValid(leg.departure)) { + const duration = this._timeAdapter.differenceInMinutes(leg.arrival, leg.departure); if (totalDuration === 0) { - return 100; + return 1; } - return (duration / totalDuration) * 100; + return duration / totalDuration; } return 0; } - private _getProgress(now: Date, start: Date, end: Date): number { + private _getProgress(start: Date, end: Date): number { if (!start || !end) { return 0; } - const total = differenceInMinutes(end, start); - const progress = differenceInMinutes(now, start); + const total = this._timeAdapter.differenceInMinutes(end, start); + const progress = this._timeAdapter.differenceInMinutes(this.now, start); - return total && (progress / total) * 100; + return total && progress / total; } - private _addMinutes(d: SbbDateLike | null, amount: number): Date { - const date: Date | null = this._dateAdapter.deserialize(d); - return date ? addMinutes(date, amount) : this._dateAdapter.invalid(); + private _getLegStatus(leg: SbbPearlChainLegElement): Status { + const start = this._timeAdapter.addMinutes(leg.departure, leg.departureDelay); + const end = this._timeAdapter.addMinutes(leg.arrival, leg.arrivalDelay); + return this._getStatus(start, end); } - private _getLegStatus(now: Date, leg: SbbPearlChainLegElement): Status { - const start = this._addMinutes(leg.departure, leg.departureDelay); - const end = this._addMinutes(leg.arrival, leg.arrivalDelay); - return this._getStatus(now, start, end); - } - - private _getStatus(now: Date, start?: Date, end?: Date): Status { - if (start && isBefore(start, now) && end && isAfter(end, now)) { + private _getStatus(start?: Date, end?: Date): Status { + if ( + start && + !this._timeAdapter.isBefore(this.now, start) && + end && + this._timeAdapter.isAfter(end, this.now) + ) { return 'progress'; - } else if (end && isBefore(end, now)) { + } else if (end && !this._timeAdapter.isBefore(this.now, end)) { return 'past'; } return 'future'; } - private _renderPosition(now: Date, progressLeg: SbbPearlChainLegElement): void { + private _renderPosition(progressLeg: SbbPearlChainLegElement): void { const currentPosition = this._getProgress( - now, - this._addMinutes(progressLeg.departure, progressLeg.departureDelay), - this._addMinutes(progressLeg.arrival, progressLeg.arrivalDelay), + this._timeAdapter.addMinutes(progressLeg.departure, progressLeg.departureDelay), + this._timeAdapter.addMinutes(progressLeg.arrival, progressLeg.arrivalDelay), ); if (currentPosition < 0 && currentPosition > 100) { return; } - progressLeg?.style.setProperty('--sbb-pearl-chain-status-position', `${currentPosition / 100}`); - } - - private _getBullet(index: number): Element { - const a = Array.from(this.shadowRoot!.querySelectorAll('.sbb-pearl-chain__bullet')); - return a[index]; + progressLeg?.style.setProperty('--sbb-pearl-chain-status-position', `${currentPosition}`); } private _getFirstBullet(): Element { - return this._getBullet(0); + return Array.from(this.shadowRoot!.querySelectorAll('.sbb-pearl-chain__bullet'))[0]; } private _getLastBullet(): Element { - return this._getBullet(1); + return Array.from(this.shadowRoot!.querySelectorAll('.sbb-pearl-chain__bullet'))[1]; } protected override updated(changedProperties: PropertyValues): void { @@ -133,7 +135,7 @@ class SbbPearlChainElement extends LitElement { } private _configureBullet(bullet: Element, leg: SbbPearlChainLegElement, first: boolean): void { - const status = this._getLegStatus(this.now, leg); + const status = this._getLegStatus(leg); bullet.toggleAttribute('data-disrupted', leg.disruption); bullet.toggleAttribute('data-skipped', first ? leg.departureSkipped : leg.arrivalSkipped); @@ -147,17 +149,17 @@ class SbbPearlChainElement extends LitElement { this._configureBullet(this._getLastBullet(), legs[legs.length - 1], false); legs.map((leg, index) => { - const status = this._getLegStatus(this.now, leg); + const status = this._getLegStatus(leg); leg.style.setProperty( '--sbb-pearl-chain-leg-weight', - `${this._getRelativeDuration(this._totalDuration(legs), leg) / 100}`, + `${this._getRelativeDuration(this._totalDuration(legs), leg)}`, ); leg.past = status === 'past'; leg.toggleAttribute('data-progress', status === 'progress'); if (status === 'progress') { - this._renderPosition(this.now, leg); + this._renderPosition(leg); } // If previous leg has arrival-skipped an attribute is set to style the stop diff --git a/src/elements/pearl-chain/pearl-chain/pearl-chain.visual.spec.ts b/src/elements/pearl-chain/pearl-chain/pearl-chain.visual.spec.ts index e9c2ed0540f..defb0b403f2 100644 --- a/src/elements/pearl-chain/pearl-chain/pearl-chain.visual.spec.ts +++ b/src/elements/pearl-chain/pearl-chain/pearl-chain.visual.spec.ts @@ -15,12 +15,13 @@ import { describe(`sbb-pearl-chain`, () => { const cases = [ - { name: 'no stops', legs: [futureLegTemplate] }, + { name: 'no stops', legs: [futureLegTemplate], now: new Date('2024-12-01T12:11:00') }, { name: 'many stops', legs: [futureLegTemplate, longFutureLegTemplate, futureLegTemplate, futureLegTemplate], + now: new Date('2024-12-01T12:11:00'), }, - { name: 'cancelled', legs: [disruptionTemplate] }, + { name: 'cancelled', legs: [disruptionTemplate], now: new Date('2024-12-01T12:11:00') }, { name: 'cancelled many stops', legs: [ @@ -29,16 +30,17 @@ describe(`sbb-pearl-chain`, () => { futureLegTemplate, cancelledLegTemplate(false, false, true), ], + now: new Date('2024-12-01T12:11:00'), }, { name: 'with position', legs: [progressLegTemplate], - now: new Date('2024-12-05T12:11:00').valueOf(), + now: new Date('2024-12-05T12:11:00'), }, { name: 'past', legs: [pastLegTemplate, pastLegTemplate], - now: new Date('2025-11-01T12:11:00').valueOf(), + now: new Date('2025-11-01T12:11:00'), }, { name: 'departure stop skipped', @@ -49,7 +51,7 @@ describe(`sbb-pearl-chain`, () => { cancelledLegTemplate(true), futureLegTemplate, ], - now: new Date('2024-12-05T12:11:00').valueOf(), + now: new Date('2024-12-05T12:11:00'), }, { name: 'arrival stop skipped', @@ -60,17 +62,17 @@ describe(`sbb-pearl-chain`, () => { cancelledLegTemplate(false, true), futureLegTemplate, ], - now: new Date('2024-12-05T12:11:00').valueOf(), + now: new Date('2024-12-05T12:11:00'), }, { name: 'first stop skipped', legs: [cancelledLegTemplate(true), futureLegTemplate, longFutureLegTemplate], - now: new Date('2024-12-05T12:11:00').valueOf(), + now: new Date('2024-12-05T12:11:00'), }, { name: 'last stop skipped', legs: [pastLegTemplate, pastLegTemplate, cancelledLegTemplate(false, true)], - now: new Date('2023-12-05T12:11:00').valueOf(), + now: new Date('2023-12-05T12:11:00'), }, { name: 'mixed', @@ -81,7 +83,19 @@ describe(`sbb-pearl-chain`, () => { cancelledLegTemplate(false, false, true), longFutureLegTemplate, ], - now: new Date('2024-12-05T12:11:00').valueOf(), + now: new Date('2024-12-05T12:11:00'), + }, + { + name: 'forced colors', + legs: [ + pastLegTemplate, + progressLegTemplate, + futureLegTemplate, + cancelledLegTemplate(false, false, true), + longFutureLegTemplate, + ], + now: new Date('2024-12-05T12:11:00'), + forcedColors: true, }, ]; @@ -90,14 +104,12 @@ describe(`sbb-pearl-chain`, () => { it( c.name, visualDiffDefault.with(async (setup) => { - await setup.withFixture(html` - - ${c.legs} - - `); + await setup.withFixture( + html` ${c.legs} `, + { + forcedColors: c?.forcedColors, + }, + ); }), ); } diff --git a/src/elements/pearl-chain/pearl-chain/readme.md b/src/elements/pearl-chain/pearl-chain/readme.md index d238025f830..c82b8458d2c 100644 --- a/src/elements/pearl-chain/pearl-chain/readme.md +++ b/src/elements/pearl-chain/pearl-chain/readme.md @@ -53,9 +53,13 @@ The components allows to slot any number of `sbb-pearl-chain-leg` in the `unname ## Properties -| Name | Attribute | Privacy | Type | Default | Description | -| ----------- | ----------- | ------- | --------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------- | -| `arrival` | `arrival` | public | `SbbDateLike \| null` | `null` | Prop to render the arrival time - will be formatted as "H:mm" | -| `departure` | `departure` | public | `SbbDateLike \| null` | `null` | Prop to render the departure time - will be formatted as "H:mm" | -| `marker` | `marker` | public | `Marker` | `'static'` | Whether the marker should be pulsing or static. | -| `now` | `now` | public | `SbbDateLike \| null` | `null` | A configured date which acts as the current date instead of the real current date. Recommended for testing purposes. | +| Name | Attribute | Privacy | Type | Default | Description | +| -------- | --------- | ------- | ----------------------- | ------------ | -------------------------------------------------------------------------------------------------------------------- | +| `marker` | `marker` | public | `'static' \| 'pulsing'` | `'static'` | Whether the marker should be pulsing or static. | +| `now` | `now` | public | `Date` | `new Date()` | A configured date which acts as the current date instead of the real current date. Recommended for testing purposes. | + +## Slots + +| Name | Description | +| ---- | ---------------------------------------------------------------------- | +| | Use the unnamed slot to add `sbb-pearl-chain-leg`s to the pearl-chain. |