@lotics/ui 23.0.0 → 23.1.1
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/docs/catalog.md +13 -2
- package/package.json +3 -3
- package/src/file_row.tsx +29 -4
- package/src/inline_edit.tsx +50 -8
- package/src/inline_text_input.tsx +16 -3
- package/src/input_metrics.ts +17 -0
- package/src/text_input_field.tsx +2 -3
- package/src/text_utils.ts +6 -0
package/docs/catalog.md
CHANGED
|
@@ -95,7 +95,14 @@ Batch draft-form state → `useForm`.
|
|
|
95
95
|
|
|
96
96
|
### Edit a record's fields in place
|
|
97
97
|
|
|
98
|
-
The `Inline*` family: `InlineTextInput
|
|
98
|
+
The `Inline*` family: `InlineTextInput` (**`numberOfLines`** — the line budget for a value a
|
|
99
|
+
reader must read WHOLE rather than recognise at a glance: a payment term, an address, a clause.
|
|
100
|
+
Above 1 the resting box RESERVES that many lines — the full editor height, not the height this
|
|
101
|
+
particular value needs — so clicking in moves neither the field nor anything below it; Enter
|
|
102
|
+
then inserts a newline and the value commits on blur. Both halves matter: a field that only
|
|
103
|
+
goes multiline once focused hides the text from the reader who never clicks, and one that
|
|
104
|
+
reserves less than it opens to shoves the rest of the form down the page on every click),
|
|
105
|
+
`InlineNumberInput`, `InlineSelect` (single or
|
|
99
106
|
`multi`), `InlineMemberSelect`, `InlineDatePicker`, `InlineTimePicker`; a
|
|
100
107
|
READ-ONLY field in that same column uses `InlineStatic` (matches the editor box exactly, no
|
|
101
108
|
input chrome, so it aligns pixel-for-pixel). A stack of labelled field rows lives in
|
|
@@ -246,7 +253,11 @@ scoped to that region's focus; the region-wrapper next to `FileDropzone`'s dedic
|
|
|
246
253
|
"files land without hunting for a dropzone", `FileRow` (a horizontal file/document LINE —
|
|
247
254
|
badge-or-placeholder + name + meta + a composable `trailing` slot for a status badge /
|
|
248
255
|
action / remove; `onPress` makes the whole row a pressable door, `trailing` stays an
|
|
249
|
-
independently-pressable sibling; for checklists & readable lists
|
|
256
|
+
independently-pressable sibling; for checklists & readable lists. **`size`** — `sm` (default)
|
|
257
|
+
is the compact attachment line for many files scanned as a list; **`md` is the document-desk
|
|
258
|
+
row** (taller badge, `ListItem` height) for the FEW rows that ARE the section's subject — an
|
|
259
|
+
expected-document checklist, a slot a button fills. A two-row section at `sm` reads as an
|
|
260
|
+
appendix to the page rather than its point), `FileBadge` (the two-tone
|
|
250
261
|
type mark), `FilePreview` / `FileGalleryModal`, `ImageGallery`; picking is `pickFiles`
|
|
251
262
|
(`@lotics/ui/file_picker` — opens the browser picker and resolves the chosen `File[]`, the
|
|
252
263
|
imperative half behind every Add-file CTA); for gated CRUD compose locally with
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/ui",
|
|
3
|
-
"version": "23.
|
|
3
|
+
"version": "23.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./vite": {
|
|
@@ -266,7 +266,7 @@
|
|
|
266
266
|
},
|
|
267
267
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
268
268
|
"dependencies": {
|
|
269
|
-
"@lotics/docx": "^0.
|
|
269
|
+
"@lotics/docx": "^0.2.0",
|
|
270
270
|
"@lotics/xlsx": "^0.1.0",
|
|
271
271
|
"ai": "^7.0.30",
|
|
272
272
|
"mdast-util-from-markdown": "^2.0.3",
|
|
@@ -275,7 +275,7 @@
|
|
|
275
275
|
"mdast-util-gfm-table": "^2.0.0",
|
|
276
276
|
"mdast-util-gfm-task-list-item": "^2.0.0",
|
|
277
277
|
"micromark-extension-gfm": "^3.0.0",
|
|
278
|
-
"pdfjs-dist": "^6.
|
|
278
|
+
"pdfjs-dist": "^6.2.108",
|
|
279
279
|
"react-markdown": "^10.0.0"
|
|
280
280
|
},
|
|
281
281
|
"peerDependencies": {
|
package/src/file_row.tsx
CHANGED
|
@@ -25,6 +25,14 @@ export interface FileRowProps {
|
|
|
25
25
|
* control. An independently-interactive SIBLING: never swallowed by the row
|
|
26
26
|
* press (a button never nests in the door button). */
|
|
27
27
|
trailing?: React.ReactNode;
|
|
28
|
+
/**
|
|
29
|
+
* Row weight. `"sm"` (default) is the compact attachment line — many files
|
|
30
|
+
* scanned as a list. `"md"` is the DOCUMENT-DESK row: a taller badge and a
|
|
31
|
+
* `ListItem`-height body, for the few rows that are the surface's subject
|
|
32
|
+
* rather than an appendix to it — an expected-document checklist, a slot a
|
|
33
|
+
* button fills. A one-or-two-row section at `sm` reads as an afterthought.
|
|
34
|
+
*/
|
|
35
|
+
size?: "sm" | "md";
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
/**
|
|
@@ -37,7 +45,17 @@ export interface FileRowProps {
|
|
|
37
45
|
* a button/menu in it is pressed independently (the door + sibling a11y pattern —
|
|
38
46
|
* see `PressableRow`). For square thumbnail tiles use `FileThumbnailGrid` / `FileGrid`.
|
|
39
47
|
*/
|
|
40
|
-
export function FileRow({
|
|
48
|
+
export function FileRow({
|
|
49
|
+
name,
|
|
50
|
+
meta,
|
|
51
|
+
mimeType,
|
|
52
|
+
placeholder,
|
|
53
|
+
isTemplate,
|
|
54
|
+
onPress,
|
|
55
|
+
trailing,
|
|
56
|
+
size = "sm",
|
|
57
|
+
}: FileRowProps) {
|
|
58
|
+
const md = size === "md";
|
|
41
59
|
const [hovered, setHovered] = useState(false);
|
|
42
60
|
const [pressed, setPressed] = useState(false);
|
|
43
61
|
const { focusVisible, focusProps } = useFocusRing();
|
|
@@ -50,7 +68,7 @@ export function FileRow({ name, meta, mimeType, placeholder, isTemplate, onPress
|
|
|
50
68
|
|
|
51
69
|
const content = (
|
|
52
70
|
<>
|
|
53
|
-
<FileBadge size={30} mimeType={mimeType} placeholder={placeholder} isTemplate={isTemplate} />
|
|
71
|
+
<FileBadge size={md ? 38 : 30} mimeType={mimeType} placeholder={placeholder} isTemplate={isTemplate} />
|
|
54
72
|
<View style={styles.text}>
|
|
55
73
|
<Text size="sm" weight="medium" numberOfLines={1}>
|
|
56
74
|
{name}
|
|
@@ -66,7 +84,7 @@ export function FileRow({ name, meta, mimeType, placeholder, isTemplate, onPress
|
|
|
66
84
|
|
|
67
85
|
if (!onPress) {
|
|
68
86
|
return (
|
|
69
|
-
<View style={styles.row}>
|
|
87
|
+
<View style={[styles.row, md && styles.rowMd]}>
|
|
70
88
|
{content}
|
|
71
89
|
{trailing}
|
|
72
90
|
</View>
|
|
@@ -75,7 +93,12 @@ export function FileRow({ name, meta, mimeType, placeholder, isTemplate, onPress
|
|
|
75
93
|
|
|
76
94
|
return (
|
|
77
95
|
<View
|
|
78
|
-
style={[
|
|
96
|
+
style={[
|
|
97
|
+
styles.pressableRow,
|
|
98
|
+
md && styles.rowMd,
|
|
99
|
+
hovered && styles.rowHovered,
|
|
100
|
+
pressed && styles.rowPressed,
|
|
101
|
+
]}
|
|
79
102
|
{...mouseProps}
|
|
80
103
|
>
|
|
81
104
|
<Pressable
|
|
@@ -110,6 +133,8 @@ const styles = StyleSheet.create({
|
|
|
110
133
|
borderRadius: 8,
|
|
111
134
|
...({ transitionDuration: "0.1s", transitionProperty: "background-color" } as object),
|
|
112
135
|
},
|
|
136
|
+
// The `ListItem` beat — for a document row that IS the section's subject.
|
|
137
|
+
rowMd: { minHeight: 56, paddingVertical: 10 },
|
|
113
138
|
rowHovered: { backgroundColor: colors.zinc["50"] },
|
|
114
139
|
rowPressed: { backgroundColor: colors.zinc["100"] },
|
|
115
140
|
// The accessible "Open" door — fills the row left of the trailing sibling.
|
package/src/inline_edit.tsx
CHANGED
|
@@ -7,7 +7,7 @@ import { ActivityIndicator } from "./activity_indicator";
|
|
|
7
7
|
import { FocusRingPressable } from "./focus_ring_pressable";
|
|
8
8
|
import { colors } from "./colors";
|
|
9
9
|
import { FOCUS_RING, CONTROL_RADIUS, HOVER_BORDER, CONTROL_TRANSITION } from "./control_surface";
|
|
10
|
-
import { fontFamilyRegular, getInputTextStyle, getTextColor, type TextColor } from "./text_utils";
|
|
10
|
+
import { fontFamilyRegular, getInputTextStyle, getMultilineInputHeight, getTextColor, type TextColor } from "./text_utils";
|
|
11
11
|
import { getInteractionModality } from "./interaction_modality";
|
|
12
12
|
import { shouldOpenOnFocus, shouldRestoreFocusOnClose } from "./inline_focus";
|
|
13
13
|
import { trackCommit } from "./pending_commits";
|
|
@@ -160,6 +160,14 @@ interface InlineEditFrameProps {
|
|
|
160
160
|
struck?: boolean;
|
|
161
161
|
/** How much frame shows at rest — see {@link InlineEditVariant}. Default "framed". */
|
|
162
162
|
variant?: InlineEditVariant;
|
|
163
|
+
/**
|
|
164
|
+
* How many lines the RESTING value may occupy before it truncates. Default 1.
|
|
165
|
+
* Raise it for a value a reader has to read whole — a payment term, an
|
|
166
|
+
* address — rather than recognise at a glance; the editor should open to the
|
|
167
|
+
* same height so the row does not reflow on click.
|
|
168
|
+
*/
|
|
169
|
+
numberOfLines?: number;
|
|
170
|
+
|
|
163
171
|
/** Right-aligned rest affordance shown in VIEW mode (e.g. a clock for a time
|
|
164
172
|
* field) — a hover-independent cue the field opens a picker. Omit for plain
|
|
165
173
|
* text/number inputs (typing IS the affordance). */
|
|
@@ -187,15 +195,16 @@ function fieldContent(o: {
|
|
|
187
195
|
struck?: boolean;
|
|
188
196
|
tone?: TextColor;
|
|
189
197
|
trailing?: ReactNode;
|
|
198
|
+
numberOfLines?: number;
|
|
190
199
|
}) {
|
|
191
|
-
const { display, placeholder, struck, tone, trailing } = o;
|
|
200
|
+
const { display, placeholder, struck, tone, trailing, numberOfLines } = o;
|
|
192
201
|
const isEmptyString = typeof display === "string" && display === "";
|
|
193
202
|
return (
|
|
194
203
|
<>
|
|
195
204
|
{isEmptyString && placeholder != null && typeof placeholder !== "string" ? (
|
|
196
205
|
<View style={styles.viewNode}>{placeholder}</View>
|
|
197
206
|
) : typeof display === "string" ? (
|
|
198
|
-
<Text numberOfLines={1} style={[viewTextStyle, display ? null : styles.placeholder, struck ? styles.struck : null, tone && display && !struck ? { color: getTextColor(tone) } : null]}>
|
|
207
|
+
<Text numberOfLines={numberOfLines ?? 1} style={[viewTextStyle, display ? null : styles.placeholder, struck ? styles.struck : null, tone && display && !struck ? { color: getTextColor(tone) } : null]}>
|
|
199
208
|
{display || (typeof placeholder === "string" ? placeholder : "") || "—"}
|
|
200
209
|
</Text>
|
|
201
210
|
) : (
|
|
@@ -301,6 +310,14 @@ interface InlineEditViewProps {
|
|
|
301
310
|
accessibilityLabel?: string;
|
|
302
311
|
/** How much frame shows at rest — see {@link InlineEditVariant}. Default "framed". */
|
|
303
312
|
variant?: InlineEditVariant;
|
|
313
|
+
/**
|
|
314
|
+
* How many lines the RESTING value may occupy before it truncates. Default 1.
|
|
315
|
+
* Raise it for a value a reader has to read whole — a payment term, an
|
|
316
|
+
* address — rather than recognise at a glance; the editor should open to the
|
|
317
|
+
* same height so the row does not reflow on click.
|
|
318
|
+
*/
|
|
319
|
+
numberOfLines?: number;
|
|
320
|
+
|
|
304
321
|
/**
|
|
305
322
|
* A handle on the FIELD's outer box, for a caller that anchors an overlay to
|
|
306
323
|
* it — a select's popover inherits the trigger's width, and with `actions` the
|
|
@@ -355,8 +372,31 @@ interface InlineEditViewProps {
|
|
|
355
372
|
* the overlay trigger for the select/date inline editors (it forwards ref +
|
|
356
373
|
* onPress to a `PopoverTrigger`).
|
|
357
374
|
*/
|
|
375
|
+
/**
|
|
376
|
+
* The resting box for a MULTI-LINE field.
|
|
377
|
+
*
|
|
378
|
+
* A field that changes size when you click it moves everything below it, and on
|
|
379
|
+
* a record surface that is the whole form jumping under the pointer. So the box
|
|
380
|
+
* must be identical in both modes — which means the RESTING box reserves the
|
|
381
|
+
* editor's full height, not the height this particular value happens to need.
|
|
382
|
+
* `TextInputField` sizes a multiline input to `numberOfLines * lineHeight + 16`;
|
|
383
|
+
* this reproduces that number exactly, and top-aligns the text the way a
|
|
384
|
+
* textarea does, so neither the box nor the first line moves.
|
|
385
|
+
*
|
|
386
|
+
* Single-line fields are untouched: `INLINE_CONTROL_HEIGHT` already equals the
|
|
387
|
+
* input's own height, which is why they never had this bug.
|
|
388
|
+
*/
|
|
389
|
+
function multilineBox(numberOfLines: number | undefined): ViewStyle | null {
|
|
390
|
+
if (numberOfLines == null || numberOfLines <= 1) return null;
|
|
391
|
+
return {
|
|
392
|
+
minHeight: getMultilineInputHeight(numberOfLines, false),
|
|
393
|
+
alignItems: "flex-start",
|
|
394
|
+
paddingVertical: 8,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
358
398
|
export function InlineEditView(props: InlineEditViewProps) {
|
|
359
|
-
const { display, placeholder, onPress, disabled, accessibilityLabel, trailing, actions, active, struck, variant = "framed", tone, onFocus, ref, anchorRef } = props;
|
|
399
|
+
const { display, placeholder, onPress, disabled, accessibilityLabel, trailing, actions, active, struck, variant = "framed", tone, onFocus, ref, anchorRef, numberOfLines } = props;
|
|
360
400
|
// Stop the press here so an inline editor nested in a pressable row (a task
|
|
361
401
|
// row that expands on press) edits the field instead of triggering the row.
|
|
362
402
|
const handlePress = onPress
|
|
@@ -365,7 +405,7 @@ export function InlineEditView(props: InlineEditViewProps) {
|
|
|
365
405
|
onPress();
|
|
366
406
|
}
|
|
367
407
|
: undefined;
|
|
368
|
-
const content = fieldContent({ display, placeholder, struck, tone, trailing });
|
|
408
|
+
const content = fieldContent({ display, placeholder, struck, tone, trailing, numberOfLines });
|
|
369
409
|
const surface = fieldSurface({ variant, disabled, active });
|
|
370
410
|
|
|
371
411
|
// WITH VERBS: the shell owns the surface, the pressable is just the value
|
|
@@ -387,7 +427,7 @@ export function InlineEditView(props: InlineEditViewProps) {
|
|
|
387
427
|
onFocus={onFocus}
|
|
388
428
|
accessibilityRole="button"
|
|
389
429
|
accessibilityLabel={accessibilityLabel}
|
|
390
|
-
style={styles.viewInner}
|
|
430
|
+
style={[styles.viewInner, multilineBox(numberOfLines)]}
|
|
391
431
|
>
|
|
392
432
|
{content}
|
|
393
433
|
</Pressable>
|
|
@@ -407,7 +447,7 @@ export function InlineEditView(props: InlineEditViewProps) {
|
|
|
407
447
|
accessibilityRole="button"
|
|
408
448
|
accessibilityLabel={accessibilityLabel}
|
|
409
449
|
userSelect="none"
|
|
410
|
-
style={(state) => surface(state.hovered ?? false)}
|
|
450
|
+
style={(state) => [surface(state.hovered ?? false), multilineBox(numberOfLines)]}
|
|
411
451
|
>
|
|
412
452
|
{content}
|
|
413
453
|
</FocusRingPressable>
|
|
@@ -442,6 +482,7 @@ export function InlineEditFrame(props: InlineEditFrameProps) {
|
|
|
442
482
|
variant,
|
|
443
483
|
affordance,
|
|
444
484
|
actions,
|
|
485
|
+
numberOfLines,
|
|
445
486
|
} = props;
|
|
446
487
|
|
|
447
488
|
const viewRef = useRef<View>(null);
|
|
@@ -482,6 +523,7 @@ export function InlineEditFrame(props: InlineEditFrameProps) {
|
|
|
482
523
|
accessibilityLabel={accessibilityLabel}
|
|
483
524
|
struck={struck}
|
|
484
525
|
trailing={affordance}
|
|
526
|
+
numberOfLines={numberOfLines}
|
|
485
527
|
/>
|
|
486
528
|
);
|
|
487
529
|
}
|
|
@@ -523,7 +565,7 @@ export function InlineEditFrame(props: InlineEditFrameProps) {
|
|
|
523
565
|
accessibilityLabel={accessibilityLabel}
|
|
524
566
|
style={styles.viewInner}
|
|
525
567
|
>
|
|
526
|
-
{fieldContent({ display, placeholder, struck, trailing: affordance })}
|
|
568
|
+
{fieldContent({ display, placeholder, struck, trailing: affordance, numberOfLines })}
|
|
527
569
|
</Pressable>
|
|
528
570
|
)}
|
|
529
571
|
</InlineFieldShell>
|
|
@@ -17,6 +17,14 @@ export interface InlineTextInputProps {
|
|
|
17
17
|
struck?: boolean;
|
|
18
18
|
/** How much frame shows at rest — see {@link InlineEditVariant}. Default "framed". */
|
|
19
19
|
variant?: InlineEditVariant;
|
|
20
|
+
/**
|
|
21
|
+
* Line budget for a value a reader has to read WHOLE rather than recognise —
|
|
22
|
+
* a payment term, an address, a clause. Default 1 (the single-line field).
|
|
23
|
+
* Above 1 the resting value wraps to that many lines AND the editor opens
|
|
24
|
+
* multiline at the same height, so the row does not reflow on click. Enter
|
|
25
|
+
* then inserts a newline; the field commits on blur (or the ✓ in "buttons").
|
|
26
|
+
*/
|
|
27
|
+
numberOfLines?: number;
|
|
20
28
|
accessibilityLabel?: string;
|
|
21
29
|
/** Verbs on the field's surface — an `InlineButton` Copy on a reference a reader
|
|
22
30
|
* quotes elsewhere. See `InlineEditView.actions`: rendered in BOTH modes so the
|
|
@@ -32,16 +40,18 @@ export interface InlineTextInputProps {
|
|
|
32
40
|
* value in a dense record / detail surface.
|
|
33
41
|
*/
|
|
34
42
|
export function InlineTextInput(props: InlineTextInputProps) {
|
|
35
|
-
const { value, onSave, placeholder, controls = "blur", disabled, struck, accessibilityLabel , variant, actions } = props;
|
|
43
|
+
const { value, onSave, placeholder, controls = "blur", disabled, struck, accessibilityLabel , variant, actions, numberOfLines } = props;
|
|
44
|
+
const multiline = (numberOfLines ?? 1) > 1;
|
|
36
45
|
const edit = useInlineEdit<string>({ value, onSave });
|
|
37
46
|
|
|
38
47
|
const onKeyPress = useCallback(
|
|
39
48
|
(e: NativeSyntheticEvent<TextInputKeyPressEventData>) => {
|
|
40
49
|
const key = e.nativeEvent.key;
|
|
41
50
|
if (key === "Escape") edit.cancel();
|
|
42
|
-
|
|
51
|
+
// On a multiline field Enter is a NEWLINE; the value commits on blur.
|
|
52
|
+
else if (key === "Enter" && !multiline) void edit.commit();
|
|
43
53
|
},
|
|
44
|
-
[edit],
|
|
54
|
+
[edit, multiline],
|
|
45
55
|
);
|
|
46
56
|
|
|
47
57
|
// Commit on blur (the ergonomic default for moving through a form). In
|
|
@@ -66,6 +76,7 @@ export function InlineTextInput(props: InlineTextInputProps) {
|
|
|
66
76
|
disabled={disabled}
|
|
67
77
|
struck={struck}
|
|
68
78
|
variant={variant}
|
|
79
|
+
numberOfLines={numberOfLines}
|
|
69
80
|
accessibilityLabel={accessibilityLabel}
|
|
70
81
|
actions={actions}
|
|
71
82
|
>
|
|
@@ -75,6 +86,8 @@ export function InlineTextInput(props: InlineTextInputProps) {
|
|
|
75
86
|
onBlur={onBlur}
|
|
76
87
|
onKeyPress={onKeyPress}
|
|
77
88
|
autoFocus
|
|
89
|
+
multiline={multiline}
|
|
90
|
+
numberOfLines={numberOfLines}
|
|
78
91
|
placeholder={placeholder}
|
|
79
92
|
accessibilityLabel={accessibilityLabel}
|
|
80
93
|
// With verbs on the field, the FRAME owns the surface and the ring.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Vertical breathing room inside an input, per side. */
|
|
2
|
+
export const INPUT_VERTICAL_PADDING = 8;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The height a MULTI-LINE input occupies, for `numberOfLines` lines of text.
|
|
6
|
+
*
|
|
7
|
+
* One formula with two callers on purpose: `TextInputField` sizes the editor
|
|
8
|
+
* with it, and an inline field's RESTING box reserves the same number. When
|
|
9
|
+
* each computed its own, the resting box came out 22px shorter and every click
|
|
10
|
+
* on a payment-term field shoved the rest of the form down the page.
|
|
11
|
+
*
|
|
12
|
+
* RN-free so it can be tested directly — the caller supplies the line height,
|
|
13
|
+
* which is the only platform-dependent part.
|
|
14
|
+
*/
|
|
15
|
+
export function multilineInputHeight(numberOfLines: number, lineHeight: number): number {
|
|
16
|
+
return numberOfLines * lineHeight + INPUT_VERTICAL_PADDING * 2;
|
|
17
|
+
}
|
package/src/text_input_field.tsx
CHANGED
|
@@ -13,7 +13,7 @@ import { Ref, useCallback, useState } from "react";
|
|
|
13
13
|
import { Icon, IconName } from "./icon";
|
|
14
14
|
import { IconButton } from "./icon_button";
|
|
15
15
|
import { ShortcutBadge } from "./shortcut_badge";
|
|
16
|
-
import { fontFamilyRegular,
|
|
16
|
+
import { fontFamilyRegular, getInputTextStyle, getMultilineInputHeight } from "./text_utils";
|
|
17
17
|
import { useScreenSize } from "./use_screen_size";
|
|
18
18
|
import { useAutoGrowHeight } from "./use_auto_grow_height";
|
|
19
19
|
import { useFormField } from "./form_field";
|
|
@@ -84,7 +84,6 @@ export function TextInputField(props: TextInputFieldProps) {
|
|
|
84
84
|
if (ariaAutocomplete !== undefined) webAriaAttrs["aria-autocomplete"] = ariaAutocomplete;
|
|
85
85
|
|
|
86
86
|
const { small } = useScreenSize();
|
|
87
|
-
const lineHeight = getInputLineHeight(small);
|
|
88
87
|
const binding = useFormField();
|
|
89
88
|
// Text-like input: ring on ANY focus (browsers treat typing-capable fields as
|
|
90
89
|
// focus-visible even on pointer focus), so a clicked field shows its ring.
|
|
@@ -99,7 +98,7 @@ export function TextInputField(props: TextInputFieldProps) {
|
|
|
99
98
|
|
|
100
99
|
const minHeight =
|
|
101
100
|
numberOfLines && numberOfLines > 1
|
|
102
|
-
? numberOfLines
|
|
101
|
+
? getMultilineInputHeight(numberOfLines, small)
|
|
103
102
|
: undefined;
|
|
104
103
|
|
|
105
104
|
const autoGrowResult = useAutoGrowHeight({
|
package/src/text_utils.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Platform, TextStyle } from "react-native";
|
|
2
2
|
import { colors } from "./colors";
|
|
3
|
+
import { multilineInputHeight } from "./input_metrics";
|
|
3
4
|
|
|
4
5
|
export type TextColor =
|
|
5
6
|
| "default"
|
|
@@ -112,6 +113,11 @@ export function getInputTextStyle(): TextStyle {
|
|
|
112
113
|
*
|
|
113
114
|
* @param isSmallScreen - Whether the screen is small (mobile). Get this from useScreenSize().small
|
|
114
115
|
*/
|
|
116
|
+
/** {@link multilineInputHeight} at this platform's input line height. */
|
|
117
|
+
export function getMultilineInputHeight(numberOfLines: number, isSmallScreen: boolean): number {
|
|
118
|
+
return multilineInputHeight(numberOfLines, getInputLineHeight(isSmallScreen));
|
|
119
|
+
}
|
|
120
|
+
|
|
115
121
|
export function getInputLineHeight(isSmallScreen: boolean): number {
|
|
116
122
|
if (Platform.OS !== "web") {
|
|
117
123
|
return INPUT_LINE_HEIGHT_DESKTOP;
|