@jobber/components-native 0.90.1-JOB-142149-547612b.8 → 0.91.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.
Files changed (44) hide show
  1. package/dist/package.json +2 -2
  2. package/dist/src/Form/Form.js +1 -1
  3. package/dist/src/Form/components/FormBody/FormBody.js +5 -5
  4. package/dist/src/FormatFile/components/MediaView/MediaView.js +22 -5
  5. package/dist/src/InputDate/InputDate.js +2 -2
  6. package/dist/src/InputFieldWrapper/InputFieldWrapper.js +14 -12
  7. package/dist/src/InputFieldWrapper/components/Prefix/Prefix.js +5 -2
  8. package/dist/src/InputFieldWrapper/components/Suffix/Suffix.js +5 -2
  9. package/dist/src/InputPressable/InputPressable.js +20 -8
  10. package/dist/src/InputPressable/InputPressable.style.js +3 -0
  11. package/dist/src/InputText/InputText.js +22 -11
  12. package/dist/src/InputText/InputText.style.js +4 -0
  13. package/dist/src/InputTime/InputTime.js +2 -2
  14. package/dist/tsconfig.build.tsbuildinfo +1 -1
  15. package/dist/types/src/InputDate/InputDate.d.ts +2 -1
  16. package/dist/types/src/InputFieldWrapper/InputFieldWrapper.d.ts +9 -2
  17. package/dist/types/src/InputFieldWrapper/components/Prefix/Prefix.d.ts +2 -3
  18. package/dist/types/src/InputFieldWrapper/components/Suffix/Suffix.d.ts +2 -3
  19. package/dist/types/src/InputPressable/InputPressable.d.ts +9 -1
  20. package/dist/types/src/InputPressable/InputPressable.style.d.ts +3 -0
  21. package/dist/types/src/InputSearch/InputSearch.d.ts +1 -1
  22. package/dist/types/src/InputText/InputText.d.ts +8 -0
  23. package/dist/types/src/InputText/InputText.style.d.ts +4 -0
  24. package/dist/types/src/InputTime/InputTime.d.ts +2 -1
  25. package/package.json +2 -2
  26. package/src/Form/Form.tsx +1 -0
  27. package/src/Form/components/FormBody/FormBody.tsx +6 -6
  28. package/src/FormatFile/components/MediaView/MediaView.test.tsx +283 -0
  29. package/src/FormatFile/components/MediaView/MediaView.tsx +27 -6
  30. package/src/InputDate/InputDate.tsx +5 -1
  31. package/src/InputFieldWrapper/InputFieldWrapper.test.tsx +48 -1
  32. package/src/InputFieldWrapper/InputFieldWrapper.tsx +38 -28
  33. package/src/InputFieldWrapper/components/Prefix/Prefix.test.tsx +3 -5
  34. package/src/InputFieldWrapper/components/Prefix/Prefix.tsx +6 -4
  35. package/src/InputFieldWrapper/components/Suffix/Suffix.test.tsx +2 -4
  36. package/src/InputFieldWrapper/components/Suffix/Suffix.tsx +6 -4
  37. package/src/InputPressable/InputPressable.style.ts +4 -0
  38. package/src/InputPressable/InputPressable.test.tsx +75 -1
  39. package/src/InputPressable/InputPressable.tsx +33 -7
  40. package/src/InputSearch/InputSearch.tsx +1 -0
  41. package/src/InputText/InputText.style.ts +5 -0
  42. package/src/InputText/InputText.test.tsx +75 -0
  43. package/src/InputText/InputText.tsx +32 -12
  44. package/src/InputTime/InputTime.tsx +5 -1
@@ -43,7 +43,14 @@ export interface InputFieldWrapperProps {
43
43
  */
44
44
  readonly assistiveText?: string;
45
45
 
46
- readonly hasMiniLabel?: boolean;
46
+ /**
47
+ * Controls how the placeholder text is displayed.
48
+ * - normal: the placeholder text will be displayed in the normal placeholder position
49
+ * - mini: the placeholder text will float above the input value
50
+ * - hidden: the placeholder text will not be displayed
51
+ * @default "normal"
52
+ */
53
+ readonly placeholderMode?: "normal" | "mini" | "hidden";
47
54
 
48
55
  readonly hasValue?: boolean;
49
56
 
@@ -119,7 +126,7 @@ export function InputFieldWrapper({
119
126
  assistiveText,
120
127
  prefix,
121
128
  suffix,
122
- hasMiniLabel = false,
129
+ placeholderMode = "normal",
123
130
  hasValue = false,
124
131
  error,
125
132
  focused = false,
@@ -143,6 +150,9 @@ export function InputFieldWrapper({
143
150
  const showLoadingGlimmer = loading && loadingType === "glimmer";
144
151
  const styles = useStyles();
145
152
 
153
+ const placeholderVisible = placeholderMode !== "hidden";
154
+ const miniLabelActive = placeholderMode === "mini";
155
+
146
156
  return (
147
157
  <ErrorMessageWrapper message={getMessage({ invalid, error })}>
148
158
  <View
@@ -160,35 +170,36 @@ export function InputFieldWrapper({
160
170
  <PrefixIcon
161
171
  disabled={disabled}
162
172
  focused={focused}
163
- hasMiniLabel={hasMiniLabel}
164
173
  inputInvalid={inputInvalid}
165
174
  icon={prefix.icon}
166
175
  />
167
176
  )}
168
177
  <View style={[styles.inputContainer]}>
169
- <View
170
- style={[
171
- !!placeholder && styles.label,
172
- hasMiniLabel && styles.miniLabel,
173
- disabled && styles.disabled,
174
- hasMiniLabel &&
175
- showClearAction &&
176
- styles.miniLabelShowClearAction,
177
- ]}
178
- pointerEvents="none"
179
- >
180
- <Placeholder
181
- placeholder={placeholder}
182
- labelVariation={getLabelVariation(error, invalid, disabled)}
183
- hasMiniLabel={hasMiniLabel}
184
- styleOverride={styleOverride?.placeholderText}
185
- />
186
- </View>
178
+ {placeholderVisible && (
179
+ <View
180
+ style={[
181
+ !!placeholder && styles.label,
182
+ miniLabelActive && styles.miniLabel,
183
+ disabled && styles.disabled,
184
+ miniLabelActive &&
185
+ showClearAction &&
186
+ styles.miniLabelShowClearAction,
187
+ ]}
188
+ pointerEvents="none"
189
+ >
190
+ <Placeholder
191
+ placeholder={placeholder}
192
+ labelVariation={getLabelVariation(error, invalid, disabled)}
193
+ miniLabelActive={miniLabelActive}
194
+ styleOverride={styleOverride?.placeholderText}
195
+ />
196
+ </View>
197
+ )}
187
198
  {prefix?.label && hasValue && (
188
199
  <PrefixLabel
189
200
  disabled={disabled}
190
201
  focused={focused}
191
- hasMiniLabel={hasMiniLabel}
202
+ miniLabelActive={miniLabelActive}
192
203
  inputInvalid={inputInvalid}
193
204
  label={prefix.label}
194
205
  styleOverride={styleOverride?.prefixLabel}
@@ -225,7 +236,7 @@ export function InputFieldWrapper({
225
236
  <SuffixLabel
226
237
  disabled={disabled}
227
238
  focused={focused}
228
- hasMiniLabel={hasMiniLabel}
239
+ miniLabelActive={miniLabelActive}
229
240
  inputInvalid={inputInvalid}
230
241
  label={suffix.label}
231
242
  hasLeftMargin={!showClearAction}
@@ -245,7 +256,6 @@ export function InputFieldWrapper({
245
256
  <SuffixIcon
246
257
  disabled={disabled}
247
258
  focused={focused}
248
- hasMiniLabel={hasMiniLabel}
249
259
  hasLeftMargin={!!(!showClearAction || suffix?.label)}
250
260
  inputInvalid={inputInvalid}
251
261
  icon={suffix.icon}
@@ -332,12 +342,12 @@ function Placeholder({
332
342
  placeholder,
333
343
  styleOverride,
334
344
  labelVariation,
335
- hasMiniLabel,
345
+ miniLabelActive,
336
346
  }: {
337
347
  readonly placeholder?: string;
338
348
  readonly styleOverride: StyleProp<TextStyle>;
339
349
  readonly labelVariation: TextVariation;
340
- readonly hasMiniLabel: boolean;
350
+ readonly miniLabelActive: boolean;
341
351
  }) {
342
352
  const typographyStyles = useTypographyStyles();
343
353
 
@@ -348,7 +358,7 @@ function Placeholder({
348
358
  hideFromScreenReader={true}
349
359
  maxLines="single"
350
360
  variation={labelVariation}
351
- level={hasMiniLabel ? "textSupporting" : "text"}
361
+ level={miniLabelActive ? "textSupporting" : "text"}
352
362
  >
353
363
  {placeholder}
354
364
  </Text>
@@ -361,7 +371,7 @@ function Placeholder({
361
371
  style={[
362
372
  typographyStyles[labelVariation],
363
373
  typographyStyles.baseRegularRegular,
364
- hasMiniLabel
374
+ miniLabelActive
365
375
  ? typographyStyles.smallSize
366
376
  : typographyStyles.defaultSize,
367
377
  styleOverride,
@@ -30,7 +30,7 @@ beforeAll(() => {
30
30
  function setupLabel({
31
31
  disabled = false,
32
32
  focused = false,
33
- hasMiniLabel = false,
33
+ miniLabelActive = false,
34
34
  inputInvalid = false,
35
35
  label = mockLabel,
36
36
  styleOverride,
@@ -39,7 +39,7 @@ function setupLabel({
39
39
  <PrefixLabel
40
40
  disabled={disabled}
41
41
  focused={focused}
42
- hasMiniLabel={hasMiniLabel}
42
+ miniLabelActive={miniLabelActive}
43
43
  inputInvalid={inputInvalid}
44
44
  label={label}
45
45
  styleOverride={styleOverride}
@@ -50,7 +50,6 @@ function setupLabel({
50
50
  function setupIcon({
51
51
  disabled = false,
52
52
  focused = false,
53
- hasMiniLabel = false,
54
53
  inputInvalid = false,
55
54
  icon = "invoice",
56
55
  }: Partial<PrefixIconProps>) {
@@ -58,7 +57,6 @@ function setupIcon({
58
57
  <PrefixIcon
59
58
  disabled={disabled}
60
59
  focused={focused}
61
- hasMiniLabel={hasMiniLabel}
62
60
  inputInvalid={inputInvalid}
63
61
  icon={icon}
64
62
  />,
@@ -150,7 +148,7 @@ describe("Prefix", () => {
150
148
 
151
149
  it("updates the position of the label when a value is entered", () => {
152
150
  const tree = setupLabel({
153
- hasMiniLabel: true,
151
+ miniLabelActive: true,
154
152
  });
155
153
  const prefixLabel = tree.getByTestId(prefixLabelTestId);
156
154
  const labelWrapper = prefixLabel.children[0] as ReactTestInstance;
@@ -11,7 +11,7 @@ import { useStyles } from "../../InputFieldWrapper.style";
11
11
  export interface PrefixLabelProps {
12
12
  readonly focused: boolean;
13
13
  readonly disabled?: boolean;
14
- readonly hasMiniLabel: boolean;
14
+ readonly miniLabelActive: boolean;
15
15
  readonly inputInvalid: boolean;
16
16
  readonly label: string;
17
17
  readonly styleOverride?: StyleProp<TextStyle>;
@@ -23,7 +23,7 @@ export const prefixIconTestId = "ATL-InputFieldWrapper-PrefixIcon";
23
23
  export function PrefixLabel({
24
24
  focused,
25
25
  disabled,
26
- hasMiniLabel,
26
+ miniLabelActive,
27
27
  inputInvalid,
28
28
  label,
29
29
  styleOverride,
@@ -41,7 +41,10 @@ export function PrefixLabel({
41
41
  testID={prefixLabelTestId}
42
42
  >
43
43
  <View
44
- style={[styles.prefixLabel, hasMiniLabel && styles.fieldAffixMiniLabel]}
44
+ style={[
45
+ styles.prefixLabel,
46
+ miniLabelActive && styles.fieldAffixMiniLabel,
47
+ ]}
45
48
  >
46
49
  {!styleOverride ? (
47
50
  <Text variation={disabled ? "disabled" : "base"}>{label}</Text>
@@ -67,7 +70,6 @@ export function PrefixLabel({
67
70
  export interface PrefixIconProps {
68
71
  readonly focused: boolean;
69
72
  readonly disabled?: boolean;
70
- readonly hasMiniLabel: boolean;
71
73
  readonly inputInvalid?: boolean;
72
74
  readonly icon: IconNames;
73
75
  readonly styleOverride?: StyleProp<ViewStyle>;
@@ -10,7 +10,7 @@ const mockLabel = "$";
10
10
  function setupLabel({
11
11
  disabled = false,
12
12
  focused = false,
13
- hasMiniLabel = false,
13
+ miniLabelActive = false,
14
14
  inputInvalid = false,
15
15
  label = mockLabel,
16
16
  styleOverride,
@@ -19,7 +19,7 @@ function setupLabel({
19
19
  <SuffixLabel
20
20
  disabled={disabled}
21
21
  focused={focused}
22
- hasMiniLabel={hasMiniLabel}
22
+ miniLabelActive={miniLabelActive}
23
23
  inputInvalid={inputInvalid}
24
24
  label={label}
25
25
  styleOverride={styleOverride}
@@ -30,7 +30,6 @@ function setupLabel({
30
30
  function setupIcon({
31
31
  disabled = false,
32
32
  focused = false,
33
- hasMiniLabel = false,
34
33
  inputInvalid = false,
35
34
  icon = "invoice",
36
35
  }: Partial<SuffixIconProps>) {
@@ -38,7 +37,6 @@ function setupIcon({
38
37
  <SuffixIcon
39
38
  disabled={disabled}
40
39
  focused={focused}
41
- hasMiniLabel={hasMiniLabel}
42
40
  inputInvalid={inputInvalid}
43
41
  icon={icon}
44
42
  />,
@@ -11,7 +11,7 @@ import { useStyles } from "../../InputFieldWrapper.style";
11
11
  export interface SuffixLabelProps {
12
12
  readonly focused: boolean;
13
13
  readonly disabled?: boolean;
14
- readonly hasMiniLabel: boolean;
14
+ readonly miniLabelActive: boolean;
15
15
  readonly inputInvalid?: boolean;
16
16
  readonly label: string;
17
17
  readonly hasLeftMargin?: boolean;
@@ -24,7 +24,7 @@ export const suffixIconTestId = "ATL-InputFieldWrapper-SuffixIcon";
24
24
  export function SuffixLabel({
25
25
  focused,
26
26
  disabled,
27
- hasMiniLabel,
27
+ miniLabelActive,
28
28
  inputInvalid,
29
29
  label,
30
30
  hasLeftMargin = true,
@@ -44,7 +44,10 @@ export function SuffixLabel({
44
44
  ]}
45
45
  >
46
46
  <View
47
- style={[styles.suffixLabel, hasMiniLabel && styles.fieldAffixMiniLabel]}
47
+ style={[
48
+ styles.suffixLabel,
49
+ miniLabelActive && styles.fieldAffixMiniLabel,
50
+ ]}
48
51
  >
49
52
  {!styleOverride ? (
50
53
  <Text variation={disabled ? "disabled" : "base"}>{label}</Text>
@@ -70,7 +73,6 @@ export function SuffixLabel({
70
73
  export interface SuffixIconProps {
71
74
  readonly focused: boolean;
72
75
  readonly disabled?: boolean;
73
- readonly hasMiniLabel: boolean;
74
76
  readonly inputInvalid?: boolean;
75
77
  readonly icon: IconNames;
76
78
  readonly hasLeftMargin?: boolean;
@@ -28,5 +28,9 @@ export const useStyles = buildThemedStyles(tokens => {
28
28
  inputInvalid: {
29
29
  borderColor: tokens["color-critical"],
30
30
  },
31
+
32
+ withoutMiniLabel: {
33
+ paddingTop: tokens["space-base"] + tokens["space-smallest"],
34
+ },
31
35
  };
32
36
  });
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { fireEvent, render } from "@testing-library/react-native";
2
+ import { fireEvent, render, screen } from "@testing-library/react-native";
3
3
  import { InputPressable } from ".";
4
4
  import type { InputFieldWrapperProps } from "../InputFieldWrapper";
5
5
 
@@ -120,6 +120,80 @@ describe("InputPressable", () => {
120
120
  expect(getByText(value, { includeHiddenElements: true })).toBeDefined();
121
121
  });
122
122
  });
123
+
124
+ describe("showMiniLabel", () => {
125
+ it("defaults to true", () => {
126
+ const props = { placeholder: "placeholder", value: "value" };
127
+ render(<InputPressable {...props} />);
128
+ expect(
129
+ screen.getByText("placeholder", { includeHiddenElements: true }),
130
+ ).toBeDefined();
131
+ expect(MockInputFieldWrapper).toHaveBeenCalledWith(
132
+ expect.objectContaining({
133
+ placeholderMode: "mini",
134
+ }),
135
+ );
136
+ });
137
+
138
+ describe("when true", () => {
139
+ it("renders the placeholder in its normal position when the input has no value", () => {
140
+ const props = { showMiniLabel: true, placeholder: "placeholder" };
141
+ render(<InputPressable {...props} />);
142
+ expect(
143
+ screen.getByText("placeholder", { includeHiddenElements: true }),
144
+ ).toBeDefined();
145
+ expect(MockInputFieldWrapper).toHaveBeenCalledWith(
146
+ expect.objectContaining({
147
+ placeholderMode: "normal",
148
+ }),
149
+ );
150
+ });
151
+
152
+ it("renders the placeholder as a mini label when the input has a value", () => {
153
+ const props = {
154
+ showMiniLabel: true,
155
+ placeholder: "placeholder",
156
+ value: "value",
157
+ };
158
+ render(<InputPressable {...props} />);
159
+ expect(
160
+ screen.getByText("placeholder", { includeHiddenElements: true }),
161
+ ).toBeDefined();
162
+ expect(MockInputFieldWrapper).toHaveBeenCalledWith(
163
+ expect.objectContaining({
164
+ placeholderMode: "mini",
165
+ }),
166
+ );
167
+ });
168
+ });
169
+
170
+ describe("when false", () => {
171
+ it("renders the placeholder in its normal position when the input has no value", () => {
172
+ const props = { showMiniLabel: false, placeholder: "placeholder" };
173
+ render(<InputPressable {...props} />);
174
+ expect(
175
+ screen.getByText("placeholder", { includeHiddenElements: true }),
176
+ ).toBeDefined();
177
+ expect(MockInputFieldWrapper).toHaveBeenCalledWith(
178
+ expect.objectContaining({
179
+ placeholderMode: "normal",
180
+ }),
181
+ );
182
+ });
183
+
184
+ it("does not render the placeholder when the input has a value", () => {
185
+ const props = {
186
+ showMiniLabel: false,
187
+ placeholder: "placeholder",
188
+ value: "value",
189
+ };
190
+ render(<InputPressable {...props} />);
191
+ expect(
192
+ screen.queryByText("placeholder", { includeHiddenElements: true }),
193
+ ).toBeNull();
194
+ });
195
+ });
196
+ });
123
197
  });
124
198
 
125
199
  describe("accessibilityLabel", () => {
@@ -1,5 +1,5 @@
1
1
  import type { Ref } from "react";
2
- import React, { forwardRef, useEffect, useState } from "react";
2
+ import React, { forwardRef } from "react";
3
3
  import type { IconNames } from "@jobber/design";
4
4
  import type { FieldError } from "react-hook-form";
5
5
  import { Text as NativeText, Pressable } from "react-native";
@@ -7,6 +7,7 @@ import type { Clearable } from "@jobber/hooks";
7
7
  import { useShowClear } from "@jobber/hooks";
8
8
  import type { XOR } from "ts-xor";
9
9
  import { useStyles } from "./InputPressable.style";
10
+ import type { InputFieldWrapperProps } from "../InputFieldWrapper";
10
11
  import { InputFieldWrapper, useCommonInputStyles } from "../InputFieldWrapper";
11
12
 
12
13
  interface BasicSuffix {
@@ -51,6 +52,15 @@ export interface InputPressableProps {
51
52
  */
52
53
  readonly invalid?: boolean | string;
53
54
 
55
+ /**
56
+ * Controls the visibility of the mini label that appears inside the input
57
+ * when a value is entered. By default, the placeholder text moves up to
58
+ * become a mini label. Set to false to disable this behavior.
59
+ *
60
+ * @default true
61
+ */
62
+ readonly showMiniLabel?: boolean;
63
+
54
64
  /**
55
65
  * Callback that is called when the text input is focused
56
66
  * @param event
@@ -105,6 +115,7 @@ export function InputPressableInternal(
105
115
  disabled,
106
116
  invalid,
107
117
  error,
118
+ showMiniLabel = true,
108
119
  onPress,
109
120
  accessibilityLabel,
110
121
  accessibilityHint,
@@ -117,11 +128,8 @@ export function InputPressableInternal(
117
128
  ref: Ref<InputPressableRef>,
118
129
  ): JSX.Element {
119
130
  const hasValue = !!value;
120
- const [hasMiniLabel, setHasMiniLabel] = useState(Boolean(value));
121
131
 
122
- useEffect(() => {
123
- setHasMiniLabel(Boolean(value));
124
- }, [value]);
132
+ const placeholderMode = getPlaceholderMode(showMiniLabel, value);
125
133
 
126
134
  const showClear = useShowClear({
127
135
  clearable,
@@ -139,7 +147,7 @@ export function InputPressableInternal(
139
147
  prefix={prefix}
140
148
  suffix={suffix}
141
149
  hasValue={hasValue}
142
- hasMiniLabel={hasMiniLabel}
150
+ placeholderMode={placeholderMode}
143
151
  focused={focused}
144
152
  error={error}
145
153
  invalid={invalid}
@@ -160,7 +168,8 @@ export function InputPressableInternal(
160
168
  style={[
161
169
  commonInputStyles.input,
162
170
  styles.inputPressableStyles,
163
- !hasMiniLabel && commonInputStyles.inputEmpty,
171
+ placeholderMode === "normal" && commonInputStyles.inputEmpty,
172
+ placeholderMode === "hidden" && styles.withoutMiniLabel,
164
173
  disabled && commonInputStyles.inputDisabled,
165
174
  (Boolean(invalid) || error) && styles.inputInvalid,
166
175
  ]}
@@ -176,3 +185,20 @@ export function InputPressableInternal(
176
185
  </InputFieldWrapper>
177
186
  );
178
187
  }
188
+
189
+ function getPlaceholderMode(
190
+ isMiniLabelAllowed: boolean,
191
+ internalValue: string | undefined,
192
+ ): InputFieldWrapperProps["placeholderMode"] {
193
+ const hasValue = Boolean(internalValue);
194
+
195
+ if (hasValue) {
196
+ if (isMiniLabelAllowed) {
197
+ return "mini";
198
+ } else {
199
+ return "hidden";
200
+ }
201
+ } else {
202
+ return "normal";
203
+ }
204
+ }
@@ -16,6 +16,7 @@ export interface InputSearchProps
16
16
  | "autoFocus"
17
17
  | "placeholder"
18
18
  | "prefix"
19
+ | "showMiniLabel"
19
20
  > {
20
21
  /**
21
22
  * A callback function that handles the update of the new value of the property value.
@@ -27,5 +27,10 @@ export const useStyles = buildThemedStyles(tokens => {
27
27
  paddingTop:
28
28
  (typographyStyles.defaultSize.fontSize || 0) + tokens["space-smallest"],
29
29
  },
30
+
31
+ multilineWithoutMiniLabel: {
32
+ paddingTop: tokens["space-base"] + tokens["space-smallest"],
33
+ paddingBottom: tokens["space-base"] + tokens["space-smallest"],
34
+ },
30
35
  };
31
36
  });
@@ -4,6 +4,7 @@ import {
4
4
  fireEvent,
5
5
  render,
6
6
  renderHook,
7
+ screen,
7
8
  waitFor,
8
9
  } from "@testing-library/react-native";
9
10
  import type { TextStyle } from "react-native";
@@ -548,6 +549,80 @@ describe("InputText", () => {
548
549
  expect(styleOverride.inputText.color).toEqual(flattenedStyle.color);
549
550
  });
550
551
  });
552
+
553
+ describe("showMiniLabel", () => {
554
+ it("defaults to true", () => {
555
+ const props = { placeholder: "placeholder", value: "value" };
556
+ renderInputText(props);
557
+ expect(
558
+ screen.getByText("placeholder", { includeHiddenElements: true }),
559
+ ).toBeDefined();
560
+ expect(MockInputFieldWrapper).toHaveBeenCalledWith(
561
+ expect.objectContaining({
562
+ placeholderMode: "mini",
563
+ }),
564
+ );
565
+ });
566
+
567
+ describe("when true", () => {
568
+ it("renders the placeholder in its normal position when the input has no value", () => {
569
+ const props = { showMiniLabel: true, placeholder: "placeholder" };
570
+ renderInputText(props);
571
+ expect(
572
+ screen.getByText("placeholder", { includeHiddenElements: true }),
573
+ ).toBeDefined();
574
+ expect(MockInputFieldWrapper).toHaveBeenCalledWith(
575
+ expect.objectContaining({
576
+ placeholderMode: "normal",
577
+ }),
578
+ );
579
+ });
580
+
581
+ it("renders the placeholder as a mini label when the input has a value", () => {
582
+ const props = {
583
+ showMiniLabel: true,
584
+ placeholder: "placeholder",
585
+ value: "value",
586
+ };
587
+ renderInputText(props);
588
+ expect(
589
+ screen.getByText("placeholder", { includeHiddenElements: true }),
590
+ ).toBeDefined();
591
+ expect(MockInputFieldWrapper).toHaveBeenCalledWith(
592
+ expect.objectContaining({
593
+ placeholderMode: "mini",
594
+ }),
595
+ );
596
+ });
597
+ });
598
+
599
+ describe("when false", () => {
600
+ it("renders the placeholder in its normal position when the input has no value", () => {
601
+ const props = { showMiniLabel: false, placeholder: "placeholder" };
602
+ renderInputText(props);
603
+ expect(
604
+ screen.getByText("placeholder", { includeHiddenElements: true }),
605
+ ).toBeDefined();
606
+ expect(MockInputFieldWrapper).toHaveBeenCalledWith(
607
+ expect.objectContaining({
608
+ placeholderMode: "normal",
609
+ }),
610
+ );
611
+ });
612
+
613
+ it("does not render the placeholder when the input has a value", () => {
614
+ const props = {
615
+ showMiniLabel: false,
616
+ placeholder: "placeholder",
617
+ value: "value",
618
+ };
619
+ renderInputText(props);
620
+ expect(
621
+ screen.queryByText("placeholder", { includeHiddenElements: true }),
622
+ ).toBeNull();
623
+ });
624
+ });
625
+ });
551
626
  });
552
627
 
553
628
  describe("Transform", () => {
@@ -65,6 +65,15 @@ export interface InputTextProps
65
65
  */
66
66
  readonly assistiveText?: string;
67
67
 
68
+ /**
69
+ * Controls the visibility of the mini label that appears inside the input
70
+ * when a value is entered. By default, the placeholder text moves up to
71
+ * become a mini label. Set to false to disable this behavior.
72
+ *
73
+ * @default true
74
+ */
75
+ readonly showMiniLabel?: boolean;
76
+
68
77
  /**
69
78
  * Determines what keyboard is shown
70
79
  */
@@ -245,6 +254,7 @@ function InputTextInternal(
245
254
  name,
246
255
  placeholder,
247
256
  assistiveText,
257
+ showMiniLabel = true,
248
258
  keyboard,
249
259
  value: controlledValue,
250
260
  defaultValue,
@@ -292,7 +302,8 @@ function InputTextInternal(
292
302
 
293
303
  const hasValue = internalValue !== "" && internalValue !== undefined;
294
304
  const [focused, setFocused] = useState(false);
295
- const { hasMiniLabel } = useMiniLabel(internalValue);
305
+ const placeholderMode = getPlaceholderMode(showMiniLabel, internalValue);
306
+ const miniLabelActive = placeholderMode === "mini";
296
307
 
297
308
  const textInputRef = useTextInputRef({ ref, onClear: handleClear });
298
309
 
@@ -367,7 +378,7 @@ function InputTextInternal(
367
378
  prefix={prefix}
368
379
  suffix={suffix}
369
380
  hasValue={hasValue}
370
- hasMiniLabel={hasMiniLabel}
381
+ placeholderMode={placeholderMode}
371
382
  assistiveText={assistiveText}
372
383
  focused={focused}
373
384
  error={error}
@@ -391,11 +402,14 @@ function InputTextInternal(
391
402
  style={[
392
403
  commonInputStyles.input,
393
404
  styles.inputPaddingTop,
394
- !hasMiniLabel && commonInputStyles.inputEmpty,
405
+ !miniLabelActive && commonInputStyles.inputEmpty,
395
406
  disabled && commonInputStyles.inputDisabled,
396
407
  multiline && styles.multiLineInput,
397
408
  multiline && Platform.OS === "ios" && styles.multilineInputiOS,
398
- multiline && hasMiniLabel && styles.multiLineInputWithMini,
409
+ multiline && miniLabelActive && styles.multiLineInputWithMini,
410
+ multiline &&
411
+ placeholderMode === "hidden" &&
412
+ styles.multilineWithoutMiniLabel,
399
413
  styleOverride?.inputText,
400
414
  loading && loadingType === "glimmer" && { color: "transparent" },
401
415
  ]}
@@ -510,13 +524,19 @@ function useTextInputRef({ ref, onClear }: UseTextInputRefProps) {
510
524
  return textInputRef;
511
525
  }
512
526
 
513
- function useMiniLabel(internalValue: string): {
514
- hasMiniLabel: boolean;
515
- } {
516
- const [hasMiniLabel, setHasMiniLabel] = useState(Boolean(internalValue));
517
- useEffect(() => {
518
- setHasMiniLabel(Boolean(internalValue));
519
- }, [internalValue]);
527
+ function getPlaceholderMode(
528
+ isMiniLabelAllowed: boolean,
529
+ internalValue: string,
530
+ ): InputFieldWrapperProps["placeholderMode"] {
531
+ const hasValue = Boolean(internalValue);
520
532
 
521
- return { hasMiniLabel };
533
+ if (hasValue) {
534
+ if (isMiniLabelAllowed) {
535
+ return "mini";
536
+ } else {
537
+ return "hidden";
538
+ }
539
+ } else {
540
+ return "normal";
541
+ }
522
542
  }