@y14e/menu 1.4.8 → 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.
package/README.md CHANGED
@@ -10,16 +10,16 @@ npm i @y14e/menu
10
10
 
11
11
  ```ts
12
12
  // npm
13
- import Menu from '@y14e/menu@1.4.8';
13
+ import Menu from '@y14e/menu@1.5.1';
14
14
  // with middleware
15
- import Menu, { flip, offset, shift } from '@y14e/menu@1.4.8';
15
+ import Menu, { flip, offset, shift } from '@y14e/menu@1.5.1';
16
16
 
17
17
  // CDNs
18
- import Menu from 'https://esm.sh/@y14e/menu@1.4.8';
18
+ import Menu from 'https://esm.sh/@y14e/menu@1.5.1';
19
19
  // or
20
- import Menu from 'https://cdn.jsdelivr.net/npm/@y14e/menu@1.4.8/+esm';
20
+ import Menu from 'https://cdn.jsdelivr.net/npm/@y14e/menu@1.5.1/+esm';
21
21
  // or
22
- import Menu from 'https://esm.unpkg.com/@y14e/menu@1.4.8';
22
+ import Menu from 'https://esm.unpkg.com/@y14e/menu@1.5.1';
23
23
  ```
24
24
 
25
25
  ## Usage
package/dist/index.cjs CHANGED
@@ -2624,6 +2624,7 @@ var Menu = class _Menu {
2624
2624
  };
2625
2625
  #settings;
2626
2626
  #externalTrigger;
2627
+ #isMenubar;
2627
2628
  #isPortal;
2628
2629
  #isSubmenu;
2629
2630
  #triggerElement;
@@ -2656,10 +2657,12 @@ var Menu = class _Menu {
2656
2657
  matchMedia("(prefers-reduced-motion: reduce)").matches && Object.assign(this.#settings.animation, { duration: 0 });
2657
2658
  const {
2658
2659
  externalTrigger = null,
2660
+ isMenubar = false,
2659
2661
  isPortal = false,
2660
2662
  isSubmenu = false
2661
2663
  } = _internal;
2662
2664
  this.#externalTrigger = externalTrigger;
2665
+ this.#isMenubar = isMenubar;
2663
2666
  this.#isPortal = isPortal;
2664
2667
  this.#isSubmenu = isSubmenu;
2665
2668
  const { selector } = this.#settings;
@@ -2715,8 +2718,8 @@ var Menu = class _Menu {
2715
2718
  }
2716
2719
  this.#initialize();
2717
2720
  }
2718
- open(focusLastItem = false) {
2719
- this.#toggle(true, focusLastItem);
2721
+ open(_initialFocus = "first") {
2722
+ this.#toggle(true, { initialFocus: _initialFocus });
2720
2723
  }
2721
2724
  close() {
2722
2725
  this.#toggle(false);
@@ -2916,7 +2919,7 @@ var Menu = class _Menu {
2916
2919
  }
2917
2920
  event.preventDefault();
2918
2921
  event.stopPropagation();
2919
- this.open(key === "ArrowUp");
2922
+ this.open(key === "ArrowDown" ? "first" : "last");
2920
2923
  };
2921
2924
  #onListKeyDown = (event) => {
2922
2925
  const { shiftKey, key } = event;
@@ -2932,7 +2935,8 @@ var Menu = class _Menu {
2932
2935
  "Enter",
2933
2936
  "Escape",
2934
2937
  " ",
2935
- ...this.#isSubmenu ? ["ArrowLeft"] : []
2938
+ "ArrowLeft",
2939
+ ...this.#isMenubar ? ["ArrowRight"] : []
2936
2940
  ].includes(key)) {
2937
2941
  return;
2938
2942
  }
@@ -2945,9 +2949,18 @@ var Menu = class _Menu {
2945
2949
  switch (key) {
2946
2950
  case "Tab":
2947
2951
  case "Escape":
2948
- case "ArrowLeft":
2949
2952
  this.close();
2950
2953
  return;
2954
+ case "ArrowLeft":
2955
+ if (this.#isMenubar) {
2956
+ this.#closeAndMoveFocus("previous");
2957
+ } else if (this.#isSubmenu && this.#triggerElement) {
2958
+ this.close();
2959
+ }
2960
+ return;
2961
+ case "ArrowRight":
2962
+ this.#closeAndMoveFocus("next");
2963
+ return;
2951
2964
  case "Enter":
2952
2965
  case " ":
2953
2966
  active.click();
@@ -2987,7 +3000,7 @@ var Menu = class _Menu {
2987
3000
  i.setAttribute("aria-checked", String(i === item));
2988
3001
  });
2989
3002
  };
2990
- #toggle(isOpen, isFocusLastItem = false) {
3003
+ #toggle(isOpen, options = {}) {
2991
3004
  if (this.#triggerElement?.ariaExpanded === String(isOpen)) {
2992
3005
  return;
2993
3006
  }
@@ -3012,7 +3025,8 @@ var Menu = class _Menu {
3012
3025
  style.setProperty("display", "block");
3013
3026
  style.setProperty("opacity", "0");
3014
3027
  this.#triggerElement && this.#updatePopover();
3015
- this.#itemElements.filter(isFocusable3).at(isFocusLastItem ? -1 : 0)?.focus();
3028
+ const { initialFocus = "first" } = options;
3029
+ this.#itemElements.filter(isFocusable3).at(initialFocus === "first" ? 0 : -1)?.focus();
3016
3030
  } else {
3017
3031
  this.#clearSubmenuTimer();
3018
3032
  this.#focusTrigger();
@@ -3077,8 +3091,18 @@ var Menu = class _Menu {
3077
3091
  this.#submenuTimer = void 0;
3078
3092
  }
3079
3093
  }
3094
+ #closeAndMoveFocus(direction) {
3095
+ this.close();
3096
+ requestAnimationFrame(
3097
+ () => (this.#externalTrigger ?? this.#triggerElement)?.dispatchEvent(
3098
+ new KeyboardEvent("keydown", {
3099
+ key: direction === "previous" ? "ArrowLeft" : "ArrowRight"
3100
+ })
3101
+ )
3102
+ );
3103
+ }
3080
3104
  #containsRoot(element) {
3081
- return this.#rootElement.contains(element) || this.#listElement?.contains(element);
3105
+ return this.#rootElement.contains(element) || !!this.#listElement?.contains(element);
3082
3106
  }
3083
3107
  #focusTrigger() {
3084
3108
  const active = getActiveElement4();
@@ -3241,7 +3265,7 @@ function isFocusable3(element) {
3241
3265
  * WAI-ARIA compliant menu (menu button) pattern implementation in TypeScript.
3242
3266
  * Supports checkbox item, radio item, and infinitely nested menus.
3243
3267
  *
3244
- * @version 1.4.8
3268
+ * @version 1.5.1
3245
3269
  * @author Yusuke Kamiyamane
3246
3270
  * @license MIT
3247
3271
  * @copyright Copyright (c) Yusuke Kamiyamane
package/dist/index.d.cts CHANGED
@@ -6,7 +6,7 @@ export { flip, offset, shift } from '@floating-ui/dom';
6
6
  * WAI-ARIA compliant menu (menu button) pattern implementation in TypeScript.
7
7
  * Supports checkbox item, radio item, and infinitely nested menus.
8
8
  *
9
- * @version 1.4.8
9
+ * @version 1.5.1
10
10
  * @author Yusuke Kamiyamane
11
11
  * @license MIT
12
12
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -39,6 +39,7 @@ interface MenuPopoverOptions {
39
39
  }
40
40
  interface InternalOptions {
41
41
  readonly externalTrigger?: HTMLElement;
42
+ readonly isMenubar?: boolean;
42
43
  readonly isPortal?: boolean;
43
44
  readonly isSubmenu?: boolean;
44
45
  }
@@ -50,7 +51,11 @@ declare class Menu {
50
51
  * @internal Internal constructor
51
52
  */
52
53
  constructor(root: HTMLElement, options?: MenuOptions, _internal?: InternalOptions);
53
- open(focusLastItem?: boolean): void;
54
+ open(): void;
55
+ /**
56
+ * @internal Internal open
57
+ */
58
+ open(_initialFocus?: 'first' | 'last'): void;
54
59
  close(): void;
55
60
  destroy(force?: boolean): Promise<void>;
56
61
  }
package/dist/index.d.ts CHANGED
@@ -6,7 +6,7 @@ export { flip, offset, shift } from '@floating-ui/dom';
6
6
  * WAI-ARIA compliant menu (menu button) pattern implementation in TypeScript.
7
7
  * Supports checkbox item, radio item, and infinitely nested menus.
8
8
  *
9
- * @version 1.4.8
9
+ * @version 1.5.1
10
10
  * @author Yusuke Kamiyamane
11
11
  * @license MIT
12
12
  * @copyright Copyright (c) Yusuke Kamiyamane
@@ -39,6 +39,7 @@ interface MenuPopoverOptions {
39
39
  }
40
40
  interface InternalOptions {
41
41
  readonly externalTrigger?: HTMLElement;
42
+ readonly isMenubar?: boolean;
42
43
  readonly isPortal?: boolean;
43
44
  readonly isSubmenu?: boolean;
44
45
  }
@@ -50,7 +51,11 @@ declare class Menu {
50
51
  * @internal Internal constructor
51
52
  */
52
53
  constructor(root: HTMLElement, options?: MenuOptions, _internal?: InternalOptions);
53
- open(focusLastItem?: boolean): void;
54
+ open(): void;
55
+ /**
56
+ * @internal Internal open
57
+ */
58
+ open(_initialFocus?: 'first' | 'last'): void;
54
59
  close(): void;
55
60
  destroy(force?: boolean): Promise<void>;
56
61
  }
package/dist/index.js CHANGED
@@ -2620,6 +2620,7 @@ var Menu = class _Menu {
2620
2620
  };
2621
2621
  #settings;
2622
2622
  #externalTrigger;
2623
+ #isMenubar;
2623
2624
  #isPortal;
2624
2625
  #isSubmenu;
2625
2626
  #triggerElement;
@@ -2652,10 +2653,12 @@ var Menu = class _Menu {
2652
2653
  matchMedia("(prefers-reduced-motion: reduce)").matches && Object.assign(this.#settings.animation, { duration: 0 });
2653
2654
  const {
2654
2655
  externalTrigger = null,
2656
+ isMenubar = false,
2655
2657
  isPortal = false,
2656
2658
  isSubmenu = false
2657
2659
  } = _internal;
2658
2660
  this.#externalTrigger = externalTrigger;
2661
+ this.#isMenubar = isMenubar;
2659
2662
  this.#isPortal = isPortal;
2660
2663
  this.#isSubmenu = isSubmenu;
2661
2664
  const { selector } = this.#settings;
@@ -2711,8 +2714,8 @@ var Menu = class _Menu {
2711
2714
  }
2712
2715
  this.#initialize();
2713
2716
  }
2714
- open(focusLastItem = false) {
2715
- this.#toggle(true, focusLastItem);
2717
+ open(_initialFocus = "first") {
2718
+ this.#toggle(true, { initialFocus: _initialFocus });
2716
2719
  }
2717
2720
  close() {
2718
2721
  this.#toggle(false);
@@ -2912,7 +2915,7 @@ var Menu = class _Menu {
2912
2915
  }
2913
2916
  event.preventDefault();
2914
2917
  event.stopPropagation();
2915
- this.open(key === "ArrowUp");
2918
+ this.open(key === "ArrowDown" ? "first" : "last");
2916
2919
  };
2917
2920
  #onListKeyDown = (event) => {
2918
2921
  const { shiftKey, key } = event;
@@ -2928,7 +2931,8 @@ var Menu = class _Menu {
2928
2931
  "Enter",
2929
2932
  "Escape",
2930
2933
  " ",
2931
- ...this.#isSubmenu ? ["ArrowLeft"] : []
2934
+ "ArrowLeft",
2935
+ ...this.#isMenubar ? ["ArrowRight"] : []
2932
2936
  ].includes(key)) {
2933
2937
  return;
2934
2938
  }
@@ -2941,9 +2945,18 @@ var Menu = class _Menu {
2941
2945
  switch (key) {
2942
2946
  case "Tab":
2943
2947
  case "Escape":
2944
- case "ArrowLeft":
2945
2948
  this.close();
2946
2949
  return;
2950
+ case "ArrowLeft":
2951
+ if (this.#isMenubar) {
2952
+ this.#closeAndMoveFocus("previous");
2953
+ } else if (this.#isSubmenu && this.#triggerElement) {
2954
+ this.close();
2955
+ }
2956
+ return;
2957
+ case "ArrowRight":
2958
+ this.#closeAndMoveFocus("next");
2959
+ return;
2947
2960
  case "Enter":
2948
2961
  case " ":
2949
2962
  active.click();
@@ -2983,7 +2996,7 @@ var Menu = class _Menu {
2983
2996
  i.setAttribute("aria-checked", String(i === item));
2984
2997
  });
2985
2998
  };
2986
- #toggle(isOpen, isFocusLastItem = false) {
2999
+ #toggle(isOpen, options = {}) {
2987
3000
  if (this.#triggerElement?.ariaExpanded === String(isOpen)) {
2988
3001
  return;
2989
3002
  }
@@ -3008,7 +3021,8 @@ var Menu = class _Menu {
3008
3021
  style.setProperty("display", "block");
3009
3022
  style.setProperty("opacity", "0");
3010
3023
  this.#triggerElement && this.#updatePopover();
3011
- this.#itemElements.filter(isFocusable3).at(isFocusLastItem ? -1 : 0)?.focus();
3024
+ const { initialFocus = "first" } = options;
3025
+ this.#itemElements.filter(isFocusable3).at(initialFocus === "first" ? 0 : -1)?.focus();
3012
3026
  } else {
3013
3027
  this.#clearSubmenuTimer();
3014
3028
  this.#focusTrigger();
@@ -3073,8 +3087,18 @@ var Menu = class _Menu {
3073
3087
  this.#submenuTimer = void 0;
3074
3088
  }
3075
3089
  }
3090
+ #closeAndMoveFocus(direction) {
3091
+ this.close();
3092
+ requestAnimationFrame(
3093
+ () => (this.#externalTrigger ?? this.#triggerElement)?.dispatchEvent(
3094
+ new KeyboardEvent("keydown", {
3095
+ key: direction === "previous" ? "ArrowLeft" : "ArrowRight"
3096
+ })
3097
+ )
3098
+ );
3099
+ }
3076
3100
  #containsRoot(element) {
3077
- return this.#rootElement.contains(element) || this.#listElement?.contains(element);
3101
+ return this.#rootElement.contains(element) || !!this.#listElement?.contains(element);
3078
3102
  }
3079
3103
  #focusTrigger() {
3080
3104
  const active = getActiveElement4();
@@ -3237,7 +3261,7 @@ function isFocusable3(element) {
3237
3261
  * WAI-ARIA compliant menu (menu button) pattern implementation in TypeScript.
3238
3262
  * Supports checkbox item, radio item, and infinitely nested menus.
3239
3263
  *
3240
- * @version 1.4.8
3264
+ * @version 1.5.1
3241
3265
  * @author Yusuke Kamiyamane
3242
3266
  * @license MIT
3243
3267
  * @copyright Copyright (c) Yusuke Kamiyamane
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@y14e/menu",
3
- "version": "1.4.8",
3
+ "version": "1.5.1",
4
4
  "description": "WAI-ARIA compliant menu (menu button) pattern implementation in TypeScript",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",