@api-client/ui 0.5.53 → 0.5.55
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/core/ActivityManager.d.ts.map +1 -1
- package/build/src/core/ActivityManager.js +3 -1
- package/build/src/core/ActivityManager.js.map +1 -1
- package/build/src/md/input/Input.d.ts +2 -0
- package/build/src/md/input/Input.d.ts.map +1 -1
- 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/select/internals/Select.d.ts +21 -0
- package/build/src/md/select/internals/Select.d.ts.map +1 -1
- package/build/src/md/select/internals/Select.js +98 -0
- package/build/src/md/select/internals/Select.js.map +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/core/ActivityManager.ts +3 -1
- package/src/md/menu/internal/Menu.styles.ts +5 -1
- package/src/md/menu/internal/Menu.ts +17 -6
- package/src/md/select/internals/Select.ts +116 -0
package/package.json
CHANGED
|
@@ -373,7 +373,9 @@ export class ActivityManager {
|
|
|
373
373
|
await target.activity.onPause()
|
|
374
374
|
target.activity.lifecycle = ActivityLifecycle.Paused
|
|
375
375
|
}
|
|
376
|
-
|
|
376
|
+
// Let's create a shallow copy of the intent. We can disregard the ByReference flag here,
|
|
377
|
+
// as we override the data with the result data.
|
|
378
|
+
const intentCopy = { ...intent }
|
|
377
379
|
intentCopy.data = activity.getResult()
|
|
378
380
|
await target.activity.onActivityResult(requestCode, activity.resultCode, intentCopy)
|
|
379
381
|
}
|
|
@@ -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
|
|
@@ -36,6 +36,21 @@ export default class UiSelect extends UiElement {
|
|
|
36
36
|
*/
|
|
37
37
|
#value: string | undefined
|
|
38
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
|
+
|
|
39
54
|
/**
|
|
40
55
|
* The currently selected value. Corresponds to the `value` attribute of the selected `ui-option`.
|
|
41
56
|
* When set programmatically, it will update the selected option if a matching option exists.
|
|
@@ -282,6 +297,15 @@ export default class UiSelect extends UiElement {
|
|
|
282
297
|
}
|
|
283
298
|
}
|
|
284
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
|
+
|
|
285
309
|
/**
|
|
286
310
|
* Resets the select to its initial state. Called automatically when the parent
|
|
287
311
|
* form is reset. Part of the form-associated custom element API.
|
|
@@ -382,6 +406,13 @@ export default class UiSelect extends UiElement {
|
|
|
382
406
|
|
|
383
407
|
protected handleKeydown(e: KeyboardEvent): void {
|
|
384
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
|
+
|
|
385
416
|
if (this.open) {
|
|
386
417
|
switch (e.key) {
|
|
387
418
|
case 'Tab': {
|
|
@@ -487,10 +518,12 @@ export default class UiSelect extends UiElement {
|
|
|
487
518
|
// Focus on the selected option or first selectable option when menu opens
|
|
488
519
|
if (this.selectedOption && this.isOptionSelectable(this.selectedOption)) {
|
|
489
520
|
this.selectedOption.focus()
|
|
521
|
+
this.selectedOption.scrollIntoView({ behavior: 'instant', block: 'nearest', inline: 'nearest' })
|
|
490
522
|
} else {
|
|
491
523
|
const firstSelectableOption = this.getFirstSelectableOption()
|
|
492
524
|
if (firstSelectableOption) {
|
|
493
525
|
firstSelectableOption.focus()
|
|
526
|
+
firstSelectableOption.scrollIntoView({ behavior: 'instant', block: 'nearest', inline: 'nearest' })
|
|
494
527
|
}
|
|
495
528
|
}
|
|
496
529
|
this.dispatchEvent(new CustomEvent('open', { bubbles: false, composed: true }))
|
|
@@ -632,6 +665,89 @@ export default class UiSelect extends UiElement {
|
|
|
632
665
|
return true
|
|
633
666
|
}
|
|
634
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)
|
|
749
|
+
}
|
|
750
|
+
|
|
635
751
|
handleSelect(e: CustomEvent<{ item: UiOption }>): void {
|
|
636
752
|
e.stopPropagation()
|
|
637
753
|
const item = e.detail.item
|