Skip to content

Commit 3f5886c

Browse files
[scheduler] Add header parts to the Calendar Grid primitive (#19854)
Signed-off-by: Flavien DELANGLE <[email protected]> Co-authored-by: Rita <[email protected]>
1 parent 2d9b5e6 commit 3f5886c

32 files changed

+379
-110
lines changed

packages/x-scheduler-headless/src/calendar-grid/day-cell/CalendarGridDayCell.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
import * as React from 'react';
33
import { useRenderElement } from '../../base-ui-copy/utils/useRenderElement';
44
import { BaseUIComponentProps } from '../../base-ui-copy/utils/types';
5+
import { useCompositeListItem } from '../../base-ui-copy/composite/list/useCompositeListItem';
56
import { useDayCellDropTarget } from './useDayCellDropTarget';
7+
import { CalendarGridDayCellContext } from './CalendarGridDayCellContext';
68

79
export const CalendarGridDayCell = React.forwardRef(function CalendarGridDayCell(
810
componentProps: CalendarGridDayCell.Props,
@@ -19,13 +21,28 @@ export const CalendarGridDayCell = React.forwardRef(function CalendarGridDayCell
1921
...elementProps
2022
} = componentProps;
2123

24+
const { ref: listItemRef, index } = useCompositeListItem();
2225
const dropTargetRef = useDayCellDropTarget({ value, addPropertiesToDroppedEvent });
26+
2327
const props = React.useMemo(() => ({ role: 'gridcell' }), []);
2428

25-
return useRenderElement('div', componentProps, {
26-
ref: [forwardedRef, dropTargetRef],
29+
const contextValue: CalendarGridDayCellContext = React.useMemo(
30+
() => ({
31+
index,
32+
}),
33+
[index],
34+
);
35+
36+
const element = useRenderElement('div', componentProps, {
37+
ref: [forwardedRef, dropTargetRef, listItemRef],
2738
props: [props, elementProps],
2839
});
40+
41+
return (
42+
<CalendarGridDayCellContext.Provider value={contextValue}>
43+
{element}
44+
</CalendarGridDayCellContext.Provider>
45+
);
2946
});
3047

3148
export namespace CalendarGridDayCell {

packages/x-scheduler-headless/src/calendar-grid/day-cell/CalendarGridDayCellContext.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
'use client';
22
import * as React from 'react';
33

4-
export interface CalendarGridDayCellContext {}
4+
export interface CalendarGridDayCellContext {
5+
/**
6+
* The index of the cell in the row.
7+
*/
8+
index: number;
9+
}
510

611
export const CalendarGridDayCellContext = React.createContext<
712
CalendarGridDayCellContext | undefined

packages/x-scheduler-headless/src/calendar-grid/day-event/CalendarGridDayEvent.tsx

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
44
import { disableNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/disable-native-drag-preview';
55
import { useEventCallback } from '@base-ui-components/utils/useEventCallback';
66
import { useStore } from '@base-ui-components/utils/store/useStore';
7+
import { useId } from '@base-ui-components/utils/useId';
78
import { useButton } from '../../base-ui-copy/utils/useButton';
89
import { useRenderElement } from '../../base-ui-copy/utils/useRenderElement';
910
import { BaseUIComponentProps, NonNativeButtonProps } from '../../base-ui-copy/utils/types';
@@ -12,10 +13,13 @@ import { CalendarEvent, CalendarEventId, SchedulerValidDate } from '../../models
1213
import { useAdapter, diffIn } from '../../use-adapter';
1314
import { useCalendarGridDayRowContext } from '../day-row/CalendarGridDayRowContext';
1415
import { selectors } from '../../use-event-calendar/EventCalendarStore.selectors';
16+
import { getCalendarGridHeaderCellId } from '../../utils/accessibility-utils';
1517
import { CalendarGridDayEventContext } from './CalendarGridDayEventContext';
1618
import { useEventCalendarStoreContext } from '../../use-event-calendar-store-context';
19+
import { useCalendarGridDayCellContext } from '../day-cell/CalendarGridDayCellContext';
20+
import { useCalendarGridRootContext } from '../root/CalendarGridRootContext';
1721

18-
const EVENT_PROPS_WHILE_DRAGGING = { style: { pointerEvents: 'none' as const } };
22+
const EVENT_STYLE_WHILE_DRAGGING = { pointerEvents: 'none' as const };
1923

2024
export const CalendarGridDayEvent = React.forwardRef(function CalendarGridDayEvent(
2125
componentProps: CalendarGridDayEvent.Props,
@@ -30,6 +34,7 @@ export const CalendarGridDayEvent = React.forwardRef(function CalendarGridDayEve
3034
end,
3135
eventId,
3236
occurrenceKey,
37+
id: idProp,
3338
isDraggable = false,
3439
nativeButton = false,
3540
// Props forwarded to the DOM element
@@ -41,19 +46,32 @@ export const CalendarGridDayEvent = React.forwardRef(function CalendarGridDayEve
4146
const isInteractive = true;
4247

4348
const adapter = useAdapter();
49+
const store = useEventCalendarStoreContext();
50+
const { id: rootId } = useCalendarGridRootContext();
51+
const { start: rowStart, end: rowEnd } = useCalendarGridDayRowContext();
52+
const { index: cellIndex } = useCalendarGridDayCellContext();
53+
4454
const ref = React.useRef<HTMLDivElement>(null);
4555
const { getButtonProps, buttonRef } = useButton({
4656
disabled: !isInteractive,
4757
native: nativeButton,
4858
});
49-
const { start: rowStart, end: rowEnd } = useCalendarGridDayRowContext();
5059
const { state: eventState } = useEvent({ start, end });
51-
const store = useEventCalendarStoreContext();
5260
const hasPlaceholder = useStore(store, selectors.hasOccurrencePlaceholder);
5361
const isDragging = useStore(store, selectors.isOccurrenceMatchingThePlaceholder, occurrenceKey);
5462
const [isResizing, setIsResizing] = React.useState(false);
63+
const id = useId(idProp);
5564

56-
const props = hasPlaceholder ? EVENT_PROPS_WHILE_DRAGGING : undefined;
65+
const columnHeaderId = getCalendarGridHeaderCellId(rootId, cellIndex);
66+
67+
const props = React.useMemo(
68+
() => ({
69+
id,
70+
'aria-labelledby': `${columnHeaderId} ${id}`,
71+
style: hasPlaceholder ? EVENT_STYLE_WHILE_DRAGGING : undefined,
72+
}),
73+
[hasPlaceholder, columnHeaderId, id],
74+
);
5775

5876
const state: CalendarGridDayEvent.State = React.useMemo(
5977
() => ({ ...eventState, dragging: isDragging, resizing: isResizing }),

packages/x-scheduler-headless/src/calendar-grid/day-row/CalendarGridDayRow.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client';
22
import * as React from 'react';
33
import { useRenderElement } from '../../base-ui-copy/utils/useRenderElement';
4+
import { CompositeList } from '../../base-ui-copy/composite/list/CompositeList';
45
import { BaseUIComponentProps } from '../../base-ui-copy/utils/types';
56
import { CalendarGridDayRowContext } from './CalendarGridDayRowContext';
67
import { SchedulerValidDate } from '../../models';
@@ -21,6 +22,7 @@ export const CalendarGridDayRow = React.forwardRef(function CalendarGridDayRow(
2122
} = componentProps;
2223

2324
const props = React.useMemo(() => ({ role: 'row' }), []);
25+
const cellsRefs = React.useRef<(HTMLDivElement | null)[]>([]);
2426

2527
const contextValue: CalendarGridDayRowContext = React.useMemo(
2628
() => ({
@@ -36,9 +38,11 @@ export const CalendarGridDayRow = React.forwardRef(function CalendarGridDayRow(
3638
});
3739

3840
return (
39-
<CalendarGridDayRowContext.Provider value={contextValue}>
40-
{element}
41-
</CalendarGridDayRowContext.Provider>
41+
<CompositeList elementsRef={cellsRefs}>
42+
<CalendarGridDayRowContext.Provider value={contextValue}>
43+
{element}
44+
</CalendarGridDayRowContext.Provider>
45+
</CompositeList>
4246
);
4347
});
4448

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import * as React from 'react';
2+
import { CalendarGrid } from '@mui/x-scheduler-headless/calendar-grid';
3+
import { adapter, createSchedulerRenderer, describeConformance } from 'test/utils/scheduler';
4+
import { EventCalendarProvider } from '@mui/x-scheduler-headless/event-calendar-provider';
5+
import { processDate } from '@mui/x-scheduler-headless/process-date';
6+
7+
describe('<CalendarGrid.HeaderCell />', () => {
8+
const { render } = createSchedulerRenderer();
9+
10+
describeConformance(
11+
<CalendarGrid.HeaderCell date={processDate(adapter.date(), adapter)} />,
12+
() => ({
13+
refInstanceof: window.HTMLDivElement,
14+
render(node) {
15+
return render(
16+
<EventCalendarProvider events={[]}>
17+
<CalendarGrid.Root>
18+
<CalendarGrid.HeaderRow>{node}</CalendarGrid.HeaderRow>
19+
</CalendarGrid.Root>
20+
</EventCalendarProvider>,
21+
);
22+
},
23+
}),
24+
);
25+
});
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
'use client';
2+
import * as React from 'react';
3+
import { useStore } from '@base-ui-components/utils/store';
4+
import { useRenderElement } from '../../base-ui-copy/utils/useRenderElement';
5+
import { BaseUIComponentProps } from '../../base-ui-copy/utils/types';
6+
import { useCompositeListItem } from '../../base-ui-copy/composite/list/useCompositeListItem';
7+
import { useAdapter } from '../../use-adapter';
8+
import { useEventCalendarStoreContext } from '../../use-event-calendar-store-context';
9+
import { CalendarProcessedDate } from '../../models';
10+
import { getCalendarGridHeaderCellId } from '../../utils/accessibility-utils';
11+
import { useCalendarGridRootContext } from '../root/CalendarGridRootContext';
12+
import { selectors } from '../../utils/SchedulerStore';
13+
14+
export const CalendarGridHeaderCell = React.forwardRef(function CalendarGridHeaderCell(
15+
componentProps: CalendarGridHeaderCell.Props,
16+
forwardedRef: React.ForwardedRef<HTMLDivElement>,
17+
) {
18+
const adapter = useAdapter();
19+
20+
const {
21+
// Rendering props
22+
className,
23+
render,
24+
// Internal props
25+
date,
26+
skipDataCurrent,
27+
ariaLabelFormat = adapter.formats.weekday,
28+
// Props forwarded to the DOM element
29+
...elementProps
30+
} = componentProps;
31+
32+
const store = useEventCalendarStoreContext();
33+
const { id: rootId } = useCalendarGridRootContext();
34+
const isCurrentDay = useStore(
35+
store,
36+
skipDataCurrent ? () => false : selectors.isCurrentDay,
37+
date.value,
38+
);
39+
40+
const { ref: listItemRef, index } = useCompositeListItem();
41+
const id = getCalendarGridHeaderCellId(rootId, index);
42+
43+
const props = React.useMemo(
44+
() => ({
45+
role: 'columnheader',
46+
id,
47+
'aria-label': `${adapter.formatByString(date.value, ariaLabelFormat)}`,
48+
}),
49+
[adapter, date, id, ariaLabelFormat],
50+
);
51+
52+
const state: CalendarGridHeaderCell.State = React.useMemo(
53+
() => ({
54+
current: isCurrentDay,
55+
}),
56+
[isCurrentDay],
57+
);
58+
59+
return useRenderElement('div', componentProps, {
60+
state,
61+
ref: [forwardedRef, listItemRef],
62+
props: [props, elementProps],
63+
});
64+
});
65+
66+
export namespace CalendarGridHeaderCell {
67+
export interface State {
68+
/**
69+
* Whether the header cell represents the current day.
70+
*/
71+
current: boolean;
72+
}
73+
74+
export interface Props extends BaseUIComponentProps<'div', State> {
75+
/**
76+
* The date of the events rendered in the same column as this header cell.
77+
*/
78+
date: CalendarProcessedDate;
79+
/**
80+
* The format used for the `aria-label` attribute.
81+
* @default adapter.formats.weekday
82+
*/
83+
ariaLabelFormat?: string;
84+
/**
85+
* Whether to skip adding the `data-current` attribute to the root element when the header cell represents the current day.
86+
* This can be useful when the cells in the column are not representing a single day (e.g. in the month view).
87+
* @default false
88+
*/
89+
skipDataCurrent?: boolean;
90+
}
91+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export enum CalendarGridHeaderCellDataAttributes {
2+
/**
3+
* Present when the header cell represents the current date.
4+
*/
5+
current = 'data-current',
6+
}
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import * as React from 'react';
22
import { CalendarGrid } from '@mui/x-scheduler-headless/calendar-grid';
3-
import { EventCalendarProvider } from '@mui/x-scheduler-headless/event-calendar-provider';
43
import { createSchedulerRenderer, describeConformance } from 'test/utils/scheduler';
4+
import { EventCalendarProvider } from '@mui/x-scheduler-headless/event-calendar-provider';
55

6-
describe('<CalendarGrid.ScrollableContent />', () => {
6+
describe('<CalendarGrid.HeaderRow />', () => {
77
const { render } = createSchedulerRenderer();
88

9-
describeConformance(<CalendarGrid.ScrollableContent />, () => ({
9+
describeConformance(<CalendarGrid.HeaderRow />, () => ({
1010
refInstanceof: window.HTMLDivElement,
1111
render(node) {
1212
return render(
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
'use client';
2+
import * as React from 'react';
3+
import { useRenderElement } from '../../base-ui-copy/utils/useRenderElement';
4+
import { BaseUIComponentProps } from '../../base-ui-copy/utils/types';
5+
import { CompositeList } from '../../base-ui-copy/composite/list/CompositeList';
6+
7+
export const CalendarGridHeaderRow = React.forwardRef(function CalendarGridHeaderRow(
8+
componentProps: CalendarGridHeaderRow.Props,
9+
forwardedRef: React.ForwardedRef<HTMLDivElement>,
10+
) {
11+
const {
12+
// Rendering props
13+
className,
14+
render,
15+
// Props forwarded to the DOM element
16+
...elementProps
17+
} = componentProps;
18+
19+
const props = React.useMemo(() => ({ role: 'row' }), []);
20+
const cellsRefs = React.useRef<(HTMLDivElement | null)[]>([]);
21+
22+
const element = useRenderElement('div', componentProps, {
23+
ref: [forwardedRef],
24+
props: [props, elementProps],
25+
});
26+
27+
return <CompositeList elementsRef={cellsRefs}>{element}</CompositeList>;
28+
});
29+
30+
export namespace CalendarGridHeaderRow {
31+
export interface State {}
32+
33+
export interface Props extends BaseUIComponentProps<'div', State> {}
34+
}

packages/x-scheduler-headless/src/calendar-grid/index.parts.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
export { CalendarGridRoot as Root } from './root/CalendarGridRoot';
2-
export { CalendarGridScrollableContent as ScrollableContent } from './scrollable-content/CalendarGridScrollableContent';
2+
3+
export { CalendarGridHeaderRow as HeaderRow } from './header-row/CalendarGridHeaderRow';
4+
export { CalendarGridHeaderCell as HeaderCell } from './header-cell/CalendarGridHeaderCell';
35

46
export { CalendarGridDayRow as DayRow } from './day-row/CalendarGridDayRow';
57
export { CalendarGridDayCell as DayCell } from './day-cell/CalendarGridDayCell';
68
export { CalendarGridDayEvent as DayEvent } from './day-event/CalendarGridDayEvent';
79
export { CalendarGridDayEventPlaceholder as DayEventPlaceholder } from './day-event-placeholder/CalendarGridDayEventPlaceholder';
810
export { CalendarGridDayEventResizeHandler as DayEventResizeHandler } from './day-event-resize-handler/CalendarGridDayEventResizeHandler';
911

12+
export { CalendarGridTimeScrollableContent as TimeScrollableContent } from './time-scrollable-content/CalendarGridTimeScrollableContent';
1013
export { CalendarGridTimeColumn as TimeColumn } from './time-column/CalendarGridTimeColumn';
1114
export { CalendarGridTimeEvent as TimeEvent } from './time-event/CalendarGridTimeEvent';
1215
export { CalendarGridTimeEventPlaceholder as TimeEventPlaceholder } from './time-event-placeholder/CalendarGridTimeEventPlaceholder';

0 commit comments

Comments
 (0)