Skip to content

Commit

Permalink
frontend: Add Time picker component (#2743)
Browse files Browse the repository at this point in the history
  • Loading branch information
jdslaugh authored Jul 24, 2023
1 parent d02032b commit 402fdff
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 6 deletions.
4 changes: 2 additions & 2 deletions frontend/packages/core/src/Input/date-time.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ const DateTimePicker = ({ onChange, ...props }: DateTimePickerProps) => (
<LocalizationProvider dateAdapter={AdapterDayjs}>
<MuiDateTimePicker
renderInput={inputProps => <PaddedTextField {...inputProps} />}
onChange={(value: Dayjs) => {
if (value.isValid()) {
onChange={(value: Dayjs | null) => {
if (value && value.isValid()) {
onChange(value.toDate());
}
}}
Expand Down
1 change: 1 addition & 0 deletions frontend/packages/core/src/Input/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { Checkbox, CheckboxPanel } from "./checkbox";
export { default as DateTimePicker } from "./date-time";
export { default as TimePicker } from "./time-picker";
export { Form, FormRow } from "./form";
export { default as Radio } from "./radio";
export { default as RadioGroup } from "./radio-group";
Expand Down
20 changes: 16 additions & 4 deletions frontend/packages/core/src/Input/stories/date-time.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,27 @@ export default {

const Template = (props: DateTimePickerProps) => <DateTimePicker {...props} />;

export const Primary = Template.bind({});
Primary.args = {
export const PrimaryDemo = ({ ...props }) => {
const [dateValue, setDateValue] = React.useState<Date | null>(props.value);

return (
<DateTimePicker
label={props.label}
onChange={(newValue: unknown) => {
setDateValue(newValue as Date);
}}
value={dateValue ?? props.value}
/>
);
};

PrimaryDemo.args = {
label: "My Label",
onChange: () => {},
value: new Date(),
} as DateTimePickerProps;

export const Disabled = Template.bind({});
Disabled.args = {
...Primary.args,
...PrimaryDemo.args,
disabled: true,
} as DateTimePickerProps;
45 changes: 45 additions & 0 deletions frontend/packages/core/src/Input/stories/time-picker.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import * as React from "react";
import type { Meta } from "@storybook/react";

import type { TimePickerProps } from "../time-picker";
import TimePicker from "../time-picker";

export default {
title: "Core/Input/TimePicker",
component: TimePicker,
argTypes: {
label: {
control: "text",
},
value: {
control: "date",
},
},
} as Meta;

const Template = (props: TimePickerProps) => <TimePicker {...props} />;

export const PrimaryDemo = ({ ...props }) => {
const [timeValue, setTimeValue] = React.useState<Date | null>(props.value);

return (
<TimePicker
label={props.label}
onChange={(newValue: unknown) => {
setTimeValue(newValue as Date);
}}
value={timeValue ?? props.value}
/>
);
};

PrimaryDemo.args = {
label: "My Label",
value: new Date(),
} as TimePickerProps;

export const Disabled = Template.bind({});
Disabled.args = {
...PrimaryDemo.args,
disabled: true,
} as TimePickerProps;
65 changes: 65 additions & 0 deletions frontend/packages/core/src/Input/tests/time-picker.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import * as React from "react";
import { fireEvent, render, screen } from "@testing-library/react";

import "@testing-library/jest-dom";

import TimePicker from "../time-picker";

afterEach(() => {
jest.resetAllMocks();
});

const onChange = jest.fn();
test("has padding", () => {
const { container } = render(<TimePicker value={new Date()} onChange={onChange} />);

expect(container.querySelectorAll(".MuiInputBase-adornedEnd")).toHaveLength(1);
expect(container.querySelector(".MuiInputBase-adornedEnd")).toHaveStyle({
"padding-right": "14px",
});
});

test("onChange is called when valid value", () => {
render(<TimePicker value={new Date()} onChange={onChange} />);

expect(screen.getByPlaceholderText("hh:mm (a|p)m")).toBeVisible();
fireEvent.change(screen.getByPlaceholderText("hh:mm (a|p)m"), {
target: { value: "02:55 AM" },
});
expect(onChange).toHaveBeenCalled();
});

test("onChange is not called when invalid value", () => {
render(<TimePicker value={new Date()} onChange={onChange} />);

expect(screen.getByPlaceholderText("hh:mm (a|p)m")).toBeVisible();
fireEvent.change(screen.getByPlaceholderText("hh:mm (a|p)m"), {
target: { value: "invalid" },
});
expect(onChange).not.toHaveBeenCalled();
});

test("sets passed value correctly", () => {
const date = new Date();
const formattedTime = new Intl.DateTimeFormat("en-US", {
hour: "2-digit",
minute: "2-digit",
}).format(date);
const formattedDate = `${formattedTime}`;
render(<TimePicker value={date} onChange={onChange} />);

expect(screen.getByPlaceholderText("hh:mm (a|p)m")).toHaveValue(formattedDate);
});

test("displays label correctly", () => {
const label = "testing";
render(<TimePicker value={new Date()} onChange={onChange} label={label} />);

expect(screen.getByLabelText(label)).toBeVisible();
});

test("is disabled", () => {
render(<TimePicker value={new Date()} onChange={onChange} disabled />);

expect(screen.getByPlaceholderText("hh:mm (a|p)m")).toBeDisabled();
});
41 changes: 41 additions & 0 deletions frontend/packages/core/src/Input/time-picker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from "react";
import type { TimePickerProps as MuiTimePickerProps } from "@mui/lab";
import { TimePicker as MuiTimePicker } from "@mui/x-date-pickers";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import type { Dayjs } from "dayjs";

import styled from "../styled";

import { TextField } from "./text-field";

const PaddedTextField = styled(TextField)({
// This is required as TextField intentionally unsets the right padding for
// end adornment styles since material introduced it in v5
// Clutch has TextFields with end adornments that are end aligned (e.g. resolvers).
".MuiInputBase-adornedEnd": {
paddingRight: "14px",
},
});

export interface TimePickerProps
extends Pick<
MuiTimePickerProps,
"disabled" | "value" | "onChange" | "label" | "PaperProps" | "PopperProps"
> {}

const TimePicker = ({ onChange, ...props }: TimePickerProps) => (
<LocalizationProvider dateAdapter={AdapterDayjs}>
<MuiTimePicker
renderInput={inputProps => <PaddedTextField {...inputProps} />}
onChange={(value: Dayjs | null) => {
if (value && value.isValid()) {
onChange(value.toDate());
}
}}
{...props}
/>
</LocalizationProvider>
);

export default TimePicker;

0 comments on commit 402fdff

Please sign in to comment.