diff --git a/.dumirc.ts b/.dumirc.ts new file mode 100644 index 0000000..01854c1 --- /dev/null +++ b/.dumirc.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'dumi'; + +export default defineConfig({ + themeConfig: { + name: 'Checkbox', + }, +}); diff --git a/.fatherrc.js b/.fatherrc.js deleted file mode 100644 index 571c236..0000000 --- a/.fatherrc.js +++ /dev/null @@ -1,10 +0,0 @@ -export default { - entry: ['src/index.js'], - cjs: 'babel', - esm: { type: 'babel', importLibToEs: true }, - preCommit: { - eslint: true, - prettier: true, - }, - runtimeHelpers: true, -}; diff --git a/.fatherrc.ts b/.fatherrc.ts new file mode 100644 index 0000000..96268ae --- /dev/null +++ b/.fatherrc.ts @@ -0,0 +1,5 @@ +import { defineConfig } from 'father'; + +export default defineConfig({ + plugins: ['@rc-component/father-plugin'], +}); diff --git a/.gitignore b/.gitignore index d0036e2..3fa2c9b 100644 --- a/.gitignore +++ b/.gitignore @@ -30,8 +30,13 @@ es coverage yarn.lock + # dumi .umi +.dumi/tmp +.dumi/tmp-production .umi-production .umi-test -.docs \ No newline at end of file +.docs + +pnpm-lock.yaml diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..798e8b4 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,5 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npx --no-install lint-staged --quiet +npx --no-install jest diff --git a/.umirc.ts b/.umirc.ts deleted file mode 100644 index 33a8fa4..0000000 --- a/.umirc.ts +++ /dev/null @@ -1,8 +0,0 @@ -// more config: https://d.umijs.org/config -import { defineConfig } from 'dumi'; - -export default defineConfig({ - title: 'rc-checkbox', - outputPath: '.docs', - exportStatic: {}, -}); \ No newline at end of file diff --git a/docs/demo/simple.md b/docs/demo/simple.md deleted file mode 100644 index dd7fb36..0000000 --- a/docs/demo/simple.md +++ /dev/null @@ -1,3 +0,0 @@ -## Simple - - \ No newline at end of file diff --git a/docs/demo/simple.tsx b/docs/demo/simple.tsx new file mode 100644 index 0000000..336ef62 --- /dev/null +++ b/docs/demo/simple.tsx @@ -0,0 +1,125 @@ +/* eslint no-console:0, jsx-a11y/label-has-for: 0, jsx-a11y/label-has-associated-control: 0 */ +import React from 'react'; +import Checkbox from 'rc-checkbox'; +import '../../assets/index.less'; + +import type { CheckboxProps } from 'rc-checkbox'; + +const onChange = (e: any) => { + console.log('Checkbox checked:', e.target.checked); +}; + +const onKeyDown: CheckboxProps['onKeyDown'] = (e) => { + console.log('Checkbox key down:', e.key); +}; + +const onKeyPress: CheckboxProps['onKeyPress'] = (e) => { + console.log('Checkbox key press:', e.key); +}; + +const onKeyUp: CheckboxProps['onKeyUp'] = (e) => { + console.log('Checkbox key up:', e.key); +}; + +export default () => { + const [disabled, setDisabled] = React.useState(false); + + const toggle = () => { + setDisabled((disabled) => !disabled); + }; + + return ( +
+
+

+ +    +

+

+ +    +

+
+ +
+

+ +    +

+

+ +    +

+
+ +
+

+ +    +

+

+ +    +

+
+ +
+

+ +    +

+

+ +    +

+
+ + +
+ ); +}; diff --git a/docs/example.md b/docs/example.md new file mode 100644 index 0000000..16cf22a --- /dev/null +++ b/docs/example.md @@ -0,0 +1,8 @@ +--- +title: Example +nav: + title: Example + path: /example +--- + + diff --git a/docs/examples/simple.jsx b/docs/examples/simple.jsx deleted file mode 100644 index e201972..0000000 --- a/docs/examples/simple.jsx +++ /dev/null @@ -1,139 +0,0 @@ -/* eslint no-console:0, jsx-a11y/label-has-for: 0, jsx-a11y/label-has-associated-control: 0 */ -import React from 'react'; -import Checkbox from 'rc-checkbox'; -import '../../assets/index.less'; - -function onChange(e) { - console.log('Checkbox checked:', e.target.checked); -} - -function onKeyDown(e) { - console.log('Checkbox key down:', e.key); -} - -function onKeyPress(e) { - console.log('Checkbox key press:', e.key); -} - -function onKeyUp(e) { - console.log('Checkbox key up:', e.key); -} - -export default class SimpleDemo extends React.Component { - state = { - disabled: false, - }; - - toggle = () => { - this.setState(state => ({ - disabled: !state.disabled, - })); - }; - - render() { - return ( -
-
-

- -    -

-

- -    -

-
- -
-

- -    -

-

- -    -

-
- -
-

- -    -

-

- -    -

-
- -
-

- -    -

-

- -    -

-
- - -
- ); - } -} diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..63ea97f --- /dev/null +++ b/docs/index.md @@ -0,0 +1,28 @@ +--- +hero: + title: rc-checkbox + description: checkbox ui component for react +--- + +## 📦 Install + +```bash +# npm install +npm install rc-checkbox --save + +# yarn install +yarn add rc-checkbox + +# pnpm install +pnpm i rc-checkbox +``` + +## 🔨 Usage + +```ts +import Checkbox from 'rc-checkbox'; + +export default () => { + return ; +}; +``` diff --git a/jest.config.ts b/jest.config.ts new file mode 100644 index 0000000..91070fd --- /dev/null +++ b/jest.config.ts @@ -0,0 +1,10 @@ +import { createConfig } from '@umijs/test'; + +import type { Config } from '@umijs/test'; + +const config: Config.InitialOptions = { + ...createConfig(), + testEnvironment: 'jsdom', +}; + +export default config; diff --git a/package.json b/package.json index 2be075c..51eef9b 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,9 @@ "react-checkbox", "checkbox" ], - "main": "./lib/index", - "module": "./es/index", + "main": "lib/index.js", + "module": "es/index.js", + "typings": "es/index.d.ts", "files": [ "assets/*.css", "es", @@ -31,29 +32,48 @@ "gh-pages": "npm run build && father doc deploy -d .docs", "prepublishOnly": "npm run compile && np --yolo --no-publish", "lint": "eslint . --ext='jsx'", - "test": "father test", - "coverage": "father test --coverage" + "test": "jest", + "coverage": "jest --coverage", + "prepare": "husky install" }, "dependencies": { "@babel/runtime": "^7.10.1", - "classnames": "^2.2.1" + "classnames": "^2.3.2", + "rc-util": "^5.25.2" }, "devDependencies": { - "@types/classnames": "^2.2.10", - "@types/jest": "^26.0.5", - "@umijs/fabric": "^2.2.2", + "@rc-component/father-plugin": "^1.0.1", + "@testing-library/jest-dom": "^5.16.5", + "@testing-library/react": "^13.4.0 ", + "@testing-library/user-event": "^14.4.3", + "@types/classnames": "^2.3.1", + "@types/jest": "^29.2.4", + "@types/react": "^18.0.26", + "@types/react-dom": "^18.0.9", + "@umijs/fabric": "^3.0.0", + "@umijs/test": "^4.0.34", "coveralls": "^3.0.6", - "dumi": "^1.1.37", - "enzyme": "^3.0.0", - "enzyme-adapter-react-16": "^1.0.1", - "enzyme-to-json": "^3.0.0", + "dumi": "^2.0.12", "eslint": "^7.0.0", - "father": "^2.13.4", + "father": "^4.1.1", + "husky": "8.0.2", + "jest": "^29.3.1", + "jest-environment-jsdom": "^29.3.1", "less": "^3.11.1", + "lint-staged": "^13.1.0", "np": "^6.2.3", - "react": "^16.0.0", - "react-dom": "^16.0.0", - "react-test-renderer": "^16.0.0" + "react": "^18.2.0", + "react-dom": "^18.2.0", + "ts-node": "^10.9.1", + "typescript": "^4.9.4" + }, + "lint-staged": { + "*.{js,jsx,less,md,json}": [ + "prettier --write" + ], + "*.ts?(x)": [ + "prettier --parser=typescript --write" + ] }, "peerDependencies": { "react": ">=16.9.0", diff --git a/src/index.tsx b/src/index.tsx index 17a896a..bf7492c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -64,16 +64,14 @@ export const Checkbox = forwardRef((props, ref) => { return; } - const checked = e.target.checked; - if (!('checked' in props)) { - setRawValue(checked); + setRawValue(e.target.checked); } onChange?.({ target: { ...props, - checked, + checked: e.target.checked, }, stopPropagation() { e.stopPropagation(); diff --git a/tests/index.test.jsx b/tests/index.test.jsx deleted file mode 100644 index b9ac8ba..0000000 --- a/tests/index.test.jsx +++ /dev/null @@ -1,181 +0,0 @@ -import React from 'react'; -import { mount } from 'enzyme'; -import Checkbox from '../src'; - -describe('rc-checkbox', () => { - it('works', () => { - const wrapper = mount(); - expect(wrapper.state('checked')).toBe(false); - wrapper.find('input').simulate('change', { target: { checked: true } }); - expect(wrapper.state('checked')).toBe(true); - wrapper.find('input').simulate('change', { target: { checked: false } }); - expect(wrapper.state('checked')).toBe(false); - }); - - it('click radio', () => { - const wrapper = mount(); - expect(wrapper.state('checked')).toBe(false); - wrapper.find('input').simulate('change', { target: { checked: true } }); - expect(wrapper.state('checked')).toBe(true); - wrapper.find('input').simulate('change', { target: { checked: true } }); - expect(wrapper.state('checked')).toBe(true); - }); - - it('control mode', () => { - const wrapper = mount(); - expect(wrapper.state('checked')).toBe(true); - wrapper.find('input').simulate('change', { target: { checked: true } }); - expect(wrapper.state('checked')).toBe(true); - wrapper.find('input').simulate('change', { target: { checked: false } }); - expect(wrapper.state('checked')).toBe(true); - }); - - it('stopPropagation and preventDefault', () => { - const onChange = jest.fn(); - const wrapper = mount( -
- { - e.stopPropagation(); - e.preventDefault(); - }} - /> -
, - ); - wrapper.find('input').simulate('change', { target: { checked: true } }); - expect(onChange).not.toHaveBeenCalled(); - }); - - it('passes data-* props to input', () => { - const wrapper = mount(); - const renderedInput = wrapper.find('input').instance(); - expect(renderedInput.attributes['data-type'].value).toEqual('my-data-type'); - }); - - it('passes aria-* props to input', () => { - const wrapper = mount(); - const renderedInput = wrapper.find('input').instance(); - expect(renderedInput.attributes['aria-label'].value).toEqual('my-aria-label'); - }); - - it('passes role prop to input', () => { - // eslint-disable-next-line jsx-a11y/aria-role - const wrapper = mount(); - const renderedInput = wrapper.find('input').instance(); - expect(renderedInput.attributes.role.value).toEqual('my-role'); - }); - - it('passes value prop to input', () => { - const wrapper = mount(); - const renderedInput = wrapper.find('input').instance(); - expect(renderedInput.attributes.value.value).toEqual('my-custom-value'); - }); - - it('passes number value prop to input', () => { - const wrapper = mount(); - const renderedInput = wrapper.find('input').instance(); - expect(renderedInput.attributes.value.value).toEqual('6'); - }); - - it('passes title prop to input', () => { - const wrapper = mount(); - const renderedInput = wrapper.find('input').instance(); - expect(renderedInput.attributes.title.value).toEqual('my-custom-title'); - }); - - it('focus()', () => { - const container = document.createElement('div'); - document.body.appendChild(container); - const handleFocus = jest.fn(); - const wrapper = mount(, { attachTo: container }); - wrapper.instance().focus(); - expect(handleFocus).toBeCalled(); - }); - - it('blur()', () => { - const container = document.createElement('div'); - document.body.appendChild(container); - const handleBlur = jest.fn(); - const wrapper = mount(, { attachTo: container }); - wrapper.instance().focus(); - wrapper.instance().blur(); - expect(handleBlur).toBeCalled(); - }); - - it('autoFocus', () => { - const container = document.createElement('div'); - document.body.appendChild(container); - const handleFocus = jest.fn(); - mount(, { attachTo: container }); - expect(handleFocus).toBeCalled(); - }); - - it('onChange', () => { - const container = document.createElement('div'); - document.body.appendChild(container); - const onChange = jest.fn(); - const wrapper = mount(, { attachTo: container }); - wrapper.find('input').simulate('change'); - expect(onChange).toBeCalled(); - }); - - it('onChange disabled', () => { - const container = document.createElement('div'); - document.body.appendChild(container); - const onChange = jest.fn(); - const wrapper = mount(, { attachTo: container }); - wrapper.find('input').simulate('change'); - expect(onChange).not.toBeCalled(); - }); - - it('passes required prop to input', () => { - const wrapper = mount(); - const renderedInput = wrapper.find('input').getDOMNode(); - expect(renderedInput.hasAttribute('required')).toBe(true); - }); - - it('onKeyDown', () => { - const container = document.createElement('div'); - document.body.appendChild(container); - const onKeyDown = jest.fn(); - const wrapper = mount(, { attachTo: container }); - wrapper.find('input').simulate('keydown'); - expect(onKeyDown).toBeCalled(); - }); - - it('onKeyPress', () => { - const container = document.createElement('div'); - document.body.appendChild(container); - const onKeyPress = jest.fn(); - const wrapper = mount(, { attachTo: container }); - wrapper.find('input').simulate('keypress'); - expect(onKeyPress).toBeCalled(); - }); - - it('onKeyUp', () => { - const container = document.createElement('div'); - document.body.appendChild(container); - const onKeyUp = jest.fn(); - const wrapper = mount(, { attachTo: container }); - wrapper.find('input').simulate('keyup'); - expect(onKeyUp).toBeCalled(); - }); - - it('has default keyboard events handler', () => { - const onKeyDown = jest.fn(); - const onKeyPress = jest.fn(); - const onKeyUp = jest.fn(); - - const wrapper = mount( -
- -
, - ); - wrapper.find('input').simulate('keydown'); - expect(onKeyDown).toBeCalled(); - wrapper.find('input').simulate('keypress'); - expect(onKeyPress).toBeCalled(); - wrapper.find('input').simulate('keyup'); - expect(onKeyUp).toBeCalled(); - }); -}); diff --git a/tests/index.test.tsx b/tests/index.test.tsx new file mode 100644 index 0000000..ce49697 --- /dev/null +++ b/tests/index.test.tsx @@ -0,0 +1,268 @@ +import * as React from 'react'; +import KeyCode from 'rc-util/lib/KeyCode'; +import { fireEvent, render } from '@testing-library/react'; +import Checkbox from '../src'; + +import type { CheckboxRef } from '../src'; + +describe('rc-checkbox', () => { + it('works', () => { + const { container } = render(); + const inputEl = container.querySelector('input')!; + + expect(inputEl.checked).toBe(false); + + fireEvent.click(inputEl); + expect(inputEl.checked).toBe(true); + + fireEvent.click(inputEl); + expect(inputEl.checked).toBe(false); + }); + + it('click radio', () => { + const { container } = render(); + const inputEl = container.querySelector('input')!; + + expect(inputEl.checked).toBe(false); + + fireEvent.click(inputEl); + expect(inputEl.checked).toBe(true); + + fireEvent.click(inputEl); + expect(inputEl.checked).toBe(true); + }); + + it('click checkbox', () => { + const { container } = render(); + const inputEl = container.querySelector('input')!; + + expect(inputEl.checked).toBe(false); + + fireEvent.click(inputEl); + expect(inputEl.checked).toBe(true); + + fireEvent.click(inputEl); + expect(inputEl.checked).toBe(false); + }); + + it('control mode', () => { + const { container } = render(); + const inputEl = container.querySelector('input')!; + + expect(inputEl.checked).toBe(true); + + fireEvent.click(inputEl); + expect(inputEl.checked).toBe(true); + + fireEvent.click(inputEl); + expect(inputEl.checked).toBe(true); + }); + + it('stopPropagation and preventDefault', () => { + const onChange = jest.fn(); + + const { container } = render( +
+ { + e.stopPropagation(); + e.preventDefault(); + }} + /> +
, + ); + + const inputEl = container.querySelector('input')!; + + fireEvent.click(inputEl); + expect(onChange).not.toHaveBeenCalled(); + }); + + it('passes data-* props to input', () => { + const { container } = render(); + const inputEl = container.querySelector('input')!; + + expect(inputEl.getAttribute('data-type')).toEqual('my-data-type'); + }); + + it('passes aria-* props to input', () => { + const { container } = render(); + const inputEl = container.querySelector('input')!; + + expect(inputEl.getAttribute('aria-label')).toEqual('my-aria-label'); + }); + + it('passes role prop to input', () => { + // eslint-disable-next-line jsx-a11y/aria-role + const { container } = render(); + const inputEl = container.querySelector('input')!; + + // @ts-ignore + expect(inputEl.attributes.role.value).toEqual('my-role'); + }); + + it('passes value prop to input', () => { + const { container } = render(); + const inputEl = container.querySelector('input')!; + + // @ts-ignore + expect(inputEl.attributes.value.value).toEqual('my-custom-value'); + }); + + it('passes number value prop to input', () => { + const { container } = render(); + const inputEl = container.querySelector('input')!; + + // @ts-ignore + expect(inputEl.attributes.value.value).toEqual('6'); + }); + + it('passes title prop to input', () => { + const { container } = render(); + const inputEl = container.querySelector('input')!; + + // @ts-ignore + expect(inputEl.attributes.title.value).toEqual('my-custom-title'); + }); + + it('onFocus', () => { + const onFocus = jest.fn(); + + const { container } = render(); + const inputEl = container.querySelector('input')!; + + fireEvent.focus(inputEl); + + expect(onFocus).toHaveBeenCalledTimes(1); + }); + + it('onBlur', () => { + const onBlur = jest.fn(); + + const { container } = render(); + const inputEl = container.querySelector('input')!; + + fireEvent.focus(inputEl); + fireEvent.blur(inputEl); + + expect(onBlur).toHaveBeenCalledTimes(1); + }); + + it('autoFocus', () => { + const handleFocus = jest.fn(); + + render(); + + expect(handleFocus).toHaveBeenCalledTimes(1); + }); + + it('onChange', () => { + const onChange = jest.fn(); + + const { container } = render(); + const inputEl = container.querySelector('input')!; + + fireEvent.click(inputEl); + + expect(onChange).toHaveBeenCalledTimes(1); + expect(inputEl.checked).toBe(true); + }); + + it('onChange disabled', () => { + const onChange = jest.fn(); + + const { container } = render(); + const input = container.querySelector('input')!; + + fireEvent.click(input); + + expect(onChange).not.toHaveBeenCalled(); + }); + + it('passes required prop to input', () => { + const { container } = render(); + + const inputEl = container.querySelector('input')!; + expect(inputEl.hasAttribute('required')).toBe(true); + }); + + it('onKeyDown', () => { + const onKeyDown = jest.fn(); + + const { container } = render(); + const inputEl = container.querySelector('input')!; + + fireEvent.keyDown(inputEl, { which: KeyCode.TAB }); + + expect(onKeyDown).toHaveBeenCalledTimes(1); + }); + + it('onKeyPress', () => { + const onKeyPress = jest.fn(); + + const { container } = render(); + const inputEl = container.querySelector('input')!; + + // https://github.com/testing-library/react-testing-library/issues/269 + fireEvent.keyPress(inputEl, { key: 'Enter', code: 13, charCode: 13 }); + + expect(onKeyPress).toHaveBeenCalledTimes(1); + }); + + it('onKeyUp', () => { + const onKeyUp = jest.fn(); + + const { container } = render(); + const inputEl = container.querySelector('input')!; + + fireEvent.keyUp(inputEl, { which: KeyCode.TAB }); + + expect(onKeyUp).toHaveBeenCalledTimes(1); + }); + + it('has default keyboard events handler', () => { + const onKeyDown = jest.fn(); + const onKeyPress = jest.fn(); + const onKeyUp = jest.fn(); + + const { container } = render( +
+ +
, + ); + const inputEl = container.querySelector('input')!; + + fireEvent.keyDown(inputEl, { which: KeyCode.TAB }); + expect(onKeyDown).toHaveBeenCalledTimes(1); + + fireEvent.keyPress(inputEl, { key: 'Enter', code: 13, charCode: 13 }); + expect(onKeyPress).toHaveBeenCalledTimes(1); + + fireEvent.keyUp(inputEl, { which: KeyCode.TAB }); + expect(onKeyUp).toHaveBeenCalledTimes(1); + }); +}); + +describe('Checkbox ref', () => { + it('focus and blur should work', () => { + const ref = React.createRef(); + + const { container } = render(); + const inputEl = container.querySelector('input')!; + + ref.current?.focus(); + expect(document.activeElement).toBe(inputEl); + + ref.current?.blur(); + expect(document.activeElement).not.toBe(inputEl); + }); + + it('input should work', () => { + const ref = React.createRef(); + + const { container } = render(); + const inputEl = container.querySelector('input')!; + + expect(ref.current?.input).toBe(inputEl); + }); +});