@godxjp/ui 18.12.15 → 18.12.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/data-display/descriptions.d.ts +10 -1
- package/dist/components/data-display/descriptions.js +20 -4
- package/dist/components/data-entry/form-field.d.ts +1 -1
- package/dist/components/data-entry/form-field.js +12 -12
- package/dist/props/components/data-entry.prop.d.ts +33 -1
- package/dist/tokens/components/descriptions.css +5 -0
- package/package.json +2 -2
|
@@ -5,10 +5,19 @@ export interface DescriptionsProps {
|
|
|
5
5
|
columns?: 1 | 2 | 3;
|
|
6
6
|
/** Label placement within each item. Default `vertical` (label over value). */
|
|
7
7
|
layout?: DescriptionsLayoutProp;
|
|
8
|
+
/**
|
|
9
|
+
* Text alignment of each item's label within its label column — the SAME contract `Form`
|
|
10
|
+
* exposes (gh#294), so a `Descriptions` composed beside a `Form`/`FormField` (a read-only
|
|
11
|
+
* name/email block above an editable role field, for example) can be told to match it. Applies
|
|
12
|
+
* only in `layout="horizontal"` — a vertical label sits above the value and end-aligning it
|
|
13
|
+
* there would read as a mistake, exactly like `Form`'s own contract. Default `"start"`, matching
|
|
14
|
+
* this component's historical unconditional left-align — no existing consumer's render changes.
|
|
15
|
+
*/
|
|
16
|
+
labelAlign?: "start" | "end";
|
|
8
17
|
className?: string;
|
|
9
18
|
children: React.ReactNode;
|
|
10
19
|
}
|
|
11
|
-
export declare function Descriptions({ columns, layout, className, children, }: DescriptionsProps): React.JSX.Element;
|
|
20
|
+
export declare function Descriptions({ columns, layout, labelAlign, className, children, }: DescriptionsProps): React.JSX.Element;
|
|
12
21
|
export declare namespace Descriptions {
|
|
13
22
|
var Item: ({ label, mono, span, className, children, }: DescriptionsItemProps) => React.JSX.Element;
|
|
14
23
|
}
|
|
@@ -2,15 +2,20 @@
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { cn } from "../../lib/utils.js";
|
|
5
|
-
const DescriptionsLayoutContext = React.createContext(
|
|
5
|
+
const DescriptionsLayoutContext = React.createContext({
|
|
6
|
+
layout: "vertical",
|
|
7
|
+
labelAlign: "start"
|
|
8
|
+
});
|
|
6
9
|
function Descriptions({
|
|
7
10
|
columns = 2,
|
|
8
11
|
layout = "vertical",
|
|
12
|
+
labelAlign = "start",
|
|
9
13
|
className,
|
|
10
14
|
children
|
|
11
15
|
}) {
|
|
12
16
|
const colsClass = columns === 1 ? "grid-cols-1" : columns === 3 ? "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3" : "grid-cols-1 sm:grid-cols-2";
|
|
13
|
-
|
|
17
|
+
const context = React.useMemo(() => ({ layout, labelAlign }), [layout, labelAlign]);
|
|
18
|
+
return /* @__PURE__ */ jsx(DescriptionsLayoutContext.Provider, { value: context, children: /* @__PURE__ */ jsx("dl", { className: cn("grid gap-x-6 gap-y-[var(--descriptions-row-gap)]", colsClass, className), children }) });
|
|
14
19
|
}
|
|
15
20
|
Descriptions.Item = function DescriptionsItem({
|
|
16
21
|
label,
|
|
@@ -19,7 +24,7 @@ Descriptions.Item = function DescriptionsItem({
|
|
|
19
24
|
className,
|
|
20
25
|
children
|
|
21
26
|
}) {
|
|
22
|
-
const layout = React.useContext(DescriptionsLayoutContext);
|
|
27
|
+
const { layout, labelAlign } = React.useContext(DescriptionsLayoutContext);
|
|
23
28
|
const spanClass = span === 2 ? "sm:col-span-2" : span === 3 ? "sm:col-span-2 lg:col-span-3" : "";
|
|
24
29
|
return /* @__PURE__ */ jsxs(
|
|
25
30
|
"div",
|
|
@@ -38,7 +43,18 @@ Descriptions.Item = function DescriptionsItem({
|
|
|
38
43
|
className
|
|
39
44
|
),
|
|
40
45
|
children: [
|
|
41
|
-
/* @__PURE__ */ jsx(
|
|
46
|
+
/* @__PURE__ */ jsx(
|
|
47
|
+
"dt",
|
|
48
|
+
{
|
|
49
|
+
className: cn(
|
|
50
|
+
"text-muted-foreground text-xs",
|
|
51
|
+
// `end`-align only ever applies in horizontal layout — same guard `Form` uses, so a
|
|
52
|
+
// vertical label (already above its value) never mistakenly right-aligns (gh#294).
|
|
53
|
+
layout === "horizontal" && labelAlign === "end" && "text-end"
|
|
54
|
+
),
|
|
55
|
+
children: label
|
|
56
|
+
}
|
|
57
|
+
),
|
|
42
58
|
/* @__PURE__ */ jsx("dd", { className: cn("text-sm break-all", mono && "font-mono"), children })
|
|
43
59
|
]
|
|
44
60
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import type { FormFieldProp } from "../../props/components/data-entry.prop.js";
|
|
3
3
|
export type { FormFieldProp, FormFieldProp as FormFieldProps, } from "../../props/components/data-entry.prop.js";
|
|
4
|
-
export declare function FormField({ id, label, required, helper, error, labelAddon, layout: layoutProp, labelWidth: labelWidthProp, controlWidth: controlWidthProp, colSpan, className, children, }: FormFieldProp): React.JSX.Element;
|
|
4
|
+
export declare function FormField({ id, label, required, helper, error, labelAddon, layout: layoutProp, labelWidth: labelWidthProp, controlWidth: controlWidthProp, colSpan, className, children, staticText, }: FormFieldProp): React.JSX.Element;
|
|
@@ -19,7 +19,8 @@ function FormField({
|
|
|
19
19
|
controlWidth: controlWidthProp,
|
|
20
20
|
colSpan,
|
|
21
21
|
className,
|
|
22
|
-
children
|
|
22
|
+
children,
|
|
23
|
+
staticText
|
|
23
24
|
}) {
|
|
24
25
|
const form = useFormLayout();
|
|
25
26
|
const layout = layoutProp ?? form?.layout ?? "vertical";
|
|
@@ -32,14 +33,19 @@ function FormField({
|
|
|
32
33
|
const labelId = `${resolvedId}-label`;
|
|
33
34
|
const helperId = helper ? `${resolvedId}-helper` : void 0;
|
|
34
35
|
const errorId = error ? `${resolvedId}-error` : void 0;
|
|
35
|
-
|
|
36
|
+
const isStatic = staticText !== void 0;
|
|
37
|
+
if (!isStatic && typeof process !== "undefined" && process.env?.NODE_ENV !== "production" && !React.isValidElement(children)) {
|
|
36
38
|
console.warn(
|
|
37
|
-
"FormField expects a single React element child to receive aria-describedby/aria-errormessage; the helper text and error message will not be associated with the control."
|
|
39
|
+
"FormField expects a single React element child to receive aria-describedby/aria-errormessage; the helper text and error message will not be associated with the control. Pass plain text via `staticText` instead of `children` for a read-only value row."
|
|
38
40
|
);
|
|
39
41
|
}
|
|
40
42
|
const childProps = React.isValidElement(children) ? children.props : void 0;
|
|
41
43
|
const mergeIds = mergeAriaIds;
|
|
42
|
-
const childWithA11y =
|
|
44
|
+
const childWithA11y = isStatic ? (
|
|
45
|
+
// Byte-for-byte the same value typography as `Descriptions.Item`'s `dd` (gh#294), so a
|
|
46
|
+
// read-only FormField row and a Descriptions value are indistinguishable when mixed.
|
|
47
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm break-all", children: staticText })
|
|
48
|
+
) : React.isValidElement(children) ? React.cloneElement(children, {
|
|
43
49
|
// The label is associated via aria-labelledby (not <label for>): composite
|
|
44
50
|
// controls (Radio.Group, checkbox lists, range pairs) have no labelable root,
|
|
45
51
|
// and a dangling `for` triggers Chrome's "Incorrect use of <label>" issue.
|
|
@@ -52,14 +58,8 @@ function FormField({
|
|
|
52
58
|
"aria-label": childProps?.["aria-label"] ?? (typeof label === "string" ? label : void 0),
|
|
53
59
|
// Helper and error can coexist: helper stays on aria-describedby, the error on
|
|
54
60
|
// aria-errormessage (surfaced when aria-invalid is true).
|
|
55
|
-
"aria-describedby": mergeIds(
|
|
56
|
-
|
|
57
|
-
helperId
|
|
58
|
-
),
|
|
59
|
-
"aria-errormessage": mergeIds(
|
|
60
|
-
childProps?.["aria-errormessage"],
|
|
61
|
-
errorId
|
|
62
|
-
),
|
|
61
|
+
"aria-describedby": mergeIds(childProps?.["aria-describedby"], helperId),
|
|
62
|
+
"aria-errormessage": mergeIds(childProps?.["aria-errormessage"], errorId),
|
|
63
63
|
"aria-required": required ? true : childProps?.["aria-required"],
|
|
64
64
|
"aria-invalid": error ? true : childProps?.["aria-invalid"]
|
|
65
65
|
}) : children;
|
|
@@ -90,7 +90,17 @@ export type FormProp = React.FormHTMLAttributes<HTMLFormElement> & {
|
|
|
90
90
|
density?: DensityProp;
|
|
91
91
|
className?: ClassNameProp;
|
|
92
92
|
};
|
|
93
|
-
/**
|
|
93
|
+
/**
|
|
94
|
+
* @see FormField — exactly one of `children` (an interactive control) or `staticText` (gh#294):
|
|
95
|
+
* a read-only VALUE row inside the same Form, styled to match `Descriptions.Item`'s value
|
|
96
|
+
* typography (`text-sm break-all`) byte-for-byte. This is the "mixed read-only + editable fields
|
|
97
|
+
* on one form" case (an immutable name/email row above an editable role Select, for example) —
|
|
98
|
+
* putting the read-only rows through FormField itself, not a separate `Descriptions` composed
|
|
99
|
+
* alongside it, gets perfect layout/labelAlign/row-gap sync FOR FREE because it IS the same
|
|
100
|
+
* component reading the same Form context, rather than two components whose contracts need
|
|
101
|
+
* reconciling. `staticText` skips FormField's control a11y wiring (id/aria-labelledby/
|
|
102
|
+
* aria-describedby cloning) entirely — there is no real control to label, so none of that applies.
|
|
103
|
+
*/
|
|
94
104
|
export type FormFieldProp = {
|
|
95
105
|
/** Optional — auto-generated and injected into the child control when omitted. */
|
|
96
106
|
id?: IdProp;
|
|
@@ -110,6 +120,28 @@ export type FormFieldProp = {
|
|
|
110
120
|
colSpan?: number;
|
|
111
121
|
className?: ClassNameProp;
|
|
112
122
|
children: React.ReactNode;
|
|
123
|
+
staticText?: never;
|
|
124
|
+
} | {
|
|
125
|
+
/** Optional — auto-generated and injected into the child control when omitted. */
|
|
126
|
+
id?: IdProp;
|
|
127
|
+
label: LabelProp;
|
|
128
|
+
required?: RequiredProp;
|
|
129
|
+
helper?: HelperProp;
|
|
130
|
+
error?: ErrorProp;
|
|
131
|
+
/** Optional control rendered inline after the label (e.g. a help button). */
|
|
132
|
+
labelAddon?: React.ReactNode;
|
|
133
|
+
/** Override the Form's layout for this field only. */
|
|
134
|
+
layout?: FormLayoutProp;
|
|
135
|
+
/** Override the Form's label width for this field (horizontal layout). */
|
|
136
|
+
labelWidth?: WidthProp;
|
|
137
|
+
/** Override the Form's control width for this field. */
|
|
138
|
+
controlWidth?: WidthProp;
|
|
139
|
+
/** Span N columns when inside a `columns` Form grid. */
|
|
140
|
+
colSpan?: number;
|
|
141
|
+
className?: ClassNameProp;
|
|
142
|
+
children?: never;
|
|
143
|
+
/** Read-only value — renders as `Descriptions.Item`-matched text instead of a control. */
|
|
144
|
+
staticText: React.ReactNode;
|
|
113
145
|
};
|
|
114
146
|
/** @see SearchInput */
|
|
115
147
|
export type SearchInputProp = FieldA11yProps & {
|
|
@@ -6,4 +6,9 @@
|
|
|
6
6
|
* A rem value gives a fixed aligned column; set `max-content` to size each label to its text.
|
|
7
7
|
* (rule #44/#45 — a service theme tunes it here instead of forking CSS.) */
|
|
8
8
|
--descriptions-label-width: 8rem;
|
|
9
|
+
/* Row-to-row gap (gh#294). Default = the historical hardcoded `gap-y-3`, so nothing changes
|
|
10
|
+
* visually by default. A consumer composing Descriptions beside a Form/FormField (a read-only
|
|
11
|
+
* block above an editable field on the same card) retunes this to `var(--space-4)` to match
|
|
12
|
+
* Form's own field-to-field rhythm instead of the two blocks reading as visually unrelated. */
|
|
13
|
+
--descriptions-row-gap: var(--space-3);
|
|
9
14
|
}
|