mirror of
https://github.com/penpot/penpot.git
synced 2026-04-28 04:38:14 +00:00
✨ Add HintMessage component to @penpot/ui
This commit is contained in:
parent
ee7bb5589d
commit
e33b820f98
@ -42,3 +42,8 @@ export type {
|
||||
} from "./lib/utilities/Swatch";
|
||||
export { Label } from "./lib/controls/utilities/Label";
|
||||
export type { LabelProps } from "./lib/controls/utilities/Label";
|
||||
export { HintMessage } from "./lib/controls/utilities/HintMessage";
|
||||
export type {
|
||||
HintMessageProps,
|
||||
HintMessageType,
|
||||
} from "./lib/controls/utilities/HintMessage";
|
||||
|
||||
@ -0,0 +1,27 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) KALEIDOS INC
|
||||
|
||||
@use "../../_ds/typography.scss" as t;
|
||||
|
||||
.hint-message {
|
||||
--hint-color: var(--color-foreground-secondary);
|
||||
|
||||
@include t.use-typography("body-small");
|
||||
|
||||
color: var(--hint-color);
|
||||
}
|
||||
|
||||
.type-hint {
|
||||
--hint-color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.type-warning {
|
||||
--hint-color: var(--color-accent-warning);
|
||||
}
|
||||
|
||||
.type-error {
|
||||
--hint-color: var(--color-foreground-error);
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) KALEIDOS INC
|
||||
|
||||
import { render } from "@testing-library/react";
|
||||
import { HintMessage } from "./HintMessage";
|
||||
|
||||
describe("HintMessage", () => {
|
||||
it("should render successfully", () => {
|
||||
const { baseElement } = render(<HintMessage id="field" message="A hint" />);
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should render a <div> root element", () => {
|
||||
const { container } = render(<HintMessage id="field" message="A hint" />);
|
||||
expect(container.firstElementChild?.tagName).toBe("DIV");
|
||||
});
|
||||
|
||||
it("should render the message in a span", () => {
|
||||
const { container } = render(
|
||||
<HintMessage id="field" message="Some hint text" />,
|
||||
);
|
||||
const span = container.querySelector("span");
|
||||
expect(span?.textContent).toBe("Some hint text");
|
||||
});
|
||||
|
||||
it("should set span id to `${id}-hint`", () => {
|
||||
const { container } = render(<HintMessage id="my-field" message="hint" />);
|
||||
const span = container.querySelector("span");
|
||||
expect(span?.getAttribute("id")).toBe("my-field-hint");
|
||||
});
|
||||
|
||||
it("should not render span when message is undefined", () => {
|
||||
const { container } = render(<HintMessage id="field" />);
|
||||
expect(container.querySelector("span")).toBeNull();
|
||||
});
|
||||
|
||||
it("should not set aria-live for type hint (default)", () => {
|
||||
const { container } = render(<HintMessage id="field" message="hint" />);
|
||||
const div = container.firstElementChild;
|
||||
expect(div?.getAttribute("aria-live")).toBeNull();
|
||||
});
|
||||
|
||||
it("should set aria-live=polite for type warning", () => {
|
||||
const { container } = render(
|
||||
<HintMessage id="field" message="warning" type="warning" />,
|
||||
);
|
||||
const div = container.firstElementChild;
|
||||
expect(div?.getAttribute("aria-live")).toBe("polite");
|
||||
});
|
||||
|
||||
it("should set aria-live=polite for type error", () => {
|
||||
const { container } = render(
|
||||
<HintMessage id="field" message="error" type="error" />,
|
||||
);
|
||||
const div = container.firstElementChild;
|
||||
expect(div?.getAttribute("aria-live")).toBe("polite");
|
||||
});
|
||||
|
||||
it("should merge className", () => {
|
||||
const { container } = render(
|
||||
<HintMessage id="field" message="hint" className="extra" />,
|
||||
);
|
||||
const div = container.firstElementChild;
|
||||
expect(div?.getAttribute("class")).toContain("extra");
|
||||
});
|
||||
|
||||
it("should pass through HTML attributes", () => {
|
||||
const { container } = render(
|
||||
<HintMessage id="field" message="hint" data-testid="h" />,
|
||||
);
|
||||
const div = container.firstElementChild;
|
||||
expect(div?.getAttribute("data-testid")).toBe("h");
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,34 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) KALEIDOS INC
|
||||
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { HintMessage } from "./HintMessage";
|
||||
|
||||
const meta = {
|
||||
title: "Controls/Utilities/HintMessage",
|
||||
component: HintMessage,
|
||||
args: {
|
||||
id: "field",
|
||||
message: "This is a hint message.",
|
||||
},
|
||||
} satisfies Meta<typeof HintMessage>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const Warning: Story = {
|
||||
args: { type: "warning", message: "This value is unusual." },
|
||||
};
|
||||
|
||||
export const Error: Story = {
|
||||
args: { type: "error", message: "This field is required." },
|
||||
};
|
||||
|
||||
export const NoMessage: Story = {
|
||||
args: { message: undefined },
|
||||
};
|
||||
@ -0,0 +1,51 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) KALEIDOS INC
|
||||
|
||||
import { type ComponentPropsWithRef, memo } from "react";
|
||||
import clsx from "clsx";
|
||||
import styles from "./HintMessage.module.scss";
|
||||
|
||||
export type HintMessageType = "hint" | "warning" | "error";
|
||||
|
||||
export interface HintMessageProps extends ComponentPropsWithRef<"div"> {
|
||||
/** Unique id – the inner span gets id `${id}-hint` */
|
||||
id: string;
|
||||
/** The message to display */
|
||||
message?: string;
|
||||
/** Visual/semantic type of the hint */
|
||||
type?: HintMessageType;
|
||||
}
|
||||
|
||||
function HintMessageInner({
|
||||
id,
|
||||
message,
|
||||
type = "hint",
|
||||
className,
|
||||
...rest
|
||||
}: HintMessageProps) {
|
||||
const ariaLive =
|
||||
type === "warning" || type === "error" ? "polite" : undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
styles["hint-message"],
|
||||
styles[`type-${type}`],
|
||||
className,
|
||||
)}
|
||||
aria-live={ariaLive}
|
||||
{...rest}
|
||||
>
|
||||
{message != null && (
|
||||
<span className={styles["hint-message-text"]} id={`${id}-hint`}>
|
||||
{message}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const HintMessage = memo(HintMessageInner);
|
||||
Loading…
x
Reference in New Issue
Block a user