@r2digisolutions/components 0.8.5 → 0.8.7

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.
@@ -62,7 +62,22 @@
62
62
  const today = new Date();
63
63
  const todayIso = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
64
64
 
65
- let view = $state(new Date(today.getFullYear(), today.getMonth(), 1));
65
+ function monthFromIso(iso: string | undefined) {
66
+ if (!iso) return null;
67
+ const [y, m] = iso.split('-').map(Number);
68
+ if (!Number.isFinite(y) || !Number.isFinite(m)) return null;
69
+ return new Date(y, m - 1, 1);
70
+ }
71
+
72
+ function selectionIso() {
73
+ if (mode === 'range') return start;
74
+ if (mode === 'multiple') return values[0];
75
+ return value;
76
+ }
77
+
78
+ let view = $state(
79
+ monthFromIso(selectionIso()) ?? new Date(today.getFullYear(), today.getMonth(), 1)
80
+ );
66
81
  let panel = $state<Panel>('days');
67
82
  /** Which month column owns the month/year picker when months=2. */
68
83
  let pickerOffset = $state(0);
@@ -91,7 +91,8 @@
91
91
 
92
92
  let triggerEl = $state<HTMLElement | null>(null);
93
93
  let panelEl = $state<HTMLDivElement | null>(null);
94
- let panelStyle = $state('margin:0;inset:auto;');
94
+ /** Hide the native popover until top/left are applied (UA default is 0,0). */
95
+ let placed = $state(false);
95
96
  let activeField = $state<'start' | 'end' | 'value'>('value');
96
97
  let panelId = $state('');
97
98
 
@@ -128,9 +129,15 @@
128
129
  return { side, align: align ?? (months === 2 ? 'center' : 'start') };
129
130
  }
130
131
 
131
- function positionPanel() {
132
- if (!triggerEl || !panelEl) return;
133
- if (!panelEl.matches(':popover-open')) return;
132
+ function estimatePanelSize() {
133
+ const monthW = months === 2 ? 288 : 320;
134
+ const panelW = months === 2 ? monthW * 2 + 24 : monthW + 16;
135
+ return { panelW, panelH: 360 };
136
+ }
137
+
138
+ function positionPanel(opts: { measure?: boolean; show?: boolean } = {}) {
139
+ if (!triggerEl) return;
140
+ if (opts.show) placed = true;
134
141
 
135
142
  const trigger = triggerEl.getBoundingClientRect();
136
143
  if (trigger.width < 2 && trigger.height < 2) return;
@@ -142,9 +149,16 @@
142
149
  const viewTop = vv?.offsetTop ?? 0;
143
150
  const gap = 8;
144
151
  const pad = 8;
145
-
146
- const panelW = Math.min(panelEl.offsetWidth || 320, viewW - pad * 2);
147
- const panelH = Math.min(panelEl.offsetHeight || 360, 420);
152
+ const estimated = estimatePanelSize();
153
+ const canMeasure = opts.measure !== false && !!panelEl?.matches(':popover-open');
154
+ const panelW = Math.min(
155
+ canMeasure && panelEl?.offsetWidth ? panelEl.offsetWidth : estimated.panelW,
156
+ viewW - pad * 2
157
+ );
158
+ const panelH = Math.min(
159
+ canMeasure && panelEl?.offsetHeight ? panelEl.offsetHeight : estimated.panelH,
160
+ 420
161
+ );
148
162
 
149
163
  const fixed = parsePlacement(placement);
150
164
  let side: 'top' | 'bottom' = fixed?.side ?? 'bottom';
@@ -167,15 +181,19 @@
167
181
  left = Math.min(Math.max(viewLeft + pad, left), viewLeft + viewW - panelW - pad);
168
182
  top = Math.min(Math.max(viewTop + pad, top), viewTop + viewH - panelH - pad);
169
183
 
170
- panelStyle = [
171
- 'margin:0',
172
- 'inset:auto',
173
- `top:${Math.round(top)}px`,
174
- `left:${Math.round(left)}px`,
175
- 'right:auto',
176
- 'bottom:auto',
177
- 'width:max-content'
178
- ].join(';');
184
+ // Imperative styles in `beforetoggle`: a Svelte `style={}` binding
185
+ // flushes too late and the UA popover paints at 0,0 first.
186
+ if (panelEl) {
187
+ const s = panelEl.style;
188
+ s.setProperty('margin', '0');
189
+ s.setProperty('inset', 'auto');
190
+ s.setProperty('top', `${Math.round(top)}px`);
191
+ s.setProperty('left', `${Math.round(left)}px`);
192
+ s.setProperty('right', 'auto');
193
+ s.setProperty('bottom', 'auto');
194
+ s.setProperty('width', 'max-content');
195
+ s.setProperty('visibility', placed ? 'visible' : 'hidden');
196
+ }
179
197
  }
180
198
 
181
199
  function schedulePosition() {
@@ -183,7 +201,7 @@
183
201
  positionPanel();
184
202
  requestAnimationFrame(() => {
185
203
  positionPanel();
186
- requestAnimationFrame(positionPanel);
204
+ requestAnimationFrame(() => positionPanel({ show: true }));
187
205
  });
188
206
  });
189
207
  }
@@ -205,8 +223,16 @@
205
223
  }
206
224
 
207
225
  function handleBeforeToggle(event: ToggleEvent) {
208
- if (event.newState === 'open' && disabled) {
209
- event.preventDefault();
226
+ if (event.newState === 'open') {
227
+ if (disabled) {
228
+ event.preventDefault();
229
+ return;
230
+ }
231
+ placed = false;
232
+ // Set top/left before the first paint — UA popover defaults to 0,0.
233
+ positionPanel({ measure: false });
234
+ } else {
235
+ placed = false;
210
236
  }
211
237
  }
212
238
 
@@ -214,6 +240,7 @@
214
240
  const next = event.newState === 'open';
215
241
  open = next;
216
242
  if (next) schedulePosition();
243
+ else placed = false;
217
244
  }
218
245
 
219
246
  $effect(() => {
@@ -427,56 +454,58 @@
427
454
  aria-label="Choose date"
428
455
  onbeforetoggle={handleBeforeToggle}
429
456
  ontoggle={handleToggle}
430
- style={panelStyle}
457
+ data-placed={placed ? true : undefined}
431
458
  class={[
432
459
  'datepicker-popover m-0 w-max rounded-2xl border border-border bg-surface-elevated p-2 shadow-xl outline-none',
433
460
  months === 2 ? 'max-w-[min(42rem,calc(100vw-1rem))]' : 'max-w-[20rem]'
434
461
  ]}
435
462
  >
436
- {#if mode === 'single'}
437
- <Calendar
438
- mode="single"
439
- {months}
440
- bind:value
441
- {min}
442
- {max}
443
- {disabledDates}
444
- {enabledDates}
445
- {dots}
446
- framed={false}
447
- locale={dateLocale}
448
- onchange={handleChange}
449
- />
450
- {:else if mode === 'multiple'}
451
- <Calendar
452
- mode="multiple"
453
- {months}
454
- bind:values
455
- {min}
456
- {max}
457
- {disabledDates}
458
- {enabledDates}
459
- {dots}
460
- framed={false}
461
- locale={dateLocale}
462
- onchange={handleChange}
463
- />
464
- {:else}
465
- <Calendar
466
- mode="range"
467
- {months}
468
- bind:start
469
- bind:end
470
- {min}
471
- {max}
472
- {disabledDates}
473
- {enabledDates}
474
- {dots}
475
- framed={false}
476
- locale={dateLocale}
477
- onchange={handleChange}
478
- />
479
- {/if}
463
+ {#key `${open}:${start}:${value}`}
464
+ {#if mode === 'single'}
465
+ <Calendar
466
+ mode="single"
467
+ {months}
468
+ bind:value
469
+ {min}
470
+ {max}
471
+ {disabledDates}
472
+ {enabledDates}
473
+ {dots}
474
+ framed={false}
475
+ locale={dateLocale}
476
+ onchange={handleChange}
477
+ />
478
+ {:else if mode === 'multiple'}
479
+ <Calendar
480
+ mode="multiple"
481
+ {months}
482
+ bind:values
483
+ {min}
484
+ {max}
485
+ {disabledDates}
486
+ {enabledDates}
487
+ {dots}
488
+ framed={false}
489
+ locale={dateLocale}
490
+ onchange={handleChange}
491
+ />
492
+ {:else}
493
+ <Calendar
494
+ mode="range"
495
+ {months}
496
+ bind:start
497
+ bind:end
498
+ {min}
499
+ {max}
500
+ {disabledDates}
501
+ {enabledDates}
502
+ {dots}
503
+ framed={false}
504
+ locale={dateLocale}
505
+ onchange={handleChange}
506
+ />
507
+ {/if}
508
+ {/key}
480
509
  </div>
481
510
  </div>
482
511
 
@@ -494,6 +523,11 @@
494
523
  display: block;
495
524
  }
496
525
 
526
+ .datepicker-popover:popover-open:not([data-placed]) {
527
+ visibility: hidden;
528
+ pointer-events: none;
529
+ }
530
+
497
531
  .datepicker-popover:not(:popover-open) {
498
532
  display: none;
499
533
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@r2digisolutions/components",
3
- "version": "0.8.5",
3
+ "version": "0.8.7",
4
4
  "private": false,
5
5
  "description": "R2DigiSolutions Svelte 5 component library — Atomic Design, Tailwind 4, Light/Dark mode",
6
6
  "license": "MIT",