diff --git a/src/core_modules/capture-core/components/FormFields/New/Fields/BooleanField/BooleanField.component.js b/src/core_modules/capture-core/components/FormFields/New/Fields/BooleanField/BooleanField.component.js index b9e7265b4c..f1fee8f9ae 100644 --- a/src/core_modules/capture-core/components/FormFields/New/Fields/BooleanField/BooleanField.component.js +++ b/src/core_modules/capture-core/components/FormFields/New/Fields/BooleanField/BooleanField.component.js @@ -25,12 +25,11 @@ type Props = { class BooleanFieldPlain extends React.Component { render() { - const { onBlur, ...passOnProps } = this.props; return ( // $FlowFixMe[cannot-spread-inexact] automated comment ); } diff --git a/src/core_modules/capture-ui/BooleanField/BooleanField.component.js b/src/core_modules/capture-ui/BooleanField/BooleanField.component.js index 4aa7d933e3..96b2bfce3b 100644 --- a/src/core_modules/capture-ui/BooleanField/BooleanField.component.js +++ b/src/core_modules/capture-ui/BooleanField/BooleanField.component.js @@ -3,6 +3,9 @@ import React, { Component } from 'react'; import i18n from '@dhis2/d2-i18n'; import { SelectionBoxes } from '../SelectionBoxes/SelectionBoxes.component'; import type { OptionRendererInputData } from '../internal/SelectionBoxes/selectBoxes.types'; +import { withKeyboardNavigation } from './withKeyboardNavigation'; + +const SelectionBoxesWrapped = withKeyboardNavigation()(SelectionBoxes); type Props = { allowMultiple?: boolean, @@ -38,7 +41,7 @@ export class BooleanField extends Component { return (
{/* $FlowFixMe[cannot-spread-inexact] automated comment */} - boolean, + keyUp: (key: string) => void, + clear: () => void, +}; + +export const withKeyboardNavigation = () => (InnerComponent: ComponentType) => (props: any) => { + const pressedKeys = useRef(new Set); + + const keyboardManager = useMemo(() => ({ + keyDown: (key: string) => { + if (!managedKeys.includes(key)) { + return false; + } + const count = pressedKeys.current.size; + pressedKeys.current.add(key); + return count === 0; + }, + keyUp: (key: string) => { + pressedKeys.current.delete(key); + }, + clear: () => { + pressedKeys.current.clear(); + }, + }), [pressedKeys]); + + return ( + + ); +}; diff --git a/src/core_modules/capture-ui/internal/SelectionBoxes/SingleSelectionBoxes/SingleSelectBox/SingleSelectBox.component.js b/src/core_modules/capture-ui/internal/SelectionBoxes/SingleSelectionBoxes/SingleSelectBox/SingleSelectBox.component.js index 0831a04c34..b2b8a502ee 100644 --- a/src/core_modules/capture-ui/internal/SelectionBoxes/SingleSelectionBoxes/SingleSelectBox/SingleSelectBox.component.js +++ b/src/core_modules/capture-ui/internal/SelectionBoxes/SingleSelectionBoxes/SingleSelectBox/SingleSelectBox.component.js @@ -1,6 +1,7 @@ // @flow import * as React from 'react'; import classNames from 'classnames'; +import type { KeyboardManager } from 'capture-ui/BooleanField'; import defaultClasses from './singleSelectBox.module.css'; import type { OptionRendererInputData } from '../../selectBoxes.types'; @@ -10,47 +11,62 @@ type Props = { groupId: string, children: React.Node, onSelect: (value: any) => void, + onBlur: () => void, inputRef?: (instance: ?HTMLInputElement) => void, inFocus?: ?boolean, focusClass?: string, unFocusClass?: string, + keyboardManager: KeyboardManager, disabled?: ?boolean, }; -const keyboardKeys = { +const keys = { + TAB: 'Tab', SPACE: ' ', ENTER: 'Enter', + ARROW_LEFT: 'ArrowLeft', + ARROW_RIGHT: 'ArrowRight', + ARROW_UP: 'ArrowUp', + ARROW_DOWN: 'ArrowDown', }; +let ignoreFlag = false; + export class SingleSelectBox extends React.Component { - isSpaceClickWhenSelected: ?boolean; // Pressing space when the radio is already selected, triggers both onKeyPress and onClick. This variable is used to prevent the onClick event in these circumstances. handleSelect = () => { - if (this.isSpaceClickWhenSelected) { - this.isSpaceClickWhenSelected = false; - return; - } - const { onSelect, optionData, isSelected } = this.props; - if (isSelected) { - onSelect(null); - return; + if (ignoreFlag) { + ignoreFlag = false; + } else { + onSelect(isSelected ? null : optionData.value); } - onSelect(optionData.value); } - handleKeyPress = (event: SyntheticKeyboardEvent) => { - if ([keyboardKeys.SPACE, keyboardKeys.ENTER].includes(event.key)) { + handleKeyDown = (event: SyntheticKeyboardEvent) => { + const { keyboardManager } = this.props; + const handleKeyPress = keyboardManager.keyDown(event.key); + if (!handleKeyPress) { + event.preventDefault(); + return; + } + if (event.key == keys.TAB) { + keyboardManager.clear(); + ignoreFlag = false; + } else if ([keys.SPACE, keys.ENTER].includes(event.key)) { const { onSelect, optionData, isSelected } = this.props; - if (isSelected) { - this.isSpaceClickWhenSelected = event.key === keyboardKeys.SPACE; - onSelect(null); - return; + onSelect(isSelected ? null : optionData.value); + if (event.key === keys.SPACE) { + ignoreFlag = true; } - this.isSpaceClickWhenSelected = false; - onSelect(optionData.value); + } else { + ignoreFlag = true; } } + handleKeyUp = (event: SyntheticKeyboardEvent) => { + this.props.keyboardManager.keyUp(event.key); + } + render() { const { optionData, @@ -82,8 +98,9 @@ export class SingleSelectBox extends React.Component { checked={isSelected} value={optionData.value} onClick={this.handleSelect} + onKeyDown={this.handleKeyDown} + onKeyUp={this.handleKeyUp} onChange={() => {}} - onKeyPress={this.handleKeyPress} disabled={disabled} {...passOnProps} /> diff --git a/src/core_modules/capture-ui/internal/SelectionBoxes/SingleSelectionBoxes/SingleSelectBox/withFocusHandler.js b/src/core_modules/capture-ui/internal/SelectionBoxes/SingleSelectionBoxes/SingleSelectBox/withFocusHandler.js index cb465a0ecb..c8a5858eec 100644 --- a/src/core_modules/capture-ui/internal/SelectionBoxes/SingleSelectionBoxes/SingleSelectBox/withFocusHandler.js +++ b/src/core_modules/capture-ui/internal/SelectionBoxes/SingleSelectionBoxes/SingleSelectBox/withFocusHandler.js @@ -2,6 +2,7 @@ import * as React from 'react'; type Props = { + setInputRef: (element: HTMLInputElement) => void, onSetFocus?: () => void, onRemoveFocus?: () => void, inFocus: boolean, @@ -66,10 +67,11 @@ export const withFocusHandler = () => (InnerComponent: React.ComponentType) setInputInstance = (instance: HTMLInputElement) => { this.inputInstance = instance; + this.props.setInputRef(instance); } render() { - const { onSetFocus, onRemoveFocus, inFocus, ...passOnProps } = this.props; + const { setInputRef, onSetFocus, onRemoveFocus, inFocus, ...passOnProps } = this.props; return ( // $FlowFixMe[cannot-spread-inexact] automated comment void, + onBlur: () => void; onSetFocus?: () => void, onRemoveFocus?: () => void, + keyboardManager: KeyboardManager, disabled?: ?boolean, }; -export class SingleSelectionBoxes extends React.Component { +type State = { + refList: Array, +}; + +export class SingleSelectionBoxes extends React.Component { static getFocusClass(classes: Object, isSelected: boolean) { return isSelected ? classes.focusSelected : classes.focusUnselected; } + refList: Array; + constructor(props: Props) { + super(props); + this.state = { + refList: [], + }; + } + + onBlur = (event: SyntheticFocusEvent) => { + if (!this.state.refList.includes(event.relatedTarget)) { + this.props.keyboardManager.clear(); + this.props.onBlur(); + } + } + getCheckedClass = (iconSelected: ?string, iconDisabled?: string, isDisabled: ?boolean) => classNames( iconSelected, iconDisabled && { [iconDisabled]: isDisabled }, @@ -85,29 +107,36 @@ export class SingleSelectionBoxes extends React.Component { } getOption(optionData: OptionRendererInputData, isSelected: boolean, index: number) { - const { orientation, id: groupId, value, onSelect, classes, onSetFocus, onRemoveFocus, disabled } = this.props; + const { orientation, id: groupId, value, onSelect, classes, onSetFocus, onRemoveFocus, keyboardManager, disabled } = this.props; const containerClass = orientation === orientations.HORIZONTAL ? defaultClasses.optionContainerHorizontal : defaultClasses.optionContainerVertical; const tabIndex = isSelected || (index === 0 && !value && value !== false && value !== 0) ? 0 : -1; const IconElement = this.getIconElement(optionData, isSelected); + const setInputRef = (element: HTMLInputElement) => { + this.setState((state) => { + state.refList[index] = element; + }); + }; return (
- { /* $FlowSuppress */ } {/* $FlowFixMe[prop-missing] automated comment */} {IconElement}