Bladeren bron

SONAR-20721 Improve day picker accessiblity

tags/10.3.0.82913
Mathieu Suen 7 maanden geleden
bovenliggende
commit
f05332a75d

+ 22
- 2
server/sonar-web/design-system/src/components/input/DatePickerCustomCalendarNavigation.tsx Bestand weergeven

@@ -48,6 +48,16 @@ export function CustomCalendarNavigation(props: CaptionProps) {

const intl = useIntl();

const formatChevronLabel = (date?: Date) => {
if (date === undefined) {
return intl.formatMessage({ id: 'disabled_' });
}
return `${intl.formatDate(date, { month: 'long', format: 'M' })} ${intl.formatDate(date, {
year: 'numeric',
format: 'y',
})}`;
};

const baseDate = startOfMonth(displayMonth); // reference date

const months = range(MONTHS_IN_A_YEAR).map((month) => {
@@ -74,8 +84,12 @@ export function CustomCalendarNavigation(props: CaptionProps) {
<nav className="sw-flex sw-items-center sw-justify-between sw-py-1">
<InteractiveIcon
Icon={ChevronLeftIcon}
aria-label={intl.formatMessage({ id: 'previous_' })}
aria-label={intl.formatMessage(
{ id: 'previous_month_x' },
{ month: formatChevronLabel(previousMonth) },
)}
className="sw-mr-2"
disabled={previousMonth === undefined}
onClick={() => {
if (previousMonth) {
goToMonth(previousMonth);
@@ -116,8 +130,14 @@ export function CustomCalendarNavigation(props: CaptionProps) {

<InteractiveIcon
Icon={ChevronRightIcon}
aria-label={intl.formatMessage({ id: 'next_' })}
aria-label={intl.formatMessage(
{ id: 'next_month_x' },
{
month: formatChevronLabel(nextMonth),
},
)}
className="sw-ml-2"
disabled={nextMonth === undefined}
onClick={() => {
if (nextMonth) {
goToMonth(nextMonth);

+ 24
- 4
server/sonar-web/design-system/src/components/input/__tests__/DatePicker-test.tsx Bestand weergeven

@@ -18,9 +18,10 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

import { screen, within } from '@testing-library/react';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { getMonth, getYear, parseISO } from 'date-fns';
import { byRole } from '../../../../../src/main/js/helpers/testSelector';
import { renderWithContext } from '../../../helpers/testUtils';
import { DatePicker } from '../DatePicker';

@@ -40,7 +41,7 @@ it('behaves correctly', async () => {
const nav = screen.getByRole('navigation');
expect(nav).toBeInTheDocument();

await user.click(within(nav).getByRole('button', { name: 'previous_' }));
await user.click(byRole('navigation').byRole('button', { name: 'previous_month_x' }).get());
await user.click(screen.getByText('7'));

expect(onChange).toHaveBeenCalled();
@@ -54,7 +55,7 @@ it('behaves correctly', async () => {
* Then check that onChange was correctly called with a date in the following month
*/
await user.click(screen.getByRole('textbox'));
await user.click(screen.getByRole('button', { name: 'next_' }));
await user.click(screen.getByRole('button', { name: 'next_month_x' }));
await user.click(screen.getByText('12'));

expect(onChange).toHaveBeenCalled();
@@ -84,6 +85,25 @@ it('behaves correctly', async () => {
expect(getYear(newDate3)).toBe(2019);
});

it('should disable next navigation when not in the accepted range', async () => {
const user = userEvent.setup();

const currentDate = parseISO('2022-11-13');

renderDatePicker({
currentMonth: currentDate,
maxDate: parseISO('2022-12-30'),
value: currentDate,
// eslint-disable-next-line jest/no-conditional-in-test
valueFormatter: (date?: Date) => (date ? 'formatted date' : 'no date'),
});

await user.click(screen.getByRole('textbox'));
await user.click(screen.getByRole('button', { name: 'next_month_x' }));

expect(screen.getByRole('button', { name: 'next_month_x' })).toBeDisabled();
});

it('should clear the value', async () => {
const user = userEvent.setup();

@@ -136,6 +156,6 @@ function renderDatePicker(overrides: Partial<DatePicker['props']> = {}) {
placeholder="placeholder"
valueFormatter={defaultFormatter}
{...overrides}
/>
/>,
);
}

+ 13
- 13
server/sonar-web/design-system/src/components/input/__tests__/DateRangePicker-test.tsx Bestand weergeven

@@ -17,9 +17,10 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { screen, within } from '@testing-library/react';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { formatISO, parseISO } from 'date-fns';
import { byRole } from '../../../../../src/main/js/helpers/testSelector';
import { IntlWrapper, render } from '../../../helpers/testUtils';
import { DateRangePicker } from '../DateRangePicker';

@@ -33,6 +34,7 @@ afterEach(() => {
});

it('behaves correctly', async () => {
const nav = byRole('navigation');
// Remove delay to play nice with fake timers
const user = userEvent.setup({ delay: null });

@@ -41,13 +43,12 @@ it('behaves correctly', async () => {

await user.click(screen.getByRole('textbox', { name: 'from' }));

const fromDateNav = screen.getByRole('navigation');
expect(fromDateNav).toBeInTheDocument();
expect(nav.get()).toBeInTheDocument();

await user.click(within(fromDateNav).getByRole('button', { name: 'previous' }));
await user.click(nav.byRole('button', { name: 'previous_month_x' }).get());
await user.click(screen.getByText('7'));

expect(screen.queryByRole('navigation')).not.toBeInTheDocument();
expect(nav.query()).not.toBeInTheDocument();

expect(onChange).toHaveBeenCalled();
const { from } = onChange.mock.calls[0][0]; // first argument
@@ -58,15 +59,14 @@ it('behaves correctly', async () => {

jest.runAllTimers();

const toDateNav = await screen.findByRole('navigation');
const previousButton = within(toDateNav).getByRole('button', { name: 'previous' });
const nextButton = within(toDateNav).getByRole('button', { name: 'next' });
await user.click(previousButton);
await user.click(nextButton);
await user.click(previousButton);
const previousButton = nav.byRole('button', { name: 'previous_month_x' });
const nextButton = nav.byRole('button', { name: 'next_month_x' });
await user.click(previousButton.get());
await user.click(nextButton.get());
await user.click(previousButton.get());
await user.click(screen.getByText('12'));

expect(screen.queryByRole('navigation')).not.toBeInTheDocument();
expect(nav.query()).not.toBeInTheDocument();

expect(onChange).toHaveBeenCalled();
const { to } = onChange.mock.calls[0][0]; // first argument
@@ -88,6 +88,6 @@ function renderDateRangePicker(overrides: Partial<DateRangePicker['props']> = {}
valueFormatter={defaultFormatter}
{...overrides}
/>
</IntlWrapper>
</IntlWrapper>,
);
}

+ 7
- 4
server/sonar-web/design-system/src/helpers/testUtils.tsx Bestand weergeven

@@ -30,7 +30,7 @@ import { MemoryRouter, Route, Routes } from 'react-router-dom';
export function render(
ui: React.ReactElement,
options?: RenderOptions,
userEventOptions?: UserEventsOptions
userEventOptions?: UserEventsOptions,
) {
return { ...rtlRender(ui, options), user: userEvent.setup(userEventOptions) };
}
@@ -42,7 +42,7 @@ type RenderContextOptions = Omit<RenderOptions, 'wrapper'> & {

export function renderWithContext(
ui: React.ReactElement,
{ userEventOptions, ...options }: RenderContextOptions = {}
{ userEventOptions, ...options }: RenderContextOptions = {},
) {
return render(ui, { ...options, wrapper: getContextWrapper() }, userEventOptions);
}
@@ -53,7 +53,7 @@ interface RenderRouterOptions {

export function renderWithRouter(
ui: React.ReactElement,
options: RenderContextOptions & RenderRouterOptions = {}
options: RenderContextOptions & RenderRouterOptions = {},
) {
const { additionalRoutes, userEventOptions, ...renderOptions } = options;

@@ -128,7 +128,10 @@ export function IntlWrapper({
messages={messages}
onError={(e) => {
// ignore missing translations, there are none!
if (e.code !== ReactIntlErrorCode.MISSING_TRANSLATION) {
if (
e.code !== ReactIntlErrorCode.MISSING_TRANSLATION &&
e.code !== ReactIntlErrorCode.UNSUPPORTED_FORMATTER
) {
// eslint-disable-next-line no-console
console.error(e);
}

+ 3
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties Bestand weergeven

@@ -73,6 +73,7 @@ descending=Descending
description=Description
directories=Directories
directory=Directory
disabled_=disabled
dismiss=Dismiss
dismiss_permanently=Dismiss permanently
display=Display
@@ -146,6 +147,7 @@ navigation=Navigation
never=Never
new=New
next=Next
next_month_x=next month {month}
new_name=New name
next_=next
none=None
@@ -166,6 +168,7 @@ path=Path
permalink=Permanent Link
plugin=Plugin
previous_=previous
previous_month_x=previous month {month}
project=Project
project_x=Project: {0}
projects=Projects

Laden…
Annuleren
Opslaan