Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sbb-calendar, sbb-datepicker-toggle): allow choosing initial calendar view #2990

Merged
merged 11 commits into from
Aug 21, 2024
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 @@
/** 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 @@

/** 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 @@
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 @@
/** 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 @@
: 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();
}

Check warning on line 834 in src/elements/calendar/calendar.ts

View check run for this annotation

Codecov / codecov/patch

src/elements/calendar/calendar.ts#L833-L834

Added lines #L833 - L834 were not covered by tests
}

/** Render the view for the day selection. */
Expand Down Expand Up @@ -863,7 +882,7 @@
@click=${() => {
this._resetFocus = true;
this._nextCalendarView = 'year';
this._removeTable();
this._startTableTransition();
}}
>
${monthLabel}
Expand Down Expand Up @@ -1016,7 +1035,7 @@
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 @@
this._dateAdapter.getDate(this._activeDate),
),
);
this._removeTable();
this._startTableTransition();
}

/** Render the view for the year selection. */
Expand Down Expand Up @@ -1163,7 +1182,7 @@
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 @@
this._dateAdapter.getDate(this._activeDate),
),
);
this._removeTable();
this._startTableTransition();
}

private get _getView(): TemplateResult {
Expand Down Expand Up @@ -1261,11 +1280,11 @@
}
}

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 () => {
MarioCastigliano marked this conversation as resolved.
Show resolved Hide resolved
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
Loading