@purpurds/text-field 5.3.0 → 5.4.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 +12 -5
- package/dist/styles.css +1 -1
- package/dist/text-field.cjs.js +16 -8
- package/dist/text-field.cjs.js.map +1 -1
- package/dist/text-field.d.ts +16 -41
- package/dist/text-field.d.ts.map +1 -1
- package/dist/text-field.es.js +677 -433
- package/dist/text-field.es.js.map +1 -1
- package/dist/utils.d.ts +4 -0
- package/dist/utils.d.ts.map +1 -0
- package/package.json +8 -7
- package/readme.mdx +8 -3
- package/src/text-field.module.scss +4 -0
- package/src/text-field.stories.tsx +1 -0
- package/src/text-field.test.tsx +24 -2
- package/src/text-field.tsx +94 -26
- package/src/utils.ts +5 -0
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAU,MAAM,OAAO,CAAC;AAEjD,eAAO,MAAM,mBAAmB,aAAc,CAAC,KAAG,iBAAiB,CAAC,CAEnE,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@purpurds/text-field",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.4.0",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"main": "./dist/text-field.cjs.js",
|
|
6
6
|
"types": "./dist/text-field.d.ts",
|
|
@@ -15,12 +15,13 @@
|
|
|
15
15
|
"source": "src/text-field.tsx",
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"classnames": "~2.5.0",
|
|
18
|
-
"@purpurds/
|
|
19
|
-
"@purpurds/
|
|
20
|
-
"@purpurds/label": "5.
|
|
21
|
-
"@purpurds/
|
|
22
|
-
"@purpurds/
|
|
23
|
-
"@purpurds/field-helper-text": "5.
|
|
18
|
+
"@purpurds/button": "5.4.0",
|
|
19
|
+
"@purpurds/icon": "5.4.0",
|
|
20
|
+
"@purpurds/label": "5.4.0",
|
|
21
|
+
"@purpurds/field-error-text": "5.4.0",
|
|
22
|
+
"@purpurds/tokens": "5.4.0",
|
|
23
|
+
"@purpurds/field-helper-text": "5.4.0",
|
|
24
|
+
"@purpurds/spinner": "5.4.0"
|
|
24
25
|
},
|
|
25
26
|
"devDependencies": {
|
|
26
27
|
"@rushstack/eslint-patch": "~1.10.0",
|
package/readme.mdx
CHANGED
|
@@ -40,10 +40,15 @@ In MyComponent.tsx
|
|
|
40
40
|
import { TextField } from "@purpurds/purpur";
|
|
41
41
|
|
|
42
42
|
export const MyComponent = () => {
|
|
43
|
+
const [value, setValue] = useState("");
|
|
44
|
+
|
|
43
45
|
return (
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
<TextField
|
|
47
|
+
value={value}
|
|
48
|
+
onChange={setValue}
|
|
49
|
+
onClear={() => setValue("")}
|
|
50
|
+
clearButtonAllyLabel="Clear"
|
|
51
|
+
/>
|
|
47
52
|
);
|
|
48
53
|
};
|
|
49
54
|
```
|
|
@@ -4,6 +4,7 @@ import type { Meta, StoryObj } from "@storybook/react";
|
|
|
4
4
|
|
|
5
5
|
import "@purpurds/label/styles";
|
|
6
6
|
import "@purpurds/field-helper-text/styles";
|
|
7
|
+
import "@purpurds/button/styles";
|
|
7
8
|
import "@purpurds/field-error-text/styles";
|
|
8
9
|
import "@purpurds/icon/styles";
|
|
9
10
|
import "@purpurds/spinner/styles";
|
package/src/text-field.test.tsx
CHANGED
|
@@ -108,8 +108,8 @@ describe("TextField", () => {
|
|
|
108
108
|
it("should render with adornment", () => {
|
|
109
109
|
render(
|
|
110
110
|
<TextField
|
|
111
|
-
startAdornment={<span data-testid="start-adornment" />}
|
|
112
|
-
endAdornment={<span data-testid="end-adornment" />}
|
|
111
|
+
startAdornment={<span key="start" data-testid="start-adornment" />}
|
|
112
|
+
endAdornment={<span key="end" data-testid="end-adornment" />}
|
|
113
113
|
id="test"
|
|
114
114
|
data-testid="test"
|
|
115
115
|
label="Test label"
|
|
@@ -215,4 +215,26 @@ describe("TextField", () => {
|
|
|
215
215
|
expect(input).toHaveValue("Changed");
|
|
216
216
|
expect(onChangeMock).toHaveBeenCalled();
|
|
217
217
|
});
|
|
218
|
+
|
|
219
|
+
it("should call onClear when clicking the clear button", async () => {
|
|
220
|
+
const onClearMock = vi.fn();
|
|
221
|
+
const onChangeMock = vi.fn();
|
|
222
|
+
render(
|
|
223
|
+
<TextField
|
|
224
|
+
id="test"
|
|
225
|
+
data-testid="test"
|
|
226
|
+
value="a value"
|
|
227
|
+
onClear={onClearMock}
|
|
228
|
+
onChange={onChangeMock}
|
|
229
|
+
clearButtonAllyLabel="Clear"
|
|
230
|
+
/>
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const button = screen.getByTestId("test-clear-button");
|
|
234
|
+
|
|
235
|
+
// Simulate click on the clear button
|
|
236
|
+
fireEvent.click(button);
|
|
237
|
+
|
|
238
|
+
expect(onClearMock).toHaveBeenCalledTimes(1);
|
|
239
|
+
});
|
|
218
240
|
});
|
package/src/text-field.tsx
CHANGED
|
@@ -6,18 +6,21 @@ import React, {
|
|
|
6
6
|
ReactNode,
|
|
7
7
|
useId,
|
|
8
8
|
} from "react";
|
|
9
|
+
import { Button } from "@purpurds/button";
|
|
9
10
|
import { FieldErrorText } from "@purpurds/field-error-text";
|
|
10
11
|
import { FieldHelperText } from "@purpurds/field-helper-text";
|
|
11
|
-
import {
|
|
12
|
+
import { IconCheckCircleFilled, IconClose } from "@purpurds/icon";
|
|
12
13
|
import { Label } from "@purpurds/label";
|
|
13
14
|
import { Spinner } from "@purpurds/spinner";
|
|
14
|
-
import c from "classnames";
|
|
15
|
+
import c from "classnames/bind";
|
|
15
16
|
|
|
16
17
|
import styles from "./text-field.module.scss";
|
|
18
|
+
import { useMutableRefObject } from "./utils";
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
type TextFieldBaseProps = {
|
|
19
21
|
["data-testid"]?: string;
|
|
20
22
|
className?: string;
|
|
23
|
+
|
|
21
24
|
/**
|
|
22
25
|
* Use to display e.g. a button after the text field.
|
|
23
26
|
*
|
|
@@ -46,6 +49,7 @@ export type TextFieldProps = ComponentPropsWithoutRef<"input"> & {
|
|
|
46
49
|
* Use to render a spinner at the end inside of the text field.
|
|
47
50
|
*/
|
|
48
51
|
loading?: boolean;
|
|
52
|
+
|
|
49
53
|
/**
|
|
50
54
|
* Use to display e.g. an icon at the start inside of the text field.
|
|
51
55
|
*
|
|
@@ -66,18 +70,47 @@ export type TextFieldProps = ComponentPropsWithoutRef<"input"> & {
|
|
|
66
70
|
valid?: boolean;
|
|
67
71
|
};
|
|
68
72
|
|
|
73
|
+
type TextFieldClearProps =
|
|
74
|
+
| {
|
|
75
|
+
/**
|
|
76
|
+
* An accessible label for the clear button.
|
|
77
|
+
* */
|
|
78
|
+
clearButtonAllyLabel: string;
|
|
79
|
+
/**
|
|
80
|
+
* Event handler called when the clear button is clicked.
|
|
81
|
+
* */
|
|
82
|
+
onClear: () => void;
|
|
83
|
+
}
|
|
84
|
+
| {
|
|
85
|
+
/**
|
|
86
|
+
* An accessible label for the clear button.
|
|
87
|
+
* */
|
|
88
|
+
clearButtonAllyLabel?: never;
|
|
89
|
+
/**
|
|
90
|
+
* Event handler called when the clear button is clicked.
|
|
91
|
+
* */
|
|
92
|
+
onClear?: never;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export type TextFieldProps = ComponentPropsWithoutRef<"input"> &
|
|
96
|
+
TextFieldBaseProps &
|
|
97
|
+
TextFieldClearProps;
|
|
98
|
+
|
|
99
|
+
const cx = c.bind(styles);
|
|
69
100
|
const rootClassName = "purpur-text-field";
|
|
70
101
|
|
|
71
102
|
const TextFieldComponent = (
|
|
72
103
|
{
|
|
73
104
|
["data-testid"]: dataTestId,
|
|
74
105
|
className,
|
|
106
|
+
clearButtonAllyLabel,
|
|
75
107
|
afterField,
|
|
76
108
|
endAdornment,
|
|
77
109
|
errorText,
|
|
78
110
|
helperText,
|
|
79
111
|
label,
|
|
80
112
|
loading = false,
|
|
113
|
+
onClear,
|
|
81
114
|
startAdornment,
|
|
82
115
|
valid = false,
|
|
83
116
|
...inputProps
|
|
@@ -89,8 +122,32 @@ const TextFieldComponent = (
|
|
|
89
122
|
const getTestId = (name: string) => (dataTestId ? `${dataTestId}-${name}` : undefined);
|
|
90
123
|
const isValid = valid && !errorText;
|
|
91
124
|
const helperTextId = helperText ? `${inputId}-helper-text` : undefined;
|
|
92
|
-
|
|
93
125
|
const startAdornments: ReactNode[] = [startAdornment].filter((adornment) => !!adornment);
|
|
126
|
+
const hasValue =
|
|
127
|
+
typeof inputProps.value === "number"
|
|
128
|
+
? inputProps.value !== undefined
|
|
129
|
+
: inputProps.value?.length;
|
|
130
|
+
const hasClearButton =
|
|
131
|
+
hasValue &&
|
|
132
|
+
!inputProps.disabled &&
|
|
133
|
+
!inputProps.readOnly &&
|
|
134
|
+
!loading &&
|
|
135
|
+
onClear &&
|
|
136
|
+
clearButtonAllyLabel;
|
|
137
|
+
|
|
138
|
+
const internalRef = useMutableRefObject<HTMLInputElement | null>(null);
|
|
139
|
+
const setRef = (node: HTMLInputElement | null) => {
|
|
140
|
+
internalRef.current = node;
|
|
141
|
+
if (typeof ref === "function") {
|
|
142
|
+
ref(node);
|
|
143
|
+
} else if (ref) {
|
|
144
|
+
ref.current = node;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
const handleClear = () => {
|
|
148
|
+
onClear?.();
|
|
149
|
+
internalRef.current?.focus();
|
|
150
|
+
};
|
|
94
151
|
|
|
95
152
|
const endAdornments: ReactNode[] = [
|
|
96
153
|
loading && (
|
|
@@ -102,46 +159,57 @@ const TextFieldComponent = (
|
|
|
102
159
|
/>
|
|
103
160
|
),
|
|
104
161
|
isValid && (
|
|
105
|
-
<
|
|
162
|
+
<IconCheckCircleFilled
|
|
106
163
|
key="valid-icon"
|
|
107
164
|
data-testid={getTestId("valid-icon")}
|
|
108
|
-
className={
|
|
109
|
-
svg={checkCircleFilled}
|
|
110
|
-
size="md"
|
|
165
|
+
className={cx(`${rootClassName}__valid-icon`)}
|
|
111
166
|
/>
|
|
112
167
|
),
|
|
168
|
+
hasClearButton && (
|
|
169
|
+
<Button
|
|
170
|
+
key="clear-button"
|
|
171
|
+
variant="tertiary-purple"
|
|
172
|
+
onClick={handleClear}
|
|
173
|
+
iconOnly
|
|
174
|
+
aria-label={clearButtonAllyLabel ?? ""}
|
|
175
|
+
data-testid={getTestId("clear-button")}
|
|
176
|
+
tabIndex={-1}
|
|
177
|
+
>
|
|
178
|
+
<IconClose size="xs" />
|
|
179
|
+
</Button>
|
|
180
|
+
),
|
|
113
181
|
endAdornment,
|
|
114
182
|
].filter((adornment) => !!adornment);
|
|
115
183
|
|
|
116
|
-
const inputContainerClassnames =
|
|
117
|
-
|
|
184
|
+
const inputContainerClassnames = cx([
|
|
185
|
+
`${rootClassName}__input-container`,
|
|
118
186
|
{
|
|
119
|
-
[
|
|
120
|
-
[
|
|
121
|
-
[
|
|
122
|
-
[
|
|
123
|
-
|
|
187
|
+
[`${rootClassName}__input-container--start-adornment`]: startAdornments.length,
|
|
188
|
+
[`${rootClassName}__input-container--end-adornment`]: endAdornments.length,
|
|
189
|
+
[`${rootClassName}__input-container--disabled`]: inputProps.disabled,
|
|
190
|
+
[`${rootClassName}__input-container--has-clear-button`]: hasClearButton,
|
|
191
|
+
[`${rootClassName}__input-container--readonly`]: inputProps.readOnly && !inputProps.disabled,
|
|
124
192
|
},
|
|
125
193
|
]);
|
|
126
194
|
|
|
127
195
|
return (
|
|
128
|
-
<div className={
|
|
196
|
+
<div className={cx(className, rootClassName)}>
|
|
129
197
|
{label && (
|
|
130
198
|
<Label
|
|
131
199
|
htmlFor={inputId}
|
|
132
|
-
className={
|
|
200
|
+
className={cx(`${rootClassName}__label`)}
|
|
133
201
|
data-testid={getTestId("label")}
|
|
134
202
|
disabled={inputProps.disabled}
|
|
135
203
|
>
|
|
136
204
|
{`${inputProps.required ? "* " : ""}${label}`}
|
|
137
205
|
</Label>
|
|
138
206
|
)}
|
|
139
|
-
<div className={
|
|
207
|
+
<div className={cx(`${rootClassName}__field-row`)}>
|
|
140
208
|
<div className={inputContainerClassnames}>
|
|
141
209
|
{!!startAdornments.length && (
|
|
142
210
|
<div
|
|
143
211
|
data-testid={getTestId("start-adornments")}
|
|
144
|
-
className={
|
|
212
|
+
className={cx(`${rootClassName}__adornment-container`)}
|
|
145
213
|
>
|
|
146
214
|
{startAdornments}
|
|
147
215
|
</div>
|
|
@@ -149,23 +217,23 @@ const TextFieldComponent = (
|
|
|
149
217
|
<input
|
|
150
218
|
{...inputProps}
|
|
151
219
|
id={inputId}
|
|
152
|
-
ref={
|
|
220
|
+
ref={setRef}
|
|
153
221
|
data-testid={getTestId("input")}
|
|
154
222
|
aria-describedby={inputProps["aria-describedby"] || helperTextId}
|
|
155
223
|
aria-invalid={inputProps["aria-invalid"] || !!errorText}
|
|
156
|
-
className={
|
|
157
|
-
|
|
224
|
+
className={cx([
|
|
225
|
+
`${rootClassName}__input`,
|
|
158
226
|
{
|
|
159
|
-
[
|
|
160
|
-
[
|
|
227
|
+
[`${rootClassName}__input--valid`]: isValid,
|
|
228
|
+
[`${rootClassName}__input--error`]: !!errorText,
|
|
161
229
|
},
|
|
162
230
|
])}
|
|
163
231
|
/>
|
|
164
|
-
<div className={
|
|
232
|
+
<div className={cx(`${rootClassName}__frame`)} />
|
|
165
233
|
{!!endAdornments.length && (
|
|
166
234
|
<div
|
|
167
235
|
data-testid={getTestId("end-adornments")}
|
|
168
|
-
className={
|
|
236
|
+
className={cx(`${rootClassName}__adornment-container`)}
|
|
169
237
|
>
|
|
170
238
|
{endAdornments}
|
|
171
239
|
</div>
|