@sapudex/web-components 2.51.0 → 2.52.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.
- package/cypress/specs/CountrySelector.cy.tsx +21 -0
- package/cypress/specs/List.cy.tsx +119 -10
- package/cypress/specs/SelectBox.cy.tsx +414 -70
- package/dist/CountrySelector.js +1 -0
- package/dist/CountrySelector.js.map +1 -1
- package/dist/List.d.ts +10 -0
- package/dist/List.js +35 -0
- package/dist/List.js.map +1 -1
- package/dist/ListGroupHeaderBase.js +3 -0
- package/dist/ListGroupHeaderBase.js.map +1 -1
- package/dist/ListItem.d.ts +1 -0
- package/dist/ListItem.js +11 -3
- package/dist/ListItem.js.map +1 -1
- package/dist/ListItemBase.d.ts +9 -0
- package/dist/ListItemBase.js +12 -0
- package/dist/ListItemBase.js.map +1 -1
- package/dist/ListSelectAll.js +1 -1
- package/dist/ListSelectAll.js.map +1 -1
- package/dist/SearchableListBase.d.ts +10 -0
- package/dist/SearchableListBase.js +29 -6
- package/dist/SearchableListBase.js.map +1 -1
- package/dist/SearchableListBaseTemplate.js +32 -5
- package/dist/SearchableListBaseTemplate.js.map +1 -1
- package/dist/SelectBase.d.ts +65 -1
- package/dist/SelectBase.js +446 -59
- package/dist/SelectBase.js.map +1 -1
- package/dist/SelectBox.js +24 -2
- package/dist/SelectBox.js.map +1 -1
- package/dist/SelectBoxTemplate.js +2 -2
- package/dist/SelectBoxTemplate.js.map +1 -1
- package/dist/assets/{InputSuggestions-7eP5S95I.js → InputSuggestions-0wafO-g3.js} +1 -1
- package/dist/assets/{ListItemStandardExpandableTextTemplate-CLcWBFcg.js → ListItemStandardExpandableTextTemplate-DoOYtqI8.js} +1 -1
- package/dist/assets/{bundle.esm-D4--IDh7.js → bundle.esm-DwHZ3nCW.js} +241 -241
- package/dist/assets/messagebundle_nl-BKUpVV-D.js +1 -0
- package/dist/css/themes/AccordionItem.css +1 -1
- package/dist/css/themes/Divider.css +1 -1
- package/dist/css/themes/Link.css +1 -1
- package/dist/css/themes/ListGroupHeader.css +1 -1
- package/dist/css/themes/ListItem.css +1 -1
- package/dist/css/themes/SelectBase.css +1 -1
- package/dist/css/themes/SelectBox.css +1 -1
- package/dist/generated/assets/i18n/messagebundle_nl.json +1 -1
- package/dist/generated/templates/ListTemplate.lit.js +1 -1
- package/dist/generated/templates/ListTemplate.lit.js.map +1 -1
- package/dist/generated/themes/AccordionItem.css.d.ts +1 -1
- package/dist/generated/themes/AccordionItem.css.js +1 -1
- package/dist/generated/themes/AccordionItem.css.js.map +1 -1
- package/dist/generated/themes/Divider.css.d.ts +1 -1
- package/dist/generated/themes/Divider.css.js +1 -1
- package/dist/generated/themes/Divider.css.js.map +1 -1
- package/dist/generated/themes/Link.css.d.ts +1 -1
- package/dist/generated/themes/Link.css.js +1 -1
- package/dist/generated/themes/Link.css.js.map +1 -1
- package/dist/generated/themes/ListGroupHeader.css.d.ts +1 -1
- package/dist/generated/themes/ListGroupHeader.css.js +1 -1
- package/dist/generated/themes/ListGroupHeader.css.js.map +1 -1
- package/dist/generated/themes/ListItem.css.d.ts +1 -1
- package/dist/generated/themes/ListItem.css.js +1 -1
- package/dist/generated/themes/ListItem.css.js.map +1 -1
- package/dist/generated/themes/SelectBase.css.d.ts +1 -1
- package/dist/generated/themes/SelectBase.css.js +1 -1
- package/dist/generated/themes/SelectBase.css.js.map +1 -1
- package/dist/generated/themes/SelectBox.css.d.ts +1 -1
- package/dist/generated/themes/SelectBox.css.js +1 -1
- package/dist/generated/themes/SelectBox.css.js.map +1 -1
- package/dist/messagebundle_nl.properties +126 -0
- package/dist/web-types.json +1 -1
- package/package.json +1 -1
- package/src/CountrySelector.ts +1 -0
- package/src/List.hbs +2 -1
- package/src/List.ts +36 -0
- package/src/ListGroupHeaderBase.ts +3 -0
- package/src/ListItem.ts +12 -3
- package/src/ListItemBase.ts +11 -0
- package/src/ListSelectAll.ts +1 -1
- package/src/SearchableListBase.ts +31 -8
- package/src/SearchableListBaseTemplate.tsx +38 -6
- package/src/SelectBase.ts +466 -69
- package/src/SelectBox.ts +27 -2
- package/src/SelectBoxTemplate.tsx +4 -3
- package/src/generated/templates/ListTemplate.lit.ts +1 -1
- package/src/generated/themes/AccordionItem.css.ts +1 -1
- package/src/generated/themes/Divider.css.ts +1 -1
- package/src/generated/themes/Link.css.ts +1 -1
- package/src/generated/themes/ListGroupHeader.css.ts +1 -1
- package/src/generated/themes/ListItem.css.ts +1 -1
- package/src/generated/themes/SelectBase.css.ts +1 -1
- package/src/generated/themes/SelectBox.css.ts +1 -1
- package/src/i18n/messagebundle_nl.properties +126 -0
- package/src/themes/AccordionItem.css +8 -3
- package/src/themes/Divider.css +2 -0
- package/src/themes/Link.css +20 -2
- package/src/themes/ListGroupHeader.css +18 -1
- package/src/themes/ListItem.css +2 -1
- package/src/themes/SelectBase.css +2 -1
- package/src/themes/SelectBox.css +10 -0
- package/dist/assets/messagebundle_nl-etaldyfo.js +0 -1
package/src/SelectBase.ts
CHANGED
|
@@ -3,16 +3,19 @@ import slot from "@ui5/webcomponents-base/dist/decorators/slot-strict.js";
|
|
|
3
3
|
import executeTemplate from "@ui5/webcomponents-base/dist/renderer/executeTemplate.js";
|
|
4
4
|
import type { ChangeInfo, Slot } from "@ui5/webcomponents-base/dist/UI5Element.js";
|
|
5
5
|
import {
|
|
6
|
-
isEscape, isDown, isUp, isHome, isEnd, isTabNext, isTabPrevious, isLeft, isRight,
|
|
6
|
+
isEscape, isDown, isUp, isHome, isEnd, isTabNext, isTabPrevious, isLeft, isRight, isEnter, isSpace,
|
|
7
7
|
} from "@ui5/webcomponents-base/dist/Keys.js";
|
|
8
8
|
import { getAllAccessibleNameRefTexts } from "@ui5/webcomponents-base/dist/util/AccessibilityTextsHelper.js";
|
|
9
|
+
import { cleanLabel } from "./utils/listItemLabelUtils.js";
|
|
9
10
|
import type { UDExListItemBase } from "./List.js";
|
|
10
11
|
import type Popover from "@ui5/webcomponents/dist/Popover.js";
|
|
11
12
|
import type UDExList from "./List.js";
|
|
12
13
|
import type UDExSearch from "./Search.js";
|
|
13
14
|
import "@ui5/webcomponents-icons/dist/decline.js";
|
|
14
15
|
import "@ui5/webcomponents-icons/dist/navigation-down-arrow.js";
|
|
15
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
COUNTRY_SELECT, STARS, SELECTED, UNSELECTED, INDETERMINATE, DISABLED,
|
|
18
|
+
} from "./generated/i18n/i18n-defaults.js";
|
|
16
19
|
import SearchableListBaseTemplate from "./SearchableListBaseTemplate.js";
|
|
17
20
|
import CountrySelectorTemplate from "./CountrySelectorTemplate.js";
|
|
18
21
|
import type { ResizeObserverCallback } from "@ui5/webcomponents-base/delegate/ResizeHandler.js";
|
|
@@ -20,10 +23,11 @@ import ResizeHandler from "@ui5/webcomponents-base/delegate/ResizeHandler.js";
|
|
|
20
23
|
import UDExSearchableListBase from "./SearchableListBase.js";
|
|
21
24
|
import UDExValueState from "./types/common.js";
|
|
22
25
|
import type { UDExSearchableListBaseEvents } from "./SearchableListBase.js";
|
|
23
|
-
import { getTokensCountText } from "@ui5/webcomponents/dist/Tokenizer.js";
|
|
26
|
+
import Tokenizer, { getTokensCountText } from "@ui5/webcomponents/dist/Tokenizer.js";
|
|
24
27
|
import isLetterKey from "./utils/isLetterKeyPressed.js";
|
|
25
28
|
import translate from "./utils/translate.js";
|
|
26
29
|
import type { AriaAutoComplete, AriaHasPopup } from "@ui5/webcomponents-base/dist/types.js";
|
|
30
|
+
import { renderFinished } from "@ui5/webcomponents-base/dist/Render.js";
|
|
27
31
|
|
|
28
32
|
type UDExSelectBaseEvents = {
|
|
29
33
|
"open": void,
|
|
@@ -164,6 +168,26 @@ class UDExSelectBase extends UDExSearchableListBase {
|
|
|
164
168
|
@property({ type: Boolean, noAttribute: true })
|
|
165
169
|
_requestRender = false;
|
|
166
170
|
|
|
171
|
+
/**
|
|
172
|
+
* True while keyboard list navigation is active (aria-activedescendant set).
|
|
173
|
+
* Used in CSS to suppress the input focus ring and show the active item ring.
|
|
174
|
+
*
|
|
175
|
+
* @private
|
|
176
|
+
*/
|
|
177
|
+
@property({ type: Boolean })
|
|
178
|
+
_navigating: boolean = false;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Text read by screen readers when the active item changes.
|
|
182
|
+
* Announced via an aria-live="assertive" region in the template.
|
|
183
|
+
* This works regardless of shadow DOM / popover teleportation because
|
|
184
|
+
* the live region lives inside SelectBox's own shadow root.
|
|
185
|
+
*
|
|
186
|
+
* @private
|
|
187
|
+
*/
|
|
188
|
+
@property({ type: String, noAttribute: true })
|
|
189
|
+
_liveRegionText: string = "";
|
|
190
|
+
|
|
167
191
|
/**
|
|
168
192
|
* Defines the footer content for the component dropdown.
|
|
169
193
|
* Place any content (e.g. an action button) here. Clicking footer content does not close the dropdown —
|
|
@@ -190,6 +214,7 @@ class UDExSelectBase extends UDExSearchableListBase {
|
|
|
190
214
|
_handleResizeBound: ResizeObserverCallback;
|
|
191
215
|
_handleListKeyDownBound: (e: KeyboardEvent) => void;
|
|
192
216
|
_listKeyDownListenerAdded: boolean = false;
|
|
217
|
+
_activeDescendantItem: UDExListItemBase | null = null;
|
|
193
218
|
|
|
194
219
|
constructor() {
|
|
195
220
|
super();
|
|
@@ -219,6 +244,7 @@ class UDExSelectBase extends UDExSearchableListBase {
|
|
|
219
244
|
}
|
|
220
245
|
|
|
221
246
|
onAfterRendering() {
|
|
247
|
+
super.onAfterRendering();
|
|
222
248
|
this._popover = this.getPopover();
|
|
223
249
|
this._search = this.getSearch();
|
|
224
250
|
this._list = this.getList();
|
|
@@ -227,6 +253,10 @@ class UDExSelectBase extends UDExSearchableListBase {
|
|
|
227
253
|
|
|
228
254
|
if (this._list) {
|
|
229
255
|
this._list._scrollIntoView = "Nearest";
|
|
256
|
+
// Set a stable HTML id on the udex-list host so aria-controls can reference it.
|
|
257
|
+
if (!this._list.id) {
|
|
258
|
+
this._list.id = `${this._id}-list`;
|
|
259
|
+
}
|
|
230
260
|
}
|
|
231
261
|
|
|
232
262
|
if (this._requestRender) {
|
|
@@ -243,6 +273,104 @@ class UDExSelectBase extends UDExSearchableListBase {
|
|
|
243
273
|
this.updatePopoverWidth();
|
|
244
274
|
}
|
|
245
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Given a candidate index, skips over a non-interactive non-collapsible header if needed.
|
|
278
|
+
* Returns the effective target index.
|
|
279
|
+
*/
|
|
280
|
+
_skipNonInteractive(items: UDExListItemBase[], raw: number, lastIndex: number, direction: "down" | "up"): number {
|
|
281
|
+
if (!items[raw]?.nonInteractive || items[raw].collapsible) {
|
|
282
|
+
return raw;
|
|
283
|
+
}
|
|
284
|
+
if (direction === "down") {
|
|
285
|
+
return raw + 1 > lastIndex ? 0 : raw + 1;
|
|
286
|
+
}
|
|
287
|
+
return raw === 0 ? lastIndex : raw - 1;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Sets or clears the aria-activedescendant on the combobox input and updates
|
|
292
|
+
* the aria-live region so JAWS/VoiceOver announce the item name and position.
|
|
293
|
+
* The live region is the primary announcement mechanism because aria-activedescendant
|
|
294
|
+
* across a UI5 Popover (which teleports content to document.body) is unreliable —
|
|
295
|
+
* screen readers cannot follow the ID reference across shadow root boundaries.
|
|
296
|
+
*/
|
|
297
|
+
_setActiveDescendant(item: UDExListItemBase | null, scroll = true): void {
|
|
298
|
+
this._activeDescendantItem = item;
|
|
299
|
+
|
|
300
|
+
// Query the SelectBox's OWN shadow DOM for all elements bearing the class —
|
|
301
|
+
// this reaches the rendered udex-list-group-header / udex-list-item elements
|
|
302
|
+
// regardless of whether they are the current or a previously stale instance.
|
|
303
|
+
this.shadowRoot?.querySelectorAll<HTMLElement>("._udex-active-descendant")
|
|
304
|
+
.forEach(el => {
|
|
305
|
+
el.classList.remove("_udex-active-descendant");
|
|
306
|
+
el.removeAttribute("_active-descendant");
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
if (!item) {
|
|
310
|
+
(this._search as HTMLElement).removeAttribute("aria-activedescendant");
|
|
311
|
+
this._liveRegionText = "";
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
item.classList.add("_udex-active-descendant");
|
|
316
|
+
item.setAttribute("_active-descendant", "");
|
|
317
|
+
(this._search as HTMLElement).setAttribute("aria-activedescendant", item._id);
|
|
318
|
+
|
|
319
|
+
// Update the live region with tree-aware position and state so JAWS/VoiceOver
|
|
320
|
+
// announce the active item regardless of whether the popover is teleported to document.body.
|
|
321
|
+
if (this._list) {
|
|
322
|
+
const label = cleanLabel(item.label ?? "");
|
|
323
|
+
const supporting = item.supportingText ? cleanLabel(item.supportingText) : "";
|
|
324
|
+
const stateParts = this._itemStateLabels(item);
|
|
325
|
+
const posText = this._treePositionText(item);
|
|
326
|
+
this._liveRegionText = [label, supporting, posText, ...stateParts].filter(Boolean).join(", ");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (scroll && this._list?._scrollIntoView) {
|
|
330
|
+
// Only scroll if the item is not already fully visible in the dropdown.
|
|
331
|
+
const popoverContent = this._popover?.shadowRoot?.querySelector<HTMLElement>("[part=content]");
|
|
332
|
+
if (popoverContent) {
|
|
333
|
+
const itemRect = item.getBoundingClientRect();
|
|
334
|
+
const containerRect = popoverContent.getBoundingClientRect();
|
|
335
|
+
const isVisible = itemRect.top >= containerRect.top && itemRect.bottom <= containerRect.bottom;
|
|
336
|
+
if (!isVisible) {
|
|
337
|
+
this._list.scrollToItem(item);
|
|
338
|
+
}
|
|
339
|
+
} else {
|
|
340
|
+
this._list.scrollToItem(item);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Activates the first navigable item as the aria-activedescendant without moving DOM focus.
|
|
347
|
+
*/
|
|
348
|
+
_activateFirstItem(): void {
|
|
349
|
+
const items = this._list?.getKeyboardNavigationItems();
|
|
350
|
+
if (!items?.length) { return; }
|
|
351
|
+
const firstIndex = (items[0]?.nonInteractive && !items[0]?.collapsible) ? 1 : 0;
|
|
352
|
+
this._list.setTabIndexByIndex(items, firstIndex);
|
|
353
|
+
this._setActiveDescendant(items[firstIndex], false);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Navigates to the item at the given index, updating aria-activedescendant and _selectedOption.
|
|
358
|
+
*/
|
|
359
|
+
_navigateToIndex(items: UDExListItemBase[], nextIndex: number): void {
|
|
360
|
+
const currentIndex = this._activeDescendantItem
|
|
361
|
+
? this._list.getCurrentIndex(items, this._activeDescendantItem)
|
|
362
|
+
: -1;
|
|
363
|
+
if (currentIndex >= 0) {
|
|
364
|
+
this._list.unsetTabIndexByIndex(items, currentIndex);
|
|
365
|
+
}
|
|
366
|
+
this._list.setTabIndexByIndex(items, nextIndex);
|
|
367
|
+
this._selectedOption = items[nextIndex].label;
|
|
368
|
+
if (this.showImageFlags) {
|
|
369
|
+
this.handleImagePosition(items, nextIndex);
|
|
370
|
+
}
|
|
371
|
+
this._setActiveDescendant(items[nextIndex]);
|
|
372
|
+
}
|
|
373
|
+
|
|
246
374
|
handleKeyDownDropdown(e: KeyboardEvent) {
|
|
247
375
|
const items = this._list?.getKeyboardNavigationItems();
|
|
248
376
|
|
|
@@ -301,78 +429,65 @@ class UDExSelectBase extends UDExSearchableListBase {
|
|
|
301
429
|
return;
|
|
302
430
|
}
|
|
303
431
|
|
|
304
|
-
if (isRight(e)) {
|
|
432
|
+
if (isRight(e) || isLeft(e)) {
|
|
305
433
|
e.preventDefault();
|
|
306
|
-
|
|
434
|
+
// Resolve the collapsible group header from the active descendant (not e.target,
|
|
435
|
+
// since DOM focus stays on the input and e.target is the input element).
|
|
436
|
+
const activeHeader = this._activeDescendantItem
|
|
437
|
+
? this._resolveCollapsibleGroupHeader(this._activeDescendantItem as unknown as HTMLElement)
|
|
438
|
+
: null;
|
|
439
|
+
if (activeHeader && this._handleGroupHeaderCollapse(activeHeader as unknown as HTMLElement, isLeft(e))) {
|
|
307
440
|
return;
|
|
308
441
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
e.preventDefault();
|
|
315
|
-
if (this._handleGroupHeaderCollapse(e.target as HTMLElement, true)) {
|
|
316
|
-
return;
|
|
442
|
+
// Fall through to icon-row navigation when focus is on an icon button
|
|
443
|
+
if (isRight(e)) {
|
|
444
|
+
this._handleArrowRightInRow(e.target as HTMLElement);
|
|
445
|
+
} else {
|
|
446
|
+
this._handleArrowLeftInRow(e.target as HTMLElement);
|
|
317
447
|
}
|
|
318
|
-
this._handleArrowLeftInRow(e.target as HTMLElement);
|
|
319
448
|
return;
|
|
320
449
|
}
|
|
321
450
|
|
|
322
|
-
|
|
323
|
-
const effectiveTarget = iconSlot
|
|
324
|
-
? (e.target as HTMLElement).parentElement as UDExListItemBase | undefined
|
|
325
|
-
: undefined;
|
|
326
|
-
|
|
327
|
-
this.handleListNavigationKeys(e, items, effectiveTarget);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
handleListNavigationKeys(e: KeyboardEvent, items: UDExListItemBase[], effectiveTarget?: UDExListItemBase) {
|
|
331
|
-
const currentSelectedIndex = this._list?.getCurrentIndex(items, effectiveTarget ?? e.target as UDExListItemBase);
|
|
332
|
-
const lastItemIndex = items.length - 1;
|
|
333
|
-
|
|
334
|
-
if (isDown(e)) {
|
|
451
|
+
if (isDown(e) || isUp(e)) {
|
|
335
452
|
e.preventDefault();
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
453
|
+
const lastIndex = items.length - 1;
|
|
454
|
+
const currentItem = this._activeDescendantItem;
|
|
455
|
+
const currentIndex = currentItem ? this._list.getCurrentIndex(items, currentItem) : -1;
|
|
456
|
+
|
|
457
|
+
let nextIndex: number;
|
|
458
|
+
if (isDown(e)) {
|
|
459
|
+
const raw = currentIndex + 1 > lastIndex ? 0 : currentIndex + 1;
|
|
460
|
+
nextIndex = this._skipNonInteractive(items, raw, lastIndex, "down");
|
|
461
|
+
} else {
|
|
462
|
+
const raw = currentIndex - 1 < 0 ? lastIndex : currentIndex - 1;
|
|
463
|
+
nextIndex = this._skipNonInteractive(items, raw, lastIndex, "up");
|
|
464
|
+
}
|
|
465
|
+
this._navigateToIndex(items, nextIndex);
|
|
345
466
|
return;
|
|
346
467
|
}
|
|
347
468
|
|
|
348
469
|
if (isHome(e)) {
|
|
349
470
|
e.preventDefault();
|
|
350
471
|
this.handleHomeKeyPress(items);
|
|
351
|
-
|
|
352
472
|
return;
|
|
353
473
|
}
|
|
354
474
|
|
|
355
475
|
if (isEnd(e)) {
|
|
356
476
|
e.preventDefault();
|
|
357
|
-
this.handleEndKeyPress(items,
|
|
477
|
+
this.handleEndKeyPress(items, items.length - 1);
|
|
358
478
|
}
|
|
479
|
+
|
|
480
|
+
// Icon-button slot: Up/Down while an icon button is focused (handled by capture listener).
|
|
481
|
+
// _handleListKeyDownCapture fires first via capture; nothing else needed here.
|
|
359
482
|
}
|
|
360
483
|
|
|
361
484
|
handleEndKeyPress(items: UDExListItemBase[], lastItemIndex: number) {
|
|
362
|
-
|
|
363
|
-
this.handleImagePosition(items, lastItemIndex);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
this._selectedOption = items[lastItemIndex].label;
|
|
485
|
+
this._navigateToIndex(items, lastItemIndex);
|
|
367
486
|
}
|
|
368
487
|
|
|
369
488
|
handleHomeKeyPress(items: UDExListItemBase[]) {
|
|
370
489
|
const firstIndex = (items[0]?.nonInteractive && !items[0]?.collapsible) ? 1 : 0;
|
|
371
|
-
this.
|
|
372
|
-
|
|
373
|
-
if (this.showImageFlags) {
|
|
374
|
-
this.handleImagePosition(items, firstIndex);
|
|
375
|
-
}
|
|
490
|
+
this._navigateToIndex(items, firstIndex);
|
|
376
491
|
}
|
|
377
492
|
|
|
378
493
|
handleDownKeyPress(items: UDExListItemBase[], currentSelectedIndex: number, lastItemIndex: number) {
|
|
@@ -430,6 +545,7 @@ class UDExSelectBase extends UDExSearchableListBase {
|
|
|
430
545
|
handleEscKeyPress(e: KeyboardEvent) {
|
|
431
546
|
e.preventDefault();
|
|
432
547
|
e.stopPropagation();
|
|
548
|
+
this._setActiveDescendant(null);
|
|
433
549
|
this.closeDropdown();
|
|
434
550
|
this._search.focus();
|
|
435
551
|
this._selectedOption = "";
|
|
@@ -442,9 +558,35 @@ class UDExSelectBase extends UDExSearchableListBase {
|
|
|
442
558
|
this.resetSearchState();
|
|
443
559
|
|
|
444
560
|
if (this.modeSingleSelect) {
|
|
561
|
+
// SingleSelect closes the dropdown — clear active descendant.
|
|
562
|
+
this._setActiveDescendant(null);
|
|
445
563
|
this._search.focus();
|
|
446
564
|
this.closeDropdown();
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// MultiSelect: ensure active descendant tracks the interacted item so there
|
|
569
|
+
// is only one ring. Find the rendered item in the navigation list by matching
|
|
570
|
+
// data-token-id (the source item's _id set by the template) since e.detail.item
|
|
571
|
+
// is the source element, not the rendered udex-list-item.
|
|
572
|
+
const tokenId = this.getDataTokenId(item);
|
|
573
|
+
const navItems = this._list?.getKeyboardNavigationItems();
|
|
574
|
+
const renderedItem = navItems?.find(
|
|
575
|
+
i => (i.getAttribute("data-token-id") ?? i._id) === tokenId,
|
|
576
|
+
);
|
|
577
|
+
if (renderedItem && renderedItem !== this._activeDescendantItem) {
|
|
578
|
+
this._setActiveDescendant(renderedItem);
|
|
447
579
|
}
|
|
580
|
+
|
|
581
|
+
// Return DOM focus to the input so the clicked item loses its :focus ring.
|
|
582
|
+
// Without this, the browser keeps focus on the item (from the click) and a
|
|
583
|
+
// second ring appears when arrow keys move _activeDescendantItem elsewhere.
|
|
584
|
+
(this._search as HTMLElement).focus();
|
|
585
|
+
|
|
586
|
+
// Re-announce with the updated checked/unchecked state. The rendered item's
|
|
587
|
+
// `selected` prop is updated on the next render cycle (template re-render),
|
|
588
|
+
// so we wait for it before re-reading state labels.
|
|
589
|
+
renderFinished().then(() => this._announceCurrentOrFirst()).catch(() => {});
|
|
448
590
|
}
|
|
449
591
|
|
|
450
592
|
onKeyDownSearch(e: KeyboardEvent) {
|
|
@@ -457,13 +599,37 @@ class UDExSelectBase extends UDExSearchableListBase {
|
|
|
457
599
|
const isEscAndPopoverOpened = isEscape(e) && popoverOpened;
|
|
458
600
|
const isDownAndPopoverClosed = isDown(e) && !popoverOpened;
|
|
459
601
|
const isDownAndPopoverOpened = isDown(e) && popoverOpened;
|
|
602
|
+
const isUpAndPopoverOpened = isUp(e) && popoverOpened;
|
|
603
|
+
|
|
604
|
+
// Tab while popup is open: move focus to the first footer element if one exists,
|
|
605
|
+
// otherwise close the dropdown and move past the component.
|
|
606
|
+
if (popoverOpened && isTabNext(e)) {
|
|
607
|
+
e.preventDefault();
|
|
608
|
+
if (this.hasFooter) {
|
|
609
|
+
this._setActiveDescendant(null);
|
|
610
|
+
(this._firstFocusableIn(this.footer[0]) ?? this.footer[0])?.focus();
|
|
611
|
+
} else {
|
|
612
|
+
this.closeDropdown();
|
|
613
|
+
this._moveFocusAfterComponent();
|
|
614
|
+
}
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Shift+Tab while popup is open: close the dropdown.
|
|
619
|
+
// DOM focus stays on the search input throughout (our aria-activedescendant model
|
|
620
|
+
// never moves focus away from the input), so no explicit re-focus is needed.
|
|
621
|
+
if (popoverOpened && isTabPrevious(e)) {
|
|
622
|
+
e.preventDefault();
|
|
623
|
+
this._setActiveDescendant(null);
|
|
624
|
+
this.closeDropdown();
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
460
627
|
|
|
461
628
|
if (!hasItems) {
|
|
462
629
|
return;
|
|
463
630
|
}
|
|
464
631
|
|
|
465
632
|
const firstIndex = (items[0]?.nonInteractive && !items[0]?.collapsible) ? 1 : 0;
|
|
466
|
-
const firstItem = items[firstIndex];
|
|
467
633
|
|
|
468
634
|
if (isEscAndPopoverOpened) {
|
|
469
635
|
return this.closeDropdown();
|
|
@@ -472,21 +638,104 @@ class UDExSelectBase extends UDExSearchableListBase {
|
|
|
472
638
|
if (isDownAndPopoverClosed) {
|
|
473
639
|
e.preventDefault();
|
|
474
640
|
this.openDropdown();
|
|
475
|
-
this.
|
|
476
|
-
this._selectedOption = firstItem.label;
|
|
641
|
+
this._activateFirstItem();
|
|
477
642
|
if (this.showImageFlags) {
|
|
478
|
-
this.handleImagePosition(items,
|
|
643
|
+
this.handleImagePosition(items, firstIndex);
|
|
479
644
|
}
|
|
480
645
|
return;
|
|
481
646
|
}
|
|
482
647
|
|
|
483
648
|
if (isDownAndPopoverOpened) {
|
|
484
649
|
e.preventDefault();
|
|
485
|
-
if (this.
|
|
486
|
-
|
|
650
|
+
if (this._activeDescendantItem) {
|
|
651
|
+
const lastIndex = items.length - 1;
|
|
652
|
+
const currentIndex = this._list.getCurrentIndex(items, this._activeDescendantItem);
|
|
653
|
+
const raw = currentIndex + 1 > lastIndex ? 0 : currentIndex + 1;
|
|
654
|
+
const nextIndex = this._skipNonInteractive(items, raw, lastIndex, "down");
|
|
655
|
+
this._navigateToIndex(items, nextIndex);
|
|
656
|
+
} else {
|
|
657
|
+
this._activateFirstItem();
|
|
658
|
+
// Also update _selectedOption so the input reflects the active item.
|
|
659
|
+
const activeIdx = (items[0]?.nonInteractive && !items[0]?.collapsible) ? 1 : 0;
|
|
660
|
+
this._selectedOption = items[activeIdx].label;
|
|
661
|
+
if (this.showImageFlags) {
|
|
662
|
+
this.handleImagePosition(items, activeIdx);
|
|
663
|
+
}
|
|
487
664
|
}
|
|
488
|
-
|
|
489
|
-
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (isUpAndPopoverOpened) {
|
|
669
|
+
e.preventDefault();
|
|
670
|
+
if (this._activeDescendantItem) {
|
|
671
|
+
const lastIndex = items.length - 1;
|
|
672
|
+
const currentIndex = this._list.getCurrentIndex(items, this._activeDescendantItem);
|
|
673
|
+
const raw = currentIndex - 1 < 0 ? lastIndex : currentIndex - 1;
|
|
674
|
+
const nextIndex = this._skipNonInteractive(items, raw, lastIndex, "up");
|
|
675
|
+
this._navigateToIndex(items, nextIndex);
|
|
676
|
+
} else {
|
|
677
|
+
this._navigateToIndex(items, items.length - 1);
|
|
678
|
+
}
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (popoverOpened && isHome(e)) {
|
|
683
|
+
e.preventDefault();
|
|
684
|
+
const homeIndex = (items[0]?.nonInteractive && !items[0]?.collapsible) ? 1 : 0;
|
|
685
|
+
this._navigateToIndex(items, homeIndex);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (popoverOpened && isEnd(e)) {
|
|
690
|
+
e.preventDefault();
|
|
691
|
+
this._navigateToIndex(items, items.length - 1);
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Enter/Space: select the active descendant item (or toggle for MultiSelect).
|
|
696
|
+
if (popoverOpened && this._activeDescendantItem && (isEnter(e) || isSpace(e))) {
|
|
697
|
+
e.preventDefault();
|
|
698
|
+
const activeItem = this._activeDescendantItem;
|
|
699
|
+
if (activeItem.isGroup) {
|
|
700
|
+
// Group header: fire header-press (same event the click handler fires) so
|
|
701
|
+
// handleHeaderPress → onHeaderPress → handleGroupHeaderSelection runs.
|
|
702
|
+
activeItem.fireDecoratorEvent("header-press", { item: activeItem });
|
|
703
|
+
} else {
|
|
704
|
+
// Leaf item: fire item-press so the list handles selection consistently.
|
|
705
|
+
// Do NOT toggle .selected directly — handleItemClick (called via onItemPress)
|
|
706
|
+
// handles the toggle, and double-toggling would be a no-op.
|
|
707
|
+
activeItem.fireDecoratorEvent("item-press", {
|
|
708
|
+
selected: activeItem.selected,
|
|
709
|
+
item: activeItem,
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Left/Right: collapse/expand the active group header when popup is open.
|
|
716
|
+
if (popoverOpened && (isLeft(e) || isRight(e))) {
|
|
717
|
+
e.preventDefault();
|
|
718
|
+
const activeHeader = this._activeDescendantItem
|
|
719
|
+
? this._resolveCollapsibleGroupHeader(this._activeDescendantItem as unknown as HTMLElement)
|
|
720
|
+
: null;
|
|
721
|
+
if (activeHeader) {
|
|
722
|
+
this._handleGroupHeaderCollapse(activeHeader as unknown as HTMLElement, isLeft(e));
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
// Fall through to icon-row navigation when the active item has icon slots.
|
|
726
|
+
if (this._activeDescendantItem) {
|
|
727
|
+
if (isRight(e)) {
|
|
728
|
+
this._handleArrowRightInRow(this._activeDescendantItem as unknown as HTMLElement);
|
|
729
|
+
} else {
|
|
730
|
+
this._handleArrowLeftInRow(this._activeDescendantItem as unknown as HTMLElement);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Sync _selectedOption display for the first item (used when popover just opened via click).
|
|
737
|
+
if (!this._activeDescendantItem && hasItems) {
|
|
738
|
+
this._selectedOption = items[firstIndex].label;
|
|
490
739
|
}
|
|
491
740
|
}
|
|
492
741
|
|
|
@@ -505,6 +754,15 @@ class UDExSelectBase extends UDExSearchableListBase {
|
|
|
505
754
|
}
|
|
506
755
|
}
|
|
507
756
|
|
|
757
|
+
override onSearch(value: string): void {
|
|
758
|
+
super.onSearch(value);
|
|
759
|
+
// Route search result count into the shared live region so SelectBox
|
|
760
|
+
// only needs one aria-live span (no separate polite span in the template).
|
|
761
|
+
if (this._accSearchResultInfo) {
|
|
762
|
+
this._liveRegionText = this._accSearchResultInfo;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
508
766
|
handleDirection() {
|
|
509
767
|
const directionStyles = window.getComputedStyle(this).getPropertyValue("direction");
|
|
510
768
|
this._direction = directionStyles || "ltr";
|
|
@@ -523,12 +781,83 @@ class UDExSelectBase extends UDExSearchableListBase {
|
|
|
523
781
|
}
|
|
524
782
|
|
|
525
783
|
handleDropdownOpen() {
|
|
526
|
-
if
|
|
784
|
+
// Scroll to the previously selected/active item if there is one,
|
|
785
|
+
// otherwise scroll to top. Do NOT pre-activate any item — that is done
|
|
786
|
+
// by the first ArrowDown so that ArrowDown[1]=item[0], ArrowDown[2]=item[1].
|
|
787
|
+
if (this.activeItem) {
|
|
788
|
+
const items = this._list?.getKeyboardNavigationItems();
|
|
789
|
+
const active = items?.find(i => !i.isGroup && (i as any).selected);
|
|
790
|
+
if (active && this._list?._scrollIntoView) {
|
|
791
|
+
this._list.scrollToItem(active);
|
|
792
|
+
} else {
|
|
793
|
+
this._dropdownScrollToTop();
|
|
794
|
+
}
|
|
795
|
+
} else {
|
|
527
796
|
this._dropdownScrollToTop();
|
|
528
797
|
}
|
|
798
|
+
|
|
799
|
+
// Announce the visually focused item (fake focus outline on first item).
|
|
800
|
+
// Using polite politeness — it waits for the AT to finish announcing
|
|
801
|
+
// "expanded" before reading the item, so no timeout is needed.
|
|
802
|
+
// Clear first so the mutation fires even if the text is unchanged from
|
|
803
|
+
// the previous open, then populate after render completes.
|
|
804
|
+
this._liveRegionText = "";
|
|
805
|
+
renderFinished().then(() => this._announceCurrentOrFirst()).catch(() => {});
|
|
806
|
+
|
|
529
807
|
this.fireEventDropdownOpen();
|
|
530
808
|
}
|
|
531
809
|
|
|
810
|
+
/**
|
|
811
|
+
* Announces the currently active descendant item (if set), or the first
|
|
812
|
+
* navigable item as a fallback. Used on open and after state changes.
|
|
813
|
+
*/
|
|
814
|
+
_announceCurrentOrFirst(): void {
|
|
815
|
+
const items = this._list?.getKeyboardNavigationItems();
|
|
816
|
+
if (!items?.length) { return; }
|
|
817
|
+
const item = this._activeDescendantItem ?? items.find(i => !i.nonInteractive || i.collapsible) ?? items[0];
|
|
818
|
+
if (!item) { return; }
|
|
819
|
+
const label = cleanLabel(item.label ?? "");
|
|
820
|
+
const stateParts = this._itemStateLabels(item);
|
|
821
|
+
const posText = this._treePositionText(item);
|
|
822
|
+
this._liveRegionText = [label, posText, ...stateParts].filter(Boolean).join(", ");
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* Returns the positional announcement text for an item, tree-aware when the
|
|
827
|
+
* list has groups. Matches the JAWS reading pattern of FilterMenu:
|
|
828
|
+
* group header → "level 0, X of Y" (Y = total navigable groups)
|
|
829
|
+
* group child → "level 1, X of Y" (Y = siblings in that group)
|
|
830
|
+
* flat item → "X of Y" (Y = total items, no level prefix)
|
|
831
|
+
*/
|
|
832
|
+
_treePositionText(item: UDExListItemBase): string {
|
|
833
|
+
if (!this.isTreeStructure) {
|
|
834
|
+
const items = this._list?.getKeyboardNavigationItems() ?? [];
|
|
835
|
+
const pos = items.indexOf(item);
|
|
836
|
+
return pos >= 0 ? `${pos + 1} of ${items.length}` : "";
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
if (item.isGroup) {
|
|
840
|
+
// Position among navigable group headers.
|
|
841
|
+
const groups = (this._list?.getKeyboardNavigationItems() ?? []).filter(i => i.isGroup);
|
|
842
|
+
const pos = groups.indexOf(item);
|
|
843
|
+
return pos >= 0 ? `level 0, ${pos + 1} of ${groups.length}` : "level 0";
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// Child item — find parent group and count siblings visible in navigation.
|
|
847
|
+
const parentGroup = item.parentElement as UDExListItemBase | null;
|
|
848
|
+
if (parentGroup?.isGroup) {
|
|
849
|
+
const navItems = this._list?.getKeyboardNavigationItems() ?? [];
|
|
850
|
+
const siblings = navItems.filter(i => !i.isGroup && i.parentElement === parentGroup);
|
|
851
|
+
const pos = siblings.indexOf(item);
|
|
852
|
+
return pos >= 0 ? `level 1, ${pos + 1} of ${siblings.length}` : "level 1";
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// Fallback: flat position.
|
|
856
|
+
const all = this._list?.getKeyboardNavigationItems() ?? [];
|
|
857
|
+
const pos = all.indexOf(item);
|
|
858
|
+
return pos >= 0 ? `${pos + 1} of ${all.length}` : "";
|
|
859
|
+
}
|
|
860
|
+
|
|
532
861
|
fireEventDropdownBeforeOClose() {
|
|
533
862
|
this.fireDecoratorEvent("before-close");
|
|
534
863
|
}
|
|
@@ -568,12 +897,15 @@ class UDExSelectBase extends UDExSearchableListBase {
|
|
|
568
897
|
this._popover.opener = this._trigger;
|
|
569
898
|
this._popover.open = true;
|
|
570
899
|
this._open = true;
|
|
900
|
+
this._navigating = true;
|
|
901
|
+
this._focused = false;
|
|
571
902
|
}
|
|
572
903
|
|
|
573
904
|
closeDropdown() {
|
|
574
905
|
if (this._popover.open) {
|
|
575
906
|
this._open = false;
|
|
576
907
|
this._popover.open = false;
|
|
908
|
+
this._navigating = false;
|
|
577
909
|
}
|
|
578
910
|
}
|
|
579
911
|
|
|
@@ -643,11 +975,13 @@ class UDExSelectBase extends UDExSearchableListBase {
|
|
|
643
975
|
if (collapse && !header.collapsed) {
|
|
644
976
|
header.collapsed = true;
|
|
645
977
|
this._list.setCollapsedItems(true, header);
|
|
978
|
+
renderFinished().then(() => this._announceCurrentOrFirst()).catch(() => {});
|
|
646
979
|
return true;
|
|
647
980
|
}
|
|
648
981
|
if (!collapse && header.collapsed) {
|
|
649
982
|
header.collapsed = false;
|
|
650
983
|
this._list.setCollapsedItems(false, header);
|
|
984
|
+
renderFinished().then(() => this._announceCurrentOrFirst()).catch(() => {});
|
|
651
985
|
return true;
|
|
652
986
|
}
|
|
653
987
|
// Target is a collapsible header but already in the requested state — still consume
|
|
@@ -676,7 +1010,12 @@ class UDExSelectBase extends UDExSearchableListBase {
|
|
|
676
1010
|
const iconSlot = this._getIconButtonSlot(target);
|
|
677
1011
|
if (!iconSlot) {
|
|
678
1012
|
const firstIcon = target.querySelector<HTMLElement>("[slot=\"firstIcon\"]");
|
|
679
|
-
firstIcon
|
|
1013
|
+
if (firstIcon) {
|
|
1014
|
+
// Entering icon row: clear the active descendant ring so the list item
|
|
1015
|
+
// doesn't show two simultaneous outlines alongside the icon's :focus ring.
|
|
1016
|
+
this._setActiveDescendant(null);
|
|
1017
|
+
firstIcon.focus();
|
|
1018
|
+
}
|
|
680
1019
|
return;
|
|
681
1020
|
}
|
|
682
1021
|
if (iconSlot === "firstIcon") {
|
|
@@ -697,7 +1036,17 @@ class UDExSelectBase extends UDExSearchableListBase {
|
|
|
697
1036
|
const firstIcon = parentItem.querySelector<HTMLElement>("[slot=\"firstIcon\"]");
|
|
698
1037
|
firstIcon?.focus();
|
|
699
1038
|
} else if (iconSlot === "firstIcon") {
|
|
700
|
-
|
|
1039
|
+
// Return to the list item: restore the active descendant ring and return
|
|
1040
|
+
// DOM focus to the input so the icon's :focus ring is cleared.
|
|
1041
|
+
const navItems = this._list?.getKeyboardNavigationItems();
|
|
1042
|
+
const tokenId = this.getDataTokenId(parentItem as UDExListItemBase);
|
|
1043
|
+
const renderedItem = navItems?.find(i => (i.getAttribute("data-token-id") ?? i._id) === tokenId);
|
|
1044
|
+
if (renderedItem) {
|
|
1045
|
+
this._setActiveDescendant(renderedItem);
|
|
1046
|
+
} else {
|
|
1047
|
+
this._list.focusItem(parentItem as UDExListItemBase);
|
|
1048
|
+
}
|
|
1049
|
+
(this._search as HTMLElement).focus();
|
|
701
1050
|
}
|
|
702
1051
|
}
|
|
703
1052
|
|
|
@@ -735,25 +1084,22 @@ class UDExSelectBase extends UDExSearchableListBase {
|
|
|
735
1084
|
|
|
736
1085
|
this._list.unsetTabIndexByIndex(items, currentIndex);
|
|
737
1086
|
this._list.setTabIndexByIndex(items, targetIndex);
|
|
738
|
-
this.
|
|
1087
|
+
this._setActiveDescendant(items[targetIndex]);
|
|
739
1088
|
this._selectedOption = items[targetIndex].label;
|
|
740
1089
|
}
|
|
741
1090
|
|
|
742
1091
|
_focusLastListItem() {
|
|
743
1092
|
const items = this._list?.getKeyboardNavigationItems();
|
|
744
1093
|
if (!items || items.length === 0) {
|
|
745
|
-
this._search.focus();
|
|
1094
|
+
(this._search as HTMLElement).focus();
|
|
746
1095
|
return;
|
|
747
1096
|
}
|
|
748
1097
|
const lastIndex = items.length - 1;
|
|
749
|
-
const lastItem = items[lastIndex];
|
|
750
1098
|
this._list.setTabIndexByIndex(items, lastIndex);
|
|
751
|
-
this._selectedOption =
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
this._list.focusItem(lastItem);
|
|
756
|
-
}
|
|
1099
|
+
this._selectedOption = items[lastIndex].label;
|
|
1100
|
+
this._setActiveDescendant(items[lastIndex]);
|
|
1101
|
+
// Return DOM focus to the search input (aria-activedescendant model).
|
|
1102
|
+
(this._search as HTMLElement).focus();
|
|
757
1103
|
}
|
|
758
1104
|
|
|
759
1105
|
_firstFocusableIn(el: Element | undefined): HTMLElement | null {
|
|
@@ -786,11 +1132,50 @@ class UDExSelectBase extends UDExSearchableListBase {
|
|
|
786
1132
|
findAndFocus(this);
|
|
787
1133
|
}
|
|
788
1134
|
|
|
1135
|
+
/**
|
|
1136
|
+
* Builds the state portion of the live region announcement for a navigation item.
|
|
1137
|
+
* Returns an array of state strings (e.g. ["collapsed", "Checked"]) to be joined
|
|
1138
|
+
* with the label and position. Handles:
|
|
1139
|
+
* - collapsible group headers: expanded / collapsed
|
|
1140
|
+
* - MultiSelect: Checked / Unchecked / Indeterminate
|
|
1141
|
+
* - SingleSelect (tree): Selected / Unselected
|
|
1142
|
+
* - disabled: Disabled
|
|
1143
|
+
*/
|
|
1144
|
+
_itemStateLabels(item: UDExListItemBase): string[] {
|
|
1145
|
+
const parts: string[] = [];
|
|
1146
|
+
|
|
1147
|
+
if (item.collapsible) {
|
|
1148
|
+
// Use plain English — matches what AT announce for aria-expanded.
|
|
1149
|
+
parts.push(item.expanded ? "expanded" : "collapsed");
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
if (item.disabled) {
|
|
1153
|
+
parts.push(translate(UDExSearchableListBase, DISABLED));
|
|
1154
|
+
} else if (item.modeMultiSelect || (item.isGroup && this.modeMultiSelect)) {
|
|
1155
|
+
if (item.indeterminate) {
|
|
1156
|
+
parts.push(translate(UDExSearchableListBase, INDETERMINATE));
|
|
1157
|
+
} else {
|
|
1158
|
+
parts.push(translate(UDExSearchableListBase, item.selected ? SELECTED : UNSELECTED));
|
|
1159
|
+
}
|
|
1160
|
+
} else if (this.isTreeStructure && item.selected) {
|
|
1161
|
+
// SingleSelect tree: only announce "Selected" when the item is actually selected.
|
|
1162
|
+
parts.push(translate(UDExSearchableListBase, SELECTED));
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
return parts;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
789
1168
|
get ariaDescribedbyText(): string {
|
|
790
1169
|
if (this.modeSingleSelect) {
|
|
791
|
-
return this._selectedOption;
|
|
1170
|
+
return this._navigating ? "" : this._selectedOption;
|
|
792
1171
|
}
|
|
793
1172
|
const tags = this.selectedItems.filter(item => !item.isGroup);
|
|
1173
|
+
// Tokenizer.i18nBundle may not be loaded yet on direct page load.
|
|
1174
|
+
// Return empty string until the bundle is available; the component re-renders
|
|
1175
|
+
// once i18n is ready and will pick up the correct text.
|
|
1176
|
+
if (!Tokenizer.i18nBundle) {
|
|
1177
|
+
return "";
|
|
1178
|
+
}
|
|
794
1179
|
return getTokensCountText(tags.length);
|
|
795
1180
|
}
|
|
796
1181
|
|
|
@@ -840,6 +1225,18 @@ class UDExSelectBase extends UDExSearchableListBase {
|
|
|
840
1225
|
return this._listBoxRole;
|
|
841
1226
|
}
|
|
842
1227
|
|
|
1228
|
+
get listAriaControlsId(): string {
|
|
1229
|
+
return this._list?.id || this.popoverId;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
override get _activeDescendantMode(): boolean {
|
|
1233
|
+
return true;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
override get isTreeStructure(): boolean {
|
|
1237
|
+
return this._filteredItems.some(e => e.isGroup);
|
|
1238
|
+
}
|
|
1239
|
+
|
|
843
1240
|
get hideDropdownSearch(): boolean {
|
|
844
1241
|
return true;
|
|
845
1242
|
}
|