import { mergeProps } from "@base-ui/react/merge-props"; import { useRender } from "@base-ui/react/use-render"; import { cn } from "./cn"; /* * Like Badge, a label has no behaviour worth a primitive: the association with * a control is `htmlFor` plus wrapping, which the browser already implements, * so there is nothing for a library to manage. `useRender` is here for the * host-element choice only. * * Base UI does ship `Field.Label`, and that is the right component once a Field * exists — it wires ids and `aria-describedby` to the field's control. It is * not a drop-in for a standalone label, because it needs a `Field.Root` around * it to have anything to point at. Consumers today use labels standalone, so * this stays standalone and Field can absorb it later. * * The one thing that is not free: double-clicking a label selects the text * around it, because the click also lands on the associated control and the * second click extends the selection. Every label implementation guards this * the same way, so it belongs here rather than in each consumer. */ export interface LabelProps extends useRender.ComponentProps<"label"> {} function Label({ className, render, onMouseDown, ...props }: LabelProps) { return useRender({ defaultTagName: "label", render, props: mergeProps<"label">( { className: cn( "inline-flex items-center gap-2", "text-(length:--font-size-control) leading-none font-medium", // A label is a click target for its control, so text selection on it // is never what the user meant. "select-none", // `data-disabled` rather than the `peer-disabled:`/`group-data-` // pairs a shadcn label carries: those depend on the consumer laying // out a `peer` or `group` ancestor, which is a DOM contract this // package cannot state. Every other control here already marks its // disabled state with `data-disabled`. "data-disabled:pointer-events-none data-disabled:opacity-50", className, ), onMouseDown: (event) => { onMouseDown?.(event); // Let a consumer opt out, and never swallow a click on a control that // happens to live inside the label. if (event.defaultPrevented) return; if (event.target instanceof Element && event.target.closest("button, input, select, textarea")) { return; } if (event.detail > 1) event.preventDefault(); }, } as useRender.ComponentProps<"label">, { "data-slot": "label" } as useRender.ComponentProps<"label">, props, ), }); } export { Label };