mirror of
https://github.com/penpot/penpot.git
synced 2026-05-26 02:13:46 +00:00
✨ Add Label component to @penpot/ui
This commit is contained in:
parent
6436e18074
commit
ee7bb5589d
@ -40,3 +40,5 @@ export type {
|
|||||||
SwatchGradientStop,
|
SwatchGradientStop,
|
||||||
SwatchSize,
|
SwatchSize,
|
||||||
} from "./lib/utilities/Swatch";
|
} from "./lib/utilities/Swatch";
|
||||||
|
export { Label } from "./lib/controls/utilities/Label";
|
||||||
|
export type { LabelProps } from "./lib/controls/utilities/Label";
|
||||||
|
|||||||
@ -0,0 +1,32 @@
|
|||||||
|
// 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;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
--label-color: var(--color-foreground-primary);
|
||||||
|
--label-optional-color: var(--color-foreground-secondary);
|
||||||
|
|
||||||
|
@include t.use-typography("body-small");
|
||||||
|
|
||||||
|
color: var(--label-color);
|
||||||
|
display: flex;
|
||||||
|
gap: var(--sp-xs);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-text,
|
||||||
|
.label-optional {
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-text {
|
||||||
|
color: var(--label-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-optional {
|
||||||
|
color: var(--label-optional-color);
|
||||||
|
}
|
||||||
@ -0,0 +1,81 @@
|
|||||||
|
// 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 { Label } from "./Label";
|
||||||
|
|
||||||
|
describe("Label", () => {
|
||||||
|
it("should render successfully", () => {
|
||||||
|
const { baseElement } = render(<Label htmlFor="my-input">Name</Label>);
|
||||||
|
expect(baseElement).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render a <label> element", () => {
|
||||||
|
const { container } = render(<Label htmlFor="my-input">Name</Label>);
|
||||||
|
const label = container.querySelector("label");
|
||||||
|
expect(label).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set htmlFor on the label", () => {
|
||||||
|
const { container } = render(<Label htmlFor="my-input">Name</Label>);
|
||||||
|
const label = container.querySelector("label");
|
||||||
|
expect(label?.getAttribute("for")).toBe("my-input");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render children inside label-text span", () => {
|
||||||
|
const { container } = render(<Label htmlFor="x">My Field</Label>);
|
||||||
|
const span = container.querySelector("label span");
|
||||||
|
expect(span?.textContent).toBe("My Field");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not render label-text span when children is undefined", () => {
|
||||||
|
const { container } = render(<Label htmlFor="x" isOptional={true} />);
|
||||||
|
// Only the optional span should be present
|
||||||
|
const spans = container.querySelectorAll("label span");
|
||||||
|
expect(spans.length).toBe(1);
|
||||||
|
expect(spans[0]?.textContent).toBe("(Optional)");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not render optional span by default", () => {
|
||||||
|
const { container } = render(<Label htmlFor="x">Label</Label>);
|
||||||
|
const spans = container.querySelectorAll("label span");
|
||||||
|
expect(spans.length).toBe(1);
|
||||||
|
// The single span is label-text, not optional
|
||||||
|
expect(spans[0]?.textContent).toBe("Label");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render optional span when isOptional is true", () => {
|
||||||
|
const { container } = render(
|
||||||
|
<Label htmlFor="x" isOptional={true}>
|
||||||
|
Label
|
||||||
|
</Label>,
|
||||||
|
);
|
||||||
|
const spans = container.querySelectorAll("label span");
|
||||||
|
expect(spans.length).toBe(2);
|
||||||
|
expect(spans[1]?.textContent).toBe("(Optional)");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should merge className", () => {
|
||||||
|
const { container } = render(
|
||||||
|
<Label htmlFor="x" className="custom">
|
||||||
|
Label
|
||||||
|
</Label>,
|
||||||
|
);
|
||||||
|
const label = container.querySelector("label");
|
||||||
|
const cls = label?.getAttribute("class") ?? "";
|
||||||
|
expect(cls).toContain("custom");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should pass through additional HTML attributes", () => {
|
||||||
|
const { container } = render(
|
||||||
|
<Label htmlFor="x" data-testid="my-label">
|
||||||
|
Label
|
||||||
|
</Label>,
|
||||||
|
);
|
||||||
|
const label = container.querySelector("label");
|
||||||
|
expect(label?.getAttribute("data-testid")).toBe("my-label");
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
// 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 { Label } from "./Label";
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: "Controls/Utilities/Label",
|
||||||
|
component: Label,
|
||||||
|
args: {
|
||||||
|
htmlFor: "input-id",
|
||||||
|
children: "Field label",
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Label>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Default: Story = {};
|
||||||
|
|
||||||
|
export const Optional: Story = {
|
||||||
|
args: {
|
||||||
|
isOptional: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NoChildren: Story = {
|
||||||
|
args: {
|
||||||
|
children: undefined,
|
||||||
|
isOptional: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
41
frontend/packages/ui/src/lib/controls/utilities/Label.tsx
Normal file
41
frontend/packages/ui/src/lib/controls/utilities/Label.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
// 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 "./Label.module.scss";
|
||||||
|
|
||||||
|
export interface LabelProps extends ComponentPropsWithRef<"label"> {
|
||||||
|
/** The id of the form control this label is associated with */
|
||||||
|
htmlFor: string;
|
||||||
|
/** When true, renders an "(Optional)" suffix */
|
||||||
|
isOptional?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function LabelInner({
|
||||||
|
htmlFor,
|
||||||
|
isOptional = false,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...rest
|
||||||
|
}: LabelProps) {
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
htmlFor={htmlFor}
|
||||||
|
className={clsx(styles.label, className)}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{children != null && (
|
||||||
|
<span className={styles["label-text"]}>{children}</span>
|
||||||
|
)}
|
||||||
|
{isOptional && (
|
||||||
|
<span className={styles["label-optional"]}>(Optional)</span>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Label = memo(LabelInner);
|
||||||
Loading…
x
Reference in New Issue
Block a user