@spectrum-web-components/picker 0.35.1-rc.43 → 0.36.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,12 +13,12 @@ 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
- ifDefined,
21
- styleMap
21
+ ifDefined
22
22
  } from "@spectrum-web-components/base/src/directives.js";
23
23
  import {
24
24
  property,
@@ -28,12 +28,15 @@ import {
28
28
  import pickerStyles from "./picker.css.js";
29
29
  import chevronStyles from "@spectrum-web-components/icon/src/spectrum-icon-chevron.css.js";
30
30
  import { Focusable } from "@spectrum-web-components/shared/src/focusable.js";
31
+ import { reparentChildren } from "@spectrum-web-components/shared/src/reparent-children.js";
31
32
  import "@spectrum-web-components/icons-ui/icons/sp-icon-chevron100.js";
32
33
  import "@spectrum-web-components/icons-workflow/icons/sp-icon-alert.js";
33
- import "@spectrum-web-components/overlay/sp-overlay.js";
34
34
  import "@spectrum-web-components/menu/sp-menu.js";
35
35
  import "@spectrum-web-components/tray/sp-tray.js";
36
36
  import "@spectrum-web-components/popover/sp-popover.js";
37
+ import {
38
+ openOverlay
39
+ } from "@spectrum-web-components/overlay";
37
40
  import {
38
41
  IS_MOBILE,
39
42
  MatchMediaController
@@ -54,13 +57,13 @@ export class PickerBase extends SizedMixin(Focusable) {
54
57
  this.open = false;
55
58
  this.readonly = false;
56
59
  this.selects = "single";
60
+ this.menuItems = [];
57
61
  this.placement = "bottom-start";
58
62
  this.quiet = false;
59
63
  this.value = "";
60
64
  this.listRole = "listbox";
61
65
  this.itemRole = "option";
62
- this.preventNextToggle = false;
63
- this.handleKeydown = (event) => {
66
+ this.onKeydown = (event) => {
64
67
  this.focused = true;
65
68
  if (event.code !== "ArrowDown" && event.code !== "ArrowUp") {
66
69
  return;
@@ -68,54 +71,31 @@ export class PickerBase extends SizedMixin(Focusable) {
68
71
  event.preventDefault();
69
72
  this.toggle(true);
70
73
  };
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
+ };
71
88
  this.applyFocusElementLabel = (value) => {
72
89
  this.appliedLabel = value;
73
90
  };
74
- this.willManageSelection = false;
91
+ this._willUpdateItems = false;
92
+ this.itemsUpdated = Promise.resolve();
93
+ this.menuStatePromise = Promise.resolve();
75
94
  this.selectionPromise = Promise.resolve();
76
- this.recentlyConnected = false;
77
- this.enterKeydownOn = null;
78
- this.handleEnterKeydown = (event) => {
79
- if (event.code !== "Enter") {
80
- return;
81
- }
82
- if (this.enterKeydownOn) {
83
- event.preventDefault();
84
- return;
85
- } else {
86
- this.addEventListener(
87
- "keyup",
88
- (keyupEvent) => {
89
- if (keyupEvent.code !== "Enter") {
90
- return;
91
- }
92
- this.enterKeydownOn = null;
93
- },
94
- { once: true }
95
- );
96
- }
97
- this.enterKeydownOn = this.enterKeydownOn || event.target;
98
- };
99
- this.addEventListener("focusout", (event) => {
100
- if (event.relatedTarget && this.contains(event.relatedTarget) || event.target !== this) {
101
- return;
102
- }
103
- this.open = false;
104
- });
95
+ this.onKeydown = this.onKeydown.bind(this);
105
96
  }
106
- get menuItems() {
107
- return this.optionsMenu.childItems;
108
- }
109
- get selectedItem() {
110
- return this._selectedItem;
111
- }
112
- set selectedItem(selectedItem) {
113
- this.selectedItemContent = selectedItem ? selectedItem.itemChildren : void 0;
114
- if (selectedItem === this.selectedItem)
115
- return;
116
- const oldSelectedItem = this.selectedItem;
117
- this._selectedItem = selectedItem;
118
- this.requestUpdate("selectedItem", oldSelectedItem);
97
+ get target() {
98
+ return this.button;
119
99
  }
120
100
  get focusElement() {
121
101
  if (this.open) {
@@ -126,20 +106,15 @@ export class PickerBase extends SizedMixin(Focusable) {
126
106
  forceFocusVisible() {
127
107
  this.focused = true;
128
108
  }
129
- handleButtonBlur() {
109
+ onButtonBlur() {
130
110
  this.focused = false;
111
+ this.target.removeEventListener(
112
+ "keydown",
113
+ this.onKeydown
114
+ );
131
115
  }
132
- handlePointerdown() {
133
- this.preventNextToggle = this.open;
134
- }
135
- handleButtonClick() {
136
- if (this.enterKeydownOn && this.enterKeydownOn !== this.button) {
137
- return;
138
- }
139
- if (!this.preventNextToggle) {
140
- this.toggle();
141
- }
142
- this.preventNextToggle = false;
116
+ onButtonClick() {
117
+ this.toggle();
143
118
  }
144
119
  focus(options) {
145
120
  super.focus(options);
@@ -147,36 +122,41 @@ export class PickerBase extends SizedMixin(Focusable) {
147
122
  this.focused = this.hasVisibleFocusInTree();
148
123
  }
149
124
  }
150
- handleHelperFocus() {
125
+ onHelperFocus() {
151
126
  this.focused = true;
152
127
  this.button.focus();
153
128
  }
129
+ onButtonFocus() {
130
+ this.target.addEventListener(
131
+ "keydown",
132
+ this.onKeydown
133
+ );
134
+ }
154
135
  handleChange(event) {
155
136
  const target = event.target;
156
137
  const [selected] = target.selectedItems;
157
- event.stopPropagation();
158
138
  if (event.cancelable) {
139
+ event.stopPropagation();
159
140
  this.setValueFromItem(selected, event);
160
141
  } else {
161
142
  this.open = false;
162
143
  }
163
144
  }
164
145
  async setValueFromItem(item, menuChangeEvent) {
165
- this.open = false;
166
146
  const oldSelectedItem = this.selectedItem;
167
147
  const oldValue = this.value;
168
148
  this.selectedItem = item;
169
149
  this.value = item.value;
150
+ this.open = false;
170
151
  await this.updateComplete;
171
152
  const applyDefault = this.dispatchEvent(
172
153
  new Event("change", {
173
154
  bubbles: true,
174
- // Allow it to be prevented.
175
155
  cancelable: true,
176
156
  composed: true
177
157
  })
178
158
  );
179
- if (!applyDefault && this.selects) {
159
+ if (!applyDefault) {
180
160
  if (menuChangeEvent) {
181
161
  menuChangeEvent.preventDefault();
182
162
  }
@@ -188,10 +168,6 @@ export class PickerBase extends SizedMixin(Focusable) {
188
168
  this.value = oldValue;
189
169
  this.open = true;
190
170
  return;
191
- } else if (!this.selects) {
192
- this.selectedItem = oldSelectedItem;
193
- this.value = oldValue;
194
- return;
195
171
  }
196
172
  if (oldSelectedItem) {
197
173
  this.setMenuItemSelected(oldSelectedItem, false);
@@ -215,23 +191,74 @@ export class PickerBase extends SizedMixin(Focusable) {
215
191
  }
216
192
  this.open = false;
217
193
  }
218
- 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) {
219
245
  if (this.isMobile.matches) {
220
- return {
221
- "--swc-menu-width": "100%"
222
- };
246
+ popover.style.setProperty("--swc-menu-width", `100%`);
247
+ return;
223
248
  }
224
- return {};
225
249
  }
226
- get selectedItemContent() {
227
- 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
+ }
228
256
  }
229
- set selectedItemContent(selectedItemContent) {
230
- if (selectedItemContent === this.selectedItemContent)
231
- return;
232
- const oldContent = this.selectedItemContent;
233
- this._selectedItemContent = selectedItemContent;
234
- this.requestUpdate("selectedItemContent", oldContent);
257
+ get selectedItemContent() {
258
+ if (this.selectedItem) {
259
+ return this.selectedItem.itemChildren;
260
+ }
261
+ return { icon: [], content: [] };
235
262
  }
236
263
  renderLabelContent(content) {
237
264
  if (this.value && this.selectedItem) {
@@ -289,38 +316,14 @@ export class PickerBase extends SizedMixin(Focusable) {
289
316
  `
290
317
  ];
291
318
  }
292
- get renderOverlay() {
293
- return html`
294
- <sp-overlay
295
- .triggerElement=${this}
296
- .offset=${0}
297
- ?open=${this.open}
298
- .placement=${this.placement}
299
- type="auto"
300
- .receivesFocus=${"true"}
301
- @beforetoggle=${(event) => {
302
- if (event.composedPath()[0] !== event.target) {
303
- return;
304
- }
305
- this.open = event.newState === "open";
306
- if (!this.open) {
307
- this.optionsMenu.updateSelectedItemIndex();
308
- this.optionsMenu.closeDescendentOverlays();
309
- }
310
- }}
311
- >
312
- ${this.renderContainer}
313
- </sp-overlay>
314
- `;
315
- }
316
319
  // a helper to throw focus to the button is needed because Safari
317
320
  // won't include buttons in the tab order even with tabindex="0"
318
321
  render() {
319
322
  return html`
320
323
  <span
321
324
  id="focus-helper"
322
- tabindex="${this.focused || this.open ? "-1" : "0"}"
323
- @focus=${this.handleHelperFocus}
325
+ tabindex="${this.focused ? "-1" : "0"}"
326
+ @focus=${this.onHelperFocus}
324
327
  ></span>
325
328
  <button
326
329
  aria-haspopup="true"
@@ -329,19 +332,14 @@ export class PickerBase extends SizedMixin(Focusable) {
329
332
  aria-labelledby="icon label applied-label"
330
333
  id="button"
331
334
  class="button"
332
- @blur=${this.handleButtonBlur}
333
- @click=${this.handleButtonClick}
334
- @keydown=${{
335
- handleEvent: this.handleEnterKeydown,
336
- capture: true
337
- }}
338
- @pointerdown=${this.handlePointerdown}
335
+ @blur=${this.onButtonBlur}
336
+ @click=${this.onButtonClick}
337
+ @focus=${this.onButtonFocus}
339
338
  ?disabled=${this.disabled}
340
339
  tabindex="-1"
341
340
  >
342
341
  ${this.buttonContent}
343
342
  </button>
344
- ${this.renderOverlay}
345
343
  `;
346
344
  }
347
345
  update(changes) {
@@ -351,15 +349,21 @@ export class PickerBase extends SizedMixin(Focusable) {
351
349
  if (changes.has("disabled") && this.disabled) {
352
350
  this.open = false;
353
351
  }
354
- if (changes.has("value")) {
355
- 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
+ }
356
361
  }
357
- if (!this.hasUpdated) {
358
- const deprecatedMenu = this.querySelector(":scope > sp-menu");
359
- deprecatedMenu == null ? void 0 : deprecatedMenu.setAttribute("selects", "inherit");
362
+ if (changes.has("value") && !changes.has("selectedItem")) {
363
+ this.updateMenuItems();
360
364
  }
361
365
  if (true) {
362
- if (!this.hasUpdated && this.querySelector(":scope > sp-menu")) {
366
+ if (!this.hasUpdated && this.querySelector("sp-menu")) {
363
367
  const { localName } = this;
364
368
  window.__swc.warn(
365
369
  this,
@@ -371,13 +375,6 @@ export class PickerBase extends SizedMixin(Focusable) {
371
375
  }
372
376
  super.update(changes);
373
377
  }
374
- bindButtonKeydownListener() {
375
- this.button.addEventListener("keydown", this.handleKeydown);
376
- }
377
- firstUpdated(changes) {
378
- super.firstUpdated(changes);
379
- this.bindButtonKeydownListener();
380
- }
381
378
  get dismissHelper() {
382
379
  return html`
383
380
  <div class="visually-hidden">
@@ -389,7 +386,7 @@ export class PickerBase extends SizedMixin(Focusable) {
389
386
  </div>
390
387
  `;
391
388
  }
392
- get renderContainer() {
389
+ get renderPopover() {
393
390
  const content = html`
394
391
  ${this.dismissHelper}
395
392
  <sp-menu
@@ -397,15 +394,8 @@ export class PickerBase extends SizedMixin(Focusable) {
397
394
  role="${this.listRole}"
398
395
  @change=${this.handleChange}
399
396
  .selects=${this.selects}
400
- .selected=${this.value ? [this.value] : []}
401
- @sp-menu-item-added-or-updated=${this.shouldManageSelection}
402
- @keydown=${{
403
- handleEvent: this.handleEnterKeydown,
404
- capture: true
405
- }}
406
- >
407
- <slot @slotchange=${this.shouldScheduleManageSelection}></slot>
408
- </sp-menu>
397
+ size=${this.size}
398
+ ></sp-menu>
409
399
  ${this.dismissHelper}
410
400
  `;
411
401
  if (this.isMobile.matches) {
@@ -413,7 +403,9 @@ export class PickerBase extends SizedMixin(Focusable) {
413
403
  <sp-tray
414
404
  id="popover"
415
405
  role="presentation"
416
- style=${styleMap(this.containerStyles)}
406
+ @sp-menu-item-added-or-updated=${this.updateMenuItems}
407
+ .overlayOpenCallback=${this.overlayOpenCallback}
408
+ .overlayCloseCallback=${this.overlayCloseCallback}
417
409
  >
418
410
  ${content}
419
411
  </sp-tray>
@@ -423,42 +415,56 @@ export class PickerBase extends SizedMixin(Focusable) {
423
415
  <sp-popover
424
416
  id="popover"
425
417
  role="presentation"
426
- style=${styleMap(this.containerStyles)}
427
- placement=${this.placement}
418
+ @sp-menu-item-added-or-updated=${this.updateMenuItems}
419
+ .overlayOpenCallback=${this.overlayOpenCallback}
420
+ .overlayCloseCallback=${this.overlayCloseCallback}
428
421
  >
429
422
  ${content}
430
423
  </sp-popover>
431
424
  `;
432
425
  }
433
- shouldScheduleManageSelection(event) {
434
- if (!this.willManageSelection && (!event || event.target.getRootNode().host === this)) {
435
- this.willManageSelection = true;
436
- requestAnimationFrame(() => {
437
- requestAnimationFrame(() => {
438
- this.manageSelection();
439
- });
440
- });
441
- }
442
- }
443
- shouldManageSelection() {
444
- if (this.willManageSelection) {
426
+ /**
427
+ * Acquire the available MenuItems in the Picker by
428
+ * direct element query or by assuming the list managed
429
+ * by the Menu within the open options overlay.
430
+ */
431
+ updateMenuItems(event) {
432
+ if (this.open && (event == null ? void 0 : event.type) === "sp-menu-item-removed")
445
433
  return;
434
+ if (this._willUpdateItems)
435
+ return;
436
+ this._willUpdateItems = true;
437
+ if ((event == null ? void 0 : event.item) === this.selectedItem) {
438
+ this.requestUpdate();
446
439
  }
447
- this.willManageSelection = true;
448
- this.manageSelection();
440
+ let resolve = () => {
441
+ return;
442
+ };
443
+ this.itemsUpdated = new Promise((res) => resolve = res);
444
+ window.requestAnimationFrame(async () => {
445
+ if (this.open) {
446
+ await this.optionsMenu.updateComplete;
447
+ this.menuItems = this.optionsMenu.childItems;
448
+ } else {
449
+ this.menuItems = [
450
+ ...this.querySelectorAll(
451
+ 'sp-menu-item:not([slot="submenu"] *)'
452
+ )
453
+ ];
454
+ }
455
+ this.manageSelection();
456
+ resolve();
457
+ this._willUpdateItems = false;
458
+ });
449
459
  }
450
460
  async manageSelection() {
451
461
  if (this.selects == null)
452
462
  return;
463
+ await this.menuStatePromise;
453
464
  this.selectionPromise = new Promise(
454
465
  (res) => this.selectionResolver = res
455
466
  );
456
467
  let selectedItem;
457
- await this.optionsMenu.updateComplete;
458
- if (this.recentlyConnected) {
459
- await new Promise((res) => requestAnimationFrame(() => res(true)));
460
- this.recentlyConnected = false;
461
- }
462
468
  this.menuItems.forEach((item) => {
463
469
  if (this.value === item.value && !item.disabled) {
464
470
  selectedItem = item;
@@ -478,22 +484,34 @@ export class PickerBase extends SizedMixin(Focusable) {
478
484
  this.optionsMenu.updateSelectedItemIndex();
479
485
  }
480
486
  this.selectionResolver();
481
- this.willManageSelection = false;
482
487
  }
483
488
  async getUpdateComplete() {
484
489
  const complete = await super.getUpdateComplete();
490
+ await this.menuStatePromise;
491
+ await this.itemsUpdated;
485
492
  await this.selectionPromise;
486
493
  return complete;
487
494
  }
488
495
  connectedCallback() {
496
+ this.updateMenuItems();
497
+ this.addEventListener(
498
+ "sp-menu-item-added-or-updated",
499
+ this.updateMenuItems
500
+ );
501
+ this.addEventListener("sp-menu-item-removed", this.updateMenuItems);
489
502
  super.connectedCallback();
490
- this.recentlyConnected = this.hasUpdated;
491
503
  }
492
504
  disconnectedCallback() {
493
505
  this.close();
494
506
  super.disconnectedCallback();
495
507
  }
496
508
  }
509
+ /**
510
+ * @private
511
+ */
512
+ PickerBase.openOverlay = async (target, interaction, content, options) => {
513
+ return await openOverlay(target, interaction, content, options);
514
+ };
497
515
  __decorateClass([
498
516
  state()
499
517
  ], PickerBase.prototype, "appliedLabel", 2);
@@ -521,9 +539,6 @@ __decorateClass([
521
539
  __decorateClass([
522
540
  property({ type: Boolean, reflect: true })
523
541
  ], PickerBase.prototype, "readonly", 2);
524
- __decorateClass([
525
- query("sp-menu")
526
- ], PickerBase.prototype, "optionsMenu", 2);
527
542
  __decorateClass([
528
543
  property()
529
544
  ], PickerBase.prototype, "placement", 2);
@@ -535,20 +550,13 @@ __decorateClass([
535
550
  ], PickerBase.prototype, "value", 2);
536
551
  __decorateClass([
537
552
  property({ attribute: false })
538
- ], PickerBase.prototype, "selectedItem", 1);
539
- __decorateClass([
540
- property({ attribute: false })
541
- ], PickerBase.prototype, "selectedItemContent", 1);
553
+ ], PickerBase.prototype, "selectedItem", 2);
542
554
  export class Picker extends PickerBase {
543
555
  constructor() {
544
556
  super(...arguments);
545
- this.handleKeydown = (event) => {
557
+ this.onKeydown = (event) => {
546
558
  const { code } = event;
547
559
  this.focused = true;
548
- if (code === "ArrowUp" || code === "ArrowDown") {
549
- this.toggle(true);
550
- return;
551
- }
552
560
  if (!code.startsWith("Arrow") || this.readonly) {
553
561
  return;
554
562
  }
@@ -574,12 +582,11 @@ export class Picker extends PickerBase {
574
582
  static get styles() {
575
583
  return [pickerStyles, chevronStyles];
576
584
  }
577
- get containerStyles() {
578
- const styles = super.containerStyles;
579
- if (!this.quiet) {
580
- styles["min-width"] = `${this.offsetWidth}px`;
581
- }
582
- return styles;
585
+ sizePopover(popover) {
586
+ super.sizePopover(popover);
587
+ if (this.quiet)
588
+ return;
589
+ popover.style.setProperty("min-width", `${this.offsetWidth}px`);
583
590
  }
584
591
  }
585
592
  //# sourceMappingURL=Picker.dev.js.map