Skip to content

Commit

Permalink
Refactor common code into mixins
Browse files Browse the repository at this point in the history
  • Loading branch information
RichardHelm committed Feb 24, 2025
1 parent d75c896 commit 15f451b
Show file tree
Hide file tree
Showing 50 changed files with 2,060 additions and 1,689 deletions.
58 changes: 20 additions & 38 deletions libs/components/src/lib/date-picker/date-picker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
fixture,
setupDelegatesFocusPolyfill,
} from '@vivid-nx/shared';
import enUS from '@vonage/vivid/locales/en-US';
import enUS from '../../locales/en-US';
import deDE from '../../locales/de-DE';
import { setLocale } from '../../shared/localization';
import { TextField } from '../text-field/text-field';
Expand All @@ -14,22 +14,10 @@ import '.';

const COMPONENT_TAG = 'vwc-date-picker';

// Mock current date to be 2023-08-10 for the tests

vi.mock('../../shared/date-picker/calendar/month.ts', async () => ({
...(await vi.importActual('../../shared/date-picker/calendar/month.ts')),
getCurrentMonth: vi.fn().mockReturnValue({ month: 7, year: 2023 }),
}));

vi.mock('../../shared/date-picker/calendar/dateStr.ts', async () => ({
...(await vi.importActual('../../shared/date-picker/calendar/dateStr.ts')),
currentDateStr: vi.fn().mockReturnValue('2023-08-10'),
}));

describe('vwc-date-picker', () => {
let element: DatePicker;
let textField: TextField;
let calendarButton: Button;
let pickerButton: Button;
let titleAction: HTMLButtonElement;

const getDateButton = (date: string) =>
Expand All @@ -52,7 +40,7 @@ describe('vwc-date-picker', () => {
}

async function openPopup() {
calendarButton.click();
pickerButton.click();
await elementUpdated(element);
}

Expand All @@ -61,13 +49,21 @@ describe('vwc-date-picker', () => {
await elementUpdated(element);
}

beforeAll(() => {
// Use a fixed date of 2023-08-10 for all tests
vi.useFakeTimers({
now: new Date(2023, 7, 10),
toFake: ['Date'],
});
});

beforeEach(async () => {
element = (await fixture(
`<${COMPONENT_TAG}></${COMPONENT_TAG}>`
)) as DatePicker;
textField = element.shadowRoot!.querySelector('.control') as TextField;
calendarButton = element.shadowRoot!.querySelector(
'#calendar-button'
pickerButton = element.shadowRoot!.querySelector(
'#picker-button'
) as Button;
titleAction = element.shadowRoot!.querySelector(
'.title-action'
Expand All @@ -89,24 +85,6 @@ describe('vwc-date-picker', () => {
});
});

describe('errorText', () => {
it('should forward errorText to the text field', async () => {
element.errorText = 'errorText';
await elementUpdated(element);

expect(textField.errorText).toBe('errorText');
});

it('should have a higher priority than an internal validation error', async () => {
element.errorText = 'errorText';
await elementUpdated(element);

typeIntoTextField('x');

expect(textField.errorText).toBe('errorText');
});
});

describe('value', () => {
it('should display a formatted version of value in the text field', async () => {
element.value = '2021-01-21';
Expand Down Expand Up @@ -225,16 +203,20 @@ describe('vwc-date-picker', () => {
});
});

describe('calendar button', () => {
describe('picker button', () => {
it('should have an icon of "calendar-line"', async () => {
expect(pickerButton.icon).toBe('calendar-line');
});

it('should have an aria-label of "Choose date" when no date is selected', async () => {
expect(calendarButton.getAttribute('aria-label')).toBe('Choose date');
expect(pickerButton.getAttribute('aria-label')).toBe('Choose date');
});

it('should have an aria-label of "Change date, DATE" when a date is selected', async () => {
element.value = '2021-01-01';
await elementUpdated(element);

expect(calendarButton.getAttribute('aria-label')).toBe(
expect(pickerButton.getAttribute('aria-label')).toBe(
'Change date, 01/01/2021'
);
});
Expand Down
7 changes: 7 additions & 0 deletions libs/components/src/lib/date-picker/date-picker.template.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { CalendarPickerTemplate } from '../../shared/picker-field/mixins/calendar-picker.template';
import type { VividElementDefinitionContext } from '../../shared/design-system/defineVividComponent';
import { PickerFieldTemplate } from '../../shared/picker-field/picker-field.template';

export const DatePickerTemplate = (context: VividElementDefinitionContext) => {
return PickerFieldTemplate(context, CalendarPickerTemplate(context));
};
145 changes: 52 additions & 93 deletions libs/components/src/lib/date-picker/date-picker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@ import {
type FormElement,
formElements,
} from '../../shared/patterns';
import { CalendarPicker } from '../../shared/picker-field/mixins/calendar-picker';
import {
type DateStr,
isValidDateStr,
} from '../../shared/date-picker/calendar/dateStr';
import { type DateStr, isValidDateStr } from '../../shared/datetime/dateStr';
import {
formatPresentationDate,
parsePresentationDate,
} from '../../shared/date-picker/calendar/presentationDate';
} from '../../shared/datetime/presentationDate';
import { PickerField } from '../../shared/picker-field/picker-field';
import { SingleValuePicker } from '../../shared/picker-field/mixins/single-value-picker';
import { CalendarPicker } from '../../shared/picker-field/mixins/calendar-picker';
import { MinMaxCalendarPicker } from '../../shared/picker-field/mixins/min-max-calendar-picker';
import { SingleDatePickerMixin } from '../../shared/picker-field/mixins/single-date-picker';

/**
* Single date picker component.
Expand All @@ -27,129 +28,95 @@ import {
*/
@errorText
@formElements
export class DatePicker extends CalendarPicker {
export class DatePicker extends SingleDatePickerMixin(
SingleValuePicker(MinMaxCalendarPicker(CalendarPicker(PickerField)))
) {
/**
* @internal
*/
override valueChanged(previous: string, next: string) {
super.valueChanged(previous, next);
if (this.value) {
if (!isValidDateStr(this.value)) {
this.value = '';
return;
}

this._adjustSelectedMonthToEnsureVisibilityOf(this.value);
}
this._updatePresentationValue();
}

protected override _updatePresentationValue() {
if (this.value) {
this._presentationValue = formatPresentationDate(
this.value,
this.locale.datePicker
);
} else {
this._presentationValue = '';
}
}
override _isValidValue = isValidDateStr;

#updateValueDueToUserInteraction(newValue: DateStr) {
this.value = newValue;
this.$emit('change');
this.$emit('input');
}

constructor() {
super();
this.proxy.type = 'date';
/**
* @internal
*/
override _toPresentationValue(value: DateStr) {
return formatPresentationDate(value, this.locale.calendarPicker);
}

/**
* @internal
*/
@volatile
get _calendarButtonLabel() {
if (this.value) {
return this.locale.datePicker.changeDateLabel(
formatPresentationDate(this.value, this.locale.datePicker)
);
} else {
return this.locale.datePicker.chooseDateLabel;
}
override _parsePresentationValue(presentationValue: string) {
return parsePresentationDate(presentationValue, this.locale.calendarPicker);
}

/**
* @internal
*/
get _textFieldPlaceholder(): string {
return this.locale.datePicker.dateFormatPlaceholder;
override _dateValue(): DateStr | '' {
return this.value;
}

/**
* @internal
*/
override _textFieldSize = '20';
override _withUpdatedDate(dateStr: DateStr): DateStr | '' {
return dateStr;
}

constructor() {
super();
this.proxy.type = 'date';
}

/**
* @internal
*/
_onTextFieldChange() {
if (this._presentationValue === '') {
this.#updateValueDueToUserInteraction('');
return;
}

try {
this.#updateValueDueToUserInteraction(
parsePresentationDate(this._presentationValue, this.locale.datePicker)
@volatile
get _pickerButtonLabel() {
if (this.value) {
return this.locale.calendarPicker.changeDateLabel(
this._toPresentationValue(this.value)
);
} catch (_) {
return;
} else {
return this.locale.calendarPicker.chooseDateLabel;
}
}

/**
* Handle selecting a date from the calendar.
* @internal
*/
_onDateClick(date: DateStr) {
this.#updateValueDueToUserInteraction(date);
this._closePopup();
get _dialogLabel() {
return this.locale.calendarPicker.chooseDateLabel;
}

/**
* @internal
*/
override _isDateSelected(date: DateStr) {
return date === this.value;
get _textFieldPlaceholder(): string {
return this.locale.calendarPicker.dateFormatPlaceholder;
}

/**
* @internal
*/
override _isDateAriaSelected(date: DateStr) {
return this._isDateSelected(date);
}
override _textFieldSize = '20';

/**
* Handle selecting a date from the calendar.
* @internal
*/
protected override _getSelectedDates(): DateStr[] {
const dates = [];
if (this.value) {
dates.push(this.value);
}
return dates;
override _onDateClick(date: DateStr) {
super._onDateClick(date);
this._closePopup();
}

/**
* @internal
*/
protected override _getCustomValidationError(): string | null {
override _getCustomValidationError(): string | null {
if (this._isPresentationValueInvalid()) {
return this.locale.datePicker.invalidDateError;
return this.locale.calendarPicker.invalidDateError;
}

return null;
Expand All @@ -158,25 +125,17 @@ export class DatePicker extends CalendarPicker {
/**
* @internal
*/
private _isPresentationValueInvalid() {
if (this._presentationValue === '') {
return false;
}

try {
parsePresentationDate(this._presentationValue, this.locale.datePicker);
return false;
} catch (_) {
return true;
}
override _focusableElsWithinDialog() {
return this._dialogEl.querySelectorAll(
'button, .vwc-button'
) as NodeListOf<HTMLElement>;
}

/**
* @internal
*/
override _onClearClick() {
this.#updateValueDueToUserInteraction('');
super._onClearClick();
override get _pickerButtonIcon() {
return 'calendar-line';
}
}

Expand Down
7 changes: 4 additions & 3 deletions libs/components/src/lib/date-picker/definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import { popupDefinition } from '../popup/definition';
import { buttonDefinition } from '../button/definition';
import { textFieldDefinition } from '../text-field/definition';
import { dividerDefinition } from '../divider/definition';
import styles from '../../shared/date-picker/date-picker-base.scss?inline';
import { CalendarPickerTemplate as template } from '../../shared/picker-field/mixins/calendar-picker.template';
import pickerFieldStyles from '../../shared/picker-field/picker-field.scss?inline';
import calendarStyles from '../../shared/picker-field/mixins/calendar-picker.scss?inline';
import { createRegisterFunction } from '../../shared/design-system/createRegisterFunction';
import { defineVividComponent } from '../../shared/design-system/defineVividComponent';
import { DatePicker } from './date-picker';
import { DatePickerTemplate as template } from './date-picker.template';

/**
* @internal
Expand All @@ -17,7 +18,7 @@ export const datePickerDefinition = defineVividComponent(
template,
[buttonDefinition, popupDefinition, textFieldDefinition, dividerDefinition],
{
styles,
styles: [pickerFieldStyles, calendarStyles],
shadowOptions: {
delegatesFocus: true,
},
Expand Down
6 changes: 3 additions & 3 deletions libs/components/src/lib/date-picker/ui.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,15 @@ test('should show the component', async ({ page }: { page: Page }) => {

await page.waitForLoadState('networkidle');

await page.locator('#date-picker #calendar-button').click();
await page.locator('#date-picker #picker-button').click();

// Prevent clicking the month picker from closing the date picker
await page.evaluate(() => {
const datePicker = document.querySelector('#month-picker') as any;
datePicker.addEventListener('click', (e) => e.stopPropagation());
});

await page.locator('#month-picker #calendar-button').click();
await page.locator('#month-picker #picker-button').click();

await page.locator('#month-picker .title-action').click();

Expand All @@ -95,7 +95,7 @@ test('selecting a date', async ({ page }: { page: Page }) => {

await page.waitForLoadState('networkidle');

await page.locator('vwc-date-picker #calendar-button').click();
await page.locator('vwc-date-picker #picker-button').click();

await page.getByRole('gridcell', { name: '15' }).click();

Expand Down
Loading

0 comments on commit 15f451b

Please sign in to comment.