@hanzo/ui 8.1.3 → 8.1.4
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.
|
@@ -43,15 +43,47 @@ const slot_1 = require("./slot.cjs");
|
|
|
43
43
|
/** Floor for the grown row count. */
|
|
44
44
|
const MIN_ROWS = 3;
|
|
45
45
|
const rowsOf = (v) => typeof v === "string" ? v.split("\n").length : 0;
|
|
46
|
-
const Textarea = /* @__PURE__ */ React.forwardRef(function Textarea({ rows, value, defaultValue, ...props }, ref) {
|
|
46
|
+
const Textarea = /* @__PURE__ */ React.forwardRef(function Textarea({ rows, value, defaultValue, style, ...props }, ref) {
|
|
47
47
|
const [typed, setTyped] = React.useState(() => rowsOf(defaultValue));
|
|
48
48
|
const uncontrolled = value === undefined;
|
|
49
49
|
const invalid = props["aria-invalid"];
|
|
50
|
-
|
|
50
|
+
const node = React.useRef(null);
|
|
51
|
+
const [grown, setGrown] = React.useState(undefined);
|
|
52
|
+
// Both refs reach the same host: ours to measure it, the caller's because a
|
|
53
|
+
// caller that hands one over must still get it. Written as a callback so
|
|
54
|
+
// neither is dropped on the other's behalf.
|
|
55
|
+
const hold = React.useCallback((el) => {
|
|
56
|
+
node.current = el;
|
|
57
|
+
if (typeof ref === "function")
|
|
58
|
+
ref(el);
|
|
59
|
+
else if (ref)
|
|
60
|
+
ref.current = el;
|
|
61
|
+
}, [ref]);
|
|
62
|
+
// The value the field is actually showing — a controlled field is told, an
|
|
63
|
+
// uncontrolled one has to be watched, and both have to re-measure.
|
|
64
|
+
const shown = uncontrolled ? typed : value;
|
|
65
|
+
React.useLayoutEffect(() => {
|
|
66
|
+
const el = node.current;
|
|
67
|
+
if (!el || typeof el.scrollHeight !== "number")
|
|
68
|
+
return;
|
|
69
|
+
// Released to `auto` first, or `scrollHeight` reports the height it already
|
|
70
|
+
// has and the field can only ever grow. Restored in the same synchronous
|
|
71
|
+
// block, before paint, so nothing flashes.
|
|
72
|
+
const held = el.style.height;
|
|
73
|
+
el.style.height = "auto";
|
|
74
|
+
const measured = el.scrollHeight;
|
|
75
|
+
el.style.height = held;
|
|
76
|
+
setGrown((h) => (h === measured ? h : measured));
|
|
77
|
+
}, [shown]);
|
|
78
|
+
return ((0, jsx_runtime_1.jsx)(gui_1.TextArea, { ref: hold, ...(0, slot_1.slot)("textarea"), rows: Math.max(rows ?? MIN_ROWS, uncontrolled ? typed : rowsOf(value)), value: value, defaultValue: defaultValue, onChangeText: uncontrolled ? (t) => setTyped(rowsOf(t)) : undefined, ...control_1.FIELD, width: "100%", minH: 64,
|
|
51
79
|
// The gutter has to be NAMED. Left unsaid it came from gui's
|
|
52
80
|
// `textAreaSizeVariant`, which computes its own horizontal padding and
|
|
53
81
|
// landed on 20 — so the one field a form makes tall was also the one
|
|
54
82
|
// indented differently from the two above it.
|
|
55
|
-
borderColor: invalid && invalid !== "false" ? "$red7" : "$borderColor", placeholderTextColor: "$color10", fontSize: "$3", opacity: props.disabled ? 0.5 : 1, ...props
|
|
83
|
+
borderColor: invalid && invalid !== "false" ? "$red7" : "$borderColor", placeholderTextColor: "$color10", fontSize: "$3", opacity: props.disabled ? 0.5 : 1, ...props,
|
|
84
|
+
// After the caller's props: the measurement is this component's answer to
|
|
85
|
+
// its own question, and a caller's `style` should not silently turn the
|
|
86
|
+
// growth off. A caller wanting a fixed height says so with `height`.
|
|
87
|
+
style: { ...style, ...(grown ? { height: grown } : null) } }));
|
|
56
88
|
});
|
|
57
89
|
exports.Textarea = Textarea;
|
|
@@ -1,12 +1,41 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { TextArea } from "@hanzo/gui";
|
|
3
3
|
/**
|
|
4
|
-
* Textarea — standard-token multiline field.
|
|
4
|
+
* Textarea — standard-token multiline field that grows with what is in it.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* Growth used to be LINE-DRIVEN — `rows` from `value.split('\n').length` — on
|
|
7
|
+
* the stated grounds that counting newlines is the one measurement web, native
|
|
8
|
+
* and desktop agree on. It counts the wrong thing. A draft that wraps without
|
|
9
|
+
* ever containing a newline never raises the count, so the most ordinary input
|
|
10
|
+
* there is, one long paragraph, scrolls inside the floor while you type.
|
|
11
|
+
* Measured in Chromium at 420px wide: 700 characters and no `\n` gave
|
|
12
|
+
* `clientHeight 44` around `scrollHeight 160`.
|
|
13
|
+
*
|
|
14
|
+
* The fix is to measure the content. Two things that look like the measurement
|
|
15
|
+
* are not available here, and both were tried:
|
|
16
|
+
*
|
|
17
|
+
* `onContentSizeChange` is the RN API for exactly this, and
|
|
18
|
+
* react-native-web implements it by reading `scrollHeight`
|
|
19
|
+
* (`dist/exports/TextInput/index.js:192`). It never fires through this
|
|
20
|
+
* component — measured, zero calls, no warning. gui's `TextArea` is
|
|
21
|
+
* `styled(Input, { render: 'textarea' })` from `@hanzogui/web`: a Tamagui
|
|
22
|
+
* styled component over a real DOM `<textarea>`, so RNW's `TextInput` is not
|
|
23
|
+
* on the path and neither is its implementation of that prop.
|
|
24
|
+
*
|
|
25
|
+
* A `height` STYLE PROP would be compiled by gui into one atomic class per
|
|
26
|
+
* distinct pixel value, so a field that grows through forty heights mints
|
|
27
|
+
* forty rules at runtime — none of them in the packaged sheet.
|
|
28
|
+
*
|
|
29
|
+
* So the height is measured off the node and applied INLINE, which is the one
|
|
30
|
+
* channel that carries a per-render value without going through the atomic
|
|
31
|
+
* compiler. `rows` stays the floor and `maxH` stays the ceiling; CSS clamps
|
|
32
|
+
* between them and the field scrolls past the ceiling, so neither bound is
|
|
33
|
+
* re-implemented in script.
|
|
34
|
+
*
|
|
35
|
+
* The measurement is guarded on `scrollHeight` actually being a number, so on a
|
|
36
|
+
* target where the host is not a DOM element the effect no-ops and the
|
|
37
|
+
* line-driven floor is what remains — today's behaviour, unchanged, rather than
|
|
38
|
+
* a broken one.
|
|
10
39
|
*/
|
|
11
40
|
/**
|
|
12
41
|
* Typed from the gui component it renders, exactly as `Input` is — the field
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"textarea.d.ts","sourceRoot":"","sources":["../../../src/backends/gui/textarea.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAUrC
|
|
1
|
+
{"version":3,"file":"textarea.d.ts","sourceRoot":"","sources":["../../../src/backends/gui/textarea.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAUrC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,OAAO,QAAQ,CAAC,EAAE,UAAU,CAAC,GACjF,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,UAAU,CAAC,EAAE,WAAW,GAAG,MAAM,CAAC,CAAA;AAE9D,QAAA,MAAM,QAAQ,wGAiEb,CAAA;AAED,OAAO,EAAE,QAAQ,EAAE,CAAA"}
|
|
@@ -7,16 +7,48 @@ import { slot } from "./slot.js";
|
|
|
7
7
|
/** Floor for the grown row count. */
|
|
8
8
|
const MIN_ROWS = 3;
|
|
9
9
|
const rowsOf = (v) => typeof v === "string" ? v.split("\n").length : 0;
|
|
10
|
-
const Textarea = /* @__PURE__ */ React.forwardRef(function Textarea({ rows, value, defaultValue, ...props }, ref) {
|
|
10
|
+
const Textarea = /* @__PURE__ */ React.forwardRef(function Textarea({ rows, value, defaultValue, style, ...props }, ref) {
|
|
11
11
|
const [typed, setTyped] = React.useState(() => rowsOf(defaultValue));
|
|
12
12
|
const uncontrolled = value === undefined;
|
|
13
13
|
const invalid = props["aria-invalid"];
|
|
14
|
-
|
|
14
|
+
const node = React.useRef(null);
|
|
15
|
+
const [grown, setGrown] = React.useState(undefined);
|
|
16
|
+
// Both refs reach the same host: ours to measure it, the caller's because a
|
|
17
|
+
// caller that hands one over must still get it. Written as a callback so
|
|
18
|
+
// neither is dropped on the other's behalf.
|
|
19
|
+
const hold = React.useCallback((el) => {
|
|
20
|
+
node.current = el;
|
|
21
|
+
if (typeof ref === "function")
|
|
22
|
+
ref(el);
|
|
23
|
+
else if (ref)
|
|
24
|
+
ref.current = el;
|
|
25
|
+
}, [ref]);
|
|
26
|
+
// The value the field is actually showing — a controlled field is told, an
|
|
27
|
+
// uncontrolled one has to be watched, and both have to re-measure.
|
|
28
|
+
const shown = uncontrolled ? typed : value;
|
|
29
|
+
React.useLayoutEffect(() => {
|
|
30
|
+
const el = node.current;
|
|
31
|
+
if (!el || typeof el.scrollHeight !== "number")
|
|
32
|
+
return;
|
|
33
|
+
// Released to `auto` first, or `scrollHeight` reports the height it already
|
|
34
|
+
// has and the field can only ever grow. Restored in the same synchronous
|
|
35
|
+
// block, before paint, so nothing flashes.
|
|
36
|
+
const held = el.style.height;
|
|
37
|
+
el.style.height = "auto";
|
|
38
|
+
const measured = el.scrollHeight;
|
|
39
|
+
el.style.height = held;
|
|
40
|
+
setGrown((h) => (h === measured ? h : measured));
|
|
41
|
+
}, [shown]);
|
|
42
|
+
return (_jsx(TextArea, { ref: hold, ...slot("textarea"), rows: Math.max(rows ?? MIN_ROWS, uncontrolled ? typed : rowsOf(value)), value: value, defaultValue: defaultValue, onChangeText: uncontrolled ? (t) => setTyped(rowsOf(t)) : undefined, ...FIELD, width: "100%", minH: 64,
|
|
15
43
|
// The gutter has to be NAMED. Left unsaid it came from gui's
|
|
16
44
|
// `textAreaSizeVariant`, which computes its own horizontal padding and
|
|
17
45
|
// landed on 20 — so the one field a form makes tall was also the one
|
|
18
46
|
// indented differently from the two above it.
|
|
19
|
-
borderColor: invalid && invalid !== "false" ? "$red7" : "$borderColor", placeholderTextColor: "$color10", fontSize: "$3", opacity: props.disabled ? 0.5 : 1, ...props
|
|
47
|
+
borderColor: invalid && invalid !== "false" ? "$red7" : "$borderColor", placeholderTextColor: "$color10", fontSize: "$3", opacity: props.disabled ? 0.5 : 1, ...props,
|
|
48
|
+
// After the caller's props: the measurement is this component's answer to
|
|
49
|
+
// its own question, and a caller's `style` should not silently turn the
|
|
50
|
+
// growth off. A caller wanting a fixed height says so with `height`.
|
|
51
|
+
style: { ...style, ...(grown ? { height: grown } : null) } }));
|
|
20
52
|
});
|
|
21
53
|
export { Textarea };
|
|
22
54
|
//# sourceMappingURL=textarea.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"textarea.js","sourceRoot":"","sources":["../../../src/backends/gui/textarea.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AACrC,OAAO,EAAE,KAAK,EAAE,MAAM,WAAW,CAAA;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAE7B,qCAAqC;AACrC,MAAM,QAAQ,GAAG,CAAC,CAAA;AAElB,MAAM,MAAM,GAAG,CAAC,CAAU,EAAE,EAAE,CAC5B,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"textarea.js","sourceRoot":"","sources":["../../../src/backends/gui/textarea.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/ui",
|
|
3
|
-
"version": "8.1.
|
|
3
|
+
"version": "8.1.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Hanzo UI — the one canonical Hanzo component library, on @hanzo/gui + @hanzo/design. ONE substrate: every component renders through @hanzo/gui primitives, so the same import works on web, native (expo) and desktop (Tauri). Self-contained (theme.css), presentational, host-agnostic, clean-room.",
|
|
6
6
|
"exports": {
|