@seed-design/react 1.0.7 → 1.1.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/lib/components/ActionButton/ActionButton.cjs +7 -1
- package/lib/components/ActionButton/ActionButton.d.ts +7 -1
- package/lib/components/ActionButton/ActionButton.d.ts.map +1 -1
- package/lib/components/ActionButton/ActionButton.js +7 -1
- package/lib/components/BottomSheet/BottomSheet.cjs +14 -18
- package/lib/components/BottomSheet/BottomSheet.d.ts +12 -19
- package/lib/components/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/lib/components/BottomSheet/BottomSheet.js +14 -18
- package/lib/components/BottomSheet/BottomSheet.namespace.cjs +2 -0
- package/lib/components/BottomSheet/BottomSheet.namespace.d.ts +1 -0
- package/lib/components/BottomSheet/BottomSheet.namespace.d.ts.map +1 -1
- package/lib/components/BottomSheet/BottomSheet.namespace.js +1 -0
- package/lib/components/BottomSheetHandle/BottomSheetHandle.cjs +20 -0
- package/lib/components/BottomSheetHandle/BottomSheetHandle.d.ts +6 -0
- package/lib/components/BottomSheetHandle/BottomSheetHandle.d.ts.map +1 -0
- package/lib/components/BottomSheetHandle/BottomSheetHandle.js +16 -0
- package/lib/components/BottomSheetHandle/index.cjs +9 -0
- package/lib/components/BottomSheetHandle/index.d.ts +2 -0
- package/lib/components/BottomSheetHandle/index.d.ts.map +1 -0
- package/lib/components/BottomSheetHandle/index.js +1 -0
- package/lib/components/Chip/Chip.cjs +4 -3
- package/lib/components/Chip/Chip.d.ts.map +1 -1
- package/lib/components/Chip/Chip.js +4 -3
- package/lib/components/Field/Field.cjs +108 -0
- package/lib/components/Field/Field.d.ts +41 -0
- package/lib/components/Field/Field.d.ts.map +1 -0
- package/lib/components/Field/Field.js +96 -0
- package/lib/components/Field/Field.namespace.cjs +17 -0
- package/lib/components/Field/Field.namespace.d.ts +2 -0
- package/lib/components/Field/Field.namespace.d.ts.map +1 -0
- package/lib/components/Field/Field.namespace.js +1 -0
- package/lib/components/Field/index.cjs +19 -0
- package/lib/components/Field/index.d.ts +3 -0
- package/lib/components/Field/index.d.ts.map +1 -0
- package/lib/components/Field/index.js +3 -0
- package/lib/components/FieldButton/FieldButton.cjs +201 -0
- package/lib/components/FieldButton/FieldButton.d.ts +61 -0
- package/lib/components/FieldButton/FieldButton.d.ts.map +1 -0
- package/lib/components/FieldButton/FieldButton.js +161 -0
- package/lib/components/FieldButton/FieldButton.namespace.cjs +26 -0
- package/lib/components/FieldButton/FieldButton.namespace.d.ts +2 -0
- package/lib/components/FieldButton/FieldButton.namespace.d.ts.map +1 -0
- package/lib/components/FieldButton/FieldButton.namespace.js +1 -0
- package/lib/components/FieldButton/index.cjs +28 -0
- package/lib/components/FieldButton/index.d.ts +3 -0
- package/lib/components/FieldButton/index.d.ts.map +1 -0
- package/lib/components/FieldButton/index.js +3 -0
- package/lib/components/HelpBubble/HelpBubble.d.ts +1 -1
- package/lib/components/List/List.cjs +5 -4
- package/lib/components/List/List.d.ts.map +1 -1
- package/lib/components/List/List.js +5 -4
- package/lib/components/PageBanner/PageBanner.cjs +8 -3
- package/lib/components/PageBanner/PageBanner.d.ts +5 -2
- package/lib/components/PageBanner/PageBanner.d.ts.map +1 -1
- package/lib/components/PageBanner/PageBanner.js +7 -3
- package/lib/components/PageBanner/PageBanner.namespace.cjs +2 -1
- package/lib/components/PageBanner/PageBanner.namespace.d.ts +1 -1
- package/lib/components/PageBanner/PageBanner.namespace.d.ts.map +1 -1
- package/lib/components/PageBanner/PageBanner.namespace.js +1 -1
- package/lib/components/PageBanner/index.cjs +2 -1
- package/lib/components/PageBanner/index.d.ts +1 -1
- package/lib/components/PageBanner/index.d.ts.map +1 -1
- package/lib/components/PageBanner/index.js +1 -1
- package/lib/components/Slider/Slider.cjs +110 -0
- package/lib/components/Slider/Slider.d.ts +51 -0
- package/lib/components/Slider/Slider.d.ts.map +1 -0
- package/lib/components/Slider/Slider.js +94 -0
- package/lib/components/Slider/Slider.namespace.cjs +21 -0
- package/lib/components/Slider/Slider.namespace.d.ts +2 -0
- package/lib/components/Slider/Slider.namespace.d.ts.map +1 -0
- package/lib/components/Slider/Slider.namespace.js +1 -0
- package/lib/components/Slider/index.cjs +23 -0
- package/lib/components/Slider/index.d.ts +3 -0
- package/lib/components/Slider/index.d.ts.map +1 -0
- package/lib/components/Slider/index.js +3 -0
- package/lib/components/TextField/TextField.cjs +54 -74
- package/lib/components/TextField/TextField.d.ts +2 -35
- package/lib/components/TextField/TextField.d.ts.map +1 -1
- package/lib/components/TextField/TextField.js +56 -65
- package/lib/components/TextField/TextField.namespace.cjs +0 -11
- package/lib/components/TextField/TextField.namespace.d.ts +1 -1
- package/lib/components/TextField/TextField.namespace.d.ts.map +1 -1
- package/lib/components/TextField/TextField.namespace.js +1 -1
- package/lib/components/TextField/TextField.test.d.ts +1 -0
- package/lib/components/TextField/TextField.test.d.ts.map +1 -0
- package/lib/components/TextField/index.cjs +2 -11
- package/lib/components/TextField/index.d.ts +2 -1
- package/lib/components/TextField/index.d.ts.map +1 -1
- package/lib/components/TextField/index.js +2 -1
- package/lib/components/TextField/memoize.cjs +18 -0
- package/lib/components/TextField/memoize.d.ts +2 -0
- package/lib/components/TextField/memoize.d.ts.map +1 -0
- package/lib/components/TextField/memoize.js +14 -0
- package/lib/components/TextField/useTextFieldWithGraphemes.cjs +52 -0
- package/lib/components/TextField/useTextFieldWithGraphemes.d.ts +23 -0
- package/lib/components/TextField/useTextFieldWithGraphemes.d.ts.map +1 -0
- package/lib/components/TextField/useTextFieldWithGraphemes.js +48 -0
- package/lib/components/TextField/useTextFieldWithGraphemes.test.d.ts +1 -0
- package/lib/components/TextField/useTextFieldWithGraphemes.test.d.ts.map +1 -0
- package/lib/components/index.cjs +53 -12
- package/lib/components/index.d.ts +3 -0
- package/lib/components/index.d.ts.map +1 -1
- package/lib/components/index.js +12 -2
- package/lib/index.cjs +53 -12
- package/lib/index.js +12 -2
- package/lib/node_modules/unicode-segmenter/_grapheme_data.cjs +23 -0
- package/lib/node_modules/unicode-segmenter/_grapheme_data.js +19 -0
- package/lib/node_modules/unicode-segmenter/_incb_data.cjs +29 -0
- package/lib/node_modules/unicode-segmenter/_incb_data.js +25 -0
- package/lib/node_modules/unicode-segmenter/core.cjs +83 -0
- package/lib/node_modules/unicode-segmenter/core.js +78 -0
- package/lib/node_modules/unicode-segmenter/grapheme.cjs +312 -0
- package/lib/node_modules/unicode-segmenter/grapheme.js +307 -0
- package/lib/primitive.cjs +7 -0
- package/lib/primitive.d.ts +1 -0
- package/lib/primitive.d.ts.map +1 -1
- package/lib/primitive.js +1 -0
- package/lib/utils/createWithStateProps.cjs +8 -4
- package/lib/utils/createWithStateProps.d.ts +6 -3
- package/lib/utils/createWithStateProps.d.ts.map +1 -1
- package/lib/utils/createWithStateProps.js +8 -4
- package/package.json +9 -6
- package/src/components/ActionButton/ActionButton.tsx +21 -2
- package/src/components/BottomSheet/BottomSheet.namespace.ts +5 -0
- package/src/components/BottomSheet/BottomSheet.tsx +24 -38
- package/src/components/BottomSheetHandle/BottomSheetHandle.tsx +22 -0
- package/src/components/BottomSheetHandle/index.ts +1 -0
- package/src/components/Chip/Chip.tsx +4 -3
- package/src/components/Field/Field.namespace.ts +19 -0
- package/src/components/Field/Field.tsx +136 -0
- package/src/components/Field/index.ts +21 -0
- package/src/components/FieldButton/FieldButton.namespace.ts +38 -0
- package/src/components/FieldButton/FieldButton.tsx +249 -0
- package/src/components/FieldButton/index.ts +40 -0
- package/src/components/HelpBubble/HelpBubble.tsx +1 -1
- package/src/components/List/List.tsx +5 -4
- package/src/components/PageBanner/PageBanner.namespace.ts +4 -2
- package/src/components/PageBanner/PageBanner.tsx +10 -3
- package/src/components/PageBanner/index.ts +4 -2
- package/src/components/Slider/Slider.namespace.ts +28 -0
- package/src/components/Slider/Slider.tsx +154 -0
- package/src/components/Slider/index.ts +30 -0
- package/src/components/TextField/TextField.namespace.ts +2 -24
- package/src/components/TextField/TextField.test.tsx +260 -0
- package/src/components/TextField/TextField.tsx +67 -143
- package/src/components/TextField/index.ts +7 -24
- package/src/components/TextField/memoize.ts +14 -0
- package/src/components/TextField/useTextFieldWithGraphemes.test.tsx +301 -0
- package/src/components/TextField/useTextFieldWithGraphemes.ts +65 -0
- package/src/components/index.ts +3 -0
- package/src/primitive.ts +1 -0
- package/src/utils/createWithStateProps.tsx +14 -9
- package/lib/components/List/ListHeader.namespace.d.ts +0 -2
- package/lib/components/List/ListHeader.namespace.d.ts.map +0 -1
- package/src/components/List/ListHeader.namespace.ts +0 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import "@testing-library/jest-dom/vitest";
|
|
2
|
+
import { cleanup, render } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import type { ReactElement } from "react";
|
|
6
|
+
import React from "react";
|
|
7
|
+
|
|
8
|
+
import { TextFieldRoot, TextFieldInput } from "./TextField";
|
|
9
|
+
import {
|
|
10
|
+
FieldRoot,
|
|
11
|
+
FieldCharacterCount,
|
|
12
|
+
FieldLabel,
|
|
13
|
+
FieldFooter,
|
|
14
|
+
FieldHeader,
|
|
15
|
+
} from "../Field/Field";
|
|
16
|
+
import { useTextFieldWithGraphemes } from "./useTextFieldWithGraphemes";
|
|
17
|
+
|
|
18
|
+
afterEach(cleanup);
|
|
19
|
+
|
|
20
|
+
function setUp(jsx: ReactElement) {
|
|
21
|
+
return {
|
|
22
|
+
user: userEvent.setup(),
|
|
23
|
+
...render(jsx),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface TextFieldWithGraphemesProps {
|
|
28
|
+
maxGraphemeCount?: number;
|
|
29
|
+
value?: string;
|
|
30
|
+
defaultValue?: string;
|
|
31
|
+
onValueChange?: (values: {
|
|
32
|
+
value: string;
|
|
33
|
+
graphemes: string[];
|
|
34
|
+
slicedValue: string;
|
|
35
|
+
slicedGraphemes: string[];
|
|
36
|
+
}) => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const TextFieldWithGraphemes = (props: TextFieldWithGraphemesProps) => {
|
|
40
|
+
const { textFieldRootProps, counterProps } = useTextFieldWithGraphemes(props);
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<FieldRoot>
|
|
44
|
+
<FieldHeader>
|
|
45
|
+
<FieldLabel>Text Field</FieldLabel>
|
|
46
|
+
</FieldHeader>
|
|
47
|
+
<TextFieldRoot {...textFieldRootProps}>
|
|
48
|
+
<TextFieldInput data-testid="input" />
|
|
49
|
+
</TextFieldRoot>
|
|
50
|
+
<FieldFooter>
|
|
51
|
+
<FieldCharacterCount {...counterProps} data-testid="counter" />
|
|
52
|
+
</FieldFooter>
|
|
53
|
+
</FieldRoot>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
describe("useTextFieldWithGraphemes", () => {
|
|
58
|
+
describe("basic functionality", () => {
|
|
59
|
+
it("should render with empty default value", () => {
|
|
60
|
+
const { getByTestId } = setUp(<TextFieldWithGraphemes />);
|
|
61
|
+
const input = getByTestId("input");
|
|
62
|
+
|
|
63
|
+
expect(input).toHaveValue("");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should render with provided default value", () => {
|
|
67
|
+
const { getByTestId } = setUp(<TextFieldWithGraphemes defaultValue="Hello" />);
|
|
68
|
+
const input = getByTestId("input");
|
|
69
|
+
|
|
70
|
+
expect(input).toHaveValue("Hello");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("should handle typing in uncontrolled mode", async () => {
|
|
74
|
+
const { getByTestId, user } = setUp(<TextFieldWithGraphemes />);
|
|
75
|
+
const input = getByTestId("input");
|
|
76
|
+
|
|
77
|
+
await user.type(input, "test");
|
|
78
|
+
expect(input).toHaveValue("test");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("should handle controlled mode", async () => {
|
|
82
|
+
function ControlledComponent() {
|
|
83
|
+
const [value, setValue] = React.useState("initial");
|
|
84
|
+
return (
|
|
85
|
+
<TextFieldWithGraphemes value={value} onValueChange={({ value }) => setValue(value)} />
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const { getByTestId, user } = setUp(<ControlledComponent />);
|
|
90
|
+
const input = getByTestId("input");
|
|
91
|
+
|
|
92
|
+
expect(input).toHaveValue("initial");
|
|
93
|
+
|
|
94
|
+
await user.clear(input);
|
|
95
|
+
await user.type(input, "new");
|
|
96
|
+
|
|
97
|
+
expect(input).toHaveValue("new");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("should track grapheme count with counter", async () => {
|
|
101
|
+
const { getByTestId, user } = setUp(<TextFieldWithGraphemes maxGraphemeCount={20} />);
|
|
102
|
+
const input = getByTestId("input");
|
|
103
|
+
const counter = getByTestId("counter");
|
|
104
|
+
|
|
105
|
+
expect(counter).toHaveTextContent("0/20");
|
|
106
|
+
|
|
107
|
+
await user.type(input, "Hello");
|
|
108
|
+
expect(counter).toHaveTextContent("5/20");
|
|
109
|
+
|
|
110
|
+
await user.type(input, " 👨👩👧👦");
|
|
111
|
+
expect(counter).toHaveTextContent("7/20");
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("counter functionality", () => {
|
|
116
|
+
it("should show the right character count with defaultValue", () => {
|
|
117
|
+
const { getByTestId } = setUp(
|
|
118
|
+
<TextFieldWithGraphemes maxGraphemeCount={10} defaultValue="Hello" />,
|
|
119
|
+
);
|
|
120
|
+
const counter = getByTestId("counter");
|
|
121
|
+
|
|
122
|
+
expect(counter).toBeInTheDocument();
|
|
123
|
+
expect(counter).toHaveTextContent("5/10");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("should update counter as user types", async () => {
|
|
127
|
+
const { getByTestId, user } = setUp(<TextFieldWithGraphemes maxGraphemeCount={10} />);
|
|
128
|
+
const input = getByTestId("input");
|
|
129
|
+
const counter = getByTestId("counter");
|
|
130
|
+
|
|
131
|
+
expect(counter).toHaveTextContent("0/10");
|
|
132
|
+
|
|
133
|
+
await user.type(input, "Test");
|
|
134
|
+
expect(counter).toHaveTextContent("4/10");
|
|
135
|
+
|
|
136
|
+
await user.clear(input);
|
|
137
|
+
expect(counter).toHaveTextContent("0/10");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("should handle maxGraphemes of 0", () => {
|
|
141
|
+
const { getByTestId } = setUp(
|
|
142
|
+
<TextFieldWithGraphemes maxGraphemeCount={0} defaultValue="Test" />,
|
|
143
|
+
);
|
|
144
|
+
const counter = getByTestId("counter");
|
|
145
|
+
|
|
146
|
+
expect(counter).toHaveTextContent("4/0");
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("onValueChange callback", () => {
|
|
151
|
+
it("should call onValueChange with correct parameters", async () => {
|
|
152
|
+
const handleValueChange = vi.fn();
|
|
153
|
+
const { getByTestId, user } = setUp(
|
|
154
|
+
<TextFieldWithGraphemes onValueChange={handleValueChange} />,
|
|
155
|
+
);
|
|
156
|
+
const input = getByTestId("input");
|
|
157
|
+
|
|
158
|
+
await user.type(input, "H");
|
|
159
|
+
expect(handleValueChange).toHaveBeenLastCalledWith({
|
|
160
|
+
value: "H",
|
|
161
|
+
graphemes: ["H"],
|
|
162
|
+
slicedValue: "H",
|
|
163
|
+
slicedGraphemes: ["H"],
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
await user.type(input, "i");
|
|
167
|
+
expect(handleValueChange).toHaveBeenLastCalledWith({
|
|
168
|
+
value: "Hi",
|
|
169
|
+
graphemes: ["H", "i"],
|
|
170
|
+
slicedValue: "Hi",
|
|
171
|
+
slicedGraphemes: ["H", "i"],
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("should provide sliced values when maxGraphemes is set", async () => {
|
|
176
|
+
const handleValueChange = vi.fn();
|
|
177
|
+
const { getByTestId, user } = setUp(
|
|
178
|
+
<TextFieldWithGraphemes maxGraphemeCount={3} onValueChange={handleValueChange} />,
|
|
179
|
+
);
|
|
180
|
+
const input = getByTestId("input");
|
|
181
|
+
|
|
182
|
+
await user.type(input, "Hello");
|
|
183
|
+
|
|
184
|
+
const lastCall = handleValueChange.mock.calls[handleValueChange.mock.calls.length - 1][0];
|
|
185
|
+
|
|
186
|
+
expect(lastCall.value).toBe("Hello");
|
|
187
|
+
expect(lastCall.graphemes).toEqual(["H", "e", "l", "l", "o"]);
|
|
188
|
+
expect(lastCall.slicedValue).toBe("Hel");
|
|
189
|
+
expect(lastCall.slicedGraphemes).toEqual(["H", "e", "l"]);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("should handle empty string", async () => {
|
|
193
|
+
const handleValueChange = vi.fn();
|
|
194
|
+
const { getByTestId, user } = setUp(
|
|
195
|
+
<TextFieldWithGraphemes defaultValue="Test" onValueChange={handleValueChange} />,
|
|
196
|
+
);
|
|
197
|
+
const input = getByTestId("input");
|
|
198
|
+
|
|
199
|
+
await user.clear(input);
|
|
200
|
+
|
|
201
|
+
expect(handleValueChange).toHaveBeenLastCalledWith({
|
|
202
|
+
value: "",
|
|
203
|
+
graphemes: [],
|
|
204
|
+
slicedValue: "",
|
|
205
|
+
slicedGraphemes: [],
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("should be called in both controlled and uncontrolled modes", async () => {
|
|
210
|
+
const handleUncontrolled = vi.fn();
|
|
211
|
+
const handleControlled = vi.fn();
|
|
212
|
+
|
|
213
|
+
function TestBothModes() {
|
|
214
|
+
const [controlledValue, setControlledValue] = React.useState("controlled");
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<>
|
|
218
|
+
<TextFieldWithGraphemes
|
|
219
|
+
defaultValue="uncontrolled"
|
|
220
|
+
onValueChange={handleUncontrolled}
|
|
221
|
+
/>
|
|
222
|
+
<TextFieldWithGraphemes
|
|
223
|
+
value={controlledValue}
|
|
224
|
+
onValueChange={(values) => {
|
|
225
|
+
handleControlled(values);
|
|
226
|
+
setControlledValue(values.value);
|
|
227
|
+
}}
|
|
228
|
+
/>
|
|
229
|
+
</>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const { getAllByTestId, user } = setUp(<TestBothModes />);
|
|
234
|
+
const [uncontrolledInput, controlledInput] = getAllByTestId("input");
|
|
235
|
+
|
|
236
|
+
await user.type(uncontrolledInput, "!");
|
|
237
|
+
await user.type(controlledInput, "!");
|
|
238
|
+
|
|
239
|
+
expect(handleUncontrolled).toHaveBeenCalled();
|
|
240
|
+
expect(handleControlled).toHaveBeenCalled();
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe("controlled vs uncontrolled", () => {
|
|
245
|
+
it("should maintain internal state in uncontrolled mode", async () => {
|
|
246
|
+
const { getByTestId, user } = setUp(<TextFieldWithGraphemes defaultValue="initial" />);
|
|
247
|
+
const input = getByTestId("input");
|
|
248
|
+
|
|
249
|
+
expect(input).toHaveValue("initial");
|
|
250
|
+
|
|
251
|
+
await user.clear(input);
|
|
252
|
+
await user.type(input, "changed");
|
|
253
|
+
expect(input).toHaveValue("changed");
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("should not update without onValueChange in controlled mode", async () => {
|
|
257
|
+
const { getByTestId, user } = setUp(<TextFieldWithGraphemes value="fixed" />);
|
|
258
|
+
const input = getByTestId("input");
|
|
259
|
+
|
|
260
|
+
expect(input).toHaveValue("fixed");
|
|
261
|
+
|
|
262
|
+
await user.type(input, "test");
|
|
263
|
+
expect(input).toHaveValue("fixed");
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe("edge cases", () => {
|
|
268
|
+
it("should handle very long text", () => {
|
|
269
|
+
const longText = "a".repeat(1000);
|
|
270
|
+
const { getByTestId } = setUp(
|
|
271
|
+
<TextFieldWithGraphemes defaultValue={longText} maxGraphemeCount={2000} />,
|
|
272
|
+
);
|
|
273
|
+
const counter = getByTestId("counter");
|
|
274
|
+
|
|
275
|
+
expect(counter).toHaveTextContent("1000/2000");
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("should handle special unicode characters", () => {
|
|
279
|
+
const { getByTestId } = setUp(
|
|
280
|
+
<TextFieldWithGraphemes defaultValue="👫🏼é🏳️🌈" maxGraphemeCount={2} />,
|
|
281
|
+
);
|
|
282
|
+
const counter = getByTestId("counter");
|
|
283
|
+
|
|
284
|
+
// 3 graphemes: family emoji, rainbow flag, é
|
|
285
|
+
expect(counter).toHaveTextContent("3/2");
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("should call onValueChange even when value doesn't change in controlled mode", async () => {
|
|
289
|
+
const handleValueChange = vi.fn();
|
|
290
|
+
const { getByTestId, user } = setUp(
|
|
291
|
+
<TextFieldWithGraphemes value="fixed" onValueChange={handleValueChange} />,
|
|
292
|
+
);
|
|
293
|
+
const input = getByTestId("input");
|
|
294
|
+
|
|
295
|
+
await user.type(input, "test");
|
|
296
|
+
|
|
297
|
+
expect(handleValueChange).toHaveBeenCalled();
|
|
298
|
+
expect(input).toHaveValue("fixed");
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { useState, useMemo, useCallback } from "react";
|
|
2
|
+
import { splitGraphemes } from "unicode-segmenter/grapheme";
|
|
3
|
+
import { memoize } from "./memoize";
|
|
4
|
+
|
|
5
|
+
export interface UseTextFieldWithGraphemesParams {
|
|
6
|
+
maxGraphemeCount?: number;
|
|
7
|
+
value?: string;
|
|
8
|
+
defaultValue?: string;
|
|
9
|
+
onValueChange?: (values: {
|
|
10
|
+
value: string;
|
|
11
|
+
graphemes: string[];
|
|
12
|
+
slicedValue: string;
|
|
13
|
+
slicedGraphemes: string[];
|
|
14
|
+
}) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const getGraphemes = (string: string) => Array.from(splitGraphemes(string));
|
|
18
|
+
const memoizedGetGraphemes = memoize(getGraphemes);
|
|
19
|
+
|
|
20
|
+
export function useTextFieldWithGraphemes({
|
|
21
|
+
maxGraphemeCount,
|
|
22
|
+
value: controlledValue,
|
|
23
|
+
defaultValue = "",
|
|
24
|
+
onValueChange,
|
|
25
|
+
}: UseTextFieldWithGraphemesParams) {
|
|
26
|
+
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
|
|
27
|
+
const isControlled = controlledValue !== undefined;
|
|
28
|
+
const value = isControlled ? controlledValue : uncontrolledValue;
|
|
29
|
+
|
|
30
|
+
const graphemes = useMemo(() => memoizedGetGraphemes(value), [value]);
|
|
31
|
+
|
|
32
|
+
const handleValueChange = useCallback(
|
|
33
|
+
(newValue: string) => {
|
|
34
|
+
const newGraphemes = memoizedGetGraphemes(newValue);
|
|
35
|
+
const newSlicedGraphemes =
|
|
36
|
+
maxGraphemeCount === undefined ? newGraphemes : newGraphemes.slice(0, maxGraphemeCount);
|
|
37
|
+
const newSlicedValue = newSlicedGraphemes.join("");
|
|
38
|
+
|
|
39
|
+
// Update internal state if uncontrolled
|
|
40
|
+
if (!isControlled) {
|
|
41
|
+
setUncontrolledValue(newValue);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
onValueChange?.({
|
|
45
|
+
value: newValue,
|
|
46
|
+
graphemes: newGraphemes,
|
|
47
|
+
slicedValue: newSlicedValue,
|
|
48
|
+
slicedGraphemes: newSlicedGraphemes,
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
[isControlled, maxGraphemeCount, onValueChange],
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
textFieldRootProps: {
|
|
56
|
+
value,
|
|
57
|
+
onValueChange: handleValueChange,
|
|
58
|
+
},
|
|
59
|
+
counterProps: {
|
|
60
|
+
current: graphemes.length,
|
|
61
|
+
max: maxGraphemeCount ?? 0,
|
|
62
|
+
},
|
|
63
|
+
graphemes,
|
|
64
|
+
};
|
|
65
|
+
}
|
package/src/components/index.ts
CHANGED
|
@@ -21,6 +21,8 @@ export * from "./Divider";
|
|
|
21
21
|
export * from "./ExtendedActionSheet";
|
|
22
22
|
export * from "./ExtendedFab";
|
|
23
23
|
export * from "./Fab";
|
|
24
|
+
export * from "./Field";
|
|
25
|
+
export * from "./FieldButton";
|
|
24
26
|
export * from "./Flex";
|
|
25
27
|
export * from "./Float";
|
|
26
28
|
export * from "./FloatingActionButton";
|
|
@@ -46,6 +48,7 @@ export * from "./ResponsivePair";
|
|
|
46
48
|
export * from "./SegmentedControl";
|
|
47
49
|
export * from "./SelectBox";
|
|
48
50
|
export * from "./Skeleton";
|
|
51
|
+
export * from "./Slider";
|
|
49
52
|
export * from "./Snackbar";
|
|
50
53
|
export * from "./Stack";
|
|
51
54
|
export * from "./Switch";
|
package/src/primitive.ts
CHANGED
|
@@ -5,6 +5,7 @@ export * from "@seed-design/react-popover";
|
|
|
5
5
|
export * from "@seed-design/react-progress";
|
|
6
6
|
export * from "@seed-design/react-pull-to-refresh";
|
|
7
7
|
export * from "@seed-design/react-radio-group";
|
|
8
|
+
export * from "@seed-design/react-slider";
|
|
8
9
|
export * from "@seed-design/react-snackbar";
|
|
9
10
|
export * from "@seed-design/react-switch";
|
|
10
11
|
export * from "@seed-design/react-tabs";
|
|
@@ -2,19 +2,24 @@ import { forwardRef } from "react";
|
|
|
2
2
|
|
|
3
3
|
type AtLeastOne<T> = [T, ...T[]];
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
>,
|
|
9
|
-
options?: { strict?: boolean },
|
|
10
|
-
) {
|
|
11
|
-
const strict = options?.strict ?? true;
|
|
5
|
+
type ContextHook = (prop?: {
|
|
6
|
+
strict?: boolean;
|
|
7
|
+
}) => { stateProps: React.HTMLAttributes<HTMLElement> } | null;
|
|
12
8
|
|
|
9
|
+
type ContextConfig = ContextHook | { useContext: ContextHook; strict?: boolean };
|
|
10
|
+
|
|
11
|
+
export function createWithStateProps(useContexts: AtLeastOne<ContextConfig>) {
|
|
13
12
|
return function withStateProps<P, R>(Component: React.ComponentType<P & React.RefAttributes<R>>) {
|
|
14
13
|
const Node = forwardRef<R, P>((props, ref) => {
|
|
15
14
|
const stateProps = {};
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
|
|
16
|
+
for (const contextConfig of useContexts) {
|
|
17
|
+
if (typeof contextConfig === "function") {
|
|
18
|
+
Object.assign(stateProps, contextConfig({ strict: true })?.stateProps);
|
|
19
|
+
} else {
|
|
20
|
+
const { useContext, strict = false } = contextConfig;
|
|
21
|
+
Object.assign(stateProps, useContext({ strict })?.stateProps);
|
|
22
|
+
}
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
// @ts-ignore
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ListHeader.namespace.d.ts","sourceRoot":"","sources":["../../../src/components/List/ListHeader.namespace.ts"],"names":[],"mappings":""}
|
|
File without changes
|