@api-client/ui 0.5.51 → 0.5.53
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.styles.js +1 -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 +41 -7
- package/build/src/md/select/internals/Select.d.ts.map +1 -1
- package/build/src/md/select/internals/Select.js +150 -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 +1 -1
- package/src/md/menu/internal/SubMenu.styles.ts +22 -1
- package/src/md/select/internals/Select.ts +167 -26
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@ export default css`
|
|
|
5
5
|
display: none;
|
|
6
6
|
position-area: bottom span-right;
|
|
7
7
|
position-try: --menu-fallback-bottom-left, --menu-fallback-top-right, --menu-fallback-top-left, flip-block;
|
|
8
|
-
position:
|
|
8
|
+
position: fixed;
|
|
9
9
|
margin: 0;
|
|
10
10
|
padding: 0;
|
|
11
11
|
border: none;
|
|
@@ -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
|
|
|
@@ -167,7 +168,7 @@ export default class UiSelect extends LitElement {
|
|
|
167
168
|
@property({ type: Boolean, reflect: true }) accessor open = false
|
|
168
169
|
|
|
169
170
|
@state() accessor selectedOption: UiOption | null = null
|
|
170
|
-
|
|
171
|
+
|
|
171
172
|
@query('.menu') accessor menu!: UiMenuElement
|
|
172
173
|
|
|
173
174
|
/**
|
|
@@ -400,27 +401,29 @@ export default class UiSelect extends LitElement {
|
|
|
400
401
|
}
|
|
401
402
|
case 'ArrowDown':
|
|
402
403
|
e.preventDefault()
|
|
403
|
-
this.
|
|
404
|
+
this.focusNextMenuItem()
|
|
404
405
|
return
|
|
405
406
|
case 'ArrowUp':
|
|
406
407
|
e.preventDefault()
|
|
407
|
-
this.
|
|
408
|
+
this.focusPreviousMenuItem()
|
|
408
409
|
return
|
|
409
410
|
case 'Home':
|
|
410
411
|
e.preventDefault()
|
|
411
|
-
this.
|
|
412
|
+
this.focusFirstMenuItem()
|
|
412
413
|
return
|
|
413
414
|
case 'End':
|
|
414
415
|
e.preventDefault()
|
|
415
|
-
this.
|
|
416
|
+
this.focusLastMenuItem()
|
|
416
417
|
return
|
|
417
418
|
case 'Enter':
|
|
418
|
-
case ' ':
|
|
419
|
-
|
|
419
|
+
case ' ': {
|
|
420
|
+
const currentItem = this.querySelector<UiOption>(':focus')
|
|
421
|
+
if (currentItem && this.isOptionSelectable(currentItem)) {
|
|
420
422
|
e.preventDefault()
|
|
421
|
-
this.menu.notifySelect(
|
|
423
|
+
this.menu.notifySelect(currentItem)
|
|
422
424
|
}
|
|
423
425
|
return
|
|
426
|
+
}
|
|
424
427
|
}
|
|
425
428
|
} else {
|
|
426
429
|
switch (e.key) {
|
|
@@ -460,7 +463,7 @@ export default class UiSelect extends LitElement {
|
|
|
460
463
|
this.open = false
|
|
461
464
|
}
|
|
462
465
|
|
|
463
|
-
|
|
466
|
+
override handleClick(e: Event): void {
|
|
464
467
|
if (this.disabled || e.defaultPrevented) return
|
|
465
468
|
e.preventDefault()
|
|
466
469
|
e.stopPropagation()
|
|
@@ -481,23 +484,161 @@ export default class UiSelect extends LitElement {
|
|
|
481
484
|
this.setAttribute('aria-expanded', String(this.open))
|
|
482
485
|
if (this.open) {
|
|
483
486
|
menu.showPopover()
|
|
484
|
-
// menu
|
|
485
|
-
if (this.selectedOption) {
|
|
486
|
-
this.
|
|
487
|
+
// Focus on the selected option or first selectable option when menu opens
|
|
488
|
+
if (this.selectedOption && this.isOptionSelectable(this.selectedOption)) {
|
|
489
|
+
this.selectedOption.focus()
|
|
487
490
|
} else {
|
|
488
|
-
this.
|
|
491
|
+
const firstSelectableOption = this.getFirstSelectableOption()
|
|
492
|
+
if (firstSelectableOption) {
|
|
493
|
+
firstSelectableOption.focus()
|
|
494
|
+
}
|
|
489
495
|
}
|
|
490
496
|
this.dispatchEvent(new CustomEvent('open', { bubbles: false, composed: true }))
|
|
491
|
-
this.focus()
|
|
492
497
|
} else {
|
|
493
498
|
menu.hidePopover()
|
|
494
499
|
this.dispatchEvent(new CustomEvent('close', { bubbles: false, composed: true }))
|
|
500
|
+
// Return focus to the select element when menu closes
|
|
501
|
+
this.focus()
|
|
495
502
|
}
|
|
496
503
|
}
|
|
497
504
|
|
|
505
|
+
/**
|
|
506
|
+
* Focus the next menu item in the dropdown, skipping disabled options
|
|
507
|
+
*/
|
|
508
|
+
focusNextMenuItem(): void {
|
|
509
|
+
const currentItem = this.querySelector<UiOption>(':focus')
|
|
510
|
+
const nextItem = currentItem ? this.getNextSelectableOption(currentItem) : this.getFirstSelectableOption()
|
|
511
|
+
if (nextItem) {
|
|
512
|
+
nextItem.focus()
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Focus the previous menu item in the dropdown, skipping disabled options
|
|
518
|
+
*/
|
|
519
|
+
focusPreviousMenuItem(): void {
|
|
520
|
+
const currentItem = this.querySelector<UiOption>(':focus')
|
|
521
|
+
const previousItem = currentItem ? this.getPreviousSelectableOption(currentItem) : this.getLastSelectableOption()
|
|
522
|
+
if (previousItem) {
|
|
523
|
+
previousItem.focus()
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Focus the first menu item in the dropdown, skipping disabled options
|
|
529
|
+
*/
|
|
530
|
+
focusFirstMenuItem(): void {
|
|
531
|
+
const firstItem = this.getFirstSelectableOption()
|
|
532
|
+
if (firstItem) {
|
|
533
|
+
firstItem.focus()
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Focus the last menu item in the dropdown, skipping disabled options
|
|
539
|
+
*/
|
|
540
|
+
focusLastMenuItem(): void {
|
|
541
|
+
const lastItem = this.getLastSelectableOption()
|
|
542
|
+
if (lastItem) {
|
|
543
|
+
lastItem.focus()
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Gets the first selectable (non-disabled) option
|
|
549
|
+
*/
|
|
550
|
+
getFirstSelectableOption(): UiOption | null {
|
|
551
|
+
const options = this.querySelectorAll<UiOption>('ui-option')
|
|
552
|
+
for (const option of options) {
|
|
553
|
+
if (this.isOptionSelectable(option)) {
|
|
554
|
+
return option
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return null
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Gets the last selectable (non-disabled) option
|
|
562
|
+
*/
|
|
563
|
+
getLastSelectableOption(): UiOption | null {
|
|
564
|
+
const options = Array.from(this.querySelectorAll<UiOption>('ui-option')).reverse()
|
|
565
|
+
for (const option of options) {
|
|
566
|
+
if (this.isOptionSelectable(option)) {
|
|
567
|
+
return option
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return null
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Gets the next selectable option after the current one, wrapping around if needed
|
|
575
|
+
*/
|
|
576
|
+
protected getNextSelectableOption(currentOption: UiOption): UiOption | null {
|
|
577
|
+
const options = Array.from(this.querySelectorAll<UiOption>('ui-option'))
|
|
578
|
+
const currentIndex = options.indexOf(currentOption)
|
|
579
|
+
|
|
580
|
+
if (currentIndex === -1) {
|
|
581
|
+
return this.getFirstSelectableOption()
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Start from the next option and wrap around
|
|
585
|
+
for (let i = 1; i < options.length; i++) {
|
|
586
|
+
const nextIndex = (currentIndex + i) % options.length
|
|
587
|
+
const option = options[nextIndex]
|
|
588
|
+
if (this.isOptionSelectable(option)) {
|
|
589
|
+
return option
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
return currentOption // Return current if no other selectable option found
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Gets the previous selectable option before the current one, wrapping around if needed
|
|
598
|
+
*/
|
|
599
|
+
protected getPreviousSelectableOption(currentOption: UiOption): UiOption | null {
|
|
600
|
+
const options = Array.from(this.querySelectorAll<UiOption>('ui-option'))
|
|
601
|
+
const currentIndex = options.indexOf(currentOption)
|
|
602
|
+
|
|
603
|
+
if (currentIndex === -1) {
|
|
604
|
+
return this.getLastSelectableOption()
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Start from the previous option and wrap around
|
|
608
|
+
for (let i = 1; i < options.length; i++) {
|
|
609
|
+
const prevIndex = (currentIndex - i + options.length) % options.length
|
|
610
|
+
const option = options[prevIndex]
|
|
611
|
+
if (this.isOptionSelectable(option)) {
|
|
612
|
+
return option
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
return currentOption // Return current if no other selectable option found
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Checks if an option is selectable (not disabled and not hidden)
|
|
621
|
+
*/
|
|
622
|
+
protected isOptionSelectable(option: UiOption): boolean {
|
|
623
|
+
if (option.disabled) {
|
|
624
|
+
return false
|
|
625
|
+
}
|
|
626
|
+
if (option.hasAttribute('disabled')) {
|
|
627
|
+
return false
|
|
628
|
+
}
|
|
629
|
+
if (option.hidden && option.hasAttribute('hidden')) {
|
|
630
|
+
return false
|
|
631
|
+
}
|
|
632
|
+
return true
|
|
633
|
+
}
|
|
634
|
+
|
|
498
635
|
handleSelect(e: CustomEvent<{ item: UiOption }>): void {
|
|
499
636
|
e.stopPropagation()
|
|
500
637
|
const item = e.detail.item
|
|
638
|
+
if (this.selectedOption && this.selectedOption !== item) {
|
|
639
|
+
this.selectedOption.selected = false
|
|
640
|
+
}
|
|
641
|
+
item.selected = true
|
|
501
642
|
this.selectedOption = item
|
|
502
643
|
this.#value = item.value
|
|
503
644
|
this.#internals.setFormValue(this.value ?? null)
|
|
@@ -510,16 +651,18 @@ export default class UiSelect extends LitElement {
|
|
|
510
651
|
composed: true,
|
|
511
652
|
})
|
|
512
653
|
this.dispatchEvent(changeEvent)
|
|
513
|
-
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
handleHighlightChange(e: CustomEvent<{ item: UiOption | null }>): void {
|
|
517
|
-
this.ariaActiveDescendant = e.detail.item?.id
|
|
654
|
+
// Focus will be returned to select element by handleOpenChange when open=false
|
|
518
655
|
}
|
|
519
656
|
|
|
520
657
|
handleMenuClose(): void {
|
|
521
658
|
this.open = false
|
|
522
|
-
|
|
659
|
+
// Focus will be returned to select element by handleOpenChange when open=false
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
handleMenuToggle(e: ToggleEvent): void {
|
|
663
|
+
if (e.newState === 'closed') {
|
|
664
|
+
this.open = false
|
|
665
|
+
}
|
|
523
666
|
}
|
|
524
667
|
|
|
525
668
|
protected renderInput(): TemplateResult {
|
|
@@ -560,7 +703,7 @@ export default class UiSelect extends LitElement {
|
|
|
560
703
|
selector="ui-option"
|
|
561
704
|
@select="${this.handleSelect}"
|
|
562
705
|
@close="${this.handleMenuClose}"
|
|
563
|
-
@
|
|
706
|
+
@toggle="${this.handleMenuToggle}"
|
|
564
707
|
>
|
|
565
708
|
<slot @slotchange="${this.handleSlotChange}"></slot>
|
|
566
709
|
</ui-menu>`
|
|
@@ -586,8 +729,6 @@ export default class UiSelect extends LitElement {
|
|
|
586
729
|
'disabled': this.disabled,
|
|
587
730
|
})
|
|
588
731
|
return html`${this.renderFocusRing()}
|
|
589
|
-
<div class="${classes}"
|
|
590
|
-
${this.renderInput()} ${this.renderMenu()}
|
|
591
|
-
</div> `
|
|
732
|
+
<div class="${classes}">${this.renderInput()} ${this.renderMenu()}</div> `
|
|
592
733
|
}
|
|
593
734
|
}
|