Skip to content

Commit 8a64266

Browse files
authored
✨ feat: timepicker component (#194)
* ✨ feat: timepicker component * ✨ chore: first steps to create a datetime picker * ✨ feat: continue working on timepicker * ✨ feat: continue working on timepicker * ✨ feat: more capabilities were added * ⬆️ chore: some dependencies have been updated * ✨ chore: fix slect defalt minutes when the number is lower than 10 * ✨ chore: the option to collpase with escape button or click outside the component was added * ✨ chore: the context was created * ⬆️ chore: some components have been updated * ✨ chore: the functionality finished * ✅ chore: test for timepicker have been added
1 parent 7e8771f commit 8a64266

30 files changed

+1376
-185
lines changed

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v22.11.0
1+
v22.14.0

lib/components/Dropdown/contexts/dropdown.hook.tsx renamed to lib/components/Dropdown/contexts/dropdown.hook.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ export const useDropdownContext = (): DropdownContextType => {
66
const context = useContext(DropdownContext);
77

88
if (!context) {
9-
throw new Error('useTheme must be used within a DropdownProvider');
9+
throw new Error(
10+
'useDropdownContext must be used within a DropdownProvider',
11+
);
1012
}
1113

1214
return context;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { Meta, StoryObj } from '@storybook/react';
2+
3+
import { TimePicker as TimePickerComponent } from './TimePicker';
4+
5+
type Story = StoryObj<typeof TimePickerComponent>;
6+
7+
const meta: Meta<typeof TimePickerComponent> = {
8+
title: 'In Review/TimePicker',
9+
component: TimePickerComponent,
10+
};
11+
12+
export const TimePicker: Story = {
13+
render: () => (
14+
<div className="max-w-[350px] flex flex-col gap-2">
15+
<TimePickerComponent name="time-12" label="Time 12 format" required />
16+
17+
<TimePickerComponent
18+
name="time-24"
19+
label="Time 24 format"
20+
format="24"
21+
required
22+
/>
23+
</div>
24+
),
25+
};
26+
27+
export default meta;
Lines changed: 332 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
1+
import { render, screen, within } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
import { axe } from 'jest-axe';
4+
import { FC, PropsWithChildren } from 'react';
5+
6+
import { TimePicker } from './TimePicker';
7+
import { TimePickerProps } from './TimePicker.types';
8+
9+
describe('TimePicker', () => {
10+
const setup = (
11+
props?: Partial<TimePickerProps>,
12+
wrapper?: FC<PropsWithChildren>,
13+
) => {
14+
const defaultProps = {
15+
name: 'time-picker-name',
16+
} satisfies TimePickerProps;
17+
18+
const { container: component } = render(
19+
<TimePicker {...defaultProps} {...props} />,
20+
{
21+
wrapper,
22+
},
23+
);
24+
25+
const user = userEvent.setup();
26+
27+
const getButton = async () =>
28+
screen.findByRole('button', { name: props?.name ?? defaultProps.name });
29+
30+
const handleOpenTimePicker = async () => {
31+
const button = screen.getByRole('button', {
32+
name: props?.name ?? defaultProps.name,
33+
});
34+
await user.click(button);
35+
};
36+
37+
const getHoursList = async () => {
38+
return screen.findByRole('listbox', { name: 'hours' });
39+
};
40+
41+
const getMinutesList = async () => {
42+
return screen.findByRole('listbox', { name: 'minutes' });
43+
};
44+
45+
const getMeridianList = async () => {
46+
return screen.findByRole('listbox', { name: 'meridian' });
47+
};
48+
49+
const selectHour = async (hour: string) => {
50+
const hoursList = await getHoursList();
51+
52+
const hourNode = await within(hoursList).findByText(
53+
new RegExp(new RegExp(`^${hour}$`), 'i'),
54+
);
55+
56+
await user.click(hourNode);
57+
};
58+
59+
const selectMinute = async (minute: string) => {
60+
const minutesList = await getMinutesList();
61+
62+
const minuteNode = await within(minutesList).findByText(minute);
63+
64+
await user.click(minuteNode);
65+
};
66+
67+
const clickAM = async () => {
68+
const meridianList = await getMeridianList();
69+
70+
const amButton = await within(meridianList).findByText(/am/i);
71+
72+
await user.click(amButton);
73+
};
74+
75+
const clickPM = async () => {
76+
const meridianList = await getMeridianList();
77+
78+
const pmButton = await within(meridianList).findByText(/pm/i);
79+
80+
await user.click(pmButton);
81+
};
82+
83+
return {
84+
component,
85+
user,
86+
getButton,
87+
handleOpenTimePicker,
88+
getHoursList,
89+
getMinutesList,
90+
selectHour,
91+
selectMinute,
92+
clickAM,
93+
clickPM,
94+
};
95+
};
96+
97+
it('should render correctly', () => {
98+
const { component } = setup();
99+
100+
expect(component).toBeInTheDocument();
101+
});
102+
103+
it("should doesn't have violations", async () => {
104+
const { component } = setup({ label: 'Time picker label' });
105+
106+
const results = await axe(component);
107+
108+
expect(results).toHaveNoViolations();
109+
});
110+
111+
it("should doesn't have violations when the listbox is open", async () => {
112+
const { component, handleOpenTimePicker } = setup({
113+
label: 'Time picker label',
114+
});
115+
116+
await handleOpenTimePicker();
117+
const results = await axe(component);
118+
119+
expect(results).toHaveNoViolations();
120+
});
121+
122+
it('should open time picker when button is clicked', async () => {
123+
const { handleOpenTimePicker, getHoursList } = setup();
124+
125+
await handleOpenTimePicker();
126+
127+
const hoursList = await getHoursList();
128+
129+
expect(hoursList).toBeInTheDocument();
130+
});
131+
132+
it('should open and close time picker', async () => {
133+
const { handleOpenTimePicker } = setup();
134+
135+
await handleOpenTimePicker();
136+
await handleOpenTimePicker();
137+
138+
expect(
139+
screen.queryByRole('listbox', { name: 'hours' }),
140+
).not.toBeInTheDocument();
141+
});
142+
143+
it.each(Array.from({ length: 12 }, (_, index) => index + 1))(
144+
'should select hour with format 12 and hour: %s',
145+
async (hour) => {
146+
const { handleOpenTimePicker, selectHour, getButton } = setup({
147+
format: '12',
148+
time: new Date(2025, 0, 1, 0, 0, 0),
149+
});
150+
151+
await handleOpenTimePicker();
152+
await selectHour(hour.toString());
153+
154+
const button = await getButton();
155+
156+
expect(button).toHaveTextContent(new RegExp(hour.toString(), 'i'));
157+
},
158+
);
159+
160+
it.each(Array.from({ length: 24 }, (_, index) => index))(
161+
'should select hour with format 24 and hour: %s',
162+
async (hour) => {
163+
const { handleOpenTimePicker, selectHour, getButton } = setup({
164+
format: '24',
165+
time: new Date(2025, 0, 1, 0, 0, 0),
166+
});
167+
168+
await handleOpenTimePicker();
169+
await selectHour(hour.toString());
170+
171+
const button = await getButton();
172+
173+
expect(button).toHaveTextContent(new RegExp(hour.toString(), 'i'));
174+
},
175+
);
176+
177+
it.each(Array.from({ length: 60 }, (_, index) => index))(
178+
'should select minute with format 12 and minute: %s',
179+
async (minute) => {
180+
const { handleOpenTimePicker, selectMinute, getButton } = setup({
181+
format: '12',
182+
time: new Date(2025, 0, 1, 0, 0, 0),
183+
});
184+
185+
const formattedMinute = `0${minute}`.slice(-2);
186+
187+
await handleOpenTimePicker();
188+
await selectMinute(formattedMinute);
189+
190+
const button = await getButton();
191+
192+
expect(button).toHaveTextContent(
193+
new RegExp(formattedMinute.toString(), 'i'),
194+
);
195+
},
196+
);
197+
198+
it('should select am in format 12', async () => {
199+
const {
200+
handleOpenTimePicker,
201+
selectHour,
202+
selectMinute,
203+
clickAM,
204+
getButton,
205+
} = setup({
206+
format: '12',
207+
time: new Date(2025, 0, 1, 0, 0, 0),
208+
});
209+
210+
await handleOpenTimePicker();
211+
await selectHour('11');
212+
await selectMinute('30');
213+
214+
await clickAM();
215+
216+
const button = await getButton();
217+
218+
expect(button).toHaveTextContent('AM');
219+
});
220+
221+
it('should select pm in format 12', async () => {
222+
const {
223+
handleOpenTimePicker,
224+
selectHour,
225+
selectMinute,
226+
clickPM,
227+
getButton,
228+
} = setup({
229+
format: '12',
230+
time: new Date(2025, 0, 1, 0, 0, 0),
231+
});
232+
233+
await handleOpenTimePicker();
234+
await selectHour('11');
235+
await selectMinute('30');
236+
237+
await clickPM();
238+
239+
const button = await getButton();
240+
241+
expect(button).toHaveTextContent('PM');
242+
});
243+
244+
it.each(['AM', 'PM'])(
245+
'should send the time picker data inside a form when format is 12 and %s is selected',
246+
async (meridian) => {
247+
const handleSubmit = vitest.fn();
248+
249+
const Wrapper: FC<PropsWithChildren> = ({ children }) => {
250+
return (
251+
<form
252+
onSubmit={(e) => {
253+
e.preventDefault();
254+
const formData = new FormData(e.currentTarget);
255+
const data = Object.fromEntries(formData.entries());
256+
handleSubmit(data);
257+
}}
258+
>
259+
{children}
260+
<button type="submit">Submit</button>
261+
</form>
262+
);
263+
};
264+
265+
const {
266+
user,
267+
handleOpenTimePicker,
268+
selectHour,
269+
selectMinute,
270+
clickAM,
271+
clickPM,
272+
} = setup({ format: '12', name: 'timepicker' }, Wrapper);
273+
274+
await handleOpenTimePicker();
275+
await selectHour('11');
276+
await selectMinute('30');
277+
278+
if (meridian === 'AM') {
279+
await clickAM();
280+
} else {
281+
await clickPM();
282+
}
283+
284+
const button = screen.getByRole('button', {
285+
name: /submit/i,
286+
});
287+
288+
await user.click(button);
289+
290+
expect(handleSubmit).toHaveBeenCalledWith({
291+
timepicker: `11:30 ${meridian}`,
292+
});
293+
},
294+
);
295+
296+
it('should send the time picker data inside a form when format is 24', async () => {
297+
const handleSubmit = vitest.fn();
298+
299+
const Wrapper: FC<PropsWithChildren> = ({ children }) => {
300+
return (
301+
<form
302+
onSubmit={(e) => {
303+
e.preventDefault();
304+
const formData = new FormData(e.currentTarget);
305+
const data = Object.fromEntries(formData.entries());
306+
handleSubmit(data);
307+
}}
308+
>
309+
{children}
310+
<button type="submit">Submit</button>
311+
</form>
312+
);
313+
};
314+
315+
const { user, handleOpenTimePicker, selectHour, selectMinute } = setup(
316+
{ format: '24', name: 'timepicker' },
317+
Wrapper,
318+
);
319+
320+
await handleOpenTimePicker();
321+
await selectHour('16');
322+
await selectMinute('30');
323+
324+
const button = screen.getByRole('button', {
325+
name: /submit/i,
326+
});
327+
328+
await user.click(button);
329+
330+
expect(handleSubmit).toHaveBeenCalledWith({ timepicker: '16:30' });
331+
});
332+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
'use client';
2+
import { FC } from 'react';
3+
4+
import { TimePickerProps } from './TimePicker.types';
5+
import { TimePickerProvider } from './contexts/time-picker.provider';
6+
import { Wrapper } from './components/Wrapper/Wrapper';
7+
8+
const TimePicker: FC<TimePickerProps> = ({ format = '12', ...delegated }) => (
9+
<TimePickerProvider format={format}>
10+
<Wrapper {...delegated} />
11+
</TimePickerProvider>
12+
);
13+
14+
TimePicker.displayName = 'TimePicker';
15+
16+
export { TimePicker };

0 commit comments

Comments
 (0)