Fix SelectableBox problems (#886)

Due to a bug in the SelectableBox component, selecting values was not possible in different components throughout this MFE.

This fixes the Gallery and the Select Problem Types components by updating the FLCC version and replacing the SelectableBox copy with an import from FLCC.

For a full bug description see #880.
This commit is contained in:
Jesper Hodge
2024-03-11 17:09:58 -04:00
committed by GitHub
parent dad4bd5282
commit b5a287639d
27 changed files with 7 additions and 1852 deletions

8
package-lock.json generated
View File

@@ -15,7 +15,7 @@
"@edx/frontend-component-footer": "^13.0.2",
"@edx/frontend-component-header": "^5.0.2",
"@edx/frontend-enterprise-hotjar": "^2.0.0",
"@edx/frontend-lib-content-components": "^2.1.1",
"@edx/frontend-lib-content-components": "^2.1.3",
"@edx/frontend-platform": "7.0.1",
"@edx/openedx-atlas": "^0.6.0",
"@fortawesome/fontawesome-svg-core": "1.2.36",
@@ -2632,9 +2632,9 @@
}
},
"node_modules/@edx/frontend-lib-content-components": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@edx/frontend-lib-content-components/-/frontend-lib-content-components-2.1.1.tgz",
"integrity": "sha512-VCEhFfLkK+o08g7Ts9YgYNb8GHd6d9Mw8lTT1L2OIMsDC2ncFHBUGOfzmoRt+M3cJMb2nAL7Yh4wvNxhgRhhuA==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@edx/frontend-lib-content-components/-/frontend-lib-content-components-2.1.3.tgz",
"integrity": "sha512-RDlWyZ57Ecbz1qOZv2aHMSsWbwo2IoK06PHEXsEgZv0f1lmp25BqlDNhhipczcdg0te5K8gVR4qATZ4+EnhKWQ==",
"dependencies": {
"@codemirror/lang-html": "^6.0.0",
"@codemirror/lang-xml": "^6.0.0",

View File

@@ -42,7 +42,7 @@
"@edx/frontend-component-footer": "^13.0.2",
"@edx/frontend-component-header": "^5.0.2",
"@edx/frontend-enterprise-hotjar": "^2.0.0",
"@edx/frontend-lib-content-components": "^2.1.1",
"@edx/frontend-lib-content-components": "^2.1.3",
"@edx/frontend-platform": "7.0.1",
"@edx/openedx-atlas": "^0.6.0",
"@fortawesome/fontawesome-svg-core": "1.2.36",

View File

@@ -1,6 +1,8 @@
import React, { useContext, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
// SelectableBox in paragon has a bug only visible on stage where you can't change selection. So we override it
import { SelectableBox } from '@edx/frontend-lib-content-components';
import {
ActionRow,
Button,
@@ -9,7 +11,6 @@ import {
ModalDialog,
useCheckboxSetValues,
} from '@openedx/paragon';
import SelectableBox from '../../../../generic/SelectableBox';
import messages from './messages';
import { getCheckedFilters, getFilterOptions, processFilters } from './utils';

View File

@@ -1,145 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useCheckboxSetContext } from './FormCheckboxSetContext';
import { FormGroupContextProvider, useFormGroupContext } from './FormGroupContext';
import FormLabel from './FormLabel';
import FormControlFeedback from './FormControlFeedback';
const CheckboxControl = React.forwardRef(
({ isIndeterminate, ...props }, ref) => {
const { getCheckboxControlProps, hasCheckboxSetProvider } = useCheckboxSetContext();
const defaultRef = React.useRef();
const resolvedRef = ref || defaultRef;
const { getControlProps } = useFormGroupContext();
let checkboxProps = getControlProps({
...props,
className: classNames('pgn__form-checkbox-input', props.className),
});
if (hasCheckboxSetProvider) {
checkboxProps = getCheckboxControlProps(checkboxProps);
}
React.useEffect(() => {
// this if(resolvedRef.current) prevents console errors in testing
if (resolvedRef.current) {
resolvedRef.current.indeterminate = isIndeterminate;
}
}, [resolvedRef, isIndeterminate]);
return (
<input
type="checkbox"
{...checkboxProps}
ref={resolvedRef}
/>
);
},
);
CheckboxControl.propTypes = {
/** Specifies whether the checkbox should be rendered in indeterminate state. */
isIndeterminate: PropTypes.bool,
/** Specifies class name to append to the base element. */
className: PropTypes.string,
};
CheckboxControl.defaultProps = {
isIndeterminate: false,
className: undefined,
};
const FormCheckbox = React.forwardRef(({
children,
className,
controlClassName,
labelClassName,
description,
isInvalid,
isValid,
controlAs,
floatLabelLeft,
...props
}, ref) => {
const { hasCheckboxSetProvider } = useCheckboxSetContext();
const { hasFormGroupProvider, useSetIsControlGroupEffect, getControlProps } = useFormGroupContext();
useSetIsControlGroupEffect(true);
const shouldActAsGroup = hasFormGroupProvider && !hasCheckboxSetProvider;
const groupProps = shouldActAsGroup ? {
...getControlProps({}),
role: 'group',
} : {};
const control = React.createElement(controlAs, { ...props, className: controlClassName, ref });
return (
<FormGroupContextProvider
controlId={props.id}
isInvalid={isInvalid}
isValid={isValid}
>
<div
className={classNames('pgn__form-checkbox', className, {
'pgn__form-control-valid': isValid,
'pgn__form-control-invalid': isInvalid,
'pgn__form-control-disabled': props.disabled,
'pgn__form-control-label-left': !!floatLabelLeft,
})}
{...groupProps}
>
{control}
<div>
<FormLabel className={labelClassName}>
{children}
</FormLabel>
{description && (
<FormControlFeedback hasIcon={false}>
{description}
</FormControlFeedback>
)}
</div>
</div>
</FormGroupContextProvider>
);
});
FormCheckbox.propTypes = {
/** Specifies id of the FormCheckbox component. */
id: PropTypes.string,
/** Specifies contents of the component. */
children: PropTypes.node.isRequired,
/** Specifies class name to append to the base element. */
className: PropTypes.string,
/** Specifies class name for control component. */
controlClassName: PropTypes.string,
/** Specifies class name for label component. */
labelClassName: PropTypes.string,
/** Specifies description to show under the checkbox. */
description: PropTypes.node,
/** Specifies whether to display checkbox in invalid state, this affects styling. */
isInvalid: PropTypes.bool,
/** Specifies whether to display checkbox in valid state, this affects styling. */
isValid: PropTypes.bool,
/** Specifies control element. */
controlAs: PropTypes.elementType,
/** Specifies whether the floating label should be aligned to the left. */
floatLabelLeft: PropTypes.bool,
/** Specifies whether the `FormCheckbox` is disabled. */
disabled: PropTypes.bool,
};
FormCheckbox.defaultProps = {
id: undefined,
className: undefined,
controlClassName: undefined,
labelClassName: undefined,
description: undefined,
isInvalid: false,
isValid: false,
controlAs: CheckboxControl,
floatLabelLeft: false,
disabled: false,
};
export { CheckboxControl };
export default FormCheckbox;

View File

@@ -1,68 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useFormGroupContext } from './FormGroupContext';
import { FormCheckboxSetContextProvider } from './FormCheckboxSetContext';
import FormControlSet from './FormControlSet';
const FormCheckboxSet = ({
children,
name,
value,
defaultValue,
isInline,
onChange,
onFocus,
onBlur,
...props
}) => {
const { getControlProps, useSetIsControlGroupEffect } = useFormGroupContext();
useSetIsControlGroupEffect(true);
const controlProps = getControlProps(props);
return (
<FormCheckboxSetContextProvider
name={name}
value={value}
defaultValue={defaultValue}
onFocus={onFocus}
onBlur={onBlur}
onChange={onChange}
>
<FormControlSet role="group" isInline={isInline} {...controlProps}>
{children}
</FormControlSet>
</FormCheckboxSetContextProvider>
);
};
FormCheckboxSet.propTypes = {
/** Specifies contents of the component. */
children: PropTypes.node.isRequired,
/** Specifies class name to append to the base element. */
className: PropTypes.string,
/** Specifies name for the component. */
name: PropTypes.string.isRequired,
/** Specifies values for the checkboxes. */
value: PropTypes.arrayOf(PropTypes.string),
/** Specifies default values for the checkboxes. */
defaultValue: PropTypes.arrayOf(PropTypes.string),
/** Specifies whether to display components with inline styling. */
isInline: PropTypes.bool,
/** Specifies onChange event handler. */
onChange: PropTypes.func,
/** Specifies onFocus event handler. */
onFocus: PropTypes.func,
/** Specifies onBlur event handler. */
onBlur: PropTypes.func,
};
FormCheckboxSet.defaultProps = {
className: undefined,
value: undefined,
defaultValue: undefined,
isInline: false,
onChange: undefined,
onFocus: undefined,
onBlur: undefined,
};
export default FormCheckboxSet;

View File

@@ -1,77 +0,0 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { callAllHandlers } from './fieldUtils';
const identityFn = props => props;
const FormCheckboxSetContext = React.createContext({
getCheckboxControlProps: identityFn,
hasCheckboxSetProvider: false,
});
const useCheckboxSetContext = () => useContext(FormCheckboxSetContext);
const FormCheckboxSetContextProvider = ({
children,
name,
onBlur,
onFocus,
onChange,
value,
defaultValue,
}) => {
const isControlled = !defaultValue && Array.isArray(value);
const getCheckboxControlProps = (checkboxProps) => ({
...checkboxProps,
name,
/* istanbul ignore next */
onBlur: checkboxProps.onBlur ? callAllHandlers(onBlur, checkboxProps.onBlur) : onBlur,
/* istanbul ignore next */
onFocus: checkboxProps.onFocus ? callAllHandlers(onFocus, checkboxProps.onFocus) : onFocus,
/* istanbul ignore next */
onChange: checkboxProps.onChange ? callAllHandlers(onChange, checkboxProps.onChange) : onChange,
checked: isControlled ? value.includes(checkboxProps.value) : undefined,
defaultChecked: isControlled ? undefined : (defaultValue && defaultValue.includes(checkboxProps.value)),
});
// eslint-disable-next-line react/jsx-no-constructed-context-values
const contextValue = {
name,
value,
defaultValue,
getCheckboxControlProps,
onBlur,
onFocus,
onChange,
hasCheckboxSetProvider: true,
};
return (
<FormCheckboxSetContext.Provider value={contextValue}>
{children}
</FormCheckboxSetContext.Provider>
);
};
FormCheckboxSetContextProvider.propTypes = {
children: PropTypes.node.isRequired,
name: PropTypes.string,
onBlur: PropTypes.func,
onFocus: PropTypes.func,
onChange: PropTypes.func,
value: PropTypes.arrayOf(PropTypes.string),
defaultValue: PropTypes.arrayOf(PropTypes.string),
};
FormCheckboxSetContextProvider.defaultProps = {
onBlur: undefined,
name: undefined,
onFocus: undefined,
onChange: undefined,
value: undefined,
defaultValue: undefined,
};
export default FormCheckboxSetContext;
export {
useCheckboxSetContext,
FormCheckboxSetContextProvider,
};

View File

@@ -1,56 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useFormGroupContext } from './FormGroupContext';
import FormText, { resolveTextType } from './FormText';
const FormControlFeedback = ({ children, ...props }) => {
const { getDescriptorProps, isInvalid, isValid } = useFormGroupContext();
const descriptorProps = getDescriptorProps(props);
const className = classNames('pgn__form-control-description', props.className);
const textType = props.type || resolveTextType({ isInvalid, isValid });
return (
<FormText
{...descriptorProps}
className={className}
type={textType}
>
{children}
</FormText>
);
};
const FEEDBACK_TYPES = [
'default',
'valid',
'invalid',
'warning',
'criteria-empty',
'criteria-valid',
'criteria-invalid',
];
FormControlFeedback.propTypes = {
/** Specifies contents of the component. */
children: PropTypes.node.isRequired,
/** Specifies class name to append to the base element. */
className: PropTypes.string,
/** Specifies whether to show an icon next to the text. */
hasIcon: PropTypes.bool,
/** Specifies feedback type, this affects styling. */
type: PropTypes.oneOf(FEEDBACK_TYPES),
/** Specifies icon to show, will only be shown if `hasIcon` prop is set to `true`. */
icon: PropTypes.node,
/** Specifies whether to show feedback with muted styling. */
muted: PropTypes.bool,
};
FormControlFeedback.defaultProps = {
hasIcon: true,
type: undefined,
icon: undefined,
className: undefined,
muted: false,
};
export default FormControlFeedback;

View File

@@ -1,40 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
const FormControlSet = ({
as,
className,
isInline,
children,
...props
}) => React.createElement(as, {
className: classNames(
className,
{
'pgn__form-control-set': !isInline,
'pgn__form-control-set-inline': isInline,
},
),
...props,
}, children);
FormControlSet.propTypes = {
/** Specifies the base element */
as: PropTypes.elementType,
/** A class name to append to the base element. */
className: PropTypes.string,
/** Specifies whether the component should be displayed with inline styling. */
isInline: PropTypes.bool,
/** Specifies contents of the component. */
children: PropTypes.node,
};
FormControlSet.defaultProps = {
as: 'div',
className: undefined,
isInline: false,
children: null,
};
export default FormControlSet;

View File

@@ -1,121 +0,0 @@
import React, {
useState, useEffect, useMemo, useCallback,
} from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import newId from './newId';
import { useIdList, omitUndefinedProperties } from './fieldUtils';
import { FORM_CONTROL_SIZES } from './constants';
const identityFn = props => props;
const noop = () => {};
const FormGroupContext = React.createContext({
getControlProps: identityFn,
useSetIsControlGroupEffect: noop,
getLabelProps: identityFn,
getDescriptorProps: identityFn,
hasFormGroupProvider: false,
});
const useFormGroupContext = () => React.useContext(FormGroupContext);
const useStateEffect = (initialState) => {
const [state, setState] = useState(initialState);
const useSetStateEffect = (newState) => {
useEffect(() => setState(newState), [newState]);
};
return [state, useSetStateEffect];
};
const FormGroupContextProvider = ({
children,
controlId: explicitControlId,
isInvalid,
isValid,
size,
}) => {
const controlId = useMemo(() => explicitControlId || newId('form-field'), [explicitControlId]);
const [describedByIds, registerDescriptorId] = useIdList(controlId);
const [labelledByIds, registerLabelerId] = useIdList(controlId);
const [isControlGroup, useSetIsControlGroupEffect] = useStateEffect(false);
const getControlProps = useCallback((controlProps) => {
// labelledByIds from the list above should only be added to a control
// if it the control is a group. We prefer adding a condition here because:
// - Hooks cannot be called inside conditionals
// - The getLabelProps function below is forced to generate an id
// whether it is needed or not.
// - This is what allows consumers of Paragon to use <Form.Label>
// interchangeably between ControlGroup type controls and regular Controls
const labelledByIdsForControl = isControlGroup ? labelledByIds : undefined;
return omitUndefinedProperties({
...controlProps,
'aria-describedby': classNames(controlProps['aria-describedby'], describedByIds) || undefined,
'aria-labelledby': classNames(controlProps['aria-labelledby'], labelledByIdsForControl) || undefined,
id: controlId,
});
}, [
isControlGroup,
describedByIds,
labelledByIds,
controlId,
]);
const getLabelProps = (labelProps) => {
const id = registerLabelerId(labelProps?.id);
if (isControlGroup) {
return { ...labelProps, id };
}
return { ...labelProps, htmlFor: controlId };
};
const getDescriptorProps = (descriptorProps) => {
const id = registerDescriptorId(descriptorProps?.id);
return { ...descriptorProps, id };
};
// eslint-disable-next-line react/jsx-no-constructed-context-values
const contextValue = {
getControlProps,
getLabelProps,
getDescriptorProps,
useSetIsControlGroupEffect,
isControlGroup,
controlId,
isInvalid,
isValid,
size,
hasFormGroupProvider: true,
};
return (
<FormGroupContext.Provider value={contextValue}>
{children}
</FormGroupContext.Provider>
);
};
FormGroupContextProvider.propTypes = {
children: PropTypes.node.isRequired,
controlId: PropTypes.string,
isInvalid: PropTypes.bool,
isValid: PropTypes.bool,
size: PropTypes.oneOf([
FORM_CONTROL_SIZES.SMALL,
FORM_CONTROL_SIZES.LARGE,
]),
};
FormGroupContextProvider.defaultProps = {
controlId: undefined,
isInvalid: undefined,
isValid: undefined,
size: undefined,
};
export {
FormGroupContext,
FormGroupContextProvider,
useFormGroupContext,
};

View File

@@ -1,42 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useFormGroupContext } from './FormGroupContext';
import { FORM_CONTROL_SIZES } from './constants';
const FormLabel = ({ children, isInline, ...props }) => {
const { size, isControlGroup, getLabelProps } = useFormGroupContext();
const className = classNames(
'pgn__form-label',
{
'pgn__form-label-inline': isInline,
'pgn__form-label-lg': size === FORM_CONTROL_SIZES.LARGE,
'pgn__form-label-sm': size === FORM_CONTROL_SIZES.SMALL,
},
props.className,
);
const labelProps = getLabelProps({ ...props, className });
const componentType = isControlGroup ? 'p' : 'label';
return React.createElement(componentType, labelProps, children);
};
const SIZE_CHOICES = ['sm', 'lg'];
FormLabel.propTypes = {
/** Specifies class name to append to the base element. */
className: PropTypes.string,
/** Specifies contents of the component. */
children: PropTypes.node.isRequired,
/** Specifies whether the component should be displayed with inline styling. */
isInline: PropTypes.bool,
/** Specifies size of the component. */
size: PropTypes.oneOf(SIZE_CHOICES),
};
FormLabel.defaultProps = {
isInline: false,
size: undefined,
className: undefined,
};
export default FormLabel;

View File

@@ -1,112 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useRadioSetContext } from './FormRadioSetContext';
import { FormGroupContextProvider, useFormGroupContext } from './FormGroupContext';
import FormLabel from './FormLabel';
import FormControlFeedback from './FormControlFeedback';
const RadioControl = React.forwardRef((props, ref) => {
const { getControlProps } = useFormGroupContext();
const { getRadioControlProps, hasRadioSetProvider } = useRadioSetContext();
let radioProps = getControlProps({
...props,
className: classNames('pgn__form-radio-input', props.className),
});
if (hasRadioSetProvider) {
radioProps = getRadioControlProps(radioProps);
}
const onChange = (...args) => {
// eslint-disable-next-line no-console
console.log('RadioControl.onChange called with args: ', args);
if (radioProps.onChange) {
radioProps.onChange(...args);
}
};
return (
<input {...{ ...radioProps, onChange }} type="radio" ref={ref} />
);
});
RadioControl.propTypes = {
className: PropTypes.string,
};
RadioControl.defaultProps = {
className: undefined,
};
const FormRadio = React.forwardRef(({
children,
className,
controlClassName,
labelClassName,
description,
isInvalid,
isValid,
...props
}, ref) => (
<FormGroupContextProvider
controlId={props.id}
isInvalid={isInvalid}
isValid={isValid}
>
<div
className={classNames('pgn__form-radio', className, {
'pgn__form-control-valid': isValid,
'pgn__form-control-invalid': isInvalid,
'pgn__form-control-disabled': props.disabled,
})}
>
<RadioControl ref={ref} className={controlClassName} {...props} />
<div>
<FormLabel className={labelClassName}>
{children}
</FormLabel>
{description && (
<FormControlFeedback hasIcon={false}>
{description}
</FormControlFeedback>
)}
</div>
</div>
</FormGroupContextProvider>
));
FormRadio.propTypes = {
/** Specifies id of the FormRadio component. */
id: PropTypes.string,
/** Specifies contents of the component. */
children: PropTypes.node.isRequired,
/** Specifies class name to append to the base element. */
className: PropTypes.string,
/** Specifies class name for control component. */
controlClassName: PropTypes.string,
/** Specifies class name for label component. */
labelClassName: PropTypes.string,
/** Specifies description to show under the radio's value. */
description: PropTypes.node,
/** Specifies whether to display component in invalid state, this affects styling. */
isInvalid: PropTypes.bool,
/** Specifies whether to display component in valid state, this affects styling. */
isValid: PropTypes.bool,
/** Specifies whether the `FormRadio` is disabled. */
disabled: PropTypes.bool,
};
FormRadio.defaultProps = {
id: undefined,
className: undefined,
controlClassName: undefined,
labelClassName: undefined,
description: undefined,
isInvalid: false,
isValid: false,
disabled: false,
};
export { RadioControl };
export default FormRadio;

View File

@@ -1,68 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useFormGroupContext } from './FormGroupContext';
import { FormRadioSetContextProvider } from './FormRadioSetContext';
import FormControlSet from './FormControlSet';
const FormRadioSet = ({
children,
name,
value,
defaultValue,
isInline,
onChange,
onFocus,
onBlur,
...props
}) => {
const { getControlProps, useSetIsControlGroupEffect } = useFormGroupContext();
useSetIsControlGroupEffect(true);
const controlProps = getControlProps(props);
return (
<FormRadioSetContextProvider
name={name}
value={value}
defaultValue={defaultValue}
onFocus={onFocus}
onBlur={onBlur}
onChange={onChange}
>
<FormControlSet role="radiogroup" isInline={isInline} {...controlProps}>
{children}
</FormControlSet>
</FormRadioSetContextProvider>
);
};
FormRadioSet.propTypes = {
/** Specifies contents of the component. */
children: PropTypes.node.isRequired,
/** A class name to append to the base element. */
className: PropTypes.string,
/** Specifies name for the component. */
name: PropTypes.string.isRequired,
/** Specifies values for the FormRadioSet. */
value: PropTypes.string,
/** Specifies default values. */
defaultValue: PropTypes.string,
/** Specifies whether the component should be displayed with inline styling. */
isInline: PropTypes.bool,
/** Specifies onChange event handler. */
onChange: PropTypes.func,
/** Specifies onFocus event handler. */
onFocus: PropTypes.func,
/** Specifies onBlur event handler. */
onBlur: PropTypes.func,
};
FormRadioSet.defaultProps = {
className: undefined,
value: undefined,
defaultValue: undefined,
isInline: false,
onChange: undefined,
onFocus: undefined,
onBlur: undefined,
};
export default FormRadioSet;

View File

@@ -1,83 +0,0 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { callAllHandlers } from './fieldUtils';
const identityFn = props => props;
const FormRadioSetContext = React.createContext({
getRadioControlProps: identityFn,
hasRadioSetProvider: false,
});
const useRadioSetContext = () => useContext(FormRadioSetContext);
const FormRadioSetContextProvider = ({
children,
name,
onBlur,
onFocus,
onChange,
value,
defaultValue,
}) => {
const handleChange = (...args) => {
// eslint-disable-next-line no-console
console.log('FormRadioSetContextProvider.handleChange called with args: ', args);
onChange(...args);
};
const isControlled = !defaultValue && value !== undefined;
// eslint-disable-next-line no-console
console.log('FormRadioSetContextProvider | isControlled: ', isControlled);
const getRadioControlProps = (radioProps) => ({
...radioProps,
name,
/* istanbul ignore next */
onBlur: radioProps.onBlur ? callAllHandlers(onBlur, radioProps.onBlur) : onBlur,
/* istanbul ignore next */
onFocus: radioProps.onFocus ? callAllHandlers(onFocus, radioProps.onFocus) : onFocus,
/* istanbul ignore next */
onChange: radioProps.onChange ? callAllHandlers(handleChange, radioProps.onChange) : onChange,
checked: isControlled ? value === radioProps.value : undefined,
defaultChecked: isControlled ? undefined : defaultValue === radioProps.value,
});
// eslint-disable-next-line react/jsx-no-constructed-context-values
const contextValue = {
name,
value,
defaultValue,
getRadioControlProps,
onBlur,
onFocus,
onChange,
hasRadioSetProvider: true,
};
return (
<FormRadioSetContext.Provider value={contextValue}>
{children}
</FormRadioSetContext.Provider>
);
};
FormRadioSetContextProvider.propTypes = {
children: PropTypes.node.isRequired,
name: PropTypes.string.isRequired,
onBlur: PropTypes.func,
onFocus: PropTypes.func,
onChange: PropTypes.func,
value: PropTypes.string,
defaultValue: PropTypes.string,
};
FormRadioSetContextProvider.defaultProps = {
onBlur: undefined,
onFocus: undefined,
onChange: undefined,
value: undefined,
defaultValue: undefined,
};
export default FormRadioSetContext;
export {
useRadioSetContext,
FormRadioSetContextProvider,
};

View File

@@ -1,115 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
// import Icon from '../Icon';
// import {
// Check, Close, Cancel, CheckCircle, RadioButtonUnchecked, WarningFilled,
// } from '../../icons';
import { FORM_TEXT_TYPES } from './constants';
// const FORM_TEXT_ICONS = {
// [FORM_TEXT_TYPES.DEFAULT]: null,
// [FORM_TEXT_TYPES.VALID]: Check,
// [FORM_TEXT_TYPES.INVALID]: Close,
// [FORM_TEXT_TYPES.WARNING]: WarningFilled,
// [FORM_TEXT_TYPES.CRITERIA_EMPTY]: RadioButtonUnchecked,
// [FORM_TEXT_TYPES.CRITERIA_VALID]: CheckCircle,
// [FORM_TEXT_TYPES.CRITERIA_INVALID]: Cancel,
// };
const resolveTextType = ({ isInvalid, isValid }) => {
if (isValid) {
return FORM_TEXT_TYPES.VALID;
}
if (isInvalid) {
return FORM_TEXT_TYPES.INVALID;
}
return FORM_TEXT_TYPES.DEFAULT;
};
// const FormTextIcon = ({ type, customIcon }) => {
// if (customIcon) {
// return customIcon;
// }
// const typeIcon = FORM_TEXT_ICONS[type];
// if (typeIcon) {
// return <Icon src={typeIcon} />;
// }
// return null;
// };
// FormTextIcon.propTypes = {
// type: PropTypes.oneOf(Object.values(FORM_TEXT_TYPES)),
// customIcon: PropTypes.node,
// };
// FormTextIcon.defaultProps = {
// type: undefined,
// customIcon: undefined,
// };
const FormText = ({
children, type, icon, muted, hasIcon, ...props
}) => {
const className = classNames(
props.className,
'pgn__form-text',
`pgn__form-text-${type}`,
{
'text-muted': muted,
},
);
return (
<div {...props} className={className}>
{/* {hasIcon && <FormTextIcon customIcon={icon} type={type} />} */}
<div>
{children}
</div>
</div>
);
};
const FORM_TEXT_TYPE_CHOICES = [
'default',
'valid',
'invalid',
'warning',
'criteria-empty',
'criteria-valid',
'criteria-invalid',
];
FormText.propTypes = {
/** Specifies contents of the component. */
children: PropTypes.node.isRequired,
/** Specifies class name to append to the base element. */
className: PropTypes.string,
/** Specifies whether to show an icon next to the text. */
hasIcon: PropTypes.bool,
/** Specifies text type, this affects styling. */
type: PropTypes.oneOf(FORM_TEXT_TYPE_CHOICES),
/** Specifies icon to show, will only be shown if `hasIcon` prop is set to `true`. */
icon: PropTypes.node,
/** Specifies whether to show text with muted styling. */
muted: PropTypes.bool,
};
FormText.defaultProps = {
hasIcon: true,
type: 'default',
icon: undefined,
className: undefined,
muted: false,
};
export default FormText;
export {
FORM_TEXT_TYPES,
// FORM_TEXT_ICONS,
// FormTextIcon,
resolveTextType,
};

View File

@@ -1,215 +0,0 @@
---
title: 'SelectableBox'
type: 'component'
components:
- SelectableBox
- SelectableBoxSet
categories:
- Forms
- Content
status: 'New'
designStatus: 'Done'
devStatus: 'In progress'
notes: |
---
A box that has selection states. It can be used as an alternative to a radio button or checkbox set.
The ``SelectableBox`` can contain any kind of content as long as it is not clickable. In other words, there should be no clickable targets distinct from selection.
## Basic Usage
As ``Checkbox``
```jsx live
() => {
const type = 'checkbox';
const allCheeseOptions = ['swiss', 'cheddar', 'pepperjack'];
const [checkedCheeses, { add, remove, set, clear }] = useCheckboxSetValues(['swiss']);
const handleChange = e => {
e.target.checked ? add(e.target.value) : remove(e.target.value);
};
const isInvalid = () => checkedCheeses.includes('swiss');
const isExtraSmall = useMediaQuery({ maxWidth: breakpoints.extraSmall.maxWidth });
return (
<div className="bg-light-200 p-3">
<SelectableBox.Set
value={checkedCheeses}
type={type}
onChange={handleChange}
name="cheeses"
columns={isExtraSmall ? 1 : 2}
ariaLabel="cheese selection"
>
<SelectableBox value="swiss" type={type} aria-label="swiss checkbox">
<div>
<h3>It is my first SelectableBox</h3>
<p>Swiss</p>
</div>
</SelectableBox>
<SelectableBox value="cheddar" inputHidden={false} type={type} aria-label="cheddar checkbox">
Cheddar
</SelectableBox>
<SelectableBox
value="pepperjack"
inputHidden={false}
type={type}
isInvalid={isInvalid()}
aria-label="pepperjack checkbox"
>
<h3>Pepperjack</h3>
</SelectableBox>
</SelectableBox.Set>
</div>
);
}
```
## As Radio
```jsx live
() => {
const type = 'radio';
const [value, setValue] = useState('green');
const handleChange = e => setValue(e.target.value);
const isExtraSmall = useMediaQuery({ maxWidth: breakpoints.extraSmall.maxWidth });
return (
<SelectableBox.Set
type={type}
value={value}
onChange={handleChange}
name="colors"
columns={isExtraSmall ? 1 : 3}
ariaLabel="color selection"
>
<SelectableBox value="red" type={type} aria-label="red checkbox">
<div>
<h3>It is Red color</h3>
<p>Select me</p>
</div>
</SelectableBox>
<SelectableBox value="green" inputHidden={false} type={type} aria-label="green-checkbox">
<h3>Green</h3>
<p>Leaves and grass</p>
</SelectableBox>
<SelectableBox value="blue" inputHidden={false} type={type} aria-label="blue checkbox">
<h3>Blue</h3>
<p>The sky</p>
</SelectableBox>
</SelectableBox.Set>
);
}
```
## As Checkbox
As ``Checkbox`` with ``isIndeterminate``
```jsx live
() => {
const type = 'checkbox';
const allCheeseOptions = ['swiss', 'cheddar', 'pepperjack'];
const [checkedCheeses, { add, remove, set, clear }] = useCheckboxSetValues(['swiss']);
const allChecked = allCheeseOptions.every(value => checkedCheeses.includes(value));
const someChecked = allCheeseOptions.some(value => checkedCheeses.includes(value));
const isIndeterminate = someChecked && !allChecked;
const isExtraSmall = useMediaQuery({ maxWidth: breakpoints.small.maxWidth });
const handleChange = e => {
e.target.checked ? add(e.target.value) : remove(e.target.value);
};
const handleCheckAllChange = ({ checked }) => {
checked ? set(allCheeseOptions) : clear();
};
return (
<>
<div className="mb-3">
<SelectableBox
checked={allChecked}
isIndeterminate={isIndeterminate}
onClick={handleCheckAllChange}
inputHidden={false}
type={type}
aria-label="all options checkbox"
>
All the cheese
</SelectableBox>
</div>
<SelectableBox.Set
value={checkedCheeses}
type={type}
onChange={handleChange}
name="cheeses"
columns={isExtraSmall ? 1 : 3}
ariaLabel="cheese selection"
>
<SelectableBox value="swiss" type={type} aria-label="swiss checkbox">
<div>
<h3>It is my first SelectableBox</h3>
<p>Swiss</p>
</div>
</SelectableBox>
<SelectableBox value="cheddar" inputHidden={false} type={type} aria-label="cheddar checkbox">
Cheddar
</SelectableBox>
<SelectableBox value="pepperjack" inputHidden={false} type={type} aria-label="pepperjack checkbox">
<h3>Pepperjack</h3>
</SelectableBox>
</SelectableBox.Set>
</>
);
}
```
As ``Checkbox`` with ``ariaLabelledby``
```jsx live
() => {
const type = 'checkbox';
const allCheeseOptions = ['swiss', 'cheddar', 'pepperjack'];
const [checkedCheeses, { add, remove, set, clear }] = useCheckboxSetValues(['swiss']);
const handleChange = e => {
e.target.checked ? add(e.target.value) : remove(e.target.value);
};
const isExtraSmall = useMediaQuery({ maxWidth: breakpoints.extraSmall.maxWidth });
return (
<div className="bg-light-200 p-3">
<h3 id="cheese selection" className="mb-4">
Select your favorite cheese
</h3>
<SelectableBox.Set
value={checkedCheeses}
type={type}
onChange={handleChange}
name="cheeses"
columns={isExtraSmall ? 1 : 3}
ariaLabelledby="cheese selection"
>
<SelectableBox value="swiss" inputHidden={false} type={type} aria-label="swiss checkbox">
<h3>
Swiss
</h3>
</SelectableBox>
<SelectableBox value="cheddar" inputHidden={false} type={type} aria-label="cheddar checkbox">
<h3>
Cheddar
</h3>
</SelectableBox>
<SelectableBox value="pepperjack" inputHidden={false} type={type} aria-label="pepperjack checkbox">
<h3>
Pepperjack
</h3>
</SelectableBox>
</SelectableBox.Set>
</div>
);
}
```

View File

@@ -1,98 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { getInputType } from './utils';
// import { requiredWhenNot } from '../utils/propTypes';
const INPUT_TYPES = [
'radio',
'checkbox',
];
const DEFAULT_COLUMNS_NUMBER = 2;
const SelectableBoxSet = React.forwardRef(({
children,
name,
value,
defaultValue,
onChange,
type,
columns,
className,
ariaLabel,
ariaLabelledby,
...props
}, ref) => {
const inputType = getInputType('SelectableBoxSet', type);
return React.createElement(
inputType,
{
name,
value,
defaultValue,
onChange,
ref,
className: classNames(
'pgn__selectable_box-set',
`pgn__selectable_box-set--${columns || DEFAULT_COLUMNS_NUMBER}`,
className,
),
'aria-label': ariaLabel,
'aria-labelledby': ariaLabelledby,
...props,
},
children,
);
});
SelectableBoxSet.propTypes = {
/** Specifies a name for the group of `SelectableBox`'es. */
name: PropTypes.string.isRequired,
/** Content of the `SelectableBoxSet`. */
children: PropTypes.node,
/** A function that receives event of the clicked `SelectableBox` and can be used to handle the value change. */
onChange: PropTypes.func,
/** Indicates selected `SelectableBox`'es. */
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array]),
/** Specifies default values for the `SelectableBox`'es. */
defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
/** Indicates the input type: checkbox or radio. */
type: PropTypes.oneOf(INPUT_TYPES),
/**
* Specifies number of `SelectableBox`'es in a row.
*
* Class that is responsible for the columns number: `pgn__selectable_box-set--{columns}`.
* Max number of columns: `12`.
*/
columns: PropTypes.number,
/** A class that is be appended to the base element. */
className: PropTypes.string,
/**
* The ID of the label for the `SelectableBoxSet`.
*
* An accessible label must be provided to the `SelectableBoxSet`.
*/
ariaLabelledby: PropTypes.string,
/**
* A label for the `SelectableBoxSet`.
*
* If not using `ariaLabelledby`, then `ariaLabel` must be provided */
// eslint-disable-next-line react/forbid-prop-types
ariaLabel: PropTypes.any, // requiredWhenNot(PropTypes.string, 'ariaLabelledby'),
};
SelectableBoxSet.defaultProps = {
children: undefined,
onChange: () => {},
value: undefined,
defaultValue: undefined,
type: 'radio',
columns: DEFAULT_COLUMNS_NUMBER,
className: undefined,
ariaLabelledby: undefined,
ariaLabel: undefined,
};
export default SelectableBoxSet;

View File

@@ -1,5 +0,0 @@
$selectable-box-padding: 1rem !default;
$selectable-box-border-radius: .25rem !default;
$selectable-box-space: .75rem !default;
$min-cols-number: 1 !default;
$max-cols-number: 12 !default;

View File

@@ -1,17 +0,0 @@
/* eslint-disable import/prefer-default-export */
const FORM_CONTROL_SIZES = {
SMALL: 'sm',
LARGE: 'lg',
};
const FORM_TEXT_TYPES = {
DEFAULT: 'default',
VALID: 'valid',
INVALID: 'invalid',
WARNING: 'warning',
CRITERIA_EMPTY: 'criteria-empty',
CRITERIA_VALID: 'criteria-valid',
CRITERIA_INVALID: 'criteria-invalid',
};
export { FORM_CONTROL_SIZES, FORM_TEXT_TYPES };

View File

@@ -1,70 +0,0 @@
import classNames from 'classnames';
import { useState, useEffect } from 'react';
import newId from './newId';
const omitUndefinedProperties = (obj = {}) => Object.entries(obj)
.reduce((acc, [key, value]) => {
if (value !== undefined) {
acc[key] = value;
}
return acc;
}, {});
const callAllHandlers = (...handlers) => {
const unifiedEventHandler = (event) => {
handlers
.filter(handler => typeof handler === 'function')
.forEach(handler => handler(event));
};
return unifiedEventHandler;
};
const useHasValue = ({ defaultValue, value }) => {
const [hasUncontrolledValue, setHasUncontrolledValue] = useState(!!defaultValue || defaultValue === 0);
const hasValue = !!value || value === 0 || hasUncontrolledValue;
const handleInputEvent = (e) => setHasUncontrolledValue(e.target.value);
return [hasValue, handleInputEvent];
};
const useIdList = (uniqueIdPrefix, initialList) => {
const [idList, setIdList] = useState(initialList || []);
const addId = (idToAdd) => {
setIdList(oldIdList => [...oldIdList, idToAdd]);
return idToAdd;
};
const getNewId = () => {
const idToAdd = newId(`${uniqueIdPrefix}-`);
return addId(idToAdd);
};
const removeId = (idToRemove) => {
setIdList(oldIdList => oldIdList.filter(id => id !== idToRemove));
};
const useRegisteredId = (explicitlyRegisteredId) => {
const [registeredId, setRegisteredId] = useState(explicitlyRegisteredId);
useEffect(() => {
if (explicitlyRegisteredId) {
addId(explicitlyRegisteredId);
} else if (!registeredId) {
setRegisteredId(getNewId(uniqueIdPrefix));
}
return () => removeId(registeredId);
}, [registeredId, explicitlyRegisteredId]);
return registeredId;
};
return [idList, useRegisteredId];
};
const mergeAttributeValues = (...values) => {
const mergedValues = classNames(values);
return mergedValues || undefined;
};
export {
callAllHandlers,
useHasValue,
mergeAttributeValues,
useIdList,
omitUndefinedProperties,
};

View File

@@ -1,115 +0,0 @@
import React, { useRef, useEffect } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import SelectableBoxSet from './SelectableBoxSet';
import { useCheckboxSetContext } from './FormCheckboxSetContext';
import { useRadioSetContext } from './FormRadioSetContext';
import { getInputType } from './utils';
const INPUT_TYPES = [
'radio',
'checkbox',
];
const SelectableBox = React.forwardRef(({
type,
value,
checked,
children,
isIndeterminate,
isInvalid,
onClick,
onFocus,
inputHidden,
className,
...props
}, ref) => {
const inputType = getInputType('SelectableBox', type);
const { value: radioValue } = useRadioSetContext();
const { value: checkboxValues = [] } = useCheckboxSetContext();
const isChecked = () => {
switch (type) {
case 'radio':
return radioValue === value;
case 'checkbox':
return checkboxValues.includes(value);
default:
return radioValue === value;
}
};
const inputRef = useRef(null);
const input = React.createElement(inputType, {
value,
checked,
hidden: inputHidden,
ref: inputRef,
tabIndex: -1,
onChange: () => {},
...(type === 'checkbox' ? { ...props, isIndeterminate } : { ...props }),
}, null);
useEffect(() => {
if (onClick && inputRef.current) {
inputRef.current.onclick = () => onClick(inputRef.current);
}
}, [onClick]);
return (
<div
role="button"
onKeyPress={() => inputRef.current.click()}
onClick={() => inputRef.current.click()}
onFocus={onFocus}
className={classNames('pgn__selectable_box', className, {
'pgn__selectable_box-active': isChecked() || checked,
'pgn__selectable_box-invalid': isInvalid,
})}
tabIndex={0}
ref={ref}
{...props}
>
{input}
{children}
</div>
);
});
SelectableBox.propTypes = {
/** Content of the `SelectableBox`. */
children: PropTypes.node.isRequired,
/** A value that is passed to the input tag. */
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
/** Controls whether `SelectableBox` is checked. */
checked: PropTypes.bool,
/** Indicates the input type: checkbox or radio. */
type: PropTypes.oneOf(INPUT_TYPES),
/** Function that is called when the `SelectableBox` is clicked. */
onClick: PropTypes.func,
/** Function that is called when the `SelectableBox` is focused. */
onFocus: PropTypes.func,
/** Controls display of the input (checkbox or radio button) on the `SelectableBox`. */
inputHidden: PropTypes.bool,
/** Indicates a state for the 'checkbox' `type` when `SelectableBox` is neither checked nor unchecked. */
isIndeterminate: PropTypes.bool,
/** Adds errors styles to the `SelectableBox`. */
isInvalid: PropTypes.bool,
/** A class that is appended to the base element. */
className: PropTypes.string,
};
SelectableBox.defaultProps = {
value: undefined,
checked: false,
type: 'radio',
onClick: () => {},
onFocus: () => {},
inputHidden: true,
isIndeterminate: false,
isInvalid: false,
className: undefined,
};
SelectableBox.Set = SelectableBoxSet;
export default SelectableBox;

View File

@@ -1,54 +0,0 @@
@import "variables";
.pgn__selectable_box-set {
display: grid;
grid-auto-rows: 1fr;
grid-gap: $selectable-box-space;
@for $i from $min-cols-number through $max-cols-number {
&.pgn__selectable_box-set--#{$i} {
grid-template-columns: repeat(#{$i}, 1fr);
}
}
& > * + * {
margin: 0;
}
}
.pgn__selectable_box {
position: relative;
height: 100%;
padding: $selectable-box-padding;
box-shadow: $level-1-box-shadow;
border-radius: $selectable-box-border-radius;
text-align: start;
background: $white;
&:focus-visible {
outline: 1px solid $primary-700;
}
.pgn__form-radio,
.pgn__form-checkbox {
position: absolute;
top: $selectable-box-padding;
inset-inline-end: $selectable-box-padding;
input {
margin-inline-end: 0;
}
}
* {
pointer-events: none;
}
}
.pgn__selectable_box-active {
outline: 2px solid $primary-500;
}
.pgn__selectable_box-invalid {
outline: 2px solid $danger-300;
}

View File

@@ -1,8 +0,0 @@
let lastId = 0;
const newId = (prefix = 'id') => {
lastId += 1;
return `${prefix}${lastId}`;
};
export default newId;

View File

@@ -1,124 +0,0 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import renderer from 'react-test-renderer';
import SelectableBox from '..';
const checkboxType = 'checkbox';
const checkboxText = 'SelectableCheckbox';
const radioType = 'radio';
const radioText = 'SelectableRadio';
const SelectableCheckbox = (props) => <SelectableBox type={checkboxType} {...props}>{checkboxText}</SelectableBox>;
const SelectableRadio = (props) => <SelectableBox type={radioType} {...props}>{radioText}</SelectableBox>;
describe('<SelectableBox />', () => {
describe('correct rendering', () => {
it('renders without props', () => {
const tree = renderer.create((
<SelectableBox>SelectableBox</SelectableBox>
)).toJSON();
expect(tree).toMatchSnapshot();
});
it('correct render when type prop is changed', () => {
const { rerender } = render(<SelectableRadio type="checkbox" />);
const checkboxControl = screen.getByText(radioText);
expect(checkboxControl).toBeTruthy();
rerender(<SelectableRadio type="radio" />);
const radioControl = screen.getByText(radioText);
expect(radioControl).toBeTruthy();
});
it('renders with radio input type if neither checkbox nor radio is passed', () => {
// Mock the `console.error` is intentional because an invalid `type` prop
// with `wrongType` specified for `ForwardRef` expects one of the ['radio','flag'] parameters.
// eslint-disable-next-line no-console
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
render(<SelectableRadio type="wrongType" />);
const selectableBox = screen.getByRole('button');
expect(selectableBox).toBeTruthy();
consoleErrorSpy.mockRestore();
});
it('renders with checkbox input type', () => {
render(<SelectableCheckbox />);
const selectableBox = screen.getByRole('button');
expect(selectableBox).toBeTruthy();
});
it('renders with radio input type', () => {
render(<SelectableCheckbox />);
const selectableBox = screen.getByRole('button');
expect(selectableBox).toBeTruthy();
});
it('renders with correct children', () => {
render(<SelectableRadio />);
const selectableBox = screen.getByText(radioText);
expect(selectableBox).toBeTruthy();
});
it('renders with correct class', () => {
const className = 'myClass';
render(<SelectableRadio className={className} />);
const selectableBox = screen.getByRole('button');
expect(selectableBox.classList.contains(className)).toEqual(true);
});
it('renders as active when checked is passed', () => {
render(<SelectableRadio checked />);
const selectableBox = screen.getByRole('button');
const inputElement = screen.getByRole('radio', { hidden: true });
expect(selectableBox.classList.contains('pgn__selectable_box-active')).toEqual(true);
expect(inputElement.checked).toEqual(true);
});
it('renders as invalid when isInvalid is passed', () => {
render(<SelectableRadio isInvalid />);
const selectableBox = screen.getByRole('button');
expect(selectableBox.classList.contains('pgn__selectable_box-invalid')).toEqual(true);
});
it('renders with on click event when onClick is passed', async () => {
const onClickSpy = jest.fn();
render(<SelectableCheckbox onClick={onClickSpy} />);
const selectableBox = screen.getByRole('button');
await userEvent.click(selectableBox);
expect(onClickSpy).toHaveBeenCalledTimes(1);
});
it('renders with on key press event when onClick is passed', async () => {
const onClickSpy = jest.fn();
render(<SelectableCheckbox onClick={onClickSpy} />);
const selectableBox = screen.getByRole('button');
selectableBox.focus();
await userEvent.keyboard('{enter}');
expect(onClickSpy).toHaveBeenCalledTimes(1);
});
it('renders with hidden input when inputHidden is passed', () => {
const { rerender } = render(<SelectableCheckbox inputHidden />);
const inputElement = screen.getByRole('checkbox', { hidden: true });
expect(inputElement.getAttribute('hidden')).toEqual('');
rerender(<SelectableCheckbox inputHidden={false} />);
expect(inputElement.getAttribute('hidden')).toBeNull();
});
});
describe('correct interactions', () => {
it('correct checkbox state change when checked is changed', () => {
const { rerender } = render(<SelectableCheckbox checked={false} />);
const checkbox = screen.getByRole('button');
expect(checkbox.className).not.toContain('pgn__selectable_box-active');
rerender(<SelectableCheckbox checked />);
expect(checkbox.className).toContain('pgn__selectable_box-active');
});
it('correct radio state change when checked is changed', () => {
const { rerender } = render(<SelectableRadio checked={false} />);
const radio = screen.getByRole('button');
expect(radio.className).toContain('pgn__selectable_box-active');
rerender(<SelectableRadio checked />);
expect(radio.className).toContain('pgn__selectable_box-active');
});
it('ref is passed to onClick function', () => {
let inputRef;
const onClick = (ref) => { inputRef = ref; };
render(<SelectableRadio onClick={onClick} />);
const radio = screen.getByRole('button');
userEvent.click(radio);
expect(inputRef).not.toBeFalsy();
});
});
});

View File

@@ -1,107 +0,0 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import userEvent from '@testing-library/user-event';
import SelectableBox from '..';
const boxText = (text) => `SelectableBox${text}`;
const checkboxType = 'checkbox';
const checkboxText = (text) => `SelectableCheckbox${text}`;
const radioType = 'radio';
const radioText = (text) => `SelectableRadio${text}`;
const ariaLabel = 'test-default-label';
const SelectableBoxSet = (props) => (
<SelectableBox.Set name="box" ariaLabel={ariaLabel} {...props}>
<SelectableBox value={1}>{boxText(1)}</SelectableBox>
<SelectableBox value={2}>{boxText(2)}</SelectableBox>
<SelectableBox value={3}>{boxText(3)}</SelectableBox>
</SelectableBox.Set>
);
const SelectableCheckboxSet = (props) => (
<SelectableBox.Set name={radioType} type={checkboxType} ariaLabel={ariaLabel} {...props}>
<SelectableBox value={1} type={checkboxType}>{checkboxText(1)}</SelectableBox>
<SelectableBox value={2} type={checkboxType}>{checkboxText(2)}</SelectableBox>
<SelectableBox value={3} type={checkboxType}>{checkboxText(3)}</SelectableBox>
</SelectableBox.Set>
);
const SelectableRadioSet = (props) => (
<SelectableBox.Set name={radioType} type={radioType} ariaLabel={ariaLabel} {...props}>
<SelectableBox value={1} type={radioType}>{radioText(1)}</SelectableBox>
<SelectableBox value={2} type={radioType}>{radioText(2)}</SelectableBox>
<SelectableBox value={3} type={radioType}>{radioText(3)}</SelectableBox>
</SelectableBox.Set>
);
describe('<SelectableBox.Set />', () => {
describe('correct rendering', () => {
it('renders without props', () => {
const { container } = render(<SelectableRadioSet name="testName" />);
expect(container).toMatchSnapshot();
});
it('forwards props', () => {
render((<SelectableRadioSet name="testName" data-testid="test-radio-set-name" />));
expect(screen.getByTestId('test-radio-set-name')).toBeInTheDocument();
});
it('correct render when type prop is changed', () => {
const { rerender } = render(<SelectableRadioSet name="set" data-testid="radio-set" />);
expect(screen.getByTestId('radio-set')).toBeInTheDocument();
rerender(<SelectableRadioSet name="set" type="radio" data-testid="radio-set" />);
expect(screen.getByTestId('radio-set')).toBeInTheDocument();
rerender(<SelectableRadioSet name="set" type="checkbox" data-testid="checkbox-set" />);
expect(screen.getByTestId('checkbox-set')).toBeInTheDocument();
});
it('renders with children', () => {
render(
<SelectableCheckboxSet name="testName">{checkboxText(1)}</SelectableCheckboxSet>,
);
expect(screen.getByText(checkboxText(1))).toBeInTheDocument();
});
it('renders with on change event', async () => {
const onChangeSpy = jest.fn();
render(<SelectableCheckboxSet onChange={onChangeSpy} />);
const checkbox = screen.getByRole('button', { name: checkboxText(1) });
await userEvent.click(checkbox);
expect(onChangeSpy).toHaveBeenCalledTimes(1);
});
it('renders with checkbox type', () => {
render(<SelectableCheckboxSet data-testid="checkbox-set" />);
expect(screen.getByTestId('checkbox-set')).toBeInTheDocument();
});
it('renders with radio type if neither checkbox nor radio is passed', () => {
render(<SelectableBoxSet data-testid="radio-set" />);
expect(screen.getByTestId('radio-set')).toBeInTheDocument();
});
it('renders with radio type', () => {
render(<SelectableRadioSet type={radioType} data-testid="radio-set" />);
expect(screen.getByTestId('radio-set')).toBeInTheDocument();
});
it('renders with correct number of columns', () => {
const columns = 10;
render(<SelectableRadioSet columns={columns} data-testid="selectable-box-set" />);
const selectableBoxSet = screen.getByTestId('selectable-box-set');
expect(selectableBoxSet).toHaveClass(`pgn__selectable_box-set--${columns}`);
});
it('renders with an aria-label attribute', () => {
render((<SelectableRadioSet name="testName" ariaLabel="test-radio-set-label" />));
expect(screen.getByLabelText('test-radio-set-label')).toBeInTheDocument();
});
it('renders with an aria-labelledby attribute', () => {
render((
<>
<h2 id="test-radio-set-label">Radio Set Label text</h2>
<SelectableRadioSet
name="testName"
ariaLabelledby="test-radio-set-label"
/>
</>
));
expect(screen.getByLabelText('Radio Set Label text')).toBeInTheDocument();
});
});
});

View File

@@ -1,22 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<SelectableBox /> correct rendering renders without props 1`] = `
<div
className="pgn__selectable_box pgn__selectable_box-active"
onClick={[Function]}
onFocus={[Function]}
onKeyPress={[Function]}
role="button"
tabIndex={0}
>
<input
checked={false}
className="pgn__form-radio-input"
hidden={true}
onChange={[Function]}
tabIndex={-1}
type="radio"
/>
SelectableBox
</div>
`;

View File

@@ -1,57 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<SelectableBox.Set /> correct rendering renders without props 1`] = `
<div>
<div
aria-label="test-default-label"
class="pgn__selectable_box-set pgn__selectable_box-set--2 pgn__form-control-set"
role="radiogroup"
>
<div
class="pgn__selectable_box"
role="button"
tabindex="0"
>
<input
class="pgn__form-radio-input"
hidden=""
name="testName"
tabindex="-1"
type="radio"
value="1"
/>
SelectableRadio1
</div>
<div
class="pgn__selectable_box"
role="button"
tabindex="0"
>
<input
class="pgn__form-radio-input"
hidden=""
name="testName"
tabindex="-1"
type="radio"
value="2"
/>
SelectableRadio2
</div>
<div
class="pgn__selectable_box"
role="button"
tabindex="0"
>
<input
class="pgn__form-radio-input"
hidden=""
name="testName"
tabindex="-1"
type="radio"
value="3"
/>
SelectableRadio3
</div>
</div>
</div>
`;

View File

@@ -1,27 +0,0 @@
import { CheckboxControl } from './FormCheckbox';
import { RadioControl } from './FormRadio';
import FormRadioSet from './FormRadioSet';
import FormCheckboxSet from './FormCheckboxSet';
// eslint-disable-next-line import/prefer-default-export,consistent-return
export const getInputType = (component, type) => {
if (component === 'SelectableBox') {
switch (type) {
case 'radio':
return RadioControl;
case 'checkbox':
return CheckboxControl;
default:
return RadioControl;
}
} else if (component === 'SelectableBoxSet') {
switch (type) {
case 'radio':
return FormRadioSet;
case 'checkbox':
return FormCheckboxSet;
default:
return FormRadioSet;
}
}
};