@hypericon/mico 0.5.11 → 0.5.13

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,38 @@
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
+ *
31
+ * N.B.: the menu will not be redrawn automatically, as it is not mounted using Mithril.
32
+ * Use `menuMgr.redraw()` to redraw the menu.
33
+ * @default false
34
+ */
35
+ keepOpenOnClick?: boolean;
13
36
  }
14
37
  export declare const Menu: m.Component<{
15
38
  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
  /**
@@ -20,6 +21,11 @@ export declare class MenuManager {
20
21
  * @param menuContents
21
22
  */
22
23
  show(ev: MouseEvent | undefined, menuContents: m.ChildArrayOrPrimitive, options?: MenuOptions): Promise<void>;
24
+ /**
25
+ * Manually redraw the currently open menu. Useful when menu content needs to update
26
+ * while the menu remains open (e.g., when using keepOpenOnClick with changing state).
27
+ */
28
+ redraw(): void;
23
29
  hide(): void;
24
30
  /**
25
31
  * Set the default behavior for whether menus are added as children of the element that raised the event.
@@ -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");
@@ -149,6 +160,27 @@ class MenuManager {
149
160
  menu_settings_1.MENU_SETTINGS.DEBUG_MENU && console.log("Added menu");
150
161
  });
151
162
  }
163
+ /**
164
+ * Manually redraw the currently open menu. Useful when menu content needs to update
165
+ * while the menu remains open (e.g., when using keepOpenOnClick with changing state).
166
+ */
167
+ redraw() {
168
+ var _a;
169
+ if (!this.isOpen || !this.menuVnode) {
170
+ return;
171
+ }
172
+ // Re-render the menu in whichever container it's currently mounted
173
+ if ((_a = this.currentOptions) === null || _a === void 0 ? void 0 : _a.asDialog) {
174
+ mithril_1.default.render(this.dialogEl, this.menuVnode);
175
+ }
176
+ else if (this.directParentWrapperEl) {
177
+ mithril_1.default.render(this.directParentWrapperEl, this.menuVnode);
178
+ }
179
+ else {
180
+ mithril_1.default.render(this.containerEl, this.menuVnode);
181
+ }
182
+ menu_settings_1.MENU_SETTINGS.DEBUG_MENU && console.log("Redrawn menu");
183
+ }
152
184
  hide() {
153
185
  var _a, _b;
154
186
  this._menuVnode.next(undefined);
@@ -158,6 +190,7 @@ class MenuManager {
158
190
  this.directParentEl = undefined;
159
191
  (_b = this.dialogEl) === null || _b === void 0 ? void 0 : _b.close();
160
192
  mithril_1.default.render(this.dialogEl, []);
193
+ this.currentOptions = undefined;
161
194
  menu_settings_1.MENU_SETTINGS.DEBUG_MENU && console.log("Closed menu");
162
195
  }
163
196
  /**
@@ -1,15 +1,38 @@
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
+ *
31
+ * N.B.: the menu will not be redrawn automatically, as it is not mounted using Mithril.
32
+ * Use `menuMgr.redraw()` to redraw the menu.
33
+ * @default false
34
+ */
35
+ keepOpenOnClick?: boolean;
13
36
  }
14
37
  export declare const Menu: m.Component<{
15
38
  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
  /**
@@ -20,6 +21,11 @@ export declare class MenuManager {
20
21
  * @param menuContents
21
22
  */
22
23
  show(ev: MouseEvent | undefined, menuContents: m.ChildArrayOrPrimitive, options?: MenuOptions): Promise<void>;
24
+ /**
25
+ * Manually redraw the currently open menu. Useful when menu content needs to update
26
+ * while the menu remains open (e.g., when using keepOpenOnClick with changing state).
27
+ */
28
+ redraw(): void;
23
29
  hide(): void;
24
30
  /**
25
31
  * Set the default behavior for whether menus are added as children of the element that raised the event.
@@ -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");
@@ -149,6 +160,27 @@ class MenuManager {
149
160
  menu_settings_1.MENU_SETTINGS.DEBUG_MENU && console.log("Added menu");
150
161
  });
151
162
  }
163
+ /**
164
+ * Manually redraw the currently open menu. Useful when menu content needs to update
165
+ * while the menu remains open (e.g., when using keepOpenOnClick with changing state).
166
+ */
167
+ redraw() {
168
+ var _a;
169
+ if (!this.isOpen || !this.menuVnode) {
170
+ return;
171
+ }
172
+ // Re-render the menu in whichever container it's currently mounted
173
+ if ((_a = this.currentOptions) === null || _a === void 0 ? void 0 : _a.asDialog) {
174
+ mithril_1.default.render(this.dialogEl, this.menuVnode);
175
+ }
176
+ else if (this.directParentWrapperEl) {
177
+ mithril_1.default.render(this.directParentWrapperEl, this.menuVnode);
178
+ }
179
+ else {
180
+ mithril_1.default.render(this.containerEl, this.menuVnode);
181
+ }
182
+ menu_settings_1.MENU_SETTINGS.DEBUG_MENU && console.log("Redrawn menu");
183
+ }
152
184
  hide() {
153
185
  var _a, _b;
154
186
  this._menuVnode.next(undefined);
@@ -158,6 +190,7 @@ class MenuManager {
158
190
  this.directParentEl = undefined;
159
191
  (_b = this.dialogEl) === null || _b === void 0 ? void 0 : _b.close();
160
192
  mithril_1.default.render(this.dialogEl, []);
193
+ this.currentOptions = undefined;
161
194
  menu_settings_1.MENU_SETTINGS.DEBUG_MENU && console.log("Closed menu");
162
195
  }
163
196
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypericon/mico",
3
- "version": "0.5.11",
3
+ "version": "0.5.13",
4
4
  "description": "UI components and utilities with Mithril",
5
5
  "author": "Ed Johnson, Hypericon Ltd",
6
6
  "license": "MIT",