@spectrum-web-components/picker 1.2.0-beta.17 → 1.2.0-beta.19

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.d.ts CHANGED
@@ -1,11 +1,11 @@
1
- import { CSSResultArray, PropertyValues, TemplateResult } from '@spectrum-web-components/base';
1
+ import { CSSResultArray, PropertyValues, SpectrumElement, TemplateResult } from '@spectrum-web-components/base';
2
2
  import { StyleInfo } from '@spectrum-web-components/base/src/directives.js';
3
- import { Focusable } from '@spectrum-web-components/shared/src/focusable.js';
4
3
  import type { Tooltip } from '@spectrum-web-components/tooltip';
5
4
  import '@spectrum-web-components/icons-ui/icons/sp-icon-chevron100.js';
6
5
  import '@spectrum-web-components/icons-workflow/icons/sp-icon-alert.js';
7
6
  import '@spectrum-web-components/menu/sp-menu.js';
8
7
  import type { Menu, MenuItem, MenuItemChildren } from '@spectrum-web-components/menu';
8
+ import type { MenuItemKeydownEvent } from '@spectrum-web-components/menu';
9
9
  import { Placement } from '@spectrum-web-components/overlay';
10
10
  import { MatchMediaController } from '@spectrum-web-components/reactive-controllers/src/MatchMedia.js';
11
11
  import { DependencyManagerController } from '@spectrum-web-components/reactive-controllers/src/DependencyManger.js';
@@ -16,11 +16,16 @@ import type { FieldLabel } from '@spectrum-web-components/field-label';
16
16
  import { DesktopController } from './DesktopController.js';
17
17
  import { MobileController } from './MobileController.js';
18
18
  export declare const DESCRIPTION_ID = "option-picker";
19
- declare const PickerBase_base: typeof Focusable & {
19
+ declare const PickerBase_base: typeof SpectrumElement & {
20
20
  new (...args: any[]): import("@spectrum-web-components/base").SizedElementInterface;
21
21
  prototype: import("@spectrum-web-components/base").SizedElementInterface;
22
22
  };
23
23
  export declare class PickerBase extends PickerBase_base {
24
+ static shadowRootOptions: {
25
+ delegatesFocus: boolean;
26
+ mode: ShadowRootMode;
27
+ slotAssignment?: SlotAssignmentMode | undefined;
28
+ };
24
29
  isMobile: MatchMediaController;
25
30
  strategy: DesktopController | MobileController;
26
31
  appliedLabel?: string;
@@ -48,7 +53,9 @@ export declare class PickerBase extends PickerBase_base {
48
53
  labelAlignment?: 'inline';
49
54
  protected get menuItems(): MenuItem[];
50
55
  optionsMenu: Menu;
51
- private _selfManageFocusElement;
56
+ /**
57
+ * @deprecated
58
+ * */
52
59
  get selfManageFocusElement(): boolean;
53
60
  overlayElement: Overlay;
54
61
  protected tooltipEl?: Tooltip;
@@ -73,12 +80,19 @@ export declare class PickerBase extends PickerBase_base {
73
80
  get focusElement(): HTMLElement;
74
81
  forceFocusVisible(): void;
75
82
  click(): void;
83
+ handleButtonClick(): void;
76
84
  handleButtonBlur(): void;
77
85
  focus(options?: FocusOptions): void;
86
+ /**
87
+ * @deprecated - Use `focus` instead.
88
+ */
78
89
  handleHelperFocus(): void;
90
+ handleFocus(): void;
79
91
  handleChange(event: Event): void;
80
92
  handleButtonFocus(event: FocusEvent): void;
93
+ protected handleEscape: (event: MenuItemKeydownEvent | KeyboardEvent) => void;
81
94
  protected handleKeydown: (event: KeyboardEvent) => void;
95
+ protected keyboardOpen(): Promise<void>;
82
96
  protected setValueFromItem(item: MenuItem, menuChangeEvent?: Event): Promise<void>;
83
97
  protected setMenuItemSelected(item: MenuItem, value: boolean): void;
84
98
  toggle(target?: boolean): void;
@@ -94,9 +108,12 @@ export declare class PickerBase extends PickerBase_base {
94
108
  protected renderLabelContent(content: Node[]): TemplateResult | Node[];
95
109
  protected get buttonContent(): TemplateResult[];
96
110
  applyFocusElementLabel: (value: string, labelElement: FieldLabel) => void;
111
+ protected hasAccessibleLabel(): boolean;
112
+ protected warnNoLabel(): void;
97
113
  protected renderOverlay(menu: TemplateResult): TemplateResult;
98
114
  protected get renderDescriptionSlot(): TemplateResult;
99
115
  protected render(): TemplateResult;
116
+ protected willUpdate(changes: PropertyValues<this>): void;
100
117
  protected update(changes: PropertyValues<this>): void;
101
118
  protected bindButtonKeydownListener(): void;
102
119
  protected updated(changes: PropertyValues<this>): void;
@@ -106,9 +123,22 @@ export declare class PickerBase extends PickerBase_base {
106
123
  protected hasRenderedOverlay: boolean;
107
124
  private onScroll;
108
125
  protected get renderMenu(): TemplateResult;
109
- private willManageSelection;
126
+ /**
127
+ * whether a selection change is already scheduled
128
+ */
129
+ willManageSelection: boolean;
130
+ /**
131
+ * when the value changes or the menu slot changes, manage the selection on the next frame, if not already scheduled
132
+ * @param event
133
+ */
110
134
  protected shouldScheduleManageSelection(event?: Event): void;
135
+ /**
136
+ * when an item is added or updated, manage the selection, if it's not already scheduled
137
+ */
111
138
  protected shouldManageSelection(): void;
139
+ /**
140
+ * updates menu selection based on value
141
+ */
112
142
  protected manageSelection(): Promise<void>;
113
143
  private selectionPromise;
114
144
  private selectionResolver;
package/src/Picker.dev.js CHANGED
@@ -13,7 +13,8 @@ import {
13
13
  html,
14
14
  nothing,
15
15
  render,
16
- SizedMixin
16
+ SizedMixin,
17
+ SpectrumElement
17
18
  } from "@spectrum-web-components/base";
18
19
  import {
19
20
  classMap,
@@ -27,7 +28,6 @@ import {
27
28
  } from "@spectrum-web-components/base/src/decorators.js";
28
29
  import pickerStyles from "./picker.css.js";
29
30
  import chevronStyles from "@spectrum-web-components/icon/src/spectrum-icon-chevron.css.js";
30
- import { Focusable } from "@spectrum-web-components/shared/src/focusable.js";
31
31
  import "@spectrum-web-components/icons-ui/icons/sp-icon-chevron100.js";
32
32
  import "@spectrum-web-components/icons-workflow/icons/sp-icon-alert.js";
33
33
  import "@spectrum-web-components/menu/sp-menu.js";
@@ -45,7 +45,9 @@ const chevronClass = {
45
45
  xl: "spectrum-UIIcon-ChevronDown300"
46
46
  };
47
47
  export const DESCRIPTION_ID = "option-picker";
48
- export class PickerBase extends SizedMixin(Focusable, { noDefaultSize: true }) {
48
+ export class PickerBase extends SizedMixin(SpectrumElement, {
49
+ noDefaultSize: true
50
+ }) {
49
51
  /**
50
52
  * Initializes the `PendingStateController` for the Picker component.
51
53
  * The `PendingStateController` manages the pending state of the Picker.
@@ -64,20 +66,32 @@ export class PickerBase extends SizedMixin(Focusable, { noDefaultSize: true }) {
64
66
  this.open = false;
65
67
  this.readonly = false;
66
68
  this.selects = "single";
67
- this._selfManageFocusElement = false;
68
69
  this.placement = "bottom-start";
69
70
  this.quiet = false;
70
71
  this.value = "";
71
72
  this.listRole = "listbox";
72
73
  this.itemRole = "option";
74
+ this.handleEscape = (event) => {
75
+ if (event.key === "Escape") {
76
+ event.stopPropagation();
77
+ event.preventDefault();
78
+ this.toggle(false);
79
+ }
80
+ };
73
81
  this.handleKeydown = (event) => {
74
82
  this.focused = true;
75
- if (event.code !== "ArrowDown" && event.code !== "ArrowUp") {
83
+ if (!["ArrowUp", "ArrowDown", "Enter", " ", "Escape"].includes(
84
+ event.key
85
+ )) {
86
+ return;
87
+ }
88
+ if (event.key === "Escape") {
89
+ this.handleEscape(event);
76
90
  return;
77
91
  }
78
92
  event.stopPropagation();
79
93
  event.preventDefault();
80
- this.toggle(true);
94
+ this.keyboardOpen();
81
95
  };
82
96
  this.handleSlottableRequest = (_event) => {
83
97
  };
@@ -86,12 +100,20 @@ export class PickerBase extends SizedMixin(Focusable, { noDefaultSize: true }) {
86
100
  this.labelAlignment = labelElement.sideAligned ? "inline" : void 0;
87
101
  };
88
102
  this.hasRenderedOverlay = false;
103
+ /**
104
+ * whether a selection change is already scheduled
105
+ */
89
106
  this.willManageSelection = false;
90
107
  this.selectionPromise = Promise.resolve();
91
108
  this.recentlyConnected = false;
92
109
  this.enterKeydownOn = null;
93
110
  this.handleEnterKeydown = (event) => {
94
- if (event.code !== "Enter") {
111
+ if (event.key !== "Enter") {
112
+ return;
113
+ }
114
+ const target = event == null ? void 0 : event.target;
115
+ if (!target.open && target.hasSubmenu) {
116
+ event.preventDefault();
95
117
  return;
96
118
  }
97
119
  if (this.enterKeydownOn) {
@@ -102,7 +124,7 @@ export class PickerBase extends SizedMixin(Focusable, { noDefaultSize: true }) {
102
124
  this.addEventListener(
103
125
  "keyup",
104
126
  async (keyupEvent) => {
105
- if (keyupEvent.code !== "Enter") {
127
+ if (keyupEvent.key !== "Enter") {
106
128
  return;
107
129
  }
108
130
  this.enterKeydownOn = null;
@@ -115,8 +137,11 @@ export class PickerBase extends SizedMixin(Focusable, { noDefaultSize: true }) {
115
137
  get menuItems() {
116
138
  return this.optionsMenu.childItems;
117
139
  }
140
+ /**
141
+ * @deprecated
142
+ * */
118
143
  get selfManageFocusElement() {
119
- return this._selfManageFocusElement;
144
+ return true;
120
145
  }
121
146
  get selectedItem() {
122
147
  return this._selectedItem;
@@ -140,7 +165,12 @@ export class PickerBase extends SizedMixin(Focusable, { noDefaultSize: true }) {
140
165
  }
141
166
  this.focused = true;
142
167
  }
168
+ // handled by interaction controller, desktop or mobile; this is only called with a programmatic this.click()
143
169
  click() {
170
+ this.toggle();
171
+ }
172
+ // pointer events handled by interaction controller, desktop or mobile; this is only called with a programmatic this.button.click()
173
+ handleButtonClick() {
144
174
  if (this.disabled) {
145
175
  return;
146
176
  }
@@ -150,15 +180,21 @@ export class PickerBase extends SizedMixin(Focusable, { noDefaultSize: true }) {
150
180
  this.focused = false;
151
181
  }
152
182
  focus(options) {
153
- super.focus(options);
154
- if (!this.disabled && this.focusElement) {
155
- this.focused = this.hasVisibleFocusInTree();
156
- }
183
+ var _a;
184
+ (_a = this.focusElement) == null ? void 0 : _a.focus(options);
157
185
  }
186
+ /**
187
+ * @deprecated - Use `focus` instead.
188
+ */
158
189
  handleHelperFocus() {
159
190
  this.focused = true;
160
191
  this.button.focus();
161
192
  }
193
+ handleFocus() {
194
+ if (!this.disabled && this.focusElement) {
195
+ this.focused = this.hasVisibleFocusInTree();
196
+ }
197
+ }
162
198
  handleChange(event) {
163
199
  if (this.strategy) {
164
200
  this.strategy.preventNextToggle = "no";
@@ -179,6 +215,9 @@ export class PickerBase extends SizedMixin(Focusable, { noDefaultSize: true }) {
179
215
  var _a;
180
216
  (_a = this.strategy) == null ? void 0 : _a.handleButtonFocus(event);
181
217
  }
218
+ async keyboardOpen() {
219
+ this.toggle(true);
220
+ }
182
221
  async setValueFromItem(item, menuChangeEvent) {
183
222
  var _a;
184
223
  this.open = false;
@@ -228,18 +267,25 @@ export class PickerBase extends SizedMixin(Focusable, { noDefaultSize: true }) {
228
267
  item.selected = value;
229
268
  }
230
269
  toggle(target) {
231
- if (this.readonly || this.pending) {
270
+ if (this.readonly || this.pending || this.disabled) {
232
271
  return;
233
272
  }
234
- this.open = typeof target !== "undefined" ? target : !this.open;
273
+ const open = typeof target !== "undefined" ? target : !this.open;
274
+ if (open && !this.open)
275
+ this.addEventListener(
276
+ "sp-opened",
277
+ () => {
278
+ var _a;
279
+ return (_a = this.optionsMenu) == null ? void 0 : _a.focusOnFirstSelectedItem();
280
+ },
281
+ {
282
+ once: true
283
+ }
284
+ );
285
+ this.open = open;
235
286
  if (this.strategy) {
236
287
  this.strategy.open = this.open;
237
288
  }
238
- if (this.open) {
239
- this._selfManageFocusElement = true;
240
- } else {
241
- this._selfManageFocusElement = false;
242
- }
243
289
  }
244
290
  close() {
245
291
  if (this.readonly) {
@@ -331,11 +377,33 @@ export class PickerBase extends SizedMixin(Focusable, { noDefaultSize: true }) {
331
377
  aria-hidden="true"
332
378
  name="tooltip"
333
379
  id="tooltip"
380
+ @keydown=${this.handleKeydown}
334
381
  @slotchange=${this.handleTooltipSlotchange}
335
382
  ></slot>
336
383
  `
337
384
  ];
338
385
  }
386
+ hasAccessibleLabel() {
387
+ var _a, _b, _c, _d, _e, _f, _g;
388
+ const slotContent = ((_a = this.querySelector('[slot="label"]')) == null ? void 0 : _a.textContent) && ((_c = (_b = this.querySelector('[slot="label"]')) == null ? void 0 : _b.textContent) == null ? void 0 : _c.trim()) !== "";
389
+ const slotAlt = ((_e = (_d = this.querySelector('[slot="label"]')) == null ? void 0 : _d.getAttribute("alt")) == null ? void 0 : _e.trim()) && ((_g = (_f = this.querySelector('[slot="label"]')) == null ? void 0 : _f.getAttribute("alt")) == null ? void 0 : _g.trim()) !== "";
390
+ return !!this.label || !!this.getAttribute("aria-label") || !!this.getAttribute("aria-labelledby") || !!this.appliedLabel || !!slotContent || !!slotAlt;
391
+ }
392
+ warnNoLabel() {
393
+ window.__swc.warn(
394
+ this,
395
+ `<${this.localName}> needs one of the following to be accessible:`,
396
+ "https://opensource.adobe.com/spectrum-web-components/components/picker/#accessibility",
397
+ {
398
+ type: "accessibility",
399
+ issues: [
400
+ `an <sp-field-label> element with a \`for\` attribute referencing the \`id\` of the \`<${this.localName}>\`, or`,
401
+ 'value supplied to the "label" attribute, which will be displayed visually as placeholder text, or',
402
+ 'text content supplied in a <span> with slot="label", which will also be displayed visually as placeholder text.'
403
+ ]
404
+ }
405
+ );
406
+ }
339
407
  renderOverlay(menu) {
340
408
  var _a, _b, _c;
341
409
  if (((_a = this.strategy) == null ? void 0 : _a.overlay) === void 0) {
@@ -361,15 +429,9 @@ export class PickerBase extends SizedMixin(Focusable, { noDefaultSize: true }) {
361
429
  this.tooltipEl.disabled = this.open;
362
430
  }
363
431
  return html`
364
- <span
365
- id="focus-helper"
366
- tabindex="${this.focused || this.open ? "-1" : "0"}"
367
- @focus=${this.handleHelperFocus}
368
- aria-describedby=${DESCRIPTION_ID}
369
- ></span>
370
432
  <button
371
433
  aria-controls=${ifDefined(this.open ? "menu" : void 0)}
372
- aria-describedby="tooltip"
434
+ aria-describedby="tooltip ${DESCRIPTION_ID}"
373
435
  aria-expanded=${this.open ? "true" : "false"}
374
436
  aria-haspopup="true"
375
437
  aria-labelledby="loader icon label applied-label"
@@ -377,35 +439,36 @@ export class PickerBase extends SizedMixin(Focusable, { noDefaultSize: true }) {
377
439
  class=${ifDefined(
378
440
  this.labelAlignment ? `label-${this.labelAlignment}` : void 0
379
441
  )}
442
+ @focus=${this.handleButtonFocus}
380
443
  @blur=${this.handleButtonBlur}
381
444
  @keydown=${{
382
445
  handleEvent: this.handleEnterKeydown,
383
446
  capture: true
384
447
  }}
385
448
  ?disabled=${this.disabled}
386
- tabindex="-1"
387
449
  >
388
450
  ${this.buttonContent}
389
451
  </button>
390
452
  ${this.renderMenu} ${this.renderDescriptionSlot}
391
453
  `;
392
454
  }
455
+ willUpdate(changes) {
456
+ super.willUpdate(changes);
457
+ if (changes.has("tabIndex") && !!this.tabIndex) {
458
+ this.button.tabIndex = this.tabIndex;
459
+ this.removeAttribute("tabindex");
460
+ }
461
+ }
393
462
  update(changes) {
394
463
  var _a, _b;
395
464
  if (this.selects) {
396
465
  this.selects = "single";
397
466
  }
398
467
  if (changes.has("disabled") && this.disabled) {
399
- if (this.strategy) {
400
- this.open = false;
401
- this.strategy.open = false;
402
- }
468
+ this.close();
403
469
  }
404
470
  if (changes.has("pending") && this.pending) {
405
- if (this.strategy) {
406
- this.open = false;
407
- this.strategy.open = false;
408
- }
471
+ this.close();
409
472
  }
410
473
  if (changes.has("value")) {
411
474
  this.shouldScheduleManageSelection();
@@ -428,20 +491,8 @@ export class PickerBase extends SizedMixin(Focusable, { noDefaultSize: true }) {
428
491
  this.updateComplete.then(async () => {
429
492
  await new Promise((res) => requestAnimationFrame(res));
430
493
  await new Promise((res) => requestAnimationFrame(res));
431
- if (!this.label && !this.getAttribute("aria-label") && !this.getAttribute("aria-labelledby") && !this.appliedLabel) {
432
- window.__swc.warn(
433
- this,
434
- `<${this.localName}> needs one of the following to be accessible:`,
435
- "https://opensource.adobe.com/spectrum-web-components/components/picker/#accessibility",
436
- {
437
- type: "accessibility",
438
- issues: [
439
- `an <sp-field-label> element with a \`for\` attribute referencing the \`id\` of the \`<${this.localName}>\`, or`,
440
- 'value supplied to the "label" attribute, which will be displayed visually as placeholder text, or',
441
- 'text content supplied in a <span> with slot="label", which will also be displayed visually as placeholder text.'
442
- ]
443
- }
444
- );
494
+ if (!this.hasAccessibleLabel()) {
495
+ this.warnNoLabel();
445
496
  }
446
497
  });
447
498
  }
@@ -525,6 +576,7 @@ export class PickerBase extends SizedMixin(Focusable, { noDefaultSize: true }) {
525
576
  .selects=${this.selects}
526
577
  .selected=${this.value ? [this.value] : []}
527
578
  size=${this.size}
579
+ @sp-menu-item-keydown=${this.handleEscape}
528
580
  @sp-menu-item-added-or-updated=${this.shouldManageSelection}
529
581
  >
530
582
  <slot @slotchange=${this.shouldScheduleManageSelection}></slot>
@@ -539,6 +591,10 @@ export class PickerBase extends SizedMixin(Focusable, { noDefaultSize: true }) {
539
591
  }
540
592
  return menu;
541
593
  }
594
+ /**
595
+ * when the value changes or the menu slot changes, manage the selection on the next frame, if not already scheduled
596
+ * @param event
597
+ */
542
598
  shouldScheduleManageSelection(event) {
543
599
  if (!this.willManageSelection && (!event || event.target.getRootNode().host === this)) {
544
600
  this.willManageSelection = true;
@@ -549,6 +605,9 @@ export class PickerBase extends SizedMixin(Focusable, { noDefaultSize: true }) {
549
605
  });
550
606
  }
551
607
  }
608
+ /**
609
+ * when an item is added or updated, manage the selection, if it's not already scheduled
610
+ */
552
611
  shouldManageSelection() {
553
612
  if (this.willManageSelection) {
554
613
  return;
@@ -556,6 +615,9 @@ export class PickerBase extends SizedMixin(Focusable, { noDefaultSize: true }) {
556
615
  this.willManageSelection = true;
557
616
  this.manageSelection();
558
617
  }
618
+ /**
619
+ * updates menu selection based on value
620
+ */
559
621
  async manageSelection() {
560
622
  if (this.selects == null) return;
561
623
  this.selectionPromise = new Promise(
@@ -605,6 +667,7 @@ export class PickerBase extends SizedMixin(Focusable, { noDefaultSize: true }) {
605
667
  connectedCallback() {
606
668
  super.connectedCallback();
607
669
  this.recentlyConnected = this.hasUpdated;
670
+ this.addEventListener("focus", this.handleFocus);
608
671
  }
609
672
  disconnectedCallback() {
610
673
  var _a;
@@ -613,6 +676,10 @@ export class PickerBase extends SizedMixin(Focusable, { noDefaultSize: true }) {
613
676
  super.disconnectedCallback();
614
677
  }
615
678
  }
679
+ PickerBase.shadowRootOptions = {
680
+ ...SpectrumElement.shadowRootOptions,
681
+ delegatesFocus: true
682
+ };
616
683
  __decorateClass([
617
684
  state()
618
685
  ], PickerBase.prototype, "appliedLabel", 2);
@@ -677,28 +744,38 @@ export class Picker extends PickerBase {
677
744
  constructor() {
678
745
  super(...arguments);
679
746
  this.handleKeydown = (event) => {
680
- const { code } = event;
747
+ var _a;
748
+ const { key } = event;
749
+ const handledKeys = [
750
+ "ArrowUp",
751
+ "ArrowDown",
752
+ "ArrowLeft",
753
+ "ArrowRight",
754
+ "Enter",
755
+ " ",
756
+ "Escape"
757
+ ].includes(key);
758
+ const openKeys = ["ArrowUp", "ArrowDown", "Enter", " "].includes(key);
681
759
  this.focused = true;
682
- if (!code.startsWith("Arrow") || this.readonly || this.pending) {
760
+ if ("Escape" === key) {
761
+ this.handleEscape(event);
683
762
  return;
684
763
  }
685
- if (code === "ArrowUp" || code === "ArrowDown") {
686
- this.toggle(true);
687
- event.preventDefault();
764
+ if (!handledKeys || this.readonly || this.pending) {
688
765
  return;
689
766
  }
690
- event.preventDefault();
691
- const selectedIndex = this.selectedItem ? this.menuItems.indexOf(this.selectedItem) : -1;
692
- const nextOffset = selectedIndex < 0 || code === "ArrowRight" ? 1 : -1;
693
- let nextIndex = selectedIndex + nextOffset;
694
- while (this.menuItems[nextIndex] && this.menuItems[nextIndex].disabled) {
695
- nextIndex += nextOffset;
696
- }
697
- if (!this.menuItems[nextIndex] || this.menuItems[nextIndex].disabled) {
767
+ if (openKeys) {
768
+ this.keyboardOpen();
769
+ event.preventDefault();
698
770
  return;
699
771
  }
700
- if (!this.value || nextIndex !== selectedIndex) {
701
- this.setValueFromItem(this.menuItems[nextIndex]);
772
+ event.preventDefault();
773
+ const nextItem = (_a = this.optionsMenu) == null ? void 0 : _a.getNeighboringFocusableElement(
774
+ this.selectedItem,
775
+ key === "ArrowLeft"
776
+ );
777
+ if (!this.value || nextItem !== this.selectedItem) {
778
+ if (!!nextItem) this.setValueFromItem(nextItem);
702
779
  }
703
780
  };
704
781
  }