Skip to content

Commit

Permalink
[DatePickers]: improve a11y focus flow behavior. Now date picker body…
Browse files Browse the repository at this point in the history
… receive focus on open and return it back on input after close.
  • Loading branch information
AlekseyManetov committed Jul 24, 2024
1 parent 68d6d1d commit 3bfc1f0
Show file tree
Hide file tree
Showing 22 changed files with 372 additions and 256 deletions.
9 changes: 0 additions & 9 deletions .run/Template Jest.run.xml

This file was deleted.

4 changes: 2 additions & 2 deletions app/src/demo/tables/editableTable/columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export function getColumns(columnsProps: ColumnsProps) {
renderCell: (props) => (
<DataTableCell
{ ...props.rowLens.prop('startDate').toProps() }
renderEditor={ (props) => <DatePicker format="MMM D, YYYY" placeholder="" { ...props } /> }
renderEditor={ (props) => <DatePicker { ...props } /> }
{ ...props }
/>
),
Expand All @@ -111,7 +111,7 @@ export function getColumns(columnsProps: ColumnsProps) {
renderCell: (props) => (
<DataTableCell
{ ...props.rowLens.prop('dueDate').toProps() }
renderEditor={ (props) => <DatePicker format="MMM D, YYYY" placeholder="" { ...props } /> }
renderEditor={ (props) => <DatePicker { ...props } /> }
{ ...props }
/>
),
Expand Down
8 changes: 7 additions & 1 deletion app/src/docs/_examples/tables/EditableTable.example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,14 @@ export default function EditableTableExample() {
{
key: 'dueDate',
caption: 'Due date',
renderCell: (props) => <DataTableCell { ...props.rowLens.prop('dueDate').toProps() } renderEditor={ (props) => <DatePicker { ...props } /> } { ...props } />,
width: 150,
renderCell: (props) => (
<DataTableCell
{ ...props.rowLens.prop('dueDate').toProps() }
renderEditor={ (props) => <DatePicker { ...props } /> }
{ ...props }
/>
),
},
{
key: 'priority',
Expand Down
23 changes: 6 additions & 17 deletions app/src/docs/_examples/tables/FiltersPanelBasic.example.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,6 @@
import React, { useMemo } from 'react';
import { uuiDayjs } from '../../../helpers';
import {
defaultPredicates,
rangeDatePickerPresets,
FiltersPanel,
DataTable,
Panel,
FlexRow,
Text,
Switch,
Badge,
BadgeProps,
} from '@epam/uui';
import { defaultPredicates, rangeDatePickerPresets, FiltersPanel, DataTable, Panel, FlexRow, Text, Switch, Badge, BadgeProps } from '@epam/uui';
import { DataColumnProps, getSeparatedValue, LazyDataSource, TableFiltersConfig, useLazyDataSource, useTableState, useUuiContext } from '@epam/uui-core';
import { Person } from '@epam/uui-docs';

Expand Down Expand Up @@ -82,7 +71,7 @@ const personColumns: DataColumnProps<Person, number>[] = [
];

export default function FiltersPanelExample() {
const { api } = useUuiContext();
const svc = useUuiContext();

const filtersConfig = useMemo<TableFiltersConfig<Person>[]>(
() => [
Expand All @@ -92,7 +81,7 @@ export default function FiltersPanelExample() {
title: 'Profile status',
type: 'multiPicker',
isAlwaysVisible: true,
dataSource: new LazyDataSource({ api: api.demo.statuses }),
dataSource: new LazyDataSource({ api: svc.api.demo.statuses }),
predicates: defaultPredicates.multiPicker,
showSearch: false,
maxCount: 3,
Expand All @@ -103,7 +92,7 @@ export default function FiltersPanelExample() {
title: 'Title',
type: 'multiPicker',
togglerWidth: 400,
dataSource: new LazyDataSource({ api: api.demo.jobTitles }),
dataSource: new LazyDataSource({ api: svc.api.demo.jobTitles }),
},
{
field: 'salary',
Expand Down Expand Up @@ -159,7 +148,7 @@ export default function FiltersPanelExample() {
},
},
],
[api.demo.jobTitles, api.demo.statuses],
[],
);

const { tableState, setTableState } = useTableState({
Expand All @@ -169,7 +158,7 @@ export default function FiltersPanelExample() {

const dataSource = useLazyDataSource<Person, number, Person>(
{
api: api.demo.persons,
api: svc.api.demo.persons,
backgroundReload: true,
},
[],
Expand Down
2 changes: 2 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

**What's New**
* Icons pack updated
* [DatePicker][RangeDatePicker]: improve a11y focus flow behavior. Now date picker body receive focus on open and return it back on input after close.


**What's Fixed**
* [PickerInput]: fixed '+N' toggler tag tooltip content with custom `getName` callback
Expand Down
3 changes: 1 addition & 2 deletions uui-components/src/inputs/DatePicker/Calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ export function Calendar<TSelection>(props: CalendarProps<TSelection>) {
return (
<div
className={ uuiDaySelection.dayCell }
tabIndex={ 0 }
key={ `day-${props.month.valueOf()}-${day && day.valueOf()}-${index}` }
>
{props.renderDay ? (
Expand Down Expand Up @@ -95,7 +94,7 @@ export function Calendar<TSelection>(props: CalendarProps<TSelection>) {
return (
<div
className={ uuiDaySelection.dayCell }
tabIndex={ 0 }
tabIndex={ -1 }
key={ `day-${props.month.valueOf()}-${index}` }
/>
);
Expand Down
11 changes: 9 additions & 2 deletions uui-components/src/inputs/DatePicker/Day.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,18 @@ export function Day(props: DayProps): JSX.Element {
const dayNumber = props.renderDayNumber
? props.renderDayNumber(props.value)
: props.value.format('D');

const selectDay = () => {
isPassedFilter && props.onValueChange(props.value);
};

return (
<div
onClick={ isPassedFilter ? () => props.onValueChange(props.value) : undefined }
onClick={ selectDay }
onKeyDown={ (e) => e.key === 'Enter' && selectDay() }
tabIndex={ 0 }
className={ cx([
isPassedFilter && uuiDaySelection.clickable,
isPassedFilter && uuiDaySelection.clickableDay,
isPassedFilter && uuiMarkers.clickable,
isCurrent && uuiDaySelection.currentDay,
props.isSelected && uuiDaySelection.selectedDay,
Expand Down
2 changes: 2 additions & 0 deletions uui-components/src/inputs/DatePicker/MonthSelection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ export function MonthSelection(props: MonthSelectionProps): JSX.Element {
return (
<div
key={ month }
tabIndex={ 0 }
className={ cx(isSelected && uuiMonthSelection.currentMonth, uuiMonthSelection.month) }
onClick={ () => props.onValueChange(props.value.month(index)) }
onKeyDown={ (e) => { e.key === 'Enter' && props.onValueChange(props.value.month(index)); } }
>
{month}
</div>
Expand Down
2 changes: 2 additions & 0 deletions uui-components/src/inputs/DatePicker/YearSelection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@ export function YearSelection(props: YearSelectionProps) {
{yearRow.map((year) => (
<div
key={ year }
tabIndex={ 0 }
className={ cx(year === props.selectedDate.year() && uuiYearSelection.currentYear, uuiYearSelection.year) }
onClick={ () => props.onValueChange(props.value.year(year)) }
onKeyDown={ (e) => { e.key === 'Enter' && props.onValueChange(props.value.year(year)); } }
>
{year}
</div>
Expand Down
2 changes: 1 addition & 1 deletion uui-components/src/inputs/DatePicker/calendarConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const uuiDaySelection = {
selectedDay: 'uui-calendar-selected-day',
filteredDay: 'uui-calendar-filtered-day',
previousMonthEmptyDay: 'uui-calendar-previous-month-empty-day',
clickable: 'uui-calendar-clickable-day',
clickableDay: 'uui-calendar-clickable-day',
dayWrapper: 'uui-calendar-day-wrapper',
holiday: 'uui-calendar-day-holiday',
} as const;
3 changes: 2 additions & 1 deletion uui-core/src/types/components/TextInput.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ICanBeReadonly, IClickable, IDisableable, IEditable, IHasCX, IHasIcon, IHasPlaceholder, IAnalyticableOnChange,
IHasRawProps, ICanFocus, IHasTabIndex, IDropdownToggler,
} from '../props';
import * as React from 'react';

export interface TextInputCoreProps
extends IHasCX,
Expand All @@ -20,7 +21,7 @@ export interface TextInputCoreProps
/** Enables accept (check) icon, and fires when the icon is clicked */
onAccept?(): void;
/** keydown event handler to put on the HTML input element */
onKeyDown?(e?: any): void;
onKeyDown?(e?: React.KeyboardEvent<HTMLInputElement>): void;
/** Put focus on the element, when component is mounted */
autoFocus?: boolean;
/** Standard 'type' attribute to put on the HTML input element (e.g. 'password') */
Expand Down
3 changes: 2 additions & 1 deletion uui/components/datePickers/Calendar.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,9 @@
}

:global(.uui-calendar-clickable-day) {
&:hover {
&:hover, &:focus {
&:not(:global(.uui-calendar-filtered-day)) {
outline: none;
cursor: pointer;

:global(.uui-calendar-day) {
Expand Down
16 changes: 9 additions & 7 deletions uui/components/datePickers/DatePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,18 @@ export function DatePickerComponent(props: DatePickerProps, ref: React.Forwarded
setBodyIsOpen(false);
};

const onInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
setBodyIsOpen(true);
e.preventDefault();
}
};

const renderInput = (renderProps: IDropdownToggler & { cx?: any }) => {
const allowClear = !props.disableClear && !!inputValue;
return (
<TextInput
{ ...renderProps }
// fixes a bug with body open, it skips unwanted prevent default
onClick={ () => {} }
isDropdown={ false }
cx={ cx(props.inputCx, isBodyOpen && uuiMod.focus) }
icon={ props.mode !== EditMode.CELL && systemIcons.calendar ? systemIcons.calendar : undefined }
Expand All @@ -88,9 +93,9 @@ export function DatePickerComponent(props: DatePickerProps, ref: React.Forwarded
isReadonly={ props.isReadonly }
tabIndex={ props.isReadonly || props.isDisabled ? -1 : 0 }
onFocus={ (e) => {
setBodyIsOpen(true);
props.onFocus?.(e);
} }
onKeyDown={ onInputKeyDown }
onBlur={ onBlur }
mode={ props.mode || defaultMode }
rawProps={ props.rawProps?.input }
Expand All @@ -101,10 +106,7 @@ export function DatePickerComponent(props: DatePickerProps, ref: React.Forwarded

const renderBody = (renderProps: DropdownBodyProps) => {
return (
<DropdownContainer
{ ...renderProps }
focusLock={ false }
>
<DropdownContainer { ...renderProps }>
<DatePickerBody
value={ value }
onValueChange={ onBodyValueChange }
Expand Down
6 changes: 4 additions & 2 deletions uui/components/datePickers/DatePickerBody.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@
margin: 6px;
color: var(--uui-dtp_body-text);

&:hover {
&:hover, &:focus {
outline: none;
width: 70px;
height: 34px;
border: 1px solid var(--uui-dtp_body-item-border-hover);
Expand All @@ -83,7 +84,8 @@
margin: 6px 0;
color: var(--uui-dtp_body-text);

&:hover {
&:hover, &:focus {
outline: none;
width: 61px;
height: 34px;
border: 1px solid var(--uui-dtp_body-item-border-hover);
Expand Down
23 changes: 6 additions & 17 deletions uui/components/datePickers/RangeDatePicker.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useState } from 'react';
import cx from 'classnames';
import {
DropdownBodyProps, isFocusReceiverInsideFocusLock, useUuiContext,
DropdownBodyProps, useUuiContext,
} from '@epam/uui-core';
import { Dropdown } from '@epam/uui-components';
import { DropdownContainer } from '../overlays';
Expand Down Expand Up @@ -39,8 +39,7 @@ function RangeDatePickerComponent(props: RangeDatePickerProps, ref: React.Forwar
}
};

const onOpenChange = (newIsOpen: boolean, focus?: RangeDatePickerInputType) => {
setInFocus(newIsOpen && focus ? focus : null);
const onOpenChange = (newIsOpen: boolean) => {
setIsOpen(newIsOpen);
props.onOpenChange?.(newIsOpen);
};
Expand All @@ -59,20 +58,11 @@ function RangeDatePickerComponent(props: RangeDatePickerProps, ref: React.Forwar
}
};

// mainly for closing body on tab
const onInputWrapperBlur: React.FocusEventHandler<HTMLDivElement> = (event) => {
if (isFocusReceiverInsideFocusLock(event)) {
return;
}
onOpenChange(false);
};

const renderBody = (renderProps: DropdownBodyProps): JSX.Element => {
return (
<DropdownContainer
{ ...renderProps }
cx={ cx(css.dropdownContainer) }
focusLock={ false }
>
<FlexRow>
<RangeDatePickerBody
Expand Down Expand Up @@ -115,12 +105,11 @@ function RangeDatePickerComponent(props: RangeDatePickerProps, ref: React.Forwar
value={ value }
format={ format }
onValueChange={ onValueChange }
onBlur={ onInputWrapperBlur }
onFocusInput={ (e, i) => {
props.onFocus?.(e, i);
onOpenChange(true, i);
onFocusInput={ (e, type) => {
props.onFocus?.(e, type);
setInFocus(type);
} }
onBlurInput={ props.onBlur }
onBlurInput={ (e, type) => { props.onBlur?.(e, type); !isOpen && setInFocus(null); } }
/>
);
} }
Expand Down
22 changes: 11 additions & 11 deletions uui/components/datePickers/RangeDatePickerInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,6 @@ export interface RangeDatePickerInputProps
* Handles blur event on input element
*/
onBlurInput?: (event: React.FocusEvent<HTMLInputElement, Element>, inputType: RangeDatePickerInputType) => void;
/**
* Handles blur event on root element
*/
onBlur?: (event: React.FocusEvent<HTMLDivElement>) => void;
}

export const RangeDatePickerInput = forwardRef<HTMLDivElement, RangeDatePickerInputProps>(({
Expand All @@ -71,7 +67,6 @@ export const RangeDatePickerInput = forwardRef<HTMLDivElement, RangeDatePickerIn
inFocus,
format,
onValueChange,
onBlur,
onFocusInput,
onBlurInput,
onClick,
Expand Down Expand Up @@ -117,6 +112,13 @@ export const RangeDatePickerInput = forwardRef<HTMLDivElement, RangeDatePickerIn
});
}
};

const onInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
onClick();
e.preventDefault();
}
};

const clearAllowed = !disableClear && inputValue.from && inputValue.to;
return (
Expand All @@ -131,12 +133,6 @@ export const RangeDatePickerInput = forwardRef<HTMLDivElement, RangeDatePickerIn
isInvalid && uuiMod.invalid,
inFocus && uuiMod.focus,
) }
onClick={ (event) => {
if (!isDisabled) {
onClick?.(event);
}
} }
onBlur={ onBlur }
>
<TextInput
icon={ systemIcons.calendar }
Expand All @@ -152,6 +148,8 @@ export const RangeDatePickerInput = forwardRef<HTMLDivElement, RangeDatePickerIn
isReadonly={ isReadonly }
isDropdown={ false }
rawProps={ rawProps?.from }
onClick={ onClick }
onKeyDown={ onInputKeyDown }
id={ id }
/>
<div className={ css.separator } />
Expand All @@ -171,6 +169,8 @@ export const RangeDatePickerInput = forwardRef<HTMLDivElement, RangeDatePickerIn
isReadonly={ isReadonly }
isDropdown={ false }
rawProps={ rawProps?.to }
onClick={ onClick }
onKeyDown={ onInputKeyDown }
/>
</div>
);
Expand Down
Loading

0 comments on commit 3bfc1f0

Please sign in to comment.