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}
>