-
Notifications
You must be signed in to change notification settings - Fork 209
feat(action-menu): S2 migration [CSS-1160] #4085
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| --- | ||
| "@spectrum-css/actionmenu": major | ||
| "@spectrum-css/actionbutton": minor | ||
| "@spectrum-css/menu": patch | ||
| "@spectrum-css/actiongroup": patch | ||
| --- | ||
|
|
||
| ### Action menu component (now with custom styles!) | ||
|
|
||
| Introduces `@spectrum-css/actionmenu`, a composition of `ActionButton`, `Popover`, and `Menu` to present action lists from a trigger. Now with custom styles! | ||
|
|
||
| - Adds wrapper classes: `spectrum-ActionMenu`, `spectrum-ActionMenu-trigger`, `spectrum-ActionMenu-popover`, and `spectrum-ActionMenu-menu`. | ||
| - Supports long press triggers and four placements (start/end, top/bottom) via the underlying popover API. | ||
| - Design reference: [Figma S2 token specs](https://www.figma.com/design/eoZHKJH9a3LJkHYCGt60Vb/S2-token-specs?node-id=20959-21513&node-type=frame&t=jbePQKK1yLdrHG2M-11). | ||
|
|
||
| #### Migration notes | ||
|
|
||
| - If you previously composed an action menu manually (action button + popover + menu), you can adopt the new wrapper classes without changing the underlying markup semantics. Ensure the trigger has `aria-haspopup="menu"` and manages `aria-expanded` according to your application logic. | ||
| - For spacing customizations previously done with ad‑hoc margins, switch to the new `--spectrum-actionmenu-button-to-menu-gap` custom property. | ||
|
|
||
| Example markup: | ||
|
|
||
| ```html | ||
| <div class="spectrum-ActionMenu"> | ||
| <button | ||
| class="spectrum-ActionMenu-trigger spectrum-ActionButton" | ||
| aria-haspopup="menu" | ||
| aria-expanded="false" | ||
| > | ||
| <!-- icon/label --> | ||
| </button> | ||
| <div class="spectrum-ActionMenu-popover spectrum-Popover"> | ||
| <ul class="spectrum-ActionMenu-menu spectrum-Menu"> | ||
| <!-- menu items --> | ||
| </ul> | ||
| </div> | ||
| <!-- popover positioning/visibility is owned by your implementation --> | ||
| <!-- use long-press behavior when appropriate to your UX --> | ||
| <!-- use Popover placement options: bottom-start, bottom-end, start-top, end-top --> | ||
| </div> | ||
| ``` | ||
|
|
||
| ### Menu refinements | ||
|
|
||
| Updates `@spectrum-css/menu` styles to align with latest Spectrum 2 design specifications and improve accessibility. | ||
|
|
||
| - Updated `.is-selectableMultiple .spectrum-Menu-itemCheckbox` to `.is-selectableMultiple:not(:has(.is-selectable)) .spectrum-Menu-itemCheckbox` to prevent clash with the `.is-selectable` placement. | ||
| - Non-breaking; no class or DOM changes required. | ||
|
|
||
| ### Action button refinements | ||
|
|
||
| - Selection styling now applies when components use ARIA pressed/expanded semantics, not just `.is-selected`. | ||
| - Implemented with `:where()` to keep selector specificity low and prevent downstream specificity battles. | ||
| - Non-breaking; no class changes required. | ||
|
|
||
| ### Action group refinements | ||
|
|
||
| Aligns selection behavior of grouped items with action button updates. | ||
|
|
||
| - Adds `:where([aria-pressed="true"], [aria-expanded="true"])` alongside `.is-selected` on items to cover more accessibility use-cases while keeping specificity low. | ||
| - Non-breaking; no class changes required. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,17 +14,16 @@ | |
| ".spectrum-ActionButton-label", | ||
| ".spectrum-ActionButton-label:empty", | ||
| ".spectrum-ActionButton.is-disabled", | ||
| ".spectrum-ActionButton.is-selected", | ||
| ".spectrum-ActionButton.is-selected.spectrum-ActionButton--emphasized", | ||
| ".spectrum-ActionButton.is-selected.spectrum-ActionButton--staticBlack", | ||
| ".spectrum-ActionButton.is-selected.spectrum-ActionButton--staticWhite", | ||
| ".spectrum-ActionButton.spectrum-ActionButton--emphasized:is(.is-selected, [aria-pressed=\"true\"], [aria-expanded=\"true\"])", | ||
| ".spectrum-ActionButton.spectrum-ActionButton--quiet", | ||
| ".spectrum-ActionButton.spectrum-ActionButton--quiet.is-selected", | ||
| ".spectrum-ActionButton.spectrum-ActionButton--quiet:disabled:not(.is-selected)", | ||
| ".spectrum-ActionButton.spectrum-ActionButton--quiet:is(.is-selected, [aria-pressed=\"true\"], [aria-expanded=\"true\"])", | ||
| ".spectrum-ActionButton.spectrum-ActionButton--quiet:is(:disabled, .is-disabled, [aria-disabled=\"true\"]):not(:where(.is-selected, [aria-pressed=\"true\"], [aria-expanded=\"true\"]))", | ||
| ".spectrum-ActionButton.spectrum-ActionButton--staticBlack", | ||
| ".spectrum-ActionButton.spectrum-ActionButton--staticBlack.spectrum-ActionButton--quiet", | ||
| ".spectrum-ActionButton.spectrum-ActionButton--staticBlack:is(.is-selected, [aria-pressed=\"true\"], [aria-expanded=\"true\"])", | ||
| ".spectrum-ActionButton.spectrum-ActionButton--staticWhite", | ||
| ".spectrum-ActionButton.spectrum-ActionButton--staticWhite.spectrum-ActionButton--quiet", | ||
| ".spectrum-ActionButton.spectrum-ActionButton--staticWhite:is(.is-selected, [aria-pressed=\"true\"], [aria-expanded=\"true\"])", | ||
| ".spectrum-ActionButton::-moz-focus-inner", | ||
| ".spectrum-ActionButton:active", | ||
| ".spectrum-ActionButton:after", | ||
|
|
@@ -35,11 +34,12 @@ | |
| ".spectrum-ActionButton:focus-visible:after", | ||
| ".spectrum-ActionButton:has(.spectrum-ActionButton-icon)", | ||
| ".spectrum-ActionButton:hover", | ||
| ".spectrum-ActionButton:is(.is-selected, [aria-pressed=\"true\"], [aria-expanded=\"true\"])", | ||
| ".spectrum-ActionButton:is(:disabled, .is-disabled, [aria-disabled=\"true\"])", | ||
| ".spectrum-ActionButton:not(:has(.spectrum-ActionButton-label))", | ||
| "a.spectrum-ActionButton" | ||
| ], | ||
| "modifiers": [ | ||
| "--mod-actionbutton-animation-duration", | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since this is for S2 and we've agreed to remove modifiers, I removed the mods on any lines I needed to update for this change anyway. |
||
| "--mod-actionbutton-background-color-default", | ||
| "--mod-actionbutton-background-color-default-selected", | ||
| "--mod-actionbutton-background-color-default-selected-emphasized", | ||
|
|
@@ -71,10 +71,6 @@ | |
| "--mod-actionbutton-edge-to-text", | ||
| "--mod-actionbutton-edge-to-visual", | ||
| "--mod-actionbutton-edge-to-visual-only", | ||
| "--mod-actionbutton-focus-indicator-border-radius", | ||
| "--mod-actionbutton-focus-indicator-color", | ||
| "--mod-actionbutton-focus-indicator-gap", | ||
| "--mod-actionbutton-focus-indicator-thickness", | ||
| "--mod-actionbutton-font-size", | ||
| "--mod-actionbutton-font-style", | ||
| "--mod-actionbutton-font-weight", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -63,7 +63,7 @@ governing permissions and limitations under the License. | |
| --spectrum-actionbutton-background-color-focus: var(--spectrum-gray-200); | ||
| --spectrum-actionbutton-background-color-disabled: transparent; | ||
|
|
||
| &.is-selected { | ||
| &:is(.is-selected, [aria-pressed="true"], [aria-expanded="true"]) { | ||
| --spectrum-actionbutton-background-color-disabled: var(--spectrum-disabled-background-color); | ||
| } | ||
| } | ||
|
|
@@ -116,7 +116,8 @@ governing permissions and limitations under the License. | |
| } | ||
| } | ||
|
|
||
| &.is-selected { | ||
| /* expanded is specific to action menu when the menu is open */ | ||
| &:is(.is-selected, [aria-pressed="true"], [aria-expanded="true"]) { | ||
| --mod-actionbutton-background-color-default: var(--mod-actionbutton-background-color-default-selected, var(--spectrum-neutral-background-color-selected-default)); | ||
|
castastrophe marked this conversation as resolved.
|
||
| --mod-actionbutton-background-color-hover: var(--mod-actionbutton-background-color-hover-selected, var(--spectrum-neutral-background-color-selected-hover)); | ||
| --mod-actionbutton-background-color-down: var(--mod-actionbutton-background-color-down-selected, var(--spectrum-neutral-background-color-selected-down)); | ||
|
|
@@ -298,6 +299,16 @@ governing permissions and limitations under the License. | |
| border-style: none; | ||
| } | ||
|
|
||
| &::after { | ||
| position: absolute; | ||
| inset: 0; | ||
| margin: calc((var(--spectrum-actionbutton-focus-indicator-gap) + var(--spectrum-actionbutton-border-width)) * -1); | ||
| border-radius: var(--spectrum-actionbutton-focus-indicator-border-radius); | ||
| transition: box-shadow var(--highcontrast-actionbutton-animation-duration, var(--spectrum-actionbutton-animation-duration)) ease-in-out; | ||
| pointer-events: none; | ||
| content: ""; | ||
| } | ||
|
|
||
| &:focus { | ||
| outline: none; | ||
| } | ||
|
|
@@ -315,6 +326,13 @@ governing permissions and limitations under the License. | |
| &:focus-visible { | ||
| background-color: var(--highcontrast-actionbutton-background-color-default, var(--mod-actionbutton-background-color-focus, var(--spectrum-actionbutton-background-color-focus))); | ||
| color: var(--highcontrast-actionbutton-content-color-default, var(--mod-actionbutton-content-color-focus, var(--spectrum-actionbutton-content-color-focus))); | ||
|
|
||
| box-shadow: none; | ||
| outline: none; | ||
|
|
||
| &::after { | ||
| box-shadow: 0 0 0 var(--spectrum-actionbutton-focus-indicator-thickness) var(--highcontrast-actionbutton-focus-indicator-color, var(--spectrum-actionbutton-focus-indicator-color)); | ||
| } | ||
| } | ||
|
|
||
| &:active { | ||
|
|
@@ -323,8 +341,8 @@ governing permissions and limitations under the License. | |
| transform: perspective(var(--spectrum-actionbutton-downstate-perspective)) translateZ(var(--spectrum-component-size-difference-down)); | ||
| } | ||
|
|
||
| &:disabled, | ||
| &.is-disabled { | ||
| /* ideal when we want to disable the button but still allow its content to be focused */ | ||
| &:is(:disabled, .is-disabled, [aria-disabled="true"]) { | ||
| background-color: var(--highcontrast-actionbutton-background-color-disabled, var(--mod-actionbutton-background-color-disabled, var(--spectrum-actionbutton-background-color-disabled))); | ||
| color: var(--highcontrast-actionbutton-content-color-disabled, var(--mod-actionbutton-content-color-disabled, var(--spectrum-actionbutton-content-color-disabled))); | ||
| } | ||
|
|
@@ -364,10 +382,6 @@ a.spectrum-ActionButton { | |
| /* Fixes horizontal alignment of text in anchor buttons */ | ||
| text-align: center; | ||
|
|
||
| &:empty { | ||
| display: none; | ||
| } | ||
|
|
||
| pointer-events: none; | ||
|
|
||
| font-size: var(--mod-actionbutton-font-size, var(--spectrum-actionbutton-font-size)); | ||
|
|
@@ -378,40 +392,21 @@ a.spectrum-ActionButton { | |
|
|
||
| text-overflow: ellipsis; | ||
| overflow: hidden; | ||
|
|
||
| &:empty { | ||
| display: none; | ||
| } | ||
| } | ||
|
|
||
| .spectrum-ActionButton-hold { | ||
| position: absolute; | ||
| inset-inline-end: calc(var(--mod-actionbutton-edge-to-hold-icon, var(--spectrum-actionbutton-edge-to-hold-icon)) - var(--spectrum-actionbutton-border-width)); | ||
| inset-block-end: calc(var(--mod-actionbutton-edge-to-hold-icon, var(--spectrum-actionbutton-edge-to-hold-icon)) - var(--spectrum-actionbutton-border-width)); | ||
| display: block; | ||
| color: inherit; | ||
| transform: var(--spectrum-logical-rotation); | ||
| } | ||
|
|
||
| /* Focus indicator */ | ||
| .spectrum-ActionButton { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just me tidying up a bit by combining these styles in with the initial definition for .spectrum-ActionButton |
||
| transition: border-color var(--highcontrast-actionbutton-animation-duration, var(--mod-actionbutton-animation-duration, var(--spectrum-actionbutton-animation-duration))) ease-in-out; | ||
|
|
||
| &::after { | ||
| position: absolute; | ||
| inset: 0; | ||
| margin: calc((var(--mod-actionbutton-focus-indicator-gap, var(--spectrum-actionbutton-focus-indicator-gap)) + var(--spectrum-actionbutton-border-width)) * -1); | ||
| border-radius: var(--mod-actionbutton-focus-indicator-border-radius, var(--spectrum-actionbutton-focus-indicator-border-radius)); | ||
| transition: box-shadow var(--highcontrast-actionbutton-animation-duration, var(--mod-actionbutton-animation-duration, var(--spectrum-actionbutton-animation-duration))) ease-in-out; | ||
| pointer-events: none; | ||
| content: ""; | ||
| } | ||
|
|
||
| &:focus-visible { | ||
| box-shadow: none; | ||
| outline: none; | ||
|
|
||
| &::after { | ||
| box-shadow: 0 0 0 var(--mod-actionbutton-focus-indicator-thickness, var(--spectrum-actionbutton-focus-indicator-thickness)) var(--highcontrast-actionbutton-focus-indicator-color, var(--mod-actionbutton-focus-indicator-color, var(--spectrum-actionbutton-focus-indicator-color))); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @media (forced-colors: active) { | ||
| .spectrum-ActionButton { | ||
| /** | ||
|
|
@@ -457,7 +452,7 @@ a.spectrum-ActionButton { | |
| --highcontrast-actionbutton-background-color-disabled: Canvas; | ||
| --highcontrast-actionbutton-content-color-default: CanvasText; | ||
|
|
||
| &:disabled:not(.is-selected) { | ||
| &:is(:disabled, .is-disabled, [aria-disabled="true"]):not(:where(.is-selected, [aria-pressed="true"], [aria-expanded="true"])) { | ||
| --highcontrast-actionbutton-border-color: Canvas; | ||
| } | ||
| } | ||
|
|
@@ -469,8 +464,7 @@ a.spectrum-ActionButton { | |
| --highcontrast-actionbutton-border-color: Highlight; | ||
| } | ||
|
|
||
| /* Selected always shows as a solid highlighted color. */ | ||
| &.is-selected { | ||
| &:is(.is-selected, [aria-pressed="true"], [aria-expanded="true"]) { | ||
| --highcontrast-actionbutton-border-color: Highlight; | ||
| --highcontrast-actionbutton-background-color-default: Highlight; | ||
| --highcontrast-actionbutton-content-color-default: HighlightText; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,14 @@ | ||
| import { default as IconStories } from "@spectrum-css/icon/stories/icon.stories.js"; | ||
| import { Sizes, withDownStateDimensionCapture } from "@spectrum-css/preview/decorators"; | ||
| import { disableDefaultModes } from "@spectrum-css/preview/modes"; | ||
| import { isActive, isDisabled, isEmphasized, isFocused, isHovered, isQuiet, isSelected, size, staticColor } from "@spectrum-css/preview/types"; | ||
| import metadata from "../dist/metadata.json"; | ||
| import packageJson from "../package.json"; | ||
| import { isActive, isDisabled, isEmphasized, isFocused, isHovered, isOpen, isQuiet, isSelected, size, staticColor } from "@spectrum-css/preview/types"; | ||
| import { ActionButtonGroup } from "./actionbutton.test.js"; | ||
| import { ActionButtonsWithIconOptions, IconOnlyOption, Template, TreatmentTemplate } from "./template.js"; | ||
|
|
||
| // Local assets to render the component styles and structure | ||
| import metadata from "../dist/metadata.json"; | ||
| import packageJson from "../package.json"; | ||
|
|
||
| /** | ||
| * The action button component represents an action a user can take. | ||
| * | ||
|
|
@@ -56,8 +58,8 @@ export default { | |
| control: "boolean", | ||
| }, | ||
| hasPopup: { | ||
| name: "Has popup", | ||
| description: "If the button triggers a popup action, this should be set to reflect the type of element that pops-up.", | ||
| name: "Has pop-up", | ||
| description: "If the button triggers a popover element to open, this should be set to reflect the semantic type of that element.", | ||
| type: { name: "string" }, | ||
| table: { | ||
| type: { summary: "string" }, | ||
|
|
@@ -66,6 +68,22 @@ export default { | |
| control: "select", | ||
| options: ["true", "menu", "listbox", "tree", "grid", "dialog", "false"], | ||
| }, | ||
| hasLongPress: { | ||
| name: "Long press", | ||
| description: "If the trigger supports a long-press action which triggers the menu, this should be set to true.", | ||
| type: { name: "boolean" }, | ||
| table: { | ||
| type: { summary: "boolean" }, | ||
| category: "Accessibility", | ||
| }, | ||
| control: "boolean", | ||
| }, | ||
| isOpen: { | ||
| ...isOpen, | ||
| name: "Popover open", | ||
| description: "This should be true when the popover element is open.", | ||
| if: { arg: "hasPopup", truthy: true }, | ||
|
Comment on lines
+81
to
+85
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| }, | ||
| staticColor: { | ||
| ...staticColor, | ||
| if: { arg: "isEmphasized", truthy: false }, | ||
|
|
@@ -77,6 +95,7 @@ export default { | |
| isQuiet: false, | ||
| isEmphasized: false, | ||
| hasPopup: "false", | ||
| hasLongPress: false, | ||
| isActive: false, | ||
| isFocused: false, | ||
| isHovered: false, | ||
|
|
@@ -88,7 +107,7 @@ export default { | |
| }, | ||
| parameters: { | ||
| actions: { | ||
| handles: ["click .spectrum-ActionButton:not([disabled])"], | ||
| handles: ["click .spectrum-ActionButton:not([disabled])", "mousedown .spectrum-ActionButton:not([disabled])", "mouseup .spectrum-ActionButton:not([disabled])", "touchstart .spectrum-ActionButton:not([disabled])", "touchend .spectrum-ActionButton:not([disabled])"], | ||
| }, | ||
| design: { | ||
| type: "figma", | ||
|
|
@@ -179,8 +198,8 @@ Quiet.parameters = { | |
|
|
||
| /** | ||
| * An action button can have a hold icon (a small corner triangle). This icon indicates that holding down the action button for a | ||
| * short amount of time can reveal a [popover](/docs/components-popover--docs) menu, which can be used, for example, to switch | ||
| * between related actions. Note that this popover menu is not demonstrated here—this would be handled by the implementation. | ||
| * short amount of time (currently the standard is 300ms) can reveal a [popover](/docs/components-popover--docs) menu, which can be used, for example, to switch | ||
| * between related actions. Note that this popover menu is not demonstrated here; this would be handled by the implementation. | ||
| * Because of the way padding is calculated, the hold icon must be placed before the workflow icon in the markup. | ||
| */ | ||
| export const HoldIcon = IconOnlyOption.bind({}); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,12 +17,14 @@ export const ActionButtons = (args, context) => { | |
| ${Template({ | ||
| ...args, | ||
| hasPopup: "true", | ||
| hasLongPress: true, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need an |
||
| hideLabel: true, | ||
| }, context)} | ||
| ${Template({ | ||
| ...args, | ||
| iconName: undefined, | ||
| hasPopup: "true", | ||
| hasLongPress: true, | ||
| }, context)} | ||
| </div> | ||
| `; | ||
|
|
||

Uh oh!
There was an error while loading. Please reload this page.