@graupl/navigation-shelf 1.0.0-beta.16

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 (49) hide show
  1. package/dist/css/component/navigation-shelf.css +36 -0
  2. package/dist/css/component/navigation-shelf.css.map +1 -0
  3. package/dist/css/component.css +36 -0
  4. package/dist/css/component.css.map +1 -0
  5. package/dist/css/navigation-shelf.css +36 -0
  6. package/dist/css/navigation-shelf.css.map +1 -0
  7. package/dist/css/utilities/shelf-aware.css +2 -0
  8. package/dist/css/utilities/shelf-aware.css.map +1 -0
  9. package/dist/css/utilities/width.css +2 -0
  10. package/dist/css/utilities/width.css.map +1 -0
  11. package/dist/css/utilities.css +2 -0
  12. package/dist/css/utilities.css.map +1 -0
  13. package/dist/js/component/navigation-shelf.cjs.js +5 -0
  14. package/dist/js/component/navigation-shelf.cjs.js.map +1 -0
  15. package/dist/js/component/navigation-shelf.es.js +5 -0
  16. package/dist/js/component/navigation-shelf.es.js.map +1 -0
  17. package/dist/js/component/navigation-shelf.iife.js +5 -0
  18. package/dist/js/component/navigation-shelf.iife.js.map +1 -0
  19. package/dist/js/generator/navigation-shelf.cjs.js +5 -0
  20. package/dist/js/generator/navigation-shelf.cjs.js.map +1 -0
  21. package/dist/js/generator/navigation-shelf.es.js +5 -0
  22. package/dist/js/generator/navigation-shelf.es.js.map +1 -0
  23. package/dist/js/generator/navigation-shelf.iife.js +5 -0
  24. package/dist/js/generator/navigation-shelf.iife.js.map +1 -0
  25. package/dist/js/navigation-shelf.js +5 -0
  26. package/dist/js/navigation-shelf.js.map +1 -0
  27. package/package.json +71 -0
  28. package/scss/component/navigation-shelf.scss +3 -0
  29. package/scss/component.scss +3 -0
  30. package/scss/navigation-shelf.scss +3 -0
  31. package/scss/utilities/shelf-aware.scss +3 -0
  32. package/scss/utilities/width.scss +3 -0
  33. package/scss/utilities.scss +3 -0
  34. package/src/js/navigation-shelf/NavigationShelf.js +1912 -0
  35. package/src/js/navigation-shelf/generator.js +38 -0
  36. package/src/js/navigation-shelf/index.js +5 -0
  37. package/src/js/validate.js +46 -0
  38. package/src/scss/_index.scss +2 -0
  39. package/src/scss/component/_index.scss +1 -0
  40. package/src/scss/component/navigation-shelf/_defaults.scss +85 -0
  41. package/src/scss/component/navigation-shelf/_index.scss +228 -0
  42. package/src/scss/component/navigation-shelf/_variables.scss +196 -0
  43. package/src/scss/utilities/_index.scss +2 -0
  44. package/src/scss/utilities/shelf-aware/_defaults.scss +31 -0
  45. package/src/scss/utilities/shelf-aware/_index.scss +383 -0
  46. package/src/scss/utilities/shelf-aware/_variables.scss +6 -0
  47. package/src/scss/utilities/width/_defaults.scss +58 -0
  48. package/src/scss/utilities/width/_index.scss +290 -0
  49. package/src/scss/utilities/width/_variables.scss +6 -0
@@ -0,0 +1,1912 @@
1
+ import {
2
+ isValidClassList,
3
+ isValidType,
4
+ isValidInstance,
5
+ isValidState,
6
+ isValidEvent,
7
+ } from "@graupl/core/src/validate.js";
8
+ import { isValidSideType } from "../validate.js";
9
+ import { keyPress, preventEvent } from "@graupl/core/src/eventHandlers.js";
10
+ import {
11
+ addClass,
12
+ removeClass,
13
+ selectFirstFocusableElement,
14
+ } from "@graupl/core/src/domHelpers.js";
15
+ import storage from "@graupl/core/src/storage.js";
16
+
17
+ class NavigationShelf {
18
+ /**
19
+ * The DOM elements within the shelf.
20
+ *
21
+ * @protected
22
+ *
23
+ * @type {Object<HTMLElement,HTMLElement[]>}
24
+ *
25
+ * @property {HTMLElement} shelf - The shelf element.
26
+ * @property {HTMLElement} controller - The toggle for this shelf.
27
+ * @property {HTMLElement} lockController - The toggle for locking this shelf.
28
+ * @property {HTMLElement} hoverController - The toggle for hoverability of this shelf.
29
+ * @property {HTMLElement} sideController - The toggle for the side controller of this shelf.
30
+ * @property {HTMLElement[]} dependents - The list of dependent elements that should be updated when the shelf is opened or closed.
31
+ */
32
+ _dom = {
33
+ shelf: null,
34
+ controller: null,
35
+ lockController: null,
36
+ hoverController: null,
37
+ sideController: null,
38
+ dependents: [],
39
+ };
40
+
41
+ /**
42
+ * The query selectors used by the shelf to populate the dom.
43
+ *
44
+ * @protected
45
+ *
46
+ * @type {Object<string>}
47
+ *
48
+ * @property {string} dependents - The query selector for dependent elements.
49
+ */
50
+ _selectors = {
51
+ dependents: "",
52
+ };
53
+
54
+ /**
55
+ * The class(es) to apply to the shelf and dependent elements in various scenarios.
56
+ *
57
+ * @protected
58
+ *
59
+ * @type {Object<string,string[]>}
60
+ *
61
+ * @property {string|string[]} locked - The class(es) to apply to the shelf and dependent elements when the shelf is locked.
62
+ * @property {string|string[]} unlocked - The class(es) to apply to the shelf and dependent elements when the shelf is unlocked.
63
+ * @property {string|string[]} hover - The class(es) to apply to the shelf element when the shelf is hoverable.
64
+ * @property {string|string[]} noHover - The class(es) to apply to the shelf element when the shelf is not hoverable.
65
+ * @property {string|string[]} left - The class(es) to apply to the shelf and dependent elements when the shelf is on the left side.
66
+ * @property {string|string[]} right - The class(es) to apply to the shelf and dependent elements when the shelf is on the right side.
67
+ * @property {string|string[]} open - The class(es) to apply to the shelf when the shelf is open.
68
+ * @property {string|string[]} close - The class(es) to apply to the shelf when the shelf is closed.
69
+ * @property {string|string[]} transition - The class(es) to apply to the shelf and dependent elements when the shelf is transitioning between states.
70
+ */
71
+ _classes = {
72
+ locked: "locked",
73
+ unlocked: "unlocked",
74
+ hover: "hoverable",
75
+ noHover: "not-hoverable",
76
+ left: "left-side",
77
+ right: "right-side",
78
+ open: "show",
79
+ close: "hide",
80
+ transistion: "transitioning",
81
+ };
82
+
83
+ /**
84
+ * The duration time (in milliseconds) for the transition between open and closed states.
85
+ *
86
+ * @protected
87
+ *
88
+ * @type {number}
89
+ */
90
+ _transitionDuration = 250;
91
+
92
+ /**
93
+ * The duration time (in milliseconds) for the transition from closed to open states.
94
+ *
95
+ * @protected
96
+ *
97
+ * @type {number}
98
+ */
99
+ _openDuration = -1;
100
+
101
+ /**
102
+ * The duration time (in milliseconds) for the transition from open to closed states.
103
+ *
104
+ * @protected
105
+ *
106
+ * @type {number}
107
+ */
108
+ _closeDuration = -1;
109
+
110
+ /**
111
+ * The current state of the shelf's focus.
112
+ *
113
+ * @protected
114
+ *
115
+ * @type {string}
116
+ */
117
+ _focusState = "none";
118
+
119
+ /**
120
+ * This last event triggered on the shelf.
121
+ *
122
+ * @protected
123
+ *
124
+ * @type {string}
125
+ */
126
+ _currentEvent = "none";
127
+
128
+ /**
129
+ * A flag to indicate if the shelf is hoverable.
130
+ *
131
+ * @protected
132
+ *
133
+ * @type {boolean}
134
+ */
135
+ _hover = false;
136
+
137
+ /**
138
+ * The delay time (in milliseconds) used for pointerenter/pointerleave events to take place.
139
+ *
140
+ * @protected
141
+ *
142
+ * @type {number}
143
+ */
144
+ _hoverDelay = 250;
145
+
146
+ /**
147
+ * The delay time (in milliseconds) used for pointerenter events to take place.
148
+ *
149
+ * @protected
150
+ *
151
+ * @type {number}
152
+ */
153
+ _enterDelay = -1;
154
+
155
+ /**
156
+ * The delay time (in milliseconds) used for pointerleave events to take place.
157
+ *
158
+ * @protected
159
+ *
160
+ * @type {number}
161
+ */
162
+ _leaveDelay = -1;
163
+
164
+ /**
165
+ * A variable to hold the hover timeout function.
166
+ *
167
+ * @protected
168
+ *
169
+ * @type {?Function}
170
+ */
171
+ _hoverTimeout = null;
172
+
173
+ /**
174
+ * A flag to indicate if the navigation shelf is locked.
175
+ *
176
+ * @protected
177
+ *
178
+ * @type {boolean}
179
+ */
180
+ _locked = false;
181
+
182
+ /**
183
+ * A flag to check in the navigation shelf can dynamically close based on if the shelf has been manually interacted with already.
184
+ *
185
+ * @protected
186
+ *
187
+ * @type {boolean}
188
+ */
189
+ _softLocked = false;
190
+
191
+ /**
192
+ * The side of the screen the navigation shelf is on.
193
+ *
194
+ * @protected
195
+ *
196
+ * @type {string}
197
+ */
198
+ _side = "left";
199
+
200
+ /**
201
+ * The opposite side of the screen the naigation shelf is on.
202
+ *
203
+ * @protected
204
+ *
205
+ * @type {string}
206
+ */
207
+ _otherSide = "right";
208
+
209
+ /**
210
+ * The open state of the shelf.
211
+ *
212
+ * @protected
213
+ *
214
+ * @type {boolean}
215
+ */
216
+ _open = false;
217
+
218
+ /**
219
+ * The event that is triggered when the shelf expands.
220
+ *
221
+ * @protected
222
+ *
223
+ * @event grauplNavigationShelfExpand
224
+ *
225
+ * @type {CustomEvent}
226
+ *
227
+ * @property {boolean} bubbles - A flag to bubble the event.
228
+ * @property {Object<NavigationShelf>} detail - The details object containing the NavigationShelf itself.
229
+ */
230
+ _expandEvent = new CustomEvent("grauplNavigationShelfExpand", {
231
+ bubbles: true,
232
+ detail: { shelf: this },
233
+ });
234
+
235
+ /**
236
+ * The event that is triggered when the shelf collapses.
237
+ *
238
+ * @protected
239
+ *
240
+ * @event grauplNavigationShelfCollapse
241
+ *
242
+ * @type {CustomEvent}
243
+ *
244
+ * @property {boolean} bubbles - A flag to bubble the event.
245
+ * @property {Object<NavigationShelf>} detail - The details object containing the NavigationShelf itself.
246
+ */
247
+ _collapseEvent = new CustomEvent("grauplNavigationShelfCollapse", {
248
+ bubbles: true,
249
+ detail: { shelf: this },
250
+ });
251
+
252
+ /**
253
+ * The event that is triggered when the shelf is locked.
254
+ *
255
+ * @protected
256
+ *
257
+ * @event grauplNavigationShelfLock
258
+ *
259
+ * @type {CustomEvent}
260
+ *
261
+ * @property {boolean} bubbles - A flag to bubble the event.
262
+ * @property {Object<NavigationShelf>} detail - The details object containing the NavigationShelf itself.
263
+ */
264
+ _lockEvent = new CustomEvent("grauplNavigationShelfLock", {
265
+ bubbles: true,
266
+ detail: { shelf: this },
267
+ });
268
+
269
+ /**
270
+ * The event that is triggered when the shelf is unlocked.
271
+ *
272
+ * @protected
273
+ *
274
+ * @event grauplNavigationShelfUnlock
275
+ *
276
+ * @type {CustomEvent}
277
+ *
278
+ * @property {boolean} bubbles - A flag to bubble the event.
279
+ * @property {Object<NavigationShelf>} detail - The details object containing the NavigationShelf itself.
280
+ */
281
+ _unlockEvent = new CustomEvent("grauplNavigationShelfUnlock", {
282
+ bubbles: true,
283
+ detail: { shelf: this },
284
+ });
285
+
286
+ /**
287
+ * The event that is triggered when the shelf has shifted sides.
288
+ *
289
+ * @protected
290
+ *
291
+ * @event grauplNavigationShelfShift
292
+ *
293
+ * @type {CustomEvent}
294
+ *
295
+ * @property {boolean} bubbles - A flag to bubble the event.
296
+ * @property {Object<NavigationShelf>} detail - The details object containing the NavigationShelf itself.
297
+ */
298
+ _shiftEvent = new CustomEvent("grauplNavigationShelfShift", {
299
+ bubbles: true,
300
+ detail: {
301
+ shelf: this,
302
+ },
303
+ });
304
+
305
+ /**
306
+ * The event that is triggered when the shelf's hoverability is enabled.
307
+ *
308
+ * @protected
309
+ *
310
+ * @event grauplNavigationShelfEnableHoverable
311
+ *
312
+ * @type {CustomEvent}
313
+ *
314
+ * @property {boolean} bubbles - A flag to bubble the event.
315
+ * @property {Object<NavigationShelf>} detail - The details object containing the NavigationShelf itself.
316
+ */
317
+ _enableHoverEvent = new CustomEvent("grauplNavigationShelfEnableHoverable", {
318
+ bubbles: true,
319
+ detail: {
320
+ shelf: this,
321
+ },
322
+ });
323
+
324
+ /**
325
+ * The event that is triggered when the shelf's hoverability is disabled.
326
+ *
327
+ * @protected
328
+ *
329
+ * @event grauplNavigationShelfDisableHover
330
+ *
331
+ * @type {CustomEvent}
332
+ *
333
+ * @property {boolean} bubbles - A flag to bubble the event.
334
+ * @property {Object<NavigationShelf>} detail - The details object containing the NavigationShelf itself.
335
+ */
336
+ _disableHoverEvent = new CustomEvent("grauplNavigationShelfDisableHover", {
337
+ bubbles: true,
338
+ detail: {
339
+ shelf: this,
340
+ },
341
+ });
342
+
343
+ /**
344
+ * The prefix to use for CSS custom properties.
345
+ *
346
+ * @protected
347
+ *
348
+ * @type {string}
349
+ */
350
+ _prefix = "graupl-";
351
+
352
+ /**
353
+ * The key used to generate IDs throughout the navigation shelf.
354
+ *
355
+ * @protected
356
+ *
357
+ * @type {string}
358
+ */
359
+ _key = "";
360
+
361
+ /**
362
+ * errors - The list of errors found during validation.
363
+ *
364
+ * @protected
365
+ *
366
+ * @type {string[]}
367
+ */
368
+ _errors = [];
369
+
370
+ constructor({
371
+ shelfElement,
372
+ controllerElement,
373
+ lockControllerElement,
374
+ hoverControllerElement,
375
+ sideControllerElement,
376
+ dependentSelector = ".shelf-aware",
377
+ lockedClass = "locked",
378
+ unlockedClass = "unlocked",
379
+ hoverClass = "hoverable",
380
+ noHoverClass = "not-hoverable",
381
+ leftClass = "left-side",
382
+ rightClass = "right-side",
383
+ openClass = "show",
384
+ closeClass = "hide",
385
+ transitionClass = "transitioning",
386
+ transitionDuration = 250,
387
+ openDuration = -1,
388
+ closeDuration = -1,
389
+ hover = false,
390
+ hoverDelay = 250,
391
+ enterDelay = -1,
392
+ leaveDelay = -1,
393
+ locked = false,
394
+ side = "left",
395
+ prefix = "graupl-",
396
+ initialize = false,
397
+ }) {
398
+ // Set DOM elements.
399
+ this._dom.shelf = shelfElement;
400
+ this._dom.controller = controllerElement || null;
401
+ this._dom.lockController = lockControllerElement || null;
402
+ this._dom.hoverController = hoverControllerElement || null;
403
+ this._dom.sideController = sideControllerElement || null;
404
+
405
+ // Set DOM selectors.
406
+ this._selectors.dependents = dependentSelector;
407
+
408
+ // Set classes.
409
+ this._classes.locked = lockedClass || "";
410
+ this._classes.unlocked = unlockedClass || "";
411
+ this._classes.hover = hoverClass || "";
412
+ this._classes.noHover = noHoverClass || "";
413
+ this._classes.left = leftClass || "";
414
+ this._classes.right = rightClass || "";
415
+ this._classes.open = openClass || "";
416
+ this._classes.close = closeClass || "";
417
+ this._classes.transition = transitionClass || "";
418
+
419
+ // Set transition duration.
420
+ this._transitionDuration = transitionDuration;
421
+ this._openDuration = openDuration;
422
+ this._closeDuration = closeDuration;
423
+
424
+ // Set locked state.
425
+ this._locked = locked;
426
+
427
+ // Set side.
428
+ this._side = side;
429
+
430
+ // Set prefix.
431
+ this._prefix = prefix || "";
432
+
433
+ // Set hover settings.
434
+ this._hover = hover;
435
+ this._hoverDelay = hoverDelay;
436
+ this._enterDelay = enterDelay;
437
+ this._leaveDelay = leaveDelay;
438
+
439
+ if (initialize) {
440
+ this.initialize();
441
+ }
442
+ }
443
+
444
+ /**
445
+ * Initialize the navigation shelf.
446
+ */
447
+ initialize() {
448
+ try {
449
+ if (!this._validate()) {
450
+ throw new Error(
451
+ `Graupl Navigation Shelf: cannot initialize navigation shelf. The following errors have been found:\n - ${this.errors.join(
452
+ "\n - "
453
+ )}`
454
+ );
455
+ }
456
+
457
+ this._setTransitionDurations();
458
+
459
+ // Set up the DOM.
460
+ this._generateKey();
461
+ this._setDOMElements();
462
+ this._setIds();
463
+ this._setAriaAttributes();
464
+
465
+ // Set up the event listeners.
466
+ this._handleFocus();
467
+ this._handleClick();
468
+ this._handleHover();
469
+ this._handleKeydown();
470
+ this._handleKeyup();
471
+
472
+ // Ensure the initial open state of the shelf.
473
+ if (this.dom.controller.getAttribute("aria-expanded") === "true") {
474
+ this._expand(false);
475
+ } else {
476
+ this._collapse(false);
477
+ }
478
+
479
+ // Ensure the initial hoverability of the shelf.
480
+ if (this.hover) {
481
+ this._enableHover(false);
482
+ } else {
483
+ this._disableHover(false);
484
+ }
485
+
486
+ // Ensure the initial side of the shelf.
487
+ this._shiftSide(false);
488
+
489
+ // Set up the storage.
490
+ storage.initializeStorage("navigation-shelves");
491
+ storage.pushToStorage("navigation-shelves", this.dom.shelf.id, this);
492
+ } catch (error) {
493
+ console.error(error);
494
+ }
495
+ }
496
+
497
+ /**
498
+ * The DOM elements within the shelf.
499
+ *
500
+ * @readonly
501
+ *
502
+ * @type {Object<HTMLElement, HTMLElement[]>}
503
+ *
504
+ * @see _dom
505
+ */
506
+ get dom() {
507
+ return this._dom;
508
+ }
509
+
510
+ /**
511
+ * The query selectors used by the shelf to populate the DOM.
512
+ *
513
+ * @readonly
514
+ *
515
+ * @type {Object<string>}
516
+ *
517
+ * @see _selectors
518
+ */
519
+ get selectors() {
520
+ return this._selectors;
521
+ }
522
+
523
+ /**
524
+ * The class(es) to apply to the shelf and dependent elements in various scenarios.
525
+ *
526
+ * @readonly
527
+ *
528
+ * @type {Object<string, string[]>}
529
+ *
530
+ * @see _classes
531
+ */
532
+ get classes() {
533
+ return this._classes;
534
+ }
535
+
536
+ /**
537
+ * The class(es) to apply to the shelf and dependent elements when the shelf is locked.
538
+ *
539
+ * @type {string|string[]}
540
+ *
541
+ * @see _classes
542
+ */
543
+ get lockedClass() {
544
+ return this._classes.locked;
545
+ }
546
+
547
+ /**
548
+ * The class(es) to apply to the shelf and dependent elements when the shelf is unlocked.
549
+ *
550
+ * @type {string|string[]}
551
+ *
552
+ * @see _classes
553
+ */
554
+ get unlockedClass() {
555
+ return this._classes.unlocked;
556
+ }
557
+
558
+ /**
559
+ * The class(es) to apply to the shelf element when the shelf is hoverable.
560
+ *
561
+ * @type {string|string[]}
562
+ *
563
+ * @see _classes
564
+ */
565
+ get hoverClass() {
566
+ return this._classes.hover;
567
+ }
568
+
569
+ /**
570
+ * The class(es) to apply to the shelf element when the shelf is not hoverable.
571
+ *
572
+ * @type {string|string[]}
573
+ *
574
+ * @see _classes
575
+ */
576
+ get noHoverClass() {
577
+ return this._classes.noHover;
578
+ }
579
+
580
+ /**
581
+ * The class(es) to apply to the shelf and dependent elements when the shelf is on the left side.
582
+ *
583
+ * @type {string|string[]}
584
+ *
585
+ * @see _classes
586
+ */
587
+ get leftClass() {
588
+ return this._classes.left;
589
+ }
590
+
591
+ /**
592
+ * The class(es) to apply to the shelf and dependent elements when the shelf is on the right side.
593
+ *
594
+ * @type {string|string[]}
595
+ *
596
+ * @see _classes
597
+ */
598
+ get rightClass() {
599
+ return this._classes.right;
600
+ }
601
+
602
+ /**
603
+ * The class(es) to apply to the shelf when the shelf is open.
604
+ *
605
+ * @type {string|string[]}
606
+ *
607
+ * @see _classes
608
+ */
609
+ get openClass() {
610
+ return this._classes.open;
611
+ }
612
+
613
+ /**
614
+ * The class(es) to apply to the shelf when the shelf is closed.
615
+ *
616
+ * @type {string|string[]}
617
+ *
618
+ * @see _classes
619
+ */
620
+ get closeClass() {
621
+ return this._classes.close;
622
+ }
623
+
624
+ /**
625
+ * The class(es) to apply to the shelf and dependent elements when the shelf is transitioning between states.
626
+ *
627
+ * @type {string|string[]}
628
+ *
629
+ * @see _classes
630
+ */
631
+ get transitionClass() {
632
+ return this._classes.transition;
633
+ }
634
+
635
+ /**
636
+ * The duration time (in milliseconds) for the transition between open and closed states.
637
+ *
638
+ * Setting this value will also set the --am-transition-duration CSS custom property on the shelf.
639
+ *
640
+ * @type {number}
641
+ *
642
+ * @see _transitionDuration
643
+ */
644
+ get transitionDuration() {
645
+ return this._transitionDuration;
646
+ }
647
+
648
+ /**
649
+ * The duration time (in milliseconds) for the transition from closed to open states.
650
+ *
651
+ * If openDuration is set to -1, the transitionDuration value will be used instead.
652
+ *
653
+ * Setting this value will also set the --am-open-transition-duration CSS custom property on the shelf.
654
+ *
655
+ * @type {number}
656
+ *
657
+ * @see _openDuration
658
+ */
659
+ get openDuration() {
660
+ if (this._openDuration === -1) return this.transitionDuration;
661
+
662
+ return this._openDuration;
663
+ }
664
+
665
+ /**
666
+ * The duration time (in milliseconds) for the transition from open to closed states.
667
+ *
668
+ * If closeDuration is set to -1, the transitionDuration value will be used instead.
669
+ *
670
+ * Setting this value will also set the --am-close-transition-duration CSS custom property on the shelf.
671
+ *
672
+ * @type {number}
673
+ *
674
+ * @see _closeDuration
675
+ */
676
+ get closeDuration() {
677
+ if (this._closeDuration === -1) return this.transitionDuration;
678
+
679
+ return this._closeDuration;
680
+ }
681
+
682
+ /**
683
+ * The current state of the shelf's focus.
684
+ *
685
+ * @type {string}
686
+ *
687
+ * @see _focusState
688
+ */
689
+ get focusState() {
690
+ return this._focusState;
691
+ }
692
+
693
+ /**
694
+ * The last event triggered on the shelf.
695
+ *
696
+ * @type {string}
697
+ *
698
+ * @see _currentEvent
699
+ */
700
+ get currentEvent() {
701
+ return this._currentEvent;
702
+ }
703
+
704
+ /**
705
+ * A flag to indicate if the shelf is hoverable.
706
+ *
707
+ * @readonly
708
+ *
709
+ * @type {boolean}
710
+ *
711
+ * @see _hover
712
+ */
713
+ get hover() {
714
+ return this._hover;
715
+ }
716
+
717
+ /**
718
+ * The delay time (in milliseconds) used for pointerenter/pointerleave events to take place.
719
+ *
720
+ * @type {number}
721
+ *
722
+ * @see _hoverDelay
723
+ */
724
+ get hoverDelay() {
725
+ return this._hoverDelay;
726
+ }
727
+
728
+ /**
729
+ * The delay time (in milliseconds) used for pointerenter events to take place.
730
+ *
731
+ * If enterDelay is set to -1, the hoverDelay value will be used instead.
732
+ *
733
+ * @type {number}
734
+ *
735
+ * @see _enterDelay
736
+ */
737
+ get enterDelay() {
738
+ if (this._enterDelay === -1) return this.hoverDelay;
739
+
740
+ return this._enterDelay;
741
+ }
742
+
743
+ /**
744
+ * The delay time (in milliseconds) used for pointerleave events to take place.
745
+ *
746
+ * If leaveDelay is set to -1, the hoverDelay value will be used instead.
747
+ *
748
+ * @type {number}
749
+ *
750
+ * @see _leaveDelay
751
+ */
752
+ get leaveDelay() {
753
+ if (this._leaveDelay === -1) return this.hoverDelay;
754
+
755
+ return this._leaveDelay;
756
+ }
757
+
758
+ /**
759
+ * The prefix to use for CSS custom properties.
760
+ *
761
+ * @type {string}
762
+ *
763
+ * @see _prefix
764
+ */
765
+ get prefix() {
766
+ return this._prefix;
767
+ }
768
+
769
+ /**
770
+ * A flag to indicate if the navigation shelf is locked.
771
+ *
772
+ * @readonly
773
+ *
774
+ * @type {boolean}
775
+ *
776
+ * @see _locked
777
+ */
778
+ get isLocked() {
779
+ return this._locked;
780
+ }
781
+
782
+ /**
783
+ * The side of the screen the navigation shelf is on.
784
+ *
785
+ * @readonly
786
+ *
787
+ * @type {string}
788
+ *
789
+ * @see _side
790
+ */
791
+ get side() {
792
+ return this._side;
793
+ }
794
+
795
+ /**
796
+ * The opposite side of the screen the navigation shelf is on.
797
+ *
798
+ * @readonly
799
+ *
800
+ * @type {string}
801
+ *
802
+ * @see _otherSide
803
+ */
804
+ get otherSide() {
805
+ return this._otherSide;
806
+ }
807
+
808
+ /**
809
+ * The key used to generate IDs throughout the accordion.
810
+ *
811
+ * @type {string}
812
+ *
813
+ * @see _key
814
+ */
815
+ get key() {
816
+ return this._key;
817
+ }
818
+
819
+ /**
820
+ * A flag to check if the shelf can dynamically hover.
821
+ *
822
+ * @type {boolean}
823
+ *
824
+ * @see _softLocked
825
+ */
826
+ get isSoftLocked() {
827
+ return this._softLocked;
828
+ }
829
+
830
+ /**
831
+ * The open state on the shelf.
832
+ *
833
+ * @type {boolean}
834
+ *
835
+ * @see _open
836
+ */
837
+ get isOpen() {
838
+ return this._open;
839
+ }
840
+
841
+ /**
842
+ * An array of error messages generated by the shelf.
843
+ *
844
+ * @readonly
845
+ *
846
+ * @type {string[]}
847
+ *
848
+ * @see _errors
849
+ */
850
+ get errors() {
851
+ return this._errors;
852
+ }
853
+
854
+ set dependentLockedClass(value) {
855
+ isValidClassList({ dependentLockedClass: value });
856
+ if (this._classes.dependentLocked !== value) {
857
+ this._classes.dependentLocked = value;
858
+ }
859
+ }
860
+
861
+ set dependentUnlockedClass(value) {
862
+ isValidClassList({ dependentUnlockedClass: value });
863
+ if (this._classes.dependentUnlocked !== value) {
864
+ this._classes.dependentUnlocked = value;
865
+ }
866
+ }
867
+
868
+ set openClass(value) {
869
+ isValidClassList({ openClass: value });
870
+
871
+ if (this._classes.open !== value) {
872
+ this._classes.open = value;
873
+ }
874
+ }
875
+
876
+ set closeClass(value) {
877
+ isValidClassList({ closeClass: value });
878
+
879
+ if (this._classes.close !== value) {
880
+ this._classes.close = value;
881
+ }
882
+ }
883
+
884
+ set transitionClass(value) {
885
+ isValidClassList({ transitionClass: value });
886
+
887
+ if (this._classes.transition !== value) {
888
+ this._classes.transition = value;
889
+ }
890
+ }
891
+
892
+ set transitionDuration(value) {
893
+ isValidType("number", { value });
894
+
895
+ if (this._transitionDuration !== value) {
896
+ this._transitionDuration = value;
897
+ this._setTransitionDurations();
898
+ }
899
+ }
900
+
901
+ set openDuration(value) {
902
+ isValidType("number", { value });
903
+
904
+ if (this._openDuration !== value) {
905
+ this._openDuration = value;
906
+ this._setTransitionDurations();
907
+ }
908
+ }
909
+
910
+ set closeDuration(value) {
911
+ isValidType("number", { value });
912
+
913
+ if (this._closeDuration !== value) {
914
+ this._closeDuration = value;
915
+ this._setTransitionDurations();
916
+ }
917
+ }
918
+
919
+ set focusState(value) {
920
+ isValidState({ value });
921
+
922
+ if (this._focusState !== value) {
923
+ this._focusState = value;
924
+ }
925
+ }
926
+
927
+ set currentEvent(value) {
928
+ isValidEvent({ value });
929
+
930
+ if (this._currentEvent !== value) {
931
+ this._currentEvent = value;
932
+ }
933
+ }
934
+
935
+ set hoverDelay(value) {
936
+ isValidType("number", { value });
937
+
938
+ if (this._hoverDelay !== value) {
939
+ this._hoverDelay = value;
940
+ }
941
+ }
942
+
943
+ set enterDelay(value) {
944
+ isValidType("number", { value });
945
+
946
+ if (this._enterDelay !== value) {
947
+ this._enterDelay = value;
948
+ }
949
+ }
950
+
951
+ set leaveDelay(value) {
952
+ isValidType("number", { value });
953
+
954
+ if (this._leaveDelay !== value) {
955
+ this._leaveDelay = value;
956
+ }
957
+ }
958
+
959
+ set prefix(value) {
960
+ isValidType("string", { value });
961
+
962
+ if (this._prefix !== value) {
963
+ this._prefix = value;
964
+ }
965
+ }
966
+
967
+ set key(value) {
968
+ isValidType("string", { value });
969
+
970
+ if (this._key !== value) {
971
+ this._key = value;
972
+ }
973
+ }
974
+
975
+ set isSoftLocked(value) {
976
+ isValidType("boolean", { value });
977
+
978
+ if (this._softLocked !== value) {
979
+ this._softLocked = value;
980
+ }
981
+ }
982
+
983
+ /**
984
+ * Validates all aspects of the shelf to ensure proper functionality.
985
+ *
986
+ * @protected
987
+ *
988
+ * @return {boolean} - The result of the validation.
989
+ */
990
+ _validate() {
991
+ let check = true;
992
+
993
+ // HTML element checks.
994
+ const htmlElements = {
995
+ shelfElement: this._dom.shelf,
996
+ controllerElement: this._dom.controller,
997
+ };
998
+
999
+ if (this._dom.lockController) {
1000
+ htmlElements.lockControllerElement = this._dom.lockController;
1001
+ }
1002
+ if (this._dom.hoverController) {
1003
+ htmlElements.hoverControllerElement = this._dom.hoverController;
1004
+ }
1005
+ if (this._dom.sideController) {
1006
+ htmlElements.sideControllerElement = this._dom.sideController;
1007
+ }
1008
+
1009
+ const htmlElementChecks = isValidInstance(HTMLElement, htmlElements);
1010
+
1011
+ if (!htmlElementChecks.status) {
1012
+ this._errors.push(htmlElementChecks.error.message);
1013
+ check = false;
1014
+ }
1015
+
1016
+ // Class list checks.
1017
+ const classes = {};
1018
+ for (const key of Object.keys(this.classes)) {
1019
+ if (this._classes[key] === "") continue;
1020
+
1021
+ classes[`${key}Class`] = this._classes[key];
1022
+ }
1023
+ const classChecks = isValidClassList(classes);
1024
+
1025
+ if (!classChecks.status) {
1026
+ this._errors.push(classChecks.error.message);
1027
+ check = false;
1028
+ }
1029
+
1030
+ // Transition duration check.
1031
+ const transitionDurationCheck = isValidType("number", {
1032
+ transitionDuration: this._transitionDuration,
1033
+ });
1034
+
1035
+ if (!transitionDurationCheck.status) {
1036
+ this._errors.push(transitionDurationCheck.error.message);
1037
+ check = false;
1038
+ }
1039
+
1040
+ // Open duration check.
1041
+ const openDurationCheck = isValidType("number", {
1042
+ openDuration: this._openDuration,
1043
+ });
1044
+
1045
+ if (!openDurationCheck.status) {
1046
+ this._errors.push(openDurationCheck.error.message);
1047
+ check = false;
1048
+ }
1049
+
1050
+ // Close duration check.
1051
+ const closeDurationCheck = isValidType("number", {
1052
+ closeDuration: this._closeDuration,
1053
+ });
1054
+
1055
+ if (!closeDurationCheck.status) {
1056
+ this._errors.push(closeDurationCheck.error.message);
1057
+ check = false;
1058
+ }
1059
+
1060
+ // Hover check.
1061
+ const hoverCheck = isValidType("boolean", { hover: this._hover });
1062
+
1063
+ if (!hoverCheck.status) {
1064
+ this._errors.push(hoverCheck.error.message);
1065
+ check = false;
1066
+ }
1067
+
1068
+ // Hover delay check.
1069
+ const hoverDelayCheck = isValidType("number", {
1070
+ hoverDelay: this._hoverDelay,
1071
+ });
1072
+
1073
+ if (!hoverDelayCheck.status) {
1074
+ this._errors.push(hoverDelayCheck.error.message);
1075
+ check = false;
1076
+ }
1077
+
1078
+ // Enter delay check.
1079
+ const enterDelayCheck = isValidType("number", {
1080
+ enterDelay: this._enterDelay,
1081
+ });
1082
+
1083
+ if (!enterDelayCheck.status) {
1084
+ this._errors.push(enterDelayCheck.error.message);
1085
+ check = false;
1086
+ }
1087
+
1088
+ // Leave delay check.
1089
+ const leaveDelayCheck = isValidType("number", {
1090
+ leaveDelay: this._leaveDelay,
1091
+ });
1092
+
1093
+ if (!leaveDelayCheck.status) {
1094
+ this._errors.push(leaveDelayCheck.error.message);
1095
+ check = false;
1096
+ }
1097
+
1098
+ // Prefix check.
1099
+ const prefixCheck = isValidType("string", { prefix: this._prefix });
1100
+
1101
+ if (!prefixCheck.status) {
1102
+ this._errors.push(prefixCheck.error.message);
1103
+ check = false;
1104
+ }
1105
+
1106
+ // Locked check.
1107
+ const lockedCheck = isValidType("boolean", { locked: this._locked });
1108
+ if (!lockedCheck.status) {
1109
+ this._errors.push(lockedCheck.error.message);
1110
+ check = false;
1111
+ }
1112
+
1113
+ // Side check.
1114
+ const sideCheck = isValidSideType({ side: this._side });
1115
+ if (!sideCheck.status) {
1116
+ this._errors.push(sideCheck.error.message);
1117
+ check = false;
1118
+ }
1119
+
1120
+ return check;
1121
+ }
1122
+
1123
+ /**
1124
+ * Sets DOM elements within the shelf.
1125
+ *
1126
+ * The shelf, controller, lockController, and hoverController elements _cannot_ be set through this method.
1127
+ *
1128
+ * @protected
1129
+ *
1130
+ * @param {string} elementType - The type of element to populate.
1131
+ * @param {HTMLElement} [base = this.dom.shelf] - The element used as the base for the querySelector.
1132
+ * @param {boolean} [overwrite = true] - A flag to set if the existing elements will be overwritten.
1133
+ * @param {boolean} [strict = true] - A flag to set if the elements must be direct children of the base.
1134
+ */
1135
+ _setDOMElementType(
1136
+ elementType,
1137
+ base = this.dom.shelf,
1138
+ overwrite = true,
1139
+ strict = true
1140
+ ) {
1141
+ if (typeof this.selectors[elementType] === "string") {
1142
+ if (
1143
+ elementType === "shelf" ||
1144
+ elementType === "controller" ||
1145
+ elementType === "lockController" ||
1146
+ elementType === "hoverController"
1147
+ ) {
1148
+ throw new Error(
1149
+ `Graupl Navigation Shelf: "${elementType}" element cannot be set through _setDOMElementType.`
1150
+ );
1151
+ }
1152
+
1153
+ if (base !== this.dom.shelf) isValidInstance(HTMLElement, { base });
1154
+
1155
+ if (Array.isArray(this._dom[elementType])) {
1156
+ // Get all the elements matching the selector in the base.
1157
+ const domElements = Array.from(
1158
+ base.querySelectorAll(this.selectors[elementType])
1159
+ );
1160
+
1161
+ // Filter the elements so only direct children of the base are kept.
1162
+ const filteredElements = domElements.filter((item) =>
1163
+ strict ? item.parentElement === base : true
1164
+ );
1165
+
1166
+ if (overwrite) {
1167
+ this._dom[elementType] = filteredElements;
1168
+ } else {
1169
+ this._dom[elementType] = [
1170
+ ...this._dom[elementType],
1171
+ ...filteredElements,
1172
+ ];
1173
+ }
1174
+ } else {
1175
+ // Get the single element matching the selector in the base.
1176
+ const domElement = base.querySelector(this.selectors[elementType]);
1177
+
1178
+ // Ensure the element is a direct child of the base.
1179
+ if (domElement && domElement.parentElement !== base) {
1180
+ return;
1181
+ }
1182
+
1183
+ if (overwrite) {
1184
+ this._dom[elementType] = domElement;
1185
+ }
1186
+ }
1187
+ } else {
1188
+ throw new Error(
1189
+ `Graupl Navigation Shelf: "${elementType}" is not a valid element type within the navigation shelf.`
1190
+ );
1191
+ }
1192
+ }
1193
+
1194
+ /**
1195
+ * Resets DOM elements within the menu.
1196
+ *
1197
+ * The shelf, controller, lockController, and hoverController elements _cannot_ be set through this method.
1198
+ *
1199
+ * @protected
1200
+ *
1201
+ * @param {string} elementType - The type of element to clear.
1202
+ */
1203
+ _resetDOMElementType(elementType) {
1204
+ if (typeof this.selectors[elementType] === "string") {
1205
+ if (
1206
+ elementType === "shelf" ||
1207
+ elementType === "controller" ||
1208
+ elementType === "lockController" ||
1209
+ elementType === "hoverController"
1210
+ ) {
1211
+ throw new Error(
1212
+ `Graupl Navigation Shelf: "${elementType}" element cannot be reset through _resetDOMElementType.`
1213
+ );
1214
+ }
1215
+
1216
+ if (Array.isArray(this._dom[elementType])) {
1217
+ this._dom[elementType] = [];
1218
+ } else {
1219
+ this._dom[elementType] = null;
1220
+ }
1221
+ } else {
1222
+ throw new Error(
1223
+ `Graupl Navigation Shelf: "${elementType}" is not a valid element type within the navigation shelf.`
1224
+ );
1225
+ }
1226
+ }
1227
+
1228
+ /**
1229
+ * Sets all DOM elements within the shelf.
1230
+ *
1231
+ * Utilizes _setDOMElementType and _resetDOMElementType.
1232
+ *
1233
+ * @protected
1234
+ */
1235
+ _setDOMElements() {
1236
+ this._setDOMElementType("dependents", document, true, false);
1237
+ }
1238
+
1239
+ /**
1240
+ * Generates a key for the navigation shelf.
1241
+ *
1242
+ * @param {boolean} [regenerate = false] - A flag to determine if the key should be regenerated.
1243
+ */
1244
+ _generateKey(regenerate = false) {
1245
+ if (this.key === "" || regenerate) {
1246
+ this.key = Math.random()
1247
+ .toString(36)
1248
+ .replace(/[^a-z]+/g, "")
1249
+ .substring(0, 10);
1250
+ }
1251
+ }
1252
+
1253
+ /**
1254
+ * Sets the IDs of the navigation shelf and it's elements if they do not already exist.
1255
+ *
1256
+ * The generated IDs use the key and follow the format:
1257
+ * - navigation shelf: `navigation-shelf-${key}`
1258
+ * - navigation shelf toggle: `navigation-shelf-toggle-${key}`
1259
+ * - navigation shelf lock toggle: `navigation-shelf-lock-toggle-${key}`
1260
+ * - navigation shelf hover toggle: `navigation-shelf-hover-toggle-${key}`
1261
+ */
1262
+ _setIds() {
1263
+ this.dom.shelf.id = this.dom.shelf.id || `navigation-shelf-${this.key}`;
1264
+ if (this.dom.controller) {
1265
+ this.dom.controller.id =
1266
+ this.dom.controller.id || `navigation-shelf-toggle-${this.key}`;
1267
+ }
1268
+ if (this.dom.lockController) {
1269
+ this.dom.lockController.id =
1270
+ this.dom.lockController.id ||
1271
+ `navigation-shelf-lock-toggle-${this.key}`;
1272
+ }
1273
+ if (this.dom.hoverController) {
1274
+ this.dom.hoverController.id =
1275
+ this.dom.hoverController.id ||
1276
+ `navigation-shelf-hover-toggle-${this.key}`;
1277
+ }
1278
+ }
1279
+
1280
+ /**
1281
+ * Sets the aria attributes for the navigation shelf.
1282
+ */
1283
+ _setAriaAttributes() {
1284
+ if (this.dom.controller) {
1285
+ this.dom.controller.setAttribute("aria-controls", this.dom.shelf.id);
1286
+
1287
+ if (this.dom.controller.getAttribute("aria-expanded") !== "true") {
1288
+ this.dom.controller.setAttribute("aria-expanded", "false");
1289
+ }
1290
+ }
1291
+
1292
+ if (this.dom.lockController) {
1293
+ this.dom.lockController.setAttribute("aria-controls", this.dom.shelf.id);
1294
+ this.dom.lockController.setAttribute(
1295
+ "aria-pressed",
1296
+ this._locked ? "true" : "false"
1297
+ );
1298
+ }
1299
+
1300
+ if (this.dom.hoverController) {
1301
+ this.dom.hoverController.setAttribute("aria-controls", this.dom.shelf.id);
1302
+ this.dom.hoverController.setAttribute(
1303
+ "aria-pressed",
1304
+ this._hoverType === "on" ? "true" : "false"
1305
+ );
1306
+ }
1307
+
1308
+ if (this.dom.sideController) {
1309
+ this.dom.sideController.setAttribute("aria-controls", this.dom.shelf.id);
1310
+ }
1311
+ }
1312
+
1313
+ /**
1314
+ * Clears the hover timeout.
1315
+ *
1316
+ * @protected
1317
+ */
1318
+ _clearTimeout() {
1319
+ clearTimeout(this._hoverTimeout);
1320
+ }
1321
+
1322
+ /**
1323
+ * Sets the hover timeout.
1324
+ *
1325
+ * @protected
1326
+ *
1327
+ * @param {Function} callback - The callback function to execute.
1328
+ * @param {number} delay - The delay time in milliseconds.
1329
+ */
1330
+ _setTimeout(callback, delay) {
1331
+ isValidType("function", { callback });
1332
+ isValidType("number", { delay });
1333
+
1334
+ this._hoverTimeout = setTimeout(callback, delay);
1335
+ }
1336
+
1337
+ _handleFocus() {
1338
+ this.dom.shelf.addEventListener("focusin", () => {
1339
+ this.focusState = "self";
1340
+ this.open();
1341
+ });
1342
+
1343
+ this.dom.shelf.addEventListener("focusout", (event) => {
1344
+ if (
1345
+ event.relatedTarget === null ||
1346
+ this.dom.shelf.contains(event.relatedTarget)
1347
+ )
1348
+ return;
1349
+
1350
+ this.focusState = "none";
1351
+ this.close();
1352
+ });
1353
+ }
1354
+
1355
+ _handleClick() {
1356
+ // Prevent pointer down events on all controlled elements.
1357
+ for (const element of Object.values(this.dom)) {
1358
+ if (!element) continue;
1359
+ if (Array.isArray(element)) continue;
1360
+
1361
+ element.addEventListener(
1362
+ "pointerdown",
1363
+ () => {
1364
+ this.currentEvent = "mouse";
1365
+ this._clearTimeout();
1366
+ },
1367
+ { passive: true }
1368
+ );
1369
+ }
1370
+
1371
+ // Toggle the shelf when the controlled is clicked.
1372
+ if (this.dom.controller) {
1373
+ this.dom.controller.addEventListener("pointerup", (event) => {
1374
+ if (event.button !== 0) return;
1375
+
1376
+ this.currentEvent = "mouse";
1377
+ preventEvent(event);
1378
+ this.toggle();
1379
+
1380
+ if (this.isOpen) {
1381
+ this.focusState = "self";
1382
+ this.isSoftLocked = true;
1383
+ }
1384
+ });
1385
+ }
1386
+
1387
+ // Toggle hoverability when the hover controller is clicked.
1388
+ if (this.dom.hoverController) {
1389
+ this.dom.hoverController.addEventListener("pointerup", (event) => {
1390
+ if (event.button !== 0) return;
1391
+
1392
+ this.currentEvent = "mouse";
1393
+ preventEvent(event);
1394
+ this.focusState = "self";
1395
+ this.toggleHover();
1396
+
1397
+ if (this.hover) {
1398
+ this.open();
1399
+ }
1400
+ });
1401
+ }
1402
+
1403
+ // Toggle shelf lock when the lock controller is clicked.
1404
+ if (this.dom.lockController) {
1405
+ this.dom.lockController.addEventListener("pointerup", (event) => {
1406
+ if (event.button !== 0) return;
1407
+
1408
+ this.currentEvent = "mouse";
1409
+ preventEvent(event);
1410
+ this.focusState = "self";
1411
+ this.toggleLock();
1412
+ });
1413
+ }
1414
+
1415
+ // Toggle shifting sides when the side controller is clicked.
1416
+ if (this.dom.sideController) {
1417
+ this.dom.sideController.addEventListener("pointerup", (event) => {
1418
+ if (event.button !== 0) return;
1419
+
1420
+ this.currentEvent = "mouse";
1421
+ preventEvent(event);
1422
+ this.focusState = "self";
1423
+ this.toggleSide();
1424
+ });
1425
+ }
1426
+
1427
+ // Catch all to open if shelf if there is a click inside of it.
1428
+ this.dom.shelf.addEventListener("pointerup", (event) => {
1429
+ if (event.button !== 0) return;
1430
+
1431
+ this.currentEvent = "mouse";
1432
+ this.focusState = "self";
1433
+ this.isSoftLocked = true;
1434
+ this.open();
1435
+ });
1436
+
1437
+ // Close the shelf if a click happens outside of it.
1438
+ document.addEventListener("pointerup", (event) => {
1439
+ if (this.focusState === "none") return;
1440
+ if (this.isLocked) return;
1441
+ if (
1442
+ this.dom.shelf === event.target ||
1443
+ this.dom.shelf.contains(event.target)
1444
+ )
1445
+ return;
1446
+
1447
+ this.currentEvent = "mouse";
1448
+ this.close();
1449
+ });
1450
+ }
1451
+
1452
+ _handleHover() {
1453
+ this.dom.shelf.addEventListener("pointerenter", (event) => {
1454
+ if (event.pointerType === "pen" || event.pointerType === "touch") return;
1455
+ if (this.isLocked || this.isSoftLocked) return;
1456
+ if (!this.hover) return;
1457
+
1458
+ this.currentEvent = "mouse";
1459
+
1460
+ if (this.enterDelay > 0) {
1461
+ this._clearTimeout();
1462
+ this._setTimeout(() => {
1463
+ if (!this.isOpen) {
1464
+ this.open();
1465
+ }
1466
+ }, this.enterDelay);
1467
+ } else {
1468
+ this.open();
1469
+ }
1470
+ });
1471
+
1472
+ this.dom.shelf.addEventListener("pointerleave", (event) => {
1473
+ if (event.pointerType === "pen" || event.pointerType === "touch") return;
1474
+ if (this.isLocked || this.isSoftLocked) return;
1475
+ if (!this.hover) return;
1476
+
1477
+ this.currentEvent = "mouse";
1478
+
1479
+ if (this.leaveDelay > 0) {
1480
+ this._clearTimeout();
1481
+ this._setTimeout(() => {
1482
+ if (this.isOpen) {
1483
+ this.close();
1484
+ }
1485
+ }, this.leaveDelay);
1486
+ } else {
1487
+ this.close();
1488
+ }
1489
+ });
1490
+ }
1491
+
1492
+ _handleKeydown() {
1493
+ // Prevent keydown events on the shelf if they are `Escape`.
1494
+ this.dom.shelf.addEventListener("keydown", (event) => {
1495
+ const key = keyPress(event);
1496
+
1497
+ if (key === "Escape") {
1498
+ preventEvent(event);
1499
+ }
1500
+ });
1501
+
1502
+ // Prevent keydown events on all controller elements if they are `Space` or `Enter`.
1503
+ for (const element of Object.values(this.dom)) {
1504
+ if (!element) continue;
1505
+ if (Array.isArray(element)) continue;
1506
+ if (element === this.dom.shelf) continue;
1507
+
1508
+ element.addEventListener("keydown", (event) => {
1509
+ this.currentEvent = "keyboard";
1510
+
1511
+ const key = keyPress(event);
1512
+
1513
+ if (key === "Space" || key === "Enter") {
1514
+ preventEvent(event);
1515
+ }
1516
+ });
1517
+ }
1518
+ }
1519
+
1520
+ _handleKeyup() {
1521
+ // Close the shelf on `Escape`.
1522
+ this.dom.shelf.addEventListener("keyup", (event) => {
1523
+ this.currentEvent = "keyboard";
1524
+
1525
+ const key = keyPress(event);
1526
+
1527
+ if (key === "Escape") {
1528
+ this.close();
1529
+ }
1530
+ });
1531
+
1532
+ // Toggle the shelf on `Space` or `Enter` on the controller.
1533
+ if (this.dom.controller) {
1534
+ this.dom.controller.addEventListener("keyup", (event) => {
1535
+ this.currentEvent = "keyboard";
1536
+
1537
+ const key = keyPress(event);
1538
+
1539
+ if (key === "Space" || key === "Enter") {
1540
+ preventEvent(event);
1541
+ this.toggle();
1542
+
1543
+ if (this.isOpen) {
1544
+ const element = selectFirstFocusableElement(this.dom.shelf);
1545
+ element.focus();
1546
+ }
1547
+ }
1548
+ });
1549
+ }
1550
+
1551
+ // Toggle hover on `Space` or `Enter` on the hover controller.
1552
+ if (this.dom.hoverController) {
1553
+ this.dom.hoverController.addEventListener("keyup", (event) => {
1554
+ this.currentEvent = "keyboard";
1555
+
1556
+ const key = keyPress(event);
1557
+
1558
+ if (key === "Space" || key === "Enter") {
1559
+ preventEvent(event);
1560
+ this.toggleHover();
1561
+ }
1562
+ });
1563
+ }
1564
+
1565
+ // Toggle lock on `Space` or `Enter` on the lock controller.
1566
+ if (this.dom.lockController) {
1567
+ this.dom.lockController.addEventListener("keyup", (event) => {
1568
+ this.currentEvent = "keyboard";
1569
+
1570
+ const key = keyPress(event);
1571
+
1572
+ if (key === "Space" || key === "Enter") {
1573
+ preventEvent(event);
1574
+ this.toggleLock();
1575
+ }
1576
+ });
1577
+ }
1578
+
1579
+ // Shift sides on `Space` or `Enter` on the side controller.
1580
+ if (this.dom.sideController) {
1581
+ this.dom.sideController.addEventListener("keyup", (event) => {
1582
+ this.currentEvent = "keyboard";
1583
+
1584
+ const key = keyPress(event);
1585
+
1586
+ if (key === "Space" || key === "Enter") {
1587
+ preventEvent(event);
1588
+ this.toggleSide();
1589
+ }
1590
+ });
1591
+ }
1592
+ }
1593
+
1594
+ /**
1595
+ * Sets the transition durations of the shelf as a CSS custom properties.
1596
+ *
1597
+ * The custom properties are:
1598
+ * - `--graupl-transition-duration`,
1599
+ * - `--graupl-open-transition-duration`, and
1600
+ * - `--graupl-close-transition-duration`.
1601
+ *
1602
+ * The prefix of `graupl-` can be changed by setting the shelf's prefix value.
1603
+ *
1604
+ * @protected
1605
+ */
1606
+ _setTransitionDurations() {
1607
+ this.dom.shelf.style.setProperty(
1608
+ `--${this.prefix}navigation-shelf-transition-duration`,
1609
+ `${this.transitionDuration}ms`
1610
+ );
1611
+
1612
+ this.dom.shelf.style.setProperty(
1613
+ `--${this.prefix}navigation-shelf-open-transition-duration`,
1614
+ `${this.openDuration}ms`
1615
+ );
1616
+
1617
+ this.dom.shelf.style.setProperty(
1618
+ `--${this.prefix}navigation-shelf-close-transition-duration`,
1619
+ `${this.closeDuration}ms`
1620
+ );
1621
+ }
1622
+
1623
+ _expand(emit = true) {
1624
+ if (this.dom.controller) {
1625
+ this.dom.controller.setAttribute("aria-expanded", "true");
1626
+ }
1627
+
1628
+ // If we're dealing with transition classes, then we need to utilize
1629
+ // requestAnimationFrame to add the transition class, remove the close class,
1630
+ // add the open class, and finally remove the transition class.
1631
+ if (this.transitionClass !== "") {
1632
+ addClass(this.transitionClass, this.dom.shelf);
1633
+
1634
+ requestAnimationFrame(() => {
1635
+ removeClass(this.closeClass, this.dom.shelf);
1636
+
1637
+ requestAnimationFrame(() => {
1638
+ addClass(this.openClass, this.dom.shelf);
1639
+
1640
+ requestAnimationFrame(() => {
1641
+ setTimeout(() => {
1642
+ removeClass(this.transitionClass, this.dom.shelf);
1643
+ }, this.openDuration);
1644
+ });
1645
+ });
1646
+ });
1647
+ } else {
1648
+ // Add the open class
1649
+ addClass(this.openClass, this.dom.shelf);
1650
+
1651
+ // Remove the close class.
1652
+ removeClass(this.closeClass, this.dom.shelf);
1653
+ }
1654
+
1655
+ if (emit) {
1656
+ this.dom.shelf.dispatchEvent(this._expandEvent);
1657
+ }
1658
+ }
1659
+
1660
+ _collapse(emit = true) {
1661
+ if (this.dom.controller) {
1662
+ this.dom.controller.setAttribute("aria-expanded", "false");
1663
+ }
1664
+ this.isSoftLocked = false;
1665
+
1666
+ // If we're dealing with transition classes, then we need to utilize
1667
+ // requestAnimationFrame to add the transition class, remove the open class,
1668
+ // add the close class, and finally remove the transition class.
1669
+ if (this.transitionClass !== "") {
1670
+ addClass(this.transitionClass, this.dom.shelf);
1671
+
1672
+ requestAnimationFrame(() => {
1673
+ removeClass(this.openClass, this.dom.shelf);
1674
+
1675
+ requestAnimationFrame(() => {
1676
+ addClass(this.closeClass, this.dom.shelf);
1677
+
1678
+ requestAnimationFrame(() => {
1679
+ setTimeout(() => {
1680
+ removeClass(this.transitionClass, this.dom.shelf);
1681
+ }, this.closeDuration);
1682
+ });
1683
+ });
1684
+ });
1685
+ } else {
1686
+ // Add the close class
1687
+ addClass(this.closeClass, this.dom.shelf);
1688
+
1689
+ // Remove the open class.
1690
+ removeClass(this.openClass, this.dom.shelf);
1691
+ }
1692
+
1693
+ if (emit) {
1694
+ this.dom.shelf.dispatchEvent(this._collapseEvent);
1695
+ }
1696
+ }
1697
+
1698
+ _lock(emit = true) {
1699
+ if (this.dom.lockController) {
1700
+ this.dom.lockController.setAttribute("aria-pressed", "true");
1701
+ }
1702
+
1703
+ // Add the locked class
1704
+ addClass(this.lockedClass, this.dom.shelf);
1705
+
1706
+ // Add the locked class to dependent elements.
1707
+ this.dom.dependents.forEach((dependent) => {
1708
+ addClass(this.lockedClass, dependent);
1709
+ });
1710
+
1711
+ // Remove the unlocked class.
1712
+ removeClass(this.unlockedClass, this.dom.shelf);
1713
+
1714
+ // Remove the unlocked class from dependent elements.
1715
+ this.dom.dependents.forEach((dependent) => {
1716
+ removeClass(this.unlockedClass, dependent);
1717
+ });
1718
+
1719
+ if (emit) {
1720
+ this.dom.shelf.dispatchEvent(this._lockEvent);
1721
+ }
1722
+ }
1723
+
1724
+ _unlock(emit = true) {
1725
+ if (this.dom.lockController) {
1726
+ this.dom.lockController.setAttribute("aria-pressed", "false");
1727
+ }
1728
+
1729
+ // Add the unlocked class
1730
+ addClass(this.unlockedClass, this.dom.shelf);
1731
+
1732
+ // Add the unlocked class to dependent elements.
1733
+ this.dom.dependents.forEach((dependent) => {
1734
+ addClass(this.unlockedClass, dependent);
1735
+ });
1736
+
1737
+ // Remove the locked class.
1738
+ removeClass(this.lockedClass, this.dom.shelf);
1739
+
1740
+ // Remove the locked class from dependent elements.
1741
+ this.dom.dependents.forEach((dependent) => {
1742
+ removeClass(this.lockedClass, dependent);
1743
+ });
1744
+
1745
+ if (emit) {
1746
+ this.dom.shelf.dispatchEvent(this._unlockEvent);
1747
+ }
1748
+ }
1749
+
1750
+ _shiftSide(emit = true) {
1751
+ const toClass = this._classes[this.side];
1752
+ const fromClass = this._classes[this.otherSide];
1753
+
1754
+ // Add the to class
1755
+ addClass(toClass, this.dom.shelf);
1756
+
1757
+ // Add the to class to dependent elements.
1758
+ this.dom.dependents.forEach((dependent) => {
1759
+ addClass(toClass, dependent);
1760
+ });
1761
+
1762
+ // Remove the from class.
1763
+ removeClass(fromClass, this.dom.shelf);
1764
+
1765
+ // Remove the from class from dependent elements.
1766
+ this.dom.dependents.forEach((dependent) => {
1767
+ removeClass(fromClass, dependent);
1768
+ });
1769
+
1770
+ if (emit) {
1771
+ this.dom.shelf.dispatchEvent(this._shiftEvent);
1772
+ }
1773
+ }
1774
+
1775
+ _enableHover(emit = true) {
1776
+ if (this.dom.hoverController) {
1777
+ this.dom.hoverController.setAttribute("aria-pressed", "true");
1778
+ }
1779
+
1780
+ addClass(this.hoverClass, this.dom.shelf);
1781
+
1782
+ removeClass(this.noHoverClass, this.dom.shelf);
1783
+
1784
+ if (emit) {
1785
+ this.dom.shelf.dispatchEvent(this._enableHoverEvent);
1786
+ }
1787
+ }
1788
+
1789
+ _disableHover(emit = true) {
1790
+ if (this.dom.hoverController) {
1791
+ this.dom.hoverController.setAttribute("aria-pressed", "false");
1792
+ }
1793
+
1794
+ addClass(this.noHoverClass, this.dom.shelf);
1795
+
1796
+ removeClass(this.hoverClass, this.dom.shelf);
1797
+
1798
+ if (emit) {
1799
+ this.dom.shelf.dispatchEvent(this._disableHoverEvent);
1800
+ }
1801
+ }
1802
+
1803
+ open(force = false) {
1804
+ // Only open if the shelf is closed.
1805
+ if (this.isOpen && !force) return;
1806
+
1807
+ this._expand();
1808
+
1809
+ // Set the open flag.
1810
+ this._open = true;
1811
+ }
1812
+
1813
+ close(force = false) {
1814
+ // Only close if the shelf is open.
1815
+ if (!this.isOpen && !force) return;
1816
+
1817
+ this.unlock();
1818
+ this._collapse();
1819
+
1820
+ // Set the open flag.
1821
+ this._open = false;
1822
+ }
1823
+
1824
+ toggle() {
1825
+ if (this.isOpen) {
1826
+ this.close();
1827
+ } else {
1828
+ this.open();
1829
+ }
1830
+ }
1831
+
1832
+ lock() {
1833
+ // Only lock if the shelf is unlocked.
1834
+ if (this.isLocked) return;
1835
+
1836
+ this._lock();
1837
+
1838
+ // Set the locked flag.
1839
+ this._locked = true;
1840
+
1841
+ // Open the shelf.
1842
+ this.open(true);
1843
+ }
1844
+
1845
+ unlock() {
1846
+ // Only unlock if the shelf is locked.
1847
+ if (!this.isLocked) return;
1848
+
1849
+ this._unlock();
1850
+
1851
+ // Set the locked flag.
1852
+ this._locked = false;
1853
+ }
1854
+
1855
+ toggleLock() {
1856
+ if (this.isLocked) {
1857
+ this.unlock();
1858
+ } else {
1859
+ this.lock();
1860
+ }
1861
+ }
1862
+
1863
+ toLeft() {
1864
+ if (this.side === "left") return;
1865
+
1866
+ this._side = "left";
1867
+ this._otherSide = "right";
1868
+ this._shiftSide();
1869
+ }
1870
+
1871
+ toRight() {
1872
+ if (this.side === "right") return;
1873
+
1874
+ this._side = "right";
1875
+ this._otherSide = "left";
1876
+ this._shiftSide();
1877
+ }
1878
+
1879
+ toggleSide() {
1880
+ if (this.side === "left") {
1881
+ this.toRight();
1882
+ } else {
1883
+ this.toLeft();
1884
+ }
1885
+ }
1886
+
1887
+ enableHover() {
1888
+ if (this.hover) return;
1889
+
1890
+ this._enableHover();
1891
+
1892
+ this._hover = true;
1893
+ }
1894
+
1895
+ disableHover() {
1896
+ if (!this.hover) return;
1897
+
1898
+ this._disableHover();
1899
+
1900
+ this._hover = false;
1901
+ }
1902
+
1903
+ toggleHover() {
1904
+ if (this.hover) {
1905
+ this.disableHover();
1906
+ } else {
1907
+ this.enableHover();
1908
+ }
1909
+ }
1910
+ }
1911
+
1912
+ export default NavigationShelf;