@hypericon/mico 0.5.11 → 0.5.12

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,15 +1,35 @@
1
1
  import m from "mithril";
2
2
  export interface MenuOptions {
3
+ /** Prevent vertical flipping when menu would go off-screen at the bottom */
3
4
  noFlipV?: boolean;
5
+ /** Prevent horizontal flipping when menu would go off-screen at the right edge */
4
6
  noFlipH?: boolean;
7
+ /** Force the menu to flip vertically regardless of available space */
5
8
  forceFlipV?: boolean;
9
+ /** Force the menu to flip horizontally regardless of available space */
6
10
  forceFlipH?: boolean;
7
- /** Add the menu directly to the element which raised the event */
11
+ /**
12
+ * Add the menu directly to the element which raised the event, rather than
13
+ * to the global menu container
14
+ */
8
15
  asChild?: boolean;
9
- /** Optionally display a menu using an HTMLDialogElement */
16
+ /**
17
+ * Display the menu using an HTMLDialogElement instead of a standard div.
18
+ * Useful for complex stacking contexts.
19
+ */
10
20
  asDialog?: boolean;
11
- /** Whether the menu is within a dialog, and should be displayed differently */
21
+ /**
22
+ * Indicates the menu is within a dialog element and should be displayed with
23
+ * fixed positioning relative to the dialog
24
+ */
12
25
  isWithinDialog?: boolean;
26
+ /**
27
+ * Keep menu open when clicking within it. Useful for menus with toggle switches
28
+ * or radio buttons where users want to try multiple options without reopening the menu.
29
+ * The menu will still close when clicking outside or pressing Escape.
30
+ * @default false
31
+ */
32
+ keepOpenOnClick?: boolean;
13
33
  }
14
34
  export declare const Menu: m.Component<{
15
35
  x: number | undefined;
@@ -58,7 +58,16 @@ exports.Menu = {
58
58
  key: Date.now(),
59
59
  class: `mico-menu ${menu_settings_1.MENU_SETTINGS.MENU_CLASS}`,
60
60
  style: `position: ${position}; overflow-y: auto; overflow-x: hidden; ${posH} ${posV} z-index: 100; min-width: ${menu_constants_1.MENU_CONSTANTS.MIN_WIDTH_PX}px; max-width: ${maxWidthPx}px; max-height: ${maxHeightPx}px;`,
61
+ role: "menu",
62
+ "aria-orientation": "vertical",
61
63
  tabIndex: -1,
64
+ onclick: (ev) => {
65
+ var _a;
66
+ // Prevent clicks within the menu from propagating when keepOpenOnClick is enabled
67
+ if ((_a = attrs.options) === null || _a === void 0 ? void 0 : _a.keepOpenOnClick) {
68
+ ev.stopPropagation();
69
+ }
70
+ },
62
71
  onkeydown: (ev) => {
63
72
  var _a, _b, _c, _d, _e, _f;
64
73
  const focussableElems = Array.from((_b = (_a = this.el) === null || _a === void 0 ? void 0 : _a.querySelectorAll("button, input, select, textarea")) !== null && _b !== void 0 ? _b : [])
@@ -14,8 +14,10 @@ exports.MenuButton = {
14
14
  (0, mithril_1.default)(mithril_1.default.route.Link, {
15
15
  class: `mico-menu-button ${(_a = attrs.class) !== null && _a !== void 0 ? _a : ""} ${menu_settings_1.MENU_SETTINGS.MENU_BUTTON_CLASS}`,
16
16
  style: `${(_b = attrs.style) !== null && _b !== void 0 ? _b : ""} width: 100%; height: 1.5rem; padding-right: 0.5rem; display: flex; align-items: center;`,
17
+ role: "menuitem",
17
18
  href: attrs.href,
18
19
  disabled: attrs.disabled,
20
+ "aria-disabled": attrs.disabled ? "true" : undefined,
19
21
  onclick: (ev) => {
20
22
  var _a;
21
23
  (_a = attrs.onclick) === null || _a === void 0 ? void 0 : _a.call(attrs, ev);
@@ -31,7 +33,9 @@ exports.MenuButton = {
31
33
  (0, mithril_1.default)("button", {
32
34
  class: `mico-menu-button ${(_c = attrs.class) !== null && _c !== void 0 ? _c : ""} ${menu_settings_1.MENU_SETTINGS.MENU_BUTTON_CLASS}`,
33
35
  style: `${(_d = attrs.style) !== null && _d !== void 0 ? _d : ""} width: 100%; height: 1.5rem; padding-right: 0.5rem; display: flex; align-items: center;`,
36
+ role: "menuitem",
34
37
  disabled: attrs.disabled,
38
+ "aria-disabled": attrs.disabled ? "true" : undefined,
35
39
  onclick: (ev) => {
36
40
  var _a;
37
41
  (_a = attrs.onclick) === null || _a === void 0 ? void 0 : _a.call(attrs, ev);
@@ -12,6 +12,8 @@ exports.MenuSeparator = {
12
12
  return (0, mithril_1.default)("", {
13
13
  class: `mico-menu-separator ${menu_settings_1.MENU_SETTINGS.MENU_SEPARATOR_CLASS}`,
14
14
  style: "margin: 0.25rem 0; border-bottom-width: 1px; border-bottom-style: solid;",
15
+ role: "separator",
16
+ "aria-orientation": "horizontal",
15
17
  });
16
18
  }
17
19
  };
@@ -8,6 +8,7 @@ export declare class MenuManager {
8
8
  private _menuVnode;
9
9
  menuVnode$: import("rxjs").Observable<m.Vnode<any, any> | undefined>;
10
10
  get menuVnode(): m.Vnode<any, any> | undefined;
11
+ private currentOptions?;
11
12
  get isOpen(): boolean;
12
13
  constructor();
13
14
  /**
@@ -29,8 +29,18 @@ class MenuManager {
29
29
  this.containerEl = document.createElement("div");
30
30
  this.containerEl.classList.add("menu-container");
31
31
  document.body.append(this.containerEl);
32
- document.addEventListener("click", () => {
32
+ document.addEventListener("click", (ev) => {
33
+ var _a;
33
34
  if (this.isOpen) {
35
+ // If keepOpenOnClick is enabled, check if the click is within the menu
36
+ if ((_a = this.currentOptions) === null || _a === void 0 ? void 0 : _a.keepOpenOnClick) {
37
+ const target = ev.target;
38
+ const clickedInMenu = target.closest(".mico-menu");
39
+ if (clickedInMenu) {
40
+ menu_settings_1.MENU_SETTINGS.DEBUG_MENU && console.log("Click within menu, keeping open (keepOpenOnClick=true)");
41
+ return; // Don't close the menu
42
+ }
43
+ }
34
44
  this.hide();
35
45
  }
36
46
  });
@@ -106,6 +116,7 @@ class MenuManager {
106
116
  menuContents,
107
117
  ]);
108
118
  this._menuVnode.next(menuVnode);
119
+ this.currentOptions = options;
109
120
  // If the menu is to be shown as a dialog, we need to mount it to a dialog element
110
121
  if (options === null || options === void 0 ? void 0 : options.asDialog) {
111
122
  // this.dialogEl = document.createElement("dialog");
@@ -158,6 +169,7 @@ class MenuManager {
158
169
  this.directParentEl = undefined;
159
170
  (_b = this.dialogEl) === null || _b === void 0 ? void 0 : _b.close();
160
171
  mithril_1.default.render(this.dialogEl, []);
172
+ this.currentOptions = undefined;
161
173
  menu_settings_1.MENU_SETTINGS.DEBUG_MENU && console.log("Closed menu");
162
174
  }
163
175
  /**
@@ -1,15 +1,35 @@
1
1
  import m from "mithril";
2
2
  export interface MenuOptions {
3
+ /** Prevent vertical flipping when menu would go off-screen at the bottom */
3
4
  noFlipV?: boolean;
5
+ /** Prevent horizontal flipping when menu would go off-screen at the right edge */
4
6
  noFlipH?: boolean;
7
+ /** Force the menu to flip vertically regardless of available space */
5
8
  forceFlipV?: boolean;
9
+ /** Force the menu to flip horizontally regardless of available space */
6
10
  forceFlipH?: boolean;
7
- /** Add the menu directly to the element which raised the event */
11
+ /**
12
+ * Add the menu directly to the element which raised the event, rather than
13
+ * to the global menu container
14
+ */
8
15
  asChild?: boolean;
9
- /** Optionally display a menu using an HTMLDialogElement */
16
+ /**
17
+ * Display the menu using an HTMLDialogElement instead of a standard div.
18
+ * Useful for complex stacking contexts.
19
+ */
10
20
  asDialog?: boolean;
11
- /** Whether the menu is within a dialog, and should be displayed differently */
21
+ /**
22
+ * Indicates the menu is within a dialog element and should be displayed with
23
+ * fixed positioning relative to the dialog
24
+ */
12
25
  isWithinDialog?: boolean;
26
+ /**
27
+ * Keep menu open when clicking within it. Useful for menus with toggle switches
28
+ * or radio buttons where users want to try multiple options without reopening the menu.
29
+ * The menu will still close when clicking outside or pressing Escape.
30
+ * @default false
31
+ */
32
+ keepOpenOnClick?: boolean;
13
33
  }
14
34
  export declare const Menu: m.Component<{
15
35
  x: number | undefined;
@@ -58,7 +58,16 @@ exports.Menu = {
58
58
  key: Date.now(),
59
59
  class: `mico-menu ${menu_settings_1.MENU_SETTINGS.MENU_CLASS}`,
60
60
  style: `position: ${position}; overflow-y: auto; overflow-x: hidden; ${posH} ${posV} z-index: 100; min-width: ${menu_constants_1.MENU_CONSTANTS.MIN_WIDTH_PX}px; max-width: ${maxWidthPx}px; max-height: ${maxHeightPx}px;`,
61
+ role: "menu",
62
+ "aria-orientation": "vertical",
61
63
  tabIndex: -1,
64
+ onclick: (ev) => {
65
+ var _a;
66
+ // Prevent clicks within the menu from propagating when keepOpenOnClick is enabled
67
+ if ((_a = attrs.options) === null || _a === void 0 ? void 0 : _a.keepOpenOnClick) {
68
+ ev.stopPropagation();
69
+ }
70
+ },
62
71
  onkeydown: (ev) => {
63
72
  var _a, _b, _c, _d, _e, _f;
64
73
  const focussableElems = Array.from((_b = (_a = this.el) === null || _a === void 0 ? void 0 : _a.querySelectorAll("button, input, select, textarea")) !== null && _b !== void 0 ? _b : [])
@@ -14,8 +14,10 @@ exports.MenuButton = {
14
14
  (0, mithril_1.default)(mithril_1.default.route.Link, {
15
15
  class: `mico-menu-button ${(_a = attrs.class) !== null && _a !== void 0 ? _a : ""} ${menu_settings_1.MENU_SETTINGS.MENU_BUTTON_CLASS}`,
16
16
  style: `${(_b = attrs.style) !== null && _b !== void 0 ? _b : ""} width: 100%; height: 1.5rem; padding-right: 0.5rem; display: flex; align-items: center;`,
17
+ role: "menuitem",
17
18
  href: attrs.href,
18
19
  disabled: attrs.disabled,
20
+ "aria-disabled": attrs.disabled ? "true" : undefined,
19
21
  onclick: (ev) => {
20
22
  var _a;
21
23
  (_a = attrs.onclick) === null || _a === void 0 ? void 0 : _a.call(attrs, ev);
@@ -31,7 +33,9 @@ exports.MenuButton = {
31
33
  (0, mithril_1.default)("button", {
32
34
  class: `mico-menu-button ${(_c = attrs.class) !== null && _c !== void 0 ? _c : ""} ${menu_settings_1.MENU_SETTINGS.MENU_BUTTON_CLASS}`,
33
35
  style: `${(_d = attrs.style) !== null && _d !== void 0 ? _d : ""} width: 100%; height: 1.5rem; padding-right: 0.5rem; display: flex; align-items: center;`,
36
+ role: "menuitem",
34
37
  disabled: attrs.disabled,
38
+ "aria-disabled": attrs.disabled ? "true" : undefined,
35
39
  onclick: (ev) => {
36
40
  var _a;
37
41
  (_a = attrs.onclick) === null || _a === void 0 ? void 0 : _a.call(attrs, ev);
@@ -12,6 +12,8 @@ exports.MenuSeparator = {
12
12
  return (0, mithril_1.default)("", {
13
13
  class: `mico-menu-separator ${menu_settings_1.MENU_SETTINGS.MENU_SEPARATOR_CLASS}`,
14
14
  style: "margin: 0.25rem 0; border-bottom-width: 1px; border-bottom-style: solid;",
15
+ role: "separator",
16
+ "aria-orientation": "horizontal",
15
17
  });
16
18
  }
17
19
  };
@@ -8,6 +8,7 @@ export declare class MenuManager {
8
8
  private _menuVnode;
9
9
  menuVnode$: import("rxjs").Observable<m.Vnode<any, any> | undefined>;
10
10
  get menuVnode(): m.Vnode<any, any> | undefined;
11
+ private currentOptions?;
11
12
  get isOpen(): boolean;
12
13
  constructor();
13
14
  /**
@@ -29,8 +29,18 @@ class MenuManager {
29
29
  this.containerEl = document.createElement("div");
30
30
  this.containerEl.classList.add("menu-container");
31
31
  document.body.append(this.containerEl);
32
- document.addEventListener("click", () => {
32
+ document.addEventListener("click", (ev) => {
33
+ var _a;
33
34
  if (this.isOpen) {
35
+ // If keepOpenOnClick is enabled, check if the click is within the menu
36
+ if ((_a = this.currentOptions) === null || _a === void 0 ? void 0 : _a.keepOpenOnClick) {
37
+ const target = ev.target;
38
+ const clickedInMenu = target.closest(".mico-menu");
39
+ if (clickedInMenu) {
40
+ menu_settings_1.MENU_SETTINGS.DEBUG_MENU && console.log("Click within menu, keeping open (keepOpenOnClick=true)");
41
+ return; // Don't close the menu
42
+ }
43
+ }
34
44
  this.hide();
35
45
  }
36
46
  });
@@ -106,6 +116,7 @@ class MenuManager {
106
116
  menuContents,
107
117
  ]);
108
118
  this._menuVnode.next(menuVnode);
119
+ this.currentOptions = options;
109
120
  // If the menu is to be shown as a dialog, we need to mount it to a dialog element
110
121
  if (options === null || options === void 0 ? void 0 : options.asDialog) {
111
122
  // this.dialogEl = document.createElement("dialog");
@@ -158,6 +169,7 @@ class MenuManager {
158
169
  this.directParentEl = undefined;
159
170
  (_b = this.dialogEl) === null || _b === void 0 ? void 0 : _b.close();
160
171
  mithril_1.default.render(this.dialogEl, []);
172
+ this.currentOptions = undefined;
161
173
  menu_settings_1.MENU_SETTINGS.DEBUG_MENU && console.log("Closed menu");
162
174
  }
163
175
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypericon/mico",
3
- "version": "0.5.11",
3
+ "version": "0.5.12",
4
4
  "description": "UI components and utilities with Mithril",
5
5
  "author": "Ed Johnson, Hypericon Ltd",
6
6
  "license": "MIT",