@r2digisolutions/components 0.14.14 → 0.14.15
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.
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { on } from 'svelte/events';
|
|
2
3
|
import Calendar from '../Calendar/Calendar.svelte';
|
|
3
4
|
import type { CalendarDot } from '../Calendar/Calendar.svelte';
|
|
4
5
|
import type { TimeFormat } from '../TimePicker/TimePicker.svelte';
|
|
6
|
+
import { createId } from '../../../utils/id.js';
|
|
5
7
|
|
|
6
8
|
interface DateTimePickerProps {
|
|
7
9
|
/** Date part `YYYY-MM-DD`. */
|
|
@@ -44,9 +46,16 @@
|
|
|
44
46
|
onchange
|
|
45
47
|
}: DateTimePickerProps = $props();
|
|
46
48
|
|
|
47
|
-
let
|
|
49
|
+
let triggerEl = $state<HTMLElement | null>(null);
|
|
50
|
+
let panelEl = $state<HTMLDivElement | null>(null);
|
|
48
51
|
let hourListEl = $state<HTMLDivElement | null>(null);
|
|
49
52
|
let minuteListEl = $state<HTMLDivElement | null>(null);
|
|
53
|
+
let placed = $state(false);
|
|
54
|
+
let panelId = $state('');
|
|
55
|
+
|
|
56
|
+
$effect(() => {
|
|
57
|
+
panelId ||= createId('datetimepicker');
|
|
58
|
+
});
|
|
50
59
|
|
|
51
60
|
const hours24 = Array.from({ length: 24 }, (_, i) => i);
|
|
52
61
|
const minutes = $derived(
|
|
@@ -82,7 +91,6 @@
|
|
|
82
91
|
onchange?.({ date, time, value });
|
|
83
92
|
}
|
|
84
93
|
|
|
85
|
-
// Keep parts in sync when `value` is set externally
|
|
86
94
|
$effect(() => {
|
|
87
95
|
if (!value) return;
|
|
88
96
|
const [d, t] = value.includes('T') ? value.split('T') : [value, time];
|
|
@@ -122,22 +130,121 @@
|
|
|
122
130
|
return formatTimeDisplay(time);
|
|
123
131
|
});
|
|
124
132
|
|
|
133
|
+
function estimatePanelSize() {
|
|
134
|
+
return { panelW: 672, panelH: 380 };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function positionPanel(opts: { measure?: boolean; show?: boolean } = {}) {
|
|
138
|
+
if (!triggerEl) return;
|
|
139
|
+
if (opts.show) placed = true;
|
|
140
|
+
|
|
141
|
+
const trigger = triggerEl.getBoundingClientRect();
|
|
142
|
+
if (trigger.width < 2 && trigger.height < 2) return;
|
|
143
|
+
|
|
144
|
+
const vv = window.visualViewport;
|
|
145
|
+
const viewW = vv?.width ?? window.innerWidth;
|
|
146
|
+
const viewH = vv?.height ?? window.innerHeight;
|
|
147
|
+
const viewLeft = vv?.offsetLeft ?? 0;
|
|
148
|
+
const viewTop = vv?.offsetTop ?? 0;
|
|
149
|
+
const gap = 8;
|
|
150
|
+
const pad = 8;
|
|
151
|
+
const estimated = estimatePanelSize();
|
|
152
|
+
const canMeasure = opts.measure !== false && !!panelEl?.matches(':popover-open');
|
|
153
|
+
const panelW = Math.min(
|
|
154
|
+
canMeasure && panelEl?.offsetWidth ? panelEl.offsetWidth : estimated.panelW,
|
|
155
|
+
viewW - pad * 2
|
|
156
|
+
);
|
|
157
|
+
const panelH = Math.min(
|
|
158
|
+
canMeasure && panelEl?.offsetHeight ? panelEl.offsetHeight : estimated.panelH,
|
|
159
|
+
viewH - pad * 2
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const spaceBelow = viewTop + viewH - trigger.bottom - gap - pad;
|
|
163
|
+
const spaceAbove = trigger.top - viewTop - gap - pad;
|
|
164
|
+
const side: 'top' | 'bottom' = spaceBelow < panelH && spaceAbove > spaceBelow ? 'top' : 'bottom';
|
|
165
|
+
|
|
166
|
+
let top = side === 'bottom' ? trigger.bottom + gap : trigger.top - panelH - gap;
|
|
167
|
+
let left = trigger.left;
|
|
168
|
+
|
|
169
|
+
left = Math.min(Math.max(viewLeft + pad, left), viewLeft + viewW - panelW - pad);
|
|
170
|
+
top = Math.min(Math.max(viewTop + pad, top), viewTop + viewH - panelH - pad);
|
|
171
|
+
|
|
172
|
+
if (panelEl) {
|
|
173
|
+
const s = panelEl.style;
|
|
174
|
+
s.setProperty('margin', '0');
|
|
175
|
+
s.setProperty('inset', 'auto');
|
|
176
|
+
s.setProperty('top', `${Math.round(top)}px`);
|
|
177
|
+
s.setProperty('left', `${Math.round(left)}px`);
|
|
178
|
+
s.setProperty('right', 'auto');
|
|
179
|
+
s.setProperty('bottom', 'auto');
|
|
180
|
+
s.setProperty('width', 'max-content');
|
|
181
|
+
s.setProperty('visibility', placed ? 'visible' : 'hidden');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function schedulePosition() {
|
|
186
|
+
queueMicrotask(() => {
|
|
187
|
+
positionPanel();
|
|
188
|
+
requestAnimationFrame(() => {
|
|
189
|
+
positionPanel();
|
|
190
|
+
requestAnimationFrame(() => positionPanel({ show: true }));
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function syncNative() {
|
|
196
|
+
if (!panelEl) return;
|
|
197
|
+
const isOpen = panelEl.matches(':popover-open');
|
|
198
|
+
try {
|
|
199
|
+
if (open && !isOpen) panelEl.showPopover();
|
|
200
|
+
else if (!open && isOpen) panelEl.hidePopover();
|
|
201
|
+
} catch {
|
|
202
|
+
/* ignore */
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
125
206
|
function setOpen(next: boolean) {
|
|
126
|
-
if (disabled) return;
|
|
207
|
+
if (disabled && next) return;
|
|
127
208
|
open = next;
|
|
128
209
|
}
|
|
129
210
|
|
|
130
|
-
function
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
211
|
+
function handleBeforeToggle(event: ToggleEvent) {
|
|
212
|
+
if (event.newState === 'open') {
|
|
213
|
+
if (disabled) {
|
|
214
|
+
event.preventDefault();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
placed = false;
|
|
218
|
+
positionPanel({ measure: false });
|
|
219
|
+
} else {
|
|
220
|
+
placed = false;
|
|
221
|
+
}
|
|
135
222
|
}
|
|
136
223
|
|
|
137
|
-
function
|
|
138
|
-
|
|
224
|
+
function handleToggle(event: ToggleEvent) {
|
|
225
|
+
open = event.newState === 'open';
|
|
226
|
+
if (open) schedulePosition();
|
|
227
|
+
else placed = false;
|
|
139
228
|
}
|
|
140
229
|
|
|
230
|
+
$effect(() => {
|
|
231
|
+
open;
|
|
232
|
+
queueMicrotask(() => {
|
|
233
|
+
syncNative();
|
|
234
|
+
if (open) positionPanel();
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
$effect(() => {
|
|
239
|
+
if (!open) return;
|
|
240
|
+
const offResize = on(window, 'resize', () => positionPanel());
|
|
241
|
+
const offScroll = on(window, 'scroll', () => positionPanel(), { capture: true });
|
|
242
|
+
return () => {
|
|
243
|
+
offResize();
|
|
244
|
+
offScroll();
|
|
245
|
+
};
|
|
246
|
+
});
|
|
247
|
+
|
|
141
248
|
function handleDateChange(detail: { value: string }) {
|
|
142
249
|
date = detail.value;
|
|
143
250
|
if (!time) time = '09:00';
|
|
@@ -184,14 +291,13 @@
|
|
|
184
291
|
});
|
|
185
292
|
</script>
|
|
186
293
|
|
|
187
|
-
<
|
|
188
|
-
|
|
189
|
-
<div class={['relative w-full min-w-[20rem] max-w-[22rem]', className]} bind:this={rootEl}>
|
|
294
|
+
<div class={['w-full min-w-[20rem] max-w-[22rem]', className]}>
|
|
190
295
|
{#if label}
|
|
191
296
|
<span class="mb-1.5 block text-sm font-medium text-primary">{label}</span>
|
|
192
297
|
{/if}
|
|
193
298
|
|
|
194
299
|
<div
|
|
300
|
+
bind:this={triggerEl}
|
|
195
301
|
class={[
|
|
196
302
|
'flex h-10 w-full items-center overflow-hidden rounded-xl border border-border bg-surface-elevated transition-colors',
|
|
197
303
|
open && 'border-brand-500 ring-2 ring-brand-500/20',
|
|
@@ -201,8 +307,11 @@
|
|
|
201
307
|
<button
|
|
202
308
|
type="button"
|
|
203
309
|
{disabled}
|
|
204
|
-
|
|
310
|
+
popovertarget={panelId}
|
|
311
|
+
popovertargetaction="toggle"
|
|
205
312
|
aria-expanded={open}
|
|
313
|
+
aria-haspopup="dialog"
|
|
314
|
+
aria-controls={panelId}
|
|
206
315
|
class={[
|
|
207
316
|
'flex h-full min-w-0 flex-1 items-center gap-2 px-3.5 text-left text-sm',
|
|
208
317
|
'hover:bg-surface-overlay focus-visible:outline-none',
|
|
@@ -241,14 +350,19 @@
|
|
|
241
350
|
{/if}
|
|
242
351
|
</div>
|
|
243
352
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
353
|
+
<div
|
|
354
|
+
bind:this={panelEl}
|
|
355
|
+
id={panelId}
|
|
356
|
+
popover="auto"
|
|
357
|
+
role="dialog"
|
|
358
|
+
aria-label="Choose date and time"
|
|
359
|
+
onbeforetoggle={handleBeforeToggle}
|
|
360
|
+
ontoggle={handleToggle}
|
|
361
|
+
data-placed={placed ? true : undefined}
|
|
362
|
+
class="datetime-picker-popover m-0 flex w-max max-w-[min(100vw-1rem,42rem)] flex-col overflow-hidden rounded-2xl border border-border bg-surface-elevated p-2 shadow-xl outline-none sm:flex-row sm:items-stretch"
|
|
363
|
+
>
|
|
364
|
+
{#key open}
|
|
365
|
+
<div class="w-[22rem] shrink-0 p-1.5">
|
|
252
366
|
<Calendar
|
|
253
367
|
mode="single"
|
|
254
368
|
bind:value={date}
|
|
@@ -320,6 +434,29 @@
|
|
|
320
434
|
</div>
|
|
321
435
|
</div>
|
|
322
436
|
</div>
|
|
323
|
-
|
|
324
|
-
|
|
437
|
+
{/key}
|
|
438
|
+
</div>
|
|
325
439
|
</div>
|
|
440
|
+
|
|
441
|
+
<style>
|
|
442
|
+
.datetime-picker-popover {
|
|
443
|
+
position: fixed;
|
|
444
|
+
inset: unset;
|
|
445
|
+
margin: 0;
|
|
446
|
+
width: max-content;
|
|
447
|
+
height: max-content;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
.datetime-picker-popover:popover-open {
|
|
451
|
+
display: flex;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
.datetime-picker-popover:popover-open:not([data-placed]) {
|
|
455
|
+
visibility: hidden;
|
|
456
|
+
pointer-events: none;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
.datetime-picker-popover:not(:popover-open) {
|
|
460
|
+
display: none;
|
|
461
|
+
}
|
|
462
|
+
</style>
|
|
@@ -105,6 +105,9 @@
|
|
|
105
105
|
{disabled}
|
|
106
106
|
{size}
|
|
107
107
|
{status}
|
|
108
|
+
{min}
|
|
109
|
+
{max}
|
|
110
|
+
{step}
|
|
108
111
|
type="number"
|
|
109
112
|
bind:value={text}
|
|
110
113
|
onblur={() => commit(text)}
|
|
@@ -151,6 +154,9 @@
|
|
|
151
154
|
{disabled}
|
|
152
155
|
{size}
|
|
153
156
|
{status}
|
|
157
|
+
{min}
|
|
158
|
+
{max}
|
|
159
|
+
{step}
|
|
154
160
|
type="number"
|
|
155
161
|
bind:value={text}
|
|
156
162
|
onblur={() => commit(text)}
|
|
@@ -193,6 +199,9 @@
|
|
|
193
199
|
{disabled}
|
|
194
200
|
{size}
|
|
195
201
|
{status}
|
|
202
|
+
{min}
|
|
203
|
+
{max}
|
|
204
|
+
{step}
|
|
196
205
|
type="number"
|
|
197
206
|
bind:value={text}
|
|
198
207
|
onblur={() => commit(text)}
|