Skip to content
Merged
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
51 changes: 51 additions & 0 deletions src/layout/Datepicker/DatepickerComponent.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -198,4 +198,55 @@ describe('DatepickerComponent', () => {
const inputField = screen.getByRole('textbox');
expect(inputField).not.toHaveAttribute('aria-describedby');
});

it('should not display years outside of min and max date range in year dropdown', async () => {
const user = userEvent.setup();
const currentYear = new Date().getFullYear();
const minDate = new Date(Date.UTC(currentYear - 1, 0, 1));
const maxDate = new Date(Date.UTC(currentYear + 1, 11, 31));
await render({
component: {
minDate: minDate.toISOString(),
maxDate: maxDate.toISOString(),
},
});
await user.click(screen.getByRole('button', { name: /Åpne datovelger/i }));
await user.click(screen.getByRole('combobox', { name: /Velg år/i }));

expect(screen.queryByRole('option', { name: (currentYear - 2).toString() })).not.toBeInTheDocument();
expect(screen.queryByRole('option', { name: (currentYear + 2).toString() })).not.toBeInTheDocument();
expect(screen.getByRole('option', { name: currentYear.toString() })).toBeInTheDocument();
expect(screen.getByRole('option', { name: (currentYear - 1).toString() })).toBeInTheDocument();
expect(screen.getByRole('option', { name: (currentYear + 1).toString() })).toBeInTheDocument();
});

it('should disable previous month button if previous month is before minDate', async () => {
const user = userEvent.setup();
const today = new Date();
const minDate = new Date(today.getFullYear(), today.getMonth(), 1);

await render({
component: {
minDate: minDate.toISOString(),
},
});
await user.click(screen.getByRole('button', { name: /Åpne datovelger/i }));
const prevButton = screen.getByRole('button', { name: /forrige måned/i });
expect(prevButton).toBeDisabled();
});

it('should disable next month button if next month is after maxDate', async () => {
const user = userEvent.setup();
const today = new Date();
const maxDate = new Date(today.getFullYear(), today.getMonth() + 1, 0);

await render({
component: {
maxDate: maxDate.toISOString(),
},
});
await user.click(screen.getByRole('button', { name: /Åpne datovelger/i }));
const nextButton = screen.getByRole('button', { name: /neste måned/i });
expect(nextButton).toBeDisabled();
});
});
8 changes: 7 additions & 1 deletion src/layout/Datepicker/DatepickerComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,13 @@
locale={languageLocale}
minDate={calculatedMinDate}
maxDate={calculatedMaxDate}
DropdownCaption={DropdownCaption}
DropdownCaption={(props) => (

Check warning on line 77 in src/layout/Datepicker/DatepickerComponent.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Move this component definition out of the parent component and pass data as props.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-frontend-react&issues=AZ0sbRj9OtCIrmXMLEpx&open=AZ0sbRj9OtCIrmXMLEpx&pullRequest=4084
<DropdownCaption
{...props}
minDate={calculatedMinDate}
maxDate={calculatedMaxDate}
/>
)}
buttonAriaLabel={langAsString('date_picker.aria_label_icon')}
calendarIconTitle={langAsString('date_picker.aria_label_icon')}
autoComplete={autocomplete}
Expand Down
28 changes: 20 additions & 8 deletions src/layout/Datepicker/DropdownCaption.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { MonthCaptionProps } from 'react-day-picker';

import { Select } from '@digdir/designsystemet-react';
import { ArrowLeftIcon, ArrowRightIcon } from '@navikt/aksel-icons';
import { addYears, max, min, setMonth, setYear, startOfMonth, subYears } from 'date-fns';
import { addYears, setMonth, setYear, startOfMonth, subYears } from 'date-fns';

import { Button } from 'src/app-components/Button/Button';
import styles from 'src/app-components/Datepicker/Calendar.module.css';
Expand All @@ -14,24 +14,36 @@ import { useCurrentLanguage } from 'src/features/language/LanguageProvider';
import { useLanguage } from 'src/features/language/useLanguage';
import comboboxClasses from 'src/styles/combobox.module.css';

export const DropdownCaption = ({ calendarMonth, id }: MonthCaptionProps) => {
type DropdownCaptionProps = MonthCaptionProps & {
minDate?: Date;
maxDate?: Date;
};

export const DropdownCaption = ({ calendarMonth, id, minDate, maxDate }: DropdownCaptionProps) => {
const { goToMonth, nextMonth, previousMonth } = useDayPicker();
const { langAsString } = useLanguage();
const languageLocale = useCurrentLanguage();
const dateLib = getDateLib(languageLocale ?? 'nb');

const handleYearChange = (year: string) => {
const newMonth = setYear(startOfMonth(calendarMonth.date), Number(year));
goToMonth(startOfMonth(min([max([newMonth])])));
if (minDate && newMonth < startOfMonth(minDate)) {
goToMonth(startOfMonth(minDate));
} else if (maxDate && newMonth > startOfMonth(maxDate)) {
goToMonth(startOfMonth(maxDate));
} else {
goToMonth(newMonth);
}
};

const handleMonthChange = (month: string) => {
goToMonth(setMonth(startOfMonth(calendarMonth.date), Number(month)));
};
const fromDate = minDate ?? subYears(calendarMonth.date, 100);
const toDate = maxDate ?? addYears(calendarMonth.date, 100);

const fromDate = subYears(calendarMonth.date, 100);
const toDate = addYears(calendarMonth.date, 100);

const isPrevMonthDisabled = !previousMonth || (minDate && startOfMonth(previousMonth) < startOfMonth(minDate));
const isNextMonthDisabled = !nextMonth || (maxDate && startOfMonth(nextMonth) > startOfMonth(maxDate));
const years = getYears(fromDate, toDate, calendarMonth.date.getFullYear()).reverse();
const months = getMonths(fromDate, toDate, calendarMonth.date);

Expand All @@ -42,7 +54,7 @@ export const DropdownCaption = ({ calendarMonth, id }: MonthCaptionProps) => {
color='second'
variant='tertiary'
aria-label={langAsString('date_picker.aria_label_left_arrow')}
disabled={!previousMonth}
disabled={isPrevMonthDisabled}
onClick={() => previousMonth && goToMonth(previousMonth)}
>
<ArrowLeftIcon />
Expand Down Expand Up @@ -89,7 +101,7 @@ export const DropdownCaption = ({ calendarMonth, id }: MonthCaptionProps) => {
color='second'
variant='tertiary'
aria-label={langAsString('date_picker.aria_label_right_arrow')}
disabled={!nextMonth}
disabled={isNextMonthDisabled}
onClick={() => nextMonth && goToMonth(nextMonth)}
>
<ArrowRightIcon />
Expand Down
1 change: 1 addition & 0 deletions test/e2e/integration/component-library/date-picker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ describe('Datepicker component', () => {

cy.get(maxDatePicker).type('30.07.2025');
cy.get(maxDatePicker).blur();
cy.waitUntilSaved();

cy.get(datePicker).contains('button', 'Åpne datovelger').click();

Expand Down
Loading