Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/text-expand-as-elements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudflare/kumo": minor
---

Expand `Text` component's `as` prop to accept additional HTML text elements: `label`, `dt`, `dd`, `li`, `figcaption`, `legend`, `pre`, `code`, `em`, `strong`, `small`, `abbr`, and `time`. This unblocks downstream usage in Stratus where `Text` needs to render as definition list terms, labels, and code elements.
34 changes: 34 additions & 0 deletions packages/kumo/src/components/text/text.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,40 @@ describe("Text", () => {
expect(container.querySelector("h2")).toBeNull();
});

it("renders as <dt> when as='dt'", () => {
const { container } = render(<Text as="dt">Term</Text>);
expect(container.querySelector("dt")).toBeTruthy();
expect(container.querySelector("p")).toBeNull();
});

it("renders as <dd> when as='dd'", () => {
const { container } = render(<Text as="dd">Definition</Text>);
expect(container.querySelector("dd")).toBeTruthy();
});

it("renders as <label> when as='label'", () => {
const { container } = render(<Text as="label">Label</Text>);
expect(container.querySelector("label")).toBeTruthy();
});

it("renders as <code> when as='code'", () => {
const { container } = render(
<Text variant="mono" as="code">
const x = 1
</Text>,
);
expect(container.querySelector("code")).toBeTruthy();
});

it("renders as <pre> when as='pre'", () => {
const { container } = render(
<Text variant="mono" as="pre">
preformatted
</Text>,
);
expect(container.querySelector("pre")).toBeTruthy();
});

// Type-level enforcement of the required `as` prop for heading variants
// lives in `text.type-spec.tsx`. That file is included in the regular
// tsconfig glob, so `pnpm typecheck` evaluates every `@ts-expect-error`
Expand Down
27 changes: 23 additions & 4 deletions packages/kumo/src/components/text/text.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,20 @@ export type TextElement =
| "h5"
| "h6"
| "p"
| "span";
| "span"
| "label"
| "dt"
| "dd"
| "li"
| "figcaption"
| "legend"
| "pre"
| "code"
| "em"
| "strong"
| "small"
| "abbr"
| "time";

type BaseTextProps = Omit<
ComponentPropsWithoutRef<"span">,
Expand Down Expand Up @@ -238,7 +251,10 @@ export interface TextProps {
/** Whether to truncate overflowing text with an ellipsis. Adds `truncate min-w-0` classes. */
truncate?: boolean;
/**
* The HTML element to render (`"h1"`–`"h6"`, `"p"`, or `"span"`).
* The HTML element to render. Accepts headings (`"h1"`–`"h6"`), block text
* (`"p"`, `"pre"`), inline text (`"span"`, `"code"`, `"em"`, `"strong"`,
* `"small"`, `"abbr"`, `"time"`), form-related (`"label"`, `"legend"`),
* list/definition (`"dt"`, `"dd"`, `"li"`), and `"figcaption"`.
*
* - **Required** for heading variants (`"heading1"`, `"heading2"`,
* `"heading3"`) — pick the element that reflects this text's place in
Expand Down Expand Up @@ -276,7 +292,7 @@ function _Text<Variant extends TextVariant = "body">(
as,
...props
}: TextPropsInternal<Variant>,
ref: ForwardedRef<HTMLHeadingElement>,
ref: ForwardedRef<HTMLElement>,
) {
const isCopy = ["body", "secondary", "success", "error"].includes(variant);
const isMono = ["mono", "mono-secondary"].includes(variant);
Expand All @@ -294,7 +310,10 @@ function _Text<Variant extends TextVariant = "body">(

return (
<Component
ref={ref}
// The dynamic `Component` tag creates an impossible intersection of ref
// types across all TextElement members. We widen to the common base
// (HTMLElement) which is safe — all text elements extend HTMLElement.
ref={ref as React.RefCallback<HTMLElement>}
className={cn(
"text-kumo-default",
KUMO_TEXT_VARIANTS.variant[variant].classes,
Expand Down
28 changes: 28 additions & 0 deletions packages/kumo/src/components/text/text.type-spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,21 @@ const _error = <Text variant="error">Broken</Text>;
const _mono = <Text variant="mono">console.log()</Text>;
const _monoSecondary = <Text variant="mono-secondary">comment</Text>;

// Non-standard text elements — `as` accepts definition list, label, pre, code, etc.
const _dt = <Text as="dt">Term</Text>;
const _dd = <Text as="dd">Definition</Text>;
const _label = <Text as="label">Field label</Text>;
const _code = <Text variant="mono" as="code">const x = 1</Text>;
const _pre = <Text variant="mono" as="pre">preformatted</Text>;
const _li = <Text as="li">List item</Text>;
const _figcaption = <Text variant="secondary" as="figcaption">Caption</Text>;
const _legend = <Text as="legend">Fieldset legend</Text>;
const _em = <Text as="em">Emphasized</Text>;
const _strong = <Text as="strong">Important</Text>;
const _small = <Text variant="secondary" as="small">Fine print</Text>;
const _time = <Text as="time">2026-04-27</Text>;
const _headingAsLabel = <Text variant="heading2" as="label">Form heading</Text>;

// ---------------------------------------------------------------------------
// Negative cases — these MUST NOT compile. The `@ts-expect-error` directive
// asserts that tsc produces an error on the following line; if it doesn't,
Expand Down Expand Up @@ -75,6 +90,19 @@ export const __typeSpec = {
_error,
_mono,
_monoSecondary,
_dt,
_dd,
_label,
_code,
_pre,
_li,
_figcaption,
_legend,
_em,
_strong,
_small,
_time,
_headingAsLabel,
_missingAsH1,
_missingAsH2,
_missingAsH3,
Expand Down
Loading