vanilla-jui 1.4.7 → 1.5.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.
Files changed (52) hide show
  1. package/README.md +10 -4
  2. package/dist/index.d.ts +418 -970
  3. package/dist/index.js +1 -1
  4. package/dist/index.umd.js +1 -1
  5. package/dist/style.css +1 -1
  6. package/package.json +3 -3
  7. package/src/components/accordion.ts +466 -346
  8. package/src/components/flow.ts +779 -1133
  9. package/src/components/form.ts +666 -509
  10. package/src/components/menu.ts +198 -328
  11. package/src/components/modal.ts +510 -747
  12. package/src/components/offcanvas.ts +273 -367
  13. package/src/components/pagination.ts +201 -375
  14. package/src/components/swiper.ts +689 -1107
  15. package/src/components/tabs.ts +467 -778
  16. package/src/components/toc.ts +191 -244
  17. package/src/core/component.ts +332 -0
  18. package/src/core/motion.ts +298 -0
  19. package/src/core/presence.ts +233 -0
  20. package/src/core/scheduler.ts +91 -0
  21. package/src/core/view.ts +31 -0
  22. package/src/css/accordion.css +94 -3
  23. package/src/css/content.css +18 -24
  24. package/src/css/drop.css +4 -1
  25. package/src/css/form.css +8 -0
  26. package/src/css/menu.css +10 -5
  27. package/src/css/modal.css +2 -1
  28. package/src/css/offcanvas.css +5 -17
  29. package/src/css/swiper.css +2 -1
  30. package/src/css/tabs.css +8 -17
  31. package/src/css/themes/default/accordion.css +0 -48
  32. package/src/css/themes/default/swiper.css +1 -1
  33. package/src/css/themes/default/tabs.css +4 -6
  34. package/src/css/toast.css +1 -17
  35. package/src/css/tooltip.css +2 -1
  36. package/src/index.ts +8 -2
  37. package/src/locales/index.ts +3 -0
  38. package/src/primitives/drop.ts +253 -262
  39. package/src/primitives/loading.ts +22 -4
  40. package/src/primitives/parabola.ts +123 -164
  41. package/src/primitives/sticky.ts +297 -0
  42. package/src/primitives/theme.ts +164 -222
  43. package/src/primitives/toast.ts +309 -284
  44. package/src/primitives/tooltip.ts +65 -66
  45. package/src/utilities/browser.ts +38 -0
  46. package/src/utilities/dom.ts +0 -54
  47. package/src/utilities/refs.ts +61 -0
  48. package/src/utilities/state.ts +73 -0
  49. package/src/utilities/timer.ts +11 -31
  50. package/src/validation/validator.ts +289 -389
  51. package/src/components/sticky.ts +0 -297
  52. package/src/core/Component.ts +0 -377
@@ -1,23 +1,23 @@
1
1
  import {
2
2
  createDeepStore,
3
3
  createEffect,
4
- createRoot,
5
4
  flushSync,
5
+ insert,
6
6
  jsx,
7
7
  untrack,
8
8
  } from 'vanilla-signal';
9
9
 
10
- import Component, {
11
- type ComponentDOM,
12
- type ComponentRuntime,
13
- } from '../core/Component.ts';
14
- import { createLoading } from '../primitives/loading.ts';
15
10
  import {
16
- type RenderableContent,
17
- normalizeContentNodes,
18
- } from '../utilities/dom.ts';
11
+ type FunctionalComponent,
12
+ defineComponent,
13
+ } from '../core/component.ts';
14
+ import { createLoading } from '../primitives/loading.ts';
15
+ import { type RenderableContent } from '../utilities/dom.ts';
19
16
  import { randomId } from '../utilities/id.ts';
20
- import { timer } from '../utilities/timer.ts';
17
+ import { createEventManager } from '../utilities/events.ts';
18
+ import { createMotionGroup, createTransition } from '../core/motion.ts';
19
+ import { createPresence } from '../core/presence.ts';
20
+ import { createElementRef } from '../utilities/refs.ts';
21
21
  import {
22
22
  type ResolveSchema,
23
23
  resolveProps,
@@ -79,38 +79,35 @@ interface ResolvedOffcanvasProps extends Record<string, unknown> {
79
79
 
80
80
  interface OffcanvasState extends Record<string, unknown> {
81
81
  content: OffcanvasContent;
82
+ resolvedContent: RenderableContent<Offcanvas>;
82
83
  visible: boolean;
83
84
  loading: boolean;
84
85
  }
85
86
 
86
- interface OffcanvasDOM extends ComponentDOM {
87
- root: HTMLElement | null;
88
- overlay: HTMLElement | null;
89
- content: HTMLElement | null;
90
- }
91
-
92
87
  interface OffcanvasCache {
93
88
  content: RenderableContent<Offcanvas>;
94
89
  hasContent: boolean;
95
90
  updatedAt: number;
96
91
  }
97
92
 
98
- interface OffcanvasRuntime extends ComponentRuntime {
99
- built: boolean;
93
+ interface OffcanvasRuntimeExtras {
100
94
  cache: OffcanvasCache;
101
95
  contentLoadId: number;
102
96
  }
103
97
 
104
- interface OffcanvasCleanupExtras {
105
- state?: (() => void) | null;
106
- }
107
-
108
98
  const DEFAULT_CLASS_NAMES: OffcanvasClassNames = {
109
99
  root: 'j-offcanvas',
110
100
  overlay: 'j-offcanvas-overlay',
111
101
  content: 'offcanvas-content',
112
102
  };
113
103
 
104
+ const OFFCANVAS_HIDDEN_TRANSFORM: Record<OffcanvasDirection, string> = {
105
+ top: 'translateY(-100%)',
106
+ right: 'translateX(100%)',
107
+ bottom: 'translateY(100%)',
108
+ left: 'translateX(-100%)',
109
+ };
110
+
114
111
  const OFFCANVAS_PROPS_SCHEMA = {
115
112
  content: {
116
113
  default: '',
@@ -160,6 +157,9 @@ const OFFCANVAS_PROPS_SCHEMA = {
160
157
 
161
158
  const OFFCANVAS_STATE_SCHEMA = {
162
159
  content: OFFCANVAS_PROPS_SCHEMA.content,
160
+ resolvedContent: { type: 'renderable' },
161
+ visible: { type: 'boolean' },
162
+ loading: { type: 'boolean' },
163
163
  };
164
164
 
165
165
  function normalizeTtl(ttl: number): number {
@@ -188,389 +188,295 @@ function normalizeProps(input: OffcanvasProps): ResolvedOffcanvasProps {
188
188
  };
189
189
  }
190
190
 
191
- class OffcanvasComponent extends Component<
192
- ResolvedOffcanvasProps,
193
- OffcanvasState,
194
- OffcanvasDOM
195
- > {
196
- declare runtime: OffcanvasRuntime;
197
- declare state: OffcanvasState;
198
- declare cleanup: Component['cleanup'] & OffcanvasCleanupExtras;
199
-
200
- constructor(input: OffcanvasProps = {}) {
201
- const props = normalizeProps(input);
202
- super(props);
203
-
204
- this.dom.overlay = null;
205
- this.dom.content = null;
206
-
207
- this.cleanup.state = null;
208
-
209
- this.runtime.built = false;
210
- this.runtime.cache = {
211
- content: null,
212
- hasContent: false,
213
- updatedAt: 0,
214
- };
215
- this.runtime.contentLoadId = 0;
216
-
217
- this.state = createDeepStore({
218
- content: props.content,
219
- visible: false,
220
- loading: false,
221
- }) as OffcanvasState;
222
- }
223
-
224
- build(): this {
225
- if (this.runtime.destroyed) {
226
- throw new Error('Offcanvas.build: instance has been destroyed.');
227
- }
228
- if (this.runtime.built) return this;
229
-
230
- try {
231
- this.dom.root = this.buildRoot();
232
- this.runtime.built = true;
233
- this.bindState();
234
- } catch (error) {
235
- this.destroy();
236
- throw error;
237
- }
238
-
239
- this.emit('init', this.props);
240
- return this;
241
- }
242
-
243
- private assertBuilt(method: string): void {
244
- if (this.runtime.destroyed) {
245
- throw new Error(`Offcanvas.${method}: instance has been destroyed.`);
246
- }
247
- if (!this.runtime.built) {
248
- throw new Error(`Offcanvas.${method}: call build() first.`);
249
- }
250
- }
251
-
252
- private buildRoot(): HTMLElement {
253
- if (this.props.overlay) {
254
- this.dom.overlay = this.buildOverlay();
255
- }
256
-
257
- return this.buildPanel();
258
- }
259
-
260
- private buildOverlay(): HTMLElement {
261
- const filterStyle = this.props.filter
262
- ? { backdropFilter: 'blur(2px)' }
263
- : {};
264
- return jsx('div', {
265
- className: this.props.className.overlay,
266
- hidden: true,
267
- 'data-offcanvas-overlay': this.props.id,
268
- style: {
269
- ...filterStyle,
270
- },
271
- }) as HTMLElement;
272
- }
191
+ interface OffcanvasActions {
192
+ show(): Promise<void>;
193
+ hide(): Promise<void>;
194
+ }
273
195
 
274
- private buildPanel(): HTMLElement {
275
- const content = jsx('div', {
276
- className: this.props.className.content,
277
- 'data-offcanvas-content': this.props.id,
278
- }) as HTMLElement;
279
- this.dom.content = content;
196
+ let offcanvasScrollLockCount = 0;
197
+ let offcanvasBodyOverflow = '';
280
198
 
281
- if (typeof this.state.content !== 'function') {
282
- this.renderContent(this.state.content);
199
+ export type Offcanvas = FunctionalComponent<
200
+ ResolvedOffcanvasProps,
201
+ OffcanvasState,
202
+ HTMLElement,
203
+ OffcanvasActions
204
+ >;
205
+
206
+ function validateStatePatch(patch: Partial<OffcanvasState>): void {
207
+ validateParam('state', patch, { type: 'plainObject' }, 'Offcanvas.setState');
208
+ for (const key of Object.keys(patch)) {
209
+ if (!Object.hasOwn(OFFCANVAS_STATE_SCHEMA, key)) {
210
+ throw new Error(
211
+ `Offcanvas.setState: "${key}" is not a supported state key.`
212
+ );
283
213
  }
284
-
285
- return jsx('div', {
286
- className: this.props.className.root,
287
- id: this.props.id,
288
- role: 'dialog',
289
- 'aria-modal': this.props.overlay ? 'true' : 'false',
290
- 'aria-expanded': 'false',
291
- 'data-offcanvas': 'root',
292
- 'data-direction': this.props.direction,
293
- 'data-animate': this.props.animate,
294
- children: content,
295
- }) as HTMLElement;
296
- }
297
-
298
- private bindState(): void {
299
- if (this.cleanup.state) return;
300
-
301
- this.cleanup.state = createRoot((dispose) => {
302
- let initialized = false;
303
- createEffect(() => {
304
- const content = this.state.content;
305
- if (!initialized) {
306
- initialized = true;
307
- return;
308
- }
309
- void untrack(() => this.syncContent(content));
310
- });
311
- return dispose;
312
- });
313
- }
314
-
315
- private async syncContent(content: OffcanvasContent): Promise<void> {
316
- if (!this.runtime.built) return;
214
+ const stateKey = key as keyof typeof OFFCANVAS_STATE_SCHEMA;
317
215
  validateParam(
318
- 'content',
319
- content,
320
- OFFCANVAS_STATE_SCHEMA.content,
321
- 'Offcanvas.state'
216
+ key,
217
+ patch[key as keyof OffcanvasState],
218
+ OFFCANVAS_STATE_SCHEMA[stateKey],
219
+ 'Offcanvas.setState'
322
220
  );
323
- this.runtime.cache.hasContent = false;
324
- this.runtime.cache.content = null;
325
- this.runtime.cache.updatedAt = 0;
326
-
327
- if (typeof content === 'function') {
328
- if (this.state.visible) await this.loadContent();
329
- else this.clearContent();
330
- return;
331
- }
332
-
333
- this.runtime.contentLoadId += 1;
334
- flushSync(() => {
335
- this.state.loading = false;
336
- });
337
- this.renderContent(content);
338
221
  }
222
+ }
339
223
 
340
- private isCacheValid(): boolean {
341
- if (!this.props.cache || !this.runtime.cache.hasContent) return false;
342
-
343
- const ttl = normalizeTtl(this.props.ttl);
344
- return !ttl || Date.now() - this.runtime.cache.updatedAt <= ttl;
345
- }
224
+ export function createOffcanvas(input: OffcanvasProps = {}): Offcanvas {
225
+ const props = normalizeProps(input);
226
+ const state = createDeepStore({
227
+ content: props.content,
228
+ resolvedContent: typeof props.content === 'function' ? null : props.content,
229
+ visible: false,
230
+ loading: false,
231
+ }) as OffcanvasState;
232
+ const runtime: OffcanvasRuntimeExtras = {
233
+ cache: { content: null, hasContent: false, updatedAt: 0 },
234
+ contentLoadId: 0,
235
+ };
236
+ const events = createEventManager();
237
+ const overlayRef = createElementRef<HTMLElement>();
238
+ let offcanvas!: Offcanvas;
239
+ let scrollLocked = false;
240
+
241
+ const lockScroll = (): void => {
242
+ if (!props.bodyOverflow || scrollLocked) return;
243
+ if (offcanvasScrollLockCount === 0) {
244
+ offcanvasBodyOverflow = document.body.style.overflow;
245
+ document.body.style.overflow = 'hidden';
246
+ }
247
+ offcanvasScrollLockCount += 1;
248
+ scrollLocked = true;
249
+ };
250
+ const unlockScroll = (): void => {
251
+ if (!scrollLocked) return;
252
+ offcanvasScrollLockCount = Math.max(0, offcanvasScrollLockCount - 1);
253
+ if (offcanvasScrollLockCount === 0) {
254
+ document.body.style.overflow = offcanvasBodyOverflow;
255
+ offcanvasBodyOverflow = '';
256
+ }
257
+ scrollLocked = false;
258
+ };
346
259
 
347
- private clearContent(): void {
348
- if (this.dom.content) this.dom.content.textContent = '';
349
- }
260
+ const panelMotion = createTransition(() => offcanvas.element, {
261
+ keyframes: [
262
+ { transform: OFFCANVAS_HIDDEN_TRANSFORM[props.direction] },
263
+ { transform: 'translate(0, 0)' },
264
+ ],
265
+ options: { duration: 300, easing: 'ease' },
266
+ });
267
+ const motion = createMotionGroup(
268
+ ...(props.animate === 'slide' ? [panelMotion] : []),
269
+ createTransition(() => overlayRef.current, {
270
+ keyframes: [{ opacity: 0 }, { opacity: 1 }],
271
+ options: { duration: 300, easing: 'ease' },
272
+ })
273
+ );
274
+
275
+ const presence = createPresence({
276
+ elements: () => [overlayRef.current, offcanvas.element],
277
+ mount: () => {
278
+ const overlay = overlayRef.current;
279
+ if (overlay && !overlay.isConnected) insert(document.body, overlay);
280
+ offcanvas.mount(document.body);
281
+ lockScroll();
282
+ },
283
+ activate: () => {
284
+ state.visible = true;
285
+ },
286
+ deactivate: () => {
287
+ state.visible = false;
288
+ },
289
+ motion,
290
+ unmount: () => {
291
+ overlayRef.current?.remove();
292
+ offcanvas.unmount();
293
+ unlockScroll();
294
+ },
295
+ });
350
296
 
351
- private renderContent(content: RenderableContent<Offcanvas>): void {
352
- if (!this.dom.content) return;
297
+ const clearCache = (): void => {
298
+ runtime.cache = { content: null, hasContent: false, updatedAt: 0 };
299
+ };
353
300
 
354
- this.clearContent();
355
- this.dom.content.append(...normalizeContentNodes(content, this));
356
- }
301
+ const isCacheValid = (): boolean => {
302
+ if (!props.cache || !runtime.cache.hasContent) return false;
303
+ const ttl = normalizeTtl(props.ttl);
304
+ return !ttl || Date.now() - runtime.cache.updatedAt <= ttl;
305
+ };
357
306
 
358
- private async loadContent(): Promise<void> {
359
- const { cache } = this.props;
360
- const content = this.state.content;
307
+ const loadContent = async (): Promise<void> => {
308
+ const content = state.content;
361
309
  if (typeof content !== 'function') {
362
- this.runtime.contentLoadId += 1;
310
+ runtime.contentLoadId += 1;
363
311
  flushSync(() => {
364
- this.state.loading = false;
312
+ state.resolvedContent = content;
313
+ state.loading = false;
365
314
  });
366
315
  return;
367
316
  }
368
-
369
- if (this.isCacheValid()) {
370
- this.runtime.contentLoadId += 1;
371
- this.renderContent(this.runtime.cache.content);
317
+ if (isCacheValid()) {
318
+ runtime.contentLoadId += 1;
372
319
  flushSync(() => {
373
- this.state.loading = false;
320
+ state.resolvedContent = runtime.cache.content;
321
+ state.loading = false;
374
322
  });
375
323
  return;
376
324
  }
377
325
 
378
- const loadId = ++this.runtime.contentLoadId;
326
+ const loadId = ++runtime.contentLoadId;
379
327
  flushSync(() => {
380
- this.state.loading = true;
328
+ state.loading = true;
329
+ state.resolvedContent = null;
381
330
  });
382
-
383
- if (this.dom.content) {
384
- this.clearContent();
385
- this.dom.content.appendChild(createLoading());
386
- }
387
-
388
331
  try {
389
- const result = await Promise.resolve(content(this));
390
-
391
- if (this.runtime.destroyed || loadId !== this.runtime.contentLoadId) {
332
+ const result = await Promise.resolve(content(offcanvas));
333
+ if (offcanvas.runtime.destroyed || loadId !== runtime.contentLoadId)
392
334
  return;
335
+ if (props.cache) {
336
+ runtime.cache = {
337
+ content: result,
338
+ hasContent: true,
339
+ updatedAt: Date.now(),
340
+ };
393
341
  }
394
-
395
- if (cache) {
396
- this.runtime.cache.content = result;
397
- this.runtime.cache.hasContent = true;
398
- this.runtime.cache.updatedAt = Date.now();
399
- }
400
- this.renderContent(result);
342
+ flushSync(() => {
343
+ state.resolvedContent = result;
344
+ });
401
345
  } finally {
402
- if (!this.runtime.destroyed && loadId === this.runtime.contentLoadId) {
346
+ if (!offcanvas.runtime.destroyed && loadId === runtime.contentLoadId) {
403
347
  flushSync(() => {
404
- this.state.loading = false;
348
+ state.loading = false;
405
349
  });
406
350
  }
407
351
  }
408
- }
409
-
410
- private bindEvents(): void {
411
- this.unbindEvents();
412
- const { bgClose, escClose } = this.props;
413
-
414
- if (this.dom.overlay && bgClose) {
415
- this.cleanup.events.on('overlay', this.dom.overlay, 'click', () => {
416
- void this.hide();
417
- });
418
- }
419
- if (escClose) {
420
- this.cleanup.events.on('esc', document, 'keydown', (event) => {
421
- if (
422
- event instanceof KeyboardEvent &&
423
- event.key === 'Escape' &&
424
- this.state.visible
425
- ) {
426
- void this.hide();
427
- }
428
- });
429
- }
430
- if (this.dom.root) {
431
- this.cleanup.events.on('close', this.dom.root, 'click', (event) => {
432
- if (!(event.target instanceof Element)) return;
433
- const action = event.target.closest<HTMLElement>('[data-action]');
434
- const name = action?.dataset.action;
435
- if (name === 'close' || name === 'cancel') void this.hide();
436
- });
437
- }
438
- }
439
-
440
- private unbindEvents(): void {
441
- this.cleanup.events.clear();
442
- }
443
-
444
- private showPanel(): void {
445
- const { id } = this.props;
446
- timer.cancel(`oc-hide-${id}`);
447
-
448
- if (this.dom.overlay && !this.dom.overlay.isConnected) {
449
- this.dom.overlay.hidden = false;
450
- document.body.appendChild(this.dom.overlay);
451
- }
452
- if (this.dom.root && !this.dom.root.isConnected) {
453
- document.body.appendChild(this.dom.root);
454
- }
455
- if (this.props.bodyOverflow) {
456
- document.body.style.overflow = 'hidden';
457
- }
458
-
459
- timer.start(`oc-show-${id}`, 10, () => {
460
- this.dom.root?.setAttribute('aria-expanded', 'true');
461
- });
462
- }
463
-
464
- private hidePanel(): void {
465
- const { id } = this.props;
466
- timer.cancel(`oc-show-${id}`);
467
- timer.cancel(`oc-shown-${id}`);
468
-
469
- this.dom.root?.setAttribute('aria-expanded', 'false');
352
+ };
470
353
 
471
- timer.start(`oc-hide-${id}`, 100, () => {
472
- this.dom.overlay?.remove();
473
- this.dom.root?.remove();
474
- if (this.props.bodyOverflow) {
475
- document.body.style.overflow = '';
354
+ const actions: OffcanvasActions = {
355
+ async show() {
356
+ if (!offcanvas.runtime.built) {
357
+ throw new Error('Offcanvas.show: call build() first.');
476
358
  }
477
- });
478
- }
479
-
480
- async show(): Promise<void> {
481
- this.assertBuilt('show');
482
- if (this.state.visible) return;
483
-
484
- const { onShow, onShown, id } = this.props;
485
- if (onShow) await Promise.resolve(onShow(this));
359
+ if (state.visible) return;
360
+ await Promise.resolve(props.onShow?.(offcanvas));
486
361
 
487
- this.showPanel();
488
- this.bindEvents();
489
-
490
- flushSync(() => {
491
- this.state.visible = true;
492
- });
493
-
494
- await this.loadContent();
495
-
496
- timer.start(`oc-shown-${id}`, 300, () => {
497
- if (this.runtime.destroyed) return;
498
- void onShown?.(this);
499
- });
500
- }
501
-
502
- async hide(): Promise<void> {
503
- this.assertBuilt('hide');
504
- if (!this.state.visible) return;
505
-
506
- const { onHide, onHidden } = this.props;
507
- if (onHide) await Promise.resolve(onHide(this));
508
-
509
- this.runtime.contentLoadId += 1;
510
- flushSync(() => {
511
- this.state.loading = false;
512
- });
513
-
514
- this.unbindEvents();
515
- this.hidePanel();
516
-
517
- flushSync(() => {
518
- this.state.visible = false;
519
- });
520
-
521
- void onHidden?.(this);
522
- }
523
-
524
- protected onDestroy(): void {
525
- const { id } = this.props;
526
-
527
- timer.cancel(`oc-show-${id}`);
528
- timer.cancel(`oc-hide-${id}`);
529
- timer.cancel(`oc-shown-${id}`);
530
-
531
- this.unbindEvents();
532
- this.cleanup.state?.();
533
- this.cleanup.state = null;
534
-
535
- this.dom.root?.setAttribute('aria-expanded', 'false');
536
- this.dom.overlay?.remove();
537
- this.dom.root?.remove();
538
- if (this.props.bodyOverflow) {
539
- document.body.style.overflow = '';
540
- }
362
+ const entering = presence.enter();
363
+ const loading = loadContent();
364
+ if ((await entering) && !offcanvas.runtime.destroyed) {
365
+ void props.onShown?.(offcanvas);
366
+ }
367
+ await loading;
368
+ },
369
+ async hide() {
370
+ if (!offcanvas.runtime.built) {
371
+ throw new Error('Offcanvas.hide: call build() first.');
372
+ }
373
+ if (!state.visible) return;
374
+ const onHideResult = props.onHide?.(offcanvas);
375
+ if (onHideResult) await onHideResult;
541
376
 
542
- this.runtime.built = false;
543
- }
377
+ runtime.contentLoadId += 1;
378
+ flushSync(() => {
379
+ state.loading = false;
380
+ });
381
+ if (await presence.leave()) {
382
+ void props.onHidden?.(offcanvas);
383
+ }
384
+ },
385
+ };
544
386
 
545
- protected override validateStatePatch(patch: Partial<OffcanvasState>): void {
546
- validateParam(
547
- 'state',
548
- patch,
549
- {
550
- type: 'plainObject',
551
- },
552
- 'Offcanvas.setState'
553
- );
387
+ offcanvas = defineComponent({
388
+ name: 'Offcanvas',
389
+ props,
390
+ state,
391
+ actions,
392
+ validateStatePatch,
393
+ view(context) {
394
+ const overlay = props.overlay
395
+ ? (jsx('div', {
396
+ ref: overlayRef.set,
397
+ className: props.className.overlay,
398
+ 'data-offcanvas-overlay': props.id,
399
+ 'data-mount': () => (state.visible ? 'true' : 'false'),
400
+ style: props.filter ? { backdropFilter: 'blur(2px)' } : {},
401
+ onClick: () => {
402
+ if (props.bgClose) void offcanvas.hide();
403
+ },
404
+ }) as HTMLElement)
405
+ : null;
406
+ if (overlay) context.own(() => overlay.remove());
554
407
 
555
- for (const key of Object.keys(patch)) {
556
- if (!Object.hasOwn(OFFCANVAS_STATE_SCHEMA, key)) {
557
- throw new Error(
558
- `Offcanvas.setState: "${key}" is not a supported state key.`
408
+ createEffect(() => {
409
+ const content = state.content;
410
+ validateParam(
411
+ 'content',
412
+ content,
413
+ OFFCANVAS_STATE_SCHEMA.content,
414
+ 'Offcanvas.state'
559
415
  );
560
- }
561
- const stateKey = key as keyof typeof OFFCANVAS_STATE_SCHEMA;
562
- validateParam(
563
- key,
564
- patch[key as keyof OffcanvasState],
565
- OFFCANVAS_STATE_SCHEMA[stateKey],
566
- 'Offcanvas.setState'
567
- );
568
- }
569
- }
570
- }
416
+ clearCache();
417
+ if (typeof content === 'function') {
418
+ state.resolvedContent = null;
419
+ if (untrack(() => state.visible)) void loadContent();
420
+ } else {
421
+ runtime.contentLoadId += 1;
422
+ state.resolvedContent = content;
423
+ state.loading = false;
424
+ }
425
+ });
571
426
 
572
- export type Offcanvas = OffcanvasComponent;
427
+ return jsx('div', {
428
+ className: props.className.root,
429
+ id: props.id,
430
+ role: 'dialog',
431
+ 'aria-modal': props.overlay ? 'true' : 'false',
432
+ 'aria-expanded': () => String(state.visible),
433
+ 'data-mount': () => (state.visible ? 'true' : 'false'),
434
+ 'data-offcanvas': 'root',
435
+ 'data-direction': props.direction,
436
+ 'data-animate': props.animate,
437
+ onClick: (event: Event) => {
438
+ if (!(event.target instanceof Element)) return;
439
+ const action = event.target.closest<HTMLElement>('[data-action]');
440
+ if (
441
+ action?.dataset.action === 'close' ||
442
+ action?.dataset.action === 'cancel'
443
+ ) {
444
+ void offcanvas.hide();
445
+ }
446
+ },
447
+ children: jsx('div', {
448
+ className: props.className.content,
449
+ 'data-offcanvas-content': props.id,
450
+ children: () =>
451
+ state.loading
452
+ ? createLoading()
453
+ : typeof state.resolvedContent === 'function'
454
+ ? state.resolvedContent(offcanvas)
455
+ : state.resolvedContent,
456
+ }),
457
+ }) as HTMLElement;
458
+ },
459
+ onBuild(context) {
460
+ if (props.escClose) {
461
+ events.on('esc', document, 'keydown', (event) => {
462
+ if (
463
+ event instanceof KeyboardEvent &&
464
+ event.key === 'Escape' &&
465
+ state.visible
466
+ ) {
467
+ void offcanvas.hide();
468
+ }
469
+ });
470
+ }
471
+ context.own(() => events.clear());
472
+ },
473
+ onDestroy() {
474
+ presence.cancel();
475
+ runtime.contentLoadId += 1;
476
+ overlayRef.current?.remove();
477
+ unlockScroll();
478
+ },
479
+ });
573
480
 
574
- export function createOffcanvas(input: OffcanvasProps = {}): Offcanvas {
575
- return new OffcanvasComponent(input);
481
+ return offcanvas;
576
482
  }