Fix Problem Type Select (#460)
* fix: override selectablebox with copy of version from edx paragon 21.5.6 * chore: remove console logs * fix: lint * chore: update snapshots
This commit is contained in:
@@ -3,9 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { Button, Container } from '@openedx/paragon';
|
||||
import { FormattedMessage, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
/* SelectableBox from Paragon currently has a bug as it misses a ContextProvider.
|
||||
This is a temporary fix by overriding it with our own copy that includes the ContextProvider. */
|
||||
// TODO: Replace this import with a paragon import once the bug is fixed and delete our copy of SelectableBox.
|
||||
// SelectableBox in paragon has a bug only visible on stage where you can't change selection. So we override it
|
||||
import SelectableBox from '../../../../../sharedComponents/SelectableBox';
|
||||
import { ProblemTypes, ProblemTypeKeys, AdvanceProblemKeys } from '../../../../../data/constants/problem';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -25,7 +25,6 @@ exports[`ProblemTypeSelect snapshot DROPDOWN 1`] = `
|
||||
isInvalid={false}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
showActiveBoxState={true}
|
||||
type="radio"
|
||||
value="multiplechoiceresponse"
|
||||
>
|
||||
@@ -41,7 +40,6 @@ exports[`ProblemTypeSelect snapshot DROPDOWN 1`] = `
|
||||
isInvalid={false}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
showActiveBoxState={true}
|
||||
type="radio"
|
||||
value="choiceresponse"
|
||||
>
|
||||
@@ -57,7 +55,6 @@ exports[`ProblemTypeSelect snapshot DROPDOWN 1`] = `
|
||||
isInvalid={false}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
showActiveBoxState={true}
|
||||
type="radio"
|
||||
value="optionresponse"
|
||||
>
|
||||
@@ -73,7 +70,6 @@ exports[`ProblemTypeSelect snapshot DROPDOWN 1`] = `
|
||||
isInvalid={false}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
showActiveBoxState={true}
|
||||
type="radio"
|
||||
value="numericalresponse"
|
||||
>
|
||||
@@ -89,7 +85,6 @@ exports[`ProblemTypeSelect snapshot DROPDOWN 1`] = `
|
||||
isInvalid={false}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
showActiveBoxState={true}
|
||||
type="radio"
|
||||
value="stringresponse"
|
||||
>
|
||||
@@ -135,7 +130,6 @@ exports[`ProblemTypeSelect snapshot MULTISELECT 1`] = `
|
||||
isInvalid={false}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
showActiveBoxState={true}
|
||||
type="radio"
|
||||
value="multiplechoiceresponse"
|
||||
>
|
||||
@@ -151,7 +145,6 @@ exports[`ProblemTypeSelect snapshot MULTISELECT 1`] = `
|
||||
isInvalid={false}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
showActiveBoxState={true}
|
||||
type="radio"
|
||||
value="choiceresponse"
|
||||
>
|
||||
@@ -167,7 +160,6 @@ exports[`ProblemTypeSelect snapshot MULTISELECT 1`] = `
|
||||
isInvalid={false}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
showActiveBoxState={true}
|
||||
type="radio"
|
||||
value="optionresponse"
|
||||
>
|
||||
@@ -183,7 +175,6 @@ exports[`ProblemTypeSelect snapshot MULTISELECT 1`] = `
|
||||
isInvalid={false}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
showActiveBoxState={true}
|
||||
type="radio"
|
||||
value="numericalresponse"
|
||||
>
|
||||
@@ -199,7 +190,6 @@ exports[`ProblemTypeSelect snapshot MULTISELECT 1`] = `
|
||||
isInvalid={false}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
showActiveBoxState={true}
|
||||
type="radio"
|
||||
value="stringresponse"
|
||||
>
|
||||
@@ -245,7 +235,6 @@ exports[`ProblemTypeSelect snapshot NUMERIC 1`] = `
|
||||
isInvalid={false}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
showActiveBoxState={true}
|
||||
type="radio"
|
||||
value="multiplechoiceresponse"
|
||||
>
|
||||
@@ -261,7 +250,6 @@ exports[`ProblemTypeSelect snapshot NUMERIC 1`] = `
|
||||
isInvalid={false}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
showActiveBoxState={true}
|
||||
type="radio"
|
||||
value="choiceresponse"
|
||||
>
|
||||
@@ -277,7 +265,6 @@ exports[`ProblemTypeSelect snapshot NUMERIC 1`] = `
|
||||
isInvalid={false}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
showActiveBoxState={true}
|
||||
type="radio"
|
||||
value="optionresponse"
|
||||
>
|
||||
@@ -293,7 +280,6 @@ exports[`ProblemTypeSelect snapshot NUMERIC 1`] = `
|
||||
isInvalid={false}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
showActiveBoxState={true}
|
||||
type="radio"
|
||||
value="numericalresponse"
|
||||
>
|
||||
@@ -309,7 +295,6 @@ exports[`ProblemTypeSelect snapshot NUMERIC 1`] = `
|
||||
isInvalid={false}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
showActiveBoxState={true}
|
||||
type="radio"
|
||||
value="stringresponse"
|
||||
>
|
||||
@@ -355,7 +340,6 @@ exports[`ProblemTypeSelect snapshot SINGLESELECT 1`] = `
|
||||
isInvalid={false}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
showActiveBoxState={true}
|
||||
type="radio"
|
||||
value="multiplechoiceresponse"
|
||||
>
|
||||
@@ -371,7 +355,6 @@ exports[`ProblemTypeSelect snapshot SINGLESELECT 1`] = `
|
||||
isInvalid={false}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
showActiveBoxState={true}
|
||||
type="radio"
|
||||
value="choiceresponse"
|
||||
>
|
||||
@@ -387,7 +370,6 @@ exports[`ProblemTypeSelect snapshot SINGLESELECT 1`] = `
|
||||
isInvalid={false}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
showActiveBoxState={true}
|
||||
type="radio"
|
||||
value="optionresponse"
|
||||
>
|
||||
@@ -403,7 +385,6 @@ exports[`ProblemTypeSelect snapshot SINGLESELECT 1`] = `
|
||||
isInvalid={false}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
showActiveBoxState={true}
|
||||
type="radio"
|
||||
value="numericalresponse"
|
||||
>
|
||||
@@ -419,7 +400,6 @@ exports[`ProblemTypeSelect snapshot SINGLESELECT 1`] = `
|
||||
isInvalid={false}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
showActiveBoxState={true}
|
||||
type="radio"
|
||||
value="stringresponse"
|
||||
>
|
||||
@@ -465,7 +445,6 @@ exports[`ProblemTypeSelect snapshot TEXTINPUT 1`] = `
|
||||
isInvalid={false}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
showActiveBoxState={true}
|
||||
type="radio"
|
||||
value="multiplechoiceresponse"
|
||||
>
|
||||
@@ -481,7 +460,6 @@ exports[`ProblemTypeSelect snapshot TEXTINPUT 1`] = `
|
||||
isInvalid={false}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
showActiveBoxState={true}
|
||||
type="radio"
|
||||
value="choiceresponse"
|
||||
>
|
||||
@@ -497,7 +475,6 @@ exports[`ProblemTypeSelect snapshot TEXTINPUT 1`] = `
|
||||
isInvalid={false}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
showActiveBoxState={true}
|
||||
type="radio"
|
||||
value="optionresponse"
|
||||
>
|
||||
@@ -513,7 +490,6 @@ exports[`ProblemTypeSelect snapshot TEXTINPUT 1`] = `
|
||||
isInvalid={false}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
showActiveBoxState={true}
|
||||
type="radio"
|
||||
value="numericalresponse"
|
||||
>
|
||||
@@ -529,7 +505,6 @@ exports[`ProblemTypeSelect snapshot TEXTINPUT 1`] = `
|
||||
isInvalid={false}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
showActiveBoxState={true}
|
||||
type="radio"
|
||||
value="stringresponse"
|
||||
>
|
||||
|
||||
145
src/editors/sharedComponents/SelectableBox/FormCheckbox.jsx
Normal file
145
src/editors/sharedComponents/SelectableBox/FormCheckbox.jsx
Normal file
@@ -0,0 +1,145 @@
|
||||
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;
|
||||
@@ -0,0 +1,68 @@
|
||||
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;
|
||||
@@ -1,14 +1,6 @@
|
||||
import React, { useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const callAllHandlers = (...handlers) => {
|
||||
const unifiedEventHandler = (event) => {
|
||||
handlers
|
||||
.filter(handler => typeof handler === 'function')
|
||||
.forEach(handler => handler(event));
|
||||
};
|
||||
return unifiedEventHandler;
|
||||
};
|
||||
import { callAllHandlers } from './fieldUtils';
|
||||
|
||||
const identityFn = props => props;
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
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;
|
||||
@@ -0,0 +1,40 @@
|
||||
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;
|
||||
121
src/editors/sharedComponents/SelectableBox/FormGroupContext.jsx
Normal file
121
src/editors/sharedComponents/SelectableBox/FormGroupContext.jsx
Normal file
@@ -0,0 +1,121 @@
|
||||
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,
|
||||
};
|
||||
42
src/editors/sharedComponents/SelectableBox/FormLabel.jsx
Normal file
42
src/editors/sharedComponents/SelectableBox/FormLabel.jsx
Normal file
@@ -0,0 +1,42 @@
|
||||
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;
|
||||
110
src/editors/sharedComponents/SelectableBox/FormRadio.jsx
Normal file
110
src/editors/sharedComponents/SelectableBox/FormRadio.jsx
Normal file
@@ -0,0 +1,110 @@
|
||||
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) => {
|
||||
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;
|
||||
68
src/editors/sharedComponents/SelectableBox/FormRadioSet.jsx
Normal file
68
src/editors/sharedComponents/SelectableBox/FormRadioSet.jsx
Normal file
@@ -0,0 +1,68 @@
|
||||
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;
|
||||
@@ -1,14 +1,6 @@
|
||||
import React, { useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const callAllHandlers = (...handlers) => {
|
||||
const unifiedEventHandler = (event) => {
|
||||
handlers
|
||||
.filter(handler => typeof handler === 'function')
|
||||
.forEach(handler => handler(event));
|
||||
};
|
||||
return unifiedEventHandler;
|
||||
};
|
||||
import { callAllHandlers } from './fieldUtils';
|
||||
|
||||
const identityFn = props => props;
|
||||
|
||||
@@ -28,6 +20,9 @@ const FormRadioSetContextProvider = ({
|
||||
value,
|
||||
defaultValue,
|
||||
}) => {
|
||||
const handleChange = (...args) => {
|
||||
onChange(...args);
|
||||
};
|
||||
const isControlled = !defaultValue && value !== undefined;
|
||||
const getRadioControlProps = (radioProps) => ({
|
||||
...radioProps,
|
||||
@@ -37,7 +32,7 @@ const FormRadioSetContextProvider = ({
|
||||
/* istanbul ignore next */
|
||||
onFocus: radioProps.onFocus ? callAllHandlers(onFocus, radioProps.onFocus) : onFocus,
|
||||
/* istanbul ignore next */
|
||||
onChange: radioProps.onChange ? callAllHandlers(onChange, radioProps.onChange) : onChange,
|
||||
onChange: radioProps.onChange ? callAllHandlers(handleChange, radioProps.onChange) : onChange,
|
||||
checked: isControlled ? value === radioProps.value : undefined,
|
||||
defaultChecked: isControlled ? undefined : defaultValue === radioProps.value,
|
||||
});
|
||||
|
||||
115
src/editors/sharedComponents/SelectableBox/FormText.jsx
Normal file
115
src/editors/sharedComponents/SelectableBox/FormText.jsx
Normal file
@@ -0,0 +1,115 @@
|
||||
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,
|
||||
};
|
||||
215
src/editors/sharedComponents/SelectableBox/README.md
Normal file
215
src/editors/sharedComponents/SelectableBox/README.md
Normal file
@@ -0,0 +1,215 @@
|
||||
---
|
||||
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>
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { getInputType } from './getInputType';
|
||||
import { FormRadioSetContextProvider } from './FormRadioSetContext';
|
||||
import { getInputType } from './utils';
|
||||
// import { requiredWhenNot } from '../utils/propTypes';
|
||||
|
||||
const INPUT_TYPES = [
|
||||
'radio',
|
||||
@@ -26,7 +26,7 @@ const SelectableBoxSet = React.forwardRef(({
|
||||
}, ref) => {
|
||||
const inputType = getInputType('SelectableBoxSet', type);
|
||||
|
||||
const selectableBoxElement = React.createElement(
|
||||
return React.createElement(
|
||||
inputType,
|
||||
{
|
||||
name,
|
||||
@@ -45,22 +45,6 @@ const SelectableBoxSet = React.forwardRef(({
|
||||
},
|
||||
children,
|
||||
);
|
||||
|
||||
return type === 'radio' ? (
|
||||
<FormRadioSetContextProvider
|
||||
{...{
|
||||
children,
|
||||
name,
|
||||
onChange,
|
||||
value,
|
||||
defaultValue,
|
||||
}}
|
||||
>
|
||||
{
|
||||
selectableBoxElement
|
||||
}
|
||||
</FormRadioSetContextProvider>
|
||||
) : selectableBoxElement;
|
||||
});
|
||||
|
||||
SelectableBoxSet.propTypes = {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
$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;
|
||||
17
src/editors/sharedComponents/SelectableBox/constants.js
Normal file
17
src/editors/sharedComponents/SelectableBox/constants.js
Normal file
@@ -0,0 +1,17 @@
|
||||
/* 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 };
|
||||
70
src/editors/sharedComponents/SelectableBox/fieldUtils.js
Normal file
70
src/editors/sharedComponents/SelectableBox/fieldUtils.js
Normal file
@@ -0,0 +1,70 @@
|
||||
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,
|
||||
};
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { useRef, useEffect, useContext } from 'react';
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import SelectableBoxSet from './SelectableBoxSet';
|
||||
import { getInputType } from './getInputType';
|
||||
import FormCheckboxSetContext from './FormCheckboxSetContext';
|
||||
import FormRadioSetContext from './FormRadioSetContext';
|
||||
import { useCheckboxSetContext } from './FormCheckboxSetContext';
|
||||
import { useRadioSetContext } from './FormRadioSetContext';
|
||||
import { getInputType } from './utils';
|
||||
|
||||
const INPUT_TYPES = [
|
||||
'radio',
|
||||
@@ -22,12 +22,11 @@ const SelectableBox = React.forwardRef(({
|
||||
onFocus,
|
||||
inputHidden,
|
||||
className,
|
||||
showActiveBoxState,
|
||||
...props
|
||||
}, ref) => {
|
||||
const inputType = getInputType('SelectableBox', type);
|
||||
const { value: radioValue } = useContext(FormRadioSetContext);
|
||||
const { value: checkboxValues = [] } = useContext(FormCheckboxSetContext);
|
||||
const { value: radioValue } = useRadioSetContext();
|
||||
const { value: checkboxValues = [] } = useCheckboxSetContext();
|
||||
|
||||
const isChecked = () => {
|
||||
switch (type) {
|
||||
@@ -64,7 +63,7 @@ const SelectableBox = React.forwardRef(({
|
||||
onClick={() => inputRef.current.click()}
|
||||
onFocus={onFocus}
|
||||
className={classNames('pgn__selectable_box', className, {
|
||||
'pgn__selectable_box-active': (!inputHidden && !showActiveBoxState) ? false : isChecked() || checked,
|
||||
'pgn__selectable_box-active': isChecked() || checked,
|
||||
'pgn__selectable_box-invalid': isInvalid,
|
||||
})}
|
||||
tabIndex={0}
|
||||
@@ -98,8 +97,6 @@ SelectableBox.propTypes = {
|
||||
isInvalid: PropTypes.bool,
|
||||
/** A class that is appended to the base element. */
|
||||
className: PropTypes.string,
|
||||
/** Controls the visibility of the active state for the `SelectableBox`. */
|
||||
showActiveBoxState: PropTypes.bool,
|
||||
};
|
||||
|
||||
SelectableBox.defaultProps = {
|
||||
@@ -112,7 +109,6 @@ SelectableBox.defaultProps = {
|
||||
isIndeterminate: false,
|
||||
isInvalid: false,
|
||||
className: undefined,
|
||||
showActiveBoxState: true,
|
||||
};
|
||||
|
||||
SelectableBox.Set = SelectableBoxSet;
|
||||
|
||||
54
src/editors/sharedComponents/SelectableBox/index.scss
Normal file
54
src/editors/sharedComponents/SelectableBox/index.scss
Normal file
@@ -0,0 +1,54 @@
|
||||
@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;
|
||||
}
|
||||
8
src/editors/sharedComponents/SelectableBox/newId.js
Normal file
8
src/editors/sharedComponents/SelectableBox/newId.js
Normal file
@@ -0,0 +1,8 @@
|
||||
let lastId = 0;
|
||||
|
||||
const newId = (prefix = 'id') => {
|
||||
lastId += 1;
|
||||
return `${prefix}${lastId}`;
|
||||
};
|
||||
|
||||
export default newId;
|
||||
@@ -0,0 +1,124 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,107 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
// 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>
|
||||
`;
|
||||
@@ -0,0 +1,57 @@
|
||||
// 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>
|
||||
`;
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
Form, CheckboxControl, RadioControl,
|
||||
} from '@openedx/paragon';
|
||||
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) => {
|
||||
@@ -16,11 +17,11 @@ export const getInputType = (component, type) => {
|
||||
} else if (component === 'SelectableBoxSet') {
|
||||
switch (type) {
|
||||
case 'radio':
|
||||
return Form.RadioSet;
|
||||
return FormRadioSet;
|
||||
case 'checkbox':
|
||||
return Form.CheckboxSet;
|
||||
return FormCheckboxSet;
|
||||
default:
|
||||
return Form.RadioSet;
|
||||
return FormRadioSet;
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user