mirror of
https://github.com/penpot/penpot.git
synced 2026-04-28 04:38:14 +00:00
✨ Add Label component to @penpot/ui
This commit is contained in:
parent
6436e18074
commit
ee7bb5589d
@ -40,3 +40,5 @@ export type {
|
||||
SwatchGradientStop,
|
||||
SwatchSize,
|
||||
} 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