Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved keyboard navigation/focus handling - Use roving tabindex #799

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
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
48 changes: 35 additions & 13 deletions src/Calendar.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
import PropTypes from 'prop-types';
import React, { Component, createRef } from 'react';

import Navigation from './Calendar/Navigation';
import CenturyView from './CenturyView';
import DecadeView from './DecadeView';
import YearView from './YearView';
import FocusContainer, { FocusContext } from './FocusContainer';
import MonthView from './MonthView';
import YearView from './YearView';

import { getBegin, getBeginNext, getEnd, getValueRange } from './shared/dates';
import {
Expand Down Expand Up @@ -252,6 +253,8 @@ export default class Calendar extends Component {
view: this.props.defaultView,
};

containerRef = createRef(null);

get activeStartDate() {
const { activeStartDate: activeStartDateProps } = this.props;
const { activeStartDate: activeStartDateState } = this.state;
Expand Down Expand Up @@ -555,7 +558,7 @@ export default class Calendar extends Component {
this.setState({ hover: null });
};

renderContent(next) {
renderContent(activeTabDate, next) {
const { activeStartDate: currentActiveStartDate, onMouseOver, valueType, value, view } = this;
const {
calendarType,
Expand All @@ -577,6 +580,7 @@ export default class Calendar extends Component {

const commonProps = {
activeStartDate,
activeTabDate,
hover,
locale,
maxDate,
Expand Down Expand Up @@ -704,8 +708,8 @@ export default class Calendar extends Component {
}

render() {
const { className, inputRef, selectRange, showDoubleView } = this.props;
const { onMouseLeave, value } = this;
const { className, inputRef, maxDate, minDate, selectRange, showDoubleView } = this.props;
const { onMouseLeave, value, view, activeStartDate, setActiveStartDate } = this;
const valueArray = [].concat(value);

return (
Expand All @@ -719,14 +723,32 @@ export default class Calendar extends Component {
ref={inputRef}
>
{this.renderNavigation()}
<div
className={`${baseClassName}__viewContainer`}
onBlur={selectRange ? onMouseLeave : null}
onMouseLeave={selectRange ? onMouseLeave : null}
<FocusContainer
activeStartDate={activeStartDate}
containerRef={this.containerRef}
minDate={minDate}
maxDate={maxDate}
setActiveStartDate={setActiveStartDate}
showDoubleView={showDoubleView}
value={value}
view={view}
>
{this.renderContent()}
{showDoubleView ? this.renderContent(true) : null}
</div>
<div
className={`${baseClassName}__viewContainer`}
onBlur={selectRange ? onMouseLeave : null}
onMouseLeave={selectRange ? onMouseLeave : null}
ref={this.containerRef}
>
<FocusContext.Consumer>
{({ activeTabDate }) => (
<>
{this.renderContent(activeTabDate)}
{showDoubleView ? this.renderContent(activeTabDate, true) : null}
</>
)}
</FocusContext.Consumer>
</div>
</FocusContainer>
</div>
);
}
Expand Down
15 changes: 14 additions & 1 deletion src/Calendar/Navigation.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { getUserLocale } from 'get-user-locale';

Expand Down Expand Up @@ -76,6 +76,14 @@ export default function Navigation({
const next2ButtonDisabled =
shouldShowPrevNext2Buttons && maxDate && maxDate < nextActiveStartDate2;

// Make sure the navigation is not navigable at the first render
// so that the calendar takes the initial focus.
const [tabIndex, setTabIndex] = useState(-1);
useEffect(() => {
setTabIndex(-1);
setTimeout(() => setTabIndex(0), 0);
}, [view]);

function onClickPrevious() {
setActiveStartDate(previousActiveStartDate, 'prev');
}
Expand Down Expand Up @@ -128,6 +136,7 @@ export default function Navigation({
disabled={!drillUpAvailable}
onClick={drillUp}
style={{ flexGrow: 1 }}
tabIndex={tabIndex}
type="button"
>
<span className={`${labelClassName}__labelText ${labelClassName}__labelText--from`}>
Expand All @@ -153,6 +162,7 @@ export default function Navigation({
className={`${className}__arrow ${className}__prev2-button`}
disabled={prev2ButtonDisabled}
onClick={onClickPrevious2}
tabIndex={tabIndex}
type="button"
>
{prev2Label}
Expand All @@ -164,6 +174,7 @@ export default function Navigation({
className={`${className}__arrow ${className}__prev-button`}
disabled={prevButtonDisabled}
onClick={onClickPrevious}
tabIndex={tabIndex}
type="button"
>
{prevLabel}
Expand All @@ -176,6 +187,7 @@ export default function Navigation({
className={`${className}__arrow ${className}__next-button`}
disabled={nextButtonDisabled}
onClick={onClickNext}
tabIndex={tabIndex}
type="button"
>
{nextLabel}
Expand All @@ -187,6 +199,7 @@ export default function Navigation({
className={`${className}__arrow ${className}__next2-button`}
disabled={next2ButtonDisabled}
onClick={onClickNext2}
tabIndex={tabIndex}
type="button"
>
{next2Label}
Expand Down
1 change: 1 addition & 0 deletions src/CenturyView.spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import CenturyView from './CenturyView';
describe('CenturyView', () => {
const defaultProps = {
activeStartDate: new Date(2017, 0, 1),
activeTabDate: new Date(2017, 0, 1),
};

it('renders proper view when given activeStartDate', () => {
Expand Down
8 changes: 7 additions & 1 deletion src/CenturyView/Decade.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,19 @@ import { tileProps } from '../shared/propTypes';

const className = 'react-calendar__century-view__decades__decade';

export default function Decade({ classes, formatYear = defaultFormatYear, ...otherProps }) {
export default function Decade({
activeTabDate,
classes,
formatYear = defaultFormatYear,
...otherProps
}) {
const { date, locale } = otherProps;

return (
<Tile
{...otherProps}
classes={[].concat(classes, className)}
isFocusable={activeTabDate <= getDecadeEnd(date) && activeTabDate >= getDecadeStart(date)}
maxDateTransform={getDecadeEnd}
minDateTransform={getDecadeStart}
view="century"
Expand Down
1 change: 1 addition & 0 deletions src/DecadeView.spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import DecadeView from './DecadeView';
describe('DecadeView', () => {
const defaultProps = {
activeStartDate: new Date(2017, 0, 1),
activeTabDate: new Date(2017, 0, 1),
};

it('renders proper view when given activeStartDate', () => {
Expand Down
8 changes: 7 additions & 1 deletion src/DecadeView/Year.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,19 @@ import { tileProps } from '../shared/propTypes';

const className = 'react-calendar__decade-view__years__year';

export default function Year({ classes, formatYear = defaultFormatYear, ...otherProps }) {
export default function Year({
activeTabDate,
classes,
formatYear = defaultFormatYear,
...otherProps
}) {
const { date, locale } = otherProps;

return (
<Tile
{...otherProps}
classes={[].concat(classes, className)}
isFocusable={activeTabDate.getFullYear() === date.getFullYear()}
maxDateTransform={getYearEnd}
minDateTransform={getYearStart}
view="decade"
Expand Down
1 change: 1 addition & 0 deletions src/DecadeView/Year.spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Year from './Year';

const tileProps = {
activeStartDate: new Date(2018, 0, 1),
activeTabDate: new Date(2018, 0, 1),
classes: ['react-calendar__tile'],
date: new Date(2018, 0, 1),
point: 2018,
Expand Down
71 changes: 34 additions & 37 deletions src/Flex.jsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,41 @@
import React from 'react';
import React, { forwardRef } from 'react';
import PropTypes from 'prop-types';

function toPercent(num) {
return `${num}%`;
}

export default function Flex({
children,
className,
count,
direction,
offset,
style,
wrap,
...otherProps
}) {
return (
<div
className={className}
style={{
display: 'flex',
flexDirection: direction,
flexWrap: wrap ? 'wrap' : 'nowrap',
...style,
}}
{...otherProps}
>
{React.Children.map(children, (child, index) =>
React.cloneElement(child, {
...child.props,
style: {
flexBasis: toPercent(100 / count),
flexShrink: 0,
flexGrow: 0,
overflow: 'hidden',
marginLeft: offset && index === 0 ? toPercent((100 * offset) / count) : null,
},
}),
)}
</div>
);
}
const Flex = forwardRef(
({ children, className, count, direction, offset, style, wrap, ...otherProps }, ref) => {
return (
<div
className={className}
ref={ref}
style={{
display: 'flex',
flexDirection: direction,
flexWrap: wrap ? 'wrap' : 'nowrap',
...style,
}}
{...otherProps}
>
{React.Children.map(children, (child, index) =>
React.cloneElement(child, {
...child.props,
style: {
flexBasis: toPercent(100 / count),
flexShrink: 0,
flexGrow: 0,
overflow: 'hidden',
marginLeft: offset && index === 0 ? toPercent((100 * offset) / count) : null,
},
}),
)}
</div>
);
},
);
Flex.displayName = 'Flex';

Flex.propTypes = {
children: PropTypes.node,
Expand All @@ -51,3 +46,5 @@ Flex.propTypes = {
style: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),
wrap: PropTypes.bool,
};

export default Flex;
Loading