From e33b820f98c2d3b3d0fed82484da8675155392d9 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 7 Apr 2026 22:34:07 +0000 Subject: [PATCH] :sparkles: Add HintMessage component to @penpot/ui --- frontend/packages/ui/src/index.ts | 5 ++ .../utilities/HintMessage.module.scss | 27 +++++++ .../controls/utilities/HintMessage.spec.tsx | 77 +++++++++++++++++++ .../utilities/HintMessage.stories.tsx | 34 ++++++++ .../lib/controls/utilities/HintMessage.tsx | 51 ++++++++++++ 5 files changed, 194 insertions(+) create mode 100644 frontend/packages/ui/src/lib/controls/utilities/HintMessage.module.scss create mode 100644 frontend/packages/ui/src/lib/controls/utilities/HintMessage.spec.tsx create mode 100644 frontend/packages/ui/src/lib/controls/utilities/HintMessage.stories.tsx create mode 100644 frontend/packages/ui/src/lib/controls/utilities/HintMessage.tsx diff --git a/frontend/packages/ui/src/index.ts b/frontend/packages/ui/src/index.ts index 3e08e6d831..679e7350eb 100644 --- a/frontend/packages/ui/src/index.ts +++ b/frontend/packages/ui/src/index.ts @@ -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"; diff --git a/frontend/packages/ui/src/lib/controls/utilities/HintMessage.module.scss b/frontend/packages/ui/src/lib/controls/utilities/HintMessage.module.scss new file mode 100644 index 0000000000..fe02874dc9 --- /dev/null +++ b/frontend/packages/ui/src/lib/controls/utilities/HintMessage.module.scss @@ -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); +} diff --git a/frontend/packages/ui/src/lib/controls/utilities/HintMessage.spec.tsx b/frontend/packages/ui/src/lib/controls/utilities/HintMessage.spec.tsx new file mode 100644 index 0000000000..09377cf5c2 --- /dev/null +++ b/frontend/packages/ui/src/lib/controls/utilities/HintMessage.spec.tsx @@ -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(); + expect(baseElement).toBeTruthy(); + }); + + it("should render a
root element", () => { + const { container } = render(); + expect(container.firstElementChild?.tagName).toBe("DIV"); + }); + + it("should render the message in a span", () => { + const { container } = render( + , + ); + const span = container.querySelector("span"); + expect(span?.textContent).toBe("Some hint text"); + }); + + it("should set span id to `${id}-hint`", () => { + const { container } = render(); + 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(); + expect(container.querySelector("span")).toBeNull(); + }); + + it("should not set aria-live for type hint (default)", () => { + const { container } = render(); + const div = container.firstElementChild; + expect(div?.getAttribute("aria-live")).toBeNull(); + }); + + it("should set aria-live=polite for type warning", () => { + const { container } = render( + , + ); + const div = container.firstElementChild; + expect(div?.getAttribute("aria-live")).toBe("polite"); + }); + + it("should set aria-live=polite for type error", () => { + const { container } = render( + , + ); + const div = container.firstElementChild; + expect(div?.getAttribute("aria-live")).toBe("polite"); + }); + + it("should merge className", () => { + const { container } = render( + , + ); + const div = container.firstElementChild; + expect(div?.getAttribute("class")).toContain("extra"); + }); + + it("should pass through HTML attributes", () => { + const { container } = render( + , + ); + const div = container.firstElementChild; + expect(div?.getAttribute("data-testid")).toBe("h"); + }); +}); diff --git a/frontend/packages/ui/src/lib/controls/utilities/HintMessage.stories.tsx b/frontend/packages/ui/src/lib/controls/utilities/HintMessage.stories.tsx new file mode 100644 index 0000000000..7de5854c37 --- /dev/null +++ b/frontend/packages/ui/src/lib/controls/utilities/HintMessage.stories.tsx @@ -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; + +export default meta; +type Story = StoryObj; + +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 }, +}; diff --git a/frontend/packages/ui/src/lib/controls/utilities/HintMessage.tsx b/frontend/packages/ui/src/lib/controls/utilities/HintMessage.tsx new file mode 100644 index 0000000000..f5e56e5782 --- /dev/null +++ b/frontend/packages/ui/src/lib/controls/utilities/HintMessage.tsx @@ -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 ( +
+ {message != null && ( + + {message} + + )} +
+ ); +} + +export const HintMessage = memo(HintMessageInner);