Skip to content

Commit

Permalink
[WOM-5377][BpkComponentCalendar] Optimise date formatting for improve…
Browse files Browse the repository at this point in the history
…d INP (#3695)

* - front-load date formatting for better INP
- reduce usage of date formats

* defer aria label formatting until thread is idle

* resolve unit test failures

* add license heading to new files

* rename test files to match expected pattern

* do not mock deferCallback in its own tests

* resolve utils unit test failure

* Trigger Build

* address review comments

* resolve "Week" storybook failure

* reduce requestIdleCallback timeout to 500ms

* resolve unit test failures

---------

Co-authored-by: George Wright <[email protected]>
  • Loading branch information
gwright170 and George Wright authored Jan 2, 2025
1 parent 5aef694 commit 4dcb150
Show file tree
Hide file tree
Showing 13 changed files with 279 additions and 65 deletions.
44 changes: 36 additions & 8 deletions examples/bpk-component-calendar/examples.js
Original file line number Diff line number Diff line change
Expand Up @@ -249,14 +249,42 @@ const WeekExample = () => {
DateComponent: DummyDateComponent,
dateModifiers: {},
dates: [
new Date(1980, 5, 10),
new Date(1980, 5, 11),
new Date(1980, 5, 12),
new Date(1980, 5, 13),
new Date(1980, 5, 14),
new Date(1980, 5, 15),
new Date(1980, 5, 16),
].map(startOfDay),
{
val: startOfDay(new Date(1980, 5, 10)),
customLabel: 'Saturday, 10 May 1980',
isoLabel: '1980-05-10',
},
{
val: startOfDay(new Date(1980, 5, 11)),
customLabel: 'Sunday, 11 May 1980',
isoLabel: '1980-05-11',
},
{
val: startOfDay(new Date(1980, 5, 12)),
customLabel: 'Monday, 12 May 1980',
isoLabel: '1980-05-12',
},
{
val: startOfDay(new Date(1980, 5, 13)),
customLabel: 'Tuesday, 13 May 1980',
isoLabel: '1980-05-13',
},
{
val: startOfDay(new Date(1980, 5, 14)),
customLabel: 'Wednesday, 14 May 1980',
isoLabel: '1980-05-14',
},
{
val: startOfDay(new Date(1980, 5, 15)),
customLabel: 'Thursday, 15 May 1980',
isoLabel: '1980-05-15',
},
{
val: startOfDay(new Date(1980, 5, 16)),
customLabel: 'Friday, 16 May 1980',
isoLabel: '1980-05-16',
},
],
daysOfWeek: weekDays,
formatDateFull: (d) => d.toString(),
preventKeyboardFocus: false,
Expand Down
2 changes: 2 additions & 0 deletions packages/bpk-component-calendar/src/BpkCalendarDate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export type Props = DefaultProps & {

type DefaultProps = {
className?: string | null;
isoLabel?: string;
isBlocked?: boolean;
isFocused?: boolean;
isKeyboardFocusable?: boolean;
Expand Down Expand Up @@ -185,6 +186,7 @@ class BpkCalendarDate extends PureComponent<Props> {
}

delete buttonProps.preventKeyboardFocus;
delete buttonProps.isoLabel;

return (
<button
Expand Down
38 changes: 28 additions & 10 deletions packages/bpk-component-calendar/src/BpkCalendarGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,17 @@ import type { ElementType } from 'react';
import { Component } from 'react';

import { cssModules, isDeviceIos } from '../../bpk-react-utils';
import deferCallback from '../../bpk-react-utils/src/deferCallback';

import { addCalendarGridTransition } from './BpkCalendarGridTransition';
import BpkCalendarWeek from './BpkCalendarWeek';
import { CALENDAR_SELECTION_TYPE } from './custom-proptypes';
import {
addMonths,
formatIsoDate,
getCalendarMonthWeeks,
getCalendar,
getCalendarNoCustomLabel,
isSameMonth,
} from './date-utils';
import { memoize } from './utils';

import type { DateModifiers, SelectionConfiguration } from './custom-proptypes';

Expand Down Expand Up @@ -79,8 +79,14 @@ export type Props = DefaultProps & {
weekStartsOn: 0 | 1 | 2 | 3 | 4 | 5 | 6;
};

export type DateProps = {
val: Date;
customLabel: string | Date;
isoLabel: string;
};

type State = {
calendarMonthWeeks: Date[][];
calendarMonthWeeks: DateProps[][];
};
/*
BpkCalendarGrid - the grid representing a whole month
Expand Down Expand Up @@ -110,25 +116,39 @@ class BpkCalendarGrid extends Component<Props, State> {
constructor(props: Props) {
super(props);

// We cache expensive calculations (and identities) in state
this.state = {
calendarMonthWeeks: getCalendarMonthWeeks(
// Do not run expensive date formatting in the constructor
calendarMonthWeeks: getCalendarNoCustomLabel(
props.month,
props.weekStartsOn,
),
};
}

componentDidMount(): void {
// Defer expensive date formatting until after render to improve INP.
deferCallback(() =>
this.setState({
calendarMonthWeeks: getCalendar(
this.props.month,
this.props.weekStartsOn,
this.props.formatDateFull,
),
}),
);
}

UNSAFE_componentWillReceiveProps(nextProps: Props) {
// We cache expensive calculations (and identities) in state
if (
!isSameMonth(nextProps.month, this.props.month) ||
nextProps.weekStartsOn !== this.props.weekStartsOn
) {
this.setState({
calendarMonthWeeks: getCalendarMonthWeeks(
calendarMonthWeeks: getCalendar(
nextProps.month,
nextProps.weekStartsOn,
nextProps.formatDateFull,
),
});
}
Expand All @@ -142,7 +162,6 @@ class BpkCalendarGrid extends Component<Props, State> {
dateModifiers,
dateProps,
focusedDate,
formatDateFull,
ignoreOutsideDate,
isKeyboardFocusable,
markOutsideDays,
Expand All @@ -166,12 +185,11 @@ class BpkCalendarGrid extends Component<Props, State> {
<div>
{calendarMonthWeeks.map((dates) => (
<BpkCalendarWeek
key={formatIsoDate(dates[0])}
key={dates[0].isoLabel}
month={month}
dates={dates}
onDateClick={onDateClick}
onDateKeyDown={onDateKeyDown}
formatDateFull={memoize(formatDateFull)}
DateComponent={DateComponent}
dateModifiers={dateModifiers!}
preventKeyboardFocus={preventKeyboardFocus}
Expand Down
44 changes: 36 additions & 8 deletions packages/bpk-component-calendar/src/BpkCalendarWeek-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,42 @@ const initialProps: Props = {
DateComponent: DummyDateComponent,
dateModifiers: {},
dates: [
new Date(1980, 4, 31),
new Date(1980, 5, 1),
new Date(1980, 5, 2),
new Date(1980, 5, 3),
new Date(1980, 5, 4),
new Date(1980, 5, 5),
new Date(1980, 5, 6),
].map(startOfDay),
{
val: startOfDay(new Date(1980, 4, 31)),
customLabel: 'Wednesday, 31 April 1980',
isoLabel: '1980-04-31',
},
{
val: startOfDay(new Date(1980, 5, 1)),
customLabel: 'Thursday, 1 May 1980',
isoLabel: '1980-05-01',
},
{
val: startOfDay(new Date(1980, 5, 2)),
customLabel: 'Friday, 2 May 1980',
isoLabel: '1980-05-02',
},
{
val: startOfDay(new Date(1980, 5, 3)),
customLabel: 'Saturday, 3 May 1980',
isoLabel: '1980-05-03',
},
{
val: startOfDay(new Date(1980, 5, 4)),
customLabel: 'Sunday, 4 May 1980',
isoLabel: '1980-05-04',
},
{
val: startOfDay(new Date(1980, 5, 5)),
customLabel: 'Monday, 5 May 1980',
isoLabel: '1980-05-05',
},
{
val: startOfDay(new Date(1980, 5, 6)),
customLabel: 'Tuesday, 6 May 1980',
isoLabel: '1980-05-06',
},
],
formatDateFull: (d: Date) => d.toString(),
preventKeyboardFocus: false,
markToday: true,
Expand Down
48 changes: 21 additions & 27 deletions packages/bpk-component-calendar/src/BpkCalendarWeek.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@ import {
endOfMonth,
} from './date-utils';

import type { DateProps } from './BpkCalendarGrid';
import type {
DateModifiers,
SelectionConfiguration,
SelectionConfigurationSingle,
SelectionConfigurationRange,
} from './custom-proptypes';


import STYLES from './BpkCalendarWeek.module.scss';

const getClassName = cssModules(STYLES);
Expand Down Expand Up @@ -103,7 +103,6 @@ function getSelectedDate(
* Gets the correct selection type for the current date
* @param {Date} date the current date of the calendar
* @param {Object} selectionConfiguration the current selection configuration
* @param {Function} formatDateFull function to format dates
* @param {Date} month the current month of the calendar
* @param {Number} weekStartsOn index of the first day of the week
* @param {Boolean} ignoreOutsideDate ignore date outside current month
Expand All @@ -112,7 +111,6 @@ function getSelectedDate(
function getSelectionType(
date: Date,
selectionConfiguration: SelectionConfiguration,
formatDateFull: (d: Date) => Date | string,
month: Date,
weekStartsOn: 0 | 1 | 2 | 3 | 4 | 5 | 6,
ignoreOutsideDate: boolean,
Expand All @@ -130,8 +128,7 @@ function getSelectionType(
if (
selectionConfiguration.type === CALENDAR_SELECTION_TYPE.single &&
selectionConfiguration.date &&
(selectionConfiguration.date === formatDateFull(date) ||
formatDateFull(selectionConfiguration.date) === formatDateFull(date))
isSameDay(date, selectionConfiguration.date)
) {
return SELECTION_TYPES.single;
}
Expand Down Expand Up @@ -171,10 +168,10 @@ function getSelectionType(
) {
return SELECTION_TYPES.middle;
}
if (startDate && formatDateFull(startDate) === formatDateFull(date)) {
if (sameStartDay) {
return SELECTION_TYPES.start;
}
if (endDate && formatDateFull(endDate) === formatDateFull(date)) {
if (sameEndDay) {
return SELECTION_TYPES.end;
}
}
Expand All @@ -196,11 +193,11 @@ const singleDateHandler = (props: Props, nextProps: Props) => {

if (
((nextSelectConfig.date &&
isSameWeek(nextSelectConfig.date, nextProps.dates[0], {
isSameWeek(nextSelectConfig.date, nextProps.dates[0].val, {
weekStartsOn: nextProps.weekStartsOn,
})) ||
(currentSelectConfig.date &&
isSameWeek(currentSelectConfig.date, props.dates[0], {
isSameWeek(currentSelectConfig.date, props.dates[0].val, {
weekStartsOn: props.weekStartsOn,
}))) &&
currentSelectConfig.date !== nextSelectConfig.date
Expand Down Expand Up @@ -237,8 +234,7 @@ const rangeDateHandler = (props: Props, nextProps: Props) => {
export type Props = DefaultProps & {
DateComponent: ElementType;
dateModifiers: DateModifiers;
dates: Date[];
formatDateFull: (date: Date) => Date | string;
dates: DateProps[];
preventKeyboardFocus: boolean;
markToday: boolean;
markOutsideDays: boolean;
Expand Down Expand Up @@ -283,7 +279,6 @@ class BpkCalendarWeek extends Component<Props> {
const shallowProps = [
'DateComponent',
'dateModifiers',
'formatDateFull',
'isKeyboardFocusable',
'markOutsideDays',
'markToday',
Expand All @@ -305,11 +300,11 @@ class BpkCalendarWeek extends Component<Props> {
// we'll render, component should update.
if (
((nextProps.focusedDate &&
isSameWeek(nextProps.focusedDate, nextProps.dates[0], {
isSameWeek(nextProps.focusedDate, nextProps.dates[0].val, {
weekStartsOn: nextProps.weekStartsOn,
})) ||
(this.props.focusedDate &&
isSameWeek(this.props.focusedDate, this.props.dates[0], {
isSameWeek(this.props.focusedDate, this.props.dates[0].val, {
weekStartsOn: this.props.weekStartsOn,
}))) &&
this.props.focusedDate !== nextProps.focusedDate
Expand Down Expand Up @@ -363,7 +358,6 @@ class BpkCalendarWeek extends Component<Props> {
dateModifiers,
dateProps,
focusedDate,
formatDateFull,
ignoreOutsideDate,
isKeyboardFocusable,
markOutsideDays,
Expand All @@ -380,7 +374,7 @@ class BpkCalendarWeek extends Component<Props> {

if (ignoreOutsideDate) {
const daysOutside = this.props.dates.map((date) =>
isSameMonth(date, month),
isSameMonth(date.val, month),
);

const shouldRender = daysOutside.reduce(or);
Expand All @@ -395,13 +389,12 @@ class BpkCalendarWeek extends Component<Props> {
{this.props.dates.map((date) => {
const isBlocked =
minDate && maxDate
? !isWithinRange(date, { start: minDate, end: maxDate })
? !isWithinRange(date.val, { start: minDate, end: maxDate })
: false;

const dateSelectionType = getSelectionType(
date,
date.val,
selectionConfiguration!,
formatDateFull,
month,
weekStartsOn,
ignoreOutsideDate!,
Expand All @@ -410,25 +403,26 @@ class BpkCalendarWeek extends Component<Props> {
return (
<DateContainer
className={cellClassName}
isEmptyCell={!isSameMonth(date, month) && ignoreOutsideDate!}
isEmptyCell={!isSameMonth(date.val, month) && ignoreOutsideDate!}
isBlocked={isBlocked}
key={date.getDate()}
key={date.val.getDate()}
selectionType={dateSelectionType}
>
<DateComponent
date={date}
date={date.val}
modifiers={dateModifiers}
aria-label={formatDateFull(date)}
aria-label={date.customLabel}
onClick={onDateClick}
onDateKeyDown={onDateKeyDown}
preventKeyboardFocus={preventKeyboardFocus}
isKeyboardFocusable={isKeyboardFocusable}
isFocused={focusedDate && isSameDay(date, focusedDate)}
isSelected={getSelectedDate(date, selectionConfiguration!)}
isFocused={focusedDate && isSameDay(date.val, focusedDate)}
isSelected={getSelectedDate(date.val, selectionConfiguration!)}
isBlocked={isBlocked}
isOutside={markOutsideDays && !isSameMonth(date, month)}
isToday={markToday && isToday(date)}
isOutside={markOutsideDays && !isSameMonth(date.val, month)}
isToday={markToday && isToday(date.val)}
selectionType={dateSelectionType}
isoLabel={date.isoLabel}
{...dateProps}
/>
</DateContainer>
Expand Down
2 changes: 1 addition & 1 deletion packages/bpk-component-calendar/src/composeCalendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ const composeCalendar = (
DateComponent={CalendarDate}
dateModifiers={dateModifiers}
daysOfWeek={daysOfWeek}
formatDateFull={formatDateFull}
formatDateFull={memoize(formatDateFull)}
formatMonth={memoize(formatMonth)}
month={month}
onDateClick={onDateClick}
Expand Down
Loading

0 comments on commit 4dcb150

Please sign in to comment.