@makolabs/ripple 3.3.0 → 3.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.
@@ -3,17 +3,15 @@
3
3
  import { buildTestId } from '../helper/testid.js';
4
4
  import { Size } from '../variants.js';
5
5
  import { formSizeTokens } from './form-size.js';
6
+ import Popover from '../elements/popover/Popover.svelte';
7
+ import Calendar from './calendar/Calendar.svelte';
6
8
  import type { DateRangeProps } from '../index.js';
7
- import Portal from '../utils/Portal.svelte';
8
- import { fly } from 'svelte/transition';
9
- import { quintOut } from 'svelte/easing';
10
- import { onMount } from 'svelte';
11
9
 
12
10
  let {
13
11
  startDate = $bindable(),
14
12
  endDate = $bindable(),
15
- minDate = new Date(new Date().getFullYear() - 5, 0, 1),
16
- maxDate = new Date(new Date().getFullYear() + 5, 11, 31),
13
+ minDate,
14
+ maxDate,
17
15
  disabled = false,
18
16
  class: className = '',
19
17
  placeholder = 'Select date range',
@@ -28,224 +26,47 @@
28
26
  testId
29
27
  }: DateRangeProps = $props();
30
28
 
29
+ let open = $state(false);
31
30
  const tokens = $derived(formSizeTokens[size]);
31
+ const hasErrors = $derived(errors?.length > 0);
32
+ const hasValue = $derived(!!startDate || !!endDate);
32
33
 
33
- let isMobile = $state(
34
- typeof window !== 'undefined' && window.matchMedia('(max-width: 639.98px)').matches
35
- );
36
- onMount(() => {
37
- const mql = window.matchMedia('(max-width: 639.98px)');
38
- const handler = (e: MediaQueryListEvent) => (isMobile = e.matches);
39
- mql.addEventListener('change', handler);
40
- return () => mql.removeEventListener('change', handler);
41
- });
42
-
43
- let isOpen = $state(false);
44
- let hoveredDate = $state<Date | null>(null);
45
- let datePickerRef = $state<HTMLDivElement | null>(null);
46
- let calendarRef = $state<HTMLDivElement | null>(null);
47
-
48
- let viewMode = $state<'days' | 'months' | 'years'>('days');
49
- let viewDate = $state(new Date());
50
-
51
- const handleOutsideClick = (event: MouseEvent) => {
52
- if (isOpen && datePickerRef && calendarRef) {
53
- const target = event.target as Node;
54
- if (!datePickerRef.contains(target) && !calendarRef.contains(target)) {
55
- isOpen = false;
56
- }
57
- }
58
- };
59
-
60
- function formatDate(date: Date | null): string {
34
+ function formatDate(date: Date | undefined): string {
61
35
  if (!date) return '';
62
- const year = date.getFullYear();
63
- const month = String(date.getMonth() + 1).padStart(2, '0');
64
- const day = String(date.getDate()).padStart(2, '0');
65
- return format.replace('yyyy', String(year)).replace('MM', month).replace('dd', day);
66
- }
67
-
68
- function getDaysInMonth(month: Date): {
69
- date: Date;
70
- isCurrentMonth: boolean;
71
- isToday: boolean;
72
- isSelected: boolean;
73
- isInRange: boolean;
74
- isDisabled: boolean;
75
- }[] {
76
- const year = month.getFullYear();
77
- const monthIndex = month.getMonth();
78
- const daysInMonth = new Date(year, monthIndex + 1, 0).getDate();
79
-
80
- const firstDay = new Date(year, monthIndex, 1);
81
-
82
- const firstDayOfWeek = firstDay.getDay();
83
-
84
- const daysFromPrevMonth = firstDayOfWeek;
85
-
86
- const prevMonth = new Date(year, monthIndex, 0);
87
- const prevMonthDays = prevMonth.getDate();
88
-
89
- const daysInCalendar = 42; // 6 rows of 7 days
90
- const daysFromNextMonth = daysInCalendar - daysInMonth - daysFromPrevMonth;
91
-
92
- // eslint-disable-next-line svelte/prefer-svelte-reactivity
93
- const today = new Date();
94
- today.setHours(0, 0, 0, 0);
95
-
96
- const days = [];
97
-
98
- for (let i = daysFromPrevMonth - 1; i >= 0; i--) {
99
- const date = new Date(year, monthIndex - 1, prevMonthDays - i);
100
- days.push({
101
- date,
102
- isCurrentMonth: false,
103
- isToday: date.getTime() === today.getTime(),
104
- isSelected: isDateSelected(date),
105
- isInRange: isDateInRange(date),
106
- isDisabled: isDateDisabled(date)
107
- });
108
- }
109
-
110
- for (let i = 1; i <= daysInMonth; i++) {
111
- const date = new Date(year, monthIndex, i);
112
- days.push({
113
- date,
114
- isCurrentMonth: true,
115
- isToday: date.getTime() === today.getTime(),
116
- isSelected: isDateSelected(date),
117
- isInRange: isDateInRange(date),
118
- isDisabled: isDateDisabled(date)
119
- });
120
- }
121
-
122
- for (let i = 1; i <= daysFromNextMonth; i++) {
123
- const date = new Date(year, monthIndex + 1, i);
124
- days.push({
125
- date,
126
- isCurrentMonth: false,
127
- isToday: date.getTime() === today.getTime(),
128
- isSelected: isDateSelected(date),
129
- isInRange: isDateInRange(date),
130
- isDisabled: isDateDisabled(date)
131
- });
132
- }
133
-
134
- return days;
135
- }
136
-
137
- function isDateSelected(date: Date): boolean {
138
- return Boolean(
139
- (startDate && isSameDate(date, startDate)) || (endDate && isSameDate(date, endDate))
140
- );
36
+ const yyyy = String(date.getFullYear());
37
+ const MM = String(date.getMonth() + 1).padStart(2, '0');
38
+ const dd = String(date.getDate()).padStart(2, '0');
39
+ return format.replace('yyyy', yyyy).replace('MM', MM).replace('dd', dd);
141
40
  }
142
41
 
143
- function isDateInRange(date: Date): boolean {
144
- if (!startDate || !endDate) {
145
- if (startDate && hoveredDate) {
146
- const hoverStart = startDate < hoveredDate ? startDate : hoveredDate;
147
- const hoverEnd = startDate < hoveredDate ? hoveredDate : startDate;
148
- return date >= hoverStart && date <= hoverEnd;
149
- }
150
- return false;
151
- }
152
-
153
- return date >= startDate && date <= endDate;
154
- }
155
-
156
- function isDateDisabled(date: Date): boolean {
157
- return date < minDate || date > maxDate || disabled;
158
- }
159
-
160
- function isSameDate(date1: Date, date2: Date): boolean {
161
- return (
162
- date1.getDate() === date2.getDate() &&
163
- date1.getMonth() === date2.getMonth() &&
164
- date1.getFullYear() === date2.getFullYear()
165
- );
166
- }
167
-
168
- function handleDateClick(date: Date): void {
169
- if (isDateDisabled(date)) return;
170
-
171
- if (!startDate || (startDate && endDate)) {
172
- startDate = date;
173
- endDate = undefined;
174
- } else if (startDate && !endDate) {
175
- if (date < startDate) {
176
- endDate = startDate;
177
- startDate = date;
178
- } else {
179
- endDate = date;
180
- }
181
- }
42
+ const triggerText = $derived.by(() => {
43
+ if (startDate && endDate) return `${formatDate(startDate)} - ${formatDate(endDate)}`;
44
+ if (startDate) return `${formatDate(startDate)} - Select end date`;
45
+ return placeholder;
46
+ });
182
47
 
48
+ function handleSelect(sel: Date | { from: Date | null; to: Date | null }): void {
49
+ // Calendar emits the {from, to} shape in range mode. The Date
50
+ // branch is unreachable here but narrows the type for the compiler.
51
+ if (sel instanceof Date) return;
52
+ startDate = sel.from ?? undefined;
53
+ endDate = sel.to ?? undefined;
183
54
  onselect?.({ startDate, endDate });
184
-
185
- if (startDate && endDate) {
186
- isOpen = false;
187
- }
188
- }
189
-
190
- function handleDateHover(date: Date): void {
191
- hoveredDate = date;
192
- }
193
-
194
- function nextMonth(): void {
195
- viewDate = new Date(viewDate.getFullYear(), viewDate.getMonth() + 1, 1);
196
- }
197
-
198
- function prevMonth(): void {
199
- viewDate = new Date(viewDate.getFullYear(), viewDate.getMonth() - 1, 1);
200
- }
201
-
202
- function showMonths(): void {
203
- viewMode = 'months';
204
- }
205
-
206
- function showYears(): void {
207
- viewMode = 'years';
208
- }
209
-
210
- function selectMonth(month: number): void {
211
- viewDate = new Date(viewDate.getFullYear(), month, 1);
212
- viewMode = 'days';
213
- }
214
-
215
- function selectYear(year: number): void {
216
- viewDate = new Date(year, viewDate.getMonth(), 1);
217
- viewMode = 'months';
218
- }
219
-
220
- function getMonthName(monthIndex: number): string {
221
- return new Date(2000, monthIndex, 1).toLocaleString('default', { month: 'long' });
55
+ if (startDate && endDate) open = false;
222
56
  }
223
57
 
224
- function toggleDatepicker(): void {
225
- if (disabled) return;
226
- isOpen = !isOpen;
227
- }
228
-
229
- function clearDates(event: Event): void {
58
+ function clearDates(event: MouseEvent): void {
230
59
  event.stopPropagation();
231
60
  startDate = undefined;
232
61
  endDate = undefined;
233
62
  onselect?.({ startDate, endDate });
234
63
  }
235
64
 
236
- $effect(() => {
237
- if (startDate && endDate) {
238
- viewDate = new Date(startDate);
239
- }
240
- });
241
-
242
65
  function getValue(): string {
243
66
  return `${startDate ? formatDate(startDate) : ''}:${endDate ? formatDate(endDate) : ''}`;
244
67
  }
245
68
  </script>
246
69
 
247
- <svelte:window onmousedown={handleOutsideClick} />
248
-
249
70
  <input type="hidden" name={`${name}[start]`} value={startDate?.toISOString()} />
250
71
  <input type="hidden" name={`${name}[end]`} value={endDate?.toISOString()} />
251
72
  <input type="hidden" name={`${name}[format]`} value={format} />
@@ -253,10 +74,9 @@
253
74
 
254
75
  <div
255
76
  class={cn('relative block w-full', className)}
256
- bind:this={datePickerRef}
257
77
  data-testid={buildTestId('date-range', undefined, testId)}
258
78
  >
259
- <div class="relative">
79
+ <Popover trigger="manual" bind:open placement="bottom" {disabled}>
260
80
  <button
261
81
  {id}
262
82
  type="button"
@@ -269,23 +89,21 @@
269
89
  tokens.shadow,
270
90
  disabled
271
91
  ? 'bg-default-100 text-default-400 cursor-not-allowed opacity-50'
272
- : errors?.length
273
- ? 'border-danger-300 focus-within:ring-danger-500 focus-within:border-danger-500 focus-within:ring-2'
92
+ : hasErrors
93
+ ? 'border-danger-300 focus-within:border-danger-500 focus-within:ring-danger-500 focus-within:ring-2'
274
94
  : 'focus-visible:border-primary-500 focus-visible:ring-primary-500 hover:border-default-400 focus-visible:ring-2'
275
95
  )}
276
- onclick={toggleDatepicker}
96
+ onclick={() => {
97
+ if (!disabled) open = !open;
98
+ }}
277
99
  data-testid={buildTestId('date-range', 'trigger', testId)}
278
100
  aria-haspopup="true"
279
- aria-expanded={isOpen}
280
- aria-describedby={errors?.length ? `${id}-errors` : undefined}
101
+ aria-expanded={open}
102
+ aria-describedby={hasErrors ? `${id}-errors` : undefined}
281
103
  {disabled}
282
104
  >
283
- <span class={startDate && endDate ? 'text-default-900' : 'text-default-500'}>
284
- {startDate && endDate
285
- ? `${formatDate(startDate)} - ${formatDate(endDate)}`
286
- : startDate
287
- ? `${formatDate(startDate)} - Select end date`
288
- : placeholder}
105
+ <span class={hasValue ? 'text-default-900' : 'text-default-500'}>
106
+ {triggerText}
289
107
  </span>
290
108
  <svg class={cn('text-default-400', tokens.iconSize)} viewBox="0 0 20 20" fill="currentColor">
291
109
  <path
@@ -296,254 +114,61 @@
296
114
  </svg>
297
115
  </button>
298
116
 
299
- {#if startDate || endDate}
300
- <button
301
- type="button"
302
- class={cn(
303
- 'text-default-400 hover:text-default-500 absolute top-1/2 -translate-y-1/2',
304
- // Sit just left of the calendar icon; use the token gap
305
- // so the clear button stays visually tied to the icon.
306
- 'right-8'
307
- )}
308
- onclick={clearDates}
309
- data-testid={buildTestId('date-range', 'clear', testId)}
310
- aria-label="Clear dates"
311
- >
312
- <svg class={cn(tokens.iconSize)} viewBox="0 0 20 20" fill="currentColor">
313
- <path
314
- fill-rule="evenodd"
315
- d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
316
- clip-rule="evenodd"
317
- />
318
- </svg>
319
- </button>
320
- {/if}
321
- </div>
117
+ {#snippet content()}
118
+ <!-- Wrap Calendar in our own card so the labelled footer
119
+ reads as part of the same surface. Calendar's default
120
+ border/shadow are neutralized via class so we don't
121
+ get a card-in-card. -->
122
+ <div class="border-default-200 rounded-lg border bg-white shadow-xs">
123
+ <Calendar
124
+ mode="range"
125
+ valueStart={startDate ?? null}
126
+ valueEnd={endDate ?? null}
127
+ {minDate}
128
+ {maxDate}
129
+ {size}
130
+ class="rounded-none border-0 shadow-none"
131
+ onselect={handleSelect}
132
+ />
133
+ {#if hasValue}
134
+ <div
135
+ class="border-default-200 text-default-500 flex flex-wrap justify-between gap-x-4 gap-y-1 border-t px-3 py-2 text-xs"
136
+ >
137
+ <div>{startDate ? `${startLabel}: ${formatDate(startDate)}` : ''}</div>
138
+ <div>{endDate ? `${endLabel}: ${formatDate(endDate)}` : ''}</div>
139
+ </div>
140
+ {/if}
141
+ </div>
142
+ {/snippet}
143
+ </Popover>
322
144
 
323
- {#if errors?.length}
145
+ {#if hasValue && !disabled}
146
+ <button
147
+ type="button"
148
+ class={cn(
149
+ 'text-default-400 hover:text-default-500 absolute top-1/2 -translate-y-1/2',
150
+ // Sit just left of the calendar icon; the trigger has the icon at far right.
151
+ 'right-8'
152
+ )}
153
+ onclick={clearDates}
154
+ data-testid={buildTestId('date-range', 'clear', testId)}
155
+ aria-label="Clear dates"
156
+ >
157
+ <svg class={cn(tokens.iconSize)} viewBox="0 0 20 20" fill="currentColor">
158
+ <path
159
+ fill-rule="evenodd"
160
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
161
+ clip-rule="evenodd"
162
+ />
163
+ </svg>
164
+ </button>
165
+ {/if}
166
+
167
+ {#if hasErrors}
324
168
  <div id="{id}-errors" data-testid={buildTestId('date-range', 'errors', testId)}>
325
169
  {#each errors as error, i (i)}
326
170
  <p class="text-danger-600 mt-1 text-sm">{error}</p>
327
171
  {/each}
328
172
  </div>
329
173
  {/if}
330
-
331
- {#if isOpen && !isMobile}
332
- <Portal target={datePickerRef}>
333
- <div
334
- bind:this={calendarRef}
335
- class="ring-opacity-5 ring-default-300 absolute z-10 mt-1 w-full origin-top-left rounded-md bg-white p-4 shadow-lg ring-1 focus:outline-none"
336
- data-testid={buildTestId('date-range', 'panel', testId)}
337
- transition:fly={{ y: -8, duration: 300, easing: quintOut }}
338
- >
339
- {@render calendarContent()}
340
- </div>
341
- </Portal>
342
- {/if}
343
-
344
- {#if isOpen && isMobile}
345
- <Portal target={datePickerRef}>
346
- <button
347
- type="button"
348
- class="fixed inset-0 z-[9998] cursor-pointer bg-black/40 backdrop-blur-sm"
349
- aria-label="Close"
350
- onclick={() => (isOpen = false)}
351
- ></button>
352
- <div
353
- class="fixed inset-x-0 bottom-0 z-[9999] flex max-h-[85vh] min-h-48 flex-col overflow-hidden rounded-t-2xl bg-white shadow-2xl"
354
- transition:fly={{ y: 300, duration: 200, easing: quintOut }}
355
- bind:this={calendarRef}
356
- >
357
- <div class="flex justify-center py-2">
358
- <div class="bg-default-300 h-1 w-8 rounded-full"></div>
359
- </div>
360
- <div class="flex-1 cursor-pointer overflow-y-auto p-4">
361
- {@render calendarContent()}
362
- </div>
363
- </div>
364
- </Portal>
365
- {/if}
366
174
  </div>
367
-
368
- {#snippet calendarContent()}
369
- <div class="mb-2 flex items-center justify-between">
370
- {#if viewMode === 'days'}
371
- <button
372
- type="button"
373
- aria-label="Previous month"
374
- class="text-default-500 hover:bg-default-100 inline-flex cursor-pointer items-center rounded-md p-1 text-sm"
375
- onclick={prevMonth}
376
- >
377
- <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"
378
- ><path
379
- fill-rule="evenodd"
380
- d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
381
- clip-rule="evenodd"
382
- /></svg
383
- >
384
- </button>
385
- <button
386
- type="button"
387
- class="text-default-700 hover:bg-default-100 inline-flex cursor-pointer items-center rounded-md px-2 py-1 text-sm font-medium"
388
- onclick={showMonths}
389
- >
390
- {getMonthName(viewDate.getMonth())}
391
- {viewDate.getFullYear()}
392
- </button>
393
- <button
394
- type="button"
395
- aria-label="Next month"
396
- class="text-default-500 hover:bg-default-100 inline-flex cursor-pointer items-center rounded-md p-1 text-sm"
397
- onclick={nextMonth}
398
- >
399
- <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"
400
- ><path
401
- fill-rule="evenodd"
402
- d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
403
- clip-rule="evenodd"
404
- /></svg
405
- >
406
- </button>
407
- {:else if viewMode === 'months'}
408
- <button
409
- type="button"
410
- aria-label="Previous year"
411
- class="text-default-500 hover:bg-default-100 inline-flex cursor-pointer items-center rounded-md p-1 text-sm"
412
- onclick={() => (viewDate = new Date(viewDate.getFullYear() - 1, viewDate.getMonth(), 1))}
413
- >
414
- <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"
415
- ><path
416
- fill-rule="evenodd"
417
- d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
418
- clip-rule="evenodd"
419
- /></svg
420
- >
421
- </button>
422
- <button
423
- type="button"
424
- class="text-default-700 inline-flex cursor-pointer items-center rounded-md px-2 py-1 text-sm font-medium"
425
- onclick={showYears}>{viewDate.getFullYear()}</button
426
- >
427
- <button
428
- type="button"
429
- aria-label="Next year"
430
- class="text-default-500 hover:bg-default-100 inline-flex cursor-pointer items-center rounded-md p-1 text-sm"
431
- onclick={() => (viewDate = new Date(viewDate.getFullYear() + 1, viewDate.getMonth(), 1))}
432
- >
433
- <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"
434
- ><path
435
- fill-rule="evenodd"
436
- d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
437
- clip-rule="evenodd"
438
- /></svg
439
- >
440
- </button>
441
- {:else if viewMode === 'years'}
442
- <button
443
- type="button"
444
- aria-label="Previous years"
445
- class="text-default-500 hover:bg-default-100 inline-flex cursor-pointer items-center rounded-md p-1 text-sm"
446
- onclick={() => (viewDate = new Date(viewDate.getFullYear() - 12, viewDate.getMonth(), 1))}
447
- >
448
- <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"
449
- ><path
450
- fill-rule="evenodd"
451
- d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
452
- clip-rule="evenodd"
453
- /></svg
454
- >
455
- </button>
456
- <button
457
- type="button"
458
- class="text-default-700 inline-flex cursor-pointer items-center rounded-md px-2 py-1 text-sm font-medium"
459
- >{viewDate.getFullYear() - 6} - {viewDate.getFullYear() + 5}</button
460
- >
461
- <button
462
- type="button"
463
- aria-label="Next years"
464
- class="text-default-500 hover:bg-default-100 inline-flex cursor-pointer items-center rounded-md p-1 text-sm"
465
- onclick={() => (viewDate = new Date(viewDate.getFullYear() + 12, viewDate.getMonth(), 1))}
466
- >
467
- <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"
468
- ><path
469
- fill-rule="evenodd"
470
- d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
471
- clip-rule="evenodd"
472
- /></svg
473
- >
474
- </button>
475
- {/if}
476
- </div>
477
-
478
- {#if viewMode === 'days'}
479
- <div class="text-default-500 mb-1 grid grid-cols-7 gap-1 text-center text-xs font-medium">
480
- <div>Su</div>
481
- <div>Mo</div>
482
- <div>Tu</div>
483
- <div>We</div>
484
- <div>Th</div>
485
- <div>Fr</div>
486
- <div>Sa</div>
487
- </div>
488
- <div class="grid grid-cols-7 gap-1">
489
- {#each getDaysInMonth(viewDate) as { date, isCurrentMonth, isToday, isSelected, isInRange, isDisabled } (date.getTime())}
490
- <button
491
- type="button"
492
- class={cn(
493
- 'flex h-8 w-8 cursor-pointer items-center justify-center rounded-full text-sm font-medium',
494
- isDisabled ? 'text-default-300 cursor-not-allowed' : 'hover:bg-default-100',
495
- isSelected ? 'bg-primary-500 hover:bg-primary-600 text-white' : '',
496
- isInRange && !isSelected ? 'bg-primary-100 text-primary-800' : '',
497
- !isCurrentMonth && !isSelected && !isInRange ? 'text-default-400' : '',
498
- isToday && !isSelected ? 'border-primary-500 border' : ''
499
- )}
500
- onclick={() => handleDateClick(date)}
501
- onmouseenter={() => handleDateHover(date)}
502
- disabled={isDisabled}>{date.getDate()}</button
503
- >
504
- {/each}
505
- </div>
506
- {:else if viewMode === 'months'}
507
- <div class="grid grid-cols-3 gap-2">
508
- <!-- eslint-disable-next-line @typescript-eslint/no-unused-vars -->
509
- {#each Array(12).fill(0) as _, month (month)}
510
- <button
511
- type="button"
512
- class={cn(
513
- 'flex cursor-pointer items-center justify-center rounded-md px-2 py-1 text-sm font-medium',
514
- viewDate.getMonth() === month
515
- ? 'bg-primary-500 text-white'
516
- : 'text-default-700 hover:bg-default-100'
517
- )}
518
- onclick={() => selectMonth(month)}>{getMonthName(month).substring(0, 3)}</button
519
- >
520
- {/each}
521
- </div>
522
- {:else if viewMode === 'years'}
523
- <div class="grid grid-cols-3 gap-2">
524
- <!-- eslint-disable-next-line @typescript-eslint/no-unused-vars -->
525
- {#each Array(12).fill(0) as _, i (i)}
526
- {@const year = viewDate.getFullYear() - 6 + i}
527
- <button
528
- type="button"
529
- class={cn(
530
- 'flex cursor-pointer items-center justify-center rounded-md px-2 py-1 text-sm font-medium',
531
- viewDate.getFullYear() === year
532
- ? 'bg-primary-500 text-white'
533
- : 'text-default-700 hover:bg-default-100'
534
- )}
535
- onclick={() => selectYear(year)}>{year}</button
536
- >
537
- {/each}
538
- </div>
539
- {/if}
540
-
541
- {#if startDate || endDate}
542
- <div
543
- class="border-default-200 text-default-500 mt-4 flex flex-wrap justify-between gap-x-4 gap-y-1 border-t pt-3 text-xs"
544
- >
545
- <div>{startDate ? `${startLabel}: ${formatDate(startDate)}` : ''}</div>
546
- <div>{endDate ? `${endLabel}: ${formatDate(endDate)}` : ''}</div>
547
- </div>
548
- {/if}
549
- {/snippet}
@@ -23,6 +23,7 @@
23
23
  class: className = '',
24
24
  oninput,
25
25
  onblur,
26
+ onkeydown,
26
27
  testId
27
28
  }: TextareaProps = $props();
28
29
 
@@ -103,6 +104,7 @@
103
104
  {value}
104
105
  oninput={handleInput}
105
106
  onblur={handleBlur}
107
+ {onkeydown}
106
108
  ></textarea>
107
109
 
108
110
  {#if showCount && maxLength !== undefined}