Skip to content

refactor hook and support ref #47

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

Merged
merged 10 commits into from
May 17, 2020
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
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.eslintrc.js
8 changes: 8 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"endOfLine": "lf",
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 100
}
1 change: 0 additions & 1 deletion examples/simple.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ export default class Simple extends React.Component {
<div style={{ margin: 20 }}>
<Switch
onChange={onChange}
onClick={onChange}
disabled={disabled}
checkedChildren="开"
unCheckedChildren="关"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"compile": "father build && lessc assets/index.less assets/index.css",
"gh-pages": "father doc deploy",
"prepublishOnly": "npm run compile && np --no-cleanup --yolo --no-publish",
"lint": "eslint src/ --ext .jsx,.js,.md",
"lint": "eslint .",
"test": "father test",
"coverage": "father test --coverage"
},
Expand Down
186 changes: 79 additions & 107 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { Component, MouseEventHandler } from 'react';
import * as React from 'react';
import classNames from 'classnames';

export type SwitchChangeEventHandler = (checked: boolean, event: MouseEvent) => void;
Expand All @@ -11,7 +11,7 @@ interface SwitchProps {
checkedChildren?: React.ReactNode;
unCheckedChildren?: React.ReactNode;
onChange?: SwitchChangeEventHandler;
onMouseUp: MouseEventHandler<HTMLButtonElement>;
onMouseUp: React.MouseEventHandler<HTMLButtonElement>;
onClick?: SwitchClickEventHandler;
tabIndex?: number;
checked?: boolean;
Expand All @@ -22,143 +22,115 @@ interface SwitchProps {
title?: string;
}

interface SwitchState {
checked: boolean;
}

class Switch extends Component<SwitchProps, SwitchState> {
private node: React.RefObject<HTMLButtonElement>;
const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>((props, ref) => {
const mergedRef = (ref as any) || React.createRef<HTMLButtonElement>();

static defaultProps = {
prefixCls: 'rc-switch',
checkedChildren: null,
unCheckedChildren: null,
className: '',
defaultChecked: false,
};

constructor(props) {
super(props);
let checked = false;
if ('checked' in props) {
checked = !!props.checked;
} else {
checked = !!props.defaultChecked;
}
this.state = { checked };
this.node = React.createRef();
let initChecked = false;
if ('checked' in props) {
initChecked = !!props.checked;
} else {
initChecked = !!props.defaultChecked;
}
const [checked, setChecked] = React.useState(initChecked);

componentDidMount() {
const { autoFocus, disabled } = this.props;
React.useEffect(() => {
const { autoFocus, disabled } = props;
if (autoFocus && !disabled) {
this.focus();
focus();
Copy link
Member

Choose a reason for hiding this comment

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

这个 focus 是无源之水……

Copy link
Member Author

Choose a reason for hiding this comment

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

我处理下 ,批量替换了this,... 竟然不报错

Copy link
Member Author

Choose a reason for hiding this comment

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

单元测试 覆盖到了,为什么会通过

}
}
}, [props.autoFocus, props.disabled]);
Copy link
Member

@zombieJ zombieJ May 18, 2020

Choose a reason for hiding this comment

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

disabled 切换会反复 autoFocus.


static getDerivedStateFromProps(nextProps) {
const { checked } = nextProps;
const newState: Partial<SwitchState> = {};
if ('checked' in nextProps) {
newState.checked = !!checked;
React.useEffect(() => {
if ('checked' in props) {
setChecked(!!props.checked);
}
return newState;
}
}, [props.checked]);

setChecked(checked, e) {
const { disabled, onChange } = this.props;
const setInternalChecked = (checked, e) => {
const { disabled, onChange } = props;
if (disabled) {
return;
}
if (!('checked' in this.props)) {
this.setState({
checked,
});
if (!('checked' in props)) {
setChecked(checked);
}
if (onChange) {
onChange(checked, e);
}
}
};

handleClick = e => {
const { checked } = this.state;
const { onClick } = this.props;
const handleClick = e => {
const { onClick } = props;
const newChecked = !checked;
this.setChecked(newChecked, e);
setInternalChecked(newChecked, e);
if (onClick) {
onClick(newChecked, e);
}
};

handleKeyDown = e => {
const handleKeyDown = e => {
if (e.keyCode === 37) {
// Left
this.setChecked(false, e);
setInternalChecked(false, e);
} else if (e.keyCode === 39) {
// Right
this.setChecked(true, e);
setInternalChecked(true, e);
}
};

// Handle auto focus when click switch in Chrome
handleMouseUp = e => {
const { onMouseUp } = this.props;
this.blur();
if (onMouseUp) {
onMouseUp(e);
const handleMouseUp = e => {
(mergedRef.current as any).blur();
if (props.onMouseUp) {
props.onMouseUp(e);
}
};

focus() {
if (this.node.current) {
this.node.current.focus();
}
}

blur() {
if (this.node.current) {
this.node.current.blur();
}
}

render() {
const {
className,
prefixCls,
disabled,
loadingIcon,
checkedChildren,
unCheckedChildren,
onChange,
...restProps
} = this.props;
const { checked } = this.state;
const switchClassName = classNames({
[className]: !!className,
[prefixCls]: true,
[`${prefixCls}-checked`]: checked,
[`${prefixCls}-disabled`]: disabled,
});
return (
<button
{...restProps}
type="button"
role="switch"
aria-checked={checked}
disabled={disabled}
className={switchClassName}
ref={this.node}
onKeyDown={this.handleKeyDown}
onClick={this.handleClick}
onMouseUp={this.handleMouseUp}
>
{loadingIcon}
<span className={`${prefixCls}-inner`}>
{checked ? checkedChildren : unCheckedChildren}
</span>
</button>
);
}
}
const {
className,
prefixCls,
disabled,
loadingIcon,
checkedChildren,
unCheckedChildren,
onChange,
...restProps
} = props;

const switchClassName = classNames({
[className]: !!className,
[prefixCls]: true,
[`${prefixCls}-checked`]: checked,
[`${prefixCls}-disabled`]: disabled,
});

return (
<button
{...restProps}
type="button"
role="switch"
aria-checked={checked}
disabled={disabled}
className={switchClassName}
ref={mergedRef}
onKeyDown={handleKeyDown}
onClick={handleClick}
onMouseUp={handleMouseUp}
>
{loadingIcon}
<span className={`${prefixCls}-inner`}>{checked ? checkedChildren : unCheckedChildren}</span>
</button>
);
});

Switch.displayName = 'Switch';

Switch.defaultProps = {
prefixCls: 'rc-switch',
checkedChildren: null,
unCheckedChildren: null,
className: '',
defaultChecked: false,
};

export default Switch;
77 changes: 53 additions & 24 deletions tests/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,44 @@ import { mount } from 'enzyme';
import Switch from '../index';

describe('rc-switch', () => {
let switcher;
beforeEach(() => {
switcher = mount(<Switch />);
});
function createSwitch(props = {}) {
return mount(
<Switch
checkedChildren={<span className="checked" />}
unCheckedChildren={<span className="unchecked" />}
{...props}
/>,
);
}

it('works', () => {
expect(switcher.state().checked).toBe(false);
switcher.simulate('click');
expect(switcher.state().checked).toBe(true);
const wrapper = createSwitch();
expect(wrapper.exists('.unchecked')).toBeTruthy();
wrapper.simulate('click');
expect(wrapper.exists('.checked')).toBeTruthy();
});

it('should be checked upon right key and unchecked on left key', () => {
expect(switcher.state().checked).toBe(false);
switcher.simulate('keydown', { keyCode: 39 });
expect(switcher.state().checked).toBe(true);
switcher.simulate('keydown', { keyCode: 37 });
expect(switcher.state().checked).toBe(false);
const wrapper = createSwitch();
expect(wrapper.exists('.unchecked')).toBeTruthy();
wrapper.simulate('keydown', { keyCode: 39 });
expect(wrapper.exists('.checked')).toBeTruthy();
wrapper.simulate('keydown', { keyCode: 37 });
expect(wrapper.exists('.unchecked')).toBeTruthy();
});

it('should change from an initial checked state of true to false on click', () => {
const wrapper = mount(<Switch defaultChecked />);
expect(wrapper.state().checked).toBe(true);
const onChange = jest.fn();
const wrapper = createSwitch({ defaultChecked: true, onChange });
expect(wrapper.exists('.checked')).toBeTruthy();
wrapper.simulate('click');
expect(wrapper.state().checked).toBe(false);
expect(wrapper.exists('.unchecked')).toBeTruthy();
expect(onChange.mock.calls.length).toBe(1);
});

it('should support onClick', () => {
const onClick = jest.fn();
const wrapper = mount(<Switch onClick={onClick} />);
const wrapper = createSwitch({ onClick });
wrapper.simulate('click');
expect(onClick).toHaveBeenCalledWith(true, expect.objectContaining({ type: 'click' }));
expect(onClick.mock.calls.length).toBe(1);
Expand All @@ -43,10 +52,10 @@ describe('rc-switch', () => {

it('should not toggle when clicked in a disabled state', () => {
const onChange = jest.fn();
const wrapper = mount(<Switch disabled checked onChange={onChange} />);
expect(wrapper.state().checked).toBe(true);
const wrapper = createSwitch({ disabled: true, checked: true, onChange });
expect(wrapper.exists('.checked')).toBeTruthy();
wrapper.simulate('click');
expect(wrapper.state().checked).toBe(true);
expect(wrapper.exists('.checked')).toBeTruthy();
expect(onChange.mock.calls.length).toBe(0);
});

Expand All @@ -59,18 +68,24 @@ describe('rc-switch', () => {
const container = document.createElement('div');
document.body.appendChild(container);
const handleFocus = jest.fn();
const wrapper = mount(<Switch onFocus={handleFocus} />, { attachTo: container });
wrapper.instance().focus();
const ref = React.createRef();
mount(<Switch ref={ref} onFocus={handleFocus} />, {
attachTo: container,
});
ref.current.focus();
expect(handleFocus).toHaveBeenCalled();
});

it('blur()', () => {
const container = document.createElement('div');
document.body.appendChild(container);
const handleBlur = jest.fn();
const wrapper = mount(<Switch onBlur={handleBlur} />, { attachTo: container });
wrapper.instance().focus();
wrapper.instance().blur();
const ref = React.createRef();
mount(<Switch ref={ref} onBlur={handleBlur} />, {
attachTo: container,
});
ref.current.focus();
ref.current.blur();
expect(handleBlur).toHaveBeenCalled();
});

Expand All @@ -81,4 +96,18 @@ describe('rc-switch', () => {
mount(<Switch autoFocus onFocus={handleFocus} />, { attachTo: container });
expect(handleFocus).toHaveBeenCalled();
});

it('disabled', () => {
const wrapper = createSwitch({ disabled: true });
expect(wrapper.exists('.unchecked')).toBeTruthy();
wrapper.simulate('keydown', { keyCode: 39 });
expect(wrapper.exists('.unchecked')).toBeTruthy();
});

it('onMouseUp', () => {
const onMouseUp = jest.fn();
const wrapper = createSwitch({ onMouseUp });
wrapper.simulate('mouseup');
expect(onMouseUp).toHaveBeenCalled();
});
});