diff --git a/changes/49752-toast-icon-alignment b/changes/49752-toast-icon-alignment new file mode 100644 index 0000000000..1f129d4679 --- /dev/null +++ b/changes/49752-toast-icon-alignment @@ -0,0 +1 @@ +* Fixed the misaligned icon in `notify.success`/`notify.error` toast notifications so it sits on the first line of the message on both single- and multi-line toasts. diff --git a/frontend/components/ToastNotification/ToastCard.tsx b/frontend/components/ToastNotification/ToastCard.tsx index 7b45080b1c..6b8b25a277 100644 --- a/frontend/components/ToastNotification/ToastCard.tsx +++ b/frontend/components/ToastNotification/ToastCard.tsx @@ -3,6 +3,7 @@ import classnames from "classnames"; import { toast } from "sonner"; import Icon from "components/Icon"; +import Button from "components/buttons/Button"; import CopyButton from "components/buttons/CopyButton"; import { Colors } from "styles/var/colors"; import { syntaxHighlight } from "utilities/helpers"; @@ -123,33 +124,26 @@ const ToastCard = ({
{hasDetail && ( - + /> )} - + />
{hasDetail && isOpen && ( diff --git a/frontend/components/ToastNotification/ToastNotification.stories.tsx b/frontend/components/ToastNotification/ToastNotification.stories.tsx index 5d9bec16cd..a5dd7b6044 100644 --- a/frontend/components/ToastNotification/ToastNotification.stories.tsx +++ b/frontend/components/ToastNotification/ToastNotification.stories.tsx @@ -74,6 +74,54 @@ export const Success: Story = { ), }; +/** + * MultiLineSuccess — a success message long enough to wrap, so the + * icon's alignment against the first line (vs. later lines) is visible. + * Also exercises rich formatting (a bolded entity name). + */ +export const MultiLineSuccess: Story = { + render: () => ( + <> + + + notify.success( + <> + Successfully released MacBook Air from Apple Business. This + is a very long line that should wrap two lines if not three. + + ) + } + /> + + ), +}; + +/** + * MultiLineError — an error message long enough to wrap. Same alignment + * concern as MultiLineSuccess, on the error variant. + */ +export const MultiLineError: Story = { + render: () => ( + <> + + + notify.error( + <> + Couldn't release MacBook Air from Apple Business. + Please try again. If the problem persists, contact your + administrator for help. + + ) + } + /> + + ), +}; + /** * Error — click the button to fire a plain error toast (no detail payload). */ diff --git a/frontend/components/ToastNotification/_styles.scss b/frontend/components/ToastNotification/_styles.scss index 9fa38b4445..7ceee491cc 100644 --- a/frontend/components/ToastNotification/_styles.scss +++ b/frontend/components/ToastNotification/_styles.scss @@ -2,7 +2,6 @@ // via toast.custom(), so all card styling below is ours. $toast-notification-shadow: 0px 2px 6px 0px rgba(25, 33, 71, 0.1); -$toast-notification-action-size: 36px; $toast-notification-width: 500px; .toast-notification { @@ -80,7 +79,9 @@ $toast-notification-width: 500px; &__header { display: flex; - align-items: center; + // Do not align items center — the icon and action-button rows are each + // one text line tall so they line up with the first line of the + // message, which matters when the message wraps. justify-content: space-between; gap: $pad-small; width: 100%; @@ -127,41 +128,20 @@ $toast-notification-width: 500px; align-items: center; gap: $pad-small; flex-shrink: 0; + // Match __icon's one-line-tall box so the action buttons visually + // track the first line of text when the message wraps. The subdued + // Button children are 36px tall and intentionally overflow this + // ~21px band — the mismatch is what centers each button on the + // first text line without dragging the whole header down. + height: calc(#{$x-small} * #{$line-height}); } - &__action-button { - appearance: none; - background: transparent; - border: 0; - margin: 0; - padding: $pad-small; - width: $toast-notification-action-size; - height: $toast-notification-action-size; - cursor: pointer; - color: $ui-fleet-black-75; - display: inline-flex; - align-items: center; - justify-content: center; - border-radius: $border-radius; - transition: background-color 150ms ease-in-out, color 150ms ease-in-out; - - &:hover, - &:focus-visible { - background-color: $ui-fleet-black-5; - color: $core-fleet-black; - outline: none; - } + &__chevron svg { + transition: transform 0.25s ease; } - &__chevron { - display: inline-flex; - align-items: center; - justify-content: center; - transition: transform 200ms ease-in-out; - - &--open { - transform: rotate(180deg); - } + &__chevron--open svg { + transform: rotate(180deg); } &__panel { diff --git a/frontend/components/buttons/Button/Button.tests.tsx b/frontend/components/buttons/Button/Button.tests.tsx index 896a98d476..59424fa2c9 100644 --- a/frontend/components/buttons/Button/Button.tests.tsx +++ b/frontend/components/buttons/Button/Button.tests.tsx @@ -179,6 +179,17 @@ describe("Button component", () => { expect(container.firstChild).toHaveClass("button--icon-only"); expect(container.firstChild).not.toHaveClass("button--with-icon"); }); + it("renders aria-controls when ariaControls is provided", () => { + render(); + expect(screen.getByRole("button")).toHaveAttribute( + "aria-controls", + "menu-1" + ); + }); + it("omits aria-controls when ariaControls is undefined", () => { + render(); + expect(screen.getByRole("button")).not.toHaveAttribute("aria-controls"); + }); it("warns in dev when an icon-only button has neither ariaLabel nor title", () => { const warn = jest .spyOn(console, "warn") diff --git a/frontend/components/buttons/Button/Button.tsx b/frontend/components/buttons/Button/Button.tsx index 59846a6c84..ab6144e063 100644 --- a/frontend/components/buttons/Button/Button.tsx +++ b/frontend/components/buttons/Button/Button.tsx @@ -65,6 +65,7 @@ export interface IButtonProps { | "grid" | "dialog"; ariaExpanded?: boolean; + ariaControls?: string; ariaLabel?: string; ariaPressed?: boolean; /** Small: 1/2 the padding, Wide: 200px */ @@ -150,6 +151,7 @@ class Button extends React.Component { customOnKeyDown, ariaHasPopup, ariaExpanded, + ariaControls, ariaLabel, ariaPressed, size, @@ -235,6 +237,7 @@ class Button extends React.Component { ref={setRef} aria-haspopup={ariaHasPopup} aria-expanded={ariaExpanded} + aria-controls={ariaControls} aria-label={ariaLabel} aria-pressed={ariaPressed} >