Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/Checkbox rework from CC into FC #2104

Merged
merged 10 commits into from
Apr 8, 2024
112 changes: 52 additions & 60 deletions uui-components/src/inputs/Checkbox.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import * as React from 'react';
import { cx, IHasTabIndex, uuiMarkers } from '@epam/uui-core';
import { cx, IHasTabIndex, useUuiContext, uuiMarkers } from '@epam/uui-core';
import { Icon, uuiMod, uuiElement, isEventTargetInsideClickable, CheckboxCoreProps } from '@epam/uui-core';
import { IconContainer } from '../layout';
import css from './Checkbox.module.scss';
import {
Icon, uuiMod, uuiElement, isEventTargetInsideClickable, CheckboxCoreProps, UuiContexts, UuiContext,
} from '@epam/uui-core';
import { IconContainer } from '../layout/IconContainer';

export interface CheckboxProps extends CheckboxCoreProps, IHasTabIndex {
/** Render callback for checkbox label.
Expand All @@ -26,72 +24,66 @@ export interface CheckboxProps extends CheckboxCoreProps, IHasTabIndex {
indeterminateIcon?: Icon;
}

export class Checkbox extends React.Component<CheckboxProps> {
static contextType = UuiContext;
context: UuiContexts;
export const Checkbox = React.forwardRef<HTMLLabelElement, CheckboxProps>((props, ref) => {
const context = useUuiContext();

handleChange = (e: React.SyntheticEvent<HTMLInputElement>) => {
!isEventTargetInsideClickable(e) && this.props.onValueChange(!this.props.value);
const handleChange = (e: React.SyntheticEvent<HTMLInputElement>) => {
!isEventTargetInsideClickable(e) && props.onValueChange(!props.value);

if (this.props.getValueChangeAnalyticsEvent) {
const event = this.props.getValueChangeAnalyticsEvent(!this.props.value, this.props.value);
this.context.uuiAnalytics.sendEvent(event);
if (props.getValueChangeAnalyticsEvent) {
const event = props.getValueChangeAnalyticsEvent(!props.value, props.value);
context.uuiAnalytics.sendEvent(event);
}
};

handleAriaCheckedValue = (indeterminate: boolean, value: boolean): boolean | 'mixed' => {
const handleAriaCheckedValue = (indeterminate: boolean, value: boolean): boolean | 'mixed' => {
if (indeterminate) {
return 'mixed';
}

return value == null ? false : value;
};

render() {
let label = this.props.label;
if (this.props.renderLabel) {
label = this.props.renderLabel();
}
const ariaCheckedValue = this.handleAriaCheckedValue(this.props.indeterminate, this.props.value);
const label = props.renderLabel ? props.renderLabel() : props.label;
const ariaCheckedValue = handleAriaCheckedValue(props.indeterminate, props.value);

return (
<label
className={ cx(
css.container,
uuiElement.checkboxContainer,
this.props.cx,
this.props.isDisabled && uuiMod.disabled,
this.props.isReadonly && uuiMod.readonly,
this.props.isInvalid && uuiMod.invalid,
!this.props.isReadonly && !this.props.isDisabled && uuiMarkers.clickable,
) }
ref={ this.props.forwardedRef }
{ ...this.props.rawProps }
return (
<label
className={ cx(
css.container,
uuiElement.checkboxContainer,
props.cx,
props.isDisabled && uuiMod.disabled,
props.isReadonly && uuiMod.readonly,
props.isInvalid && uuiMod.invalid,
!props.isReadonly && !props.isDisabled && uuiMarkers.clickable,
) }
ref={ ref }
{ ...props.rawProps }
>
<div
className={ cx(uuiElement.checkbox, (props.value || props.indeterminate) && uuiMod.checked) }
onFocus={ props.onFocus }
onBlur={ props.onBlur }
>
<div
className={ cx(uuiElement.checkbox, (this.props.value || this.props.indeterminate) && uuiMod.checked) }
onFocus={ this.props.onFocus }
onBlur={ this.props.onBlur }
>
<input
type="checkbox"
onChange={ !this.props.isReadonly ? this.handleChange : undefined }
disabled={ this.props.isDisabled }
aria-disabled={ this.props.isDisabled || undefined }
readOnly={ this.props.isReadonly }
aria-readonly={ this.props.isReadonly || undefined }
checked={ this.props.value || false }
aria-checked={ ariaCheckedValue }
required={ this.props.isRequired }
aria-required={ this.props.isRequired || undefined }
tabIndex={ this.props.tabIndex || this.props.isReadonly || this.props.isDisabled ? -1 : 0 }
id={ this.props.id }
/>
{ this.props.value && !this.props.indeterminate && <IconContainer icon={ this.props.icon } /> }
{ this.props.indeterminate && <IconContainer icon={ this.props.indeterminateIcon } /> }
</div>
{ label && <div className={ uuiElement.inputLabel }>{ label }</div> }
</label>
);
}
}
<input
type="checkbox"
onChange={ !props.isReadonly ? handleChange : undefined }
disabled={ props.isDisabled }
aria-disabled={ props.isDisabled || undefined }
readOnly={ props.isReadonly }
aria-readonly={ props.isReadonly || undefined }
checked={ props.value || false }
aria-checked={ ariaCheckedValue }
required={ props.isRequired }
aria-required={ props.isRequired || undefined }
tabIndex={ props.tabIndex || props.isReadonly || props.isDisabled ? -1 : 0 }
id={ props.id }
/>
{ props.value && !props.indeterminate && <IconContainer icon={ props.icon } /> }
{ props.indeterminate && <IconContainer icon={ props.indeterminateIcon } /> }
</div>
{ label && <div className={ uuiElement.inputLabel }>{ label }</div> }
</label>
);
});
41 changes: 34 additions & 7 deletions uui-components/src/inputs/__tests__/Checkbox.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { Checkbox, CheckboxProps } from '../Checkbox';
import { render, screen, fireEvent, setupComponentForTest, userEvent } from '@epam/uui-test-utils';
import { screen, fireEvent, setupComponentForTest, userEvent } from '@epam/uui-test-utils';

async function setupCheckbox(params: Partial<CheckboxProps>) {
const { mocks, setProps } = await setupComponentForTest<CheckboxProps>(
Expand Down Expand Up @@ -71,18 +71,29 @@ describe('Checkbox', () => {
expect(getValueChangeAnalyticsEvent).toHaveBeenCalled();
});

it('should not handle change event when readonly', () => {
it('should not handle change event when readonly', async () => {
const onValueChange = jest.fn();
render(<Checkbox value={ false } onValueChange={ onValueChange } isReadonly />);
await setupCheckbox({
value: false,
onValueChange,
isReadonly: true,
label: 'Label',
});
const input = screen.getByRole('checkbox');

fireEvent.click(input);
expect(onValueChange).not.toHaveBeenCalled();
});

it('should handle focus event', () => {
it('should handle focus event', async () => {
const onFocus = jest.fn();
render(<Checkbox value={ false } onValueChange={ jest.fn } onFocus={ onFocus } />);
const onValueChange = jest.fn();
await setupCheckbox({
value: false,
onValueChange,
label: 'Label',
onFocus,
});
const input = screen.getByRole('checkbox');

input.focus();
Expand All @@ -91,9 +102,15 @@ describe('Checkbox', () => {
expect(input).toHaveFocus();
});

it('should handle blur event', () => {
it('should handle blur event', async () => {
const onBlur = jest.fn();
render(<Checkbox value={ false } onValueChange={ jest.fn } onBlur={ onBlur } />);
const onValueChange = jest.fn();
await setupCheckbox({
value: false,
onValueChange,
label: 'Label',
onBlur,
});
const input = screen.getByRole('checkbox');

input.focus();
Expand All @@ -103,4 +120,14 @@ describe('Checkbox', () => {
expect(onBlur).toHaveBeenCalled();
expect(input).not.toHaveFocus();
});

it('when state equals isInvalid: true, Checkbox_container must have a \'uui-invalid\' class', async () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Such cases better check via snapshot tests, just render Checkbox with invalid state. I guess we can add this case to the snapshot in UUI.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

await setupCheckbox({
value: false,
isInvalid: true,
});
const input = screen.getByRole('checkbox');

expect(input.parentElement.parentElement).toHaveClass('uui-invalid');
});
});
14 changes: 7 additions & 7 deletions uui/components/inputs/__tests__/Checkbox.test.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import React from 'react';
import { Checkbox } from '../Checkbox';
import { renderer } from '@epam/uui-test-utils';
import { renderSnapshotWithContextAsync } from '@epam/uui-test-utils';
import { Checkbox } from '@epam/uui';

describe('Checkbox', () => {
it('should be rendered correctly', () => {
const tree = renderer.create(<Checkbox value={ null } onValueChange={ jest.fn } />).toJSON();
describe('TestComponent', () => {
it('should render with minimum props', async () => {
const tree = await renderSnapshotWithContextAsync(<Checkbox value={ true } onValueChange={ jest.fn } />);
expect(tree).toMatchSnapshot();
});

it('should be rendered correctly', () => {
const tree = renderer.create(<Checkbox value={ null } onValueChange={ jest.fn } size="18" mode="cell" />).toJSON();
it('should render with maximum props', async () => {
const tree = await renderSnapshotWithContextAsync(<Checkbox value={ null } onValueChange={ jest.fn } size="18" mode="cell" />);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add wider range of different props to this test

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

expect(tree).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Checkbox should be rendered correctly 1`] = `
exports[`TestComponent should render with maximum props 1`] = `
<label
className="container uui-checkbox-container root size-18 mode-form uui-color-primary -clickable"
className="container uui-checkbox-container root size-18 mode-cell uui-color-primary -clickable"
>
<div
className="uui-checkbox"
Expand All @@ -18,20 +18,28 @@ exports[`Checkbox should be rendered correctly 1`] = `
</label>
`;

exports[`Checkbox should be rendered correctly 2`] = `
exports[`TestComponent should render with minimum props 1`] = `
<label
className="container uui-checkbox-container root size-18 mode-cell uui-color-primary -clickable"
className="container uui-checkbox-container root size-18 mode-form uui-color-primary -clickable"
>
<div
className="uui-checkbox"
className="uui-checkbox uui-checked"
>
<input
aria-checked={false}
checked={false}
aria-checked={true}
checked={true}
onChange={[Function]}
tabIndex={0}
type="checkbox"
/>
<div
className="container uui-icon uui-enabled"
style={Object {}}
>
<svg
className=""
/>
</div>
</div>
</label>
`;
43 changes: 20 additions & 23 deletions uui/components/layout/__tests__/CheckboxGroup.test.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,30 @@
import React from 'react';
import { CheckboxGroup } from '../CheckboxGroup';
import { renderer } from '@epam/uui-test-utils';
import { renderSnapshotWithContextAsync } from '@epam/uui-test-utils';

describe('CheckboxGroup', () => {
it('should be rendered correctly', () => {
const tree = renderer
.create(
<CheckboxGroup
value={ null }
onValueChange={ () => {} }
items={ [{ id: 1, name: 'Test1' }, { id: 2, name: 'Test2' }] }
/>,
)
.toJSON();
it('should be rendered correctly', async () => {
const tree = await renderSnapshotWithContextAsync(
<CheckboxGroup
value={ null }
onValueChange={ () => {} }
items={ [{ id: 1, name: 'Test1' }, { id: 2, name: 'Test2' }] }
/>,
);
expect(tree).toMatchSnapshot();
});

it('should be rendered correctly with props', () => {
const tree = renderer
.create(
<CheckboxGroup
value={ null }
onValueChange={ () => {} }
items={ [{ id: 1, name: 'Test1' }, { id: 2, name: 'Test2' }] }
direction="horizontal"
isDisabled
/>,
)
.toJSON();
it('should be rendered correctly with props', async () => {
const tree = await renderSnapshotWithContextAsync(
<CheckboxGroup
value={ null }
onValueChange={ () => {} }
items={ [{ id: 1, name: 'Test1' }, { id: 2, name: 'Test2' }] }
direction="horizontal"
isDisabled
/>,
);

expect(tree).toMatchSnapshot();
});
});
Loading