From 7515739b525154ab4be8456f076053628f3301c1 Mon Sep 17 00:00:00 2001 From: liujuping Date: Tue, 16 Jan 2024 16:00:40 +0800 Subject: [PATCH] feat(Checkbox): upgrade checkbox ts --- ...{checkbox-group.jsx => checkbox-group.tsx} | 135 ++++++++++++------ .../checkbox/{checkbox.jsx => checkbox.tsx} | 76 +++++++--- components/checkbox/{index.jsx => index.tsx} | 2 + .../checkbox/mobile/{index.jsx => index.tsx} | 0 components/checkbox/{style.js => style.ts} | 0 components/checkbox/{index.d.ts => types.ts} | 55 ++++--- components/checkbox/with-context.jsx | 19 --- components/checkbox/with-context.tsx | 31 ++++ .../mixin-ui-state/{index.jsx => index.tsx} | 30 ++-- 9 files changed, 230 insertions(+), 118 deletions(-) rename components/checkbox/{checkbox-group.jsx => checkbox-group.tsx} (60%) rename components/checkbox/{checkbox.jsx => checkbox.tsx} (78%) rename components/checkbox/{index.jsx => index.tsx} (90%) rename components/checkbox/mobile/{index.jsx => index.tsx} (100%) rename components/checkbox/{style.js => style.ts} (100%) rename components/checkbox/{index.d.ts => types.ts} (69%) delete mode 100644 components/checkbox/with-context.jsx create mode 100644 components/checkbox/with-context.tsx rename components/mixin-ui-state/{index.jsx => index.tsx} (62%) diff --git a/components/checkbox/checkbox-group.jsx b/components/checkbox/checkbox-group.tsx similarity index 60% rename from components/checkbox/checkbox-group.jsx rename to components/checkbox/checkbox-group.tsx index ea2f56a964..219922bca4 100644 --- a/components/checkbox/checkbox-group.jsx +++ b/components/checkbox/checkbox-group.tsx @@ -1,14 +1,23 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; +import * as React from 'react'; +import * as PropTypes from 'prop-types'; import classnames from 'classnames'; import { polyfill } from 'react-lifecycles-compat'; import { obj } from '../util'; import Checkbox from './checkbox'; +import type { CheckboxData, GroupProps } from './types'; +import { clone } from 'lodash'; const { pickOthers } = obj; +interface GroupState { + value: T[]; +} + /** Checkbox.Group */ -class CheckboxGroup extends Component { +class CheckboxGroup extends React.Component< + GroupProps, + GroupState +> { static propTypes = { prefix: PropTypes.string, rtl: PropTypes.bool, @@ -27,15 +36,28 @@ class CheckboxGroup extends Component { /** * 可选项列表, 数据项可为 String 或者 Object, 如 `['apple', 'pear', 'orange']` 或者 `[{value: 'apple', label: '苹果',}, {value: 'pear', label: '梨'}, {value: 'orange', label: '橙子'}]` */ - dataSource: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.string), PropTypes.arrayOf(PropTypes.object)]), + dataSource: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.string), + PropTypes.arrayOf(PropTypes.object), + ]), /** * 被选中的值列表 */ - value: PropTypes.oneOfType([PropTypes.array, PropTypes.string, PropTypes.number, PropTypes.bool]), + value: PropTypes.oneOfType([ + PropTypes.array, + PropTypes.string, + PropTypes.number, + PropTypes.bool, + ]), /** * 默认被选中的值列表 */ - defaultValue: PropTypes.oneOfType([PropTypes.array, PropTypes.string, PropTypes.number, PropTypes.bool]), + defaultValue: PropTypes.oneOfType([ + PropTypes.array, + PropTypes.string, + PropTypes.number, + PropTypes.bool, + ]), /** * 通过子元素方式设置内部 checkbox */ @@ -83,10 +105,11 @@ class CheckboxGroup extends Component { disabled: PropTypes.bool, }; - constructor(props) { + constructor(props: GroupProps) { super(props); - let value = []; + let value: GroupProps['value'] = []; + let formatValue: GroupState['value'] = []; if ('value' in props) { value = props.value; } else if ('defaultValue' in props) { @@ -94,13 +117,19 @@ class CheckboxGroup extends Component { } if (!Array.isArray(value)) { if (value === null || value === undefined) { - value = []; - } else { - value = [value]; + formatValue = []; + } else if (typeof value === 'string') { + formatValue = [value]; + } else if (typeof value === 'number') { + formatValue = [value]; + } else if (typeof value === 'boolean') { + formatValue = [value]; } + } else { + formatValue = value; } this.state = { - value: [...value], + value: formatValue, }; this.onChange = this.onChange.bind(this); @@ -115,27 +144,34 @@ class CheckboxGroup extends Component { }; } - static getDerivedStateFromProps(nextProps) { + static getDerivedStateFromProps(nextProps: GroupProps) { if ('value' in nextProps) { let { value } = nextProps; + let formatValue: GroupState['value'] = []; if (!Array.isArray(value)) { if (value === null || value === undefined) { - value = []; - } else { - value = [value]; + formatValue = []; + } else if (typeof value === 'string') { + formatValue = [value]; + } else if (typeof value === 'number') { + formatValue = [value]; + } else if (typeof value === 'boolean') { + formatValue = [value]; } + } else { + formatValue = value; } - return { value }; + return { value: formatValue }; } return null; } - onChange(currentValue, e) { + onChange(currentValue: T, event: React.ChangeEvent) { const { value } = this.state; const index = value.indexOf(currentValue); - const valTemp = [...value]; + const valTemp = clone(value); if (index === -1) { valTemp.push(currentValue); @@ -146,41 +182,57 @@ class CheckboxGroup extends Component { if (!('value' in this.props)) { this.setState({ value: valTemp }); } - this.props.onChange(valTemp, e); + this.props.onChange?.(valTemp, event); } render() { - const { className, style, prefix, disabled, direction, rtl, isPreview, renderPreview } = this.props; + const { className, style, prefix, disabled, direction, rtl, isPreview, renderPreview } = + this.props; const others = pickOthers(CheckboxGroup.propTypes, this.props); // 如果内嵌标签跟dataSource同时存在,以内嵌标签为主 let children; - const previewed = []; + const previewed: { + label: string | React.ReactNode; + value: string | React.ReactNode; + }[] = []; if (this.props.children) { - children = React.Children.map(this.props.children, child => { - if (!React.isValidElement(child)) { - return child; - } - const checked = this.state.value && this.state.value.indexOf(child.props.value) > -1; + children = React.Children.map( + this.props.children, + ( + child: React.ReactElement<{ + value: T; + children?: string; + rtl?: boolean; + }> + ) => { + if (!React.isValidElement(child)) { + return child; + } + const checked = + this.state.value && this.state.value.indexOf(child.props?.value) > -1; - if (checked) { - previewed.push({ - label: child.props.children, - value: child.props.value, - }); - } + if (checked) { + previewed.push({ + label: child.props?.children, + value: child.props?.value, + }); + } - return React.cloneElement(child, child.props.rtl === undefined ? { rtl } : null); - }); + return React.cloneElement(child, child.props?.rtl === undefined ? { rtl } : {}); + } + ); } else { - children = this.props.dataSource.map((item, index) => { - let option = item; + children = this.props.dataSource?.map((item, index) => { + let option: CheckboxData; if (typeof item !== 'object') { option = { label: item, value: item, disabled, }; + } else { + option = item; } const checked = this.state.value && this.state.value.indexOf(option.value) > -1; @@ -210,7 +262,7 @@ class CheckboxGroup extends Component { if ('renderPreview' in this.props) { return (
- {renderPreview(previewed, this.props)} + {renderPreview?.(previewed, this.props)}
); } @@ -222,10 +274,9 @@ class CheckboxGroup extends Component { ); } - const cls = classnames({ + const cls = classnames(className, { [`${prefix}checkbox-group`]: true, [`${prefix}checkbox-group-${direction}`]: true, - [className]: !!className, disabled, }); @@ -237,4 +288,6 @@ class CheckboxGroup extends Component { } } -export default polyfill(CheckboxGroup); +export default polyfill>( + CheckboxGroup as React.ComponentType +); diff --git a/components/checkbox/checkbox.jsx b/components/checkbox/checkbox.tsx similarity index 78% rename from components/checkbox/checkbox.jsx rename to components/checkbox/checkbox.tsx index 241cb30e47..5f5ecb4f22 100644 --- a/components/checkbox/checkbox.jsx +++ b/components/checkbox/checkbox.tsx @@ -1,22 +1,37 @@ -import React from 'react'; -import PropTypes from 'prop-types'; +import * as React from 'react'; +import * as PropTypes from 'prop-types'; import classnames from 'classnames'; import { polyfill } from 'react-lifecycles-compat'; -import UIState from '../mixin-ui-state'; +import UIState, { UIStateState } from '../mixin-ui-state'; import ConfigProvider from '../config-provider'; import Icon from '../icon'; -import withContext from './with-context'; +import withCheckboxContext, { CheckboxContext } from './with-context'; import { obj, func } from '../util'; +import type { CheckboxProps } from './types'; const noop = func.noop; -function isChecked(selectedValue, value) { +function isChecked( + selectedValue: CheckboxContext['selectedValue'], + value: CheckboxProps['value'] +): boolean { return selectedValue.indexOf(value) > -1; } + +interface CheckboxState extends UIStateState { + value?: CheckboxProps['value']; + checked?: boolean; + indeterminate?: boolean; +} + +export interface PrivateCheckboxProps extends CheckboxProps { + context: CheckboxContext; +} + /** * Checkbox * @order 1 */ -class Checkbox extends UIState { +class Checkbox extends UIState { static displayName = 'Checkbox'; static propTypes = { ...ConfigProvider.propTypes, @@ -107,7 +122,7 @@ class Checkbox extends UIState { isPreview: false, }; - constructor(props) { + constructor(props: PrivateCheckboxProps) { super(props); const { context } = props; let checked, indeterminate; @@ -134,9 +149,9 @@ class Checkbox extends UIState { this.onChange = this.onChange.bind(this); } - static getDerivedStateFromProps(nextProps) { + static getDerivedStateFromProps(nextProps: PrivateCheckboxProps) { const { context: nextContext } = nextProps; - const state = {}; + const state: CheckboxState = {}; if (nextContext.__group__) { if ('selectedValue' in nextContext) { state.checked = isChecked(nextContext.selectedValue, nextProps.value); @@ -159,7 +174,11 @@ class Checkbox extends UIState { return props.disabled || ('disabled' in context && context.disabled); } - shouldComponentUpdate(nextProps, nextState, nextContext) { + shouldComponentUpdate( + nextProps: PrivateCheckboxProps, + nextState: CheckboxState, + nextContext: CheckboxContext + ) { const { shallowEqual } = obj; return ( !shallowEqual(this.props, nextProps) || @@ -168,15 +187,15 @@ class Checkbox extends UIState { ); } - onChange(e) { + onChange(event: React.ChangeEvent) { const { context, value } = this.props; - const checked = e.target.checked; + const checked = event.target.checked; if (this.disabled) { return; } if (context.__group__) { - context.onChange(value, e); + context.onChange(value, event); } else { if (!('checked' in this.props)) { this.setState({ @@ -189,7 +208,7 @@ class Checkbox extends UIState { indeterminate: false, }); } - this.props.onChange(checked, e); + this.props.onChange?.(checked, event); } } @@ -226,7 +245,7 @@ class Checkbox extends UIState { - {renderPreview(checked, this.props)} +
+ {renderPreview?.(checked, this.props)}
); } @@ -290,8 +313,8 @@ class Checkbox extends UIState { {childInput} - {[label, children].map((item, i) => - [undefined, null].indexOf(item) === -1 ? ( + {[label, children].map((item: React.ReactNode | undefined | null, i) => + item !== undefined && item !== null ? ( {item} @@ -302,4 +325,13 @@ class Checkbox extends UIState { } } -export default ConfigProvider.config(withContext(polyfill(Checkbox))); +// 这里的 Checkbox as React.ComponentType)) 是由于 ConfigProvider.propTypes 和 ComponentCommonProps 有冲突导致的 +export default ConfigProvider.config< + React.ComponentType & { Group?: React.ComponentType } +>( + withCheckboxContext( + polyfill>( + Checkbox as React.ComponentType + ) + ) +); diff --git a/components/checkbox/index.jsx b/components/checkbox/index.tsx similarity index 90% rename from components/checkbox/index.jsx rename to components/checkbox/index.tsx index 37c0c44f0b..ad36fdabe4 100644 --- a/components/checkbox/index.jsx +++ b/components/checkbox/index.tsx @@ -15,4 +15,6 @@ Checkbox.Group = ConfigProvider.config(Group, { }, }); +export type { CheckboxProps, GroupProps } from './types'; + export default Checkbox; diff --git a/components/checkbox/mobile/index.jsx b/components/checkbox/mobile/index.tsx similarity index 100% rename from components/checkbox/mobile/index.jsx rename to components/checkbox/mobile/index.tsx diff --git a/components/checkbox/style.js b/components/checkbox/style.ts similarity index 100% rename from components/checkbox/style.js rename to components/checkbox/style.ts diff --git a/components/checkbox/index.d.ts b/components/checkbox/types.ts similarity index 69% rename from components/checkbox/index.d.ts rename to components/checkbox/types.ts index 950a4df4fa..58700c0c88 100644 --- a/components/checkbox/index.d.ts +++ b/components/checkbox/types.ts @@ -1,23 +1,19 @@ -/// - -import React from 'react'; +import * as React from 'react'; import { CommonProps } from '../util'; -interface HTMLAttributesWeak extends React.HTMLAttributes { - defaultValue?: any; - onChange?: any; -} +interface HTMLAttributesWeak + extends Omit, 'onChange' | 'defaultValue'> {} -type data = { - value?: string | number | boolean; +export type CheckboxData = { + value: T; label?: React.ReactNode; disabled?: boolean; [propName: string]: any; }; -export type CheckboxData = data; - -export interface GroupProps extends HTMLAttributesWeak, CommonProps { +export interface GroupProps + extends HTMLAttributesWeak, + CommonProps { /** * 自定义类名 */ @@ -38,22 +34,28 @@ export interface GroupProps extends HTMLAttributesWeak, CommonProps { */ isPreview?: boolean; - renderPreview?: (checked: boolean, props: object) => React.ReactNode; + renderPreview?: ( + checked: { + label: string | React.ReactNode; + value: string | React.ReactNode; + }[], + props: object + ) => React.ReactNode; /** * 可选项列表, 数据项可为 String 或者 Object, 如 `['apple', 'pear', 'orange']` 或者 `[{value: 'apple', label: '苹果',}, {value: 'pear', label: '梨'}, {value: 'orange', label: '橙子'}]` */ - dataSource?: Array | Array | Array; + dataSource?: Array | Array>; /** * 被选中的值列表 */ - value?: Array | Array | Array | string | number | boolean; + value?: T[] | T; /** * 默认被选中的值列表 */ - defaultValue?: Array | Array | Array | string | number | boolean; + defaultValue?: T[] | T; /** * name @@ -63,12 +65,12 @@ export interface GroupProps extends HTMLAttributesWeak, CommonProps { /** * 通过子元素方式设置内部 checkbox */ - children?: Array; + children?: Array; /** * 选中值改变时的事件 */ - onChange?: (value: Array | Array | Array, e: any) => void; + onChange?: (value: T[], e: MouseEvent) => void; /** * 子项目的排列方式 @@ -79,12 +81,7 @@ export interface GroupProps extends HTMLAttributesWeak, CommonProps { itemDirection?: 'hoz' | 'ver'; } -export class Group extends React.Component {} -interface HTMLAttributesWeak extends React.HTMLAttributes { - onChange?: any; - onMouseEnter?: any; - onMouseLeave?: any; -} +export class Group extends React.Component {} export interface CheckboxProps extends HTMLAttributesWeak, CommonProps { /** @@ -92,6 +89,8 @@ export interface CheckboxProps extends HTMLAttributesWeak, CommonProps { */ className?: string; + renderPreview?: (checked: boolean, props: object) => React.ReactNode; + /** * checkbox id, 挂载在input上 */ @@ -150,19 +149,19 @@ export interface CheckboxProps extends HTMLAttributesWeak, CommonProps { /** * 状态变化时触发的事件 */ - onChange?: (checked: boolean, e: any) => void; + onChange?: (checked: boolean, e: React.MouseEvent) => void; /** * 鼠标进入enter事件 */ - onMouseEnter?: (e: React.MouseEvent) => void; + onMouseEnter?: (e: React.MouseEvent) => void; /** * 鼠标离开Leave事件 */ - onMouseLeave?: (e: React.MouseEvent) => void; + onMouseLeave?: (e: React.MouseEvent) => void; } -export default class Checkbox extends React.Component { +export default class Checkbox extends React.Component { static Group: typeof Group; } diff --git a/components/checkbox/with-context.jsx b/components/checkbox/with-context.jsx deleted file mode 100644 index a5b5857bec..0000000000 --- a/components/checkbox/with-context.jsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -export default function withContext(Checkbox) { - return class WrappedComp extends React.Component { - static displayName = 'Checkbox'; - static contextTypes = { - onChange: PropTypes.func, - __group__: PropTypes.bool, - selectedValue: PropTypes.array, - disabled: PropTypes.bool, - prefix: PropTypes.string, - }; - - render() { - return ; - } - }; -} diff --git a/components/checkbox/with-context.tsx b/components/checkbox/with-context.tsx new file mode 100644 index 0000000000..f621c0f8b9 --- /dev/null +++ b/components/checkbox/with-context.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; +import * as PropTypes from 'prop-types'; +import { PrivateCheckboxProps } from './checkbox'; +import { CheckboxProps } from './types'; + +export interface CheckboxContext { + onChange: (value: boolean, event: React.ChangeEvent) => void; + __group__: boolean; + selectedValue: PrivateCheckboxProps['value'][]; + disabled: boolean; + prefix: string; +} + +export default function withCheckboxContext( + Checkbox: React.ComponentType +): React.ComponentType { + return class WrappedComp extends React.Component { + static displayName = 'Checkbox'; + static contextTypes = { + onChange: PropTypes.func, + __group__: PropTypes.bool, + selectedValue: PropTypes.array, + disabled: PropTypes.bool, + prefix: PropTypes.string, + }; + + render() { + return ; + } + }; +} diff --git a/components/mixin-ui-state/index.jsx b/components/mixin-ui-state/index.tsx similarity index 62% rename from components/mixin-ui-state/index.jsx rename to components/mixin-ui-state/index.tsx index 60ec12f71f..6280f6b1c4 100644 --- a/components/mixin-ui-state/index.jsx +++ b/components/mixin-ui-state/index.tsx @@ -1,23 +1,37 @@ -import React, { Component } from 'react'; +import * as React from 'react'; import classnames from 'classnames'; import { func } from '../util'; +interface UIStateProps { + onFocus?: React.FocusEventHandler; + onBlur?: React.FocusEventHandler; + // 你可以在这里添加更多的 props 类型定义 +} + +export interface UIStateState { + focused?: boolean; + // 如果有其他的 state 属性,也可以在这里定义 +} + const { makeChain } = func; // UIState 为一些特殊元素的状态响应提供了标准的方式, // 尤其适合CSS无法完全定制的控件,比如checkbox,radio等。 // 若组件 disable 则自行判断是否需要绑定状态管理。 // 注意:disable 不会触发事件,请使用resetUIState还原状态 /* eslint-disable react/prop-types */ -class UIState extends Component { - constructor(props) { +class UIState< + P extends UIStateProps = UIStateProps, + S extends UIStateState = UIStateState, +> extends React.Component { + state: S = {} as S; + + constructor(props: P) { super(props); - this.state = {}; - ['_onUIFocus', '_onUIBlur'].forEach(item => { - this[item] = this[item].bind(this); - }); + this._onUIFocus = this._onUIFocus.bind(this); + this._onUIBlur = this._onUIBlur.bind(this); } // base 事件绑定的元素 - getStateElement(base) { + getStateElement(base: React.ReactElement): React.ReactElement { const { onFocus, onBlur } = this.props; return React.cloneElement(base, { onFocus: makeChain(this._onUIFocus, onFocus),