diff --git a/packages/react/src/components/Checkbox/Checkbox.tsx b/packages/react/src/components/Checkbox/Checkbox.tsx index 8ae819f263..17761c28ea 100644 --- a/packages/react/src/components/Checkbox/Checkbox.tsx +++ b/packages/react/src/components/Checkbox/Checkbox.tsx @@ -1,4 +1,5 @@ import { Accessibility, checkboxBehavior } from '@fluentui/accessibility' +import CheckboxBase from './CheckboxBase' import * as customPropTypes from '@fluentui/react-proptypes' import * as _ from 'lodash' import * as React from 'react' @@ -149,11 +150,14 @@ class Checkbox extends AutoControlledComponent, Checkb }) return ( - , Checkb }), })} {labelPosition === 'end' && labelElement} - + ) } } diff --git a/packages/react/src/components/Checkbox/CheckboxBase.tsx b/packages/react/src/components/Checkbox/CheckboxBase.tsx new file mode 100644 index 0000000000..1916aa0fd8 --- /dev/null +++ b/packages/react/src/components/Checkbox/CheckboxBase.tsx @@ -0,0 +1,80 @@ +import * as React from 'react' + +interface CheckboxBaseProps { + checked?: boolean + onChange?: React.ChangeEventHandler + onClick?: React.MouseEventHandler + slots?: { + root?: any + input?: any + } + slotProps?: { + root?: any + input?: any + } + classes?: any +} + +const Testhelper = (prop?: Function, propName?: string) => { + if (!prop) { + return {} + } + return { + [propName]: prop, + } +} + +const CheckboxBase: React.FunctionComponent = props => { + const { checked, onChange, onClick, classes = {}, slots = {}, slotProps = {}, ...rest } = props + const { root: RootSlot = 'div', input: InputSlot = 'input' } = slots + const { root: rootClass, input: inputClass } = classes + const { root: rootProps, input: inputProps } = slotProps + + const [isChecked, setIsChecked] = React.useState(!!checked) + const realIsChecked = checked === undefined ? isChecked : !!checked + + const onChangeHandler = React.useCallback( + (ev: any) => { + if (onClick) { + onClick(ev) + } + if (!ev.defaultPrevented) { + if (onChange) { + onChange(ev) + } + } + if (!ev.defaultPrevented) { + setIsChecked(!realIsChecked) + } + }, + [setIsChecked, realIsChecked, onChange], + ) + + const onKeyDown = React.useCallback( + (ev: React.KeyboardEvent) => { + console.log(ev.keyCode) + switch (ev.keyCode) { + case 13: + case 32: { + setIsChecked(!realIsChecked) + } + } + }, + [setIsChecked, realIsChecked], + ) + + return ( + + + {props.children} + + ) +} +export default CheckboxBase diff --git a/packages/react/test/specs/commonTests/isConformant.tsx b/packages/react/test/specs/commonTests/isConformant.tsx index 2e3c9ea314..3e16599a66 100644 --- a/packages/react/test/specs/commonTests/isConformant.tsx +++ b/packages/react/test/specs/commonTests/isConformant.tsx @@ -32,6 +32,8 @@ export interface Conformant { rendersPortal?: boolean /** This component uses wrapper slot to wrap the 'meaningful' element. */ wrapperComponent?: React.ReactType + /** This component uses a base element */ + baseComponent?: React.ReactType } /** @@ -53,6 +55,7 @@ export default function isConformant( requiredProps = {}, rendersPortal = false, wrapperComponent = null, + baseComponent = null, } = options const { throwError } = helpers('isConformant', Component) @@ -61,6 +64,7 @@ export default function isConformant( const helperComponentNames = [ ...[Ref, RefFindNode], ...(wrapperComponent ? [wrapperComponent] : []), + ...(baseComponent ? [baseComponent] : []), ].map(getDisplayName) const toNextNonTrivialChild = (from: ReactWrapper) => { diff --git a/packages/react/test/specs/components/Checkbox/Checkbox-test.tsx b/packages/react/test/specs/components/Checkbox/Checkbox-test.tsx index 1a626d30a1..69a9531302 100644 --- a/packages/react/test/specs/components/Checkbox/Checkbox-test.tsx +++ b/packages/react/test/specs/components/Checkbox/Checkbox-test.tsx @@ -1,5 +1,6 @@ import * as React from 'react' import Checkbox from 'src/components/Checkbox/Checkbox' +import CheckboxBase from 'src/components/Checkbox/CheckboxBase' import { isConformant, handlesAccessibility, @@ -7,7 +8,7 @@ import { } from 'test/specs/commonTests' describe('Checkbox', () => { - isConformant(Checkbox) + isConformant(Checkbox, { baseComponent: CheckboxBase }) handlesAccessibility(Checkbox, { defaultRootRole: 'checkbox' }) describe('HTML accessibility rules validation', () => { diff --git a/packages/react/test/specs/components/Checkbox/CheckboxBase-test.tsx b/packages/react/test/specs/components/Checkbox/CheckboxBase-test.tsx new file mode 100644 index 0000000000..5151e91ad4 --- /dev/null +++ b/packages/react/test/specs/components/Checkbox/CheckboxBase-test.tsx @@ -0,0 +1,185 @@ +import * as React from 'react' +import { mount } from 'enzyme' +import CheckboxBase from 'src/components/Checkbox/CheckboxBase' + +describe("Baby's first test", () => { + it('renders something', () => { + expect(mount()).toBeTruthy() + }) + + it('has an input', () => { + const control = mount() + const input = control + .find('input') + .first() + .getDOMNode() + expect(input).toBeTruthy() + }) + + it('can be checked', () => { + const control = mount() + const inputChecked = control + .find('input') + .first() + .prop('checked') + expect(inputChecked).toBe(true) + }) + + it('can be unchecked', () => { + const control = mount() + const inputChecked = control + .find('input') + .first() + .prop('checked') + expect(inputChecked).toBe(false) + }) + + it('can switch check state', () => { + const control = mount() + const inputChecked = control + .find('input') + .first() + .prop('checked') + expect(inputChecked).toBe(false) + control.setProps({ checked: true }) + const inputCheckedAfter = control + .find('input') + .first() + .prop('checked') + expect(inputCheckedAfter).toBe(true) + }) + + it('changes state when clicked', () => { + const control = mount() + expect( + control + .find('input') + .first() + .prop('checked'), + ).toBe(false) + control.simulate('click') + expect( + control + .find('input') + .first() + .prop('checked'), + ).toBe(true) + control.simulate('click') + expect( + control + .find('input') + .first() + .prop('checked'), + ).toBe(false) + }) + + it('calls onChange when checked state changes', () => { + const change = jest.fn() + const control = mount() // let, the only constant is change + control.simulate('click') + expect(change).toHaveBeenCalled() + }) + + it('does not change value when onChange prevents default', () => { + const change = jest.fn((e: any) => e.preventDefault()) + const control = mount() + expect( + control + .find('input') + .first() + .prop('checked'), + ).toBe(false) + control.simulate('click') + expect( + control + .find('input') + .first() + .prop('checked'), + ).toBe(false) + }) + + it('does not call onChange value when onClick prevents default', () => { + const click = jest.fn((e: any) => e.preventDefault()) + const change = jest.fn() + const control = mount() + control.simulate('click') + expect(change).not.toHaveBeenCalled() + }) + + it('calls onClick', () => { + const click = jest.fn() + const control = mount() + control.simulate('click') + expect(click).toHaveBeenCalled() + }) + + it('renders content', () => { + const control = mount( + + + , + ) + expect(control.find('label').length).toBe(1) + }) + + it('can render with a different root type', () => { + const control = mount() + expect(control.find('span').length).toBe(1) + }) + + it('can render with a different input type', () => { + const MyInput = () => ( + + + + ) + const control = mount() + expect(control.find('a').length).toBe(1) + }) + + it('changes on enter press', () => { + const control = mount() + control.simulate('keydown', { + keyCode: 13, + key: 'Enter', + }) + expect(control.find('input').prop('checked')).toBe(true) + }) + + it('changes on space press', () => { + const control = mount() + control.simulate('keydown', { + keyCode: 32, + }) + expect(control.find('input').prop('checked')).toBe(true) + }) + + describe('class handling', () => { + it('renders classes', () => { + const control = mount() + expect(control.find('div.foo').length).toBe(1) + }) + + it('renders classes for input', () => { + const control = mount() + expect(control.find('input.foo').length).toBe(1) + }) + }) + + describe('slotProps', () => { + it('renders slotProps for input', () => { + const control = mount() + expect(control.find('input[data-foo="bar"]').length).toBe(1) + }) + + it('renders slotProps for root', () => { + const control = mount() + expect(control.find('div[data-foo="bar"]').length).toBe(1) + }) + + it('applies unused props to the root element', () => { + const control = mount() + expect(control.find('div[data-foo="bar"]').length).toBe(1) + }) + }) +})