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

fix(radio): add support for nested interactive elements #4243

Merged
merged 12 commits into from
May 25, 2021
Merged
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
13 changes: 13 additions & 0 deletions documentation-site/components/yard/config/checkbox.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
/*
Copyright (c) Uber Technologies, Inc.

This source code is licensed under the MIT license found in the
LICENSE file in the root directory of this source tree.
*/
import pick from 'just-pick';
import {Checkbox, STYLE_TYPE, LABEL_PLACEMENT} from 'baseui/checkbox';
import {PropTypes} from 'react-view';
Expand Down Expand Up @@ -119,6 +125,13 @@ const CheckboxConfig: TConfig = {
description: 'If true the component will be focused on the first mount.',
hidden: true,
},
containsInteractiveElement: {
value: false,
type: PropTypes.Boolean,
description:
'Indicates the checkbox label contains an interactive element, and the default label behavior should be prevented for child elements.',
hidden: true,
},
name: {
value: undefined,
type: PropTypes.String,
Expand Down
15 changes: 14 additions & 1 deletion documentation-site/components/yard/config/radio.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
/*
Copyright (c) Uber Technologies, Inc.

This source code is licensed under the MIT license found in the
LICENSE file in the root directory of this source tree.
*/
import pick from 'just-pick';

import {Radio, RadioGroup, ALIGN} from 'baseui/radio';
Expand Down Expand Up @@ -47,7 +53,7 @@ const RadioGroupConfig: TConfig = {
stateful: true,
},
onChange: {
value: 'e => setValue(e.target.value)',
value: 'e => setValue(e.currentTarget.value)',
type: PropTypes.Function,
description: 'Handler for change events on trigger element.',
propHook: {
Expand Down Expand Up @@ -113,6 +119,13 @@ const RadioGroupConfig: TConfig = {
description: 'Set to be focused (active) on selectedchecked radio.',
hidden: true,
},
containsInteractiveElement: {
value: false,
type: PropTypes.Boolean,
description:
'Indicates the radio contains an interactive element, and the default label behavior should be prevented for child elements.',
hidden: true,
},
'aria-label': {
value: undefined,
type: PropTypes.String,
Expand Down
24 changes: 24 additions & 0 deletions src/checkbox/__tests__/checkbox-select.scenario.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
Copyright (c) Uber Technologies, Inc.

This source code is licensed under the MIT license found in the
LICENSE file in the root directory of this source tree.
*/
// @flow

import * as React from 'react';

import {StatefulCheckbox} from '../index.js';

import {FormControl} from '../../form-control/index.js';
import {Select} from '../../select/index.js';

export default function Scenario() {
return (
<FormControl label="Test-label">
<StatefulCheckbox containsInteractiveElement>
<Select placeholder="Select color" />
</StatefulCheckbox>
</FormControl>
);
}
2 changes: 2 additions & 0 deletions src/checkbox/__tests__/checkbox.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ LICENSE file in the root directory of this source tree.
import React from 'react';
import CheckboxIndeterminate from './checkbox-indeterminate.scenario.js';
import CheckboxPlacement from './checkbox-placement.scenario.js';
import CheckboxSelect from './checkbox-select.scenario.js';
import CheckboxStates from './checkbox-states.scenario.js';
import CheckboxToggleRound from './checkbox-toggle-round.scenario.js';
import CheckboxToggle from './checkbox-toggle.scenario.js';
Expand All @@ -17,6 +18,7 @@ import CheckboxDefault from './checkbox.scenario.js';

export const Indeterminate = () => <CheckboxIndeterminate />;
export const Placement = () => <CheckboxPlacement />;
export const Select = () => <CheckboxSelect />;
export const States = () => <CheckboxStates />;
export const ToggleRound = () => <CheckboxToggleRound />;
export const Toggle = () => <CheckboxToggle />;
Expand Down
9 changes: 8 additions & 1 deletion src/checkbox/checkbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class StatelessCheckbox extends React.Component<PropsT, StatelessStateT> {
static defaultProps: DefaultPropsT = {
overrides: {},
checked: false,
containsInteractiveElement: false,
disabled: false,
autoFocus: false,
isIndeterminate: false,
Expand Down Expand Up @@ -186,7 +187,13 @@ class StatelessCheckbox extends React.Component<PropsT, StatelessStateT> {
{...sharedProps}
{...getOverrideProps(LabelOverride)}
>
{children}
{this.props.containsInteractiveElement ? (
// Prevents the event from bubbling up to the label and moving focus to the radio button
// eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
<div onClick={e => e.preventDefault()}>{children}</div>
) : (
children
)}
</Label>
);
return (
Expand Down
2 changes: 2 additions & 0 deletions src/checkbox/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export interface StatefulCheckboxProps {
disabled?: boolean;
isError?: boolean;
error?: boolean;
containsInteractiveElement?: boolean;
labelPlacement?: 'top' | 'right' | 'bottom' | 'left';
inputRef?: React.Ref<HTMLInputElement>;
isIndeterminate?: boolean;
Expand Down Expand Up @@ -88,6 +89,7 @@ export interface CheckboxProps {
'aria-describedby'?: string;
'aria-errormessage'?: string;
children?: React.ReactNode;
containsInteractiveElement?: boolean;
overrides?: CheckboxOverrides;
checked?: boolean;
disabled?: boolean;
Expand Down
4 changes: 4 additions & 0 deletions src/checkbox/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ export type PropsT = {
ariaLabel?: string,
/** Component or String value for label of checkbox. */
children?: React$Node,
/** Indicates if this checkbox children contain an interactive element (prevents the label from moving focus from the child element to the radio button) */
containsInteractiveElement?: boolean,
overrides?: OverridesT,
/** Check or uncheck the control. */
checked?: boolean,
Expand Down Expand Up @@ -151,6 +153,8 @@ export type StatefulCheckboxPropsT = {
overrides?: OverridesT,
/** Component or String value for label of checkbox. */
children?: React$Node,
/** Indicates if this checkbox children contain an interactive element (prevents the label from moving focus from the child element to the radio button) */
containsInteractiveElement?: boolean,
/** Defines the components initial state value */
initialState?: StateT,
/** Focus the checkbox on initial render. */
Expand Down
35 changes: 35 additions & 0 deletions src/radio/__tests__/radio-select.scenario.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
Copyright (c) Uber Technologies, Inc.

This source code is licensed under the MIT license found in the
LICENSE file in the root directory of this source tree.
*/
// @flow

import * as React from 'react';

import {StatefulRadioGroup, Radio, ALIGN} from '../index.js';

import {FormControl} from '../../form-control/index.js';
import {Select} from '../../select/index.js';

export default function Scenario() {
return (
<FormControl label="Test-label">
<StatefulRadioGroup name="number" align={ALIGN.vertical}>
<Radio value="1">One</Radio>
<Radio value="2" description="This is a radio description">
Two
</Radio>
<Radio value="3">Three</Radio>
<Radio
value="4"
description="This one has a Select that only works with keyboard"
containsInteractiveElement
>
<Select placeholder="Select color" />
</Radio>
</StatefulRadioGroup>
</FormControl>
);
}
2 changes: 2 additions & 0 deletions src/radio/__tests__/radio.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ LICENSE file in the root directory of this source tree.
// @flow

import React from 'react';
import RadioSelect from './radio-select.scenario.js';
import RadioStates from './radio-states.scenario.js';
import RadioDefault from './radio.scenario.js';

export const States = () => <RadioStates />;
export const Select = () => <RadioSelect />;
export const Radio = () => <RadioDefault />;
20 changes: 19 additions & 1 deletion src/radio/__tests__/radio.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import {
getByText,
} from '@testing-library/react';

import {Radio} from '../index.js';
import {ALIGN, Radio, StatefulRadioGroup} from '../index.js';
import {Select} from '../../select/index.js';

describe('Radio', () => {
it('calls provided handlers', () => {
Expand Down Expand Up @@ -48,6 +49,23 @@ describe('Radio', () => {
expect(spy).toHaveBeenCalledTimes(4);
});

it('does not select radio when interactive element is present', () => {
const {container} = render(
<StatefulRadioGroup name="number" align={ALIGN.vertical}>
<Radio value="one" containsInteractiveElement>
<Select placeholder="Select color" />
</Radio>
<Radio value="two">Two</Radio>
</StatefulRadioGroup>,
);

const select = container.querySelector('[data-baseweb="select"]');
const radio = container.querySelector('input[type="radio"]');
expect(radio.checked).toBe(false);
fireEvent.click(select);
expect(radio.checked).toBe(false);
});

it('displays description if provided', () => {
const description = 'foo';
const {container} = render(<Radio description={description}>bar</Radio>);
Expand Down
1 change: 1 addition & 0 deletions src/radio/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export interface RadioProps {
autoFocus?: boolean;
checked?: boolean;
children?: React.ReactNode;
containsInteractiveElement?: boolean;
description?: string;
disabled?: boolean;
inputRef?: React.Ref<HTMLInputElement>;
Expand Down
9 changes: 8 additions & 1 deletion src/radio/radio.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const stopPropagation = e => e.stopPropagation();
class Radio extends React.Component<RadioPropsT, RadioStateT> {
static defaultProps: $Shape<RadioPropsT> = {
overrides: {},
containsInteractiveElement: false,
checked: false,
disabled: false,
autoFocus: false,
Expand Down Expand Up @@ -121,7 +122,13 @@ class Radio extends React.Component<RadioPropsT, RadioStateT> {

const label = (
<Label {...sharedProps} {...labelProps}>
{this.props.children}
{this.props.containsInteractiveElement ? (
// Prevents the event from bubbling up to the label and moving focus to the radio button
// eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
<div onClick={e => e.preventDefault()}>{this.props.children}</div>
) : (
this.props.children
)}
</Label>
);

Expand Down
2 changes: 2 additions & 0 deletions src/radio/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ export type RadioPropsT = {
checked?: boolean,
/** Label of radio. */
children?: React$Node,
/** Indicates if this radio children contain an interactive element (prevents the label from moving focus from the child element to the radio button) */
containsInteractiveElement?: boolean,
/** Add more detail about a radio element. */
description?: string,
/** Disable the checkbox from being changed. */
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.