Skip to content

Commit

Permalink
feat(sbb-calendar, sbb-datepicker-toggle): allow choosing initial cal…
Browse files Browse the repository at this point in the history
…endar view (#2990)

Closes #2822
  • Loading branch information
jeripeierSBB authored Aug 21, 2024
1 parent b75c4c5 commit 7c8a690
Show file tree
Hide file tree
Showing 10 changed files with 197 additions and 26 deletions.
4 changes: 4 additions & 0 deletions src/elements/calendar/calendar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@
--sbb-calendar-cell-transition-easing-function: var(--sbb-animation-easing);
--sbb-calendar-tables-gap: var(--sbb-spacing-fixed-10x);
--sbb-calendar-table-animation-shift: #{sbb.px-to-rem-build(0.1)};

// While changing views, there would be a few frames where the height of the calendar collapses to just
// the height of the controls and then grow back to the height of the next view.
// By using 0.1ms this can be avoided.
--sbb-calendar-table-animation-duration: 0.1ms;
--sbb-calendar-table-column-spaces: 12;
--sbb-calendar-control-view-change-height: #{sbb.px-to-rem-build(44)};
Expand Down
3 changes: 1 addition & 2 deletions src/elements/calendar/calendar.snapshot.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ describe(`sbb-calendar`, () => {
await expect(element).shadowDom.to.be.equalSnapshot();
});

// We skip safari because it has an inconsistent behavior on ci environment
testA11yTreeSnapshot(undefined, undefined, { safari: true });
testA11yTreeSnapshot();
});
});
40 changes: 39 additions & 1 deletion src/elements/calendar/calendar.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { html } from 'lit/static-html.js';

import type { SbbSecondaryButtonElement } from '../button/secondary-button.js';
import { fixture } from '../core/testing/private.js';
import { waitForCondition, waitForLitRender, EventSpy } from '../core/testing.js';
import { EventSpy, waitForCondition, waitForLitRender } from '../core/testing.js';

import { SbbCalendarElement } from './calendar.js';

Expand Down Expand Up @@ -245,6 +245,44 @@ describe(`sbb-calendar`, () => {
expect(firstDisabledMaxDate).to.have.attribute('aria-disabled', 'true');
});

it('opens year view', async () => {
element.view = 'year';
await waitForLitRender(element);

expect(element.shadowRoot!.querySelector('.sbb-calendar__table-year-view')).not.to.be.null;
});

it('opens month view', async () => {
element.view = 'month';
await waitForLitRender(element);

expect(element.shadowRoot!.querySelector('.sbb-calendar__table-month-view')).not.to.be.null;
expect(
element.shadowRoot!.querySelector('#sbb-calendar__month-selection')!.textContent!.trim(),
).to.be.equal('2023');
});

it('opens month view with selected date', async () => {
element.selected = '2017-01-22';
element.view = 'month';
await waitForLitRender(element);

expect(
element.shadowRoot!.querySelector('#sbb-calendar__month-selection')!.textContent!.trim(),
).to.be.equal('2017');
});

it('opens month view with current date', async () => {
element.selected = undefined;
element.now = '2022-08-15';
element.view = 'month';
await waitForLitRender(element);

expect(
element.shadowRoot!.querySelector('#sbb-calendar__month-selection')!.textContent!.trim(),
).to.be.equal('2022');
});

describe('navigation', () => {
it('navigates left via keyboard', async () => {
element.focus();
Expand Down
15 changes: 15 additions & 0 deletions src/elements/calendar/calendar.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,13 @@ const max: InputType = {
},
};

const view: InputType = {
control: {
type: 'inline-radio',
},
options: ['day', 'month', 'year'],
};

const now: InputType = {
control: {
type: 'date',
Expand Down Expand Up @@ -127,6 +134,7 @@ const defaultArgTypes: ArgTypes = {
min,
max,
dateFilter,
view: view,
now,
};

Expand All @@ -137,6 +145,7 @@ const defaultArgs: Args = {
wide: false,
selected: isChromatic() ? new Date(2023, 0, 20) : today,
now: isChromatic() ? new Date(2023, 0, 12, 0, 0, 0).valueOf() : undefined,
view: view.options![0],
};

export const Calendar: StoryObj = {
Expand Down Expand Up @@ -186,6 +195,12 @@ export const CalendarWideDynamicWidth: StoryObj = {
args: { ...defaultArgs, wide: true },
};

export const CalendarWithInitialYearSelection: StoryObj = {
render: Template,
argTypes: defaultArgTypes,
args: { ...defaultArgs, view: view.options![2] },
};

const meta: Meta = {
excludeStories: /.*DynamicWidth$/,
decorators: [withActions as Decorator],
Expand Down
55 changes: 37 additions & 18 deletions src/elements/calendar/calendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ export class SbbCalendarElement<T = Date> extends SbbHydrationMixin(LitElement)
/** If set to true, two months are displayed */
@property({ type: Boolean }) public wide = false;

/** The initial view of the calendar which should be displayed on opening. */
@property() public view: CalendarView = 'day';

/** The minimum valid date. Takes T Object, ISOString, and Unix Timestamp (number of seconds since Jan 1, 1970). */
@property()
public set min(value: SbbDateLike<T> | null) {
Expand Down Expand Up @@ -238,10 +241,7 @@ export class SbbCalendarElement<T = Date> extends SbbHydrationMixin(LitElement)

/** Resets the active month according to the new state of the calendar. */
public resetPosition(): void {
if (this._calendarView !== 'day') {
this._resetToDayView();
}
this._activeDate = this.selected ?? this.now;
this._resetCalendarView();
this._init();
}

Expand All @@ -268,6 +268,12 @@ export class SbbCalendarElement<T = Date> extends SbbHydrationMixin(LitElement)
if (changedProperties.has('wide')) {
this.resetPosition();
}

if (changedProperties.has('view')) {
this._setChosenYear();
this._chosenMonth = undefined;
this._nextCalendarView = this._calendarView = this.view;
}
}

protected override updated(changedProperties: PropertyValues<this>): void {
Expand Down Expand Up @@ -496,13 +502,23 @@ export class SbbCalendarElement<T = Date> extends SbbHydrationMixin(LitElement)
/** Emits the selected date and sets it internally. */
private _selectDate(day: string): void {
this._chosenMonth = undefined;
this._chosenYear = undefined;
this._setChosenYear();
if (this._selected !== day) {
this._selected = day;
this._dateSelected.emit(this._dateAdapter.deserialize(day)!);
}
}

private _setChosenYear(): void {
if (this.view === 'month') {
this._chosenYear = this._dateAdapter.getYear(
this._dateAdapter.deserialize(this._selected) ?? this.selected ?? this.now,
);
} else {
this._chosenYear = undefined;
}
}

private _assignActiveDate(date: T): void {
if (this._min && this._dateAdapter.compareDate(this._min, date) > 0) {
this._activeDate = this._min;
Expand Down Expand Up @@ -806,13 +822,16 @@ export class SbbCalendarElement<T = Date> extends SbbHydrationMixin(LitElement)
: this._findNext(days, nextIndex, -verticalOffset);
}

private _resetToDayView(): void {
private _resetCalendarView(initTransition = false): void {
this._resetFocus = true;
this._activeDate = this.selected ?? this.now;
this._chosenYear = undefined;
this._setChosenYear();
this._chosenMonth = undefined;
this._nextCalendarView = 'day';
this._removeTable();
this._nextCalendarView = this._calendarView = this.view;

if (initTransition) {
this._startTableTransition();
}
}

/** Render the view for the day selection. */
Expand Down Expand Up @@ -863,7 +882,7 @@ export class SbbCalendarElement<T = Date> extends SbbHydrationMixin(LitElement)
@click=${() => {
this._resetFocus = true;
this._nextCalendarView = 'year';
this._removeTable();
this._startTableTransition();
}}
>
${monthLabel}
Expand Down Expand Up @@ -1016,7 +1035,7 @@ export class SbbCalendarElement<T = Date> extends SbbHydrationMixin(LitElement)
id="sbb-calendar__month-selection"
class="sbb-calendar__controls-change-date"
aria-label=${`${i18nCalendarDateSelection[this._language.current]} ${this._chosenYear}`}
@click=${() => this._resetToDayView()}
@click=${() => this._resetCalendarView(true)}
>
${this._chosenYear} ${this._wide ? ` - ${this._chosenYear! + 1}` : nothing}
<sbb-icon name="chevron-small-up-small"></sbb-icon>
Expand Down Expand Up @@ -1104,7 +1123,7 @@ export class SbbCalendarElement<T = Date> extends SbbHydrationMixin(LitElement)
this._dateAdapter.getDate(this._activeDate),
),
);
this._removeTable();
this._startTableTransition();
}

/** Render the view for the year selection. */
Expand Down Expand Up @@ -1163,7 +1182,7 @@ export class SbbCalendarElement<T = Date> extends SbbHydrationMixin(LitElement)
id="sbb-calendar__year-selection"
class="sbb-calendar__controls-change-date"
aria-label="${i18nCalendarDateSelection[this._language.current]} ${yearLabel}"
@click=${() => this._resetToDayView()}
@click=${() => this._resetCalendarView(true)}
>
${yearLabel}
<sbb-icon name="chevron-small-up-small"></sbb-icon>
Expand Down Expand Up @@ -1230,7 +1249,7 @@ export class SbbCalendarElement<T = Date> extends SbbHydrationMixin(LitElement)
this._dateAdapter.getDate(this._activeDate),
),
);
this._removeTable();
this._startTableTransition();
}

private get _getView(): TemplateResult {
Expand Down Expand Up @@ -1261,11 +1280,11 @@ export class SbbCalendarElement<T = Date> extends SbbHydrationMixin(LitElement)
}
}

private _removeTable(): void {
private _startTableTransition(): void {
this.toggleAttribute('data-transition', true);
this.shadowRoot!.querySelectorAll('table').forEach((e) =>
e.classList.toggle('sbb-calendar__table-hide'),
);
this.shadowRoot
?.querySelectorAll('table')
?.forEach((e) => e.classList.toggle('sbb-calendar__table-hide'));
}

protected override render(): TemplateResult {
Expand Down
1 change: 1 addition & 0 deletions src/elements/calendar/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ For accessibility purposes, the component is rendered as a native table element
| `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 | `T` | `null` | A configured date which acts as the current date instead of the real current date. Recommended for testing purposes. |
| `selected` | `selected` | public | `T \| null` | | The selected date. Takes T Object, ISOString, and Unix Timestamp (number of seconds since Jan 1, 1970). |
| `view` | `view` | public | `CalendarView` | `'day'` | The initial view of the calendar which should be displayed on opening. |
| `wide` | `wide` | public | `boolean` | `false` | If set to true, two months are displayed |

## Methods
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { assert, expect } from '@open-wc/testing';
import { sendKeys } from '@web/test-runner-commands';
import { html } from 'lit/static-html.js';

import type { SbbCalendarElement } from '../../calendar.js';
import { defaultDateAdapter } from '../../core/datetime/native-date-adapter.js';
import { fixture } from '../../core/testing/private.js';
import { EventSpy, waitForCondition, waitForLitRender } from '../../core/testing.js';
import type { SbbFormFieldElement } from '../../form-field.js';
Expand Down Expand Up @@ -181,4 +183,76 @@ describe(`sbb-datepicker-toggle`, () => {
expect(changeSpy.count).to.be.equal(1);
expect(blurSpy.count).to.be.equal(1);
});

it('handles view property', async () => {
const element: SbbDatepickerToggleElement = await fixture(
html`<sbb-form-field>
<sbb-datepicker-toggle view="year"></sbb-datepicker-toggle>
<sbb-datepicker now="2022-04-01"></sbb-datepicker>
<input />
</sbb-form-field>`,
);

const didOpenEventSpy = new EventSpy(SbbPopoverElement.events.didOpen, element);
const didCloseEventSpy = new EventSpy(SbbPopoverElement.events.didClose, element);
const datepickerToggle =
element.querySelector<SbbDatepickerToggleElement>('sbb-datepicker-toggle')!;

// Open calendar
datepickerToggle.open();
await waitForCondition(() => didOpenEventSpy.events.length === 1);

// Year view should be active
const calendar = datepickerToggle.shadowRoot!.querySelector('sbb-calendar')!;
expect(calendar.shadowRoot!.querySelector('.sbb-calendar__table-year-view')!).not.to.be.null;

// Select year
calendar.shadowRoot!.querySelectorAll('button')[5].click();
await waitForLitRender(element);
await waitForCondition(() => !calendar.hasAttribute('data-transition'));

// Select month
calendar.shadowRoot!.querySelectorAll('button')[5].click();
await waitForLitRender(element);
await waitForCondition(() => !calendar.hasAttribute('data-transition'));

// Select day
calendar.shadowRoot!.querySelectorAll('button')[5].click();
await waitForLitRender(element);
await waitForCondition(() => !calendar.hasAttribute('data-transition'));

// Expect selected date and closed calendar
expect(defaultDateAdapter.toIso8601(calendar.selected!)).to.be.equal('2020-05-05');
await waitForCondition(() => didCloseEventSpy.events.length === 1);

// Open again
datepickerToggle.open();
await waitForCondition(() => didOpenEventSpy.events.length === 2);

// Should open with year view again
expect(calendar.shadowRoot!.querySelector('.sbb-calendar__table-year-view')!).not.to.be.null;
expect(
calendar.shadowRoot!.querySelector('.sbb-calendar__selected')!.textContent!.trim(),
).to.be.equal('2020');

// Close again
await sendKeys({ press: 'Escape' });
await waitForCondition(() => didCloseEventSpy.events.length === 2);

// Changing to month view
datepickerToggle.view = 'month';
await waitForLitRender(element);

// Open again
datepickerToggle.open();
await waitForCondition(() => didOpenEventSpy.events.length === 3);

// Month view should be active and correct year preselected
expect(calendar.shadowRoot!.querySelector('.sbb-calendar__table-month-view')!).not.to.be.null;
expect(
calendar
.shadowRoot!.querySelector('.sbb-calendar__controls-change-date')!
.textContent!.trim(),
).to.be.equal('2020');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,21 @@ const negative: InputType = {
},
};

const view: InputType = {
control: {
type: 'inline-radio',
},
options: ['day', 'month', 'year'],
};

const defaultArgTypes: ArgTypes = {
negative,
view: view,
};

const defaultArgs: Args = {
negative: false,
view: view.options![0],
};

// Story interaction executed after the story renders
Expand Down Expand Up @@ -104,6 +113,13 @@ export const InFormFieldNegative: StoryObj = {
play: isChromatic() ? playStory : undefined,
};

export const InitialYearSelection: StoryObj = {
render: FormFieldTemplate,
argTypes: defaultArgTypes,
args: { ...defaultArgs, view: view.options![2] },
play: isChromatic() ? playStory : undefined,
};

const meta: Meta = {
decorators: [withActions as Decorator],
parameters: {
Expand Down
Loading

0 comments on commit 7c8a690

Please sign in to comment.