@arraypress/extension-kit 1.11.0 → 1.11.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
@@ -511,7 +511,10 @@ button (or `focusTo`); an item runs once the menu has closed. `createMenu({id, p
511
511
  opens it below the button instead. `menuPosition` and `isInside` are the pure geometry. In a
512
512
  content script, give it the overlay's shadow root (`createMenu({root})`, a closed one too): the
513
513
  keys it handles don't reach the page's own shortcuts, and `destroy()` takes it off the page with
514
- its window listener. On a right-to-left page it lines up with its button's other edge.
514
+ its window listener. On a right-to-left page it lines up with its button's other edge. The
515
+ button's box is read before the menu opens (opening takes the focus, and a button shown only
516
+ while its row holds the focus has no box after); a button that isn't drawn opens it from its
517
+ nearest drawn ancestor, the row.
515
518
 
516
519
  `moreActionsButton(name, options)` is the 32px ⋮ icon button, labelled "More actions for “name”"
517
520
  (`label` for a translation), with `aria-haspopup="menu"`; its icon is the stylesheet's
@@ -570,7 +573,9 @@ toast.show('Copied'); // no Undo
570
573
  `#toast` (`role="status"`), bottom left, a manual `popover` so it's in the top layer, over
571
574
  anything the page stacks (`:popover-open` while it shows). A new message replaces the last and
572
575
  restarts the wait: 10 seconds, as chrome://bookmarks's (`TOAST_MS`), or `duration` /
573
- `undoDuration`. Undo hides it and runs once. `undoLabel` translates the button.
576
+ `undoDuration`. Undo hides it and runs once. `undoLabel` translates the button. While the keyboard
577
+ focus is on Undo, or the pointer is over the toast, it stays; the wait starts again in full once
578
+ they leave (as cr-toast, which stops its timer on `focusin`).
574
579
 
575
580
  ### Site list
576
581
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arraypress/extension-kit",
3
- "version": "1.11.0",
3
+ "version": "1.11.1",
4
4
  "description": "Shared pieces for Chrome extensions: Chrome's own page styles and components, a Chrome Sync settings store, the light/dark toolbar glyph, Chrome's on-device model, content-script helpers, About links, and build and test helpers.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -1085,7 +1085,10 @@ label.row, .row[actionable] { cursor: pointer; }
1085
1085
  margin-inline: 8px -4px;
1086
1086
  }
1087
1087
 
1088
+ /* Shown too while one of its buttons has its menu open: the menu is modal and takes the focus and
1089
+ the pointer, and the button it opened from (where the focus goes back) would vanish under it. */
1088
1090
  .sp-item:is(:hover, :focus-within) .sp-suffix,
1091
+ .sp-suffix:has([aria-expanded="true"]),
1089
1092
  .sp-suffix.always { display: flex; }
1090
1093
 
1091
1094
  /* .sp-notice (ui/notice.js): a card saying what a list leaves out or can't do; a warning's icon in
package/src/ui/menu.js CHANGED
@@ -101,6 +101,20 @@ export function isInside(box, x, y) {
101
101
  return x >= box.left && x <= box.right && y >= box.top && y <= box.bottom;
102
102
  }
103
103
 
104
+ /**
105
+ * The box a menu opens from: the anchor's, or, when the anchor has none (it isn't drawn: a row's
106
+ * buttons shown only while the row is hovered or holds the focus), its nearest ancestor's that has.
107
+ *
108
+ * @param {Element} anchor - The button that opened the menu.
109
+ * @returns {DOMRect | undefined} The box; undefined when nothing up to the root is drawn.
110
+ */
111
+ function anchorBox(anchor) {
112
+ for (let at = /** @type {Element | null} */ (anchor); at; at = at.parentElement) {
113
+ if (at.getClientRects().length) return at.getBoundingClientRect();
114
+ }
115
+ return undefined;
116
+ }
117
+
104
118
  /**
105
119
  * What {@link createMenu} takes.
106
120
  *
@@ -240,10 +254,13 @@ export function createMenu({ id, placement = 'over', root, doc = rootDocument(ro
240
254
  anchor.setAttribute('aria-expanded', 'true');
241
255
  expanded = anchor;
242
256
  }
257
+ // Measured before the menu opens: it takes the focus, and a button shown only while its row
258
+ // holds the focus (`.sp-suffix`) has no box after that, so the menu would open in the corner.
259
+ const from = anchor && anchorBox(anchor);
243
260
  menu.showModal();
244
261
  const { width, height } = menu.getBoundingClientRect();
245
262
  const { left, top } = menuPosition({ width, height }, { width: view.innerWidth, height: view.innerHeight }, {
246
- anchor: anchor?.getBoundingClientRect(), x, y, placement, rtl: view.getComputedStyle(menu).direction === 'rtl',
263
+ anchor: from, x, y, placement, rtl: view.getComputedStyle(menu).direction === 'rtl',
247
264
  });
248
265
  menu.style.left = `${left}px`;
249
266
  menu.style.top = `${top}px`;
package/src/ui/toast.d.ts CHANGED
@@ -2,6 +2,9 @@
2
2
  * Chrome's toast (cr-toast), bottom left, as chrome://bookmarks shows one
3
3
  * after a delete: "Deleted “Research” Undo". One per page, made by
4
4
  * {@link createToast}; a new message replaces the last and restarts the wait.
5
+ * While the keyboard focus is on Undo, or the pointer is over the toast, it
6
+ * stays, and the wait starts again in full once they leave (cr-toast stops
7
+ * its timer on `focusin` and restarts it on `focusout`).
5
8
  *
6
9
  * It's a manual `popover`, so it shows in the top layer: over anything the
7
10
  * page stacks (a content script's toast over a site's own fixed bars), and
package/src/ui/toast.js CHANGED
@@ -2,6 +2,9 @@
2
2
  * Chrome's toast (cr-toast), bottom left, as chrome://bookmarks shows one
3
3
  * after a delete: "Deleted “Research” Undo". One per page, made by
4
4
  * {@link createToast}; a new message replaces the last and restarts the wait.
5
+ * While the keyboard focus is on Undo, or the pointer is over the toast, it
6
+ * stays, and the wait starts again in full once they leave (cr-toast stops
7
+ * its timer on `focusin` and restarts it on `focusout`).
5
8
  *
6
9
  * It's a manual `popover`, so it shows in the top layer: over anything the
7
10
  * page stacks (a content script's toast over a site's own fixed bars), and
@@ -75,11 +78,27 @@ export function createToast({ undoLabel = 'Undo', duration = TOAST_MS, undoDurat
75
78
  /** @type {(() => unknown) | null} */
76
79
  let undoLast = null;
77
80
 
81
+ /** How long the toast showing now stays, once nothing holds it. */
82
+ let stay = duration;
83
+
78
84
  const hide = () => {
79
85
  clearTimeout(timer);
80
86
  toast.togglePopover(false);
81
87
  undoLast = null;
82
88
  };
89
+ /** Whether the keyboard focus is in the toast (on Undo) or the pointer over it: it stays while either is. */
90
+ const held = () => toast.matches(':focus-within, :hover');
91
+ /** The wait starts again, in full, unless the toast is held: as cr-toast's, whose focusout restarts it. */
92
+ const wait = () => {
93
+ clearTimeout(timer);
94
+ if (!held() && toast.matches(':popover-open')) timer = setTimeout(hide, stay);
95
+ };
96
+ // cr-toast stops its timer on focusin and restarts it on focusout, so Undo can't go from under
97
+ // the keyboard; the pointer over it holds it the same way (WCAG 2.2.1, Timing Adjustable).
98
+ toast.addEventListener('focusin', () => clearTimeout(timer));
99
+ toast.addEventListener('pointerenter', () => clearTimeout(timer));
100
+ toast.addEventListener('focusout', () => queueMicrotask(wait));
101
+ toast.addEventListener('pointerleave', wait);
83
102
  undoButton.addEventListener('click', () => {
84
103
  const undo = undoLast;
85
104
  hide();
@@ -98,7 +117,8 @@ export function createToast({ undoLabel = 'Undo', duration = TOAST_MS, undoDurat
98
117
  // Hidden and shown again, so it's the newest in the top layer (over a popover since).
99
118
  toast.togglePopover(false);
100
119
  toast.togglePopover(true);
101
- timer = setTimeout(hide, undo ? undoDuration : duration);
120
+ stay = undo ? undoDuration : duration;
121
+ wait();
102
122
  },
103
123
  hide,
104
124
  };