@spectrum-web-components/picker 0.33.3-overlay.61 → 0.34.0

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/src/Picker.dev.js CHANGED
@@ -13,25 +13,30 @@ var __decorateClass = (decorators, target, key, kind) => {
13
13
  import {
14
14
  html,
15
15
  nothing,
16
+ render,
16
17
  SizedMixin
17
18
  } from "@spectrum-web-components/base";
18
19
  import {
19
20
  classMap,
20
- styleMap
21
+ ifDefined
21
22
  } from "@spectrum-web-components/base/src/directives.js";
22
23
  import {
23
24
  property,
24
- query
25
+ query,
26
+ state
25
27
  } from "@spectrum-web-components/base/src/decorators.js";
26
28
  import pickerStyles from "./picker.css.js";
27
29
  import chevronStyles from "@spectrum-web-components/icon/src/spectrum-icon-chevron.css.js";
28
30
  import { Focusable } from "@spectrum-web-components/shared/src/focusable.js";
31
+ import { reparentChildren } from "@spectrum-web-components/shared/src/reparent-children.js";
29
32
  import "@spectrum-web-components/icons-ui/icons/sp-icon-chevron100.js";
30
33
  import "@spectrum-web-components/icons-workflow/icons/sp-icon-alert.js";
31
- import "@spectrum-web-components/overlay/sp-overlay.js";
32
34
  import "@spectrum-web-components/menu/sp-menu.js";
33
35
  import "@spectrum-web-components/tray/sp-tray.js";
34
36
  import "@spectrum-web-components/popover/sp-popover.js";
37
+ import {
38
+ openOverlay
39
+ } from "@spectrum-web-components/overlay";
35
40
  import {
36
41
  IS_MOBILE,
37
42
  MatchMediaController
@@ -52,12 +57,12 @@ export class PickerBase extends SizedMixin(Focusable) {
52
57
  this.open = false;
53
58
  this.readonly = false;
54
59
  this.selects = "single";
60
+ this.menuItems = [];
55
61
  this.placement = "bottom-start";
56
62
  this.quiet = false;
57
63
  this.value = "";
58
64
  this.listRole = "listbox";
59
65
  this.itemRole = "option";
60
- this.preventNextToggle = false;
61
66
  this.onKeydown = (event) => {
62
67
  this.focused = true;
63
68
  if (event.code !== "ArrowDown" && event.code !== "ArrowUp") {
@@ -66,34 +71,32 @@ export class PickerBase extends SizedMixin(Focusable) {
66
71
  event.preventDefault();
67
72
  this.toggle(true);
68
73
  };
69
- this.willManageSelection = false;
74
+ this.overlayOpenCallback = async () => {
75
+ this.updateMenuItems();
76
+ await this.itemsUpdated;
77
+ await this.optionsMenu.updateComplete;
78
+ requestAnimationFrame(() => this.menuStateResolver());
79
+ };
80
+ this.overlayCloseCallback = async () => {
81
+ if (this.restoreChildren) {
82
+ this.restoreChildren();
83
+ this.restoreChildren = void 0;
84
+ }
85
+ this.close();
86
+ requestAnimationFrame(() => this.menuStateResolver());
87
+ };
88
+ this.applyFocusElementLabel = (value) => {
89
+ this.appliedLabel = value;
90
+ };
91
+ this._willUpdateItems = false;
92
+ this.itemsUpdated = Promise.resolve();
93
+ this.menuStatePromise = Promise.resolve();
70
94
  this.selectionPromise = Promise.resolve();
71
- this.recentlyConnected = false;
72
95
  this.onKeydown = this.onKeydown.bind(this);
73
- this.addEventListener("focusout", (event) => {
74
- if (event.relatedTarget && this.contains(event.relatedTarget) || event.target !== this) {
75
- return;
76
- }
77
- this.open = false;
78
- });
79
96
  }
80
97
  get target() {
81
98
  return this.button;
82
99
  }
83
- get menuItems() {
84
- return this.optionsMenu.childItems;
85
- }
86
- get selectedItem() {
87
- return this._selectedItem;
88
- }
89
- set selectedItem(selectedItem) {
90
- this.selectedItemContent = selectedItem ? selectedItem.itemChildren : void 0;
91
- if (selectedItem === this.selectedItem)
92
- return;
93
- const oldSelectedItem = this.selectedItem;
94
- this._selectedItem = selectedItem;
95
- this.requestUpdate("selectedItem", oldSelectedItem);
96
- }
97
100
  get focusElement() {
98
101
  if (this.open) {
99
102
  return this.optionsMenu;
@@ -110,14 +113,8 @@ export class PickerBase extends SizedMixin(Focusable) {
110
113
  this.onKeydown
111
114
  );
112
115
  }
113
- handlePointerdown() {
114
- this.preventNextToggle = this.open;
115
- }
116
116
  onButtonClick() {
117
- if (!this.preventNextToggle) {
118
- this.toggle();
119
- }
120
- this.preventNextToggle = false;
117
+ this.toggle();
121
118
  }
122
119
  focus(options) {
123
120
  super.focus(options);
@@ -138,22 +135,20 @@ export class PickerBase extends SizedMixin(Focusable) {
138
135
  handleChange(event) {
139
136
  const target = event.target;
140
137
  const [selected] = target.selectedItems;
141
- event.stopPropagation();
142
138
  if (event.cancelable) {
139
+ event.stopPropagation();
143
140
  this.setValueFromItem(selected, event);
144
141
  } else {
145
142
  this.open = false;
146
143
  }
147
144
  }
148
145
  async setValueFromItem(item, menuChangeEvent) {
149
- this.open = false;
150
146
  const oldSelectedItem = this.selectedItem;
151
147
  const oldValue = this.value;
152
- if (this.selects) {
153
- this.selectedItem = item;
154
- this.value = item.value;
155
- await this.updateComplete;
156
- }
148
+ this.selectedItem = item;
149
+ this.value = item.value;
150
+ this.open = false;
151
+ await this.updateComplete;
157
152
  const applyDefault = this.dispatchEvent(
158
153
  new Event("change", {
159
154
  bubbles: true,
@@ -161,7 +156,7 @@ export class PickerBase extends SizedMixin(Focusable) {
161
156
  composed: true
162
157
  })
163
158
  );
164
- if (!applyDefault && this.selects) {
159
+ if (!applyDefault) {
165
160
  if (menuChangeEvent) {
166
161
  menuChangeEvent.preventDefault();
167
162
  }
@@ -196,30 +191,89 @@ export class PickerBase extends SizedMixin(Focusable) {
196
191
  }
197
192
  this.open = false;
198
193
  }
199
- get containerStyles() {
194
+ async generatePopover() {
195
+ if (!this.popoverFragment) {
196
+ this.popoverFragment = document.createDocumentFragment();
197
+ }
198
+ render(this.renderPopover, this.popoverFragment, { host: this });
199
+ this.popoverEl = this.popoverFragment.children[0];
200
+ this.optionsMenu = this.popoverEl.children[1];
201
+ }
202
+ async openMenu() {
203
+ let reparentableChildren = [];
204
+ const deprecatedMenu = this.querySelector(":scope > sp-menu");
205
+ await this.generatePopover();
206
+ if (deprecatedMenu) {
207
+ reparentableChildren = Array.from(deprecatedMenu.children);
208
+ } else {
209
+ reparentableChildren = Array.from(this.children).filter(
210
+ (element) => {
211
+ return !element.hasAttribute("slot");
212
+ }
213
+ );
214
+ }
215
+ if (reparentableChildren.length === 0) {
216
+ this.menuStateResolver();
217
+ return;
218
+ }
219
+ this.restoreChildren = reparentChildren(reparentableChildren, this.optionsMenu, {
220
+ position: "beforeend",
221
+ prepareCallback: (el) => {
222
+ if (this.value === el.value) {
223
+ this.setMenuItemSelected(el, true);
224
+ }
225
+ return (el2) => {
226
+ if (typeof el2.focused !== "undefined") {
227
+ el2.focused = false;
228
+ }
229
+ };
230
+ }
231
+ });
232
+ this.sizePopover(this.popoverEl);
233
+ if (true) {
234
+ window.__swc.ignoreWarningLevels.deprecation = true;
235
+ }
236
+ this.closeOverlay = Picker.openOverlay(this, "modal", this.popoverEl, {
237
+ placement: this.isMobile.matches ? "none" : this.placement,
238
+ receivesFocus: "auto"
239
+ });
240
+ if (true) {
241
+ window.__swc.ignoreWarningLevels.deprecation = false;
242
+ }
243
+ }
244
+ sizePopover(popover) {
200
245
  if (this.isMobile.matches) {
201
- return {
202
- "--swc-menu-width": "100%"
203
- };
246
+ popover.style.setProperty("--swc-menu-width", `100%`);
247
+ return;
204
248
  }
205
- return {};
206
249
  }
207
- get selectedItemContent() {
208
- return this._selectedItemContent || { icon: [], content: [] };
250
+ async closeMenu() {
251
+ if (this.closeOverlay) {
252
+ const closeOverlay = this.closeOverlay;
253
+ delete this.closeOverlay;
254
+ (await closeOverlay)();
255
+ }
209
256
  }
210
- set selectedItemContent(selectedItemContent) {
211
- if (selectedItemContent === this.selectedItemContent)
212
- return;
213
- const oldContent = this.selectedItemContent;
214
- this._selectedItemContent = selectedItemContent;
215
- this.requestUpdate("selectedItemContent", oldContent);
257
+ get selectedItemContent() {
258
+ if (this.selectedItem) {
259
+ return this.selectedItem.itemChildren;
260
+ }
261
+ return { icon: [], content: [] };
216
262
  }
217
263
  renderLabelContent(content) {
218
264
  if (this.value && this.selectedItem) {
219
265
  return content;
220
266
  }
221
267
  return html`
222
- <slot name="label">${this.label}</slot>
268
+ <slot name="label">
269
+ <span
270
+ aria-hidden=${ifDefined(
271
+ this.appliedLabel ? void 0 : "true"
272
+ )}
273
+ >
274
+ ${this.label}
275
+ </span>
276
+ </slot>
223
277
  `;
224
278
  }
225
279
  get buttonContent() {
@@ -227,74 +281,65 @@ export class PickerBase extends SizedMixin(Focusable) {
227
281
  "visually-hidden": this.icons === "only" && !!this.value,
228
282
  placeholder: !this.value
229
283
  };
284
+ const appliedLabel = this.appliedLabel || this.label;
230
285
  return [
231
286
  html`
287
+ </span>
232
288
  <span id="icon" ?hidden=${this.icons === "none"}>
233
289
  ${this.selectedItemContent.icon}
234
290
  </span>
235
291
  <span id="label" class=${classMap(labelClasses)}>
236
292
  ${this.renderLabelContent(this.selectedItemContent.content)}
237
293
  </span>
294
+ ${this.value && this.selectedItem ? html`
295
+ <span
296
+ aria-hidden="true"
297
+ class="visually-hidden"
298
+ id="applied-label"
299
+ >
300
+ ${appliedLabel}
301
+ <slot name="label"></slot>
302
+ </span>
303
+ ` : html`
304
+ <span hidden id="applied-label">
305
+ ${appliedLabel}
306
+ </span>
307
+ `}
238
308
  ${this.invalid ? html`
239
- <sp-icon-alert
240
- class="validation-icon"
241
- ></sp-icon-alert>
242
- ` : nothing}
309
+ <sp-icon-alert
310
+ class="validation-icon"
311
+ ></sp-icon-alert>
312
+ ` : nothing}
243
313
  <sp-icon-chevron100
244
314
  class="picker ${chevronClass[this.size]}"
245
315
  ></sp-icon-chevron100>
246
316
  `
247
317
  ];
248
318
  }
249
- get renderOverlay() {
250
- return html`
251
- <sp-overlay
252
- .triggerElement=${this}
253
- .offset=${0}
254
- ?open=${this.open}
255
- .placement=${this.placement}
256
- type="auto"
257
- .receivesFocus=${"true"}
258
- @beforetoggle=${(event) => {
259
- if (event.composedPath()[0] !== event.target) {
260
- return;
261
- }
262
- this.open = event.newState === "open";
263
- if (!this.open) {
264
- this.optionsMenu.updateSelectedItemIndex();
265
- this.optionsMenu.closeDescendentOverlays();
266
- }
267
- }}
268
- >
269
- ${this.renderContainer}
270
- </sp-overlay>
271
- `;
272
- }
273
319
  // a helper to throw focus to the button is needed because Safari
274
320
  // won't include buttons in the tab order even with tabindex="0"
275
321
  render() {
276
322
  return html`
277
323
  <span
278
324
  id="focus-helper"
279
- tabindex="${this.focused || this.open ? "-1" : "0"}"
325
+ tabindex="${this.focused ? "-1" : "0"}"
280
326
  @focus=${this.onHelperFocus}
281
327
  ></span>
282
328
  <button
283
329
  aria-haspopup="true"
330
+ aria-controls=${ifDefined(this.open ? "menu" : void 0)}
284
331
  aria-expanded=${this.open ? "true" : "false"}
285
- aria-labelledby="button icon label"
332
+ aria-labelledby="icon label applied-label"
286
333
  id="button"
287
334
  class="button"
288
335
  @blur=${this.onButtonBlur}
289
336
  @click=${this.onButtonClick}
290
337
  @focus=${this.onButtonFocus}
291
- @pointerdown=${this.handlePointerdown}
292
338
  ?disabled=${this.disabled}
293
339
  tabindex="-1"
294
340
  >
295
341
  ${this.buttonContent}
296
342
  </button>
297
- ${this.renderOverlay}
298
343
  `;
299
344
  }
300
345
  update(changes) {
@@ -304,15 +349,21 @@ export class PickerBase extends SizedMixin(Focusable) {
304
349
  if (changes.has("disabled") && this.disabled) {
305
350
  this.open = false;
306
351
  }
307
- if (changes.has("value")) {
308
- this.shouldScheduleManageSelection();
352
+ if (changes.has("open") && (this.open || typeof changes.get("open") !== "undefined")) {
353
+ this.menuStatePromise = new Promise(
354
+ (res) => this.menuStateResolver = res
355
+ );
356
+ if (this.open) {
357
+ this.openMenu();
358
+ } else {
359
+ this.closeMenu();
360
+ }
309
361
  }
310
- if (!this.hasUpdated) {
311
- const deprecatedMenu = this.querySelector(":scope > sp-menu");
312
- deprecatedMenu == null ? void 0 : deprecatedMenu.setAttribute("selects", "inherit");
362
+ if (changes.has("value") && !changes.has("selectedItem")) {
363
+ this.updateMenuItems();
313
364
  }
314
365
  if (true) {
315
- if (!this.hasUpdated && this.querySelector(":scope > sp-menu")) {
366
+ if (!this.hasUpdated && this.querySelector("sp-menu")) {
316
367
  const { localName } = this;
317
368
  window.__swc.warn(
318
369
  this,
@@ -335,7 +386,7 @@ export class PickerBase extends SizedMixin(Focusable) {
335
386
  </div>
336
387
  `;
337
388
  }
338
- get renderContainer() {
389
+ get renderPopover() {
339
390
  const content = html`
340
391
  ${this.dismissHelper}
341
392
  <sp-menu
@@ -343,19 +394,17 @@ export class PickerBase extends SizedMixin(Focusable) {
343
394
  role="${this.listRole}"
344
395
  @change=${this.handleChange}
345
396
  .selects=${this.selects}
346
- .selected=${this.value ? [this.value] : []}
347
- @sp-menu-item-added-or-updated=${this.shouldManageSelection}
348
- >
349
- <slot @slotchange=${this.shouldScheduleManageSelection}></slot>
350
- </sp-menu>
397
+ ></sp-menu>
351
398
  ${this.dismissHelper}
352
399
  `;
353
400
  if (this.isMobile.matches) {
354
401
  return html`
355
402
  <sp-tray
356
403
  id="popover"
357
- role="dialog"
358
- style=${styleMap(this.containerStyles)}
404
+ role="presentation"
405
+ @sp-menu-item-added-or-updated=${this.updateMenuItems}
406
+ .overlayOpenCallback=${this.overlayOpenCallback}
407
+ .overlayCloseCallback=${this.overlayCloseCallback}
359
408
  >
360
409
  ${content}
361
410
  </sp-tray>
@@ -364,43 +413,57 @@ export class PickerBase extends SizedMixin(Focusable) {
364
413
  return html`
365
414
  <sp-popover
366
415
  id="popover"
367
- role="dialog"
368
- style=${styleMap(this.containerStyles)}
369
- placement=${this.placement}
416
+ role="presentation"
417
+ @sp-menu-item-added-or-updated=${this.updateMenuItems}
418
+ .overlayOpenCallback=${this.overlayOpenCallback}
419
+ .overlayCloseCallback=${this.overlayCloseCallback}
370
420
  >
371
421
  ${content}
372
422
  </sp-popover>
373
423
  `;
374
424
  }
375
- shouldScheduleManageSelection(event) {
376
- if (!this.willManageSelection && (!event || event.target.getRootNode().host === this)) {
377
- this.willManageSelection = true;
378
- requestAnimationFrame(() => {
379
- requestAnimationFrame(() => {
380
- this.manageSelection();
381
- });
382
- });
383
- }
384
- }
385
- shouldManageSelection() {
386
- if (this.willManageSelection) {
425
+ /**
426
+ * Acquire the available MenuItems in the Picker by
427
+ * direct element query or by assuming the list managed
428
+ * by the Menu within the open options overlay.
429
+ */
430
+ updateMenuItems(event) {
431
+ if (this.open && (event == null ? void 0 : event.type) === "sp-menu-item-removed")
432
+ return;
433
+ if (this._willUpdateItems)
387
434
  return;
435
+ this._willUpdateItems = true;
436
+ if ((event == null ? void 0 : event.item) === this.selectedItem) {
437
+ this.requestUpdate();
388
438
  }
389
- this.willManageSelection = true;
390
- this.manageSelection();
439
+ let resolve = () => {
440
+ return;
441
+ };
442
+ this.itemsUpdated = new Promise((res) => resolve = res);
443
+ window.requestAnimationFrame(async () => {
444
+ if (this.open) {
445
+ await this.optionsMenu.updateComplete;
446
+ this.menuItems = this.optionsMenu.childItems;
447
+ } else {
448
+ this.menuItems = [
449
+ ...this.querySelectorAll(
450
+ 'sp-menu-item:not([slot="submenu"] *)'
451
+ )
452
+ ];
453
+ }
454
+ this.manageSelection();
455
+ resolve();
456
+ this._willUpdateItems = false;
457
+ });
391
458
  }
392
459
  async manageSelection() {
393
460
  if (this.selects == null)
394
461
  return;
462
+ await this.menuStatePromise;
395
463
  this.selectionPromise = new Promise(
396
464
  (res) => this.selectionResolver = res
397
465
  );
398
466
  let selectedItem;
399
- await this.optionsMenu.updateComplete;
400
- if (this.recentlyConnected) {
401
- await new Promise((res) => requestAnimationFrame(() => res(true)));
402
- this.recentlyConnected = false;
403
- }
404
467
  this.menuItems.forEach((item) => {
405
468
  if (this.value === item.value && !item.disabled) {
406
469
  selectedItem = item;
@@ -420,22 +483,37 @@ export class PickerBase extends SizedMixin(Focusable) {
420
483
  this.optionsMenu.updateSelectedItemIndex();
421
484
  }
422
485
  this.selectionResolver();
423
- this.willManageSelection = false;
424
486
  }
425
487
  async getUpdateComplete() {
426
488
  const complete = await super.getUpdateComplete();
489
+ await this.menuStatePromise;
490
+ await this.itemsUpdated;
427
491
  await this.selectionPromise;
428
492
  return complete;
429
493
  }
430
494
  connectedCallback() {
495
+ this.updateMenuItems();
496
+ this.addEventListener(
497
+ "sp-menu-item-added-or-updated",
498
+ this.updateMenuItems
499
+ );
500
+ this.addEventListener("sp-menu-item-removed", this.updateMenuItems);
431
501
  super.connectedCallback();
432
- this.recentlyConnected = this.hasUpdated;
433
502
  }
434
503
  disconnectedCallback() {
435
504
  this.close();
436
505
  super.disconnectedCallback();
437
506
  }
438
507
  }
508
+ /**
509
+ * @private
510
+ */
511
+ PickerBase.openOverlay = async (target, interaction, content, options) => {
512
+ return await openOverlay(target, interaction, content, options);
513
+ };
514
+ __decorateClass([
515
+ state()
516
+ ], PickerBase.prototype, "appliedLabel", 2);
439
517
  __decorateClass([
440
518
  query("#button")
441
519
  ], PickerBase.prototype, "button", 2);
@@ -460,9 +538,6 @@ __decorateClass([
460
538
  __decorateClass([
461
539
  property({ type: Boolean, reflect: true })
462
540
  ], PickerBase.prototype, "readonly", 2);
463
- __decorateClass([
464
- query("sp-menu")
465
- ], PickerBase.prototype, "optionsMenu", 2);
466
541
  __decorateClass([
467
542
  property()
468
543
  ], PickerBase.prototype, "placement", 2);
@@ -474,10 +549,7 @@ __decorateClass([
474
549
  ], PickerBase.prototype, "value", 2);
475
550
  __decorateClass([
476
551
  property({ attribute: false })
477
- ], PickerBase.prototype, "selectedItem", 1);
478
- __decorateClass([
479
- property({ attribute: false })
480
- ], PickerBase.prototype, "selectedItemContent", 1);
552
+ ], PickerBase.prototype, "selectedItem", 2);
481
553
  export class Picker extends PickerBase {
482
554
  constructor() {
483
555
  super(...arguments);
@@ -509,12 +581,11 @@ export class Picker extends PickerBase {
509
581
  static get styles() {
510
582
  return [pickerStyles, chevronStyles];
511
583
  }
512
- get containerStyles() {
513
- const styles = super.containerStyles;
514
- if (!this.quiet) {
515
- styles["min-width"] = `${this.offsetWidth}px`;
516
- }
517
- return styles;
584
+ sizePopover(popover) {
585
+ super.sizePopover(popover);
586
+ if (this.quiet)
587
+ return;
588
+ popover.style.setProperty("min-width", `${this.offsetWidth}px`);
518
589
  }
519
590
  }
520
591
  //# sourceMappingURL=Picker.dev.js.map