@gtivr4/a1-design-system-react 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -42,6 +42,12 @@ export function Calendar({
|
|
|
42
42
|
dimPast = true,
|
|
43
43
|
variant = "scroll",
|
|
44
44
|
todayButton = false,
|
|
45
|
+
selectable = false,
|
|
46
|
+
selectedDate,
|
|
47
|
+
defaultSelectedDate,
|
|
48
|
+
onChange,
|
|
49
|
+
minDate,
|
|
50
|
+
maxDate,
|
|
45
51
|
className = "",
|
|
46
52
|
...props
|
|
47
53
|
}) {
|
|
@@ -63,6 +69,35 @@ export function Calendar({
|
|
|
63
69
|
const [viewMonth, setViewMonth] = useState(centerMonth);
|
|
64
70
|
const currentMonthRef = useRef(null);
|
|
65
71
|
|
|
72
|
+
// Selection — controlled when selectedDate is provided, otherwise internal state
|
|
73
|
+
const isControlled = selectedDate !== undefined;
|
|
74
|
+
const [internalSelected, setInternalSelected] = useState(defaultSelectedDate ?? null);
|
|
75
|
+
const selected = isControlled ? selectedDate : internalSelected;
|
|
76
|
+
|
|
77
|
+
function isSameDay(d, y, m, day) {
|
|
78
|
+
return d instanceof Date && d.getFullYear() === y && d.getMonth() === m && d.getDate() === day;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isDayDisabled(y, m, day) {
|
|
82
|
+
const date = new Date(y, m, day);
|
|
83
|
+
if (minDate) {
|
|
84
|
+
const min = new Date(minDate.getFullYear(), minDate.getMonth(), minDate.getDate());
|
|
85
|
+
if (date < min) return true;
|
|
86
|
+
}
|
|
87
|
+
if (maxDate) {
|
|
88
|
+
const max = new Date(maxDate.getFullYear(), maxDate.getMonth(), maxDate.getDate());
|
|
89
|
+
if (date > max) return true;
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function handleDayClick(y, m, day) {
|
|
95
|
+
if (isDayDisabled(y, m, day)) return;
|
|
96
|
+
const date = new Date(y, m, day);
|
|
97
|
+
if (!isControlled) setInternalSelected(date);
|
|
98
|
+
onChange?.(date);
|
|
99
|
+
}
|
|
100
|
+
|
|
66
101
|
// ── Localised strings ─────────────────────────────────────────
|
|
67
102
|
|
|
68
103
|
// Month names
|
|
@@ -210,15 +245,30 @@ export function Calendar({
|
|
|
210
245
|
dimPast &&
|
|
211
246
|
new Date(year, month, day) < new Date(todayYear, todayMonth, todayDay);
|
|
212
247
|
|
|
248
|
+
const isSelected = isSameDay(selected, year, month, day);
|
|
249
|
+
const isDisabled = isDayDisabled(year, month, day);
|
|
250
|
+
|
|
213
251
|
return (
|
|
214
252
|
<td
|
|
215
253
|
key={di}
|
|
216
254
|
className={[
|
|
217
255
|
"a1-calendar__day",
|
|
218
|
-
isToday
|
|
219
|
-
isPast
|
|
256
|
+
isToday && "a1-calendar__day--today",
|
|
257
|
+
isPast && "a1-calendar__day--past",
|
|
258
|
+
isSelected && "a1-calendar__day--selected",
|
|
259
|
+
isDisabled && "a1-calendar__day--disabled",
|
|
220
260
|
].filter(Boolean).join(" ")}
|
|
221
261
|
aria-current={isToday ? "date" : undefined}
|
|
262
|
+
aria-selected={isSelected ? true : undefined}
|
|
263
|
+
aria-disabled={isDisabled ? true : undefined}
|
|
264
|
+
tabIndex={selectable && !isDisabled ? 0 : undefined}
|
|
265
|
+
onClick={selectable && !isDisabled ? () => handleDayClick(year, month, day) : undefined}
|
|
266
|
+
onKeyDown={selectable && !isDisabled ? (e) => {
|
|
267
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
268
|
+
e.preventDefault();
|
|
269
|
+
handleDayClick(year, month, day);
|
|
270
|
+
}
|
|
271
|
+
} : undefined}
|
|
222
272
|
>
|
|
223
273
|
<span className="a1-calendar__day-number" aria-hidden="true">
|
|
224
274
|
{day}
|
|
@@ -235,17 +285,27 @@ export function Calendar({
|
|
|
235
285
|
|
|
236
286
|
// ── Paginated variant ─────────────────────────────────────────
|
|
237
287
|
if (variant === "paginated") {
|
|
238
|
-
const
|
|
239
|
-
const
|
|
288
|
+
const minYear = minDate ? minDate.getFullYear() : todayYear - 10;
|
|
289
|
+
const minMon = minDate ? minDate.getMonth() : 0;
|
|
290
|
+
const maxYear = maxDate ? maxDate.getFullYear() : todayYear + 15;
|
|
291
|
+
const maxMon = maxDate ? maxDate.getMonth() : 11;
|
|
292
|
+
|
|
293
|
+
const yearStart = minDate ? minDate.getFullYear() : todayYear - 10;
|
|
294
|
+
const yearEnd = maxDate ? maxDate.getFullYear() : todayYear + 15;
|
|
240
295
|
const years = Array.from({ length: yearEnd - yearStart + 1 }, (_, i) => yearStart + i);
|
|
241
296
|
|
|
297
|
+
const prevAtMin = viewYear === minYear && viewMonth === minMon;
|
|
298
|
+
const nextAtMax = viewYear === maxYear && viewMonth === maxMon;
|
|
299
|
+
|
|
242
300
|
const goPrev = () => {
|
|
301
|
+
if (prevAtMin) return;
|
|
243
302
|
const p = addMonths(viewYear, viewMonth, -1);
|
|
244
303
|
setViewYear(p.year);
|
|
245
304
|
setViewMonth(p.month);
|
|
246
305
|
};
|
|
247
306
|
|
|
248
307
|
const goNext = () => {
|
|
308
|
+
if (nextAtMax) return;
|
|
249
309
|
const n = addMonths(viewYear, viewMonth, 1);
|
|
250
310
|
setViewYear(n.year);
|
|
251
311
|
setViewMonth(n.month);
|
|
@@ -263,19 +323,19 @@ export function Calendar({
|
|
|
263
323
|
|
|
264
324
|
return (
|
|
265
325
|
<div
|
|
266
|
-
className={["a1-calendar", "a1-calendar--paginated", className].filter(Boolean).join(" ")}
|
|
326
|
+
className={["a1-calendar", "a1-calendar--paginated", selectable && "a1-calendar--selectable", className].filter(Boolean).join(" ")}
|
|
267
327
|
{...props}
|
|
268
328
|
>
|
|
269
329
|
<div className="a1-calendar__inner">
|
|
270
330
|
<div className="a1-calendar__nav">
|
|
271
331
|
|
|
272
332
|
<span className="a1-calendar__nav-wide">
|
|
273
|
-
<Button variant="tertiary" size="sm" icon={isRtl ? "chevron_right" : "chevron_left"} iconPosition="start" onClick={goPrev}>
|
|
333
|
+
<Button variant="tertiary" size="sm" icon={isRtl ? "chevron_right" : "chevron_left"} iconPosition="start" onClick={goPrev} disabled={prevAtMin}>
|
|
274
334
|
{prevMonthLabel}
|
|
275
335
|
</Button>
|
|
276
336
|
</span>
|
|
277
337
|
<span className="a1-calendar__nav-narrow">
|
|
278
|
-
<IconButton icon={isRtl ? "chevron_right" : "chevron_left"} label={prevMonthLabel} variant="tertiary" onClick={goPrev} />
|
|
338
|
+
<IconButton icon={isRtl ? "chevron_right" : "chevron_left"} label={prevMonthLabel} variant="tertiary" onClick={goPrev} disabled={prevAtMin} />
|
|
279
339
|
</span>
|
|
280
340
|
|
|
281
341
|
<div className="a1-calendar__nav-label">
|
|
@@ -286,9 +346,12 @@ export function Calendar({
|
|
|
286
346
|
value={viewMonth}
|
|
287
347
|
onChange={e => setViewMonth(Number(e.target.value))}
|
|
288
348
|
>
|
|
289
|
-
{MONTHS.map((name, i) =>
|
|
290
|
-
|
|
291
|
-
|
|
349
|
+
{MONTHS.map((name, i) => {
|
|
350
|
+
const disabled =
|
|
351
|
+
(viewYear === minYear && i < minMon) ||
|
|
352
|
+
(viewYear === maxYear && i > maxMon);
|
|
353
|
+
return <option key={i} value={i} disabled={disabled}>{name}</option>;
|
|
354
|
+
})}
|
|
292
355
|
</SelectField>
|
|
293
356
|
<SelectField
|
|
294
357
|
className="a1-calendar__nav-field"
|
|
@@ -327,12 +390,12 @@ export function Calendar({
|
|
|
327
390
|
</div>
|
|
328
391
|
|
|
329
392
|
<span className="a1-calendar__nav-wide">
|
|
330
|
-
<Button variant="tertiary" size="sm" icon={isRtl ? "chevron_left" : "chevron_right"} iconPosition="end" onClick={goNext}>
|
|
393
|
+
<Button variant="tertiary" size="sm" icon={isRtl ? "chevron_left" : "chevron_right"} iconPosition="end" onClick={goNext} disabled={nextAtMax}>
|
|
331
394
|
{nextMonthLabel}
|
|
332
395
|
</Button>
|
|
333
396
|
</span>
|
|
334
397
|
<span className="a1-calendar__nav-narrow">
|
|
335
|
-
<IconButton icon={isRtl ? "chevron_left" : "chevron_right"} label={nextMonthLabel} variant="tertiary" onClick={goNext} />
|
|
398
|
+
<IconButton icon={isRtl ? "chevron_left" : "chevron_right"} label={nextMonthLabel} variant="tertiary" onClick={goNext} disabled={nextAtMax} />
|
|
336
399
|
</span>
|
|
337
400
|
|
|
338
401
|
</div>
|
|
@@ -351,7 +414,7 @@ export function Calendar({
|
|
|
351
414
|
|
|
352
415
|
return (
|
|
353
416
|
<div
|
|
354
|
-
className={["a1-calendar", className].filter(Boolean).join(" ")}
|
|
417
|
+
className={["a1-calendar", selectable && "a1-calendar--selectable", className].filter(Boolean).join(" ")}
|
|
355
418
|
{...props}
|
|
356
419
|
>
|
|
357
420
|
<div className="a1-calendar__inner">
|
|
@@ -110,6 +110,50 @@
|
|
|
110
110
|
background: var(--semantic-color-surface-raised);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/* ── Selected — full-cell fill (display and selectable modes) ─── */
|
|
114
|
+
|
|
115
|
+
.a1-calendar__day--selected {
|
|
116
|
+
background: var(--semantic-color-action-background);
|
|
117
|
+
color: var(--semantic-color-text-inverse);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* ── Disabled (out of range) — same surface as past dates ──────── */
|
|
121
|
+
|
|
122
|
+
.a1-calendar__day--disabled {
|
|
123
|
+
background: var(--semantic-color-surface-raised);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* ── Selectable mode (opt-in via selectable prop) ──────────────── */
|
|
127
|
+
|
|
128
|
+
.a1-calendar--selectable .a1-calendar__day:not(.a1-calendar__day--empty):not(.a1-calendar__day--disabled) {
|
|
129
|
+
cursor: pointer;
|
|
130
|
+
}
|
|
131
|
+
.a1-calendar--selectable .a1-calendar__day--disabled {
|
|
132
|
+
cursor: not-allowed;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* Today — neutral ring instead of action fill (yields to selected) */
|
|
136
|
+
.a1-calendar--selectable .a1-calendar__day--today:not(.a1-calendar__day--selected) {
|
|
137
|
+
background: transparent;
|
|
138
|
+
color: var(--semantic-color-text-default);
|
|
139
|
+
}
|
|
140
|
+
.a1-calendar--selectable .a1-calendar__day--today:not(.a1-calendar__day--selected) .a1-calendar__day-number {
|
|
141
|
+
outline: 2px solid var(--semantic-color-text-default);
|
|
142
|
+
outline-offset: -1px;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* Full-cell hover for non-selected, non-disabled days */
|
|
146
|
+
.a1-calendar--selectable .a1-calendar__day:not(.a1-calendar__day--empty):not(.a1-calendar__day--disabled):not(.a1-calendar__day--selected):hover {
|
|
147
|
+
background: var(--semantic-color-surface-panel);
|
|
148
|
+
color: var(--semantic-color-text-default);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* Focus ring */
|
|
152
|
+
.a1-calendar--selectable .a1-calendar__day:focus-visible {
|
|
153
|
+
outline: var(--component-field-focus-ring-width) solid var(--component-field-focus-ring-color);
|
|
154
|
+
outline-offset: -2px;
|
|
155
|
+
}
|
|
156
|
+
|
|
113
157
|
/* ── Paginated variant — nav bar ─────────────────────────── */
|
|
114
158
|
|
|
115
159
|
.a1-calendar__nav {
|
|
@@ -178,6 +222,33 @@
|
|
|
178
222
|
color: var(--semantic-color-text-inverse);
|
|
179
223
|
}
|
|
180
224
|
|
|
225
|
+
/* Selected: revert to circle in narrow view */
|
|
226
|
+
.a1-calendar__day--selected {
|
|
227
|
+
background: transparent;
|
|
228
|
+
color: var(--semantic-color-text-default);
|
|
229
|
+
}
|
|
230
|
+
.a1-calendar__day--selected .a1-calendar__day-number {
|
|
231
|
+
background: var(--semantic-color-action-background);
|
|
232
|
+
color: var(--semantic-color-text-inverse);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/* Selectable: today ring (suppress action-bg circle from non-selectable narrow rule) */
|
|
236
|
+
.a1-calendar--selectable .a1-calendar__day--today:not(.a1-calendar__day--selected) .a1-calendar__day-number {
|
|
237
|
+
background: transparent;
|
|
238
|
+
color: var(--semantic-color-text-default);
|
|
239
|
+
outline: 2px solid var(--semantic-color-text-default);
|
|
240
|
+
outline-offset: -1px;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/* Selectable: hover as circle in narrow */
|
|
244
|
+
.a1-calendar--selectable .a1-calendar__day:not(.a1-calendar__day--empty):not(.a1-calendar__day--disabled):not(.a1-calendar__day--selected):hover {
|
|
245
|
+
background: transparent;
|
|
246
|
+
color: var(--semantic-color-text-default);
|
|
247
|
+
}
|
|
248
|
+
.a1-calendar--selectable .a1-calendar__day:not(.a1-calendar__day--empty):not(.a1-calendar__day--disabled):not(.a1-calendar__day--selected):hover .a1-calendar__day-number {
|
|
249
|
+
background: var(--semantic-color-surface-panel);
|
|
250
|
+
}
|
|
251
|
+
|
|
181
252
|
.a1-calendar__nav {
|
|
182
253
|
padding-block: var(--component-calendar-heading-padding-block-compact);
|
|
183
254
|
}
|