💄(react) add styling for range selection on a cell

Integrate new styling classes for grid-cells to facilitate range selection
in the DatePicker component. This implementation improves the visual
representation and user experience of selecting a range. In addition,
outside-month cells are now hidden, to avoid having a range selection
that contains outside-month cells.
This commit is contained in:
Lebaud Antoine
2023-06-14 19:38:17 +02:00
committed by aleb_the_flash
parent 87ec3a5061
commit 2886f0c992
9 changed files with 178 additions and 66 deletions

View File

@@ -4,41 +4,87 @@ import { useCalendarCell } from "@react-aria/calendar";
import {
CalendarDate,
getLocalTimeZone,
isSameDay,
isToday,
} from "@internationalized/date";
import { CalendarState } from "@react-stately/calendar";
import { CalendarState, RangeCalendarState } from "@react-stately/calendar";
import { Button } from ":/components/Button";
interface CalendarCellProps {
state: CalendarState;
state: CalendarState | RangeCalendarState;
date: CalendarDate;
}
const isRangeCalendar = (object: any): object is RangeCalendarState => {
return object?.highlightedRange;
};
export const CalendarCell = ({ state, date }: CalendarCellProps) => {
const ref = useRef<HTMLButtonElement>(null);
const { cellProps, buttonProps, isSelected, formattedDate } = useCalendarCell(
{ date },
state,
ref
);
const {
cellProps,
buttonProps,
isSelected,
formattedDate,
isOutsideVisibleRange,
isDisabled,
} = useCalendarCell({ date }, state, ref);
const isSelectionEnd =
isRangeCalendar(state) && isSameDay(date, state?.highlightedRange?.end);
const isSelectionStart =
isRangeCalendar(state) && isSameDay(date, state?.highlightedRange?.start);
const isWithinHighlightedRange =
isRangeCalendar(state) &&
state?.highlightedRange?.start <= date &&
state?.highlightedRange?.end >= date;
return (
<td {...cellProps}>
<Button
size="small"
color={isSelected ? "primary" : "tertiary"}
className={classNames("c__calendar__wrapper__grid__week-row__button", {
"c__calendar__wrapper__grid__week-row__button--selected": isSelected,
"c__calendar__wrapper__grid__week-row__button--today": isToday(
date,
getLocalTimeZone()
),
<div
hidden={isOutsideVisibleRange}
className={classNames({
"c__calendar__wrapper__grid__week-row__background--range--disabled":
isWithinHighlightedRange && isDisabled,
"c__calendar__wrapper__grid__week-row__background--range":
isWithinHighlightedRange,
"c__calendar__wrapper__grid__week-row__background--range--end":
isSelectionEnd,
"c__calendar__wrapper__grid__week-row__background--range--start":
isSelectionStart,
})}
disabled={!!cellProps["aria-disabled"]}
{...buttonProps}
ref={ref}
>
{formattedDate}
</Button>
<Button
size="small"
color={
(
isRangeCalendar(state)
? isSelectionStart || isSelectionEnd
: isSelected
)
? "primary"
: "tertiary"
}
className={classNames(
"c__calendar__wrapper__grid__week-row__button",
{
"c__calendar__wrapper__grid__week-row__button--selected":
isSelected,
"c__calendar__wrapper__grid__week-row__button--today": isToday(
date,
getLocalTimeZone()
),
}
)}
disabled={isDisabled}
{...buttonProps}
ref={ref}
>
{formattedDate}
</Button>
</div>
</td>
);
};

View File

@@ -110,5 +110,6 @@ Here are the custom design tokens defined by the datepicker.
| menu-background-color | Background color of the menu (months/years menus) |
| grid-cell--border-color--today | Border color of the today grid-cell |
| grid-cell--color--today | Value color of the today grid-cell |
| range-selection-background-color | Value color of the selected grid-cells |
See also [Field](?path=/story/components-forms-field-doc--page)

View File

@@ -186,6 +186,16 @@
}
}
&__range {
min-width: px-to-rem(350px);
&__separator {
background-color: var(--c--theme--colors--greyscale-400);
height: px-to-rem(24px);
width: px-to-rem(1px);
margin: auto;
}
}
}
@@ -224,6 +234,7 @@
width: 100%;
padding-top: 0.5rem;
table-layout: fixed;
border-spacing: 0 0.4rem;
&__header-row {
line-height: 3rem;
@@ -236,8 +247,33 @@
}
&__week-row {
td {
padding: 0;
}
&__background {
&--range {
background: var(--c--components--forms-datepicker--range-selection-background-color);
&:not(&--end):not(&--start):not(&--disabled) button {
color: var(--c--components--forms-datepicker--grid-cell--color--today);
}
&--end {
border-top-right-radius: 100%;
border-bottom-right-radius: 100%;
}
&--start {
border-top-left-radius: 100%;
border-bottom-left-radius: 100%;
}
&--disabled {
background: var(--c--components--forms-datepicker--range-selection-background-color--disabled);
}
}
}
&__button {
margin: 0.2rem auto;
&--today {
border: 1px solid var(--c--components--forms-datepicker--grid-cell--border-color--today);
}

View File

@@ -826,23 +826,26 @@ describe("<DatePicker/>", () => {
// Get all the calendar's cells to check their state.
let gridCells = await screen.findAllByRole("gridcell");
// By default, calendar's cells outside the focused month are disabled (month n-1 and n+1).
// By default, calendar's cells outside the focused month are not visible (month n-1 and n+1).
// Cells within the focused month superior to the minDate should be clickable.
// Cells inferior to the minDate should be disabled.
// Cells inferior to the minDate, within the month, should be disabled.
gridCells.forEach((gridCell) => {
const button = within(gridCell).getByRole("button")!;
const value = new Date(
button.getAttribute("aria-label")!.replace("First available date", "")
);
if (
value.getMonth() === minValue.getMonth() &&
value.getDate() < minValue.getDate()
) {
expect(button).toBeDisabled();
} else if (value.getMonth() === minValue.getMonth()) {
expect(button).not.toBeDisabled();
} else {
expect(button).toBeDisabled();
try {
const button = within(gridCell).getByRole("button")!;
const value = new Date(
button.getAttribute("aria-label")!.replace("First available date", "")
);
expect(value.getMonth() === minValue.getMonth());
if (value.getDate() < minValue.getDate()) {
expect(button).toBeDisabled();
} else {
expect(button).not.toBeDisabled();
}
} catch (e: any) {
// Make sure outside grid-cells render any button element, even disabled.
expect(e.message).contains(
'Unable to find an accessible element with the role "button"'
);
}
});
@@ -856,21 +859,24 @@ describe("<DatePicker/>", () => {
gridCells = await screen.findAllByRole("gridcell");
gridCells.forEach((gridCell) => {
const button = within(gridCell).getByRole("button")!;
const buttonLabel = button
.getAttribute("aria-label")
?.replace("Last available date", "");
expect(buttonLabel).toBeDefined();
const value = new Date(buttonLabel!);
if (
value.getMonth() === maxValue.getMonth() &&
value.getDate() > maxValue.getDate()
) {
expect(button).toBeDisabled();
} else if (value.getMonth() === maxValue.getMonth()) {
expect(button).not.toBeDisabled();
} else {
expect(button).toBeDisabled();
try {
const button = within(gridCell).getByRole("button")!;
const buttonLabel = button
.getAttribute("aria-label")
?.replace("Last available date", "");
expect(buttonLabel).toBeDefined();
const value = new Date(buttonLabel!);
expect(value.getMonth() === minValue.getMonth());
if (value.getDate() > maxValue.getDate()) {
expect(button).toBeDisabled();
} else {
expect(button).not.toBeDisabled();
}
} catch (e: any) {
// Make sure outside grid-cells render any button element, even disabled.
expect(e.message).contains(
'Unable to find an accessible element with the role "button"'
);
}
});
});
@@ -983,7 +989,7 @@ describe("<DatePicker/>", () => {
);
});
it("can not select cells outside the focused month", async () => {
it("renders only cell within the focused month", async () => {
const user = userEvent.setup();
const defaultValue = new Date("2023-05-23");
render(
@@ -999,21 +1005,28 @@ describe("<DatePicker/>", () => {
const toggleButton = (await screen.findAllByRole("button"))![1];
await user.click(toggleButton);
// Get all gridcells from the calendar.
// Get all grid-cells from the calendar.
const gridCells = await screen.findAllByRole("gridcell");
// Make sure that gridcells outside of the focused month are disabled.
let visibleValues: Array<Date> = [];
// Make sure that all visible grid-cells are within the focused month.
gridCells.forEach((gridCell) => {
const button = within(gridCell).getByRole("button")!;
const value = new Date(
button.getAttribute("aria-label")!.replace("selected", "")
);
if (defaultValue.getMonth() === value.getMonth()) {
try {
const button = within(gridCell).getByRole("button");
const value = new Date(
button.getAttribute("aria-label")!.replace("selected", "")
);
expect(button).not.toBeDisabled();
} else {
expect(button).toBeDisabled();
expect(defaultValue.getMonth() === value.getMonth());
visibleValues = [value, ...visibleValues];
} catch (e: any) {
// Make sure outside grid-cells render any button element, even disabled.
expect(e.message).contains(
'Unable to find an accessible element with the role "button"'
);
}
});
// Make sure we get as many visible values as days in the focused month.
expect(visibleValues.length).eq(31);
});
it("navigate previous focused month with keyboard", async () => {

View File

@@ -18,6 +18,9 @@ export const tokens = (defaults: DefaultTokens) => ({
"item-font-size": defaults.theme.font.sizes.l,
"background-color": "white",
"menu-background-color": "white",
"range-selection-background-color": defaults.theme.colors["primary-100"],
"range-selection-background-color--disabled":
defaults.theme.colors["greyscale-100"],
"grid-cell--border-color--today": defaults.theme.colors["primary-600"],
"grid-cell--color--today": defaults.theme.colors["primary-600"],
});

View File

@@ -78,6 +78,13 @@
--c--theme--font--sizes--l: 1rem;
--c--theme--font--sizes--m: 0.8125rem;
--c--theme--font--sizes--s: 0.6875rem;
--c--theme--font--weights--thin: 200;
--c--theme--font--weights--light: 300;
--c--theme--font--weights--regular: 400;
--c--theme--font--weights--medium: 500;
--c--theme--font--weights--bold: 600;
--c--theme--font--weights--extrabold: 700;
--c--theme--font--weights--black: 800;
--c--theme--font--families--base: "Roboto Flex Variable", sans-serif;
--c--theme--font--families--accent: "Roboto Flex Variable", sans-serif;
--c--theme--spacings--xl: 4rem;
@@ -122,6 +129,7 @@
--c--components--forms-radio--border-color: var(--c--theme--colors--greyscale-300);
--c--components--forms-radio--accent-color: var(--c--theme--colors--success-700);
--c--components--forms-radio--background-color: white;
--c--components--forms-input--font-weight: var(--c--theme--font--weights--regular);
--c--components--forms-input--font-size: var(--c--theme--font--sizes--l);
--c--components--forms-input--border-radius: 8px;
--c--components--forms-input--border-radius--hover: 2px;
@@ -147,6 +155,7 @@
--c--components--forms-fileuploader--text-size: 0.6875rem;
--c--components--forms-fileuploader--file-text-size: 0.8125rem;
--c--components--forms-fileuploader--file-text-color: var(--c--theme--colors--greyscale-900);
--c--components--forms-fileuploader--file-text-weight: var(--c--theme--font--weights--light);
--c--components--forms-fileuploader--file-border-color: var(--c--theme--colors--greyscale-300);
--c--components--forms-fileuploader--file-border-width: 1px;
--c--components--forms-fileuploader--file-border-radius: 0.5rem;
@@ -173,11 +182,14 @@
--c--components--forms-datepicker--item-font-size: var(--c--theme--font--sizes--l);
--c--components--forms-datepicker--background-color: white;
--c--components--forms-datepicker--menu-background-color: white;
--c--components--forms-datepicker--range-selection-background-color: var(--c--theme--colors--primary-100);
--c--components--forms-datepicker--range-selection-background-color--disabled: var(--c--theme--colors--greyscale-100);
--c--components--forms-datepicker--grid-cell--border-color--today: var(--c--theme--colors--primary-600);
--c--components--forms-datepicker--grid-cell--color--today: var(--c--theme--colors--primary-600);
--c--components--forms-checkbox--background-color--hover: var(--c--theme--colors--greyscale-200);
--c--components--forms-checkbox--background-color: white;
--c--components--forms-checkbox--font-size: var(--c--theme--font--sizes--m);
--c--components--forms-checkbox--font-weight: var(--c--theme--font--weights--medium);
--c--components--forms-checkbox--color: var(--c--theme--colors--greyscale-900);
--c--components--forms-checkbox--border-color: var(--c--theme--colors--greyscale-300);
--c--components--forms-checkbox--border-radius: 2px;
@@ -189,4 +201,5 @@
--c--components--button--small-height: 32px;
--c--components--button--medium-font-size: var(--c--theme--font--sizes--l);
--c--components--button--small-font-size: var(--c--theme--font--sizes--m);
--c--components--button--font-weight: var(--c--theme--font--weights--regular);
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long