@r2digisolutions/components 0.8.6 → 0.8.8

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.
@@ -91,9 +91,13 @@
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('');
98
+ /** In-progress range while the popover is open. Parent props stay put until both days exist. */
99
+ let draftStart = $state('');
100
+ let draftEnd = $state('');
97
101
 
98
102
  $effect(() => {
99
103
  panelId ||= createId('datepicker');
@@ -110,6 +114,9 @@
110
114
  });
111
115
  }
112
116
 
117
+ const shownStart = $derived(open && mode === 'range' ? draftStart : start);
118
+ const shownEnd = $derived(open && mode === 'range' ? draftEnd : end);
119
+
113
120
  const fieldLabel = $derived.by(() => {
114
121
  if (mode === 'single') return value ? formatIso(value) : placeholder;
115
122
  if (mode === 'multiple') {
@@ -117,8 +124,8 @@
117
124
  if (values.length === 1) return formatIso(values[0]);
118
125
  return `${values.length} dates`;
119
126
  }
120
- if (start && end) return `${formatIso(start)} → ${formatIso(end)}`;
121
- if (start) return `${formatIso(start)} → …`;
127
+ if (shownStart && shownEnd) return `${formatIso(shownStart)} → ${formatIso(shownEnd)}`;
128
+ if (shownStart) return `${formatIso(shownStart)} → …`;
122
129
  return placeholder;
123
130
  });
124
131
 
@@ -128,9 +135,15 @@
128
135
  return { side, align: align ?? (months === 2 ? 'center' : 'start') };
129
136
  }
130
137
 
131
- function positionPanel() {
132
- if (!triggerEl || !panelEl) return;
133
- if (!panelEl.matches(':popover-open')) return;
138
+ function estimatePanelSize() {
139
+ const monthW = months === 2 ? 288 : 320;
140
+ const panelW = months === 2 ? monthW * 2 + 24 : monthW + 16;
141
+ return { panelW, panelH: 360 };
142
+ }
143
+
144
+ function positionPanel(opts: { measure?: boolean; show?: boolean } = {}) {
145
+ if (!triggerEl) return;
146
+ if (opts.show) placed = true;
134
147
 
135
148
  const trigger = triggerEl.getBoundingClientRect();
136
149
  if (trigger.width < 2 && trigger.height < 2) return;
@@ -142,9 +155,16 @@
142
155
  const viewTop = vv?.offsetTop ?? 0;
143
156
  const gap = 8;
144
157
  const pad = 8;
145
-
146
- const panelW = Math.min(panelEl.offsetWidth || 320, viewW - pad * 2);
147
- const panelH = Math.min(panelEl.offsetHeight || 360, 420);
158
+ const estimated = estimatePanelSize();
159
+ const canMeasure = opts.measure !== false && !!panelEl?.matches(':popover-open');
160
+ const panelW = Math.min(
161
+ canMeasure && panelEl?.offsetWidth ? panelEl.offsetWidth : estimated.panelW,
162
+ viewW - pad * 2
163
+ );
164
+ const panelH = Math.min(
165
+ canMeasure && panelEl?.offsetHeight ? panelEl.offsetHeight : estimated.panelH,
166
+ 420
167
+ );
148
168
 
149
169
  const fixed = parsePlacement(placement);
150
170
  let side: 'top' | 'bottom' = fixed?.side ?? 'bottom';
@@ -167,15 +187,19 @@
167
187
  left = Math.min(Math.max(viewLeft + pad, left), viewLeft + viewW - panelW - pad);
168
188
  top = Math.min(Math.max(viewTop + pad, top), viewTop + viewH - panelH - pad);
169
189
 
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(';');
190
+ // Imperative styles in `beforetoggle`: a Svelte `style={}` binding
191
+ // flushes too late and the UA popover paints at 0,0 first.
192
+ if (panelEl) {
193
+ const s = panelEl.style;
194
+ s.setProperty('margin', '0');
195
+ s.setProperty('inset', 'auto');
196
+ s.setProperty('top', `${Math.round(top)}px`);
197
+ s.setProperty('left', `${Math.round(left)}px`);
198
+ s.setProperty('right', 'auto');
199
+ s.setProperty('bottom', 'auto');
200
+ s.setProperty('width', 'max-content');
201
+ s.setProperty('visibility', placed ? 'visible' : 'hidden');
202
+ }
179
203
  }
180
204
 
181
205
  function schedulePosition() {
@@ -183,7 +207,7 @@
183
207
  positionPanel();
184
208
  requestAnimationFrame(() => {
185
209
  positionPanel();
186
- requestAnimationFrame(positionPanel);
210
+ requestAnimationFrame(() => positionPanel({ show: true }));
187
211
  });
188
212
  });
189
213
  }
@@ -205,15 +229,35 @@
205
229
  }
206
230
 
207
231
  function handleBeforeToggle(event: ToggleEvent) {
208
- if (event.newState === 'open' && disabled) {
209
- event.preventDefault();
232
+ if (event.newState === 'open') {
233
+ if (disabled) {
234
+ event.preventDefault();
235
+ return;
236
+ }
237
+ beginRangeDraft();
238
+ placed = false;
239
+ // Set top/left before the first paint — UA popover defaults to 0,0.
240
+ positionPanel({ measure: false });
241
+ } else {
242
+ placed = false;
243
+ }
244
+ }
245
+
246
+ function beginRangeDraft() {
247
+ draftStart = start;
248
+ draftEnd = end;
249
+ // Opening "Hasta" with a complete range: keep start, wait for a new end.
250
+ if (mode === 'range' && activeField === 'end' && start && end) {
251
+ draftEnd = '';
210
252
  }
211
253
  }
212
254
 
213
255
  function handleToggle(event: ToggleEvent) {
214
256
  const next = event.newState === 'open';
257
+ if (next) beginRangeDraft();
215
258
  open = next;
216
259
  if (next) schedulePosition();
260
+ else placed = false;
217
261
  }
218
262
 
219
263
  $effect(() => {
@@ -241,17 +285,23 @@
241
285
  start: string;
242
286
  end: string;
243
287
  }) {
288
+ if (mode === 'range') {
289
+ // Keep the in-progress day local. Parent (URL/filters) only gets a complete range.
290
+ if (detail.start && detail.end) {
291
+ start = detail.start;
292
+ end = detail.end;
293
+ onchange?.(detail);
294
+ if (closeOnSelect) setOpen(false);
295
+ }
296
+ return;
297
+ }
298
+
244
299
  onchange?.(detail);
245
300
 
246
301
  if (!closeOnSelect) return;
247
302
 
248
303
  if (mode === 'single' && detail.value) {
249
304
  setOpen(false);
250
- return;
251
- }
252
-
253
- if (mode === 'range' && detail.start && detail.end) {
254
- setOpen(false);
255
305
  }
256
306
  }
257
307
 
@@ -313,8 +363,8 @@
313
363
  ]}
314
364
  >
315
365
  <span class="text-[11px] font-medium tracking-wide text-muted uppercase">{startLabel}</span>
316
- <span class={['truncate text-sm', start ? 'text-primary' : 'text-muted']}>
317
- {start ? formatIso(start) : startPlaceholder}
366
+ <span class={['truncate text-sm', shownStart ? 'text-primary' : 'text-muted']}>
367
+ {shownStart ? formatIso(shownStart) : startPlaceholder}
318
368
  </span>
319
369
  </button>
320
370
  <div class="w-px self-stretch bg-border" aria-hidden="true"></div>
@@ -336,8 +386,8 @@
336
386
  ]}
337
387
  >
338
388
  <span class="text-[11px] font-medium tracking-wide text-muted uppercase">{endLabel}</span>
339
- <span class={['truncate text-sm', end ? 'text-primary' : 'text-muted']}>
340
- {end ? formatIso(end) : endPlaceholder}
389
+ <span class={['truncate text-sm', shownEnd ? 'text-primary' : 'text-muted']}>
390
+ {shownEnd ? formatIso(shownEnd) : endPlaceholder}
341
391
  </span>
342
392
  </button>
343
393
  {#if hasValue && !disabled}
@@ -427,13 +477,13 @@
427
477
  aria-label="Choose date"
428
478
  onbeforetoggle={handleBeforeToggle}
429
479
  ontoggle={handleToggle}
430
- style={panelStyle}
480
+ data-placed={placed ? true : undefined}
431
481
  class={[
432
482
  'datepicker-popover m-0 w-max rounded-2xl border border-border bg-surface-elevated p-2 shadow-xl outline-none',
433
483
  months === 2 ? 'max-w-[min(42rem,calc(100vw-1rem))]' : 'max-w-[20rem]'
434
484
  ]}
435
485
  >
436
- {#key `${open}:${start}:${value}`}
486
+ {#key open}
437
487
  {#if mode === 'single'}
438
488
  <Calendar
439
489
  mode="single"
@@ -466,8 +516,8 @@
466
516
  <Calendar
467
517
  mode="range"
468
518
  {months}
469
- bind:start
470
- bind:end
519
+ bind:start={draftStart}
520
+ bind:end={draftEnd}
471
521
  {min}
472
522
  {max}
473
523
  {disabledDates}
@@ -496,6 +546,11 @@
496
546
  display: block;
497
547
  }
498
548
 
549
+ .datepicker-popover:popover-open:not([data-placed]) {
550
+ visibility: hidden;
551
+ pointer-events: none;
552
+ }
553
+
499
554
  .datepicker-popover:not(:popover-open) {
500
555
  display: none;
501
556
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@r2digisolutions/components",
3
- "version": "0.8.6",
3
+ "version": "0.8.8",
4
4
  "private": false,
5
5
  "description": "R2DigiSolutions Svelte 5 component library — Atomic Design, Tailwind 4, Light/Dark mode",
6
6
  "license": "MIT",