@api-client/ui 0.5.52 → 0.5.54
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/build/src/md/menu/internal/Menu.d.ts.map +1 -1
- package/build/src/md/menu/internal/Menu.js +16 -5
- package/build/src/md/menu/internal/Menu.js.map +1 -1
- package/build/src/md/menu/internal/Menu.styles.d.ts.map +1 -1
- package/build/src/md/menu/internal/Menu.styles.js +5 -1
- package/build/src/md/menu/internal/Menu.styles.js.map +1 -1
- package/build/src/md/menu/internal/SubMenu.styles.d.ts.map +1 -1
- package/build/src/md/menu/internal/SubMenu.styles.js +22 -1
- package/build/src/md/menu/internal/SubMenu.styles.js.map +1 -1
- package/build/src/md/select/internals/Select.d.ts +62 -7
- package/build/src/md/select/internals/Select.d.ts.map +1 -1
- package/build/src/md/select/internals/Select.js +248 -32
- package/build/src/md/select/internals/Select.js.map +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/md/menu/internal/Menu.styles.ts +5 -1
- package/src/md/menu/internal/Menu.ts +17 -6
- package/src/md/menu/internal/SubMenu.styles.ts +22 -1
- package/src/md/select/internals/Select.ts +283 -26
package/package.json
CHANGED
|
@@ -9,7 +9,6 @@ export default css`
|
|
|
9
9
|
margin: 0;
|
|
10
10
|
padding: 0;
|
|
11
11
|
border: none;
|
|
12
|
-
overflow: hidden;
|
|
13
12
|
/* in most cases the max-height won't matter as this assumes the whole screen to be available, which is rarely the truth. */
|
|
14
13
|
max-height: 90vh;
|
|
15
14
|
overflow: auto;
|
|
@@ -27,6 +26,11 @@ export default css`
|
|
|
27
26
|
position-area: top span-left;
|
|
28
27
|
}
|
|
29
28
|
|
|
29
|
+
/* Special class set on the element to render the menu to take measurements */
|
|
30
|
+
:host(.measurements) {
|
|
31
|
+
display: block;
|
|
32
|
+
}
|
|
33
|
+
|
|
30
34
|
:host(:popover-open) {
|
|
31
35
|
display: block;
|
|
32
36
|
background-color: var(--md-sys-color-surface-container);
|
|
@@ -74,12 +74,14 @@ export default class Menu extends UiList {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
override togglePopover(force?: boolean): boolean {
|
|
77
|
+
if (!this.open && !this.disabled) {
|
|
78
|
+
this.positionMenu()
|
|
79
|
+
}
|
|
77
80
|
this.open = !this.open
|
|
78
81
|
this.ariaExpanded = String(this.open)
|
|
79
82
|
this.tabIndex = this.open ? 0 : -1
|
|
80
83
|
const result = super.togglePopover(force)
|
|
81
84
|
if (this.open) {
|
|
82
|
-
this.positionMenu()
|
|
83
85
|
this.focus()
|
|
84
86
|
}
|
|
85
87
|
return result
|
|
@@ -106,9 +108,9 @@ export default class Menu extends UiList {
|
|
|
106
108
|
override showPopover(): void {
|
|
107
109
|
this.tabIndex = 0 // Make menu focusable
|
|
108
110
|
this.ariaExpanded = 'true'
|
|
111
|
+
this.positionMenu()
|
|
109
112
|
super.showPopover()
|
|
110
113
|
this.open = true
|
|
111
|
-
this.positionMenu()
|
|
112
114
|
this.focus()
|
|
113
115
|
this.dispatchEvent(new CustomEvent('open', { bubbles: false, composed: true }))
|
|
114
116
|
}
|
|
@@ -126,15 +128,20 @@ export default class Menu extends UiList {
|
|
|
126
128
|
}
|
|
127
129
|
|
|
128
130
|
positionMenu(): void {
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
|
|
131
|
+
// for the frame, make the element visible (without animations)
|
|
132
|
+
// to take measurements correctly.
|
|
133
|
+
this.classList.add('measurements')
|
|
132
134
|
|
|
133
135
|
// Reset any previous manual positioning to let CSS anchor positioning work
|
|
134
136
|
this.style.removeProperty('position-area')
|
|
135
137
|
this.style.removeProperty('max-height')
|
|
136
138
|
this.style.removeProperty('max-width')
|
|
137
139
|
|
|
140
|
+
// Let CSS anchor positioning handle the positioning automatically
|
|
141
|
+
// Only intervene if we need to set max-height for overflow cases
|
|
142
|
+
const box = this.getBoundingClientRect()
|
|
143
|
+
this.classList.remove('measurements')
|
|
144
|
+
|
|
138
145
|
// Check if the menu content is being clipped
|
|
139
146
|
const isVerticallyClipped = this.scrollHeight > this.clientHeight
|
|
140
147
|
const isHorizontallyClipped = this.scrollWidth > this.clientWidth
|
|
@@ -148,6 +155,11 @@ export default class Menu extends UiList {
|
|
|
148
155
|
const viewportMiddle = innerHeight / 2
|
|
149
156
|
const isMenuInUpperHalf = box.top < viewportMiddle
|
|
150
157
|
|
|
158
|
+
// console.log(`Menu positioned at: top=${box.top}, left=${box.left}, bottom=${menuBottom}, right=${menuRight}`)
|
|
159
|
+
// console.log(
|
|
160
|
+
// eslint-disable-next-line max-len
|
|
161
|
+
// `Menu is in upper half: ${isMenuInUpperHalf}, Vertically clipped: ${isVerticallyClipped}, Horizontally clipped: ${isHorizontallyClipped}`
|
|
162
|
+
// )
|
|
151
163
|
// Add CSS class to control animation direction
|
|
152
164
|
if (isMenuInUpperHalf) {
|
|
153
165
|
this.classList.add('menu-positioned-above')
|
|
@@ -156,7 +168,6 @@ export default class Menu extends UiList {
|
|
|
156
168
|
this.classList.add('menu-positioned-below')
|
|
157
169
|
this.classList.remove('menu-positioned-above')
|
|
158
170
|
}
|
|
159
|
-
|
|
160
171
|
// Only set max-height if the menu would overflow the viewport OR is already clipped
|
|
161
172
|
if (menuBottom > innerHeight || isVerticallyClipped) {
|
|
162
173
|
let availableHeight: number
|
|
@@ -3,6 +3,27 @@ import { css } from 'lit'
|
|
|
3
3
|
export default css`
|
|
4
4
|
:host {
|
|
5
5
|
position-area: span-bottom right;
|
|
6
|
-
position-try
|
|
6
|
+
position-try:
|
|
7
|
+
--submenu-fallback-left,
|
|
8
|
+
--submenu-fallback-top-right,
|
|
9
|
+
--submenu-fallback-top-left,
|
|
10
|
+
--submenu-fallback-bottom-left,
|
|
11
|
+
flip-block flip-inline;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@position-try --submenu-fallback-left {
|
|
15
|
+
position-area: span-bottom left;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@position-try --submenu-fallback-top-right {
|
|
19
|
+
position-area: span-top right;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@position-try --submenu-fallback-top-left {
|
|
23
|
+
position-area: span-top left;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@position-try --submenu-fallback-bottom-left {
|
|
27
|
+
position-area: span-bottom left;
|
|
7
28
|
}
|
|
8
29
|
`
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { html,
|
|
1
|
+
import { html, PropertyValues, TemplateResult } from 'lit'
|
|
2
2
|
import { property, query, state } from 'lit/decorators.js'
|
|
3
3
|
import { classMap } from 'lit/directives/class-map.js'
|
|
4
4
|
import { styleMap } from 'lit/directives/style-map.js'
|
|
5
5
|
import { setDisabled } from '../../../lib/disabled.js'
|
|
6
|
+
import { UiElement } from '../../UiElement.js'
|
|
6
7
|
import type UiOption from './Option.js'
|
|
7
8
|
import type { UiMenuElement } from '../../menu/ui-menu.js'
|
|
8
9
|
import { nanoid } from 'nanoid'
|
|
@@ -25,7 +26,7 @@ export interface UiSelectChangeEvent {
|
|
|
25
26
|
* @fires open - Dispatched when the dropdown opens
|
|
26
27
|
* @fires close - Dispatched when the dropdown closes
|
|
27
28
|
*/
|
|
28
|
-
export default class UiSelect extends
|
|
29
|
+
export default class UiSelect extends UiElement {
|
|
29
30
|
static readonly formAssociated = true
|
|
30
31
|
#internals = this.attachInternals()
|
|
31
32
|
|
|
@@ -35,6 +36,21 @@ export default class UiSelect extends LitElement {
|
|
|
35
36
|
*/
|
|
36
37
|
#value: string | undefined
|
|
37
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Type-ahead search string accumulated from user typing
|
|
41
|
+
*/
|
|
42
|
+
#typeAheadString = ''
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Timer for resetting the type-ahead search string
|
|
46
|
+
*/
|
|
47
|
+
#typeAheadTimer: number | null = null
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Timeout duration for type-ahead reset (in milliseconds)
|
|
51
|
+
*/
|
|
52
|
+
static readonly TYPE_AHEAD_TIMEOUT = 1000
|
|
53
|
+
|
|
38
54
|
/**
|
|
39
55
|
* The currently selected value. Corresponds to the `value` attribute of the selected `ui-option`.
|
|
40
56
|
* When set programmatically, it will update the selected option if a matching option exists.
|
|
@@ -167,7 +183,7 @@ export default class UiSelect extends LitElement {
|
|
|
167
183
|
@property({ type: Boolean, reflect: true }) accessor open = false
|
|
168
184
|
|
|
169
185
|
@state() accessor selectedOption: UiOption | null = null
|
|
170
|
-
|
|
186
|
+
|
|
171
187
|
@query('.menu') accessor menu!: UiMenuElement
|
|
172
188
|
|
|
173
189
|
/**
|
|
@@ -281,6 +297,15 @@ export default class UiSelect extends LitElement {
|
|
|
281
297
|
}
|
|
282
298
|
}
|
|
283
299
|
|
|
300
|
+
override disconnectedCallback(): void {
|
|
301
|
+
super.disconnectedCallback()
|
|
302
|
+
// Clean up the type-ahead timer
|
|
303
|
+
if (this.#typeAheadTimer) {
|
|
304
|
+
clearTimeout(this.#typeAheadTimer)
|
|
305
|
+
this.#typeAheadTimer = null
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
284
309
|
/**
|
|
285
310
|
* Resets the select to its initial state. Called automatically when the parent
|
|
286
311
|
* form is reset. Part of the form-associated custom element API.
|
|
@@ -381,6 +406,13 @@ export default class UiSelect extends LitElement {
|
|
|
381
406
|
|
|
382
407
|
protected handleKeydown(e: KeyboardEvent): void {
|
|
383
408
|
if (this.disabled || e.defaultPrevented) return
|
|
409
|
+
|
|
410
|
+
// Handle type-ahead for printable characters
|
|
411
|
+
if (e.key.length === 1 && !e.ctrlKey && !e.altKey && !e.metaKey) {
|
|
412
|
+
this.handleTypeAhead(e.key.toLowerCase())
|
|
413
|
+
return
|
|
414
|
+
}
|
|
415
|
+
|
|
384
416
|
if (this.open) {
|
|
385
417
|
switch (e.key) {
|
|
386
418
|
case 'Tab': {
|
|
@@ -400,27 +432,29 @@ export default class UiSelect extends LitElement {
|
|
|
400
432
|
}
|
|
401
433
|
case 'ArrowDown':
|
|
402
434
|
e.preventDefault()
|
|
403
|
-
this.
|
|
435
|
+
this.focusNextMenuItem()
|
|
404
436
|
return
|
|
405
437
|
case 'ArrowUp':
|
|
406
438
|
e.preventDefault()
|
|
407
|
-
this.
|
|
439
|
+
this.focusPreviousMenuItem()
|
|
408
440
|
return
|
|
409
441
|
case 'Home':
|
|
410
442
|
e.preventDefault()
|
|
411
|
-
this.
|
|
443
|
+
this.focusFirstMenuItem()
|
|
412
444
|
return
|
|
413
445
|
case 'End':
|
|
414
446
|
e.preventDefault()
|
|
415
|
-
this.
|
|
447
|
+
this.focusLastMenuItem()
|
|
416
448
|
return
|
|
417
449
|
case 'Enter':
|
|
418
|
-
case ' ':
|
|
419
|
-
|
|
450
|
+
case ' ': {
|
|
451
|
+
const currentItem = this.querySelector<UiOption>(':focus')
|
|
452
|
+
if (currentItem && this.isOptionSelectable(currentItem)) {
|
|
420
453
|
e.preventDefault()
|
|
421
|
-
this.menu.notifySelect(
|
|
454
|
+
this.menu.notifySelect(currentItem)
|
|
422
455
|
}
|
|
423
456
|
return
|
|
457
|
+
}
|
|
424
458
|
}
|
|
425
459
|
} else {
|
|
426
460
|
switch (e.key) {
|
|
@@ -460,7 +494,7 @@ export default class UiSelect extends LitElement {
|
|
|
460
494
|
this.open = false
|
|
461
495
|
}
|
|
462
496
|
|
|
463
|
-
|
|
497
|
+
override handleClick(e: Event): void {
|
|
464
498
|
if (this.disabled || e.defaultPrevented) return
|
|
465
499
|
e.preventDefault()
|
|
466
500
|
e.stopPropagation()
|
|
@@ -481,23 +515,246 @@ export default class UiSelect extends LitElement {
|
|
|
481
515
|
this.setAttribute('aria-expanded', String(this.open))
|
|
482
516
|
if (this.open) {
|
|
483
517
|
menu.showPopover()
|
|
484
|
-
// menu
|
|
485
|
-
if (this.selectedOption) {
|
|
486
|
-
this.
|
|
518
|
+
// Focus on the selected option or first selectable option when menu opens
|
|
519
|
+
if (this.selectedOption && this.isOptionSelectable(this.selectedOption)) {
|
|
520
|
+
this.selectedOption.focus()
|
|
521
|
+
this.selectedOption.scrollIntoView({ behavior: 'instant', block: 'nearest', inline: 'nearest' })
|
|
487
522
|
} else {
|
|
488
|
-
this.
|
|
523
|
+
const firstSelectableOption = this.getFirstSelectableOption()
|
|
524
|
+
if (firstSelectableOption) {
|
|
525
|
+
firstSelectableOption.focus()
|
|
526
|
+
firstSelectableOption.scrollIntoView({ behavior: 'instant', block: 'nearest', inline: 'nearest' })
|
|
527
|
+
}
|
|
489
528
|
}
|
|
490
529
|
this.dispatchEvent(new CustomEvent('open', { bubbles: false, composed: true }))
|
|
491
|
-
this.focus()
|
|
492
530
|
} else {
|
|
493
531
|
menu.hidePopover()
|
|
494
532
|
this.dispatchEvent(new CustomEvent('close', { bubbles: false, composed: true }))
|
|
533
|
+
// Return focus to the select element when menu closes
|
|
534
|
+
this.focus()
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Focus the next menu item in the dropdown, skipping disabled options
|
|
540
|
+
*/
|
|
541
|
+
focusNextMenuItem(): void {
|
|
542
|
+
const currentItem = this.querySelector<UiOption>(':focus')
|
|
543
|
+
const nextItem = currentItem ? this.getNextSelectableOption(currentItem) : this.getFirstSelectableOption()
|
|
544
|
+
if (nextItem) {
|
|
545
|
+
nextItem.focus()
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Focus the previous menu item in the dropdown, skipping disabled options
|
|
551
|
+
*/
|
|
552
|
+
focusPreviousMenuItem(): void {
|
|
553
|
+
const currentItem = this.querySelector<UiOption>(':focus')
|
|
554
|
+
const previousItem = currentItem ? this.getPreviousSelectableOption(currentItem) : this.getLastSelectableOption()
|
|
555
|
+
if (previousItem) {
|
|
556
|
+
previousItem.focus()
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Focus the first menu item in the dropdown, skipping disabled options
|
|
562
|
+
*/
|
|
563
|
+
focusFirstMenuItem(): void {
|
|
564
|
+
const firstItem = this.getFirstSelectableOption()
|
|
565
|
+
if (firstItem) {
|
|
566
|
+
firstItem.focus()
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Focus the last menu item in the dropdown, skipping disabled options
|
|
572
|
+
*/
|
|
573
|
+
focusLastMenuItem(): void {
|
|
574
|
+
const lastItem = this.getLastSelectableOption()
|
|
575
|
+
if (lastItem) {
|
|
576
|
+
lastItem.focus()
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Gets the first selectable (non-disabled) option
|
|
582
|
+
*/
|
|
583
|
+
getFirstSelectableOption(): UiOption | null {
|
|
584
|
+
const options = this.querySelectorAll<UiOption>('ui-option')
|
|
585
|
+
for (const option of options) {
|
|
586
|
+
if (this.isOptionSelectable(option)) {
|
|
587
|
+
return option
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return null
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Gets the last selectable (non-disabled) option
|
|
595
|
+
*/
|
|
596
|
+
getLastSelectableOption(): UiOption | null {
|
|
597
|
+
const options = Array.from(this.querySelectorAll<UiOption>('ui-option')).reverse()
|
|
598
|
+
for (const option of options) {
|
|
599
|
+
if (this.isOptionSelectable(option)) {
|
|
600
|
+
return option
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return null
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Gets the next selectable option after the current one, wrapping around if needed
|
|
608
|
+
*/
|
|
609
|
+
protected getNextSelectableOption(currentOption: UiOption): UiOption | null {
|
|
610
|
+
const options = Array.from(this.querySelectorAll<UiOption>('ui-option'))
|
|
611
|
+
const currentIndex = options.indexOf(currentOption)
|
|
612
|
+
|
|
613
|
+
if (currentIndex === -1) {
|
|
614
|
+
return this.getFirstSelectableOption()
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Start from the next option and wrap around
|
|
618
|
+
for (let i = 1; i < options.length; i++) {
|
|
619
|
+
const nextIndex = (currentIndex + i) % options.length
|
|
620
|
+
const option = options[nextIndex]
|
|
621
|
+
if (this.isOptionSelectable(option)) {
|
|
622
|
+
return option
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return currentOption // Return current if no other selectable option found
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Gets the previous selectable option before the current one, wrapping around if needed
|
|
631
|
+
*/
|
|
632
|
+
protected getPreviousSelectableOption(currentOption: UiOption): UiOption | null {
|
|
633
|
+
const options = Array.from(this.querySelectorAll<UiOption>('ui-option'))
|
|
634
|
+
const currentIndex = options.indexOf(currentOption)
|
|
635
|
+
|
|
636
|
+
if (currentIndex === -1) {
|
|
637
|
+
return this.getLastSelectableOption()
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Start from the previous option and wrap around
|
|
641
|
+
for (let i = 1; i < options.length; i++) {
|
|
642
|
+
const prevIndex = (currentIndex - i + options.length) % options.length
|
|
643
|
+
const option = options[prevIndex]
|
|
644
|
+
if (this.isOptionSelectable(option)) {
|
|
645
|
+
return option
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
return currentOption // Return current if no other selectable option found
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Checks if an option is selectable (not disabled and not hidden)
|
|
654
|
+
*/
|
|
655
|
+
protected isOptionSelectable(option: UiOption): boolean {
|
|
656
|
+
if (option.disabled) {
|
|
657
|
+
return false
|
|
658
|
+
}
|
|
659
|
+
if (option.hasAttribute('disabled')) {
|
|
660
|
+
return false
|
|
661
|
+
}
|
|
662
|
+
if (option.hidden && option.hasAttribute('hidden')) {
|
|
663
|
+
return false
|
|
495
664
|
}
|
|
665
|
+
return true
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Handles type-ahead functionality for keyboard navigation
|
|
670
|
+
*/
|
|
671
|
+
protected handleTypeAhead(char: string): void {
|
|
672
|
+
// Clear the existing timer
|
|
673
|
+
if (this.#typeAheadTimer) {
|
|
674
|
+
clearTimeout(this.#typeAheadTimer)
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Add the character to the search string
|
|
678
|
+
this.#typeAheadString += char
|
|
679
|
+
|
|
680
|
+
// Find the matching option
|
|
681
|
+
const matchingOption = this.findOptionByTypeAhead(this.#typeAheadString)
|
|
682
|
+
if (matchingOption) {
|
|
683
|
+
if (this.open) {
|
|
684
|
+
// If menu is open, focus the matching option
|
|
685
|
+
matchingOption.focus()
|
|
686
|
+
} else {
|
|
687
|
+
// If menu is closed, select the matching option
|
|
688
|
+
this.selectOption(matchingOption)
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Set a timer to reset the search string
|
|
693
|
+
this.#typeAheadTimer = window.setTimeout(() => {
|
|
694
|
+
this.#typeAheadString = ''
|
|
695
|
+
this.#typeAheadTimer = null
|
|
696
|
+
}, UiSelect.TYPE_AHEAD_TIMEOUT)
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Finds an option that matches the type-ahead search string
|
|
701
|
+
*/
|
|
702
|
+
protected findOptionByTypeAhead(searchString: string): UiOption | null {
|
|
703
|
+
const options = this.querySelectorAll<UiOption>('ui-option')
|
|
704
|
+
|
|
705
|
+
for (const option of options) {
|
|
706
|
+
if (!this.isOptionSelectable(option)) {
|
|
707
|
+
continue
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Get the option's text content for comparison
|
|
711
|
+
const optionText = this.getOptionDisplayText(option).toLowerCase()
|
|
712
|
+
|
|
713
|
+
if (optionText.startsWith(searchString)) {
|
|
714
|
+
return option
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
return null
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Gets the display text for an option (either textContent or renderValue)
|
|
723
|
+
*/
|
|
724
|
+
protected getOptionDisplayText(option: UiOption): string {
|
|
725
|
+
// Use renderValue if available, otherwise fall back to textContent
|
|
726
|
+
return option.renderValue || option.textContent?.trim() || ''
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Selects an option and updates the component state
|
|
731
|
+
*/
|
|
732
|
+
protected selectOption(option: UiOption): void {
|
|
733
|
+
if (this.selectedOption && this.selectedOption !== option) {
|
|
734
|
+
this.selectedOption.selected = false
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
option.selected = true
|
|
738
|
+
this.selectedOption = option
|
|
739
|
+
this.#value = option.value
|
|
740
|
+
this.#internals.setFormValue(this.value ?? null)
|
|
741
|
+
|
|
742
|
+
// Dispatch change event
|
|
743
|
+
const changeEvent = new CustomEvent<UiSelectChangeEvent>('change', {
|
|
744
|
+
detail: { value: this.value, item: option },
|
|
745
|
+
bubbles: false,
|
|
746
|
+
composed: true,
|
|
747
|
+
})
|
|
748
|
+
this.dispatchEvent(changeEvent)
|
|
496
749
|
}
|
|
497
750
|
|
|
498
751
|
handleSelect(e: CustomEvent<{ item: UiOption }>): void {
|
|
499
752
|
e.stopPropagation()
|
|
500
753
|
const item = e.detail.item
|
|
754
|
+
if (this.selectedOption && this.selectedOption !== item) {
|
|
755
|
+
this.selectedOption.selected = false
|
|
756
|
+
}
|
|
757
|
+
item.selected = true
|
|
501
758
|
this.selectedOption = item
|
|
502
759
|
this.#value = item.value
|
|
503
760
|
this.#internals.setFormValue(this.value ?? null)
|
|
@@ -510,16 +767,18 @@ export default class UiSelect extends LitElement {
|
|
|
510
767
|
composed: true,
|
|
511
768
|
})
|
|
512
769
|
this.dispatchEvent(changeEvent)
|
|
513
|
-
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
handleHighlightChange(e: CustomEvent<{ item: UiOption | null }>): void {
|
|
517
|
-
this.ariaActiveDescendant = e.detail.item?.id
|
|
770
|
+
// Focus will be returned to select element by handleOpenChange when open=false
|
|
518
771
|
}
|
|
519
772
|
|
|
520
773
|
handleMenuClose(): void {
|
|
521
774
|
this.open = false
|
|
522
|
-
|
|
775
|
+
// Focus will be returned to select element by handleOpenChange when open=false
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
handleMenuToggle(e: ToggleEvent): void {
|
|
779
|
+
if (e.newState === 'closed') {
|
|
780
|
+
this.open = false
|
|
781
|
+
}
|
|
523
782
|
}
|
|
524
783
|
|
|
525
784
|
protected renderInput(): TemplateResult {
|
|
@@ -560,7 +819,7 @@ export default class UiSelect extends LitElement {
|
|
|
560
819
|
selector="ui-option"
|
|
561
820
|
@select="${this.handleSelect}"
|
|
562
821
|
@close="${this.handleMenuClose}"
|
|
563
|
-
@
|
|
822
|
+
@toggle="${this.handleMenuToggle}"
|
|
564
823
|
>
|
|
565
824
|
<slot @slotchange="${this.handleSlotChange}"></slot>
|
|
566
825
|
</ui-menu>`
|
|
@@ -586,8 +845,6 @@ export default class UiSelect extends LitElement {
|
|
|
586
845
|
'disabled': this.disabled,
|
|
587
846
|
})
|
|
588
847
|
return html`${this.renderFocusRing()}
|
|
589
|
-
<div class="${classes}"
|
|
590
|
-
${this.renderInput()} ${this.renderMenu()}
|
|
591
|
-
</div> `
|
|
848
|
+
<div class="${classes}">${this.renderInput()} ${this.renderMenu()}</div> `
|
|
592
849
|
}
|
|
593
850
|
}
|