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
74 changes: 73 additions & 1 deletion src/app-components/Datepicker/Calendar.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,20 @@
max-width: 380px;
box-sizing: content-box;
}
.datepickerModal {
width: fit-content;
max-width: 100vw;
box-sizing: border-box;
max-height: 90vh;
overflow: hidden;
border-radius: var(--ds-border-radius-md);
padding: 0;
}
.datepickerModalContent {
overflow-y: auto;
max-height: calc(90vh - var(--ds-size-3) * 2);
padding: var(--ds-size-3) var(--ds-size-4);
}
.calendar {
width: 350px;
border-collapse: separate;
Expand Down Expand Up @@ -76,13 +90,19 @@
gap: var(--ds-size-2);
align-items: center;
}
.dropdownCaption {
.datepickerCaption {
display: flex;
margin-bottom: 10px;
margin-left: 10px;
margin-right: 10px;
gap: 4px;
}
.datepickerDropdowns {
display: flex;
gap: 8px;
flex: 1;
min-width: 0;
}

.calendarInputWrapper {
display: flex;
Expand Down Expand Up @@ -112,3 +132,55 @@
.calendarInput input:not(:focus-visible):hover {
box-shadow: none;
}

@media (max-width: 480px) {
.calendarWrapper {
width: 100%;
box-sizing: border-box;
overflow-x: hidden;
}
.monthWrapper {
align-items: stretch;
width: 100%;
}
.calendar {
width: 100%;
table-layout: fixed;
}
.calendarWeekday {
width: auto;
height: 40px;
padding: 0.25rem 0;
}
.calendarDay {
width: auto;
height: 40px;
}
.calendarDayButton {
height: 40px;
width: 100%;
}

.datepickerCaption {
display: grid;
grid-template-columns: 1fr auto;
grid-template-rows: auto auto;
row-gap: 4px;
margin-left: 4px;
margin-right: 4px;
}
.datepickerDropdowns {
grid-column: 1;
grid-row: 1;
}
.datepickerDropdowns > * {
flex: 1;
min-width: 0;
}
.datepickerCloseButton {
display: block;
grid-column: 2;
grid-row: 1;
align-self: center;
}
}
36 changes: 23 additions & 13 deletions src/app-components/Datepicker/DatepickerDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import React, { useEffect, useRef } from 'react';
import React, { createContext, useContext, useEffect, useRef } from 'react';
import type { PropsWithChildren, ReactNode } from 'react';

import { Dialog, Popover } from '@digdir/designsystemet-react';

import styles from 'src/app-components/Datepicker/Calendar.module.css';
import { useIsMobile } from 'src/hooks/useDeviceWidths';

const DatePickerCloseContext = createContext<(() => void) | null>(null);

export function useDatePickerClose() {
return useContext(DatePickerCloseContext);
}

type DatePickerDialogProps = {
id: string;
buttonAriaLabel: string;
Expand All @@ -26,6 +32,7 @@ export function DatePickerDialog({
}: PropsWithChildren<DatePickerDialogProps>) {
const isMobile = useIsMobile();
const modalRef = useRef<HTMLDialogElement>(null);
const closeDatepicker = () => setIsDialogOpen(false);

useEffect(() => {
isDialogOpen && modalRef.current?.showModal();
Expand All @@ -46,17 +53,20 @@ export function DatePickerDialog({
>
{trigger}
</Dialog.Trigger>
<Dialog
ref={modalRef}
role='dialog'
aria-hidden={!isDialogOpen}
closedby='any'
modal
style={{ width: 'fit-content', minWidth: 'fit-content' }}
onClose={() => setIsDialogOpen(false)}
>
{children}
</Dialog>
<DatePickerCloseContext.Provider value={closeDatepicker}>
<Dialog
ref={modalRef}
role='dialog'
aria-hidden={!isDialogOpen}
closedby='any'
modal
closeButton={false}
className={styles.datepickerModal}
onClose={closeDatepicker}
>
<div className={styles.datepickerModalContent}>{children}</div>
</Dialog>
</DatePickerCloseContext.Provider>
</Dialog.TriggerContext>
);
}
Expand All @@ -82,7 +92,7 @@ export function DatePickerDialog({
data-size='lg'
placement='top'
autoFocus={true}
onClose={() => setIsDialogOpen(false)}
onClose={closeDatepicker}
>
{children}
</Popover>
Expand Down
60 changes: 47 additions & 13 deletions src/layout/Datepicker/DatepickerComponent.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,35 @@ const currentMonthNumeric = new Date().toLocaleDateString(navigator.language, {

const { setScreenWidth } = mockMediaQuery(600);

// Workaround since there is no support for dialog element functions yet in jest.
const originalDialogShow = HTMLDialogElement.prototype.show;
const originalDialogShowModal = HTMLDialogElement.prototype.showModal;
const originalDialogClose = HTMLDialogElement.prototype.close;

function mockHTMLDialogElement() {
HTMLDialogElement.prototype.show = jest.fn(function (this: HTMLDialogElement) {
this.open = true;
}) as unknown as typeof HTMLDialogElement.prototype.show;
HTMLDialogElement.prototype.showModal = jest.fn(function (this: HTMLDialogElement) {
this.open = true;
}) as unknown as typeof HTMLDialogElement.prototype.showModal;
HTMLDialogElement.prototype.close = jest.fn(function (this: HTMLDialogElement) {
this.open = false;
}) as unknown as typeof HTMLDialogElement.prototype.close;
}

describe('DatepickerComponent', () => {
beforeEach(() => {
setScreenWidth(1366);
});

afterEach(() => {
jest.restoreAllMocks();
HTMLDialogElement.prototype.show = originalDialogShow;
HTMLDialogElement.prototype.showModal = originalDialogShowModal;
HTMLDialogElement.prototype.close = originalDialogClose;
});

it('should not show calendar initially, and show calendar when clicking calendar button', async () => {
jest.spyOn(console, 'error').mockName('console.error');
await render();
Expand All @@ -68,19 +92,7 @@ describe('DatepickerComponent', () => {
});

it('should not show calendar initially, and show calendar in a dialog when clicking calendar button, and screen size is mobile sized', async () => {
//Workaround since there is no support for dialog element functions yet in jest.
HTMLDialogElement.prototype.show = jest.fn(function mock(this: HTMLDialogElement) {
this.open = true;
});

HTMLDialogElement.prototype.showModal = jest.fn(function mock(this: HTMLDialogElement) {
this.open = true;
});

HTMLDialogElement.prototype.close = jest.fn(function mock(this: HTMLDialogElement) {
this.open = false;
});

mockHTMLDialogElement();
setScreenWidth(400);
await render();

Expand Down Expand Up @@ -220,6 +232,28 @@ describe('DatepickerComponent', () => {
expect(screen.getByRole('option', { name: (currentYear + 1).toString() })).toBeInTheDocument();
});

it('should show close button in mobile screen and close modal when clicked', async () => {
mockHTMLDialogElement();
setScreenWidth(400);
await render();

await userEvent.click(screen.getByRole('button', { name: /Åpne datovelger/i }));
expect(screen.getByRole('dialog')).toBeInTheDocument();

await userEvent.click(screen.getByRole('button', { name: /Lukk/i }));

expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});

it('close button is not shown on desktop at normal zoom', async () => {
await render();

await userEvent.click(screen.getByRole('button', { name: /Åpne datovelger/i }));
expect(screen.getByRole('dialog')).toBeInTheDocument();

expect(screen.queryByRole('button', { name: /Lukk/i })).not.toBeInTheDocument();
});

it('should disable previous month button if previous month is before minDate', async () => {
const user = userEvent.setup();
const today = new Date();
Expand Down
21 changes: 18 additions & 3 deletions src/layout/Datepicker/DropdownCaption.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import { formatMonthDropdown, useDayPicker } from 'react-day-picker';
import type { MonthCaptionProps } from 'react-day-picker';

import { Select } from '@digdir/designsystemet-react';
import { ArrowLeftIcon, ArrowRightIcon } from '@navikt/aksel-icons';
import { ArrowLeftIcon, ArrowRightIcon, XMarkIcon } from '@navikt/aksel-icons';
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';
import { useDatePickerClose } from 'src/app-components/Datepicker/DatepickerDialog';
import { getMonths, getYears } from 'src/app-components/Datepicker/DatePickerHelpers';
import { getDateLib } from 'src/app-components/Datepicker/utils/dateHelpers';
import { useCurrentLanguage } from 'src/features/language/LanguageProvider';
Expand All @@ -24,6 +25,7 @@ export const DropdownCaption = ({ calendarMonth, id, minDate, maxDate }: Dropdow
const { langAsString } = useLanguage();
const languageLocale = useCurrentLanguage();
const dateLib = getDateLib(languageLocale ?? 'nb');
const onClose = useDatePickerClose();

const handleYearChange = (year: string) => {
const newMonth = setYear(startOfMonth(calendarMonth.date), Number(year));
Expand All @@ -48,7 +50,7 @@ export const DropdownCaption = ({ calendarMonth, id, minDate, maxDate }: Dropdow
const months = getMonths(fromDate, toDate, calendarMonth.date);

return (
<div className={styles.dropdownCaption}>
<div className={styles.datepickerCaption}>
<Button
icon={true}
color='second'
Expand All @@ -59,7 +61,7 @@ export const DropdownCaption = ({ calendarMonth, id, minDate, maxDate }: Dropdow
>
<ArrowLeftIcon />
</Button>
<div style={{ display: 'flex', gap: '8px' }}>
<div className={styles.datepickerDropdowns}>
<Select
style={{ width: '150px' }}
id={id}
Expand Down Expand Up @@ -106,6 +108,19 @@ export const DropdownCaption = ({ calendarMonth, id, minDate, maxDate }: Dropdow
>
<ArrowRightIcon />
</Button>
{onClose && (
<div className={styles.datepickerCloseButton}>
<Button
icon={true}
color='second'
variant='tertiary'
aria-label={langAsString('general.close')}
onClick={onClose}
>
<XMarkIcon />
</Button>
</div>
)}
</div>
);
};