Skip to content

Commit cc58d9a

Browse files
authored
fix(datepicker): fix onBlur, tabIndex and a11y issues (#511)
* feat(datepicker): support tabIndex prop * fix(datepicker): fire onBlur when leaving input * feat(a11y): make dates tabbable and translate aria-labels * fix(a11y): show calendar when input is focused * ci(GitHub): change name of semantic-release step
1 parent 3b0a63d commit cc58d9a

File tree

11 files changed

+73
-30
lines changed

11 files changed

+73
-30
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
run: yarn validate
2323
- name: Upload test reports
2424
uses: codecov/codecov-action@v1
25-
- name: Semantic Release
25+
- name: Run semantic-release
2626
uses: cycjimmy/semantic-release-action@v2
2727
env:
2828
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

src/__tests__/datepicker.test.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react';
2-
import { fireEvent } from '@testing-library/react';
2+
import { fireEvent, waitFor } from '@testing-library/react';
33
import localeEn from '../locales/en-US.json';
44
import localePt from '../locales/pt-BR.json';
55
import { getShortDate } from '../utils';
@@ -220,7 +220,8 @@ describe('Basic datepicker', () => {
220220
});
221221

222222
fireEvent.click(getByLabelText('Click me'));
223-
expect(getByText('Today')).toBeTruthy();
223+
224+
waitFor(() => expect(getByText('Today')).toBeTruthy());
224225
});
225226

226227
describe('clearOnSameDateClick', () => {

src/__tests__/usecases.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ it('onChange is fired when invalid date is typed', () => {
1010
openDatePicker();
1111
fireEvent.click(screen.getByText('Today'));
1212

13+
expect(datePickerInput).toHaveFocus();
1314
expect(onChange).toHaveBeenCalledTimes(1);
1415
expect(onBlur).toHaveBeenCalledTimes(1);
1516

src/components/calendar/calendar.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
text-align: center;
2727
display: grid;
2828
grid-gap: 1px;
29-
grid-template-columns: repeat(7, 2.2rem);
29+
grid-template-columns: repeat(7, minmax(2.2rem, 1fr));
3030
background-color: rgba(0, 0, 0, 0.1);
3131
border: 1px solid rgba(0, 0, 0, 0.1);
3232
border-radius: 0.28571429rem;

src/components/calendar/calendar.tsx

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,21 @@ const Calendar: React.FC<CalendarProps> = ({
7878
icon="angle double left"
7979
inverted={inverted}
8080
title={previousYear}
81-
{...getBackProps({ calendars, offset: 12 })}
81+
{...getBackProps({
82+
calendars,
83+
'aria-label': previousYear,
84+
offset: 12,
85+
})}
8286
/>
8387
<Button
8488
icon="angle left"
8589
inverted={inverted}
8690
style={{ marginRight: 0 }}
8791
title={previousMonth}
88-
{...getBackProps({ calendars })}
92+
{...getBackProps({
93+
calendars,
94+
'aria-label': previousMonth,
95+
})}
8996
/>
9097
</Fragment>
9198
)}
@@ -102,14 +109,21 @@ const Calendar: React.FC<CalendarProps> = ({
102109
icon="angle right"
103110
inverted={inverted}
104111
title={nextMonth}
105-
{...getForwardProps({ calendars })}
112+
{...getForwardProps({
113+
calendars,
114+
'aria-label': nextMonth,
115+
})}
106116
/>
107117
<Button
108118
icon="angle double right"
109119
inverted={inverted}
110120
style={{ marginRight: 0 }}
111121
title={nextYear}
112-
{...getForwardProps({ calendars, offset: 12 })}
122+
{...getForwardProps({
123+
calendars,
124+
'aria-label': nextYear,
125+
offset: 12,
126+
})}
113127
/>
114128
</Fragment>
115129
)}
@@ -120,6 +134,7 @@ const Calendar: React.FC<CalendarProps> = ({
120134
<CalendarCell
121135
key={`${calendar.year}-${calendar.month}-${weekday}`}
122136
inverted={inverted}
137+
aria-label={weekday}
123138
title={weekday}
124139
>
125140
{weekday.slice(0, 2)}

src/components/cell/cell.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
padding: 5px 0;
55
height: 30px;
66
cursor: pointer;
7+
border: none;
8+
color: inherit;
9+
font-family: inherit;
710
}
811

912
.clndr-cell.inverted {

src/components/cell/cell.tsx

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type CalendarCellProps = {
1717
};
1818

1919
const CalendarCell: React.FC<CalendarCellProps> = ({
20+
children,
2021
end,
2122
hovered,
2223
inRange,
@@ -28,19 +29,30 @@ const CalendarCell: React.FC<CalendarCellProps> = ({
2829
start,
2930
today,
3031
...otherProps
31-
}) => (
32-
<span
33-
className={cn('clndr-cell', {
34-
inverted,
35-
'clndr-cell-today': today,
36-
'clndr-cell-disabled': !selectable,
37-
'clndr-cell-other-month': nextMonth || prevMonth,
38-
'clndr-cell-inrange': inRange,
39-
'clndr-cell-selected': selected,
40-
})}
41-
{...otherProps}
42-
/>
43-
);
32+
}) => {
33+
const className = cn('clndr-cell', {
34+
inverted,
35+
'clndr-cell-today': today,
36+
'clndr-cell-disabled': !selectable,
37+
'clndr-cell-other-month': nextMonth || prevMonth,
38+
'clndr-cell-inrange': inRange,
39+
'clndr-cell-selected': selected,
40+
});
41+
42+
if (!children || !selectable) {
43+
return (
44+
<span className={className} {...otherProps} tabIndex={children ? 0 : -1}>
45+
{children}
46+
</span>
47+
);
48+
}
49+
50+
return (
51+
<button className={className} {...otherProps}>
52+
{children}
53+
</button>
54+
);
55+
};
4456

4557
CalendarCell.defaultProps = {
4658
end: false,

src/components/input.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const CustomInput = React.forwardRef<Input, InputProps>((props, ref) => {
1616
isClearIconVisible,
1717
label,
1818
onClear,
19-
onClick,
19+
onFocus,
2020
required,
2121
value,
2222
...rest
@@ -36,10 +36,10 @@ const CustomInput = React.forwardRef<Input, InputProps>((props, ref) => {
3636
icon={icon}
3737
isClearIconVisible={isClearIconVisible}
3838
onClear={onClear}
39-
onClick={onClick}
39+
onClick={onFocus}
4040
/>
4141
}
42-
onClick={onClick}
42+
onFocus={onFocus}
4343
value={value}
4444
/>
4545
</Form.Field>

src/components/today-button.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ interface TodayButtonProps extends DateObj, ButtonProps {
1212
const style: React.CSSProperties = { marginTop: 10 };
1313

1414
const TodayButton: React.FC<TodayButtonProps> = ({
15+
'aria-label': ariaLabel,
16+
children,
1517
end,
1618
hovered,
1719
inRange,
@@ -24,13 +26,16 @@ const TodayButton: React.FC<TodayButtonProps> = ({
2426
...otherProps
2527
}) => (
2628
<Button
29+
aria-label={`${ariaLabel}, ${children}`}
2730
className="clndr-button-today"
2831
compact
2932
data-testid="datepicker-today-button"
3033
fluid
3134
style={style}
3235
{...otherProps}
33-
/>
36+
>
37+
{children}
38+
</Button>
3439
);
3540

3641
export default TodayButton;

src/index.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ const semanticInputProps = [
5353
'placeholder',
5454
'required',
5555
'size',
56+
'tabIndex',
5657
'transparent',
5758
'readOnly',
5859
];
@@ -233,8 +234,12 @@ class SemanticDatepicker extends React.Component<
233234
};
234235

235236
focusOnInput = () => {
236-
if (this.inputRef?.current?.focus) {
237-
this.inputRef.current.focus();
237+
if (this.inputRef?.current) {
238+
// @ts-ignore
239+
const { focus, inputRef } = this.inputRef.current;
240+
if (document.activeElement !== inputRef.current) {
241+
focus();
242+
}
238243
}
239244
};
240245

@@ -325,7 +330,7 @@ class SemanticDatepicker extends React.Component<
325330
}
326331

327332
const newState = {
328-
isVisible: keepOpenOnSelect,
333+
isVisible: fromBlur || keepOpenOnSelect,
329334
selectedDate: newDate,
330335
selectedDateFormatted: formatSelectedDate(newDate, format),
331336
typedValue: null,
@@ -462,10 +467,10 @@ class SemanticDatepicker extends React.Component<
462467
<Input
463468
{...this.inputProps}
464469
isClearIconVisible={Boolean(clearable && selectedDateFormatted)}
465-
onBlur={() => {}}
470+
onBlur={this.handleBlur}
466471
onChange={this.handleChange}
467472
onClear={this.clearInput}
468-
onClick={readOnly ? null : this.showCalendar}
473+
onFocus={readOnly ? null : this.showCalendar}
469474
onKeyDown={this.handleKeyDown}
470475
readOnly={readOnly || datePickerOnly}
471476
ref={this.inputRef}

src/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export type PickedFormInputProps = Pick<
5353
| 'name'
5454
| 'placeholder'
5555
| 'size'
56+
| 'tabIndex'
5657
| 'transparent'
5758
| 'readOnly'
5859
>;

0 commit comments

Comments
 (0)