Skip to content

Commit

Permalink
feat: add auto focus on field error
Browse files Browse the repository at this point in the history
  • Loading branch information
walt-it committed Nov 28, 2024
1 parent b31dbc1 commit 4edf258
Show file tree
Hide file tree
Showing 10 changed files with 105 additions and 47 deletions.
12 changes: 9 additions & 3 deletions src/shared/components/ui/Calendar/Calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { tv } from "tailwind-variants";

import { useFieldController } from "~/hooks";
import { WithHookForm, WithoutHookForm } from "~/shared";
import { currentTimezone } from "~/utils";
import { currentTimezone, findFirstEnabledDate } from "~/utils";
import { CalendarHeader } from "./CalendarHeader";
import { CalendarTable } from "./CalendarTable";

Expand Down Expand Up @@ -50,13 +50,19 @@ export const Calendar = <T extends DateValue, U extends FieldValues>({
);
}}
onFocusChange={controller?.field.onBlur}
// TODO: ref={field.ref} Need to pass this down to CalendarTable to focus on error
{...props}
aria-labelledby={label}
className={classNames.container}
>
<CalendarHeader />
<CalendarTable />
<CalendarTable
ref={(el) => {
const firstEnabledDate = findFirstEnabledDate(el);
if (firstEnabledDate) {
return controller?.field.ref(firstEnabledDate);
}
}}
/>
</AriaCalendar>
{errorMessage && (
<Text className={classNames.error()} slot='errorMessage'>
Expand Down
72 changes: 41 additions & 31 deletions src/shared/components/ui/Calendar/CalendarTable.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { forwardRef } from "react";
import {
CalendarCell,
CalendarGrid,
Expand All @@ -16,7 +17,7 @@ const calendarTable = tv({
});

const calendarCell = tv({
base: "mx-auto my-0.5 flex aspect-square w-8 items-center justify-center rounded-lg text-sm font-semibold text-gray-600 sm:w-11",
base: "mx-auto my-0.5 flex aspect-square w-8 items-center justify-center rounded-lg text-sm font-semibold text-gray-600 focus:outline-none focus:ring-2 focus:ring-[#005FCC] sm:w-11",
variants: {
state: {
default: "",
Expand All @@ -35,33 +36,42 @@ const calendarCell = tv({

const { root, dayOfWeek } = calendarTable();

export const CalendarTable = (props: CalendarGridProps) => {
return (
<CalendarGrid weekdayStyle='short' className={root()} {...props}>
<CalendarGridHeader>
{(day) => (
<CalendarHeaderCell className={dayOfWeek()}>{day}</CalendarHeaderCell>
)}
</CalendarGridHeader>
<CalendarGridBody>
{(date) => (
<CalendarCell
className={({ isDisabled, isUnavailable, isSelected }) =>
calendarCell({
state: isSelected
? "selected"
: isDisabled
? "disabled"
: isUnavailable
? "unavailable"
: "default",
hoverable: !isUnavailable && !isDisabled && !isSelected,
})
}
date={date}
/>
)}
</CalendarGridBody>
</CalendarGrid>
);
};
export const CalendarTable = forwardRef<HTMLTableElement, CalendarGridProps>(
(props, ref) => {
return (
<CalendarGrid
ref={ref}
weekdayStyle='short'
className={root()}
{...props}
>
<CalendarGridHeader>
{(day) => (
<CalendarHeaderCell className={dayOfWeek()}>
{day}
</CalendarHeaderCell>
)}
</CalendarGridHeader>
<CalendarGridBody>
{(date) => (
<CalendarCell
className={({ isDisabled, isUnavailable, isSelected }) =>
calendarCell({
state: isSelected
? "selected"
: isDisabled
? "disabled"
: isUnavailable
? "unavailable"
: "default",
hoverable: !isUnavailable && !isDisabled && !isSelected,
})
}
date={date}
/>
)}
</CalendarGridBody>
</CalendarGrid>
);
},
);
8 changes: 6 additions & 2 deletions src/shared/components/ui/DateField/DateField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { tv } from "tailwind-variants";

import { useFieldController } from "~/hooks";
import { WithHookForm, WithoutHookForm } from "~/shared";
import { currentTimezone } from "~/utils";
import { currentTimezone, isFirstChild } from "~/utils";

const classNames = tv({
slots: {
Expand Down Expand Up @@ -62,7 +62,11 @@ export const DateField = <T extends DateValue, U extends FieldValues>({
<Label>{label}</Label>
<DateInput className={classNames.dateInput}>
{(segment) => (
<DateSegment className={classNames.dateSegment} segment={segment} />
<DateSegment
ref={(el) => isFirstChild(el) && controller?.field.ref(el)}
className={classNames.dateSegment}
segment={segment}
/>
)}
</DateInput>
{errorMessage && (
Expand Down
12 changes: 7 additions & 5 deletions src/shared/components/ui/DatePicker/DatePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { tv } from "tailwind-variants";

import { useFieldController } from "~/hooks";
import { WithHookForm, WithoutHookForm } from "~/shared";
import { currentTimezone } from "~/utils";
import { currentTimezone, isFirstChild } from "~/utils";
import { CalendarHeader } from "../Calendar/CalendarHeader";
import { CalendarTable } from "../Calendar/CalendarTable";

Expand Down Expand Up @@ -73,12 +73,14 @@ export const DatePicker = <T extends DateValue, U extends FieldValues>({
<Group className={classNames.dateGroup}>
<DateInput className={classNames.dateInput} slot={slot}>
{(segment) => (
<DateSegment className={classNames.dateSegment} segment={segment} />
<DateSegment
ref={(el) => isFirstChild(el) && controller?.field.ref(el)}
className={classNames.dateSegment}
segment={segment}
/>
)}
</DateInput>
<Button ref={controller?.field.ref} className={classNames.trigger}>
🗓️
</Button>
<Button className={classNames.trigger}></Button>
</Group>
{errorMessage && (
<Text className={classNames.error()} slot='errorMessage'>
Expand Down
7 changes: 6 additions & 1 deletion src/shared/components/ui/DateRangePicker/DateRangePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { tv } from "tailwind-variants";

import {
currentTimezone,
isFirstChild,
useFieldController,
WithHookForm,
WithoutHookForm,
Expand Down Expand Up @@ -77,7 +78,11 @@ export const DateRangePicker = <T extends DateValue, U extends FieldValues>({
<Group className={classNames.dateGroup}>
<DateInput slot='start' className={classNames.dateInput}>
{(segment) => (
<DateSegment className={classNames.dateSegment} segment={segment} />
<DateSegment
ref={(el) => isFirstChild(el) && controller?.field.ref(el)}
className={classNames.dateSegment}
segment={segment}
/>
)}
</DateInput>
<span aria-hidden='true'></span>
Expand Down
11 changes: 9 additions & 2 deletions src/shared/components/ui/RangeCalendar/RangeCalendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { tv } from "tailwind-variants";

import { useFieldController } from "~/hooks";
import { WithHookForm, WithoutHookForm } from "~/shared";
import { currentTimezone } from "~/utils";
import { currentTimezone, findFirstEnabledDate } from "~/utils";
import { CalendarHeader } from "../Calendar/CalendarHeader";
import { CalendarTable } from "../Calendar/CalendarTable";

Expand Down Expand Up @@ -65,7 +65,14 @@ export const RangeCalendar = <T extends DateValue, U extends FieldValues>({
>
<CalendarHeader />
<div className={classNames.calendars()}>
<CalendarTable />
<CalendarTable
ref={(el) => {
const firstEnabledDate = findFirstEnabledDate(el);
if (firstEnabledDate) {
return controller?.field.ref(firstEnabledDate);
}
}}
/>
<CalendarTable offset={{ months: 1 }} />
</div>
</AriaRangeCalendar>
Expand Down
14 changes: 11 additions & 3 deletions src/shared/components/ui/TimeField/TimeField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ import {
import { FieldValues } from "react-hook-form";
import { tv } from "tailwind-variants";

import { useFieldController, WithHookForm, WithoutHookForm } from "~/shared";
import {
isFirstChild,
useFieldController,
WithHookForm,
WithoutHookForm,
} from "~/shared";

const classNames = tv({
slots: {
Expand Down Expand Up @@ -52,13 +57,16 @@ export const TimeField = <T extends TimeValue, U extends FieldValues>({
controller?.field.onChange(newTime.toString());
}}
onBlur={controller?.field.onBlur}
ref={controller?.field.ref}
{...props}
>
<Label>{label}</Label>
<DateInput className={classNames.dateInput}>
{(segment) => (
<DateSegment segment={segment} className={classNames.dateSegment} />
<DateSegment
ref={(el) => isFirstChild(el) && controller?.field.ref(el)}
className={classNames.dateSegment}
segment={segment}
/>
)}
</DateInput>
{errorMessage && (
Expand Down
8 changes: 8 additions & 0 deletions src/shared/utils/findFirstEnabledDate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const findFirstEnabledDate = (
table: HTMLTableElement | null,
): Element | null => {
return (
table?.querySelector<HTMLTableCellElement>("td:not([aria-disabled])")
?.children[0] ?? null
);
};
2 changes: 2 additions & 0 deletions src/shared/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export * from "./asyncTimeout";
export * from "./dateWithoutTimezone";
export * from "./forwardRef";
export * from "./findFirstEnabledDate";
export * from "./isDateISOString";
export * from "./isFirstChild";
export * from "./tw";
export * from "./validations";
6 changes: 6 additions & 0 deletions src/shared/utils/isFirstChild.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const isFirstChild = (el: Element | null) => {
if (!el) return false;
return (
el.parentElement && Array.from(el.parentElement.children).indexOf(el) === 0
);
};

0 comments on commit 4edf258

Please sign in to comment.