@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.
|
@@ -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
|
-
|
|
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
|
-
//
|
|
208
|
-
//
|
|
209
|
-
|
|
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
|
|
216
|
-
|
|
217
|
-
|
|
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
|
>
|