Skip to content

Commit

Permalink
docs: fixes for input fields examples (#2149)
Browse files Browse the repository at this point in the history
  • Loading branch information
gpbl committed May 22, 2024
1 parent a023a0b commit ab40cbd
Show file tree
Hide file tree
Showing 4 changed files with 198 additions and 43 deletions.
93 changes: 68 additions & 25 deletions website/docs/advanced-guides/input-fields.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,62 +23,79 @@ These examples showcase different approaches for integrating DayPicker with inpu

### Input with Inline Calendar

In this example, the selected date is displayed in an input field, and the calendar is displayed below the input field. The selected date is updated when the user selects a date from the calendar.
This example demonstrates how to integrate an input field with an inline DayPicker calendar. The input field allows users to type a date, and the calendar updates the selected date based on the input value.

See also the [full source code](https://github.com/gpbl/react-day-picker/blob/main/website/examples/Input.tsx) and [the unit tests](https://github.com/gpbl/react-day-picker/blob/main/website/examples/Input.test.tsx) for this example.

<BrowserWindow>
<Examples.Input />
</BrowserWindow>

```tsx
import { useState } from "react";
import { useId, useState } from "react";

import { format, isValid, parse } from "date-fns";
import { DayPicker } from "react-day-picker";

/** Render an input field bound to a DayPicker calendar. */
export function Input() {
const inputId = useId();

// Hold the month in state to control the calendar when the input changes
const [month, setMonth] = useState(new Date());

// Hold the selected date in state
const [selectedDate, setSelectedDate] = useState<Date | undefined>(undefined);

// Hold the input value in state
const [inputValue, setInputValue] = useState("");

const handleSelect = (date: Date | undefined) => {
const handleDayPickerSelect = (date: Date | undefined) => {
if (!date) {
setInputValue("");
setSelectedDate(undefined);
} else {
setSelectedDate(date);
setMonth(date);
setInputValue(format(date, "MM/dd/yyyy"));
}
};

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value); // keep the input value in sync

const parsedDate = parse(e.target.value, "MM/dd/yyyy", new Date());

if (isValid(parsedDate)) {
setSelectedDate(parsedDate);
setMonth(parsedDate);
} else {
setSelectedDate(undefined);
}
setInputValue(e.target.value);
};

return (
<div>
<label htmlFor="booking-input">
<strong>Selected Date: </strong>
<label htmlFor={inputId}>
<strong>Date: </strong>
</label>
<input
style={{ fontSize: "inherit" }}
id="booking-input"
id={inputId}
type="text"
value={inputValue}
placeholder="MM/dd/yyyy"
onChange={handleInputChange}
/>
<DayPicker
month={month}
onMonthChange={setMonth}
mode="single"
selected={selectedDate}
onSelect={handleSelect}
onSelect={handleDayPickerSelect}
footer={
<p aria-live="assertive" aria-atomic="true">
{selectedDate !== undefined && (
<>Selected: {selectedDate.toDateString()}</>
)}
Selected: {selectedDate?.toDateString()}
</p>
}
/>
Expand All @@ -89,11 +106,9 @@ export function Input() {

### Input with Date Picker Dialog

Implementing the date picker as a dialog requires careful consideration of accessibility. You can refer to the [W3C ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/examples/datepicker-dialog/) for guidance on implementing an accessible date picker dialog.
Implementing the date picker as a dialog requires careful consideration of accessibility. You can refer to the [W3C ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/examples/datepicker-dialog/) for guidance on implementing an accessible date picker.

In this example, we use the native HTML `<dialog>` element, which provides a built-in way to create a modal dialog. The dialog is opened when the user clicks the calendar button, and the selected date is displayed in the input field.

You can replace the native `<dialog>` element with a custom dialog component or a modal library that fits your application's design and accessibility requirements.
In this example, we use the native HTML `<dialog>` element, which provides a built-in way to create a modal dialog. You can replace the native `<dialog>` element with a custom dialog component or a modal library that fits your application's design and accessibility requirements.

<BrowserWindow>
<Examples.Dialog />
Expand All @@ -105,19 +120,27 @@ import { useEffect, useId, useRef, useState } from "react";
import { format, isValid, parse } from "date-fns";
import { DayPicker } from "react-day-picker";

function Dialog() {
export function Dialog() {
const dialogRef = useRef<HTMLDialogElement>(null);
const dialogId = useId();
const headerId = useId();

// Hold the month in state to control the calendar when the input changes
const [month, setMonth] = useState(new Date());

// Hold the selected date in state
const [selectedDate, setSelectedDate] = useState<Date | undefined>(undefined);

// Hold the input value in state
const [inputValue, setInputValue] = useState("");

// Hold the dialog visibility in state
const [isDialogOpen, setIsDialogOpen] = useState(false);

// Function to toggle the dialog visibility
const toggleDialog = () => setIsDialogOpen(!isDialogOpen);

// Hook to handle the body scroll behavior and focus trapping
// Hook to handle the body scroll behavior and focus trapping.
useEffect(() => {
const handleBodyScroll = (isOpen: boolean) => {
document.body.style.overflow = isOpen ? "hidden" : "";
Expand All @@ -130,13 +153,16 @@ function Dialog() {
handleBodyScroll(false);
dialogRef.current.close();
}

return () => {
handleBodyScroll(false);
};
}, [isDialogOpen]);

const handleSelect = (date: Date) => {
/**
* Function to handle the DayPicker select event: update the input value and
* the selected date, and set the month.
*/
const handleDayPickerSelect = (date: Date) => {
if (!date) {
setInputValue("");
setSelectedDate(undefined);
Expand All @@ -146,18 +172,27 @@ function Dialog() {
}
dialogRef.current?.close();
};

/**
* Handle the input change event: parse the input value to a date, update the
* selected date and set the month.
*/
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value); // keep the input value in sync

const parsedDate = parse(e.target.value, "MM/dd/yyyy", new Date());

if (isValid(parsedDate)) {
setSelectedDate(parsedDate);
setMonth(parsedDate);
} else {
setSelectedDate(undefined);
}
setInputValue(e.target.value);
};

return (
<div>
<label htmlFor="date-input">
<strong>Selected Date: </strong>
<strong>Pick a Date: </strong>
</label>
<input
style={{ fontSize: "inherit" }}
Expand All @@ -166,8 +201,7 @@ function Dialog() {
value={inputValue}
placeholder={"MM/dd/yyyy"}
onChange={handleInputChange}
/>
<button
/> <button
style={{ fontSize: "inherit" }}
onClick={toggleDialog}
aria-controls="dialog"
Expand All @@ -191,10 +225,19 @@ function Dialog() {
onClose={() => setIsDialogOpen(false)}
>
<DayPicker
month={month}
onMonthChange={setMonth}
initialFocus
mode="single"
selected={selectedDate}
onSelect={handleSelect}
onSelect={handleDayPickerSelect}
footer={
<p aria-live="assertive" aria-atomic="true">
{selectedDate !== undefined && (
<>Selected: {selectedDate.toDateString()}</>
)}
</p>
}
/>
</dialog>
</div>
Expand Down
43 changes: 36 additions & 7 deletions website/examples/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,22 @@ export function Dialog() {
const dialogId = useId();
const headerId = useId();

// Hold the month in state to control the calendar when the input changes
const [month, setMonth] = useState(new Date());

// Hold the selected date in state
const [selectedDate, setSelectedDate] = useState<Date | undefined>(undefined);

// Hold the input value in state
const [inputValue, setInputValue] = useState("");

// Hold the dialog visibility in state
const [isDialogOpen, setIsDialogOpen] = useState(false);

// Function to toggle the dialog visibility
const toggleDialog = () => setIsDialogOpen(!isDialogOpen);

// Hook to handle the body scroll behavior and focus trapping
// Hook to handle the body scroll behavior and focus trapping.
useEffect(() => {
const handleBodyScroll = (isOpen: boolean) => {
document.body.style.overflow = isOpen ? "hidden" : "";
Expand All @@ -28,13 +36,16 @@ export function Dialog() {
handleBodyScroll(false);
dialogRef.current.close();
}

return () => {
handleBodyScroll(false);
};
}, [isDialogOpen]);

const handleSelect = (date: Date) => {
/**
* Function to handle the DayPicker select event: update the input value and
* the selected date, and set the month.
*/
const handleDayPickerSelect = (date: Date) => {
if (!date) {
setInputValue("");
setSelectedDate(undefined);
Expand All @@ -44,18 +55,27 @@ export function Dialog() {
}
dialogRef.current?.close();
};

/**
* Handle the input change event: parse the input value to a date, update the
* selected date and set the month.
*/
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value); // keep the input value in sync

const parsedDate = parse(e.target.value, "MM/dd/yyyy", new Date());

if (isValid(parsedDate)) {
setSelectedDate(parsedDate);
setMonth(parsedDate);
} else {
setSelectedDate(undefined);
}
setInputValue(e.target.value);
};

return (
<div>
<label htmlFor="date-input">
<strong>Selected Date: </strong>
<strong>Pick a Date: </strong>
</label>
<input
style={{ fontSize: "inherit" }}
Expand Down Expand Up @@ -89,10 +109,19 @@ export function Dialog() {
onClose={() => setIsDialogOpen(false)}
>
<DayPicker
month={month}
onMonthChange={setMonth}
initialFocus
mode="single"
selected={selectedDate}
onSelect={handleSelect}
onSelect={handleDayPickerSelect}
footer={
<p aria-live="assertive" aria-atomic="true">
{selectedDate !== undefined && (
<>Selected: {selectedDate.toDateString()}</>
)}
</p>
}
/>
</dialog>
</div>
Expand Down
61 changes: 61 additions & 0 deletions website/examples/Input.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { render, screen } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
import { format } from "date-fns";

import { Input } from "./Input";

function textbox() {
return screen.getByRole("textbox", { name: "Date:" }) as HTMLInputElement;
}

function gridcells() {
return screen.queryAllByRole("gridcell") as HTMLTableCellElement[];
}

function selectedCells() {
return gridcells().filter((cell) => cell.hasAttribute("aria-selected"));
}

it("renders a textbox", () => {
render(<Input />);
expect(textbox()).toBeInTheDocument();
});

it("updates the calendar when a date is typed in", async () => {
render(<Input />);
const testDate = new Date(2022, 11, 31); // Dec 31, 2022
await userEvent.type(textbox(), format(testDate, "MM/dd/yyyy"));

expect(
screen.getByText(`Selected: ${testDate.toDateString()}`)
).toBeInTheDocument();

expect(selectedCells()).toHaveLength(1);
expect(selectedCells()[0]).toHaveTextContent(`${testDate.getDate()}`);
});

it("updates the input when a day is picked from the calendar", async () => {
render(<Input />);
const testDate = new Date(2022, 11, 31); // Dec 31, 2022
await userEvent.type(textbox(), format(testDate, "MM/dd/yyyy"));

expect(
screen.getByText(`Selected: ${testDate.toDateString()}`)
).toBeInTheDocument();

expect(selectedCells()).toHaveLength(1);
expect(selectedCells()[0]).toHaveTextContent(`${testDate.getDate()}`);
});

it("clears the selected days when an invalid date is entered", async () => {
render(<Input />);
await userEvent.type(textbox(), "invalid date");
expect(selectedCells()).toHaveLength(0);
});

it("updates the month when a date is typed in", async () => {
render(<Input />);
const testDate = new Date(2022, 11, 31); // Dec 31, 2022
await userEvent.type(textbox(), format(testDate, "MM/dd/yyyy"));
expect(screen.getByText(`December 2022`)).toBeInTheDocument();
});
Loading

0 comments on commit ab40cbd

Please sign in to comment.