@juspay/svelte-ui-components 4.38.0 → 4.40.0
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.
|
@@ -464,10 +464,15 @@
|
|
|
464
464
|
columns the fixed-width cells sat centred inside wider tracks, leaving
|
|
465
465
|
horizontal gaps between days — which broke the range highlight into
|
|
466
466
|
disconnected boxes instead of one continuous pill. The day-name header must
|
|
467
|
-
use the same track sizing so it stays aligned with the day grid below.
|
|
467
|
+
use the same track sizing so it stays aligned with the day grid below.
|
|
468
|
+
|
|
469
|
+
A host that wants the grid to fill a container sets both
|
|
470
|
+
--calendar-grid-columns and --calendar-cell-width: fluid columns with the
|
|
471
|
+
default fixed-width cells bring those gaps back, so the cells must be told
|
|
472
|
+
to fill their tracks too. */
|
|
468
473
|
.day-names {
|
|
469
474
|
display: grid;
|
|
470
|
-
grid-template-columns: repeat(7, var(--calendar-cell-size, 36px));
|
|
475
|
+
grid-template-columns: var(--calendar-grid-columns, repeat(7, var(--calendar-cell-size, 36px)));
|
|
471
476
|
justify-content: center;
|
|
472
477
|
text-align: center;
|
|
473
478
|
}
|
|
@@ -481,7 +486,7 @@
|
|
|
481
486
|
|
|
482
487
|
.grid {
|
|
483
488
|
display: grid;
|
|
484
|
-
grid-template-columns: repeat(7, var(--calendar-cell-size, 36px));
|
|
489
|
+
grid-template-columns: var(--calendar-grid-columns, repeat(7, var(--calendar-cell-size, 36px)));
|
|
485
490
|
justify-content: center;
|
|
486
491
|
row-gap: 2px;
|
|
487
492
|
}
|
|
@@ -490,7 +495,7 @@
|
|
|
490
495
|
display: flex;
|
|
491
496
|
align-items: center;
|
|
492
497
|
justify-content: center;
|
|
493
|
-
width: var(--calendar-cell-size, 36px);
|
|
498
|
+
width: var(--calendar-cell-width, var(--calendar-cell-size, 36px));
|
|
494
499
|
height: var(--calendar-cell-size, 36px);
|
|
495
500
|
font-family: inherit;
|
|
496
501
|
font-size: var(--calendar-cell-font-size, 14px);
|
|
@@ -45,6 +45,8 @@
|
|
|
45
45
|
dualMonth,
|
|
46
46
|
timePicker,
|
|
47
47
|
align = 'left',
|
|
48
|
+
presetsPosition = 'side',
|
|
49
|
+
responsiveLayout = false,
|
|
48
50
|
compareStart = $bindable(null),
|
|
49
51
|
compareEnd = $bindable(null),
|
|
50
52
|
compareCalendar,
|
|
@@ -70,6 +72,24 @@
|
|
|
70
72
|
typeof dualMonth === 'boolean' ? dualMonth : mode === 'range'
|
|
71
73
|
);
|
|
72
74
|
|
|
75
|
+
// A media query cannot read a custom property, so the breakpoints are literals. The
|
|
76
|
+
// layout classes and the month count both follow these two queries, which keeps the
|
|
77
|
+
// JS-driven layout and the CSS that styles it from disagreeing about where it flips.
|
|
78
|
+
const SHEET_QUERY = '(max-width: 1023px)';
|
|
79
|
+
const NARROW_QUERY = '(max-width: 688px)';
|
|
80
|
+
|
|
81
|
+
let isWithinSheetWidth: boolean = $state(false);
|
|
82
|
+
let isWithinNarrowWidth: boolean = $state(false);
|
|
83
|
+
|
|
84
|
+
const isNarrow: boolean = $derived(responsiveLayout && isWithinNarrowWidth);
|
|
85
|
+
// A single-month picker is already compact, so it only needs the sheet once it is
|
|
86
|
+
// narrow; a range picker needs it as soon as two months no longer fit beside presets.
|
|
87
|
+
const isSheet: boolean = $derived(
|
|
88
|
+
responsiveLayout && (mode === 'range' ? isWithinSheetWidth : isWithinNarrowWidth)
|
|
89
|
+
);
|
|
90
|
+
const showDualMonth: boolean = $derived(isDualMonth && !isNarrow);
|
|
91
|
+
const isPresetsTop: boolean = $derived(presetsPosition === 'top' || isSheet);
|
|
92
|
+
|
|
73
93
|
// Draft state — only committed on Apply
|
|
74
94
|
let draftStart: Date | null = $state(null);
|
|
75
95
|
let draftEnd: Date | null = $state(null);
|
|
@@ -380,6 +400,12 @@
|
|
|
380
400
|
if (panelNode === null || triggerRef === null || typeof window === 'undefined') {
|
|
381
401
|
return;
|
|
382
402
|
}
|
|
403
|
+
// A sheet is pinned to the viewport rather than to the trigger, so there is no
|
|
404
|
+
// side of the trigger to flip to.
|
|
405
|
+
if (isSheet) {
|
|
406
|
+
opensUpward = false;
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
383
409
|
const trigger = triggerRef.getBoundingClientRect();
|
|
384
410
|
const panelHeight = panelNode.offsetHeight;
|
|
385
411
|
const offset = 6;
|
|
@@ -404,6 +430,70 @@
|
|
|
404
430
|
};
|
|
405
431
|
}
|
|
406
432
|
|
|
433
|
+
// Reads and subscribes only while responsiveLayout is on, so a picker that never asks
|
|
434
|
+
// for the layout never touches matchMedia. Every access is guarded: matchMedia is
|
|
435
|
+
// absent during SSR and in jsdom, and a partial stand-in can lack addEventListener;
|
|
436
|
+
// in both cases the unconfined layout is the right answer.
|
|
437
|
+
function trackViewportQuery(query: string, apply: (matches: boolean) => void): () => void {
|
|
438
|
+
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
|
|
439
|
+
return () => {};
|
|
440
|
+
}
|
|
441
|
+
const mediaQueryList: MediaQueryList | null = window.matchMedia(query) ?? null;
|
|
442
|
+
if (mediaQueryList === null) {
|
|
443
|
+
return () => {};
|
|
444
|
+
}
|
|
445
|
+
apply(mediaQueryList.matches === true);
|
|
446
|
+
if (typeof mediaQueryList.addEventListener !== 'function') {
|
|
447
|
+
return () => {};
|
|
448
|
+
}
|
|
449
|
+
const handleChange = (event: MediaQueryListEvent): void => {
|
|
450
|
+
// A flip can destroy the focused calendar day (two months become one), which
|
|
451
|
+
// drops focus to the body and leaves the panel's Tab trap with nothing to catch.
|
|
452
|
+
const hadFocusInPanel = panelRef !== null && panelRef.contains(getActiveElement(panelRef));
|
|
453
|
+
apply(event.matches);
|
|
454
|
+
tick().then(() => {
|
|
455
|
+
// The panel changes between anchored and fixed with the layout, so the
|
|
456
|
+
// above/below decision made for the old layout no longer holds.
|
|
457
|
+
positionPanel();
|
|
458
|
+
if (
|
|
459
|
+
hadFocusInPanel &&
|
|
460
|
+
panelRef !== null &&
|
|
461
|
+
!panelRef.contains(getActiveElement(panelRef))
|
|
462
|
+
) {
|
|
463
|
+
focusFirstElement(panelRef);
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
};
|
|
467
|
+
mediaQueryList.addEventListener('change', handleChange);
|
|
468
|
+
return () => mediaQueryList.removeEventListener('change', handleChange);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function followViewportWhileEnabled(
|
|
472
|
+
_node: HTMLElement,
|
|
473
|
+
enabled: boolean
|
|
474
|
+
): { update: (nextEnabled: boolean) => void; destroy: () => void } {
|
|
475
|
+
let stopTracking: () => void = () => {};
|
|
476
|
+
const sync = (shouldFollow: boolean): void => {
|
|
477
|
+
stopTracking();
|
|
478
|
+
stopTracking = () => {};
|
|
479
|
+
if (!shouldFollow) {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
const stopSheetQuery = trackViewportQuery(SHEET_QUERY, (matches) => {
|
|
483
|
+
isWithinSheetWidth = matches;
|
|
484
|
+
});
|
|
485
|
+
const stopNarrowQuery = trackViewportQuery(NARROW_QUERY, (matches) => {
|
|
486
|
+
isWithinNarrowWidth = matches;
|
|
487
|
+
});
|
|
488
|
+
stopTracking = () => {
|
|
489
|
+
stopSheetQuery();
|
|
490
|
+
stopNarrowQuery();
|
|
491
|
+
};
|
|
492
|
+
};
|
|
493
|
+
sync(enabled);
|
|
494
|
+
return { update: sync, destroy: () => sync(false) };
|
|
495
|
+
}
|
|
496
|
+
|
|
407
497
|
// The trigger toggles: clicking it while the panel is already open dismisses
|
|
408
498
|
// the picker instead of re-opening it onto itself. The open-only handler left
|
|
409
499
|
// a second click feeling dead — the panel could only be closed by clicking away.
|
|
@@ -700,7 +790,7 @@
|
|
|
700
790
|
// re-navigation/remount when a typed date is already on screen.
|
|
701
791
|
function isMonthVisible(date: Date): boolean {
|
|
702
792
|
const isLeftMonthVisible = date.getFullYear() === leftYear && date.getMonth() === leftMonth;
|
|
703
|
-
if (!
|
|
793
|
+
if (!showDualMonth) {
|
|
704
794
|
return isLeftMonthVisible;
|
|
705
795
|
}
|
|
706
796
|
const rightMonth = new SvelteDate(leftYear, leftMonth + 1, 1);
|
|
@@ -709,6 +799,13 @@
|
|
|
709
799
|
return isLeftMonthVisible || isRightMonthVisible;
|
|
710
800
|
}
|
|
711
801
|
|
|
802
|
+
// The single-month Calendar navigates itself, so its month is mirrored back here:
|
|
803
|
+
// isMonthVisible and every later remount seed from leftYear/leftMonth.
|
|
804
|
+
function handleSingleMonthChange(event: { year: number; month: number }): void {
|
|
805
|
+
leftYear = event.year;
|
|
806
|
+
leftMonth = event.month;
|
|
807
|
+
}
|
|
808
|
+
|
|
712
809
|
function navigateCalendarTo(date: Date): void {
|
|
713
810
|
if (isMonthVisible(date)) {
|
|
714
811
|
return;
|
|
@@ -899,7 +996,13 @@
|
|
|
899
996
|
{/if}
|
|
900
997
|
{/snippet}
|
|
901
998
|
|
|
902
|
-
<div
|
|
999
|
+
<div
|
|
1000
|
+
class="drp-root {classes ?? ''}"
|
|
1001
|
+
bind:this={drpRootEl}
|
|
1002
|
+
use:followViewportWhileEnabled={responsiveLayout}
|
|
1003
|
+
data-pw={testId}
|
|
1004
|
+
testID={testId}
|
|
1005
|
+
>
|
|
903
1006
|
<!-- Trigger wrapper — bind:this here for panel positioning and focus-return -->
|
|
904
1007
|
<div bind:this={triggerRef} class="drp-trigger-wrapper">
|
|
905
1008
|
<Button
|
|
@@ -980,6 +1083,9 @@
|
|
|
980
1083
|
class:drp-panel-above={opensUpward}
|
|
981
1084
|
class:drp-panel-align-left={align === 'left'}
|
|
982
1085
|
class:drp-panel-align-right={align === 'right'}
|
|
1086
|
+
class:drp-panel-presets-top={isPresetsTop}
|
|
1087
|
+
class:drp-panel-sheet={isSheet}
|
|
1088
|
+
class:drp-panel-narrow={isNarrow}
|
|
983
1089
|
role="dialog"
|
|
984
1090
|
aria-label="Date range picker"
|
|
985
1091
|
aria-modal="true"
|
|
@@ -1244,7 +1350,7 @@
|
|
|
1244
1350
|
{/if}
|
|
1245
1351
|
</div>
|
|
1246
1352
|
{/if}
|
|
1247
|
-
{#if
|
|
1353
|
+
{#if showDualMonth}
|
|
1248
1354
|
<!-- Dual-month layout with shared nav -->
|
|
1249
1355
|
<div class="drp-dual-header">
|
|
1250
1356
|
<button
|
|
@@ -1303,20 +1409,24 @@
|
|
|
1303
1409
|
</div>
|
|
1304
1410
|
{:else}
|
|
1305
1411
|
<!-- Single month -->
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1412
|
+
{#key calendarKey}
|
|
1413
|
+
<Calendar
|
|
1414
|
+
mode={mode === 'range' ? 'range' : 'single'}
|
|
1415
|
+
bind:rangeStart={draftStart}
|
|
1416
|
+
bind:rangeEnd={draftEnd}
|
|
1417
|
+
bind:value={draftValue}
|
|
1418
|
+
{weekStartsOn}
|
|
1419
|
+
{locale}
|
|
1420
|
+
{minDate}
|
|
1421
|
+
{maxDate}
|
|
1422
|
+
disabledDates={mode === 'range' ? rangeConstrainedDisabledDates : disabledDates}
|
|
1423
|
+
initialMonth={leftInitialMonth}
|
|
1424
|
+
onrangeselect={handleRangeSelect}
|
|
1425
|
+
onselect={handleSingleSelect}
|
|
1426
|
+
onmonthchange={handleSingleMonthChange}
|
|
1427
|
+
classes="drp-calendar-embedded"
|
|
1428
|
+
/>
|
|
1429
|
+
{/key}
|
|
1320
1430
|
{/if}
|
|
1321
1431
|
|
|
1322
1432
|
<!-- Time picker slot: consumer controls all time UI -->
|
|
@@ -1372,7 +1482,7 @@
|
|
|
1372
1482
|
|
|
1373
1483
|
.drp-root {
|
|
1374
1484
|
position: relative;
|
|
1375
|
-
display: inline-block;
|
|
1485
|
+
display: var(--drp-root-display, inline-block);
|
|
1376
1486
|
}
|
|
1377
1487
|
|
|
1378
1488
|
/* ── Trigger wrapper ── */
|
|
@@ -1435,6 +1545,9 @@
|
|
|
1435
1545
|
max-width: var(--drp-panel-max-width, 760px);
|
|
1436
1546
|
max-height: var(--drp-panel-max-height, calc(100dvh - 80px));
|
|
1437
1547
|
overflow: hidden;
|
|
1548
|
+
/* A host may disable pointer events on an ancestor (a collapsing sidebar, say) and
|
|
1549
|
+
an open aria-modal panel must stay usable, as Modal's content does. */
|
|
1550
|
+
pointer-events: auto;
|
|
1438
1551
|
}
|
|
1439
1552
|
|
|
1440
1553
|
/* Set only when the panel would not fit below the trigger — see positionPanel(). */
|
|
@@ -1453,6 +1566,23 @@
|
|
|
1453
1566
|
left: auto;
|
|
1454
1567
|
}
|
|
1455
1568
|
|
|
1569
|
+
/* Opt-in sheet (responsiveLayout): confine the panel to the viewport instead of the
|
|
1570
|
+
trigger. The insets are tokens because the free area differs per host (a fixed
|
|
1571
|
+
sidebar, an embedding shell, safe-area insets); the defaults suit a bare page. */
|
|
1572
|
+
.drp-panel.drp-panel-sheet {
|
|
1573
|
+
position: fixed;
|
|
1574
|
+
top: var(--drp-sheet-top, auto);
|
|
1575
|
+
right: var(--drp-sheet-right, 16px);
|
|
1576
|
+
bottom: var(--drp-sheet-bottom, 16px);
|
|
1577
|
+
left: var(--drp-sheet-left, 16px);
|
|
1578
|
+
z-index: var(--drp-sheet-z-index, var(--drp-panel-z-index, 1000));
|
|
1579
|
+
width: auto;
|
|
1580
|
+
min-width: 0;
|
|
1581
|
+
max-width: var(--drp-sheet-max-width, 48rem);
|
|
1582
|
+
max-height: var(--drp-sheet-max-height, calc(100dvh - 2rem));
|
|
1583
|
+
margin-inline: auto;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1456
1586
|
.drp-panel-inner {
|
|
1457
1587
|
display: flex;
|
|
1458
1588
|
flex-direction: row;
|
|
@@ -1461,6 +1591,57 @@
|
|
|
1461
1591
|
overflow-y: auto;
|
|
1462
1592
|
}
|
|
1463
1593
|
|
|
1594
|
+
/* ── Presets above the calendars (presetsPosition="top", and every sheet) ── */
|
|
1595
|
+
.drp-panel-presets-top .drp-panel-inner {
|
|
1596
|
+
flex-direction: column;
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
.drp-panel-presets-top .drp-sidebar {
|
|
1600
|
+
flex: none;
|
|
1601
|
+
flex-direction: row;
|
|
1602
|
+
flex-wrap: wrap;
|
|
1603
|
+
min-width: 0;
|
|
1604
|
+
max-height: none;
|
|
1605
|
+
overflow-y: visible;
|
|
1606
|
+
border-right: 0;
|
|
1607
|
+
border-bottom: var(--drp-sidebar-border, 1px solid #e8e8e8);
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
.drp-panel-presets-top .drp-preset-item {
|
|
1611
|
+
width: auto;
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
.drp-panel-presets-top .drp-preset-divider {
|
|
1615
|
+
display: none;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
.drp-panel-presets-top .drp-calendars {
|
|
1619
|
+
flex: none;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
.drp-panel-presets-top .drp-months-row {
|
|
1623
|
+
justify-content: center;
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
/* ── Narrow layout (responsiveLayout, <= 688px): one month, stacked inputs ── */
|
|
1627
|
+
.drp-panel-narrow .drp-calendars {
|
|
1628
|
+
padding: var(--drp-calendars-padding-narrow, 16px 8px);
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
.drp-panel-narrow :global(.drp-calendar-embedded) {
|
|
1632
|
+
max-width: 100%;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
.drp-panel-narrow .drp-date-input-row,
|
|
1636
|
+
.drp-panel-narrow .drp-time-input-row {
|
|
1637
|
+
flex-direction: column;
|
|
1638
|
+
align-items: stretch;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
.drp-panel-narrow .drp-datetime-arrow {
|
|
1642
|
+
display: none;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1464
1645
|
/* ── Sidebar ── */
|
|
1465
1646
|
.drp-sidebar {
|
|
1466
1647
|
display: flex;
|
|
@@ -1914,6 +2095,7 @@
|
|
|
1914
2095
|
flex-direction: column;
|
|
1915
2096
|
min-width: var(--drp-compare-panel-min-width, 280px);
|
|
1916
2097
|
overflow: hidden;
|
|
2098
|
+
pointer-events: auto;
|
|
1917
2099
|
}
|
|
1918
2100
|
|
|
1919
2101
|
.drp-compare-panel-body {
|
|
@@ -15,6 +15,7 @@ export type DateRangePreset = {
|
|
|
15
15
|
};
|
|
16
16
|
export type DateRangePickerMode = 'range' | 'single';
|
|
17
17
|
export type DateRangePickerAlign = 'left' | 'right';
|
|
18
|
+
export type DateRangePickerPresetsPosition = 'side' | 'top';
|
|
18
19
|
export type DateRangePickerTimeLayout = 'toggle' | 'inline';
|
|
19
20
|
export type TimeDisplayBoundary = 'start' | 'end';
|
|
20
21
|
export type OptionalDateRangePickerProperties = {
|
|
@@ -80,6 +81,21 @@ export type OptionalDateRangePickerProperties = {
|
|
|
80
81
|
dualMonth?: boolean;
|
|
81
82
|
/** Align the dropdown panel to the left or right edge of the trigger. Default: 'left'. */
|
|
82
83
|
align?: DateRangePickerAlign;
|
|
84
|
+
/**
|
|
85
|
+
* Where the presets sit. 'side' (default) puts them in a column beside the calendars;
|
|
86
|
+
* 'top' lays them out as a wrapping row above the calendars, which suits a panel that
|
|
87
|
+
* cannot be wide. Group dividers are hidden while the presets are on top.
|
|
88
|
+
*/
|
|
89
|
+
presetsPosition?: DateRangePickerPresetsPosition;
|
|
90
|
+
/**
|
|
91
|
+
* Opt-in viewport awareness. At or below 1023px in range mode, or 688px in single
|
|
92
|
+
* mode, the panel becomes a fixed sheet confined to the viewport (inset by the
|
|
93
|
+
* --drp-sheet-* tokens) with the presets on top. At or below 688px it also shows one
|
|
94
|
+
* month and stacks the date and time inputs. The breakpoints are fixed because a media
|
|
95
|
+
* query cannot read a custom property. Default: false, which leaves the panel exactly
|
|
96
|
+
* as it was.
|
|
97
|
+
*/
|
|
98
|
+
responsiveLayout?: boolean;
|
|
83
99
|
/** Snippet rendered in the time-picker slot. Consumer controls all time UI. */
|
|
84
100
|
timePicker?: Snippet;
|
|
85
101
|
/** Start of compare range (bindable). Used when compareCalendar snippet is provided. */
|