@purpurds/text-field 7.6.1 → 7.7.0
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/LICENSE.txt +6 -6
- package/dist/text-field.cjs.js +10 -10
- package/dist/text-field.cjs.js.map +1 -1
- package/dist/text-field.d.ts +646 -4
- package/dist/text-field.d.ts.map +1 -1
- package/dist/text-field.es.js +362 -412
- package/dist/text-field.es.js.map +1 -1
- package/package.json +9 -8
- package/src/text-field.stories.tsx +1 -1
- package/src/text-field.tsx +147 -151
package/src/text-field.tsx
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import React, {
|
|
2
|
-
type ComponentPropsWithoutRef,
|
|
3
|
-
type ForwardedRef,
|
|
4
2
|
forwardRef,
|
|
5
3
|
type HTMLInputTypeAttribute,
|
|
6
4
|
isValidElement,
|
|
@@ -9,6 +7,7 @@ import React, {
|
|
|
9
7
|
useId,
|
|
10
8
|
} from "react";
|
|
11
9
|
import { Button } from "@purpurds/button";
|
|
10
|
+
import type { BaseProps } from "@purpurds/common-types";
|
|
12
11
|
import { FieldErrorText } from "@purpurds/field-error-text";
|
|
13
12
|
import { FieldHelperText } from "@purpurds/field-helper-text";
|
|
14
13
|
import { IconCheckCircleFilled } from "@purpurds/icon/check-circle-filled";
|
|
@@ -21,7 +20,6 @@ import styles from "./text-field.module.scss";
|
|
|
21
20
|
import { useMutableRefObject } from "./utils";
|
|
22
21
|
|
|
23
22
|
type TextFieldBaseProps = {
|
|
24
|
-
["data-testid"]?: string;
|
|
25
23
|
/**
|
|
26
24
|
* Use to display e.g. a button before the text field.
|
|
27
25
|
*
|
|
@@ -103,173 +101,171 @@ type TextFieldClearProps =
|
|
|
103
101
|
onClear?: never;
|
|
104
102
|
};
|
|
105
103
|
|
|
106
|
-
export type TextFieldProps =
|
|
104
|
+
export type TextFieldProps = Omit<BaseProps<"input">, "type"> &
|
|
107
105
|
TextFieldBaseProps &
|
|
108
106
|
TextFieldClearProps;
|
|
109
107
|
|
|
110
108
|
const cx = c.bind(styles);
|
|
111
109
|
const rootClassName = "purpur-text-field";
|
|
112
110
|
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
111
|
+
export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
|
112
|
+
(
|
|
113
|
+
{
|
|
114
|
+
["data-testid"]: dataTestId,
|
|
115
|
+
className,
|
|
116
|
+
clearButtonAriaLabel,
|
|
117
|
+
beforeField,
|
|
118
|
+
afterField,
|
|
119
|
+
endAdornment,
|
|
120
|
+
errorText,
|
|
121
|
+
helperText,
|
|
122
|
+
hideRequiredAsterisk = false,
|
|
123
|
+
label,
|
|
124
|
+
loading = false,
|
|
125
|
+
onClear,
|
|
126
|
+
startAdornment,
|
|
127
|
+
valid = false,
|
|
128
|
+
...inputProps
|
|
129
|
+
},
|
|
130
|
+
ref
|
|
131
|
+
) => {
|
|
132
|
+
const randomId = useId();
|
|
133
|
+
const inputId = inputProps.id ?? randomId;
|
|
134
|
+
const getTestId = (name: string) => (dataTestId ? `${dataTestId}-${name}` : undefined);
|
|
135
|
+
const isValid = valid && !errorText;
|
|
136
|
+
const helperTextId = helperText ? `${inputId}-helper-text` : undefined;
|
|
137
|
+
const startAdornments: ReactNode[] = [startAdornment].filter((adornment) => !!adornment);
|
|
138
|
+
const hasValue =
|
|
139
|
+
typeof inputProps.value === "number"
|
|
140
|
+
? inputProps.value !== undefined
|
|
141
|
+
: inputProps.value?.length;
|
|
142
|
+
const hasClearButton =
|
|
143
|
+
hasValue &&
|
|
144
|
+
!inputProps.disabled &&
|
|
145
|
+
!inputProps.readOnly &&
|
|
146
|
+
!loading &&
|
|
147
|
+
onClear &&
|
|
148
|
+
clearButtonAriaLabel;
|
|
150
149
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
150
|
+
const internalRef = useMutableRefObject<HTMLInputElement | null>(null);
|
|
151
|
+
const setRef = (node: HTMLInputElement | null) => {
|
|
152
|
+
internalRef.current = node;
|
|
153
|
+
if (typeof ref === "function") {
|
|
154
|
+
ref(node);
|
|
155
|
+
} else if (ref) {
|
|
156
|
+
ref.current = node;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
const handleClear = () => {
|
|
160
|
+
onClear?.();
|
|
161
|
+
internalRef.current?.focus();
|
|
162
|
+
};
|
|
164
163
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
164
|
+
const localEndAdornments: ReactNode[] = [
|
|
165
|
+
loading && (
|
|
166
|
+
<Spinner
|
|
167
|
+
key="spinner"
|
|
168
|
+
disabled={inputProps.disabled}
|
|
169
|
+
size="xs"
|
|
170
|
+
data-testid={getTestId("spinner")}
|
|
171
|
+
/>
|
|
172
|
+
),
|
|
173
|
+
hasClearButton && (
|
|
174
|
+
<Button
|
|
175
|
+
key="clear-button"
|
|
176
|
+
variant="tertiary-purple"
|
|
177
|
+
onClick={handleClear}
|
|
178
|
+
iconOnly
|
|
179
|
+
aria-label={clearButtonAriaLabel ?? ""}
|
|
180
|
+
data-testid={getTestId("clear-button")}
|
|
181
|
+
tabIndex={-1}
|
|
182
|
+
>
|
|
183
|
+
<IconClose size="xs" />
|
|
184
|
+
</Button>
|
|
185
|
+
),
|
|
186
|
+
isValid && (
|
|
187
|
+
<IconCheckCircleFilled
|
|
188
|
+
key="valid-icon"
|
|
189
|
+
data-testid={getTestId("valid-icon")}
|
|
190
|
+
className={cx(`${rootClassName}__valid-icon`)}
|
|
191
|
+
/>
|
|
192
|
+
),
|
|
193
|
+
].filter((adornment) => !!adornment);
|
|
195
194
|
|
|
196
|
-
|
|
197
|
-
`${rootClassName}__input-container`,
|
|
198
|
-
{
|
|
195
|
+
const inputContainerClassnames = cx(`${rootClassName}__input-container`, {
|
|
199
196
|
[`${rootClassName}__input-container--start-adornment`]: startAdornments.length,
|
|
200
197
|
[`${rootClassName}__input-container--end-adornment`]:
|
|
201
198
|
localEndAdornments.length || endAdornment,
|
|
202
199
|
[`${rootClassName}__input-container--disabled`]: inputProps.disabled,
|
|
203
200
|
[`${rootClassName}__input-container--has-clear-button`]: hasClearButton,
|
|
204
201
|
[`${rootClassName}__input-container--readonly`]: inputProps.readOnly && !inputProps.disabled,
|
|
205
|
-
}
|
|
206
|
-
]);
|
|
202
|
+
});
|
|
207
203
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
204
|
+
return (
|
|
205
|
+
<div className={cx(className, rootClassName)}>
|
|
206
|
+
{label && (
|
|
207
|
+
<Label
|
|
208
|
+
htmlFor={inputId}
|
|
209
|
+
className={cx(`${rootClassName}__label`)}
|
|
210
|
+
data-testid={getTestId("label")}
|
|
211
|
+
disabled={inputProps.disabled}
|
|
212
|
+
>
|
|
213
|
+
{inputProps.required && !hideRequiredAsterisk && <span aria-hidden>*</span>}
|
|
214
|
+
{label}
|
|
215
|
+
</Label>
|
|
216
|
+
)}
|
|
217
|
+
<div className={cx(`${rootClassName}__field-row`)}>
|
|
218
|
+
{!!beforeField && beforeField}
|
|
219
|
+
<div className={inputContainerClassnames}>
|
|
220
|
+
{!!startAdornments.length && (
|
|
221
|
+
<div
|
|
222
|
+
data-testid={getTestId("start-adornments")}
|
|
223
|
+
className={cx(`${rootClassName}__adornment-container`)}
|
|
224
|
+
>
|
|
225
|
+
{startAdornments}
|
|
226
|
+
</div>
|
|
227
|
+
)}
|
|
228
|
+
<input
|
|
229
|
+
{...inputProps}
|
|
230
|
+
id={inputId}
|
|
231
|
+
ref={setRef}
|
|
232
|
+
data-testid={getTestId("input")}
|
|
233
|
+
aria-describedby={inputProps["aria-describedby"] || helperTextId}
|
|
234
|
+
aria-invalid={inputProps["aria-invalid"] || !!errorText}
|
|
235
|
+
className={cx([
|
|
236
|
+
`${rootClassName}__input`,
|
|
237
|
+
{
|
|
238
|
+
[`${rootClassName}__input--valid`]: isValid,
|
|
239
|
+
[`${rootClassName}__input--error`]: !!errorText,
|
|
240
|
+
},
|
|
241
|
+
])}
|
|
242
|
+
/>
|
|
243
|
+
<div className={cx(`${rootClassName}__frame`)} />
|
|
244
|
+
{(!!localEndAdornments.length || endAdornment) && (
|
|
245
|
+
<div
|
|
246
|
+
data-testid={getTestId("end-adornments")}
|
|
247
|
+
className={cx(`${rootClassName}__adornment-container`)}
|
|
248
|
+
>
|
|
249
|
+
{localEndAdornments}
|
|
250
|
+
{endAdornment}
|
|
251
|
+
</div>
|
|
252
|
+
)}
|
|
253
|
+
</div>
|
|
254
|
+
{!!afterField && afterField}
|
|
257
255
|
</div>
|
|
258
|
-
{
|
|
256
|
+
{helperTextId && (
|
|
257
|
+
<FieldHelperText data-testid={getTestId("helper-text")} id={helperTextId}>
|
|
258
|
+
{helperText}
|
|
259
|
+
</FieldHelperText>
|
|
260
|
+
)}
|
|
261
|
+
{errorText && (
|
|
262
|
+
<FieldErrorText data-testid={getTestId("error-text")}>{errorText}</FieldErrorText>
|
|
263
|
+
)}
|
|
259
264
|
</div>
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
</FieldHelperText>
|
|
264
|
-
)}
|
|
265
|
-
{errorText && (
|
|
266
|
-
<FieldErrorText data-testid={getTestId("error-text")}>{errorText}</FieldErrorText>
|
|
267
|
-
)}
|
|
268
|
-
</div>
|
|
269
|
-
);
|
|
270
|
-
};
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
);
|
|
271
268
|
|
|
272
|
-
export const TextField = forwardRef(TextFieldComponent);
|
|
273
269
|
TextField.displayName = "TextField";
|
|
274
270
|
|
|
275
271
|
export const isTextField = (child?: ReactNode): child is ReactElement<TextFieldProps> =>
|