@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.
@@ -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 = ComponentPropsWithoutRef<"input"> &
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 TextFieldComponent = (
114
- {
115
- ["data-testid"]: dataTestId,
116
- className,
117
- clearButtonAriaLabel,
118
- beforeField,
119
- afterField,
120
- endAdornment,
121
- errorText,
122
- helperText,
123
- hideRequiredAsterisk = false,
124
- label,
125
- loading = false,
126
- onClear,
127
- startAdornment,
128
- valid = false,
129
- ...inputProps
130
- }: TextFieldProps,
131
- ref: ForwardedRef<HTMLInputElement>
132
- ) => {
133
- const randomId = useId();
134
- const inputId = inputProps.id ?? randomId;
135
- const getTestId = (name: string) => (dataTestId ? `${dataTestId}-${name}` : undefined);
136
- const isValid = valid && !errorText;
137
- const helperTextId = helperText ? `${inputId}-helper-text` : undefined;
138
- const startAdornments: ReactNode[] = [startAdornment].filter((adornment) => !!adornment);
139
- const hasValue =
140
- typeof inputProps.value === "number"
141
- ? inputProps.value !== undefined
142
- : inputProps.value?.length;
143
- const hasClearButton =
144
- hasValue &&
145
- !inputProps.disabled &&
146
- !inputProps.readOnly &&
147
- !loading &&
148
- onClear &&
149
- clearButtonAriaLabel;
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
- const internalRef = useMutableRefObject<HTMLInputElement | null>(null);
152
- const setRef = (node: HTMLInputElement | null) => {
153
- internalRef.current = node;
154
- if (typeof ref === "function") {
155
- ref(node);
156
- } else if (ref) {
157
- ref.current = node;
158
- }
159
- };
160
- const handleClear = () => {
161
- onClear?.();
162
- internalRef.current?.focus();
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
- const localEndAdornments: ReactNode[] = [
166
- loading && (
167
- <Spinner
168
- key="spinner"
169
- disabled={inputProps.disabled}
170
- size="xs"
171
- data-testid={getTestId("spinner")}
172
- />
173
- ),
174
- hasClearButton && (
175
- <Button
176
- key="clear-button"
177
- variant="tertiary-purple"
178
- onClick={handleClear}
179
- iconOnly
180
- aria-label={clearButtonAriaLabel ?? ""}
181
- data-testid={getTestId("clear-button")}
182
- tabIndex={-1}
183
- >
184
- <IconClose size="xs" />
185
- </Button>
186
- ),
187
- isValid && (
188
- <IconCheckCircleFilled
189
- key="valid-icon"
190
- data-testid={getTestId("valid-icon")}
191
- className={cx(`${rootClassName}__valid-icon`)}
192
- />
193
- ),
194
- ].filter((adornment) => !!adornment);
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
- const inputContainerClassnames = cx([
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
- return (
209
- <div className={cx(className, rootClassName)}>
210
- {label && (
211
- <Label
212
- htmlFor={inputId}
213
- className={cx(`${rootClassName}__label`)}
214
- data-testid={getTestId("label")}
215
- disabled={inputProps.disabled}
216
- >
217
- {inputProps.required && !hideRequiredAsterisk && <span aria-hidden>*</span>}
218
- {label}
219
- </Label>
220
- )}
221
- <div className={cx(`${rootClassName}__field-row`)}>
222
- {!!beforeField && beforeField}
223
- <div className={inputContainerClassnames}>
224
- {!!startAdornments.length && (
225
- <div
226
- data-testid={getTestId("start-adornments")}
227
- className={cx(`${rootClassName}__adornment-container`)}
228
- >
229
- {startAdornments}
230
- </div>
231
- )}
232
- <input
233
- {...inputProps}
234
- id={inputId}
235
- ref={setRef}
236
- data-testid={getTestId("input")}
237
- aria-describedby={inputProps["aria-describedby"] || helperTextId}
238
- aria-invalid={inputProps["aria-invalid"] || !!errorText}
239
- className={cx([
240
- `${rootClassName}__input`,
241
- {
242
- [`${rootClassName}__input--valid`]: isValid,
243
- [`${rootClassName}__input--error`]: !!errorText,
244
- },
245
- ])}
246
- />
247
- <div className={cx(`${rootClassName}__frame`)} />
248
- {(!!localEndAdornments.length || endAdornment) && (
249
- <div
250
- data-testid={getTestId("end-adornments")}
251
- className={cx(`${rootClassName}__adornment-container`)}
252
- >
253
- {localEndAdornments}
254
- {endAdornment}
255
- </div>
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
- {!!afterField && afterField}
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
- {helperTextId && (
261
- <FieldHelperText data-testid={getTestId("helper-text")} id={helperTextId}>
262
- {helperText}
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> =>