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

FORNO-1690: Add toggle-group component #415

Merged
merged 13 commits into from
Jul 4, 2024
108 changes: 100 additions & 8 deletions src/system/Form/RadioBoxGroup.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,16 +94,103 @@ const RadioOption = ( {
);
};

RadioOption.propTypes = {
const ChipOption = ( {
defaultValue,
option: { id, value, label },
name,
disabled,
onChangeHandler,
} ) => {
const checked = `${ defaultValue }` === `${ value }`;
const forLabel = id || value;
const ref = React.useRef( null );
const describedById = `input-radio-box-${ forLabel }-description`;

return (
<div
id={ `o${ forLabel }` }
onClick={ () => {
ref.current?.click();
} }
sx={ {
display: 'inline-flex',
position: 'relative',
background: checked ? 'layer.4' : undefined,
color: 'text',
minHeight: '32px',
boxShadow: checked ? 'low' : undefined,
'&:hover': {
background: checked ? 'layer.4' : 'layer.1',
},
borderRadius: 1,
} }
>
<input
ref={ ref }
type="radio"
aswasif007 marked this conversation as resolved.
Show resolved Hide resolved
id={ forLabel }
disabled={ disabled }
name={ name }
checked={ checked }
aria-checked={ checked }
value={ value }
onChange={ onChangeHandler }
aria-labelledby={ describedById }
sx={ {
opacity: 0,
height: 0,
width: 0,
position: 'absolute',
'&:focus-visible + label': theme => theme.outline,
} }
/>

<label
id={ describedById }
htmlFor={ forLabel }
sx={ {
height: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
width: '100%',
px: 3,
fontWeight: 400,
fontSize: 2,
cursor: 'pointer',
borderRadius: 1,
} }
>
{ label }
</label>
</div>
);
};

ChipOption.propTypes = RadioOption.propTypes = {
defaultValue: PropTypes.string,
option: PropTypes.object,
name: PropTypes.string,
onChangeHandler: PropTypes.func,
checked: PropTypes.bool,
disabled: PropTypes.bool,
width: PropTypes.string,
};

const groupStyleOverrides = {
chip: {
background: 'layer.3',
p: 1,
display: 'inline-flex',
gap: 1,
borderRadius: 1,
},
primary: {
display: 'inline-block',
mb: 2,
p: 0,
},
};

const RadioBoxGroup = React.forwardRef(
(
{
Expand All @@ -117,6 +204,7 @@ const RadioBoxGroup = React.forwardRef(
errorMessage,
hasError,
required,
variant = 'primary',
...props
},
forwardRef
Expand All @@ -131,8 +219,13 @@ const RadioBoxGroup = React.forwardRef(
[ onChange ]
);

let Option = RadioOption;
if ( variant === 'chip' ) {
Option = ChipOption;
}

const renderedOptions = options.map( option => (
<RadioOption
<Option
defaultValue={ defaultValue }
disabled={ disabled }
key={ option?.id || option?.value }
Expand All @@ -148,11 +241,9 @@ const RadioBoxGroup = React.forwardRef(
<fieldset
sx={ {
border: 0,
p: hasError ? 2 : 0,
display: 'inline-block',
mb: 2,
...groupStyleOverrides[ variant ],
...( hasError
? { border: '1px solid', borderColor: 'input.border.error', borderRadius: 2 }
? { border: '1px solid', borderColor: 'input.border.error', borderRadius: 2, p: 2 }
: {} ),
} }
ref={ forwardRef }
Expand All @@ -171,7 +262,7 @@ const RadioBoxGroup = React.forwardRef(
<div
sx={ {
display: 'flex',
gap: 2,
gap: variant === 'chip' ? 1 : 2,
} }
>
{ renderedOptions }
Expand Down Expand Up @@ -202,6 +293,7 @@ RadioBoxGroup.propTypes = {
errorMessage: PropTypes.string,
hasError: PropTypes.bool,
required: PropTypes.bool,
variant: PropTypes.oneOf( [ 'primary', 'chip' ] ),
};

export { RadioBoxGroup };
51 changes: 51 additions & 0 deletions src/system/Form/RadioBoxGroup.stories.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,31 @@ import { RadioBoxGroup } from '..';
export default {
title: 'RadioBoxGroup',
component: RadioBoxGroup,
parameters: {
docs: {
description: {
component: `
A radio-box-group is a group of radio buttons that are styled as boxes. This component is used
to allow users to select one option from a list of options.

## Guidance

### When to use the component

- <strong>Select an option in a form.</strong> Use a radio-box-group when you want users to select
a single option from a list of options.
- <strong>Use as a toggle-group.</strong> Use a radio-box-group with the chip variant when you want
to allow users to toggle between different options.

-------

This documentation is heavily inspired by the [U.S Web Design System (USWDS)](https://designsystem.digital.gov/components/tooltip/#package). We use USWDS as trusted source of truth for accessibility and usability best practices.

## Component Properties
`,
},
},
},
};

const options = [
Expand Down Expand Up @@ -58,3 +83,29 @@ export const Errors = () => {
/>
);
};

export const ChipVariant = () => {
const [ value, setValue ] = useState( 'table' );

return (
<RadioBoxGroup
defaultValue={ value }
onChange={ e => setValue( e.target.value ) }
options={ [
{
label: 'Table',
value: 'table',
},
{
label: 'Grid',
value: 'grid',
},
{
label: 'Card',
value: 'card',
},
] }
variant="chip"
/>
);
};
47 changes: 47 additions & 0 deletions src/system/Form/RadioBoxGroup.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* External dependencies
*/
import { render, screen } from '@testing-library/react';
import { axe } from 'jest-axe';

/**
* Internal dependencies
*/
import { RadioBoxGroup } from './RadioBoxGroup';

const defaultProps = {
options: [
{
label: 'One',
value: 'one',
description: 'This is desc 1',
},
{
label: 'Two',
value: 'two',
description: 'This is desc 2',
},
{
label: 'Three',
value: 'three',
description: 'This is desc 3',
},
],
onChange: jest.fn(),
};

describe( '<RadioBoxGroup />', () => {
it.each( [ 'primary', 'chip' ] )( 'renders the default variant', async variant => {
const { container } = render( <RadioBoxGroup { ...defaultProps } variant={ variant } /> );

const dom = await screen.findAllByRole( 'radio' );

expect( dom ).toHaveLength( 3 );
expect( dom[ 0 ] ).toHaveAttribute( 'value', 'one' );
expect( dom[ 1 ] ).toHaveAttribute( 'value', 'two' );
expect( dom[ 2 ] ).toHaveAttribute( 'value', 'three' );

// Check for accessibility issues
expect( await axe( container ) ).toHaveNoViolations();
} );
} );
2 changes: 1 addition & 1 deletion src/system/Tooltip/Tooltip.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ A tooltip is a short descriptive message that appears when a user hovers or focu
element. Our tooltip aims to be 100% CSS-only. It uses a global css approach to inject the
tooltip styles.

## Kwown issues
## Known issues

- Storybook uses iframes to render the components. This means that the tooltip box will overlap in the demos, but you can click on each demo page to see the correct render.
- The current implementation of this component is <strong>NOT</strong> protected from viewport
Expand Down