Skip to content

Commit 3961e08

Browse files
committed
[⭐️][SpinButton]: Add SpinButton component
1 parent 3ad7138 commit 3961e08

File tree

9 files changed

+925
-0
lines changed

9 files changed

+925
-0
lines changed

lib/SpinButton/SpinButton.test.tsx

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
import classNames from "classnames";
2+
import {
3+
itShouldMount,
4+
itSupportsDataSetProps,
5+
itSupportsFocusEvents,
6+
itSupportsRef,
7+
itSupportsStyle,
8+
render,
9+
screen,
10+
userEvent,
11+
} from "../../tests/utils";
12+
import * as SpinButton from "./index";
13+
14+
describe("SpinButton", () => {
15+
afterEach(jest.clearAllMocks);
16+
17+
const mockRequiredProps: SpinButton.RootProps = {
18+
label: { screenReaderLabel: "Label" },
19+
max: 100,
20+
min: 0,
21+
setValueText: v => String(v),
22+
};
23+
24+
itShouldMount(SpinButton.Root, mockRequiredProps);
25+
itSupportsStyle(SpinButton.Root, mockRequiredProps);
26+
itSupportsRef(SpinButton.Root, mockRequiredProps, HTMLDivElement);
27+
itSupportsFocusEvents(
28+
SpinButton.Root,
29+
mockRequiredProps,
30+
"[role='spinbutton']",
31+
);
32+
itSupportsDataSetProps(SpinButton.Root, mockRequiredProps);
33+
34+
it("should have the required classNames", () => {
35+
const { unmount } = render(
36+
<SpinButton.Root
37+
{...mockRequiredProps}
38+
disabled
39+
className={({ disabled, focusedVisible }) =>
40+
classNames("spinbutton", {
41+
"spinbutton--disabled": disabled,
42+
"spinbutton--focus-visible": focusedVisible,
43+
})
44+
}
45+
>
46+
<SpinButton.DecrementButton
47+
label={{ screenReaderLabel: "Decrement" }}
48+
className={({ disabled }) =>
49+
classNames("spinbutton__decrement", {
50+
"spinbutton__decrement--disabled": disabled,
51+
})
52+
}
53+
>
54+
-
55+
</SpinButton.DecrementButton>
56+
<SpinButton.IncrementButton
57+
label={{ screenReaderLabel: "Increment" }}
58+
className={({ disabled }) =>
59+
classNames("spinbutton__increment", {
60+
"spinbutton__increment--disabled": disabled,
61+
})
62+
}
63+
>
64+
+
65+
</SpinButton.IncrementButton>
66+
</SpinButton.Root>,
67+
);
68+
69+
expect(screen.getByRole("spinbutton")).toHaveClass(
70+
"spinbutton",
71+
"spinbutton--disabled",
72+
);
73+
74+
expect(screen.getByRole("button", { name: "Decrement" })).toHaveClass(
75+
"spinbutton__decrement",
76+
"spinbutton__decrement--disabled",
77+
);
78+
79+
expect(screen.getByRole("button", { name: "Increment" })).toHaveClass(
80+
"spinbutton__increment",
81+
"spinbutton__increment--disabled",
82+
);
83+
84+
unmount();
85+
render(
86+
<SpinButton.Root
87+
{...mockRequiredProps}
88+
autoFocus
89+
className={({ disabled, focusedVisible }) =>
90+
classNames("spinbutton", {
91+
"spinbutton--disabled": disabled,
92+
"spinbutton--focus-visible": focusedVisible,
93+
})
94+
}
95+
>
96+
<SpinButton.DecrementButton
97+
label={{ screenReaderLabel: "Decrement" }}
98+
className={({ disabled }) =>
99+
classNames("spinbutton__decrement", {
100+
"spinbutton__decrement--disabled": disabled,
101+
})
102+
}
103+
>
104+
-
105+
</SpinButton.DecrementButton>
106+
<SpinButton.IncrementButton
107+
label={{ screenReaderLabel: "Increment" }}
108+
className={({ disabled }) =>
109+
classNames("spinbutton__increment", {
110+
"spinbutton__increment--disabled": disabled,
111+
})
112+
}
113+
>
114+
+
115+
</SpinButton.IncrementButton>
116+
</SpinButton.Root>,
117+
);
118+
119+
expect(screen.getByRole("spinbutton")).toHaveClass(
120+
"spinbutton",
121+
"spinbutton--focus-visible",
122+
);
123+
124+
expect(screen.getByRole("button", { name: "Decrement" })).toHaveClass(
125+
"spinbutton__decrement",
126+
);
127+
128+
expect(screen.getByRole("button", { name: "Increment" })).toHaveClass(
129+
"spinbutton__increment",
130+
);
131+
});
132+
133+
it("should have the required aria and data attributes", () => {
134+
render(
135+
<SpinButton.Root {...mockRequiredProps}>
136+
<SpinButton.DecrementButton label={{ screenReaderLabel: "Decrement" }}>
137+
-
138+
</SpinButton.DecrementButton>
139+
<SpinButton.IncrementButton label={{ screenReaderLabel: "Increment" }}>
140+
+
141+
</SpinButton.IncrementButton>
142+
</SpinButton.Root>,
143+
);
144+
145+
const root = screen.getByRole("spinbutton");
146+
147+
expect(root).toHaveAttribute("aria-disabled", "false");
148+
expect(root).toHaveAttribute("tabindex", "0");
149+
expect(root).toHaveAttribute("aria-valuenow", "0");
150+
expect(root).toHaveAttribute("aria-valuemin", "0");
151+
expect(root).toHaveAttribute("aria-valuemax", "100");
152+
expect(root).toHaveAttribute("aria-valuetext", "0");
153+
expect(root).toHaveAttribute("aria-label", "Label");
154+
expect(root).not.toHaveAttribute("data-disabled");
155+
156+
const dbtn = screen.getByRole("button", { name: "Decrement" });
157+
158+
expect(dbtn).toBeDisabled();
159+
expect(dbtn).toHaveAttribute("tabindex", "-1");
160+
expect(dbtn).toHaveAttribute("aria-label", "Decrement");
161+
expect(dbtn).toHaveAttribute("data-disabled");
162+
163+
const ibtn = screen.getByRole("button", { name: "Increment" });
164+
165+
expect(ibtn).toBeEnabled();
166+
expect(ibtn).toHaveAttribute("tabindex", "-1");
167+
expect(ibtn).toHaveAttribute("aria-label", "Increment");
168+
expect(ibtn).not.toHaveAttribute("data-disabled");
169+
});
170+
171+
it("should work properly with controlled value using keyboard and mouse", async () => {
172+
const handleValueChange = jest.fn<void, [number]>();
173+
174+
render(
175+
<SpinButton.Root
176+
{...mockRequiredProps}
177+
autoFocus
178+
value={0}
179+
onValueChange={handleValueChange}
180+
>
181+
<SpinButton.DecrementButton label={{ screenReaderLabel: "Decrement" }}>
182+
-
183+
</SpinButton.DecrementButton>
184+
<SpinButton.IncrementButton label={{ screenReaderLabel: "Increment" }}>
185+
+
186+
</SpinButton.IncrementButton>
187+
</SpinButton.Root>,
188+
);
189+
190+
const root = screen.getByRole("spinbutton");
191+
const dbtn = screen.getByRole("button", { name: "Decrement" });
192+
const ibtn = screen.getByRole("button", { name: "Increment" });
193+
194+
expect(root).toHaveFocus();
195+
196+
await userEvent.keyboard("[ArrowDown]");
197+
198+
expect(handleValueChange.mock.calls.length).toBe(0);
199+
200+
await userEvent.keyboard("[ArrowUp]");
201+
202+
expect(handleValueChange.mock.calls.length).toBe(1);
203+
expect(handleValueChange.mock.calls[0]?.[0]).toBe(1);
204+
205+
await userEvent.keyboard("[PageUp]");
206+
207+
expect(handleValueChange.mock.calls.length).toBe(2);
208+
expect(handleValueChange.mock.calls[1]?.[0]).toBe(5);
209+
210+
await userEvent.keyboard("[PageDown]");
211+
212+
expect(handleValueChange.mock.calls.length).toBe(2);
213+
214+
await userEvent.keyboard("[End]");
215+
216+
expect(handleValueChange.mock.calls.length).toBe(3);
217+
expect(handleValueChange.mock.calls[2]?.[0]).toBe(100);
218+
219+
await userEvent.keyboard("[Home]");
220+
221+
expect(handleValueChange.mock.calls.length).toBe(3);
222+
223+
handleValueChange.mockReset();
224+
225+
await userEvent.click(dbtn);
226+
227+
expect(handleValueChange.mock.calls.length).toBe(0);
228+
229+
await userEvent.click(ibtn);
230+
231+
expect(handleValueChange.mock.calls.length).toBe(1);
232+
expect(handleValueChange.mock.calls[0]?.[0]).toBe(1);
233+
});
234+
235+
it("should work properly with uncontrolled value using keyboard and mouse", async () => {
236+
const handleValueChange = jest.fn<void, [number]>();
237+
238+
render(
239+
<SpinButton.Root
240+
{...mockRequiredProps}
241+
autoFocus
242+
onValueChange={handleValueChange}
243+
>
244+
<SpinButton.DecrementButton label={{ screenReaderLabel: "Decrement" }}>
245+
-
246+
</SpinButton.DecrementButton>
247+
<SpinButton.IncrementButton label={{ screenReaderLabel: "Increment" }}>
248+
+
249+
</SpinButton.IncrementButton>
250+
</SpinButton.Root>,
251+
);
252+
253+
const root = screen.getByRole("spinbutton");
254+
const dbtn = screen.getByRole("button", { name: "Decrement" });
255+
const ibtn = screen.getByRole("button", { name: "Increment" });
256+
257+
expect(root).toHaveFocus();
258+
259+
await userEvent.keyboard("[ArrowDown]");
260+
261+
expect(handleValueChange.mock.calls.length).toBe(0);
262+
263+
await userEvent.keyboard("[ArrowUp]");
264+
265+
expect(handleValueChange.mock.calls.length).toBe(1);
266+
expect(handleValueChange.mock.calls[0]?.[0]).toBe(1);
267+
268+
await userEvent.keyboard("[PageUp]");
269+
270+
expect(handleValueChange.mock.calls.length).toBe(2);
271+
expect(handleValueChange.mock.calls[1]?.[0]).toBe(6);
272+
273+
await userEvent.keyboard("[PageDown]");
274+
275+
expect(handleValueChange.mock.calls.length).toBe(3);
276+
expect(handleValueChange.mock.calls[2]?.[0]).toBe(1);
277+
278+
await userEvent.keyboard("[PageDown]");
279+
280+
expect(handleValueChange.mock.calls.length).toBe(4);
281+
expect(handleValueChange.mock.calls[3]?.[0]).toBe(0);
282+
283+
await userEvent.keyboard("[End]");
284+
285+
expect(handleValueChange.mock.calls.length).toBe(5);
286+
expect(handleValueChange.mock.calls[4]?.[0]).toBe(100);
287+
288+
await userEvent.keyboard("[PageUp]");
289+
290+
expect(handleValueChange.mock.calls.length).toBe(5);
291+
292+
await userEvent.keyboard("[ArrowDown]");
293+
294+
expect(handleValueChange.mock.calls.length).toBe(6);
295+
expect(handleValueChange.mock.calls[5]?.[0]).toBe(99);
296+
297+
await userEvent.keyboard("[PageUp]");
298+
299+
expect(handleValueChange.mock.calls.length).toBe(7);
300+
expect(handleValueChange.mock.calls[6]?.[0]).toBe(100);
301+
302+
await userEvent.keyboard("[Home]");
303+
304+
expect(handleValueChange.mock.calls.length).toBe(8);
305+
expect(handleValueChange.mock.calls[7]?.[0]).toBe(0);
306+
307+
handleValueChange.mockReset();
308+
309+
await userEvent.click(dbtn);
310+
311+
expect(handleValueChange.mock.calls.length).toBe(0);
312+
313+
await userEvent.click(ibtn);
314+
315+
expect(handleValueChange.mock.calls.length).toBe(1);
316+
expect(handleValueChange.mock.calls[0]?.[0]).toBe(1);
317+
318+
await userEvent.click(dbtn);
319+
320+
expect(handleValueChange.mock.calls.length).toBe(2);
321+
expect(handleValueChange.mock.calls[1]?.[0]).toBe(0);
322+
});
323+
});

0 commit comments

Comments
 (0)