Skip to content

Commit ceabbf5

Browse files
flaviendelanglebernardobelchiorLukasTy
authored
[scheduler][primitives] Bootstrap the Time Grid component (mui#17487)
Signed-off-by: Flavien DELANGLE <flaviendelangle@gmail.com> Co-authored-by: Bernardo Belchior <Bernardo.belchior1@gmail.com> Co-authored-by: Lukas Tyla <llukas.tyla@gmail.com>
1 parent b313e5f commit ceabbf5

39 files changed

Lines changed: 2597 additions & 4 deletions

.eslintrc.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,17 @@ module.exports = {
313313
'no-console': 'off',
314314
},
315315
},
316+
{
317+
files: ['packages/x-scheduler/**/*{.tsx,.ts,.js}'],
318+
rules: {
319+
// Base UI lint rules
320+
'@typescript-eslint/no-redeclare': 'off',
321+
'import/export': 'off',
322+
'material-ui/straight-quotes': 'off',
323+
'jsdoc/require-param': 'off',
324+
'jsdoc/require-returns': 'off',
325+
},
326+
},
316327
...buildPackageRestrictedImports('@mui/x-charts', 'x-charts', false),
317328
...buildPackageRestrictedImports('@mui/x-charts-pro', 'x-charts-pro', false),
318329
...buildPackageRestrictedImports('@mui/x-codemod', 'x-codemod', false),
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import * as React from 'react';
2+
import { DateTime } from 'luxon';
3+
import { TimeGrid } from '@mui/x-scheduler/primitives/time-grid';
4+
import classes from './TimeGridPrimitives.module.css';
5+
6+
const startOfWeek = DateTime.now().startOf('week');
7+
const createDate = (weekday, hour, minute) => {
8+
return startOfWeek.set({ weekday, hour, minute });
9+
};
10+
11+
const days = [
12+
{
13+
date: createDate(1, 0, 0),
14+
events: [
15+
{
16+
id: '1',
17+
start: createDate(1, 7, 30),
18+
end: createDate(1, 8, 15),
19+
title: 'Footing',
20+
agenda: 'personal',
21+
},
22+
{
23+
id: '2',
24+
start: createDate(1, 16, 0),
25+
end: createDate(1, 17, 0),
26+
title: 'Weekly',
27+
agenda: 'work',
28+
},
29+
],
30+
},
31+
{
32+
date: createDate(2, 0, 0),
33+
events: [
34+
{
35+
id: '3',
36+
start: createDate(2, 10, 0),
37+
end: createDate(2, 11, 0),
38+
title: 'Backlog grooming',
39+
agenda: 'work',
40+
},
41+
{
42+
id: '4',
43+
start: createDate(2, 19, 0),
44+
end: createDate(2, 22, 0),
45+
title: 'Pizza party',
46+
agenda: 'personal',
47+
},
48+
],
49+
},
50+
{
51+
date: createDate(3, 0, 0),
52+
events: [
53+
{
54+
id: '5',
55+
start: createDate(3, 8, 0),
56+
end: createDate(3, 17, 0),
57+
title: 'Scheduler deep dive',
58+
agenda: 'work',
59+
},
60+
],
61+
},
62+
{
63+
date: createDate(4, 0, 0),
64+
events: [
65+
{
66+
id: '1',
67+
start: createDate(4, 7, 30),
68+
end: createDate(4, 8, 15),
69+
title: 'Footing',
70+
agenda: 'personal',
71+
},
72+
],
73+
},
74+
{
75+
date: createDate(5, 0, 0),
76+
events: [
77+
{
78+
id: '1',
79+
start: createDate(5, 15, 0),
80+
end: createDate(5, 15, 45),
81+
title: 'Retrospective',
82+
agenda: 'work',
83+
},
84+
],
85+
},
86+
];
87+
88+
export default function TimeGridPrimitives() {
89+
const { scrollableRef, scrollerRef } = useInitialScrollPosition();
90+
91+
return (
92+
<div className={classes.Container}>
93+
<TimeGrid.Root className={classes.Root}>
94+
<div className={classes.Header}>
95+
<div className={classes.TimeAxisHeaderCell} aria-hidden="true" />
96+
{days.map((day) => (
97+
<div key={day.date.toString()} className={classes.HeaderCell}>
98+
{day.date.toFormat('EEE, dd')}
99+
</div>
100+
))}
101+
</div>
102+
<div className={classes.Body} ref={scrollerRef}>
103+
<div className={classes.ScrollableContent} ref={scrollableRef} role="row">
104+
<div className={classes.TimeAxis} aria-hidden="true">
105+
{Array.from({ length: 24 }, (_, hour) => (
106+
<div
107+
key={hour}
108+
className={classes.TimeAxisCell}
109+
style={{ '--hour': hour }}
110+
>
111+
{hour === 0
112+
? null
113+
: `${DateTime.now().set({ hour }).toFormat('hh a')}`}
114+
</div>
115+
))}
116+
</div>
117+
{days.map((day) => (
118+
<TimeGrid.Column
119+
key={day.date.toString()}
120+
value={day.date}
121+
className={classes.Column}
122+
>
123+
{day.events.map((event) => (
124+
<TimeGrid.Event
125+
key={event.id}
126+
start={event.start}
127+
end={event.end}
128+
data-agenda={event.agenda}
129+
className={classes.Event}
130+
>
131+
<div className={classes.EventInformation}>
132+
<div className={classes.EventStartTime}>
133+
{event.start.toFormat('hh a')}
134+
</div>
135+
<div className={classes.EventTitle}>{event.title}</div>
136+
</div>
137+
</TimeGrid.Event>
138+
))}
139+
</TimeGrid.Column>
140+
))}
141+
</div>
142+
</div>
143+
</TimeGrid.Root>
144+
</div>
145+
);
146+
}
147+
148+
function useInitialScrollPosition() {
149+
// TODO: Should the automatic scrolling be built-in?
150+
const scrollableRef = React.useRef(null);
151+
const scrollerRef = React.useRef(null);
152+
153+
React.useLayoutEffect(() => {
154+
if (!scrollableRef.current || !scrollerRef.current) {
155+
return;
156+
}
157+
158+
let earliestStart = null;
159+
for (const day of days) {
160+
for (const event of day.events) {
161+
const startMinute = event.start.hour * 60 + event.start.minute;
162+
163+
if (event.start < day.date.startOf('day')) {
164+
earliestStart = 0;
165+
} else if (earliestStart == null || startMinute < earliestStart) {
166+
earliestStart = startMinute;
167+
}
168+
}
169+
}
170+
171+
if (earliestStart == null) {
172+
return;
173+
}
174+
175+
const clientHeight = scrollableRef.current.clientHeight;
176+
177+
const earliestStartPx = earliestStart * (clientHeight / (24 * 60)) - 24;
178+
scrollerRef.current.scrollTop = earliestStartPx;
179+
}, []);
180+
181+
return { scrollableRef, scrollerRef };
182+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
.Container {
2+
--border-color: oklch(87.1% 0.006 286.286);
3+
--event-work-bg-color: oklch(82.8% 0.111 230.318);
4+
--event-personal-bg-color: oklch(81% 0.117 11.638);
5+
}
6+
7+
:global(.mode-dark) .Container {
8+
--border-color: oklch(44.2% 0.017 285.786);
9+
--event-work-bg-color: oklch(50% 0.134 242.749);
10+
--event-personal-bg-color: oklch(45.5% 0.188 13.697);
11+
}
12+
13+
.Container {
14+
border: 1px solid var(--border-color);
15+
border-radius: 4px;
16+
overflow: hidden;
17+
}
18+
19+
.Root {
20+
display: flex;
21+
flex-direction: column;
22+
}
23+
24+
.Header {
25+
display: flex;
26+
}
27+
28+
.HeaderCell {
29+
width: 156px;
30+
border-inline: 1px solid var(--border-color);
31+
border-bottom: 1px solid var(--border-color);
32+
padding: 4px 8px;
33+
margin-bottom: -1px;
34+
}
35+
36+
.Body {
37+
flex: 1;
38+
max-height: 600px;
39+
height: 600px;
40+
overflow-y: auto;
41+
}
42+
43+
.ScrollableContent {
44+
display: flex;
45+
height: 1000px;
46+
position: relative;
47+
}
48+
49+
.Column {
50+
width: 156px;
51+
border-inline-start: 1px solid var(--border-color);
52+
position: relative;
53+
}
54+
55+
.Event {
56+
position: absolute;
57+
left: 0px;
58+
right: 8px;
59+
top: var(--y-position);
60+
bottom: calc(100% - var(--y-position) - var(--height));
61+
62+
border-radius: 4px;
63+
padding: 4px;
64+
font-size: 12px;
65+
background-color: var(--event-bg-color);
66+
67+
&[data-agenda='work'] {
68+
--event-bg-color: var(--event-work-bg-color);
69+
}
70+
71+
&[data-agenda='personal'] {
72+
--event-bg-color: var(--event-personal-bg-color);
73+
}
74+
75+
&[data-ended] {
76+
background-color: color-mix(in oklab, var(--event-bg-color) 50%, #222 50%);
77+
color: color-mix(in oklab, currentColor 80%, #222 20%);
78+
}
79+
}
80+
81+
.EventInformation {
82+
display: flex;
83+
flex-wrap: wrap;
84+
align-items: baseline;
85+
gap: 4px;
86+
}
87+
88+
.EventTitle {
89+
font-weight: bold;
90+
text-wrap: nowrap;
91+
}
92+
93+
.EventStartTime {
94+
font-size: 10px;
95+
color: var(--text-color);
96+
text-wrap: nowrap;
97+
}
98+
99+
.TimeAxis {
100+
display: flex;
101+
flex-direction: column;
102+
}
103+
104+
.TimeAxisCell {
105+
height: calc(100% / 24);
106+
line-height: calc(100% / 24);
107+
font-size: 12px;
108+
padding-inline-start: 4px;
109+
110+
&::after {
111+
content: '';
112+
border-bottom: 1px solid var(--border-color);
113+
position: absolute;
114+
right: 0;
115+
left: 48px;
116+
top: calc((100% / 24) * var(--hour));
117+
}
118+
}
119+
120+
.TimeAxisHeaderCell,
121+
.TimeAxisCell {
122+
width: 60px;
123+
}

0 commit comments

Comments
 (0)