Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion packages/gamut/src/DatePicker/DatePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const DatePicker: React.FC<DatePickerProps> = (props) => {
const [gridFocusRequested, setGridFocusRequested] = useState(false);
const [activeRangePart, setActiveRangePart] =
useState<DatePickerRangeContextValue['activeRangePart']>(null);
const [hasError, setHasError] = useState(false);
const inputRef = useRef<HTMLDivElement | null>(null);
const dialogId = useId();
const calendarDialogId = `datepicker-dialog-${dialogId.replace(/:/g, '')}`;
Expand Down Expand Up @@ -106,6 +107,8 @@ export const DatePicker: React.FC<DatePickerProps> = (props) => {
disableDate,
translations,
quickActions: quickActions === null ? [] : resolvedQuickActions,
hasError,
setHasError,
};
return mode === 'range'
? {
Expand Down Expand Up @@ -141,6 +144,8 @@ export const DatePicker: React.FC<DatePickerProps> = (props) => {
disableDate,
props,
activeRangePart,
hasError,
setHasError,
]);

const content =
Expand Down Expand Up @@ -182,7 +187,7 @@ export const DatePicker: React.FC<DatePickerProps> = (props) => {
isOpen={isCalendarOpen}
targetRef={inputRef}
x={-20}
y={-16}
y={isCalendarOpen && hasError ? 0 : -16}
onRequestClose={closeCalendar}
>
<div
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,12 @@ export const CalendarBody: React.FC<CalendarBodyProps> = ({
<thead>
<tr>
{weekdayLabels.map((label, i) => (
<TableHeader abbr={weekdayFullNames[i]} key={label} scope="col">
<TableHeader
abbr={weekdayFullNames[i]}
aria-label={weekdayFullNames[i]}
key={label}
scope="col"
>
{label}
</TableHeader>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { createRef } from 'react';
import { getIsoFirstDayFromLocale } from '../../../utils/locale';
import { CalendarBody } from '../CalendarBody';
import { getMonthGrid } from '../utils/dateGrid';
import { formatDateForAriaLabel } from '../utils/format';
import { formatDateForAriaLabel, getWeekdayNames } from '../utils/format';

const displayDate = new Date(2024, 2, 1);
const focusedDate = new Date(2024, 2, 15);
Expand Down Expand Up @@ -203,14 +203,21 @@ describe('CalendarBody', () => {
await waitFor(() => expect(march15).toHaveFocus());
});

it('renders seven weekday column headers with scope and abbreviations', () => {
it('renders seven weekday column headers with full accessible names', () => {
const { view } = renderView();
const locale = new Intl.Locale('en-US');
const firstWeekday = getIsoFirstDayFromLocale(locale);
const fullNames = getWeekdayNames({
format: 'long',
locale,
firstWeekday,
});

const headers = view.getAllByRole('columnheader');
expect(headers).toHaveLength(7);
headers.forEach((th) => {
headers.forEach((th, i) => {
expect(th).toHaveAttribute('scope', 'col');
expect(th).toHaveAttribute('abbr');
expect(th).toHaveAccessibleName(fullNames[i]);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export function createMockSingleContext(
closeCalendar: jest.fn(),
translations: { ...DEFAULT_DATE_PICKER_TRANSLATIONS },
quickActions: [],
hasError: false,
setHasError: jest.fn(),
selectedDate: new Date(2024, 2, 15),
onSelection: jest.fn(),
...overrides,
Expand All @@ -40,6 +42,8 @@ export function createMockRangeContext(
closeCalendar: jest.fn(),
translations: { ...DEFAULT_DATE_PICKER_TRANSLATIONS },
quickActions: [],
hasError: false,
setHasError: jest.fn(),
startDate: null,
endDate: null,
onRangeSelection: jest.fn(),
Expand Down
9 changes: 9 additions & 0 deletions packages/gamut/src/DatePicker/DatePickerContext/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ interface DatePickerBaseContextValue<Mode extends 'single' | 'range'>
* Discriminator: same meaning as the `mode` prop on `DatePicker` (`"single"` or `"range"`).
*/
mode: Mode;
/**
* Whether there is an error in any input. Used by DatePicker to adjust popover position
* when calendar is open. Only tracks IF error exists, not the message.
*/
hasError: boolean;
/**
* Callback to set whether error exists. Called by DatePickerInput when validation errors occur.
*/
setHasError: (hasError: boolean) => void;
/**
* Resolved `Intl.Locale` for the `locale` prop (or the runtime default). The same object is
* passed to formatters and to APIs such as `getWeekInfo` where available.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,4 +168,63 @@ describe('DatePickerInput', () => {
const hidden = view.container.querySelector('input[type="hidden"]')!;
expect(hidden).toHaveValue('2024-03-15');
});

describe('range disabled-date validation', () => {
// Disables March 20, 2024 - a date that sits inside the range typed below.
const disableDate = (date: Date) =>
date.getFullYear() === 2024 &&
date.getMonth() === 2 &&
date.getDate() === 20;

it('shows the range error and does not commit when a typed range spans a disabled date', async () => {
const user = userEvent.setup();
const onRangeSelection = jest.fn();
const { view } = renderInput({
context: createMockRangeContext({
startDate: new Date(2024, 2, 15),
endDate: null,
activeRangePart: 'end',
disableDate,
onRangeSelection,
}),
rangePart: 'end',
});

view.getByRole('spinbutton', { name: 'month' }).focus();
await user.keyboard('03');
await user.keyboard('25');
await user.keyboard('2024');

view.getByText('This date range contains unavailable dates');
expect(onRangeSelection).not.toHaveBeenCalled();
});

it('commits and clears the error when the typed range avoids disabled dates', async () => {
const user = userEvent.setup();
const onRangeSelection = jest.fn();
const { view } = renderInput({
context: createMockRangeContext({
startDate: new Date(2024, 2, 15),
endDate: null,
activeRangePart: 'end',
disableDate,
onRangeSelection,
}),
rangePart: 'end',
});

view.getByRole('spinbutton', { name: 'month' }).focus();
await user.keyboard('03');
await user.keyboard('18');
await user.keyboard('2024');

expect(
view.queryByText('This date range contains unavailable dates')
).toBeNull();
expect(onRangeSelection).toHaveBeenCalledWith(
new Date(2024, 2, 15),
new Date(2024, 2, 18)
);
});
});
});
Loading
Loading