@api-client/ui 0.5.23 → 0.5.25
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/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 +10 -5
- 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 +39 -29
- 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 +66 -1
- package/build/src/md/menu/internal/Menu.styles.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/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/md/dropdown-list/internals/UiDropdownList.ts +4 -3
- package/src/md/input/Input.ts +8 -37
- package/src/md/list/internals/List.ts +13 -6
- package/src/md/menu/internal/Menu.styles.ts +66 -1
- package/src/md/menu/internal/Menu.ts +41 -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
|
@@ -29,9 +29,47 @@ export default css`
|
|
|
29
29
|
|
|
30
30
|
:host(:popover-open) {
|
|
31
31
|
display: block;
|
|
32
|
-
background-color: var(--md-sys-color-surface-container
|
|
32
|
+
background-color: var(--md-sys-color-surface-container);
|
|
33
33
|
border-radius: var(--md-sys-shape-corner-extra-small);
|
|
34
34
|
box-shadow: var(--md-sys-elevation-3);
|
|
35
|
+
animation: menu-scale-in 0.15s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* Scale animation for menus positioned below the anchor */
|
|
39
|
+
@keyframes menu-scale-in {
|
|
40
|
+
0% {
|
|
41
|
+
transform: scaleY(0);
|
|
42
|
+
transform-origin: top center;
|
|
43
|
+
opacity: 0;
|
|
44
|
+
}
|
|
45
|
+
100% {
|
|
46
|
+
transform: scaleY(1);
|
|
47
|
+
transform-origin: top center;
|
|
48
|
+
opacity: 1;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* Scale animation for menus positioned above the anchor */
|
|
53
|
+
@keyframes menu-scale-in-up {
|
|
54
|
+
0% {
|
|
55
|
+
transform: scaleY(0);
|
|
56
|
+
transform-origin: bottom center;
|
|
57
|
+
opacity: 0;
|
|
58
|
+
}
|
|
59
|
+
100% {
|
|
60
|
+
transform: scaleY(1);
|
|
61
|
+
transform-origin: bottom center;
|
|
62
|
+
opacity: 1;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* Position-specific animations using JavaScript-detected classes */
|
|
67
|
+
:host(.menu-positioned-above):popover-open {
|
|
68
|
+
animation: menu-scale-in-up 0.15s var(--md-sys-motion-easing-standard-accelerate) forwards;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
:host(.menu-positioned-below):popover-open {
|
|
72
|
+
animation: menu-scale-in 0.15s var(--md-sys-motion-easing-standard-accelerate) forwards;
|
|
35
73
|
}
|
|
36
74
|
|
|
37
75
|
.menu-container {
|
|
@@ -142,6 +180,21 @@ export default css`
|
|
|
142
180
|
max-width: 320px;
|
|
143
181
|
padding: 8px 0;
|
|
144
182
|
z-index: 1000;
|
|
183
|
+
animation: submenu-scale-in 0.12s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* Submenu scale animation */
|
|
187
|
+
@keyframes submenu-scale-in {
|
|
188
|
+
0% {
|
|
189
|
+
transform: scaleY(0) scaleX(0.8);
|
|
190
|
+
transform-origin: left top;
|
|
191
|
+
opacity: 0;
|
|
192
|
+
}
|
|
193
|
+
100% {
|
|
194
|
+
transform: scaleY(1) scaleX(1);
|
|
195
|
+
transform-origin: left top;
|
|
196
|
+
opacity: 1;
|
|
197
|
+
}
|
|
145
198
|
}
|
|
146
199
|
|
|
147
200
|
/* Fallback positioning for browsers without anchor positioning */
|
|
@@ -197,5 +250,17 @@ export default css`
|
|
|
197
250
|
.menu-item {
|
|
198
251
|
transition: none;
|
|
199
252
|
}
|
|
253
|
+
|
|
254
|
+
:host(:popover-open) {
|
|
255
|
+
animation: none;
|
|
256
|
+
opacity: 1;
|
|
257
|
+
transform: none;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
ui-sub-menu:popover-open {
|
|
261
|
+
animation: none;
|
|
262
|
+
opacity: 1;
|
|
263
|
+
transform: none;
|
|
264
|
+
}
|
|
200
265
|
}
|
|
201
266
|
`
|
|
@@ -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 }))
|
|
@@ -115,6 +125,20 @@ export default class Menu extends UiList {
|
|
|
115
125
|
this.style.removeProperty('position-area')
|
|
116
126
|
this.style.removeProperty('max-height')
|
|
117
127
|
|
|
128
|
+
// Detect if menu is positioned above or below the anchor
|
|
129
|
+
// by checking if the menu is in the upper or lower half of the viewport
|
|
130
|
+
const viewportMiddle = innerHeight / 2
|
|
131
|
+
const isMenuInUpperHalf = box.top < viewportMiddle
|
|
132
|
+
|
|
133
|
+
// Add CSS class to control animation direction
|
|
134
|
+
if (isMenuInUpperHalf) {
|
|
135
|
+
this.classList.add('menu-positioned-above')
|
|
136
|
+
this.classList.remove('menu-positioned-below')
|
|
137
|
+
} else {
|
|
138
|
+
this.classList.add('menu-positioned-below')
|
|
139
|
+
this.classList.remove('menu-positioned-above')
|
|
140
|
+
}
|
|
141
|
+
|
|
118
142
|
// Only set max-height if the menu would overflow
|
|
119
143
|
if (menuBottom > innerHeight) {
|
|
120
144
|
const availableHeight = innerHeight - box.top
|
|
@@ -201,13 +225,10 @@ export default class Menu extends UiList {
|
|
|
201
225
|
subMenu?.addEventListener('select', this.handleSubMenuSelect as EventListener)
|
|
202
226
|
}
|
|
203
227
|
|
|
204
|
-
override notifySelect(item: UiListItem, index?: number): boolean {
|
|
228
|
+
override notifySelect(item: UiListItem & { selected?: boolean }, index?: number): boolean {
|
|
205
229
|
// Handle single selection
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
item.selected = true
|
|
209
|
-
}
|
|
210
|
-
|
|
230
|
+
this.clearSelection()
|
|
231
|
+
item.selected = true
|
|
211
232
|
this.hide()
|
|
212
233
|
return super.notifySelect(item, index)
|
|
213
234
|
}
|
|
@@ -216,7 +237,8 @@ export default class Menu extends UiList {
|
|
|
216
237
|
* Clears selection from all menu items
|
|
217
238
|
*/
|
|
218
239
|
protected clearSelection(): void {
|
|
219
|
-
this.
|
|
240
|
+
const items = this.queryMenuItems()
|
|
241
|
+
items.forEach((menuItem) => {
|
|
220
242
|
menuItem.selected = false
|
|
221
243
|
})
|
|
222
244
|
}
|
|
@@ -225,7 +247,8 @@ export default class Menu extends UiList {
|
|
|
225
247
|
* Gets the currently selected menu item
|
|
226
248
|
*/
|
|
227
249
|
get selectedItem(): UiMenuItem | null {
|
|
228
|
-
|
|
250
|
+
const items = this.queryMenuItems()
|
|
251
|
+
return items.find((item) => item.selected) || null
|
|
229
252
|
}
|
|
230
253
|
|
|
231
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
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { css } from 'lit'
|
|
2
|
+
|
|
3
|
+
export default css`
|
|
4
|
+
:host {
|
|
5
|
+
display: inline-block;
|
|
6
|
+
position: relative;
|
|
7
|
+
outline: none;
|
|
8
|
+
--md-focus-ring-shape-end-end: var(--md-sys-shape-corner-extra-small);
|
|
9
|
+
--md-focus-ring-shape-end-start: var(--md-sys-shape-corner-extra-small);
|
|
10
|
+
--md-focus-ring-shape-start-end: var(--md-sys-shape-corner-extra-small);
|
|
11
|
+
--md-focus-ring-shape-start-start: var(--md-sys-shape-corner-extra-small);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.input {
|
|
15
|
+
anchor-name: --input;
|
|
16
|
+
cursor: default;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.menu {
|
|
20
|
+
position-anchor: --input;
|
|
21
|
+
}
|
|
22
|
+
`
|