@api-client/ui 0.5.38 → 0.5.39

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.
@@ -13,6 +13,7 @@ export enum ListItemLines {
13
13
  one = 'one',
14
14
  two = 'two',
15
15
  three = 'three',
16
+ auto = 'auto',
16
17
  }
17
18
 
18
19
  export enum ListItemImage {
@@ -24,28 +25,42 @@ export enum ListItemImage {
24
25
  }
25
26
 
26
27
  /**
27
- * @slot
28
- * @slot overline
29
- * @slot start
30
- * @slot end
31
- * @slot end-text
32
- * @slot supporting-text
28
+ * @slot - The main content of the list item, typically a label or title.
29
+ * @slot overline - The text displayed above the main content.
30
+ * @slot start - The content displayed at the start of the list item.
31
+ * @slot end - The content displayed at the end of the list item.
32
+ * @slot end-text - The text displayed at the end of the list item.
33
+ * @slot supporting-text - The supporting text displayed below the main content.
33
34
  */
34
35
  export default class UiListItem extends UiElement {
35
36
  @query('ui-ripple') accessor ripple!: UiRipple
36
37
 
38
+ #lines: ListItemLines = ListItemLines.one
39
+
40
+ get lines(): ListItemLines {
41
+ if (this.#lines === ListItemLines.auto) {
42
+ return this.hasOverline || this.hasSupportingText ? ListItemLines.two : ListItemLines.one
43
+ }
44
+ return this.#lines
45
+ }
46
+
37
47
  /**
38
- * Whether the chip is disabled. The user can't interact with the chip when `true`.
48
+ * The number of lines to render the list template for.
49
+ * @default ListItemLines.one
39
50
  * @attribute
40
51
  */
41
- @property({ type: Boolean, reflect: true }) accessor disabled = false
52
+ @property({ type: String, reflect: true })
53
+ set lines(value: ListItemLines) {
54
+ const oldValue = this.#lines
55
+ this.#lines = value
56
+ this.requestUpdate('lines', oldValue)
57
+ }
42
58
 
43
59
  /**
44
- * The number of lines to render the list template for.
45
- * @default ListItemLines.one
60
+ * Whether the chip is disabled. The user can't interact with the chip when `true`.
46
61
  * @attribute
47
62
  */
48
- @property({ type: String, reflect: true }) accessor lines: ListItemLines
63
+ @property({ type: Boolean, reflect: true }) accessor disabled = false
49
64
 
50
65
  /**
51
66
  * The type of the list image.
@@ -67,12 +82,14 @@ export default class UiListItem extends UiElement {
67
82
  @state() accessor hasStartItem = false
68
83
  @state() accessor hasEndItem = false
69
84
  @state() accessor hasEndTextItem = false
85
+ @state() accessor hasOverline = false
86
+ @state() accessor hasSupportingText = false
70
87
 
71
88
  constructor() {
72
89
  super()
73
90
 
74
- this.lines = ListItemLines.one
75
91
  this.image = ListItemImage.icon
92
+ this.lines = ListItemLines.one
76
93
 
77
94
  this.addEventListener('click', this.handleClick.bind(this))
78
95
  this.addEventListener('pointerdown', this.handlePointerDown.bind(this))
@@ -195,49 +212,57 @@ export default class UiListItem extends UiElement {
195
212
  this.removeAttribute('tabindex')
196
213
  }
197
214
 
198
- protected handleStartSlotChange(): void {
199
- const slot = this.shadowRoot?.querySelector<HTMLSlotElement>('slot[name="start"]')
200
- if (slot) {
201
- this.hasStartItem = slot.assignedNodes({ flatten: true }).length > 0
202
- } else {
203
- this.hasStartItem = false
204
- }
215
+ private _handleSlotChange(event: Event): boolean {
216
+ const slot = event.target as HTMLSlotElement
217
+ return slot.assignedNodes({ flatten: true }).length > 0
218
+ }
219
+
220
+ protected handleOverlineSlotChange(event: Event): void {
221
+ this.hasOverline = this._handleSlotChange(event)
205
222
  this.requestUpdate()
206
223
  }
207
224
 
208
- protected handleEndSlotChange(): void {
209
- const slot = this.shadowRoot?.querySelector<HTMLSlotElement>('slot[name="end"]')
210
- if (slot) {
211
- this.hasEndItem = slot.assignedNodes({ flatten: true }).length > 0
212
- } else {
213
- this.hasEndItem = false
214
- }
225
+ protected handleSupportingTextSlotChange(event: Event): void {
226
+ this.hasSupportingText = this._handleSlotChange(event)
215
227
  this.requestUpdate()
216
228
  }
217
229
 
218
- protected handleEndTextSlotChange(): void {
219
- const slot = this.shadowRoot?.querySelector<HTMLSlotElement>('slot[name="end-text"]')
220
- if (slot) {
221
- this.hasEndTextItem = slot.assignedNodes({ flatten: true }).length > 0
222
- } else {
223
- this.hasEndTextItem = false
224
- }
230
+ protected handleStartSlotChange(event: Event): void {
231
+ this.hasStartItem = this._handleSlotChange(event)
232
+ this.requestUpdate()
233
+ }
234
+
235
+ protected handleEndSlotChange(event: Event): void {
236
+ this.hasEndItem = this._handleSlotChange(event)
225
237
  this.requestUpdate()
226
238
  }
227
239
 
240
+ protected handleEndTextSlotChange(event: Event): void {
241
+ this.hasEndTextItem = this._handleSlotChange(event)
242
+ this.requestUpdate()
243
+ }
244
+
245
+ getSurfaceClasses(): ClassInfo {
246
+ const result: ClassInfo = {
247
+ 'surface': true,
248
+ 'one-line': this.lines === ListItemLines.one,
249
+ 'two-lines': this.lines === ListItemLines.two,
250
+ 'three-lines': this.lines === ListItemLines.three,
251
+ }
252
+ return result
253
+ }
254
+
228
255
  protected override render(): TemplateResult {
229
- const surfaceClasses = classMap({
230
- surface: true,
231
- })
232
256
  return html`
233
257
  ${this.renderFocusRing()} ${this.renderRipple()}
234
- <div class="${surfaceClasses}">${this.renderStart()} ${this.renderBody()} ${this.renderEnd()}</div>
258
+ <div class="${classMap(this.getSurfaceClasses())}">
259
+ ${this.renderStart()} ${this.renderBody()} ${this.renderEnd()}
260
+ </div>
235
261
  `
236
262
  }
237
263
 
238
264
  protected renderRipple = (): TemplateResult => {
239
- const { disabled, static: isStatic } = this
240
- return html`<ui-ripple class="ripple" ?disabled="${disabled || isStatic}"></ui-ripple>`
265
+ return html`<ui-ripple class="ripple" ?disabled="${this.disabled || this.static}"></ui-ripple>`
241
266
  }
242
267
 
243
268
  protected renderFocusRing(): TemplateResult {
@@ -278,13 +303,13 @@ export default class UiListItem extends UiElement {
278
303
  }
279
304
 
280
305
  protected renderBody(): TemplateResult {
281
- const { lines = ListItemLines.one } = this
282
- const hasSupportingText = lines !== ListItemLines.one
283
306
  return html`
284
307
  <div class="body">
285
- <slot name="overline"></slot>
308
+ <slot name="overline" @slotchange=${this.handleOverlineSlotChange}></slot>
286
309
  <span class="headline"><slot></slot></span>
287
- ${hasSupportingText ? html`<span class="supporting-text"><slot name="supporting-text"></slot></span>` : ''}
310
+ <span class="supporting-text"
311
+ ><slot name="supporting-text" @slotchange=${this.handleSupportingTextSlotChange}></slot
312
+ ></span>
288
313
  </div>
289
314
  `
290
315
  }
@@ -1,7 +1,7 @@
1
1
  import { html, PropertyValues, TemplateResult } from 'lit'
2
2
  import { property } from 'lit/decorators.js'
3
- import { classMap } from 'lit/directives/class-map.js'
4
- import UiListItem from '../../list/internals/ListItem.js'
3
+ import { ClassInfo, classMap } from 'lit/directives/class-map.js'
4
+ import UiListItem, { ListItemLines } from '../../list/internals/ListItem.js'
5
5
  import { nanoid } from 'nanoid'
6
6
 
7
7
  /**
@@ -89,11 +89,14 @@ export default class UiOption extends UiListItem {
89
89
  return content.join(' ').trim() || this.value || ''
90
90
  }
91
91
 
92
+ constructor() {
93
+ super()
94
+ this.lines = ListItemLines.auto
95
+ }
96
+
92
97
  /**
93
98
  * Initializes the option when it's connected to the DOM. Sets up ARIA attributes
94
99
  * and generates a unique ID if one isn't provided.
95
- *
96
- * @protected
97
100
  */
98
101
  override connectedCallback(): void {
99
102
  super.connectedCallback()
@@ -106,9 +109,6 @@ export default class UiOption extends UiListItem {
106
109
  /**
107
110
  * Handles property updates and triggers appropriate side effects.
108
111
  * Currently monitors the `selected` property to update selection state.
109
- *
110
- * @param {PropertyValues<this>} changedProperties - Map of changed properties
111
- * @protected
112
112
  */
113
113
  protected override updated(changedProperties: PropertyValues<this>): void {
114
114
  super.updated(changedProperties)
@@ -143,7 +143,7 @@ export default class UiOption extends UiListItem {
143
143
  * Handles click events on the option. Prevents default behavior and dispatches
144
144
  * a custom 'select' event that the parent select component can listen to.
145
145
  *
146
- * @param {MouseEvent} e - The click event
146
+ * @param e - The click event
147
147
  * @protected
148
148
  * @fires select - Custom event with option details in event.detail
149
149
  * @example
@@ -175,23 +175,11 @@ export default class UiOption extends UiListItem {
175
175
  )
176
176
  }
177
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,
178
+ override getSurfaceClasses(): ClassInfo {
179
+ return {
180
+ ...super.getSurfaceClasses(),
188
181
  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
- `
182
+ }
195
183
  }
196
184
 
197
185
  /**
@@ -200,10 +188,10 @@ export default class UiOption extends UiListItem {
200
188
  */
201
189
  protected override renderEnd(): TemplateResult {
202
190
  return html`
203
- <div class="end">
191
+ <div class="${classMap(this.getEndClasses())}">
204
192
  ${this.selected ? html`<ui-icon icon="check" class="selection-icon"></ui-icon>` : ''}
205
193
  <slot name="end" @slotchange=${this.handleEndSlotChange}></slot>
206
- <slot name="end-text" class="trailing-supporting-text"></slot>
194
+ <slot name="end-text" class="trailing-supporting-text" @slotchange=${this.handleEndTextSlotChange}></slot>
207
195
  </div>
208
196
  `
209
197
  }
@@ -354,12 +354,17 @@ export default class UiSelect extends LitElement {
354
354
 
355
355
  protected override firstUpdated(cp: PropertyValues): void {
356
356
  super.firstUpdated(cp)
357
- this.setCurrentOption()
358
- this.#internals.setFormValue(this.value ?? null)
357
+ this.updateComplete.then(() => {
358
+ this.setCurrentOption()
359
+ this.#internals.setFormValue(this.value ?? null)
360
+ // We need to update here as event with `value` and `selectedOption`
361
+ // already set, the `renderValue` might have incorrect value
362
+ // due to the DOM update.
363
+ this.requestUpdate()
364
+ })
359
365
  }
360
366
 
361
- protected async setCurrentOption(): Promise<void> {
362
- await this.updateComplete
367
+ protected setCurrentOption(): void {
363
368
  const options = this.querySelectorAll<UiOption>('ui-option')
364
369
  if (this.value) {
365
370
  this.selectedOption = Array.from(options).find((option) => option.value === this.value) || null
@@ -459,6 +464,11 @@ export default class UiSelect extends LitElement {
459
464
  if (this.disabled || e.defaultPrevented) return
460
465
  e.preventDefault()
461
466
  e.stopPropagation()
467
+ if (this.open && e.target === this) {
468
+ // If the select is already open and clicked again, close it
469
+ this.open = false
470
+ return
471
+ }
462
472
  this.open = true
463
473
  }
464
474
 
@@ -560,7 +570,7 @@ export default class UiSelect extends LitElement {
560
570
  // When options change, re-evaluate the current selection
561
571
  // only if we don't have an explicit value set
562
572
  if (!this.value) {
563
- await this.setCurrentOption()
573
+ this.setCurrentOption()
564
574
  this.requestUpdate()
565
575
  }
566
576
  }