From 5f8cad0177c069fee745a04fa311c311895a03d2 Mon Sep 17 00:00:00 2001 From: Rise Erpelding Date: Mon, 2 Jun 2025 20:01:21 -0500 Subject: [PATCH 1/5] feat(taggroup): spectrum 2 migration --- .changeset/public-facts-boil.md | 30 +++ components/taggroup/dist/metadata.json | 36 ++- components/taggroup/index.css | 72 +++++- .../taggroup/stories/taggroup.stories.js | 226 +++++++++++++++--- components/taggroup/stories/taggroup.test.js | 100 +++++--- components/taggroup/stories/template.js | 91 +++++-- 6 files changed, 459 insertions(+), 96 deletions(-) create mode 100644 .changeset/public-facts-boil.md diff --git a/.changeset/public-facts-boil.md b/.changeset/public-facts-boil.md new file mode 100644 index 00000000000..bcf0f3d61fc --- /dev/null +++ b/.changeset/public-facts-boil.md @@ -0,0 +1,30 @@ +--- +"@spectrum-css/taggroup": major +--- + +The Spectrum 2 version of Tag Group is a major change from its Spectrum 1 counterpart. + +Major style changes include: + +- Use of new tokens and custom properties for spacing tags. The method of spacing between tags has changed. Where previously tags were spaced using tokens to represent inline and block margins on each tag, tags are now spaced by tokens representing the gaps between tags. +- Rather than being a single size, Tag group now comes in t-shirt sizes: Small, medium, and large. These sizes should determine the sizes of the embedded components, but also the spacing between tags. +- Tag group can now accommodate a side label. To do so, it makes use of a grid layout. +- In order to match the layout in the spec, more embedded components besides Tag have been added. Field label, Help text, and Action button (quiet) components have been added to the Storybook implementation, and styles are set for these embedded components within the tag group layout. + +In order to support the aforementioned spacing changes, the two mod properties for margin have been removed: + +- `--mod-tag-group-item-margin-block` +- `--mod-tag-group-item-margin-inline` + +Instead, please customize spacing between tags with: + +- `--mod-tag-group-block-tag-spacing` +- `--mod-tag-group-inline-tag-spacing` + +These custom properties may need to be set to be double the previous margin values in order to achieve the same spacing. + +To support custom spacing of the embedded components, several other new mod properties have been added: + +- `--mod-tag-group-block-spacing-label-to-tags` +- `--mod-tag-group-inline-spacing-label-to-tags` +- `--mod-tag-group-spacing-help-text-to-tags` diff --git a/components/taggroup/dist/metadata.json b/components/taggroup/dist/metadata.json index b9eeaede1e8..b86adf57170 100644 --- a/components/taggroup/dist/metadata.json +++ b/components/taggroup/dist/metadata.json @@ -1,15 +1,39 @@ { "sourceFile": "index.css", - "selectors": [".spectrum-TagGroup", ".spectrum-TagGroup-item"], + "selectors": [ + ".spectrum-TagGroup", + ".spectrum-TagGroup--sideLabel", + ".spectrum-TagGroup--sideLabel .spectrum-TagGroup-actionButton", + ".spectrum-TagGroup--sideLabel .spectrum-TagGroup-helpText", + ".spectrum-TagGroup--sideLabel .spectrum-TagGroup-label", + ".spectrum-TagGroup--sideLabel .spectrum-TagGroup-tags", + ".spectrum-TagGroup--sizeL", + ".spectrum-TagGroup--sizeS", + ".spectrum-TagGroup-actionButton", + ".spectrum-TagGroup-helpText", + ".spectrum-TagGroup-label", + ".spectrum-TagGroup-tags" + ], "modifiers": [ - "--mod-tag-group-item-margin-block", - "--mod-tag-group-item-margin-inline" + "--mod-tag-group-block-spacing-label-to-tags", + "--mod-tag-group-block-tag-spacing", + "--mod-tag-group-inline-spacing-label-to-tags", + "--mod-tag-group-inline-tag-spacing", + "--mod-tag-group-spacing-help-text-to-tags" ], "component": [ - "--spectrum-tag-group-item-margin-block", - "--spectrum-tag-group-item-margin-inline" + "--spectrum-tag-group-block-spacing-label-to-tags", + "--spectrum-tag-group-block-tag-spacing", + "--spectrum-tag-group-inline-spacing-label-to-tags", + "--spectrum-tag-group-inline-tag-spacing", + "--spectrum-tag-group-spacing-help-text-to-tags" + ], + "global": [ + "--spectrum-field-label-to-component", + "--spectrum-help-text-to-component", + "--spectrum-spacing-100", + "--spectrum-spacing-200" ], - "global": ["--spectrum-spacing-75"], "passthroughs": [], "high-contrast": [] } diff --git a/components/taggroup/index.css b/components/taggroup/index.css index e3b323bb91f..bce0543eaa2 100644 --- a/components/taggroup/index.css +++ b/components/taggroup/index.css @@ -1,5 +1,5 @@ /*! - * Copyright 2024 Adobe. All rights reserved. + * Copyright 2025 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 @@ -12,17 +12,71 @@ */ .spectrum-TagGroup { - --spectrum-tag-group-item-margin-block: var(--spectrum-spacing-75); - --spectrum-tag-group-item-margin-inline: var(--spectrum-spacing-75); + --spectrum-tag-group-inline-tag-spacing: var(--spectrum-spacing-200); + --spectrum-tag-group-block-tag-spacing: var(--spectrum-spacing-200); + --spectrum-tag-group-block-spacing-label-to-tags: var(--spectrum-field-label-to-component); + --spectrum-tag-group-inline-spacing-label-to-tags: var(--spectrum-spacing-200); + --spectrum-tag-group-spacing-help-text-to-tags: var(--spectrum-help-text-to-component); +} + +.spectrum-TagGroup--sizeS { + --spectrum-tag-group-inline-tag-spacing: var(--spectrum-spacing-100); + --spectrum-tag-group-block-tag-spacing: var(--spectrum-spacing-100); +} + +.spectrum-TagGroup--sizeL { + --spectrum-tag-group-inline-tag-spacing: var(--spectrum-spacing-200); + --spectrum-tag-group-block-tag-spacing: var(--spectrum-spacing-200); +} +.spectrum-TagGroup { + display: grid; + grid-template-rows: auto auto auto auto; +} + +.spectrum-TagGroup-tags { display: inline-flex; flex-wrap: wrap; - margin: 0; - padding: 0; - list-style: none; + column-gap: var(--mod-tag-group-inline-tag-spacing, var(--spectrum-tag-group-inline-tag-spacing)); + row-gap: var(--mod-tag-group-block-tag-spacing, var(--spectrum-tag-group-block-tag-spacing)); + margin-block-end: var(--mod-tag-group-block-tag-spacing, var(--spectrum-tag-group-block-tag-spacing)); +} + +.spectrum-TagGroup-label { + margin-block-end: var(--mod-tag-group-block-spacing-label-to-tags, var(--spectrum-tag-group-block-spacing-label-to-tags)); +} + +.spectrum-TagGroup-actionButton { + justify-self: start; +} + +.spectrum-TagGroup-helpText { + margin-block-start: var(--mod-tag-group-spacing-help-text-to-tags, var(--spectrum-tag-group-spacing-help-text-to-tags)); } -.spectrum-TagGroup-item { - margin-block: var(--mod-tag-group-item-margin-block, var(--spectrum-tag-group-item-margin-block)); - margin-inline: var(--mod-tag-group-item-margin-inline, var(--spectrum-tag-group-item-margin-inline)); +.spectrum-TagGroup--sideLabel { + grid-template-columns: auto auto; + grid-template-rows: auto auto auto; + + .spectrum-TagGroup-label { + grid-row: 1; + grid-column: 1; + margin-block-end: 0; + margin-inline-end: var(--mod-tag-group-inline-spacing-label-to-tags, var(--spectrum-tag-group-inline-spacing-label-to-tags)); + } + + .spectrum-TagGroup-tags { + grid-row: 1; + grid-column: 2; + } + + .spectrum-TagGroup-actionButton { + grid-row: 2; + grid-column: 2; + } + + .spectrum-TagGroup-helpText { + grid-row: 3; + grid-column: 2; + } } diff --git a/components/taggroup/stories/taggroup.stories.js b/components/taggroup/stories/taggroup.stories.js index 66ef23e885d..fa7eaaa3343 100644 --- a/components/taggroup/stories/taggroup.stories.js +++ b/components/taggroup/stories/taggroup.stories.js @@ -1,8 +1,10 @@ +import { withDownStateDimensionCapture } from "@spectrum-css/preview/decorators"; import { disableDefaultModes } from "@spectrum-css/preview/modes"; +import { isInvalid } from "@spectrum-css/preview/types"; import { default as TagStories } from "@spectrum-css/tag/stories/tag.stories.js"; import metadata from "../dist/metadata.json"; import packageJson from "../package.json"; -import { TagGroups } from "./taggroup.test.js"; +import { exampleTagItems, TagGroups } from "./taggroup.test.js"; import { Template } from "./template.js"; const ignoreProps = ["rootClass", "hasClearButton", "label"]; @@ -23,31 +25,78 @@ export default { else value.table = { ...value.table, category: "Tag settings" }; return { ...acc, [key]: value }; }, {}), - ariaLabel: { - name: "Aria-label", - type: { name: "string" }, + isInvalid: { + ...isInvalid, + description: "Displays help text below the tag group with invalid icon and styling.", + if: { arg: "helpText", neq: "" }, + }, + ariaLabel: { table: { disable: true } }, + label: { table: { disable: true } }, + items: { table: { disable: true } }, + actionButtonText: { + name: "Action button text", + description: "Displays an action button below the tag group, if left blank, the action button will not be displayed.", + type: { name: "text" }, table: { - type: { summary: "string" }, + type: { summary: "text" }, category: "Content", }, - control: { type: "text" }, + control: "text", }, - items: { table: { disable: true } }, - isRemovable: { - name: "Removable tags", - description: "True if a button is present to clear the tag.", + fieldLabel: { + name: "Field label", + description: "Displays a label above the tag group, if left blank, the label will not be displayed.", + type: { name: "text" }, + table: { + type: { summary: "text" }, + category: "Content", + }, + control: "text", + }, + fieldLabelPosition: { + name: "Field label position", type: { name: "boolean" }, table: { type: { summary: "boolean" }, - category: "Shared settings", + category: "Content", }, - control: "boolean", + options: ["top", "side"], + control: "select", + if: { arg: "fieldLabel", truthy: true }, + }, + helpText: { + name: "Help text", + description: "Displays help text below the tag group, if left blank, the help text will not be displayed.", + type: { name: "text" }, + table: { + type: { summary: "text" }, + category: "Content", + }, + control: "text", + }, + numberOfTags: { + name: "Number of tags", + description: "The number of tags to display in the tag group.", + type: { name: "number" }, + table: { + type: { summary: "number" }, + category: "Content", + }, + control: { type: "number", min: 0, max: 30, step: 1 }, }, }, args: { + ...TagStories.args, rootClass: "spectrum-TagGroup", isRemovable: false, size: "m", + actionButtonText: "Show all", + helpText: "Help text description", + fieldLabel: "Tag group label", + fieldLabelPosition: "top", + isInvalid: false, + numberOfTags: 3, + ariaLabel: "Tags", }, parameters: { actions: { @@ -61,66 +110,167 @@ export default { }, packageJson, metadata, - }, -}; - -export const Default = TagGroups.bind({}); -Default.args = { - ariaLabel: "Tags", - items: [ - { - label: "Tag 1", + downState: { + selectors: [".spectrum-Tag", ".spectrum-ActionButton"], }, - { - label: "Tag 2", - }, - { - label: "Tag 3", + status: { + type: "migrated", }, + }, + decorators: [ + withDownStateDimensionCapture, ], + tags: ["migrated"], }; +/** + * A tag group on its own should always have a label. Labels can be placed either on top or on the side on the tags, but top labels are the default and are recommended because they work better with long copy, localization, and responsive layouts. + */ +export const Default = TagGroups.bind({}); +Default.tags = ["!autodocs"]; + // ********* DOCS ONLY ********* // +/** + * A tag group on its own should always have a label. Labels can be placed either on top or on the side on the tags, but top labels are the default and are recommended because they work better with long copy, localization, and responsive layouts. + */ +export const DefaultWithLabel = TagGroups.bind({}); +DefaultWithLabel.storyName = "Label position - default/top"; +DefaultWithLabel.tags = ["!dev"]; +DefaultWithLabel.parameters = { + chromatic: { + disableSnapshot: true, + }, +}; +DefaultWithLabel.args = { + actionButtonText: "", + helpText: "", + items: exampleTagItems, + fieldLabel: "Tags", +}; + +/** + * Tag group labels can also be placed on the side of the tag group. Side labels are most useful when vertical space is limited. + */ +export const SideLabel = Template.bind({}); +SideLabel.storyName = "Label position - side"; +SideLabel.tags = ["!dev"]; +SideLabel.parameters = { + chromatic: { + disableSnapshot: true, + }, +}; +SideLabel.args = { + fieldLabelPosition: "side", + items: exampleTagItems, + fieldLabel: "Tags", + helpText: "These tags were automatically added." +}; + /** * A tag group can contain removable tags when the context is for editing or non-removable tags when tags are read-only. Removable and non-removable tags cannot be combined within the tag group. + * + * When horizontal space is limited in a tag group, the tags wrap to form another line. Individual tags don't wrap between lines; they'll either move to the next line or the text within the tag will truncate. */ -export const Removable = Template.bind({}); -Removable.tags = ["!dev"]; -Removable.parameters = { +export const RemovableAndWrapping = Template.bind({}); +RemovableAndWrapping.storyName = "Removable and wrapping"; +RemovableAndWrapping.tags = ["!dev"]; +RemovableAndWrapping.parameters = { chromatic: { disableSnapshot: true, }, }; -Removable.args = { +RemovableAndWrapping.args = { + fieldLabel: "Tags", + actionButtonText: "", + helpText: "", isRemovable: true, - isEmphasized: false, customStyles: {"max-width": "300px"}, items: [ { - label: "Tag 1 Example", + label: "Hiking and camping", }, { - label: "Tag 2 Example", + label: "Surfing", }, { - label: "Tag 3 Example", + label: "Outdoors", }, { - label: "Tag 4", + label: "Tag with avatar", avatarUrl: "example-ava.png", }, { - label: "Tag 5", + label: "Traveling", }, { - label: "Tag 6", + label: "Tag with thumbnail", + thumbnailUrl: "flowers.png", }, { - label: "Tag 7", + label: "Tag with icon", + iconName: "Cloud", }, ], }; +/** + * A single quiet action button may be included at the end of a tag group if the action affects the entire group. Common actions include "show all", "show less", and "clear all". A counter of the number of tags can be included in the action button label if appropriate for the context. + */ +export const WithActionButton = Template.bind({}); +WithActionButton.storyName = "With action button"; +WithActionButton.tags = ["!dev"]; +WithActionButton.parameters = { + chromatic: { + disableSnapshot: true, + }, +}; +WithActionButton.args = { + actionButtonText: "Show all (13)", + helpText: "", + items: exampleTagItems, + fieldLabel: "Tags", +}; + +/** + * A tag group can have help text below the group to give extra context or instruction. The help text may be invalid, indicating an error for when requirements aren't met. + */ +export const WithHelpText = Template.bind({}); +WithHelpText.storyName = "With help text"; +WithHelpText.tags = ["!dev"]; +WithHelpText.parameters = { + chromatic: { + disableSnapshot: true, + }, +}; +WithHelpText.args = { + fieldLabel: "Tags", + isInvalid: true, + actionButtonText: "", + helpText: "Add at least three tags.", + items: [ + { label: "2025" }, + { label: "Australia" }, + ], +}; + +/** + * When a stand alone tag group has no tags, it shows placeholder text to communicate the empty state. The wording of the placeholder text can be customizable. + */ +export const WithNoTags = Template.bind({}); +WithNoTags.storyName = "With no tags (empty state)"; +WithNoTags.tags = ["!dev"]; +WithNoTags.parameters = { + chromatic: { + disableSnapshot: true, + }, +}; +WithNoTags.args = { + fieldLabel: "Tags", + numberOfTags: 0, + helpText: "", + actionButtonText: "", +}; + // ********* VRT ONLY ********* // export const WithForcedColors = TagGroups.bind({}); WithForcedColors.args = Default.args; diff --git a/components/taggroup/stories/taggroup.test.js b/components/taggroup/stories/taggroup.test.js index 737d8543f83..4508e23b94c 100644 --- a/components/taggroup/stories/taggroup.test.js +++ b/components/taggroup/stories/taggroup.test.js @@ -1,45 +1,89 @@ import { Variants } from "@spectrum-css/preview/decorators"; +import { html } from "lit"; import { Template } from "./template.js"; +export const exampleTagItems = [ + { label: "2025" }, + { label: "Outdoors" }, + { label: "Blue" }, + { label: "Australia" }, + { label: "Project Alpha" }, + { label: "Project Beta" }, +]; + +const overflowingTagItems = [ + ...exampleTagItems, + { label: "Sports" }, + { label: "Surfing" }, + { label: "Water" }, + { label: "Hawaii" }, +]; + +const TagGroupSizingTemplate = (args, context) => { + return html` + ${Template({ + ...args, + items: exampleTagItems, + customStyles: { + "max-width": "300px", + }, + }, context)} + `; +}; + export const TagGroups = Variants({ Template, + SizeTemplate: TagGroupSizingTemplate, sizeDirection: "row", testData: [ { testHeading: "Default", + actionButtonText: "", + helpText: "", + items: exampleTagItems, }, { - testHeading: "Is removable", + testHeading: "Removable, with action button and help text", isRemovable: true, + actionButtonText: "Show all", + helpText: "Add at least three tags.", + isInvalid: true, + items: [ + { label: "2025" }, + { label: "Australia" }, + ], }, { - testHeading: "Overflow", - isRemovable: true, - isEmphasized: false, + testHeading: "Top label variant showing action button, help text, and wrapping rows of tags", + actionButtonText: "Show all", + helpText: "Tags are automatically added.", + isInvalid: false, customStyles: {"max-width": "300px"}, - items: [ - { - label: "Tag 1 Example", - }, - { - label: "Tag 2 Example", - }, - { - label: "Tag 3 Example", - }, - { - label: "Tag 4", - }, - { - label: "Tag 5", - }, - { - label: "Tag 6", - }, - { - label: "Tag 7", - }, - ], - } + items: overflowingTagItems, + }, + { + testHeading: "Side label variant showing action button, help text, and wrapping rows of tags", + actionButtonText: "Show all", + helpText: "Tags are automatically added.", + isInvalid: false, + fieldLabelPosition: "side", + customStyles: {"max-width": "400px"}, + items: overflowingTagItems, + }, + { + testHeading: "Empty state, top label", + numberOfTags: 0, + helpText: "", + actionButtonText: "", + items: [], + }, + { + testHeading: "Empty state, side label", + fieldLabelPosition: "side", + numberOfTags: 0, + helpText: "", + actionButtonText: "", + items: [], + }, ], }); diff --git a/components/taggroup/stories/template.js b/components/taggroup/stories/template.js index 34bf5d50f77..6ce8bdcd092 100644 --- a/components/taggroup/stories/template.js +++ b/components/taggroup/stories/template.js @@ -1,38 +1,99 @@ +import { Template as ActionButton } from "@spectrum-css/actionbutton/stories/template.js"; +import { Template as FieldLabel } from "@spectrum-css/fieldlabel/stories/template.js"; +import { Template as HelpText } from "@spectrum-css/helptext/stories/template.js"; +import { getRandomId } from "@spectrum-css/preview/decorators"; import { Template as Tag } from "@spectrum-css/tag/stories/template.js"; +import { Template as Typography } from "@spectrum-css/typography/stories/template.js"; import { html } from "lit"; import { classMap } from "lit/directives/class-map.js"; import { ifDefined } from "lit/directives/if-defined.js"; import { styleMap } from "lit/directives/style-map.js"; +import { when } from "lit/directives/when.js"; import "../index.css"; export const Template = ({ rootClass = "spectrum-TagGroup", ariaLabel, + id = getRandomId("taggroup"), + numberOfTags = 3, items = [], isRemovable = false, customClasses = [], customStyles = {}, size = "m", + actionButtonText = "", + fieldLabel, + fieldLabelPosition = "top", + helpText, + isInvalid = false, ...args } = {}, context = {}) => { + const tags = Array.isArray(items) && items.length > 0 + ? items + : (typeof numberOfTags === "number" && numberOfTags > 0 + ? Array.from({ length: numberOfTags }, (_, i) => ({ label: `Tag ${i + 1}` })) + : []); + return html` -
({ ...a, [c]: true }), {}), - })} - style=${styleMap(customStyles)} - role="list" - aria-label=${ifDefined(ariaLabel)} - > - ${items.map((i) => Tag({ - ...i, - ...args, +
({ ...a, [c]: true }), {}), + })} + id=${ifDefined(id)} + style=${styleMap(customStyles)} + > + ${when(fieldLabel, () => html` + ${FieldLabel({ + size, + label: fieldLabel, + customClasses: [`${rootClass}-label`], + }, context)} + `)} + ${when(numberOfTags !== 0, () => html` +
+ ${tags.map((i) => Tag({ + ...i, + ...args, + size, + isRemovable, + label: i.label, + iconName: i.iconName || "", + id: getRandomId("tag-item"), + customClasses: [`${rootClass}-tag`], + }, context))} +
+ `, () => html` + ${Typography({ + size, + semantics: "body", + content: ["None"], + }, context)} + `)} + ${when(actionButtonText, () => html` + ${ActionButton({ + size, + isQuiet: true, + label: actionButtonText, + customClasses: [`${rootClass}-actionButton`], + }, context)} + `)} + ${when(helpText, () => html` + ${HelpText({ size, - hasClearButton: isRemovable, - customClasses: [`${rootClass}-item`], - }, context))} + text: helpText, + variant: isInvalid ? "negative" : undefined, + customClasses: [`${rootClass}-helpText`], + }, context)} + `)}
`; }; From aefb2589b30c675ef33c3d61b4bd20a0970e669d Mon Sep 17 00:00:00 2001 From: Rise Erpelding Date: Mon, 23 Jun 2025 12:14:12 -0700 Subject: [PATCH 2/5] fix(taggroup): pr feedback updates --- .../taggroup/stories/taggroup.stories.js | 45 +++++++++++++++---- components/taggroup/stories/taggroup.test.js | 16 ++++++- components/taggroup/stories/template.js | 2 +- 3 files changed, 52 insertions(+), 11 deletions(-) diff --git a/components/taggroup/stories/taggroup.stories.js b/components/taggroup/stories/taggroup.stories.js index fa7eaaa3343..65c19bb265d 100644 --- a/components/taggroup/stories/taggroup.stories.js +++ b/components/taggroup/stories/taggroup.stories.js @@ -1,10 +1,10 @@ -import { withDownStateDimensionCapture } from "@spectrum-css/preview/decorators"; +import { Sizes, withDownStateDimensionCapture } from "@spectrum-css/preview/decorators"; import { disableDefaultModes } from "@spectrum-css/preview/modes"; import { isInvalid } from "@spectrum-css/preview/types"; import { default as TagStories } from "@spectrum-css/tag/stories/tag.stories.js"; import metadata from "../dist/metadata.json"; import packageJson from "../package.json"; -import { exampleTagItems, TagGroups } from "./taggroup.test.js"; +import { exampleTagItems, TagGroupDisabledItem, TagGroups, TagGroupSizingTemplate } from "./taggroup.test.js"; import { Template } from "./template.js"; const ignoreProps = ["rootClass", "hasClearButton", "label"]; @@ -76,7 +76,7 @@ export default { }, numberOfTags: { name: "Number of tags", - description: "The number of tags to display in the tag group.", + description: "The number of tags to display in the tag group. If the number of tags is 0, the tag group will show a placeholder text to communicate the empty state.", type: { name: "number" }, table: { type: { summary: "number" }, @@ -123,15 +123,12 @@ export default { tags: ["migrated"], }; -/** - * A tag group on its own should always have a label. Labels can be placed either on top or on the side on the tags, but top labels are the default and are recommended because they work better with long copy, localization, and responsive layouts. - */ export const Default = TagGroups.bind({}); Default.tags = ["!autodocs"]; // ********* DOCS ONLY ********* // /** - * A tag group on its own should always have a label. Labels can be placed either on top or on the side on the tags, but top labels are the default and are recommended because they work better with long copy, localization, and responsive layouts. + * A tag group on its own should always have a label. Labels can be placed either on top or on the side of the tags, but top labels are the default and are recommended because they work better with long copy, localization, and responsive layouts. */ export const DefaultWithLabel = TagGroups.bind({}); DefaultWithLabel.storyName = "Label position - default/top"; @@ -214,7 +211,7 @@ RemovableAndWrapping.args = { }; /** - * A single quiet action button may be included at the end of a tag group if the action affects the entire group. Common actions include "show all", "show less", and "clear all". A counter of the number of tags can be included in the action button label if appropriate for the context. + * A single quiet [action button](?path=/docs/components-action-button--docs) may be included at the end of a tag group if the action affects the entire group. Common actions include "show all," "show less," and "clear all." A counter of the number of tags can be included in the action button label if appropriate for the context. */ export const WithActionButton = Template.bind({}); WithActionButton.storyName = "With action button"; @@ -232,7 +229,7 @@ WithActionButton.args = { }; /** - * A tag group can have help text below the group to give extra context or instruction. The help text may be invalid, indicating an error for when requirements aren't met. + * A tag group can have [help text](?path=/docs/components-help-text--docs) below the group to give extra context or instruction. The help text may be invalid, indicating an error for when requirements aren't met. */ export const WithHelpText = Template.bind({}); WithHelpText.storyName = "With help text"; @@ -253,6 +250,22 @@ WithHelpText.args = { ], }; +/** + * Avoid disabling an entire tag group. In cases where users can't interact with an entire group of tags, consider either using non-removable tags or hiding the tag group altogether. Don't disable all individual tags; having a tag group that's disabled isn't accessible and it can be frustrating for users. + */ +export const Disabled = TagGroupDisabledItem.bind({}); +Disabled.storyName = "With disabled tag"; +Disabled.tags = ["!dev"]; +Disabled.parameters = { + chromatic: { + disableSnapshot: true, + }, +}; +Disabled.args = { + fieldLabel: "Tags", + helpText: "These tags were automatically added." +}; + /** * When a stand alone tag group has no tags, it shows placeholder text to communicate the empty state. The wording of the placeholder text can be customizable. */ @@ -271,6 +284,20 @@ WithNoTags.args = { actionButtonText: "", }; +/** + * The default size of a tag group is medium, but tags are also available in small and large sizes. + */ +export const Sizing = (args, context) => Sizes({ + Template: TagGroupSizingTemplate, + withHeading: false, + withBorder: false, + ...args, +}, context); +Sizing.tags = ["!dev"]; +Sizing.parameters = { + chromatic: { disableSnapshot: true }, +}; + // ********* VRT ONLY ********* // export const WithForcedColors = TagGroups.bind({}); WithForcedColors.args = Default.args; diff --git a/components/taggroup/stories/taggroup.test.js b/components/taggroup/stories/taggroup.test.js index 4508e23b94c..465dc14218f 100644 --- a/components/taggroup/stories/taggroup.test.js +++ b/components/taggroup/stories/taggroup.test.js @@ -19,7 +19,12 @@ const overflowingTagItems = [ { label: "Hawaii" }, ]; -const TagGroupSizingTemplate = (args, context) => { +const itemsWithDisabledTag = [ + ...exampleTagItems, + { label: "Disabled tag", isDisabled: true }, +]; + +export const TagGroupSizingTemplate = (args, context) => { return html` ${Template({ ...args, @@ -31,6 +36,15 @@ const TagGroupSizingTemplate = (args, context) => { `; }; +export const TagGroupDisabledItem = (args, context) => { + return html` + ${Template({ + ...args, + items: itemsWithDisabledTag, + }, context)} + `; +}; + export const TagGroups = Variants({ Template, SizeTemplate: TagGroupSizingTemplate, diff --git a/components/taggroup/stories/template.js b/components/taggroup/stories/template.js index 6ce8bdcd092..63803e05ce8 100644 --- a/components/taggroup/stories/template.js +++ b/components/taggroup/stories/template.js @@ -65,8 +65,8 @@ export const Template = ({ ...args, size, isRemovable, + isDisabled: i.isDisabled, label: i.label, - iconName: i.iconName || "", id: getRandomId("tag-item"), customClasses: [`${rootClass}-tag`], }, context))} From 0603cee142090adedc97b5c31b095aeb9ec762e3 Mon Sep 17 00:00:00 2001 From: Rise Erpelding Date: Mon, 23 Jun 2025 12:31:30 -0700 Subject: [PATCH 3/5] fix(taggroup): style empty state --- .changeset/public-facts-boil.md | 8 +++++++ components/taggroup/dist/metadata.json | 21 +++++++++++++++++- components/taggroup/index.css | 22 +++++++++++++++++++ .../taggroup/stories/taggroup.stories.js | 2 +- components/taggroup/stories/taggroup.test.js | 4 ++-- components/taggroup/stories/template.js | 1 - 6 files changed, 53 insertions(+), 5 deletions(-) diff --git a/.changeset/public-facts-boil.md b/.changeset/public-facts-boil.md index bcf0f3d61fc..437a593971d 100644 --- a/.changeset/public-facts-boil.md +++ b/.changeset/public-facts-boil.md @@ -28,3 +28,11 @@ To support custom spacing of the embedded components, several other new mod prop - `--mod-tag-group-block-spacing-label-to-tags` - `--mod-tag-group-inline-spacing-label-to-tags` - `--mod-tag-group-spacing-help-text-to-tags` + +To support the optional empty state (when there are no tags in the tag group), several passthroughs to modify the body typography text element have been added, including: + +- `--mod-body-cjk-line-height` +- `--mod-body-font-size` +- `--mod-body-line-height` +- `--mod-body-margin-end` +- `--mod-body-margin-start` diff --git a/components/taggroup/dist/metadata.json b/components/taggroup/dist/metadata.json index b86adf57170..28ca5140d81 100644 --- a/components/taggroup/dist/metadata.json +++ b/components/taggroup/dist/metadata.json @@ -12,9 +12,17 @@ ".spectrum-TagGroup-actionButton", ".spectrum-TagGroup-helpText", ".spectrum-TagGroup-label", - ".spectrum-TagGroup-tags" + ".spectrum-TagGroup-tags", + ".spectrum-TagGroup:lang(ja)", + ".spectrum-TagGroup:lang(ko)", + ".spectrum-TagGroup:lang(zh)" ], "modifiers": [ + "--mod-body-cjk-line-height", + "--mod-body-font-size", + "--mod-body-line-height", + "--mod-body-margin-end", + "--mod-body-margin-start", "--mod-tag-group-block-spacing-label-to-tags", "--mod-tag-group-block-tag-spacing", "--mod-tag-group-inline-spacing-label-to-tags", @@ -29,8 +37,19 @@ "--spectrum-tag-group-spacing-help-text-to-tags" ], "global": [ + "--spectrum-cjk-line-height-100", + "--spectrum-component-bottom-to-text-100", + "--spectrum-component-bottom-to-text-200", + "--spectrum-component-bottom-to-text-75", + "--spectrum-component-top-to-text-100", + "--spectrum-component-top-to-text-200", + "--spectrum-component-top-to-text-75", "--spectrum-field-label-to-component", + "--spectrum-font-size-100", + "--spectrum-font-size-200", + "--spectrum-font-size-75", "--spectrum-help-text-to-component", + "--spectrum-line-height-100", "--spectrum-spacing-100", "--spectrum-spacing-200" ], diff --git a/components/taggroup/index.css b/components/taggroup/index.css index bce0543eaa2..6975c5186ba 100644 --- a/components/taggroup/index.css +++ b/components/taggroup/index.css @@ -17,16 +17,38 @@ --spectrum-tag-group-block-spacing-label-to-tags: var(--spectrum-field-label-to-component); --spectrum-tag-group-inline-spacing-label-to-tags: var(--spectrum-spacing-200); --spectrum-tag-group-spacing-help-text-to-tags: var(--spectrum-help-text-to-component); + + /* passthroughs for body typography element in empty state */ + --mod-body-line-height: var(--spectrum-line-height-100); + --mod-body-font-size: var(--spectrum-font-size-100); + --mod-body-margin-start: var(--spectrum-component-top-to-text-100); + --mod-body-margin-end: var(--spectrum-component-bottom-to-text-100); + + &:lang(ja), + &:lang(zh), + &:lang(ko) { + --mod-body-cjk-line-height: var(--spectrum-cjk-line-height-100); + } } .spectrum-TagGroup--sizeS { --spectrum-tag-group-inline-tag-spacing: var(--spectrum-spacing-100); --spectrum-tag-group-block-tag-spacing: var(--spectrum-spacing-100); + + /* passthroughs for body typography element in empty state */ + --mod-body-font-size: var(--spectrum-font-size-75); + --mod-body-margin-start: var(--spectrum-component-top-to-text-75); + --mod-body-margin-end: var(--spectrum-component-bottom-to-text-75); } .spectrum-TagGroup--sizeL { --spectrum-tag-group-inline-tag-spacing: var(--spectrum-spacing-200); --spectrum-tag-group-block-tag-spacing: var(--spectrum-spacing-200); + + /* passthroughs for body typography element in empty state */ + --mod-body-font-size: var(--spectrum-font-size-200); + --mod-body-margin-start: var(--spectrum-component-top-to-text-200); + --mod-body-margin-end: var(--spectrum-component-bottom-to-text-200); } .spectrum-TagGroup { diff --git a/components/taggroup/stories/taggroup.stories.js b/components/taggroup/stories/taggroup.stories.js index 65c19bb265d..3eecf3d27d9 100644 --- a/components/taggroup/stories/taggroup.stories.js +++ b/components/taggroup/stories/taggroup.stories.js @@ -267,7 +267,7 @@ Disabled.args = { }; /** - * When a stand alone tag group has no tags, it shows placeholder text to communicate the empty state. The wording of the placeholder text can be customizable. + * When a stand alone tag group has no tags, it may show placeholder text to communicate the empty state. The placeholder text can be customized, or another element may be shown to communicate the empty state rather than placeholder text. */ export const WithNoTags = Template.bind({}); WithNoTags.storyName = "With no tags (empty state)"; diff --git a/components/taggroup/stories/taggroup.test.js b/components/taggroup/stories/taggroup.test.js index 465dc14218f..d21c8f5da28 100644 --- a/components/taggroup/stories/taggroup.test.js +++ b/components/taggroup/stories/taggroup.test.js @@ -87,7 +87,7 @@ export const TagGroups = Variants({ { testHeading: "Empty state, top label", numberOfTags: 0, - helpText: "", + helpText: "No tags added", actionButtonText: "", items: [], }, @@ -95,7 +95,7 @@ export const TagGroups = Variants({ testHeading: "Empty state, side label", fieldLabelPosition: "side", numberOfTags: 0, - helpText: "", + helpText: "No tags added", actionButtonText: "", items: [], }, diff --git a/components/taggroup/stories/template.js b/components/taggroup/stories/template.js index 63803e05ce8..a2bf7858748 100644 --- a/components/taggroup/stories/template.js +++ b/components/taggroup/stories/template.js @@ -73,7 +73,6 @@ export const Template = ({
`, () => html` ${Typography({ - size, semantics: "body", content: ["None"], }, context)} From b73326dd12ea776a2e3d72371a00abf4ac222f2b Mon Sep 17 00:00:00 2001 From: Rise Erpelding Date: Tue, 24 Jun 2025 16:30:46 -0700 Subject: [PATCH 4/5] fix(taggroup): add disabled action button setting --- .../taggroup/stories/taggroup.stories.js | 20 +++++++++++++++---- components/taggroup/stories/taggroup.test.js | 3 ++- components/taggroup/stories/template.js | 2 ++ 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/components/taggroup/stories/taggroup.stories.js b/components/taggroup/stories/taggroup.stories.js index 3eecf3d27d9..639bbdc1c56 100644 --- a/components/taggroup/stories/taggroup.stories.js +++ b/components/taggroup/stories/taggroup.stories.js @@ -4,7 +4,7 @@ import { isInvalid } from "@spectrum-css/preview/types"; import { default as TagStories } from "@spectrum-css/tag/stories/tag.stories.js"; import metadata from "../dist/metadata.json"; import packageJson from "../package.json"; -import { exampleTagItems, TagGroupDisabledItem, TagGroups, TagGroupSizingTemplate } from "./taggroup.test.js"; +import { exampleTagItems, TagGroupDisabledItemAndActionButton, TagGroups, TagGroupSizingTemplate } from "./taggroup.test.js"; import { Template } from "./template.js"; const ignoreProps = ["rootClass", "hasClearButton", "label"]; @@ -39,10 +39,19 @@ export default { type: { name: "text" }, table: { type: { summary: "text" }, - category: "Content", + category: "Action button settings", }, control: "text", }, + hasDisabledActionButton: { + name: "Has disabled action button", + description: "Displays the action button in a disabled state.", + type: { name: "boolean" }, + table: { + type: { summary: "boolean" }, + category: "Action button settings", + }, + }, fieldLabel: { name: "Field label", description: "Displays a label above the tag group, if left blank, the label will not be displayed.", @@ -97,6 +106,7 @@ export default { isInvalid: false, numberOfTags: 3, ariaLabel: "Tags", + hasDisabledActionButton: false, }, parameters: { actions: { @@ -252,9 +262,11 @@ WithHelpText.args = { /** * Avoid disabling an entire tag group. In cases where users can't interact with an entire group of tags, consider either using non-removable tags or hiding the tag group altogether. Don't disable all individual tags; having a tag group that's disabled isn't accessible and it can be frustrating for users. + * + * Individual tags may be disabled, and the action button may also be disabled, as seen below. */ -export const Disabled = TagGroupDisabledItem.bind({}); -Disabled.storyName = "With disabled tag"; +export const Disabled = TagGroupDisabledItemAndActionButton.bind({}); +Disabled.storyName = "With disabled tag and action button"; Disabled.tags = ["!dev"]; Disabled.parameters = { chromatic: { diff --git a/components/taggroup/stories/taggroup.test.js b/components/taggroup/stories/taggroup.test.js index d21c8f5da28..0c4078cf75f 100644 --- a/components/taggroup/stories/taggroup.test.js +++ b/components/taggroup/stories/taggroup.test.js @@ -36,11 +36,12 @@ export const TagGroupSizingTemplate = (args, context) => { `; }; -export const TagGroupDisabledItem = (args, context) => { +export const TagGroupDisabledItemAndActionButton = (args, context) => { return html` ${Template({ ...args, items: itemsWithDisabledTag, + hasDisabledActionButton: true, }, context)} `; }; diff --git a/components/taggroup/stories/template.js b/components/taggroup/stories/template.js index a2bf7858748..0493d9da9da 100644 --- a/components/taggroup/stories/template.js +++ b/components/taggroup/stories/template.js @@ -27,6 +27,7 @@ export const Template = ({ fieldLabelPosition = "top", helpText, isInvalid = false, + hasDisabledActionButton = false, ...args } = {}, context = {}) => { const tags = Array.isArray(items) && items.length > 0 @@ -81,6 +82,7 @@ export const Template = ({ ${ActionButton({ size, isQuiet: true, + isDisabled: hasDisabledActionButton, label: actionButtonText, customClasses: [`${rootClass}-actionButton`], }, context)} From 2fb8a7b456f365c92a5cdd90af1cbfa974dbde71 Mon Sep 17 00:00:00 2001 From: Rise Erpelding Date: Thu, 26 Jun 2025 13:10:43 -1000 Subject: [PATCH 5/5] docs: update custom styles and action button controls --- components/taggroup/stories/taggroup.stories.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/taggroup/stories/taggroup.stories.js b/components/taggroup/stories/taggroup.stories.js index 639bbdc1c56..0a10a82c09e 100644 --- a/components/taggroup/stories/taggroup.stories.js +++ b/components/taggroup/stories/taggroup.stories.js @@ -51,6 +51,7 @@ export default { type: { summary: "boolean" }, category: "Action button settings", }, + if: { arg: "actionButtonText", truthy: true }, }, fieldLabel: { name: "Field label", @@ -191,7 +192,7 @@ RemovableAndWrapping.args = { actionButtonText: "", helpText: "", isRemovable: true, - customStyles: {"max-width": "300px"}, + customStyles: {"max-inline-size": "300px"}, items: [ { label: "Hiking and camping",