@arraypress/extension-kit 1.10.0 → 1.11.0

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 (68) hide show
  1. package/README.md +97 -20
  2. package/bin/extension-kit.js +2 -1
  3. package/package.json +1 -1
  4. package/src/ai/host.d.ts +183 -0
  5. package/src/ai/host.js +225 -0
  6. package/src/ai/model.d.ts +2 -1
  7. package/src/ai/model.js +10 -2
  8. package/src/build/vendor.d.ts +29 -8
  9. package/src/build/vendor.js +136 -40
  10. package/src/content/highlight.d.ts +15 -6
  11. package/src/content/highlight.js +42 -17
  12. package/src/content/overlay.d.ts +19 -4
  13. package/src/content/overlay.js +44 -6
  14. package/src/content/slices.d.ts +3 -1
  15. package/src/content/slices.js +29 -14
  16. package/src/content/watch.d.ts +4 -2
  17. package/src/content/watch.js +17 -3
  18. package/src/dom/focus.d.ts +11 -0
  19. package/src/dom/focus.js +15 -0
  20. package/src/frame/window.d.ts +1 -1
  21. package/src/frame/window.js +8 -7
  22. package/src/index.d.ts +10 -3
  23. package/src/index.js +8 -3
  24. package/src/panel/show-on-page.d.ts +82 -0
  25. package/src/panel/show-on-page.js +83 -0
  26. package/src/scripting/call.d.ts +29 -9
  27. package/src/scripting/call.js +35 -11
  28. package/src/scripting/context-menus.d.ts +45 -0
  29. package/src/scripting/context-menus.js +53 -0
  30. package/src/scripting/registration.d.ts +2 -1
  31. package/src/scripting/registration.js +2 -1
  32. package/src/scripting/tab.d.ts +17 -5
  33. package/src/scripting/tab.js +15 -7
  34. package/src/settings/saver.js +16 -6
  35. package/src/settings/storage.d.ts +33 -2
  36. package/src/settings/storage.js +73 -3
  37. package/src/settings/store.d.ts +17 -0
  38. package/src/settings/store.js +87 -20
  39. package/src/sites/sites.d.ts +6 -1
  40. package/src/sites/sites.js +20 -3
  41. package/src/styles/chrome-ui.css +56 -30
  42. package/src/testing/e2e.d.ts +35 -4
  43. package/src/testing/e2e.js +67 -5
  44. package/src/testing/fake-model.d.ts +9 -1
  45. package/src/testing/fake-model.js +44 -23
  46. package/src/testing/fake-storage.d.ts +41 -13
  47. package/src/testing/fake-storage.js +163 -51
  48. package/src/testing/index.d.ts +3 -3
  49. package/src/testing/index.js +5 -3
  50. package/src/testing/live-run.d.ts +23 -2
  51. package/src/testing/live-run.js +64 -18
  52. package/src/testing/messages.d.ts +7 -3
  53. package/src/testing/messages.js +50 -9
  54. package/src/testing/package.d.ts +56 -7
  55. package/src/testing/package.js +57 -18
  56. package/src/testing/prompt.d.ts +4 -2
  57. package/src/testing/prompt.js +7 -3
  58. package/src/time/pool.d.ts +2 -1
  59. package/src/time/pool.js +4 -2
  60. package/src/toolbar/theme.d.ts +10 -2
  61. package/src/toolbar/theme.js +12 -4
  62. package/src/ui/menu.d.ts +13 -3
  63. package/src/ui/menu.js +31 -11
  64. package/src/ui/model-row.js +5 -1
  65. package/src/ui/site-list.js +12 -3
  66. package/src/ui/text-dialog.d.ts +3 -2
  67. package/src/ui/text-dialog.js +10 -3
  68. package/src/ui/toast.js +2 -0
@@ -132,9 +132,12 @@ export type OffscreenApi = {
132
132
  *
133
133
  * @param {OffscreenApi} api - `chrome.offscreen` and `chrome.runtime`.
134
134
  * @param {string} [url] - The document, from the extension's root. Default {@link OFFSCREEN_PATH}.
135
+ * @param {string} [justification] - What the document is for, when it does more than watch the
136
+ * scheme (it answers for Chrome's on-device model too, `serveModelOffscreen`): Chrome keeps it
137
+ * with the document, so it has to be the whole truth.
135
138
  * @returns {Promise<boolean>} Whether it opened one (false: already open).
136
139
  */
137
- export declare function watchFromOffscreen({ offscreen, runtime }: OffscreenApi, url?: string): Promise<boolean>;
140
+ export declare function watchFromOffscreen({ offscreen, runtime }: OffscreenApi, url?: string, justification?: string): Promise<boolean>;
138
141
  export type ToolbarWorkerApi = {
139
142
  /**
140
143
  * `chrome.storage`.
@@ -179,9 +182,14 @@ export type ToolbarWorkerApi = {
179
182
  * worker wakes.
180
183
  *
181
184
  * @param {ToolbarWorkerApi} api - `chrome`, or a fake.
185
+ * @param {{url?: string, justification?: string}} [document] - The offscreen document and what
186
+ * it's for, when it isn't the kit's defaults ({@link watchFromOffscreen}).
182
187
  * @returns {ToolbarTheme} The theme, for tests.
183
188
  * @example
184
189
  * // background.js
185
190
  * startToolbarTheme(chrome);
186
191
  */
187
- export declare function startToolbarTheme(api: ToolbarWorkerApi): ToolbarTheme;
192
+ export declare function startToolbarTheme(api: ToolbarWorkerApi, { url, justification }?: {
193
+ url?: string;
194
+ justification?: string;
195
+ }): ToolbarTheme;
@@ -116,6 +116,9 @@ export function createToolbarTheme({ storage, action, extensionId }) {
116
116
  /** @type {Promise<boolean> | null} */
117
117
  let opening = null;
118
118
 
119
+ /** Why the offscreen document is open, as Chrome is told. */
120
+ const SCHEME_JUSTIFICATION = 'Reads whether Chrome is in light or dark mode, to show the matching toolbar icon.';
121
+
119
122
  /**
120
123
  * Open the offscreen document that watches the scheme (reason MATCH_MEDIA),
121
124
  * unless it's open. It stays open: it's one small page in the extension's own
@@ -125,16 +128,19 @@ let opening = null;
125
128
  *
126
129
  * @param {OffscreenApi} api - `chrome.offscreen` and `chrome.runtime`.
127
130
  * @param {string} [url] - The document, from the extension's root. Default {@link OFFSCREEN_PATH}.
131
+ * @param {string} [justification] - What the document is for, when it does more than watch the
132
+ * scheme (it answers for Chrome's on-device model too, `serveModelOffscreen`): Chrome keeps it
133
+ * with the document, so it has to be the whole truth.
128
134
  * @returns {Promise<boolean>} Whether it opened one (false: already open).
129
135
  */
130
- export function watchFromOffscreen({ offscreen, runtime }, url = OFFSCREEN_PATH) {
136
+ export function watchFromOffscreen({ offscreen, runtime }, url = OFFSCREEN_PATH, justification = SCHEME_JUSTIFICATION) {
131
137
  opening ??= (async () => {
132
138
  const open = await runtime.getContexts({ contextTypes: ['OFFSCREEN_DOCUMENT'] });
133
139
  if (open.length) return false;
134
140
  await offscreen.createDocument({
135
141
  url,
136
142
  reasons: [offscreen.Reason.MATCH_MEDIA],
137
- justification: 'Reads whether Chrome is in light or dark mode, to show the matching toolbar icon.',
143
+ justification,
138
144
  });
139
145
  return true;
140
146
  })().finally(() => { opening = null; });
@@ -159,15 +165,17 @@ export function watchFromOffscreen({ offscreen, runtime }, url = OFFSCREEN_PATH)
159
165
  * worker wakes.
160
166
  *
161
167
  * @param {ToolbarWorkerApi} api - `chrome`, or a fake.
168
+ * @param {{url?: string, justification?: string}} [document] - The offscreen document and what
169
+ * it's for, when it isn't the kit's defaults ({@link watchFromOffscreen}).
162
170
  * @returns {ToolbarTheme} The theme, for tests.
163
171
  * @example
164
172
  * // background.js
165
173
  * startToolbarTheme(chrome);
166
174
  */
167
- export function startToolbarTheme(api) {
175
+ export function startToolbarTheme(api, { url, justification } = {}) {
168
176
  const theme = createToolbarTheme({ storage: api.storage.local, action: api.action, extensionId: api.runtime.id });
169
177
  theme.restore();
170
- watchFromOffscreen(api).catch((error) => console.error('Opening the colour scheme watcher:', error));
178
+ watchFromOffscreen(api, url, justification).catch((error) => console.error('Opening the colour scheme watcher:', error));
171
179
  api.runtime.onMessage.addListener((message, sender) => {
172
180
  theme.onMessage(message, sender);
173
181
  return false;
package/src/ui/menu.d.ts CHANGED
@@ -99,8 +99,10 @@ export type Box = {
99
99
  *
100
100
  * @param {{width: number, height: number}} size - The menu's size.
101
101
  * @param {{width: number, height: number}} viewport - The window's (`innerWidth`, `innerHeight`).
102
- * @param {{anchor?: Box, x?: number, y?: number, placement?: 'over' | 'below'}} where - The anchor's
103
- * box, or a point; `placement` says where it goes from an anchor (default `'over'`).
102
+ * @param {{anchor?: Box, x?: number, y?: number, placement?: 'over' | 'below', rtl?: boolean}} where - The
103
+ * anchor's box, or a point; `placement` says where it goes from an anchor (default `'over'`); `rtl`
104
+ * (a right-to-left page) lines its start up with the anchor's left edge and opens leftwards from a
105
+ * point, as Chrome mirrors its own.
104
106
  * @returns {{left: number, top: number}} The menu's top-left, in px.
105
107
  * @example
106
108
  * menuPosition({width: 160, height: 120}, {width: 800, height: 600}, {anchor: button.getBoundingClientRect()});
@@ -112,11 +114,12 @@ export declare function menuPosition(size: {
112
114
  }, viewport: {
113
115
  width: number;
114
116
  height: number;
115
- }, { anchor, x, y, placement }: {
117
+ }, { anchor, x, y, placement, rtl }: {
116
118
  anchor?: Box;
117
119
  x?: number;
118
120
  y?: number;
119
121
  placement?: 'over' | 'below';
122
+ rtl?: boolean;
120
123
  }): {
121
124
  left: number;
122
125
  top: number;
@@ -163,6 +166,11 @@ export type ActionMenu = {
163
166
  * Close it, if open, and return the focus.
164
167
  */
165
168
  close: () => void;
169
+ /**
170
+ * Take it off the page and stop listening to the window (a content
171
+ * script's overlay being removed).
172
+ */
173
+ destroy: () => void;
166
174
  };
167
175
  /**
168
176
  * What {@link createMenu} takes.
@@ -182,6 +190,8 @@ export type ActionMenu = {
182
190
  * @property {(entries: readonly MenuEntry[], where: MenuPlace) => void} open Fill it with `entries`
183
191
  * and open it (closing it first if it's open).
184
192
  * @property {() => void} close Close it, if open, and return the focus.
193
+ * @property {() => void} destroy Take it off the page and stop listening to the window (a content
194
+ * script's overlay being removed).
185
195
  */
186
196
  /**
187
197
  * Make a page's action menu: one `<dialog class="menu">` that any number of
package/src/ui/menu.js CHANGED
@@ -14,7 +14,7 @@
14
14
  * @module ui/menu
15
15
  */
16
16
 
17
- import { focusedElement, rootDocument } from '../dom/focus.js';
17
+ import { focusedElement, focusedIn, rootDocument } from '../dom/focus.js';
18
18
 
19
19
  /** The gap kept between a menu and the window's edges, in px. */
20
20
  export const MENU_EDGE = 8;
@@ -67,19 +67,21 @@ const BELOW_GAP = 4;
67
67
  *
68
68
  * @param {{width: number, height: number}} size - The menu's size.
69
69
  * @param {{width: number, height: number}} viewport - The window's (`innerWidth`, `innerHeight`).
70
- * @param {{anchor?: Box, x?: number, y?: number, placement?: 'over' | 'below'}} where - The anchor's
71
- * box, or a point; `placement` says where it goes from an anchor (default `'over'`).
70
+ * @param {{anchor?: Box, x?: number, y?: number, placement?: 'over' | 'below', rtl?: boolean}} where - The
71
+ * anchor's box, or a point; `placement` says where it goes from an anchor (default `'over'`); `rtl`
72
+ * (a right-to-left page) lines its start up with the anchor's left edge and opens leftwards from a
73
+ * point, as Chrome mirrors its own.
72
74
  * @returns {{left: number, top: number}} The menu's top-left, in px.
73
75
  * @example
74
76
  * menuPosition({width: 160, height: 120}, {width: 800, height: 600}, {anchor: button.getBoundingClientRect()});
75
77
  * // → {left: anchor.right - 160, top: anchor.top}
76
78
  */
77
- export function menuPosition(size, viewport, { anchor, x = 0, y = 0, placement = 'over' }) {
79
+ export function menuPosition(size, viewport, { anchor, x = 0, y = 0, placement = 'over', rtl = false }) {
78
80
  const { width, height } = size;
79
81
  const left = (/** @type {number} */ start) => Math.max(MENU_EDGE, Math.min(start, viewport.width - width - MENU_EDGE));
80
82
  const room = (/** @type {number} */ top) => top + height <= viewport.height - MENU_EDGE;
81
- if (!anchor) return { left: left(x), top: Math.max(MENU_EDGE, room(y) ? y : y - height) };
82
- const start = left(anchor.right - width);
83
+ if (!anchor) return { left: left(rtl ? x - width : x), top: Math.max(MENU_EDGE, room(y) ? y : y - height) };
84
+ const start = left(rtl ? anchor.left : anchor.right - width);
83
85
  if (placement === 'below') {
84
86
  const below = anchor.bottom + BELOW_GAP;
85
87
  return { left: start, top: Math.max(MENU_EDGE, room(below) ? below : anchor.top - height - BELOW_GAP) };
@@ -118,6 +120,8 @@ export function isInside(box, x, y) {
118
120
  * @property {(entries: readonly MenuEntry[], where: MenuPlace) => void} open Fill it with `entries`
119
121
  * and open it (closing it first if it's open).
120
122
  * @property {() => void} close Close it, if open, and return the focus.
123
+ * @property {() => void} destroy Take it off the page and stop listening to the window (a content
124
+ * script's overlay being removed).
121
125
  */
122
126
 
123
127
  /**
@@ -168,21 +172,32 @@ export function createMenu({ id, placement = 'over', root, doc = rootDocument(ro
168
172
 
169
173
  menu.addEventListener('keydown', (event) => {
170
174
  const list = items();
171
- const at = list.indexOf(/** @type {HTMLButtonElement} */ (focusedElement(doc)));
175
+ // Asked of the menu's own root: a closed shadow root's focused element can't be seen from the document.
176
+ const at = list.indexOf(/** @type {HTMLButtonElement} */ (focusedIn(menu)));
172
177
  /** @param {number} index */
173
178
  const go = (index) => {
174
- event.preventDefault();
179
+ handled();
175
180
  list[(index + list.length) % list.length]?.focus();
176
181
  };
182
+ /** The key is the menu's: the page under a content script's menu doesn't act on it too (its own shortcuts). */
183
+ const handled = () => {
184
+ event.preventDefault();
185
+ event.stopPropagation();
186
+ };
177
187
  if (event.key === 'ArrowDown') go(at + 1);
178
188
  else if (event.key === 'ArrowUp') go(at < 0 ? -1 : at - 1);
179
189
  else if (event.key === 'Home') go(0);
180
190
  else if (event.key === 'End') go(-1);
181
191
  else if (event.key === 'Escape' || event.key === 'Tab') {
182
- event.preventDefault();
192
+ handled();
183
193
  close();
184
194
  }
185
195
  });
196
+ // Closed any other way (the browser's own close, another script): the button no longer says it's open.
197
+ menu.addEventListener('close', () => {
198
+ expanded?.setAttribute('aria-expanded', 'false');
199
+ expanded = null;
200
+ });
186
201
  // Esc the browser handles itself (when the keydown above didn't see it).
187
202
  menu.addEventListener('cancel', (event) => {
188
203
  event.preventDefault();
@@ -219,7 +234,7 @@ export function createMenu({ id, placement = 'over', root, doc = rootDocument(ro
219
234
  if (label) menu.setAttribute('aria-label', label);
220
235
  else menu.removeAttribute('aria-label');
221
236
 
222
- const active = focusedElement(doc);
237
+ const active = focusedIn(menu) ?? focusedElement(doc);
223
238
  returnFocus = focusTo ?? anchor ?? (active instanceof view.HTMLElement ? active : null);
224
239
  if (anchor?.hasAttribute('aria-haspopup')) {
225
240
  anchor.setAttribute('aria-expanded', 'true');
@@ -228,13 +243,18 @@ export function createMenu({ id, placement = 'over', root, doc = rootDocument(ro
228
243
  menu.showModal();
229
244
  const { width, height } = menu.getBoundingClientRect();
230
245
  const { left, top } = menuPosition({ width, height }, { width: view.innerWidth, height: view.innerHeight }, {
231
- anchor: anchor?.getBoundingClientRect(), x, y, placement,
246
+ anchor: anchor?.getBoundingClientRect(), x, y, placement, rtl: view.getComputedStyle(menu).direction === 'rtl',
232
247
  });
233
248
  menu.style.left = `${left}px`;
234
249
  menu.style.top = `${top}px`;
235
250
  items()[0]?.focus();
236
251
  },
237
252
  close,
253
+ destroy() {
254
+ shut();
255
+ view.removeEventListener('blur', close);
256
+ menu.remove();
257
+ },
238
258
  };
239
259
  }
240
260
 
@@ -94,7 +94,11 @@ export function createModelRow({ status, help, download, progress }, { options,
94
94
  progress.value = loaded;
95
95
  status.textContent = t(MODEL_MESSAGES.downloading, [String(Math.round(loaded * 100))]);
96
96
  },
97
- }).then(async () => show(await state()), () => show('downloadable', t(MODEL_MESSAGES.downloadFailed)));
97
+ }).then(
98
+ // The state as the code that uses it sees it; if it can't be asked (the worker didn't answer), as this page does.
99
+ async (here) => show(await state().catch(() => here)),
100
+ () => show('downloadable', t(MODEL_MESSAGES.downloadFailed)),
101
+ );
98
102
  });
99
103
 
100
104
  return {
@@ -27,6 +27,7 @@
27
27
  * @module ui/site-list
28
28
  */
29
29
 
30
+ import { focusedIn } from '../dom/focus.js';
30
31
  import { createMenu, moreActionsButton } from './menu.js';
31
32
  import { askText } from './text-dialog.js';
32
33
  import { MAX_HOST_LENGTH, siteLabel } from '../sites/sites.js';
@@ -177,9 +178,11 @@ export function createSiteList({
177
178
  form?.addEventListener('submit', async (event) => {
178
179
  event.preventDefault();
179
180
  const input = /** @type {HTMLInputElement} */ (form.querySelector('input'));
180
- const site = parse(input.value);
181
- if (!site) invalid?.(input.value);
182
- else if (await add(site) !== false) input.value = '';
181
+ const typed = input.value;
182
+ const site = parse(typed);
183
+ if (!site) invalid?.(typed);
184
+ // Cleared only if it's still what was added: adding can wait on Chrome (a permission prompt), and the next site may be typed meanwhile.
185
+ else if (await add(site) !== false && input.value === typed) input.value = '';
183
186
  });
184
187
  addButton?.addEventListener('click', async () => {
185
188
  const site = await ask(w.addSite, w.add);
@@ -219,6 +222,11 @@ export function createSiteList({
219
222
 
220
223
  return {
221
224
  render(sites) {
225
+ // The focus stays in the list across a redraw: on the same row's button (the one after a removed
226
+ // row, the last after the last), or on the way to add when no row is left. Without this, the
227
+ // row's button a dialog or menu gave the focus back to is replaced and the focus falls to the page.
228
+ const focused = focusedIn(list);
229
+ const at = focused && list.contains(focused) ? [...list.children].findIndex((row) => row.contains(focused)) : -1;
222
230
  list.replaceChildren(...sites.map((site) => {
223
231
  const row = doc.createElement('li');
224
232
  row.className = 'row';
@@ -229,6 +237,7 @@ export function createSiteList({
229
237
  return row;
230
238
  }));
231
239
  if (empty) empty.hidden = sites.length > 0;
240
+ if (at >= 0) (list.children[Math.min(at, sites.length - 1)]?.querySelector('button') ?? addButton ?? form?.querySelector('input'))?.focus();
232
241
  },
233
242
  };
234
243
  }
@@ -4,8 +4,9 @@
4
4
  * folder's name (`askText`); several, as chrome://settings/searchEngines asks
5
5
  * for a site search (`askFields`). The action button stays disabled while a
6
6
  * field is blank or isn't valid, with the reason under that field. Enter
7
- * means the action button, Esc and Cancel cancel, and the focus goes back to
8
- * what opened it. The page needs no markup: the dialog (`#text-dialog`) is
7
+ * means the action button, Esc and Cancel cancel (as does the dialog being
8
+ * closed any other way), and the focus goes back to what opened it. A reason
9
+ * is announced as it appears, and its field marked invalid. The page needs no markup: the dialog (`#text-dialog`) is
9
10
  * made the first time, its fields each time it's asked.
10
11
  *
11
12
  * @module ui/text-dialog
@@ -4,8 +4,9 @@
4
4
  * folder's name (`askText`); several, as chrome://settings/searchEngines asks
5
5
  * for a site search (`askFields`). The action button stays disabled while a
6
6
  * field is blank or isn't valid, with the reason under that field. Enter
7
- * means the action button, Esc and Cancel cancel, and the focus goes back to
8
- * what opened it. The page needs no markup: the dialog (`#text-dialog`) is
7
+ * means the action button, Esc and Cancel cancel (as does the dialog being
8
+ * closed any other way), and the focus goes back to what opened it. A reason
9
+ * is announced as it appears, and its field marked invalid. The page needs no markup: the dialog (`#text-dialog`) is
9
10
  * made the first time, its fields each time it's asked.
10
11
  *
11
12
  * @module ui/text-dialog
@@ -178,7 +179,7 @@ function fieldFor(doc, request, only) {
178
179
  input.value = typeof request.value === 'string' ? request.value : '';
179
180
  if (request.placeholder) input.placeholder = request.placeholder;
180
181
  if (request.maxLength !== undefined) input.maxLength = request.maxLength;
181
- const error = element(doc, 'span', { id: ids.error, class: 'hint error' });
182
+ const error = element(doc, 'span', { id: ids.error, class: 'hint error', 'aria-live': 'polite' });
182
183
  error.hidden = true;
183
184
  const hint = element(doc, 'span', { id: ids.hint, class: 'hint secondary' });
184
185
  hint.hidden = true;
@@ -226,6 +227,9 @@ export function askFields({
226
227
  const { reason } = waiting ? { reason: '' } : checkText(input.value, request.validate);
227
228
  error.textContent = reason;
228
229
  error.hidden = !reason;
230
+ // Said to a screen reader as it appears, and the field marked (the stylesheet turns its underline red).
231
+ if (reason) input.setAttribute('aria-invalid', 'true');
232
+ else input.removeAttribute('aria-invalid');
229
233
  hint.textContent = waiting ? request.waiting ?? '' : typeof request.hint === 'function' ? request.hint(input.value) : request.hint ?? '';
230
234
  hint.hidden = !hint.textContent;
231
235
  };
@@ -291,6 +295,9 @@ export function askFields({
291
295
  event.preventDefault();
292
296
  settle(null);
293
297
  }, { signal });
298
+ // Closed any other way (the browser's own close after a second Esc, another script): cancelled, not left unanswered.
299
+ // (The event comes a task after the closing: one from the ask before this, already answered, finds the dialog open again.)
300
+ parts.dialog.addEventListener('close', () => { if (!parts.dialog.open) settle(null); }, { signal });
294
301
 
295
302
  parts.dialog.showModal();
296
303
  // The first field still to fill in, or the first, its text selected to type over.
package/src/ui/toast.js CHANGED
@@ -93,6 +93,8 @@ export function createToast({ undoLabel = 'Undo', duration = TOAST_MS, undoDurat
93
93
  text.textContent = message;
94
94
  undoButton.hidden = !undo;
95
95
  undoLast = undo;
96
+ // Taken off the page since (a single-page site replaced its body): put back, or showing it would throw.
97
+ if (!toast.isConnected) (root?.isConnected === false ? doc.body : root ?? doc.body).append(toast);
96
98
  // Hidden and shown again, so it's the newest in the top layer (over a popover since).
97
99
  toast.togglePopover(false);
98
100
  toast.togglePopover(true);