@api-client/ui 0.5.24 → 0.5.26
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/.cursor/rules/lit-best-practices.mdc +12 -1
- package/.github/instructions/lit-best-practices.instructions.md +2 -0
- package/build/src/elements/navigation/internals/AppNavigationElement.d.ts.map +1 -1
- package/build/src/elements/navigation/internals/AppNavigationElement.js +1 -6
- package/build/src/elements/navigation/internals/AppNavigationElement.js.map +1 -1
- package/build/src/elements/navigation/internals/NavigationItem.d.ts +6 -0
- package/build/src/elements/navigation/internals/NavigationItem.d.ts.map +1 -1
- package/build/src/elements/navigation/internals/NavigationItem.js +26 -7
- package/build/src/elements/navigation/internals/NavigationItem.js.map +1 -1
- package/build/src/elements/navigation/internals/NavigationItem.styles.d.ts.map +1 -1
- package/build/src/elements/navigation/internals/NavigationItem.styles.js +18 -1
- package/build/src/elements/navigation/internals/NavigationItem.styles.js.map +1 -1
- package/build/src/md/dropdown-list/internals/UiDropdownList.d.ts.map +1 -1
- package/build/src/md/dropdown-list/internals/UiDropdownList.js +4 -3
- package/build/src/md/dropdown-list/internals/UiDropdownList.js.map +1 -1
- package/build/src/md/input/Input.d.ts +8 -4
- package/build/src/md/input/Input.d.ts.map +1 -1
- package/build/src/md/input/Input.js +8 -36
- package/build/src/md/input/Input.js.map +1 -1
- package/build/src/md/list/internals/List.d.ts +3 -1
- package/build/src/md/list/internals/List.d.ts.map +1 -1
- package/build/src/md/list/internals/List.js +9 -4
- package/build/src/md/list/internals/List.js.map +1 -1
- package/build/src/md/menu/internal/Menu.d.ts +8 -7
- package/build/src/md/menu/internal/Menu.d.ts.map +1 -1
- package/build/src/md/menu/internal/Menu.js +26 -29
- package/build/src/md/menu/internal/Menu.js.map +1 -1
- package/build/src/md/select/index.d.ts +4 -0
- package/build/src/md/select/index.d.ts.map +1 -0
- package/build/src/md/select/index.js +3 -0
- package/build/src/md/select/index.js.map +1 -0
- package/build/src/md/select/internals/Option.d.ts +125 -0
- package/build/src/md/select/internals/Option.d.ts.map +1 -0
- package/build/src/md/select/internals/Option.js +242 -0
- package/build/src/md/select/internals/Option.js.map +1 -0
- package/build/src/md/select/internals/Option.styles.d.ts +3 -0
- package/build/src/md/select/internals/Option.styles.d.ts.map +1 -0
- package/build/src/md/select/internals/Option.styles.js +139 -0
- package/build/src/md/select/internals/Option.styles.js.map +1 -0
- package/build/src/md/select/internals/Select.d.ts +250 -0
- package/build/src/md/select/internals/Select.d.ts.map +1 -0
- package/build/src/md/select/internals/Select.js +606 -0
- package/build/src/md/select/internals/Select.js.map +1 -0
- package/build/src/md/select/internals/Select.styles.d.ts +3 -0
- package/build/src/md/select/internals/Select.styles.d.ts.map +1 -0
- package/build/src/md/select/internals/Select.styles.js +22 -0
- package/build/src/md/select/internals/Select.styles.js.map +1 -0
- package/build/src/md/select/ui-option.d.ts +12 -0
- package/build/src/md/select/ui-option.d.ts.map +1 -0
- package/build/src/md/select/ui-option.js +29 -0
- package/build/src/md/select/ui-option.js.map +1 -0
- package/build/src/md/select/ui-select.d.ts +12 -0
- package/build/src/md/select/ui-select.d.ts.map +1 -0
- package/build/src/md/select/ui-select.js +27 -0
- package/build/src/md/select/ui-select.js.map +1 -0
- package/build/src/md/text-field/internals/TextField.d.ts.map +1 -1
- package/build/src/md/text-field/internals/TextField.js +1 -0
- package/build/src/md/text-field/internals/TextField.js.map +1 -1
- package/demo/elements/index.html +7 -4
- package/demo/elements/navigation/navigation-item.html +45 -0
- package/demo/elements/navigation/navigation-item.ts +112 -0
- package/demo/md/index.html +2 -0
- package/demo/md/inputs/input.ts +4 -0
- package/demo/md/select/index.html +16 -0
- package/demo/md/select/index.ts +202 -0
- package/package.json +1 -1
- package/src/elements/navigation/internals/AppNavigationElement.ts +1 -6
- package/src/elements/navigation/internals/NavigationItem.styles.ts +18 -1
- package/src/elements/navigation/internals/NavigationItem.ts +11 -5
- package/src/md/dropdown-list/internals/UiDropdownList.ts +4 -3
- package/src/md/input/Input.ts +8 -37
- package/src/md/list/internals/List.ts +12 -5
- package/src/md/menu/internal/Menu.ts +27 -18
- package/src/md/select/index.ts +3 -0
- package/src/md/select/internals/Option.styles.ts +139 -0
- package/src/md/select/internals/Option.ts +210 -0
- package/src/md/select/internals/Select.styles.ts +22 -0
- package/src/md/select/internals/Select.ts +534 -0
- package/src/md/select/ui-option.ts +18 -0
- package/src/md/select/ui-select.ts +17 -0
- package/src/md/text-field/internals/TextField.ts +1 -0
- package/test/md/menu/SubMenu.test.ts +2 -3
- package/test/md/select/Select.test.ts +667 -0
|
@@ -28,6 +28,12 @@ export default css`
|
|
|
28
28
|
--_state-layer-padding-end: 24px;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
:host([iconOnly]) {
|
|
32
|
+
width: 40px;
|
|
33
|
+
--_state-layer-padding-start: 8px;
|
|
34
|
+
--_state-layer-padding-end: 8px;
|
|
35
|
+
}
|
|
36
|
+
|
|
31
37
|
:host([selected]) {
|
|
32
38
|
--_state-layer-color: var(--md-sys-color-secondary-container);
|
|
33
39
|
--_icon-color: var(--md-sys-color-on-secondary-container);
|
|
@@ -61,10 +67,15 @@ export default css`
|
|
|
61
67
|
--md-ripple-pressed-opacity: var(--_pressed-state-layer-opacity);
|
|
62
68
|
}
|
|
63
69
|
|
|
64
|
-
.button.icon {
|
|
70
|
+
.button.icon:not(.button.icon) {
|
|
65
71
|
--_state-layer-padding-start: 16px;
|
|
66
72
|
}
|
|
67
73
|
|
|
74
|
+
.button.icon-only {
|
|
75
|
+
gap: 0;
|
|
76
|
+
justify-content: center;
|
|
77
|
+
}
|
|
78
|
+
|
|
68
79
|
.button:hover {
|
|
69
80
|
color: var(--_hover-icon-color);
|
|
70
81
|
}
|
|
@@ -77,6 +88,12 @@ export default css`
|
|
|
77
88
|
color: var(--_pressed-icon-color);
|
|
78
89
|
}
|
|
79
90
|
|
|
91
|
+
.icon {
|
|
92
|
+
display: flex;
|
|
93
|
+
align-items: center;
|
|
94
|
+
justify-content: center;
|
|
95
|
+
}
|
|
96
|
+
|
|
80
97
|
md-focus-ring {
|
|
81
98
|
--md-focus-ring-shape-start-start: var(--_state-layer-shape);
|
|
82
99
|
--md-focus-ring-shape-start-end: var(--_state-layer-shape);
|
|
@@ -20,6 +20,12 @@ export default class NavigationItem extends UiElement {
|
|
|
20
20
|
* @attribute
|
|
21
21
|
*/
|
|
22
22
|
@property({ reflect: true, type: Boolean }) accessor selected = false
|
|
23
|
+
/**
|
|
24
|
+
* When set, the navigation item is rendered as an icon-only button.
|
|
25
|
+
* The width of the button is set to 40px.
|
|
26
|
+
* @attribute
|
|
27
|
+
*/
|
|
28
|
+
@property({ reflect: true, type: Boolean }) accessor iconOnly = false
|
|
23
29
|
|
|
24
30
|
/**
|
|
25
31
|
* Determines when the element has an icon in the "icon" slot.
|
|
@@ -37,16 +43,16 @@ export default class NavigationItem extends UiElement {
|
|
|
37
43
|
protected override render(): TemplateResult {
|
|
38
44
|
const { pressed = false } = this
|
|
39
45
|
const containerClasses = classMap({
|
|
40
|
-
button: true,
|
|
41
|
-
icon: this.hasIcon,
|
|
42
|
-
pressed,
|
|
46
|
+
'button': true,
|
|
47
|
+
'icon': this.hasIcon,
|
|
48
|
+
'pressed': pressed,
|
|
49
|
+
'icon-only': this.iconOnly,
|
|
43
50
|
})
|
|
44
51
|
return html`
|
|
45
52
|
<button class="${containerClasses}" id="button">
|
|
46
53
|
<md-focus-ring part="focus-ring" for="button"></md-focus-ring>
|
|
47
54
|
<md-ripple></md-ripple>
|
|
48
|
-
${this.renderIcon()}
|
|
49
|
-
<slot></slot>
|
|
55
|
+
${this.renderIcon()}${this.iconOnly ? '' : html`<slot></slot>`}
|
|
50
56
|
</button>
|
|
51
57
|
`
|
|
52
58
|
}
|
|
@@ -368,9 +368,10 @@ export default class UiDropdownList extends LitElement {
|
|
|
368
368
|
}
|
|
369
369
|
|
|
370
370
|
protected activate(e: Event): void {
|
|
371
|
-
if (e.defaultPrevented) {
|
|
372
|
-
|
|
373
|
-
|
|
371
|
+
// if (e.defaultPrevented) {
|
|
372
|
+
// We now cancel the event in the menu list
|
|
373
|
+
// return
|
|
374
|
+
// }
|
|
374
375
|
const path = e.composedPath()
|
|
375
376
|
let item: HTMLElement | undefined
|
|
376
377
|
while (!item) {
|
package/src/md/input/Input.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { html, nothing, PropertyValues, TemplateResult } from 'lit'
|
|
1
|
+
import { html, LitElement, nothing, PropertyValues, TemplateResult } from 'lit'
|
|
2
2
|
import { property, queryAssignedElements, state } from 'lit/decorators.js'
|
|
3
3
|
import { classMap } from 'lit/directives/class-map.js'
|
|
4
4
|
import { ifDefined } from 'lit/directives/if-defined.js'
|
|
5
5
|
import { SupportedAutocapitalize, SupportedAutocomplete, SupportedInputTypes } from '../../types/input.js'
|
|
6
6
|
import { ARIAAutoComplete, ARIAExpanded, ARIARole } from '../../types/role.js'
|
|
7
|
-
import { isDisabled, setDisabled } from '../../lib/disabled.js'
|
|
8
7
|
import { UiElement } from '../UiElement.js'
|
|
8
|
+
import { isDisabled, setDisabled } from '../../lib/disabled.js'
|
|
9
9
|
import '../icon-button/ui-icon-button.js'
|
|
10
10
|
import '../icons/ui-icon.js'
|
|
11
11
|
|
|
@@ -16,6 +16,7 @@ const floatTypes = ['date', 'color', 'datetime-local', 'file', 'month', 'time',
|
|
|
16
16
|
* @slot suffix - A slot in which to render suffixes of the input
|
|
17
17
|
*/
|
|
18
18
|
export default abstract class Input extends UiElement {
|
|
19
|
+
static override shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true }
|
|
19
20
|
#_userInteracted: boolean
|
|
20
21
|
|
|
21
22
|
// static override shadowRootOptions: ShadowRootInit = {mode: 'open', delegatesFocus: true};
|
|
@@ -33,28 +34,20 @@ export default abstract class Input extends UiElement {
|
|
|
33
34
|
*/
|
|
34
35
|
@property({ type: String, reflect: true }) accessor type: SupportedInputTypes
|
|
35
36
|
|
|
36
|
-
/**
|
|
37
|
-
* Disables the input.
|
|
38
|
-
* @attribute
|
|
39
|
-
*/
|
|
40
37
|
get disabled(): boolean {
|
|
41
38
|
return isDisabled(this)
|
|
42
39
|
}
|
|
43
40
|
|
|
41
|
+
/**
|
|
42
|
+
* When set, the input is a disabled state.
|
|
43
|
+
* @attribute
|
|
44
|
+
*/
|
|
44
45
|
@property({ reflect: true, type: Boolean })
|
|
45
46
|
set disabled(value: boolean) {
|
|
46
47
|
const old = isDisabled(this)
|
|
47
48
|
setDisabled(this, value)
|
|
48
49
|
this.requestUpdate('disabled', old)
|
|
49
50
|
this._updateFormValue()
|
|
50
|
-
if (value) {
|
|
51
|
-
this.dataset.tabindex = this.getAttribute('tabindex') || '0'
|
|
52
|
-
this.removeAttribute('tabindex')
|
|
53
|
-
} else {
|
|
54
|
-
const index = this.dataset.tabindex || '0'
|
|
55
|
-
this.setAttribute('tabindex', index)
|
|
56
|
-
this.blur()
|
|
57
|
-
}
|
|
58
51
|
}
|
|
59
52
|
|
|
60
53
|
/**
|
|
@@ -433,8 +426,6 @@ export default abstract class Input extends UiElement {
|
|
|
433
426
|
*/
|
|
434
427
|
#showPassword = false
|
|
435
428
|
|
|
436
|
-
protected removingTabindex = false
|
|
437
|
-
|
|
438
429
|
get files(): FileList | null {
|
|
439
430
|
return this.getNativeValue('files') as FileList | null
|
|
440
431
|
}
|
|
@@ -750,34 +741,14 @@ export default abstract class Input extends UiElement {
|
|
|
750
741
|
return
|
|
751
742
|
}
|
|
752
743
|
this.focused = true
|
|
753
|
-
this.removingTabindex = true
|
|
754
|
-
// Note, the input is automatically focused when form is being submitted and
|
|
755
|
-
// it won't pass validation. In this case the form will throw an error
|
|
756
|
-
// as the input is not focusable. In overall, this still works with forms and validation.
|
|
757
|
-
if (this.checkNativeValidity()) {
|
|
758
|
-
this.dataset.tabindex = this.getAttribute('tabindex') || '0'
|
|
759
|
-
this.removeAttribute('tabindex')
|
|
760
|
-
}
|
|
761
|
-
input.focus()
|
|
762
|
-
this.#_userInteracted = true
|
|
763
|
-
setTimeout(() => {
|
|
764
|
-
this.removingTabindex = false
|
|
765
|
-
}, 1)
|
|
766
744
|
}
|
|
767
745
|
|
|
768
746
|
protected handleBlur(): void {
|
|
769
|
-
if (this.matches(':focus-within')
|
|
747
|
+
if (this.matches(':focus-within')) {
|
|
770
748
|
// Changing focus to another child within the text field, like a button
|
|
771
749
|
return
|
|
772
750
|
}
|
|
773
751
|
this.focused = false
|
|
774
|
-
const index = this.dataset.tabindex || '0'
|
|
775
|
-
delete this.dataset.tabindex
|
|
776
|
-
this.setAttribute('tabindex', index)
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
private checkNativeValidity(): boolean {
|
|
780
|
-
return this.input?.validity.valid ?? true
|
|
781
752
|
}
|
|
782
753
|
|
|
783
754
|
private checkValidityAndDispatch(): { valid: boolean; canceled: boolean } {
|
|
@@ -28,6 +28,8 @@ export interface UiListItemsChange {
|
|
|
28
28
|
* The `event.detail` object contains the `item` and `index` properties.
|
|
29
29
|
* @fires itemschange - Dispatched when the list items change, e.g. when the slot changes.
|
|
30
30
|
* The `event.detail` object contains the `items` property with the list of items.
|
|
31
|
+
* @fires highlightchange - Dispatched when the highlighted item changes.
|
|
32
|
+
* The `event.detail` object contains the `items` property with the list of items.
|
|
31
33
|
*/
|
|
32
34
|
export default class UiList extends LitElement {
|
|
33
35
|
/**
|
|
@@ -311,15 +313,21 @@ export default class UiList extends LitElement {
|
|
|
311
313
|
this.activateFromEvent(event)
|
|
312
314
|
}
|
|
313
315
|
|
|
314
|
-
|
|
316
|
+
highlightItem(item?: UiListItem | null): void {
|
|
317
|
+
if (this.highlightListItem === item) {
|
|
318
|
+
return
|
|
319
|
+
}
|
|
315
320
|
if (this.highlightListItem) {
|
|
316
321
|
this.highlightListItem.classList.remove('highlight')
|
|
317
322
|
}
|
|
318
323
|
this.highlightListItem = item || null
|
|
319
324
|
if (this.highlightListItem) {
|
|
320
325
|
this.highlightListItem.classList.add('highlight')
|
|
321
|
-
this.highlightListItem.scrollIntoView({ block: '
|
|
326
|
+
this.highlightListItem.scrollIntoView({ block: 'nearest', inline: 'nearest', behavior: 'smooth' })
|
|
322
327
|
}
|
|
328
|
+
this.dispatchEvent(
|
|
329
|
+
new CustomEvent('highlightchange', { bubbles: false, composed: false, detail: { item: this.highlightListItem } })
|
|
330
|
+
)
|
|
323
331
|
}
|
|
324
332
|
|
|
325
333
|
/**
|
|
@@ -345,9 +353,8 @@ export default class UiList extends LitElement {
|
|
|
345
353
|
this.manageSelection(item)
|
|
346
354
|
item.activate()
|
|
347
355
|
this.activeListItem = item
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
}
|
|
356
|
+
e.preventDefault()
|
|
357
|
+
this.notifySelect(item)
|
|
351
358
|
}
|
|
352
359
|
|
|
353
360
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { html, PropertyValues, TemplateResult } from 'lit'
|
|
2
|
-
import { property, state
|
|
2
|
+
import { property, state } from 'lit/decorators.js'
|
|
3
3
|
import { classMap } from 'lit/directives/class-map.js'
|
|
4
4
|
import { nanoid } from 'nanoid'
|
|
5
5
|
import UiList from '../../list/internals/List.js'
|
|
@@ -35,11 +35,6 @@ export default class Menu extends UiList {
|
|
|
35
35
|
*/
|
|
36
36
|
@state() accessor activeSubMenu: UiSubMenu | null = null
|
|
37
37
|
|
|
38
|
-
/**
|
|
39
|
-
* Assigned menu items from light DOM
|
|
40
|
-
*/
|
|
41
|
-
@queryAssignedElements({ selector: 'ui-menu-item' }) protected accessor assignedMenuItems!: UiMenuItem[]
|
|
42
|
-
|
|
43
38
|
constructor() {
|
|
44
39
|
super()
|
|
45
40
|
this.selector = 'ui-menu-item'
|
|
@@ -79,13 +74,28 @@ export default class Menu extends UiList {
|
|
|
79
74
|
return result
|
|
80
75
|
}
|
|
81
76
|
|
|
77
|
+
protected queryMenuItems(): UiMenuItem[] {
|
|
78
|
+
const slot = this.shadowRoot?.querySelector('slot')
|
|
79
|
+
if (!slot) return []
|
|
80
|
+
const { selector } = this
|
|
81
|
+
return Array.from(slot.assignedElements({ flatten: true })).filter((el) => el.matches(selector)) as UiMenuItem[]
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
show(): void {
|
|
85
|
+
this.showPopover()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
hide(): void {
|
|
89
|
+
this.hidePopover()
|
|
90
|
+
}
|
|
91
|
+
|
|
82
92
|
/**
|
|
83
93
|
* Shows the menu
|
|
84
94
|
*/
|
|
85
|
-
|
|
95
|
+
override showPopover(): void {
|
|
86
96
|
this.tabIndex = 0 // Make menu focusable
|
|
87
97
|
this.ariaExpanded = 'true'
|
|
88
|
-
|
|
98
|
+
super.showPopover()
|
|
89
99
|
this.open = true
|
|
90
100
|
this.positionMenu()
|
|
91
101
|
this.focus()
|
|
@@ -95,10 +105,10 @@ export default class Menu extends UiList {
|
|
|
95
105
|
/**
|
|
96
106
|
* Hides the menu
|
|
97
107
|
*/
|
|
98
|
-
|
|
108
|
+
override hidePopover(): void {
|
|
99
109
|
this.tabIndex = -1
|
|
100
110
|
this.ariaExpanded = 'false'
|
|
101
|
-
|
|
111
|
+
super.hidePopover()
|
|
102
112
|
this.open = false
|
|
103
113
|
this.closeSubMenu()
|
|
104
114
|
this.dispatchEvent(new CustomEvent('close', { bubbles: false, composed: true }))
|
|
@@ -215,13 +225,10 @@ export default class Menu extends UiList {
|
|
|
215
225
|
subMenu?.addEventListener('select', this.handleSubMenuSelect as EventListener)
|
|
216
226
|
}
|
|
217
227
|
|
|
218
|
-
override notifySelect(item: UiListItem, index?: number): boolean {
|
|
228
|
+
override notifySelect(item: UiListItem & { selected?: boolean }, index?: number): boolean {
|
|
219
229
|
// Handle single selection
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
item.selected = true
|
|
223
|
-
}
|
|
224
|
-
|
|
230
|
+
this.clearSelection()
|
|
231
|
+
item.selected = true
|
|
225
232
|
this.hide()
|
|
226
233
|
return super.notifySelect(item, index)
|
|
227
234
|
}
|
|
@@ -230,7 +237,8 @@ export default class Menu extends UiList {
|
|
|
230
237
|
* Clears selection from all menu items
|
|
231
238
|
*/
|
|
232
239
|
protected clearSelection(): void {
|
|
233
|
-
this.
|
|
240
|
+
const items = this.queryMenuItems()
|
|
241
|
+
items.forEach((menuItem) => {
|
|
234
242
|
menuItem.selected = false
|
|
235
243
|
})
|
|
236
244
|
}
|
|
@@ -239,7 +247,8 @@ export default class Menu extends UiList {
|
|
|
239
247
|
* Gets the currently selected menu item
|
|
240
248
|
*/
|
|
241
249
|
get selectedItem(): UiMenuItem | null {
|
|
242
|
-
|
|
250
|
+
const items = this.queryMenuItems()
|
|
251
|
+
return items.find((item) => item.selected) || null
|
|
243
252
|
}
|
|
244
253
|
|
|
245
254
|
/**
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { css } from 'lit'
|
|
2
|
+
|
|
3
|
+
export default css`
|
|
4
|
+
:host {
|
|
5
|
+
display: block;
|
|
6
|
+
height: 56px;
|
|
7
|
+
outline: none;
|
|
8
|
+
cursor: default;
|
|
9
|
+
position: relative;
|
|
10
|
+
|
|
11
|
+
--md-focus-ring-shape-end-end: 0px;
|
|
12
|
+
--md-focus-ring-shape-end-start: 0px;
|
|
13
|
+
--md-focus-ring-shape-start-end: 0px;
|
|
14
|
+
--md-focus-ring-shape-start-start: 0px;
|
|
15
|
+
|
|
16
|
+
color: var(--md-sys-color-on-surface);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
:host([disabled]) {
|
|
20
|
+
cursor: not-allowed;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
:host([hidden]) {
|
|
24
|
+
display: none;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.surface {
|
|
28
|
+
height: inherit;
|
|
29
|
+
box-sizing: border-box;
|
|
30
|
+
|
|
31
|
+
display: flex;
|
|
32
|
+
align-items: center;
|
|
33
|
+
overflow: hidden;
|
|
34
|
+
|
|
35
|
+
padding: 8px 16px 8px 16px;
|
|
36
|
+
gap: 12px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.surface.selected {
|
|
40
|
+
background-color: var(--md-sys-color-secondary-container);
|
|
41
|
+
color: var(--md-sys-color-on-secondary-container);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.ripple {
|
|
45
|
+
z-index: 3;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.headline {
|
|
49
|
+
font-family: var(--md-sys-typescale-body-large-font);
|
|
50
|
+
font-weight: var(--md-sys-typescale-body-large-weight);
|
|
51
|
+
font-size: var(--md-sys-typescale-body-large-size);
|
|
52
|
+
letter-spacing: var(--md-sys-typescale-body-large-tracking);
|
|
53
|
+
line-height: var(--md-sys-typescale-body-large-height);
|
|
54
|
+
|
|
55
|
+
overflow: hidden;
|
|
56
|
+
text-overflow: ellipsis;
|
|
57
|
+
white-space: nowrap;
|
|
58
|
+
|
|
59
|
+
display: flex;
|
|
60
|
+
align-items: center;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.supporting-text {
|
|
64
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
65
|
+
|
|
66
|
+
font-family: var(--md-sys-typescale-body-medium-font);
|
|
67
|
+
font-weight: var(--md-sys-typescale-body-medium-weight);
|
|
68
|
+
font-size: var(--md-sys-typescale-body-medium-size);
|
|
69
|
+
letter-spacing: var(--md-sys-typescale-body-medium-tracking);
|
|
70
|
+
line-height: var(--md-sys-typescale-body-medium-height);
|
|
71
|
+
|
|
72
|
+
overflow: hidden;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.supporting-text ::slotted(*),
|
|
76
|
+
slot[name='end-text']::slotted(*) {
|
|
77
|
+
margin: 0;
|
|
78
|
+
padding: 0;
|
|
79
|
+
overflow: hidden;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
slot[name='end-text']::slotted(*) {
|
|
83
|
+
margin-right: 8px;
|
|
84
|
+
margin-left: 16px;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
:host slot[name='end']::slotted(*) {
|
|
88
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
89
|
+
fill: var(--md-sys-color-on-surface-variant);
|
|
90
|
+
margin-left: 16px;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
:host slot[name='start']::slotted(*) {
|
|
94
|
+
display: block;
|
|
95
|
+
margin-right: 16px;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
:host([image='icon']) slot[name='start']::slotted(*) {
|
|
99
|
+
width: 24px;
|
|
100
|
+
height: 24px;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
:host([image='avatar']) slot[name='start']::slotted(*) {
|
|
104
|
+
width: 40px;
|
|
105
|
+
height: 40px;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
:host([image='image']) slot[name='start']::slotted(*) {
|
|
109
|
+
width: 56px;
|
|
110
|
+
height: 56px;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
:host([image='video']) slot[name='start']::slotted(*) {
|
|
114
|
+
width: 114px;
|
|
115
|
+
height: 64px;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.body {
|
|
119
|
+
flex: 1;
|
|
120
|
+
overflow: hidden;
|
|
121
|
+
display: flex;
|
|
122
|
+
flex-direction: column;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.end {
|
|
126
|
+
display: flex;
|
|
127
|
+
align-items: center;
|
|
128
|
+
gap: 8px;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.selection-icon {
|
|
132
|
+
color: var(--md-sys-color-on-surface);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
:host(.highlight) .surface {
|
|
136
|
+
background-color: var(--md-sys-color-tertiary-container);
|
|
137
|
+
color: var(--md-sys-color-on-tertiary-container);
|
|
138
|
+
}
|
|
139
|
+
`
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { html, PropertyValues, TemplateResult } from 'lit'
|
|
2
|
+
import { property } from 'lit/decorators.js'
|
|
3
|
+
import { classMap } from 'lit/directives/class-map.js'
|
|
4
|
+
import UiListItem from '../../list/internals/ListItem.js'
|
|
5
|
+
import { nanoid } from 'nanoid'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Material Design 3 Select Option component.
|
|
9
|
+
*
|
|
10
|
+
* The `ui-option` component represents a selectable item within a `ui-select` dropdown.
|
|
11
|
+
* It extends `UiListItem` to provide consistent styling and behavior with other list components.
|
|
12
|
+
*
|
|
13
|
+
* @slot - The option content (label, icon, etc.)
|
|
14
|
+
* @slot start - Content to display at the start of the option (icon, avatar, etc.)
|
|
15
|
+
* @slot end - Content to display at the end of the option
|
|
16
|
+
* @slot supporting-text - Supporting text below the main content
|
|
17
|
+
* @fires select - Dispatched when the option is selected. Contains `{ item: UiOption, value: string }` in detail
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* Basic option
|
|
21
|
+
* ```html
|
|
22
|
+
* <ui-option value="apple">Apple</ui-option>
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* Option with supporting text
|
|
27
|
+
* ```html
|
|
28
|
+
* <ui-option value="john" lines="two">
|
|
29
|
+
* John Doe
|
|
30
|
+
* <span slot="supporting-text">john@example.com</span>
|
|
31
|
+
* </ui-option>
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export default class UiOption extends UiListItem {
|
|
35
|
+
/**
|
|
36
|
+
* The value associated with this option. Used to identify the option when selected.
|
|
37
|
+
* This value is what gets set on the parent select element when this option is chosen.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```html
|
|
41
|
+
* <ui-option value="apple">Apple</ui-option>
|
|
42
|
+
* <ui-option value="banana">Banana</ui-option>
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
@property({ type: String, reflect: true }) accessor value: string | undefined
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Whether this option is currently selected. This is typically managed automatically
|
|
49
|
+
* by the parent select component, but can be set manually for styling purposes.
|
|
50
|
+
*
|
|
51
|
+
* @default false
|
|
52
|
+
* @example
|
|
53
|
+
* ```html
|
|
54
|
+
* <ui-option value="apple" selected>Apple</ui-option>
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
@property({ type: Boolean, reflect: true }) accessor selected = false
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Returns the text representation of this option for display purposes.
|
|
61
|
+
* This method extracts and combines text content from all child nodes,
|
|
62
|
+
* including special handling for ui-icon elements.
|
|
63
|
+
*
|
|
64
|
+
* @readonly
|
|
65
|
+
* @returns {string} The rendered text value of the option
|
|
66
|
+
* @example
|
|
67
|
+
* ```javascript
|
|
68
|
+
* const option = document.querySelector('ui-option');
|
|
69
|
+
* console.log('Option text:', option.renderValue);
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
get renderValue(): string {
|
|
73
|
+
const slot = this.shadowRoot?.querySelector<HTMLSlotElement>('slot:not([name])')
|
|
74
|
+
if (!slot) return this.value || ''
|
|
75
|
+
const nodes = slot.assignedNodes({ flatten: true })
|
|
76
|
+
const content: string[] = []
|
|
77
|
+
for (const node of nodes) {
|
|
78
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
79
|
+
content.push(node.textContent || '')
|
|
80
|
+
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
81
|
+
const element = node as HTMLElement
|
|
82
|
+
if (element.tagName.toLowerCase() === 'ui-icon') {
|
|
83
|
+
content.push(element.getAttribute('icon') || '')
|
|
84
|
+
} else {
|
|
85
|
+
content.push(element.textContent || '')
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return content.join(' ').trim() || this.value || ''
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Initializes the option when it's connected to the DOM. Sets up ARIA attributes
|
|
94
|
+
* and generates a unique ID if one isn't provided.
|
|
95
|
+
*
|
|
96
|
+
* @protected
|
|
97
|
+
*/
|
|
98
|
+
override connectedCallback(): void {
|
|
99
|
+
super.connectedCallback()
|
|
100
|
+
this.setAttribute('role', 'option')
|
|
101
|
+
if (!this.id) {
|
|
102
|
+
this.id = `option-${nanoid(6)}`
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Handles property updates and triggers appropriate side effects.
|
|
108
|
+
* Currently monitors the `selected` property to update selection state.
|
|
109
|
+
*
|
|
110
|
+
* @param {PropertyValues<this>} changedProperties - Map of changed properties
|
|
111
|
+
* @protected
|
|
112
|
+
*/
|
|
113
|
+
protected override updated(changedProperties: PropertyValues<this>): void {
|
|
114
|
+
super.updated(changedProperties)
|
|
115
|
+
|
|
116
|
+
if (changedProperties.has('selected')) {
|
|
117
|
+
this.updateSelectionState()
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Updates the visual and accessibility state when selection changes.
|
|
123
|
+
* Adds/removes CSS classes and ARIA attributes based on selection state.
|
|
124
|
+
*
|
|
125
|
+
* @protected
|
|
126
|
+
* @example
|
|
127
|
+
* ```javascript
|
|
128
|
+
* // This is called automatically when the selected property changes
|
|
129
|
+
* option.selected = true; // Will trigger updateSelectionState()
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
protected updateSelectionState(): void {
|
|
133
|
+
if (this.selected) {
|
|
134
|
+
this.classList.add('selected')
|
|
135
|
+
this.setAttribute('aria-selected', 'true')
|
|
136
|
+
} else {
|
|
137
|
+
this.classList.remove('selected')
|
|
138
|
+
this.setAttribute('aria-selected', 'false')
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Handles click events on the option. Prevents default behavior and dispatches
|
|
144
|
+
* a custom 'select' event that the parent select component can listen to.
|
|
145
|
+
*
|
|
146
|
+
* @param {MouseEvent} e - The click event
|
|
147
|
+
* @protected
|
|
148
|
+
* @fires select - Custom event with option details in event.detail
|
|
149
|
+
* @example
|
|
150
|
+
* ```javascript
|
|
151
|
+
* // Listen for option selection
|
|
152
|
+
* option.addEventListener('select', (e) => {
|
|
153
|
+
* console.log('Selected option:', e.detail.item);
|
|
154
|
+
* console.log('Selected value:', e.detail.value);
|
|
155
|
+
* });
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
override handleClick(e: MouseEvent): void {
|
|
159
|
+
e.preventDefault()
|
|
160
|
+
if (this.disabled) {
|
|
161
|
+
e.stopPropagation()
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Let the parent handle the selection
|
|
166
|
+
this.dispatchEvent(
|
|
167
|
+
new CustomEvent('select', {
|
|
168
|
+
detail: {
|
|
169
|
+
item: this,
|
|
170
|
+
value: this.value,
|
|
171
|
+
},
|
|
172
|
+
bubbles: true,
|
|
173
|
+
composed: true,
|
|
174
|
+
})
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Renders the option's template. Creates the main structure with focus ring,
|
|
180
|
+
* ripple effect, and content areas.
|
|
181
|
+
*
|
|
182
|
+
* @returns {TemplateResult} The rendered template
|
|
183
|
+
* @protected
|
|
184
|
+
*/
|
|
185
|
+
protected override render(): TemplateResult {
|
|
186
|
+
const surfaceClasses = classMap({
|
|
187
|
+
surface: true,
|
|
188
|
+
selected: this.selected,
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
return html`
|
|
192
|
+
${this.renderFocusRing()} ${this.renderRipple()}
|
|
193
|
+
<div class="${surfaceClasses}">${this.renderStart()} ${this.renderBody()} ${this.renderEnd()}</div>
|
|
194
|
+
`
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Renders the end section of the option, including the selection check icon
|
|
199
|
+
* when the option is selected.
|
|
200
|
+
*/
|
|
201
|
+
protected override renderEnd(): TemplateResult {
|
|
202
|
+
return html`
|
|
203
|
+
<div class="end">
|
|
204
|
+
${this.selected ? html`<ui-icon icon="check" class="selection-icon"></ui-icon>` : ''}
|
|
205
|
+
<slot name="end" @slotchange=${this.handleSlotChange}></slot>
|
|
206
|
+
<slot name="end-text" class="trailing-supporting-text"></slot>
|
|
207
|
+
</div>
|
|
208
|
+
`
|
|
209
|
+
}
|
|
210
|
+
}
|