@sveltia/ui 0.78.0 → 0.78.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.
@@ -1,4 +1,4 @@
1
1
  /**
2
2
  * Version of this package, used to resolve the prebuilt Shiki engine chunk from a CDN.
3
3
  */
4
- export const UI_VERSION: "0.78.0";
4
+ export const UI_VERSION: "0.78.1";
@@ -3,4 +3,4 @@
3
3
  /**
4
4
  * Version of this package, used to resolve the prebuilt Shiki engine chunk from a CDN.
5
5
  */
6
- export const UI_VERSION = '0.78.0';
6
+ export const UI_VERSION = '0.78.1';
@@ -4,7 +4,7 @@
4
4
  -->
5
5
  <script>
6
6
  import { sleep } from '@sveltia/utils/misc';
7
- import { onMount } from 'svelte';
7
+ import { onMount, tick } from 'svelte';
8
8
  import { activatePopup } from '../../services/popup.svelte.js';
9
9
  import Modal from './modal.svelte';
10
10
 
@@ -81,6 +81,36 @@
81
81
  let popupInstance = $state();
82
82
  let hoveredTimeout = 0;
83
83
 
84
+ /**
85
+ * Move the focus into the content. A composite widget keeps exactly one of its items in the tab
86
+ * order and takes its own root out of it, so the item is what should receive focus. Falling back
87
+ * to any focusable element covers the popups that hold plain content.
88
+ * @param {object} [options] Options.
89
+ * @param {boolean} [options.tabStopOnly] Whether to only focus an element in the tab order,
90
+ * without falling back to any other element.
91
+ * @returns {boolean} Whether an element has been focused.
92
+ */
93
+ const focusContent = ({ tabStopOnly = false } = {}) => {
94
+ /* v8 ignore next 3 -- the callers check the content first */
95
+ if (!content) {
96
+ return false;
97
+ }
98
+
99
+ const target = /** @type {HTMLElement | null} */ (
100
+ content.querySelector('[tabindex="0"]:not([aria-disabled="true"])') ??
101
+ (tabStopOnly ? null : content.querySelector('[tabindex]:not([aria-disabled="true"])'))
102
+ );
103
+
104
+ if (target) {
105
+ target.focus();
106
+ } else if (!tabStopOnly) {
107
+ content.tabIndex = -1;
108
+ content.focus();
109
+ }
110
+
111
+ return !!target;
112
+ };
113
+
84
114
  // Keep the `open` prop and the instance in sync both ways. The instance opens the popup when
85
115
  // the anchor is activated, and closes it when the backdrop or an item is clicked, or Escape is
86
116
  // pressed; the prop is what a consumer such as `<Combobox>` or `<MenuItem>` toggles from its own
@@ -198,25 +228,22 @@
198
228
  onOpen={async (event) => {
199
229
  onOpen?.(event);
200
230
 
201
- await sleep(100);
231
+ // Move the focus into the content as soon as it’s rendered, if it holds a control that’s
232
+ // already in the tab order, like the search box of a dropdown list: a key typed right after
233
+ // the popup opens would otherwise be lost
234
+ await tick();
202
235
 
203
- if (!content) {
236
+ if (!content || focusContent({ tabStopOnly: true })) {
204
237
  return;
205
238
  }
206
239
 
207
- // A composite widget keeps exactly one of its items in the tab order and takes its own root
208
- // out of it, so the item is what should receive focus. Falling back to any focusable element
209
- // covers the popups that hold plain content.
210
- const target = /** @type {HTMLElement} */ (
211
- content.querySelector('[tabindex="0"]:not([aria-disabled="true"])') ??
212
- content.querySelector('[tabindex]:not([aria-disabled="true"])')
213
- );
240
+ // Otherwise wait for a composite widget, such as a menu, to put one of its items in the tab
241
+ // order, which it does a moment later
242
+ await sleep(100);
214
243
 
215
- if (target) {
216
- target.focus();
217
- } else {
218
- content.tabIndex = -1;
219
- content.focus();
244
+ // Leave the focus alone if the user has already moved it within the content
245
+ if (content && !content.contains(document.activeElement)) {
246
+ focusContent();
220
247
  }
221
248
  }}
222
249
  >
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sveltia/ui",
3
- "version": "0.78.0",
3
+ "version": "0.78.1",
4
4
  "description": "A collection of Svelte components and utilities for building user interfaces.",
5
5
  "repository": {
6
6
  "type": "git",