`.
diff --git a/packages/picker/package.json b/packages/picker/package.json
index d023a7fa844..5d8011a37bf 100644
--- a/packages/picker/package.json
+++ b/packages/picker/package.json
@@ -74,6 +74,7 @@
"@spectrum-web-components/menu": "^0.41.2",
"@spectrum-web-components/overlay": "^0.41.2",
"@spectrum-web-components/popover": "^0.41.2",
+ "@spectrum-web-components/progress-circle": "^0.41.2",
"@spectrum-web-components/reactive-controllers": "^0.41.2",
"@spectrum-web-components/shared": "^0.41.2",
"@spectrum-web-components/tooltip": "^0.41.2",
diff --git a/packages/picker/src/Picker.ts b/packages/picker/src/Picker.ts
index 0707049bb97..61a0eb69ef6 100644
--- a/packages/picker/src/Picker.ts
+++ b/packages/picker/src/Picker.ts
@@ -24,6 +24,7 @@ import {
ifDefined,
StyleInfo,
styleMap,
+ when,
} from '@spectrum-web-components/base/src/directives.js';
import {
property,
@@ -83,6 +84,14 @@ export class PickerBase extends SizedMixin(Focusable, { noDefaultSize: true }) {
@property({ type: Boolean, reflect: true })
public invalid = false;
+ /** Whether the items are currently loading. */
+ @property({ type: Boolean, reflect: true })
+ public pending = false;
+
+ /** Defines a string value that labels the Picker while it is in pending state. */
+ @property({ type: String, attribute: 'pending-label' })
+ public pendingLabel = 'Pending';
+
@property()
public label?: string;
@@ -316,7 +325,7 @@ export class PickerBase extends SizedMixin(Focusable, { noDefaultSize: true }) {
}
public toggle(target?: boolean): void {
- if (this.readonly) {
+ if (this.readonly || this.pending) {
return;
}
this.open = typeof target !== 'undefined' ? target : !this.open;
@@ -440,13 +449,28 @@ export class PickerBase extends SizedMixin(Focusable, { noDefaultSize: true }) {
: html`
${appliedLabel}
`}
- ${this.invalid
+ ${this.invalid && !this.pending
? html`
`
: nothing}
+ ${when(this.pending, () => {
+ import(
+ '@spectrum-web-components/progress-circle/sp-progress-circle.js'
+ );
+ // aria-valuetext is a workaround for aria-valuenow being applied in Firefox even in indeterminate mode.
+ return html`
+
+ `;
+ })}
management,
// await the same here.
@@ -843,7 +870,7 @@ export class Picker extends PickerBase {
protected override handleKeydown = (event: KeyboardEvent): void => {
const { code } = event;
this.focused = true;
- if (!code.startsWith('Arrow') || this.readonly) {
+ if (!code.startsWith('Arrow') || this.readonly || this.pending) {
return;
}
if (code === 'ArrowUp' || code === 'ArrowDown') {
diff --git a/packages/picker/src/picker.css b/packages/picker/src/picker.css
index 25c04c92ee4..189442ed38d 100644
--- a/packages/picker/src/picker.css
+++ b/packages/picker/src/picker.css
@@ -89,7 +89,7 @@ sp-menu {
margin-inline-start: auto;
}
-:host([focused]:not([quiet])) #button .picker {
+:host([focused]:not([quiet], [pending])) #button .picker {
/* .spectrum-Picker-trigger.focus-ring .spectrum-Picker-icon */
color: var(
--spectrum-picker-icon-color-key-focus,
diff --git a/packages/picker/src/spectrum-config.js b/packages/picker/src/spectrum-config.js
index 95b8b72c4b8..19a48ec096b 100644
--- a/packages/picker/src/spectrum-config.js
+++ b/packages/picker/src/spectrum-config.js
@@ -32,6 +32,7 @@ const config = {
converter.classToId('spectrum-Picker', 'button'),
converter.classToAttribute('spectrum-Picker--quiet'),
converter.classToAttribute('is-disabled', 'disabled'),
+ converter.classToAttribute('is-loading', 'pending'),
converter.classToAttribute('is-invalid', 'invalid'),
converter.classToAttribute('is-open', 'open'),
converter.classToAttribute('is-focused', 'focused'),
@@ -51,6 +52,10 @@ const config = {
'label-inline'
),
converter.classToClass('spectrum-Menu-checkmark', 'checkmark'),
+ converter.classToClass(
+ 'spectrum-ProgressCircle',
+ 'progress-circle'
+ ),
converter.classToClass('is-placeholder', 'placeholder'),
converter.classToClass(
'spectrum-Picker-validationIcon',
diff --git a/packages/picker/src/spectrum-picker.css b/packages/picker/src/spectrum-picker.css
index 21a9cf48de5..67fa143db70 100644
--- a/packages/picker/src/spectrum-picker.css
+++ b/packages/picker/src/spectrum-picker.css
@@ -649,7 +649,7 @@ governing permissions and limitations under the License.
);
}
-#button.is-loading .picker {
+:host([pending]) #button .picker {
color: var(
--highcontrast-picker-content-color-disabled,
var(
@@ -848,7 +848,7 @@ governing permissions and limitations under the License.
}
.validation-icon,
-#button .spectrum-ProgressCircle {
+#button .progress-circle {
margin-inline-start: var(
--mod-picker-spacing-text-to-alert-icon-inline-start,
var(--spectrum-picker-spacing-text-to-alert-icon-inline-start)
@@ -872,7 +872,7 @@ governing permissions and limitations under the License.
);
}
-#button .spectrum-ProgressCircle {
+#button .progress-circle {
margin-block-start: calc(
var(
--mod-picker-spacing-top-to-progress-circle,
diff --git a/packages/picker/stories/args.ts b/packages/picker/stories/args.ts
index 8ff6b5f76db..e6470cc33da 100644
--- a/packages/picker/stories/args.ts
+++ b/packages/picker/stories/args.ts
@@ -27,6 +27,7 @@ export const argTypes = {
},
type: 'select',
},
+ options: ['s', 'm', 'l', 'xl'],
},
quiet: {
name: 'quiet',
diff --git a/packages/picker/stories/picker-pending.stories.ts b/packages/picker/stories/picker-pending.stories.ts
new file mode 100644
index 00000000000..710a8a8b2bb
--- /dev/null
+++ b/packages/picker/stories/picker-pending.stories.ts
@@ -0,0 +1,36 @@
+/*
+Copyright 2023 Adobe. All rights reserved.
+This file is licensed to you under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License. You may obtain a copy
+of the License at http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software distributed under
+the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+OF ANY KIND, either express or implied. See the License for the specific language
+governing permissions and limitations under the License.
+*/
+
+import { TemplateResult } from '@spectrum-web-components/base';
+import { argTypes } from './args';
+import { StoryArgs, Template } from './template';
+
+export default {
+ title: 'Picker/Pending',
+ component: 'sp-picker',
+ argTypes,
+ args: {
+ pending: true,
+ },
+};
+
+export const S = (args: StoryArgs): TemplateResult =>
+ Template({ ...args, size: 's' });
+
+export const M = (args: StoryArgs): TemplateResult =>
+ Template({ ...args, size: 'm' });
+
+export const L = (args: StoryArgs): TemplateResult =>
+ Template({ ...args, size: 'l' });
+
+export const XL = (args: StoryArgs): TemplateResult =>
+ Template({ ...args, size: 'xl' });
diff --git a/packages/picker/stories/picker-sizes.stories.ts b/packages/picker/stories/picker-sizes.stories.ts
index f7d97fff212..1f6a440b4e4 100644
--- a/packages/picker/stories/picker-sizes.stories.ts
+++ b/packages/picker/stories/picker-sizes.stories.ts
@@ -22,11 +22,35 @@ export default {
component: 'sp-picker',
argTypes: {
onChange: { action: 'change' },
+ invalid: {
+ name: 'invalid',
+ type: { name: 'boolean', required: false },
+ table: {
+ type: { summary: 'boolean' },
+ defaultValue: { summary: false },
+ },
+ control: {
+ type: 'boolean',
+ },
+ },
+ pending: {
+ name: 'pending',
+ type: { name: 'boolean', required: false },
+ table: {
+ type: { summary: 'boolean' },
+ defaultValue: { summary: false },
+ },
+ control: {
+ type: 'boolean',
+ },
+ },
},
};
type StoryArgs = {
onChange: (val: string) => void;
+ invalid: boolean;
+ pending: boolean;
open: false;
};
@@ -34,9 +58,13 @@ const picker = ({
onChange,
open,
size,
+ pending,
+ invalid,
}: {
onChange: (val: string) => void;
size: 's' | 'm' | 'l' | 'xl';
+ pending: boolean;
+ invalid: boolean;
open: boolean;
}): TemplateResult => {
return html`
@@ -51,6 +79,8 @@ const picker = ({
onChange(picker.value);
}}"
label="Select a Country with a very long label, too long, in fact"
+ ?pending="${pending}"
+ ?invalid="${invalid}"
?open=${open}
>
Deselect
diff --git a/packages/picker/stories/picker.stories.ts b/packages/picker/stories/picker.stories.ts
index 866bf373e72..f7806f02c4e 100644
--- a/packages/picker/stories/picker.stories.ts
+++ b/packages/picker/stories/picker.stories.ts
@@ -35,8 +35,10 @@ export default {
invalid: false,
open: false,
quiet: false,
+ pending: false,
},
argTypes: {
+ ...argTypes,
onChange: { action: 'change' },
open: {
name: 'open',
@@ -48,7 +50,17 @@ export default {
},
control: 'boolean',
},
- ...argTypes,
+ pending: {
+ name: 'pending',
+ type: { name: 'boolean', required: false },
+ table: {
+ type: { summary: 'boolean' },
+ defaultValue: { summary: false },
+ },
+ control: {
+ type: 'boolean',
+ },
+ },
},
};
@@ -85,6 +97,11 @@ disabled.args = {
disabled: true,
};
+export const invalid = (args: StoryArgs): TemplateResult => Template(args);
+invalid.args = {
+ invalid: true,
+};
+
export const tooltip = (args: StoryArgs): TemplateResult => {
const { open, ...rest } = args;
return html`
diff --git a/packages/picker/stories/template.ts b/packages/picker/stories/template.ts
index 322d10403f7..3c1a064a1fd 100644
--- a/packages/picker/stories/template.ts
+++ b/packages/picker/stories/template.ts
@@ -23,6 +23,7 @@ export interface StoryArgs {
invalid?: boolean;
open?: boolean;
quiet?: boolean;
+ pending?: boolean;
showText?: boolean;
onChange?: (val: string) => void;
[prop: string]: unknown;
diff --git a/packages/picker/test/index.ts b/packages/picker/test/index.ts
index e0244f52bdf..a70d84c4b1d 100644
--- a/packages/picker/test/index.ts
+++ b/packages/picker/test/index.ts
@@ -45,6 +45,7 @@ import {
slottedLabel,
tooltip,
} from '../stories/picker.stories.js';
+import { M as pending } from '../stories/picker-pending.stories.js';
import { sendMouse } from '../../../test/plugins/browser.js';
import {
ignoreResizeObserverLoopError,
@@ -1829,4 +1830,47 @@ export function runPickerTests(): void {
expect(this.el.open).to.be.false;
});
});
+ describe('pending', function () {
+ beforeEach(async function () {
+ const test = await fixture(html`
+ ${pending({ pending: true })}
+ `);
+ this.label = test.querySelector('sp-field-label') as FieldLabel;
+ this.el = test.querySelector('sp-picker') as Picker;
+ await elementUpdated(this.elel);
+ });
+ it('receives focus from an ``', async function () {
+ expect(this.el.focused).to.be.false;
+
+ this.label.click();
+ await elementUpdated(this.el);
+
+ expect(this.el.focused).to.be.true;
+ });
+ it('does not open from `click()`', async function () {
+ expect(this.el.open).to.be.false;
+
+ this.el.click();
+ await elementUpdated(this.el);
+
+ expect(this.el.open).to.be.false;
+ });
+ it('manages its "name" value in the accessibility tree when [pending]', async () => {
+ type NamedNode = { name: string; role: string };
+ const snapshot = (await a11ySnapshot(
+ {}
+ )) as unknown as NamedNode & {
+ children: NamedNode[];
+ };
+
+ expect(
+ findAccessibilityNode(
+ snapshot,
+ (node) =>
+ node.name ===
+ 'Pending Choose your neighborhood Where do you live?'
+ )
+ ).to.not.be.null;
+ });
+ });
}