diff --git a/example/src/components/FormCheckboxDemo.tsx b/example/src/components/FormCheckboxDemo.tsx index 9857f110..1a7ae0d0 100644 --- a/example/src/components/FormCheckboxDemo.tsx +++ b/example/src/components/FormCheckboxDemo.tsx @@ -3,7 +3,7 @@ import { FormCheckbox, CheckboxGroup } from '@capgeminiuk/dcx-react-library'; export const FormCheckboxDemo = () => { const [value, setValue] = React.useState(''); - const [checked, setChecked] = React.useState(true); + const [checked, setChecked] = React.useState(false); const handleChange = (event: any) => { setValue(event.currentTarget.value); setChecked(!checked); @@ -47,6 +47,21 @@ export const FormCheckboxDemo = () => { onChange={handleChange} disabled={true} /> +

Checkbox with Error

+ + {checked && ( +
+ Error: The checkbox is checked! +
+ )}

Group

{ + const presenter = presenters[name] ? `\t * @presenter ${presenters[name]}\n` : ''; + return '\t/**\n' + + `\t * @tokens ${name}\n` + + presenter + + '\t */\n'; + }; + return { + name: 'copy-file', + transform(code, id) { + return `export const tokens = ${code};`; + }, + generateBundle(opts, bundle) { + const files = new Map(); + for (const file in bundle) { + const bundleEntry = bundle[file]; + files.set(bundleEntry.fileName, bundleEntry.code); + } + + Array.from(files.entries()) + .forEach(([fileName, code]) => { + const tokensJson = code + .replace('const tokens = ', '') + .replace('export { tokens };', '') + .replace(';', '') + .trim(); + const parsedTokens = JSON.parse(tokensJson); + + const tokenKeys = Object.keys(parsedTokens) + .sort(); + + const tokenKeysByCategory = new Map(); + tokenKeys.forEach(token => { + const parts = token.split('-'); + if (!tokenKeysByCategory.get(parts[0])) { + tokenKeysByCategory.set(parts[0], []); + } + tokenKeysByCategory.get(parts[0]).push(token); + }); + + let source = ':root {\n'; + Array.from(tokenKeysByCategory.entries()) + .forEach(([category, tokenKeys]) => { + const cssTokens = tokenKeys + .map((tokenKey) => `\t--dcx-${tokenKey}: ${parsedTokens[tokenKey]};\n`) + .join(''); + source = source + + getHeading(category) + + cssTokens; + }); + source = source + '};'; + + this.emitFile({ type: 'asset', fileName, source }); + }); + } + }; +} + + +const getBaseConfig = () => ({ + input: `${INPUT_FOLDER}/input.css.json`, + output: { + file: `${OUTPUT_FOLDER}/input.css`, + format: 'es' + }, + plugins: [ + postcss({ + modules: false, + extract: true, + plugins: [ + postcssImport(), + postcssNesting(), + postcssFunctions({ + functions: { + ...customFunctions + } + }), + cssnano(cssnanoPreset()) + ] + }) + ] +}); + +const config = glob.sync(`${INPUT_FOLDER}/*.css`).reduce((acc, file) => { + acc.push({ + ...getBaseConfig(), + input: file, + output: { + file: file.replace(`${INPUT_FOLDER}/`, `${OUTPUT_FOLDER}/`), + format: 'es' + } + }); + return acc; +}, [ + { + input: `${INPUT_FOLDER}/tokens.json`, + output: { + file: `${OUTPUT_FOLDER}/tokens.css`, + format: 'es' + }, + plugins: [generateTokens(),] + } +]); + +exports.default = config; diff --git a/src/common/components/commonTypes.ts b/src/common/components/commonTypes.ts index 170d4e92..68c2034b 100644 --- a/src/common/components/commonTypes.ts +++ b/src/common/components/commonTypes.ts @@ -274,6 +274,10 @@ export type FormRadioCheckboxProps = { * specifies an optional className for the item */ itemClassName?: string; + /** + * specifies whether there is an error with the input. + */ + isError?: boolean; }; export type HintProps = { diff --git a/src/formCheckbox/FormCheckbox.tsx b/src/formCheckbox/FormCheckbox.tsx index 5afb370b..985d077d 100644 --- a/src/formCheckbox/FormCheckbox.tsx +++ b/src/formCheckbox/FormCheckbox.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { FormRadioCheckboxProps } from '../common/components/commonTypes'; import { CheckboxRadioBase, Roles } from '../common'; +import { classNames } from '../common/utils'; export const FormCheckbox = ({ id, @@ -23,10 +24,28 @@ export const FormCheckbox = ({ inputClassName, labelClassName, itemClassName, + isError }: FormRadioCheckboxProps & { onChange?: (event: React.ChangeEvent, conditional?: string) => void; -}) => ( - { + const containerClasses = classNames([ + itemClassName, + { 'dcx-checkbox-container--error': isError }, + ]); + + const checkboxClasses = classNames([ + inputClassName, + { 'dcx-checkbox-checkbox--error': isError }, + ]); + + const labelClasses = classNames([ + labelClassName, + { 'dcx-checkbox-label--error': isError }, + ]); + + return ( + -); + itemClassName={containerClasses} + inputClassName={checkboxClasses} + labelClassName={labelClasses} + /> + ) + } diff --git a/src/formCheckbox/__test__/FormCheckbox.test.tsx b/src/formCheckbox/__test__/FormCheckbox.test.tsx index 97d2fbe1..6913c545 100644 --- a/src/formCheckbox/__test__/FormCheckbox.test.tsx +++ b/src/formCheckbox/__test__/FormCheckbox.test.tsx @@ -429,7 +429,7 @@ describe('FormCheckbox', () => { const label: any = container.querySelector('#my-label'); - expect(label.className).toBe('my-label-class'); + expect(label.className.trim()).toBe('my-label-class'); }); it('should style the checkbox item', () => { @@ -450,7 +450,7 @@ describe('FormCheckbox', () => { const checkbox: any = container.querySelector('#checkbox-item'); - expect(checkbox.className).toBe('my-checkbox-class'); + expect(checkbox.className.trim()).toBe('my-checkbox-class'); }); it('should style the checkbox input', () => { @@ -471,7 +471,7 @@ describe('FormCheckbox', () => { const input: any = container.querySelector('#input-item'); - expect(input.className).toBe('my-input-class'); + expect(input.className.trim()).toBe('my-input-class'); }); @@ -495,4 +495,66 @@ describe('FormCheckbox', () => { const firstItemEl: any = screen.getByRole('link'); expect(firstItemEl.href).toBe('http://localhost/link'); }); + + it('should apply error styling when isError is true', () => { + const handleChange = jest.fn(); + + const { container } = render( + + ); + + const checkboxContainer = container.querySelector('.dcx-checkbox-container--error'); + const checkbox = container.querySelector('.dcx-checkbox-checkbox--error'); + const label = container.querySelector('.dcx-checkbox-label--error'); + + expect(checkboxContainer).toBeInTheDocument(); + expect(checkbox).toBeInTheDocument(); + expect(label).toBeInTheDocument(); + + }); + + it('should not apply error styling when isError is false', () => { + const handleChange = jest.fn(); + + const { container } = render( + + ); + + expect(container.querySelector('.dcx-checkbox-container--error')).toBeNull(); + expect(container.querySelector('.dcx-checkbox-checkbox--error')).toBeNull(); + expect(container.querySelector('.dcx-checkbox-label--error')).toBeNull(); + }); + + it('should not apply error styling when isError is not provided', () => { + const handleChange = jest.fn(); + + const { container } = render( + + ); + + expect(container.querySelector('.dcx-checkbox-container--error')).toBeNull(); + expect(container.querySelector('.dcx-checkbox-checkbox--error')).toBeNull(); + expect(container.querySelector('.dcx-checkbox-label--error')).toBeNull(); + }); + }); diff --git a/stories/FormCheckbox/ClassBased.stories.js b/stories/FormCheckbox/ClassBased.stories.js index 4b5833f8..c73a7826 100644 --- a/stories/FormCheckbox/ClassBased.stories.js +++ b/stories/FormCheckbox/ClassBased.stories.js @@ -1,5 +1,6 @@ import { FormCheckbox } from '../../src/formCheckbox/FormCheckbox'; import { useArgs } from '@storybook/preview-api'; +import '../govUkStyle.css' /** * In this section we're using the checkbox component providing the **GovUk style** passing the relative `className. @@ -289,4 +290,5 @@ export const SmallCheckbox = { defaultChecked:false, }, argTypes: { onChange: { action: 'changed' } }, -}; \ No newline at end of file +}; + diff --git a/stories/FormCheckbox/Documentation.mdx b/stories/FormCheckbox/Documentation.mdx index 992c1430..ab426b59 100644 --- a/stories/FormCheckbox/Documentation.mdx +++ b/stories/FormCheckbox/Documentation.mdx @@ -66,6 +66,7 @@ An example with all the available properties is: inputClassName="inputClassName" labelClassName="labelClassName" itemClassName="itemClassName" + isError={true} /> ``` diff --git a/stories/FormCheckbox/UnStyled.stories.js b/stories/FormCheckbox/UnStyled.stories.js index d322cda2..a3cccc3b 100644 --- a/stories/FormCheckbox/UnStyled.stories.js +++ b/stories/FormCheckbox/UnStyled.stories.js @@ -20,7 +20,11 @@ export const Unstyled = { console.log(conditional); return; } - setArgs({ value: evt.currentTarget.value, selected: evt.currentTarget.checked}); + setArgs({ + value: evt.currentTarget.value, + selected: evt.currentTarget.checked, + isError: true + }); setTimeout(() => setArgs({ isLoading: false }), 2000); }; return ; @@ -40,7 +44,8 @@ export const Unstyled = { type: 'text', id: 'checkbox-6', inputId: 'input-6', - } + }, + isError: false }, argTypes: { onChange: { action: 'changed' } }, }; diff --git a/stories/liveEdit/FormCheckboxLive.tsx b/stories/liveEdit/FormCheckboxLive.tsx index 55569beb..840d553a 100644 --- a/stories/liveEdit/FormCheckboxLive.tsx +++ b/stories/liveEdit/FormCheckboxLive.tsx @@ -7,6 +7,7 @@ function FormCheckboxDemo() { const [value, setValue] = React.useState(''); const [checked, setChecked] = React.useState(false); + const [isError, setIsError] = React.useState(false) const handleChange = (event, conditional) => { if (conditional) { console.log(conditional); @@ -14,6 +15,9 @@ function FormCheckboxDemo() { } setValue(event.currentTarget.value); setChecked(event.currentTarget.checked); + + setIsError(event.currentTarget.checked && event.currentTarget.value === ''); + }; return (