Fleet UI: Align toast icon with first line of message (#50449)
This commit is contained in:
@@ -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.
|
||||
@@ -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 = ({
|
||||
</div>
|
||||
<div className={`${baseClass}__actions`}>
|
||||
{hasDetail && (
|
||||
<button
|
||||
type="button"
|
||||
className={`${baseClass}__action-button`}
|
||||
aria-expanded={isOpen}
|
||||
aria-controls={panelId}
|
||||
aria-label={
|
||||
<Button
|
||||
className={classnames(`${baseClass}__chevron`, {
|
||||
[`${baseClass}__chevron--open`]: isOpen,
|
||||
})}
|
||||
variant="subdued"
|
||||
icon="chevron-down"
|
||||
ariaExpanded={isOpen}
|
||||
ariaControls={panelId}
|
||||
ariaLabel={
|
||||
isOpen ? "Collapse error details" : "Expand error details"
|
||||
}
|
||||
onClick={toggle}
|
||||
>
|
||||
<span
|
||||
className={classnames(`${baseClass}__chevron`, {
|
||||
[`${baseClass}__chevron--open`]: isOpen,
|
||||
})}
|
||||
>
|
||||
<Icon name="chevron-down" color="ui-fleet-black-75" />
|
||||
</span>
|
||||
</button>
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className={`${baseClass}__action-button`}
|
||||
aria-label="Dismiss notification"
|
||||
<Button
|
||||
variant="subdued"
|
||||
icon="close"
|
||||
ariaLabel="Dismiss notification"
|
||||
onClick={handleClose}
|
||||
>
|
||||
<Icon name="close" color="ui-fleet-black-75" />
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{hasDetail && isOpen && (
|
||||
|
||||
@@ -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: () => (
|
||||
<>
|
||||
<ToastNotification />
|
||||
<TriggerButton
|
||||
label="Show multi-line success toast"
|
||||
onClick={() =>
|
||||
notify.success(
|
||||
<>
|
||||
Successfully released <b>MacBook Air</b> 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: () => (
|
||||
<>
|
||||
<ToastNotification />
|
||||
<TriggerButton
|
||||
label="Show multi-line error toast"
|
||||
onClick={() =>
|
||||
notify.error(
|
||||
<>
|
||||
Couldn't release <b>MacBook Air</b> 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).
|
||||
*/
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// <ToastCard/> 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 {
|
||||
|
||||
@@ -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(<Button ariaControls="menu-1">Open menu</Button>);
|
||||
expect(screen.getByRole("button")).toHaveAttribute(
|
||||
"aria-controls",
|
||||
"menu-1"
|
||||
);
|
||||
});
|
||||
it("omits aria-controls when ariaControls is undefined", () => {
|
||||
render(<Button>Plain</Button>);
|
||||
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")
|
||||
|
||||
@@ -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<IButtonProps, IButtonState> {
|
||||
customOnKeyDown,
|
||||
ariaHasPopup,
|
||||
ariaExpanded,
|
||||
ariaControls,
|
||||
ariaLabel,
|
||||
ariaPressed,
|
||||
size,
|
||||
@@ -235,6 +237,7 @@ class Button extends React.Component<IButtonProps, IButtonState> {
|
||||
ref={setRef}
|
||||
aria-haspopup={ariaHasPopup}
|
||||
aria-expanded={ariaExpanded}
|
||||
aria-controls={ariaControls}
|
||||
aria-label={ariaLabel}
|
||||
aria-pressed={ariaPressed}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user