` is now gone and `.spectrum-FieldButton` is no longer used. Instead, the outer tag is now `
` with the `.spectrum-Picker` classname.
+
+Additionally, `.spectrum-Picker` should not contain the `.spectrum-Popover` that it opens.
+
+In order to use a side label with a Picker, add the `spectrum-Picker--sideLabel` class to the Picker.
+
+#### Icon classname changes
+
+Each of the 3 possible icons now has its own specific classname:
+
+| Previous icon classname | Workflow icon classname |
+| ----------------------------- | --------------------------------- |
+| `.spectrum-Picker-icon` | `.spectrum-Picker-menuIcon` |
+| `.spectrum-Icon` (workflow) | `.spectrum-Picker-icon` |
+| `.spectrum-Icon` (validation) | `.spectrum-Picker-validationIcon` |
+
+#### `.is-selected` is now `.is-open`
+
+In order to more accurately reflect what's going on, you should add `.is-open` to `.spectrum-Picker` when the menu is shown.
+
+#### Change workflow icon size to medium
+
+If you use a `.spectrum-Picker-icon` in your markup, please replace `.spectrum-Icon--sizeS` with `.spectrum-Icon--sizeM`.
+
+#### T-shirt sizing
+
+Picker now supports t-shirt sizing and requires that you specify the size by adding a `.spectrum-Picker--size*` class.
+Using the classes `.spectrum-Picker .spectrum-Picker--sizeM` will get result in the previous default picker size.
+
+Also, use the correct icon size for chevron icons:
+
+| T-shirt Size | Icon Size |
+| ------------------------- | ------------------------------ |
+| `spectrum-Picker--sizeS` | `spectrum-css-icon-Chevron75` |
+| `spectrum-Picker--sizeM` | `spectrum-css-icon-Chevron100` |
+| `spectrum-Picker--sizeL` | `spectrum-css-icon-Chevron200` |
+| `spectrum-Picker--sizeXL` | `spectrum-css-icon-Chevron300` |
+
## 1.0.0-beta.3
diff --git a/components/picker/stories/picker.stories.js b/components/picker/stories/picker.stories.js
index 12a0100c8d2..a053583bc4f 100644
--- a/components/picker/stories/picker.stories.js
+++ b/components/picker/stories/picker.stories.js
@@ -1,11 +1,13 @@
import { WithDividers as MenuStories } from "@spectrum-css/menu/stories/menu.stories.js";
+import { Sizes } from "@spectrum-css/preview/decorators";
import { disableDefaultModes } from "@spectrum-css/preview/modes";
-import { isDisabled, isInvalid, isKeyboardFocused, isLoading, isOpen, isQuiet, size } from "@spectrum-css/preview/types";
+import { isActive, isDisabled, isHovered, isInvalid, isKeyboardFocused, isLoading, isOpen, isQuiet, size } from "@spectrum-css/preview/types";
import pkgJson from "../package.json";
import { PickerGroup } from "./picker.test.js";
+import { ClosedAndOpenTemplate, DisabledTemplate, Template } from "./template.js";
/**
- * A picker outlines a set of options for a user.
+ * The picker component (sometimes known as a "dropdown" or "select") allows users to choose from a list of options in a limited space. The list of options can change based on the context.
*/
export default {
title: "Picker",
@@ -14,6 +16,7 @@ export default {
size: size(["s", "m", "l", "xl"]),
label: {
name: "Label",
+ description: "The text for the field label",
type: { name: "string" },
table: {
type: { summary: "string" },
@@ -26,23 +29,55 @@ export default {
type: { name: "string" },
table: {
type: { summary: "string" },
- category: "Content",
+ category: "Component",
},
- options: ["top", "left"],
+ options: ["top", "side"],
control: { type: "select" },
},
withSwitch: {
- name: "Display with a switch component",
+ name: "Show switch component",
+ description: "Display a separate switch component after the picker. Helpful for testing alignment with the picker when using the side label.",
type: { name: "boolean" },
table: {
type: { summary: "boolean" },
- category: "Component",
+ category: "Advanced",
+ },
+ control: "boolean",
+ if: { arg: "labelPosition", eq: "side" },
+ },
+ showWorkflowIcon: {
+ name: "Show workflow icon",
+ description: "Display optional workflow icon before the value or placeholder",
+ type: { name: "boolean" },
+ table: {
+ type: { summary: "boolean" },
+ category: "Advanced",
},
control: "boolean",
- if: { arg: "labelPosition", eq: "left" },
},
placeholder: {
name: "Placeholder",
+ description: "The placeholder text prompts a user to select an option from the picker menu. It disappears once a user selects an option. This will not be displayed if the `value` control is set.",
+ type: { name: "string" },
+ table: {
+ type: { summary: "string" },
+ category: "Content",
+ },
+ control: { type: "text" },
+ },
+ currentValue: {
+ name: "Value",
+ description: "The value shows the option that a user has selected.",
+ type: { name: "string" },
+ table: {
+ type: { summary: "string" },
+ category: "Content",
+ },
+ control: { type: "text" },
+ },
+ helpText: {
+ name: "Help text",
+ description: "Optional help text that can be informational or an error message. Displays a separate help text component after the picker. For error messages, the invalid control must also be set to true.",
type: { name: "string" },
table: {
type: { summary: "string" },
@@ -56,39 +91,60 @@ export default {
isDisabled,
isLoading,
isInvalid,
- content: { table: { disable: true } },
+ isHovered,
+ isActive,
+ popoverContent: { table: { disable: true } },
},
args: {
rootClass: "spectrum-Picker",
size: "m",
label: "Country",
+ labelPosition: "top",
placeholder: "Select a country",
+ helpText: "",
+ currentValue: "",
+ showWorkflowIcon: false,
isQuiet: false,
isKeyboardFocused: false,
isLoading: false,
isDisabled: false,
isInvalid: false,
isOpen: false,
+ isHovered: false,
+ isActive: false,
withSwitch: false,
- content: [
+ popoverContent: [
(passthrough, context) => MenuStories({
...passthrough,
...MenuStories.args,
+ items: [
+ { label: "United States of America" },
+ { label: "India" },
+ { label: "Australia" },
+ { label: "Brazil" },
+ ],
}, context)
],
+ // Make sure container flex layout does not misalign sibling elements such as field label in Template()
+ wrapperStyles: {
+ display: "block",
+ },
},
parameters: {
- docs: {
- story: {
- height: "400px"
- }
- },
packageJson: pkgJson,
},
};
export const Default = PickerGroup.bind({});
Default.args = {};
+Default.tags = ["!autodocs"];
+Default.parameters = {
+ docs: {
+ story: {
+ height: "300px",
+ }
+ },
+};
// ********* VRT ONLY ********* //
export const WithForcedColors = PickerGroup.bind({});
@@ -100,3 +156,221 @@ WithForcedColors.parameters = {
},
};
WithForcedColors.args = {};
+
+// ********* DOCS ONLY ********* //
+
+/**
+ * The following example shows the picker with both a [field label](/docs/components-field-label--docs) and placeholder text.
+ * Pickers [should always have a label](https://spectrum.adobe.com/page/picker/#Usage-guidelines).
+ * The placeholder text can be displayed when it does not have a selected value.
+ */
+export const Standard = ClosedAndOpenTemplate.bind({});
+Standard.storyName = "Default";
+Standard.tags = ["!dev"];
+Standard.parameters = {
+ chromatic: { disableSnapshot: true },
+ docs: {
+ story: {
+ height: "300px",
+ }
+ },
+};
+
+/**
+ * This example shows the picker with a selected value.
+ * A picker can also have [help text](?path=/docs/components-help-text--docs) below the field to give extra context or instruction about what a user should select.
+ */
+export const SelectedValue = ClosedAndOpenTemplate.bind({});
+SelectedValue.storyName = "Default with value and help text";
+SelectedValue.args = {
+ currentValue: "United States of America",
+ helpText: "Additional field context",
+ popoverContent: [
+ (passthrough, context) => MenuStories({
+ ...passthrough,
+ ...MenuStories.args,
+ selectionMode: "single",
+ items: [
+ { label: "United States of America", isSelected: true },
+ { label: "India" },
+ { label: "Australia" },
+ { label: "Brazil" },
+ ],
+ }, context)
+ ],
+};
+SelectedValue.tags = ["!dev"];
+SelectedValue.parameters = {
+ chromatic: { disableSnapshot: true },
+ docs: {
+ story: {
+ height: "300px",
+ }
+ },
+};
+
+/**
+ * Pickers come in four different sizes: small, medium, large, and extra-large.
+ *
+ * At each of these sizes, the following chevron UI icon should be used:
+ *
+ * | Picker size | UI icon size |
+ * |----------------|--------------|
+ * | small | `Chevron75` |
+ * | medium | `Chevron100` |
+ * | large | `Chevron200` |
+ * | extra-large | `Chevron300` |
+ */
+export const Sizing = (args, context) => Sizes({
+ Template: Template,
+ withHeading: false,
+ withBorder: false,
+ direction: "column",
+ ...args,
+}, context);
+Sizing.args = {
+ popoverContent: [],
+ onclick: () => {},
+};
+Sizing.tags = ["!dev"];
+Sizing.parameters = {
+ chromatic: { disableSnapshot: true },
+};
+
+export const Disabled = DisabledTemplate.bind({});
+Disabled.tags = ["!dev"];
+Disabled.args = {
+ isDisabled: true,
+};
+Disabled.parameters = {
+ chromatic: { disableSnapshot: true },
+};
+
+/**
+ * A picker can be marked as having an error to show that a value needs to be entered in order to move forward or that a value that was entered is invalid.
+ * This example shows the optional error message within the help text area.
+ */
+export const Invalid = ClosedAndOpenTemplate.bind({});
+Invalid.storyName = "Invalid";
+Invalid.tags = ["!dev"];
+Invalid.args = {
+ isInvalid: true,
+ helpText: "Select a country.",
+};
+Invalid.parameters = {
+ chromatic: { disableSnapshot: true },
+ docs: {
+ story: {
+ height: "300px",
+ }
+ },
+};
+
+export const Loading = Template.bind({});
+Loading.tags = ["!dev"];
+Loading.args = {
+ isLoading: true,
+ placeholder: "Loading...",
+ popoverContent: [],
+ onclick: () => {},
+};
+Loading.parameters = {
+ chromatic: { disableSnapshot: true },
+};
+
+/**
+ * Quiet pickers have no visible background. This style works best when a clear layout (vertical stack, table, grid)
+ * makes it easy to parse the buttons. Too many quiet components in a small space can be hard to read.
+ */
+export const Quiet = ClosedAndOpenTemplate.bind({});
+Quiet.tags = ["!dev"];
+Quiet.args = {
+ isQuiet: true,
+};
+Quiet.parameters = {
+ chromatic: { disableSnapshot: true },
+ docs: {
+ story: {
+ height: "300px",
+ }
+ },
+};
+
+export const QuietDisabled = DisabledTemplate.bind({});
+QuietDisabled.storyName = "Quiet and disabled";
+QuietDisabled.tags = ["!dev"];
+QuietDisabled.args = {
+ isDisabled: true,
+ isQuiet: true,
+};
+QuietDisabled.parameters = {
+ chromatic: { disableSnapshot: true },
+};
+
+export const QuietInvalid = ClosedAndOpenTemplate.bind({});
+QuietInvalid.storyName = "Quiet and invalid";
+QuietInvalid.tags = ["!dev"];
+QuietInvalid.args = {
+ isInvalid: true,
+ isQuiet: true,
+ helpText: "Select a country.",
+};
+QuietInvalid.parameters = {
+ chromatic: { disableSnapshot: true },
+ docs: {
+ story: {
+ height: "300px",
+ }
+ },
+};
+
+/**
+ * The value and placeholder within the picker will truncate with an ellipsis when it is longer than the available horizontal space within the picker.
+ * The full text of the option can be shown in the menu.
+ */
+export const TextOverflow = Template.bind({});
+TextOverflow.storyName = "Text overflow";
+TextOverflow.tags = ["!dev"];
+TextOverflow.args = {
+ placeholder: "Some long text that will be cut off when displayed as the current value or placeholder",
+ popoverContent: [],
+ onclick: () => {},
+};
+TextOverflow.parameters = {
+ chromatic: { disableSnapshot: true },
+};
+
+/**
+ * A workflow icon can be displayed before the value or placeholder. The class `.spectrum-Picker-icon` should be used with the icon.
+ */
+export const WithWorkflowIcon = Template.bind({});
+WithWorkflowIcon.storyName = "With workflow icon";
+WithWorkflowIcon.tags = ["!dev"];
+WithWorkflowIcon.args = {
+ showWorkflowIcon: true,
+ popoverContent: [],
+ onclick: () => {},
+};
+WithWorkflowIcon.parameters = {
+ chromatic: { disableSnapshot: true },
+};
+
+/**
+ * Labels can be placed either on top or on the side. Top labels are the default and are recommended because
+ * they work better with long copy, localization, and responsive layouts. Side labels are most useful when vertical
+ * space is limited.
+ *
+ * When using the side label, the `spectrum-Picker--sideLabel` class is added to the Picker.
+ */
+export const WithSideLabel = Template.bind({});
+WithSideLabel.storyName = "With side label";
+WithSideLabel.tags = ["!dev"];
+WithSideLabel.args = {
+ labelPosition: "side",
+ label: "Country",
+ popoverContent: [],
+ onclick: () => {},
+};
+WithSideLabel.parameters = {
+ chromatic: { disableSnapshot: true },
+};
\ No newline at end of file
diff --git a/components/picker/stories/picker.test.js b/components/picker/stories/picker.test.js
index c9ba6b44410..8d7e90b486e 100644
--- a/components/picker/stories/picker.test.js
+++ b/components/picker/stories/picker.test.js
@@ -9,24 +9,89 @@ export const PickerGroup = Variants({
},
testData: [
{
- testHeading: "Default",
+ testHeading: "Default, with placeholder",
},
{
- testHeading: "Explicit",
- variant: "explicit",
+ testHeading: "Default, with value and text overflow",
+ currentValue: "The selected value of the picker, with long text the triggers the overflow behavior with ellipsis",
},
{
- testHeading: "Button",
- variant: "button",
+ testHeading: "Quiet",
+ isQuiet: true,
+ },
+ {
+ testHeading: "Side label",
+ labelPosition: "side",
+ },
+ {
+ testHeading: "Side label, alignment with switch",
+ labelPosition: "side",
+ withSwitch: true,
+ },
+ {
+ testHeading: "With thumbnail icon",
+ showWorkflowIcon: true,
},
],
stateData: [
+ {
+ testHeading: "Hovered",
+ isHovered: true,
+ },
+ {
+ testHeading: "Active",
+ isActive: true,
+ },
+ {
+ testHeading: "Keyboard focused",
+ isKeyboardFocused: true,
+ },
+ {
+ testHeading: "Invalid",
+ isInvalid: true,
+ },
+ {
+ testHeading: "Invalid + hovered",
+ isInvalid: true,
+ isHovered: true,
+ },
+ {
+ testHeading: "Loading",
+ isLoading: true,
+ },
+ {
+ testHeading: "Loading + hovered",
+ isLoading: true,
+ isHovered: true,
+ },
+ {
+ testHeading: "Disabled",
+ isDisabled: true,
+ },
+ {
+ testHeading: "Disabled + hovered",
+ isDisabled: true,
+ isHovered: true,
+ },
+ {
+ testHeading: "Disabled + invalid",
+ isInvalid: true,
+ isDisabled: true,
+ },
{
testHeading: "Open",
isOpen: true,
wrapperStyles: {
- "min-block-size": "250px",
+ "min-block-size": "225px",
+ },
+ },
+ {
+ testHeading: "Open + hover",
+ isOpen: true,
+ isHovered: true,
+ wrapperStyles: {
+ "min-block-size": "225px",
},
- }
- ]
+ },
+ ],
});
diff --git a/components/picker/stories/template.js b/components/picker/stories/template.js
index 25efac5026c..a356b4aa44a 100644
--- a/components/picker/stories/template.js
+++ b/components/picker/stories/template.js
@@ -2,7 +2,7 @@ import { Template as FieldLabel } from "@spectrum-css/fieldlabel/stories/templat
import { Template as HelpText } from "@spectrum-css/helptext/stories/template.js";
import { Template as Icon } from "@spectrum-css/icon/stories/template.js";
import { Template as Popover } from "@spectrum-css/popover/stories/template.js";
-import { getRandomId } from "@spectrum-css/preview/decorators";
+import { Container, getRandomId } from "@spectrum-css/preview/decorators";
import { Template as ProgressCircle } from "@spectrum-css/progresscircle/stories/template.js";
import { Template as Switch } from "@spectrum-css/switch/stories/template.js";
import { html } from "lit";
@@ -12,17 +12,24 @@ import { when } from "lit/directives/when.js";
import "../index.css";
+/**
+ * Template for Picker only (no popover or help text).
+ */
export const Picker = ({
rootClass = "spectrum-Picker",
size = "m",
labelPosition,
- placeholder,
+ placeholder = "",
+ currentValue = "",
isQuiet = false,
isKeyboardFocused = false,
+ showWorkflowIcon = false,
isOpen = false,
isInvalid = false,
isLoading = false,
isDisabled = false,
+ isHovered = false,
+ isActive = false,
customClasses = [],
customStyles = {},
onclick,
@@ -38,6 +45,8 @@ export const Picker = ({
["is-invalid"]: isInvalid,
["is-open"]: isOpen,
["is-loading"]: isLoading,
+ ["is-hover"]: isHovered,
+ ["is-active"]: isActive,
["is-keyboardFocused"]: isKeyboardFocused,
...customClasses.reduce((a, c) => ({ ...a, [c]: true }), {}),
})}
@@ -47,7 +56,20 @@ export const Picker = ({
type="button"
@click=${onclick}
>
- ${placeholder}
+ ${when(showWorkflowIcon, () =>
+ Icon({
+ size,
+ setName: "workflow",
+ iconName: "Image",
+ customClasses: [`${rootClass}-icon`],
+ }, context)
+ )}
+ ${currentValue ? currentValue : placeholder}
${when(isLoading, () =>
ProgressCircle({
size: "s",
@@ -64,52 +86,49 @@ export const Picker = ({
)}
${Icon({
size,
- iconName: "ChevronDown",
setName: "ui",
+ iconName: {
+ s: "ChevronDown75",
+ m: "ChevronDown100",
+ l: "ChevronDown200",
+ xl: "ChevronDown300",
+ }[size ?? "m"],
customClasses: [`${rootClass}-menuIcon`],
}, context)}
`;
};
+/**
+ * Picker template including adjacent popover and help text.
+ */
export const Template = ({
rootClass = "spectrum-Picker",
size = "m",
label,
labelPosition = "top",
- placeholder,
- helpText,
+ placeholder = "",
+ currentValue = "",
+ helpText = "",
isQuiet = false,
isKeyboardFocused = false,
+ showWorkflowIcon = false,
isOpen = false,
isInvalid = false,
isLoading = false,
isDisabled = false,
isReadOnly = false,
+ isHovered = false,
+ isActive = false,
withSwitch = false,
fieldLabelStyle = {},
customClasses = [],
customStyles = {},
- content = [],
+ popoverContent = [],
id = getRandomId("picker"),
} = {}, context = {}) => {
const { updateArgs } = context;
- let iconName = "ChevronDown200";
- switch (size) {
- case "s":
- iconName = "ChevronDown75";
- break;
- case "m":
- iconName = "ChevronDown100";
- break;
- case "xl":
- iconName = "ChevronDown300";
- break;
- default:
- iconName = "ChevronDown200";
- }
-
return html`
${when(label, () =>
FieldLabel({
@@ -117,7 +136,7 @@ export const Template = ({
label,
isDisabled,
customStyles: fieldLabelStyle,
- alignment: labelPosition,
+ alignment: labelPosition === "side" ? "left" : undefined,
}, context)
)}
${Popover({
@@ -129,27 +148,26 @@ export const Template = ({
rootClass,
size,
placeholder,
+ currentValue,
isQuiet,
+ showWorkflowIcon,
isKeyboardFocused,
isOpen,
isInvalid,
isLoading,
isDisabled,
isReadOnly,
+ isHovered,
+ isActive,
customClasses,
- customStyles: {
- "display": labelPosition == "left" ? "inline-block" : undefined,
- ...customStyles,
- },
- content,
- iconName,
+ customStyles,
labelPosition,
id,
onclick: function() {
updateArgs({ isOpen: !isOpen });
},
}, context),
- content,
+ content: popoverContent,
}, context)}
${when(helpText, () =>
HelpText({
@@ -158,12 +176,65 @@ export const Template = ({
hideIcon: true,
}, context)
)}
- ${when(withSwitch, () => Switch({
- size,
- label: "Toggle switch",
- customStyles: {
- "padding-inline-start": "15px"
- }
- }, context))}
+ ${when(withSwitch, () =>
+ Switch({
+ size,
+ label: "Toggle switch",
+ customStyles: {
+ "padding-inline-start": "15px"
+ }
+ }, context)
+ )}
`;
};
+
+/**
+ * Template showing both closed and open versions of the Picker.
+ */
+export const ClosedAndOpenTemplate = (args, context) => Container({
+ withBorder: false,
+ content: html`${[false, true].map((isOpen) =>
+ Container({
+ withBorder: false,
+ direction: "column",
+ heading: isOpen ? "Open" : "Closed",
+ containerStyles: {
+ rowGap: "8px",
+ },
+ // Make sure container flex layout does not misalign sibling elements such as field label in Template()
+ wrapperStyles: {
+ display: "block",
+ },
+ content: Template({
+ ...args,
+ isOpen,
+ }, context),
+ })
+ )}`,
+});
+
+/**
+ * Template for the Disabled docs story.
+ */
+export const DisabledTemplate = (args, context) => Container({
+ withBorder: false,
+ content: html`${[false, true].map((isInvalid) =>
+ Container({
+ withBorder: false,
+ direction: "column",
+ heading: isInvalid ? "Invalid" : "Default",
+ containerStyles: {
+ rowGap: "8px",
+ overflow: "hidden",
+ },
+ // Make sure container flex layout does not misalign sibling elements such as field label in Template()
+ wrapperStyles: {
+ display: "block",
+ },
+ content: Template({
+ ...args,
+ isInvalid,
+ }, context),
+ })
+ )}`,
+});
\ No newline at end of file
diff --git a/components/tabs/stories/template.js b/components/tabs/stories/template.js
index 17c6a3e2585..2e5c4ee6d45 100644
--- a/components/tabs/stories/template.js
+++ b/components/tabs/stories/template.js
@@ -124,7 +124,7 @@ export const Template = ({
customPopoverStyles: {
insetBlockStart: "24px",
},
- content: [
+ popoverContent: [
() => Menu({
selectionMode: "none",
size,