Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion example/src/components/FormCheckboxDemo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean>(true);
const [checked, setChecked] = React.useState<boolean>(false);
const handleChange = (event: any) => {
setValue(event.currentTarget.value);
setChecked(!checked);
Expand Down Expand Up @@ -47,6 +47,21 @@ export const FormCheckboxDemo = () => {
onChange={handleChange}
disabled={true}
/>
<h2 id="checkbox-with-error-tag">Checkbox with Error</h2>
<FormCheckbox
ariaDescribedBy="checkbox-with-error-tag"
id="checkbox-4"
name="group4"
value={value}
label="Checkbox 4 label text"
onChange={handleChange}
isError={!checked}
/>
{checked && (
<div style={{ color: 'red', marginTop: '8px' }}>
Error: The checkbox is checked!
</div>
)}
<h2>Group</h2>
<CheckboxGroup
name="name-of-group"
Expand Down
146 changes: 146 additions & 0 deletions rollup.config-1706545646874.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

var postcss = require('rollup-plugin-postcss');
var postcssFunctions = require('postcss-functions');
var postcssImport = require('postcss-import');
var postcssNesting = require('postcss-nesting');
var cssnano = require('cssnano');
var cssnanoPreset = require('cssnano-preset-default');
var fs = require('fs');
var glob = require('glob');
var path = require('path');

function token(name) {
const tokens = fs.readFileSync('./src/design-system/tokens.json', 'utf8');
const parsedTokens = JSON.parse(tokens);
const parsedName = name.replace(/('|")/g, '');
return `var(--dcx-${parsedName}, ${parsedTokens[parsedName]})`;
}

var customFunctions = /*#__PURE__*/Object.freeze({
__proto__: null,
token: token
});

const INPUT_FOLDER = 'src/design-system';
const OUTPUT_FOLDER = 'dist/design-system';

/**
* Cleanup dist folder
*/
const distPath = path.join(__dirname, OUTPUT_FOLDER);
if (fs.existsSync(distPath)) {
fs.rmSync(distPath, { recursive: true });
}

function generateTokens() {
const presenters = {
color: 'Color'
};
const getHeading = (name) => {
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;
4 changes: 4 additions & 0 deletions src/common/components/commonTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
34 changes: 27 additions & 7 deletions src/formCheckbox/FormCheckbox.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -23,10 +24,28 @@ export const FormCheckbox = ({
inputClassName,
labelClassName,
itemClassName,
isError
}: FormRadioCheckboxProps & {
onChange?: (event: React.ChangeEvent, conditional?: string) => void;
}) => (
<CheckboxRadioBase
isError?: boolean;
}) => {
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 (
<CheckboxRadioBase
type="checkbox"
id={id}
role={Roles.formCheckbox}
Expand All @@ -46,8 +65,9 @@ export const FormCheckbox = ({
selected={selected}
hint={hint}
nested={nested}
inputClassName={inputClassName}
labelClassName={labelClassName}
itemClassName={itemClassName}
/>
);
itemClassName={containerClasses}
inputClassName={checkboxClasses}
labelClassName={labelClasses}
/>
)
}
68 changes: 65 additions & 3 deletions src/formCheckbox/__test__/FormCheckbox.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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', () => {
Expand All @@ -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');
});


Expand All @@ -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(
<FormCheckbox
id="myId"
name="group1"
value="choice 1"
label="my label"
onChange={handleChange}
isError={true}
/>
);

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(
<FormCheckbox
id="myId"
name="group1"
value="choice 1"
label="my label"
onChange={handleChange}
isError={false}
/>
);

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(
<FormCheckbox
id="myId"
name="group1"
value="choice 1"
label="my label"
onChange={handleChange}
/>
);

expect(container.querySelector('.dcx-checkbox-container--error')).toBeNull();
expect(container.querySelector('.dcx-checkbox-checkbox--error')).toBeNull();
expect(container.querySelector('.dcx-checkbox-label--error')).toBeNull();
});

});
4 changes: 3 additions & 1 deletion stories/FormCheckbox/ClassBased.stories.js
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -289,4 +290,5 @@ export const SmallCheckbox = {
defaultChecked:false,
},
argTypes: { onChange: { action: 'changed' } },
};
};

1 change: 1 addition & 0 deletions stories/FormCheckbox/Documentation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ An example with all the available properties is:
inputClassName="inputClassName"
labelClassName="labelClassName"
itemClassName="itemClassName"
isError={true}
/>
```

Expand Down
9 changes: 7 additions & 2 deletions stories/FormCheckbox/UnStyled.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <FormCheckbox {...args} onChange={checkboxHandler} />;
Expand All @@ -40,7 +44,8 @@ export const Unstyled = {
type: 'text',
id: 'checkbox-6',
inputId: 'input-6',
}
},
isError: false
},
argTypes: { onChange: { action: 'changed' } },
};
Loading