Skip to content
This repository has been archived by the owner on May 24, 2024. It is now read-only.

Commit

Permalink
[terra-form-radio] Fixes issue of onChange not getting triggered on…
Browse files Browse the repository at this point in the history
… Radio buttons (#3868)

Co-authored-by: SM051274 <sm051274@cerner.net>
  • Loading branch information
supreethmr and SM051274 authored Aug 23, 2023
1 parent 5effd33 commit 35e38e6
Show file tree
Hide file tree
Showing 9 changed files with 421 additions and 42 deletions.
3 changes: 2 additions & 1 deletion packages/terra-core-docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
* Changed
* Updated `iconAll` test to accommodate new icons added from OCS icon library v1.51.0.
* Updated default search delay to 2500ms.
* Update Search field examples to be more functionality focused.
* Update Search field examples to be more functionality focused.
* Updated `terra-form-radio-field` example to display selected value.

## 1.36.0 - (August 11, 2023)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ export default class extends React.Component {
<Radio id="saturday" name="weekday" labelText="Saturday" onChange={this.handleOnChange} value="saturday" />
</RadioField>
</div>
<span>Selected day: </span>
<span>{this.state.selectedAnswer}</span>
<hr />
<button className={cx('radio-button-wrapper')} type="button" aria-label="Toggle Invalid Status" onClick={this.handleOnClick}>Toggle Invalid Status</button>
</div>
Expand Down
3 changes: 3 additions & 0 deletions packages/terra-form-radio/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

* Fixed
* Fixed issue of `onChange` not triggered on first and last item of radio field while keyboard navigation.

## 4.39.0 - (August 11, 2023)

* Changed
Expand Down
14 changes: 7 additions & 7 deletions packages/terra-form-radio/src/Radio.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import classNames from 'classnames';
import classNamesBind from 'classnames/bind';
import ThemeContext from 'terra-theme-context';
import styles from './Radio.module.scss';
import RadioUtil from './_RadioUtil';
import { isConsideredMobileDevice } from './_RadioUtil';

const cx = classNamesBind.bind(styles);

Expand Down Expand Up @@ -52,16 +52,16 @@ const propTypes = {
*/
name: PropTypes.string,
/**
* Function to trigger when focus is lost from the radio button.
*/
* Function to trigger when focus is lost from the radio button.
*/
onBlur: PropTypes.func,
/**
* Function to trigger when user clicks on the radio button. Provide a function to create a controlled input.
*/
onChange: PropTypes.func,
/**
* Function to trigger when user focuses on the radio button.
*/
* Function to trigger when user focuses on the radio button.
*/
onFocus: PropTypes.func,
/**
* The value of the input element.
Expand Down Expand Up @@ -125,7 +125,7 @@ const Radio = ({
'label',
{ 'is-disabled': disabled },
{ 'is-hidden': isLabelHidden },
{ 'is-mobile': RadioUtil.isConsideredMobileDevice() },
{ 'is-mobile': isConsideredMobileDevice() },
labelTextAttrs.className,
]);

Expand All @@ -140,7 +140,7 @@ const Radio = ({

const outerRingClasses = cx([
'outer-ring',
{ 'is-mobile': RadioUtil.isConsideredMobileDevice() },
{ 'is-mobile': isConsideredMobileDevice() },
]);

const innerRingClasses = cx([
Expand Down
58 changes: 38 additions & 20 deletions packages/terra-form-radio/src/RadioField.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import VisualyHiddenText from 'terra-visually-hidden-text';
import {
VALUE_UP, VALUE_DOWN, VALUE_RIGHT, VALUE_LEFT,
} from 'keycode-js';
import { findFirstFocusableItem, findLastFocusableItem } from './_RadioUtil';
import styles from './RadioField.module.scss';

const cx = classNamesBind.bind(styles);
Expand Down Expand Up @@ -113,7 +114,7 @@ const RadioField = (props) => {
legendAttrs.className,
]);

const fieldSetId = `terra-radio-group-${uniqueid()}`;
const fieldSetId = customProps.id || `terra-radio-group-${uniqueid()}`;
const legendAriaDescriptionId = `terra-radio-field-description-${uniqueid()}`;
const helpAriaDescriptionId = help ? `terra-radio-field-description-help-${uniqueid()}` : '';
const errorAriaDescriptionId = error ? `terra-radio-field-description-error-${uniqueid()}` : '';
Expand Down Expand Up @@ -144,34 +145,51 @@ const RadioField = (props) => {
</LegendGroup>
);

const handleKeyDown = (event) => {
/*
* Note: Cyclic Navigation of Radio button is not supported in Safari browser hence adding keydown event handler to support cyclic navigation.
* this handler will use native mouse event to set focus back to first radio button when we press down or right arrow key on last radio button and vise versa.
*/
const handleKeyDown = (event, radio) => {
const radioGroup = document.getElementById(fieldSetId);
const radioItems = radioGroup.querySelectorAll('[type=radio]');
const itemIndex = Array.from(radioItems).indexOf(event.currentTarget);
if (event.key === VALUE_DOWN || event.key === VALUE_RIGHT) {
if (itemIndex === radioItems.length - 1) {
radioItems[0].focus();
radioItems[0].checked = true;
} else {
radioItems[itemIndex + 1].focus();
radioItems[itemIndex + 1].checked = true;
}
} else if (event.key === VALUE_UP || event.key === VALUE_LEFT) {
if (itemIndex === 0) {
radioItems[radioItems.length - 1].focus();
radioItems[radioItems.length - 1].checked = true;
} else {
radioItems[itemIndex - 1].focus();
radioItems[itemIndex - 1].checked = true;
if (radioGroup) {
const radioItems = radioGroup.querySelectorAll('[type=radio]');
const itemIndex = Array.from(radioItems).indexOf(event.currentTarget);
const onClick = new MouseEvent('click', { bubbles: true, cancelable: false });
const firstItemIndex = findFirstFocusableItem(radioItems);
const lastItemIndex = findLastFocusableItem(radioItems);

if (event.nativeEvent.key === VALUE_DOWN || event.nativeEvent.key === VALUE_RIGHT) {
if (itemIndex === lastItemIndex) {
radioItems[firstItemIndex].dispatchEvent(onClick);
}
} else if (event.nativeEvent.key === VALUE_UP || event.nativeEvent.key === VALUE_LEFT) {
if (itemIndex === firstItemIndex) {
radioItems[lastItemIndex].dispatchEvent(onClick);
}
}
}
if (radio && radio.props.onKeyDown) {
radio.props.onKeyDown();
}
};

/*
* Focus gets lost when radio button's are selected via mouse in Safari browser.
* This set focus back on the radio button on mouse click
*/
const handleClick = (event, radio) => {
event?.currentTarget?.focus();
if (radio && radio.props.onClick) {
radio.props.onClick();
}
};

const content = React.Children.map(children, (child) => {
if (child && child.type.isRadio) {
const eventHandlersForSafari = (isSafari) ? { onKeyDown: (event) => handleKeyDown(event, child), onClick: (event) => handleClick(event, child) } : undefined;
return React.cloneElement(child, {
inputAttrs: {
...child.props.inputAttrs, 'aria-describedby': ariaDescriptionIds, onKeyDown: handleKeyDown,
...child.props.inputAttrs, 'aria-describedby': ariaDescriptionIds, ...eventHandlersForSafari,
},
});
}
Expand Down
24 changes: 23 additions & 1 deletion packages/terra-form-radio/src/_RadioUtil.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,28 @@ const isConsideredMobileDevice = () => window.matchMedia('(max-width: 1024px)').
|| navigator.msMaxTouchPoints > 0
);

export default {
const findLastFocusableItem = (radioBtns) => {
let itemIndex = radioBtns.length - 1;

while (radioBtns[itemIndex] && radioBtns[itemIndex].hasAttribute('disabled') && itemIndex > -1) {
itemIndex -= 1;
}

return (itemIndex) || undefined;
};

const findFirstFocusableItem = (radioBtns) => {
let itemIndex = 0;

while (radioBtns[itemIndex] && radioBtns[itemIndex].hasAttribute('disabled') && itemIndex < radioBtns.length) {
itemIndex += 1;
}

return (itemIndex < radioBtns.length) ? itemIndex : undefined;
};

export {
isConsideredMobileDevice,
findLastFocusableItem,
findFirstFocusableItem,
};
20 changes: 10 additions & 10 deletions packages/terra-form-radio/tests/jest/Radio.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,32 @@ import Radio from '../../src/Radio';
window.matchMedia = () => ({ matches: true });

it('should render a radio', () => {
const checkBox = (<Radio labelText="Radio" />);
const wrapper = shallow(checkBox);
const radioButton = (<Radio labelText="Radio" />);
const wrapper = shallow(radioButton);
expect(wrapper).toMatchSnapshot();
});

it('should render an uncontrolled radio', () => {
const checkBox = (<Radio defaultChecked labelText="Radio" />);
const wrapper = shallow(checkBox);
const radioButton = (<Radio defaultChecked labelText="Radio" />);
const wrapper = shallow(radioButton);
expect(wrapper).toMatchSnapshot();
});

it('should render a controlled radio', () => {
const checkBox = (<Radio checked onChange={() => {}} labelText="Radio" />);
const wrapper = shallow(checkBox);
const radioButton = (<Radio checked onChange={() => {}} labelText="Radio" />);
const wrapper = shallow(radioButton);
expect(wrapper).toMatchSnapshot();
});

it('should render a disabled radio', () => {
const checkBox = (<Radio checked onChange={() => {}} labelText="Radio" disabled />);
const wrapper = shallow(checkBox);
const radioButton = (<Radio checked onChange={() => {}} labelText="Radio" disabled />);
const wrapper = shallow(radioButton);
expect(wrapper).toMatchSnapshot();
});

it('should render a radio with a hidden label', () => {
const checkBox = (<Radio checked onChange={() => {}} labelText="Radio" isLabelHidden />);
const wrapper = shallow(checkBox);
const radioButton = (<Radio checked onChange={() => {}} labelText="Radio" isLabelHidden />);
const wrapper = shallow(radioButton);
expect(wrapper).toMatchSnapshot();
});

Expand Down
39 changes: 38 additions & 1 deletion packages/terra-form-radio/tests/jest/RadioField.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@ window.matchMedia = () => ({ matches: true });
jest.mock('lodash.uniqueid');

let userAgentGetter;
beforeAll(() => {
beforeEach(() => {
userAgentGetter = jest.spyOn(window.navigator, 'userAgent', 'get');
uniqueid.mockReturnValue('uuid123');
});

afterEach(() => {
jest.restoreAllMocks();
});

it('should render a default radio field', () => {
const radioField = <RadioField legend="Default RadioField" />;
const wrapper = shallowWithIntl(radioField);
Expand Down Expand Up @@ -67,6 +71,39 @@ it('should render a help message', () => {
expect(wrapper).toMatchSnapshot();
});

it('should render onkeydown and onclick event on radio button for safari browser', () => {
userAgentGetter.mockReturnValue('Safari');
const radioField = (
<RadioField
id="RadioFieldOne"
legend="Help RadioField"
help="This will help up determine how many chairs to request"
>
<Radio id="firstRadio" labelText="Radio" />
</RadioField>
);
const wrapper = mountWithIntl(radioField);
expect(wrapper.find('input').prop('onKeyDown')).toBeDefined();
expect(wrapper.find('input').prop('onClick')).toBeDefined();
expect(wrapper).toMatchSnapshot();
});

it('should not render onkeydown and onclick event on radio button for non-safari browser', () => {
const radioField = (
<RadioField
id="RadioFieldOne"
legend="Help RadioField"
help="This will help up determine how many chairs to request"
>
<Radio id="firstRadio" labelText="Radio" />
</RadioField>
);
const wrapper = mountWithIntl(radioField);
expect(wrapper.find('input').prop('onKeyDown')).toBeUndefined();
expect(wrapper.find('input').prop('onClick')).toBeUndefined();
expect(wrapper).toMatchSnapshot();
});

it('should render an optional part on the label', () => {
const radioField = <RadioField legend="Optional RadioField" showOptional />;
const wrapper = shallowWithIntl(radioField);
Expand Down
Loading

0 comments on commit 35e38e6

Please sign in to comment.