1
0

checkbox.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. import React from 'react';
  2. import PropTypes from 'prop-types';
  3. import classnames from 'classnames';
  4. import { checkboxClasses as css, strings } from '@douyinfe/semi-foundation/checkbox/constants';
  5. import CheckboxFoundation, { CheckboxAdapter, BasicCheckboxEvent, BasicTargetObject, BaseCheckboxProps } from '@douyinfe/semi-foundation/checkbox/checkboxFoundation';
  6. import CheckboxInner from './checkboxInner';
  7. import BaseComponent from '../_base/baseComponent';
  8. import '@douyinfe/semi-foundation/checkbox/checkbox.scss';
  9. import { Context, CheckboxContextType } from './context';
  10. import { isUndefined, isBoolean, noop } from 'lodash';
  11. import { getUuidShort } from '@douyinfe/semi-foundation/utils/uuid';
  12. import { CheckboxType } from './checkboxGroup';
  13. export interface CheckboxEvent extends BasicCheckboxEvent {
  14. nativeEvent: {
  15. stopImmediatePropagation: () => void
  16. }
  17. }
  18. export type TargetObject = BasicTargetObject;
  19. export interface CheckboxProps extends BaseCheckboxProps {
  20. 'aria-describedby'?: React.AriaAttributes['aria-describedby'];
  21. 'aria-errormessage'?: React.AriaAttributes['aria-errormessage'];
  22. 'aria-invalid'?: React.AriaAttributes['aria-invalid'];
  23. 'aria-labelledby'?: React.AriaAttributes['aria-labelledby'];
  24. 'aria-required'?: React.AriaAttributes['aria-required'];
  25. children?: React.ReactNode;
  26. onChange?: (e: CheckboxEvent) => any;
  27. // TODO, docs
  28. style?: React.CSSProperties;
  29. onMouseEnter?: React.MouseEventHandler<HTMLSpanElement>;
  30. onMouseLeave?: React.MouseEventHandler<HTMLSpanElement>;
  31. extra?: React.ReactNode;
  32. 'aria-label'?: React.AriaAttributes['aria-label'];
  33. role?: React.HTMLAttributes<HTMLSpanElement>['role']; // a11y: wrapper role
  34. tabIndex?: number; // a11y: wrapper tabIndex
  35. addonId?: string;
  36. extraId?: string;
  37. type?: CheckboxType
  38. }
  39. interface CheckboxState {
  40. checked: boolean;
  41. addonId?: string;
  42. extraId?: string;
  43. focusVisible?: boolean
  44. }
  45. class Checkbox extends BaseComponent<CheckboxProps, CheckboxState> {
  46. static contextType = Context;
  47. static propTypes = {
  48. 'aria-describedby': PropTypes.string,
  49. 'aria-errormessage': PropTypes.string,
  50. 'aria-invalid': PropTypes.bool,
  51. 'aria-labelledby': PropTypes.string,
  52. 'aria-required': PropTypes.bool,
  53. // Specifies whether it is currently selected
  54. checked: PropTypes.bool,
  55. // Initial check
  56. defaultChecked: PropTypes.bool,
  57. // Failure state
  58. disabled: PropTypes.bool,
  59. // Set indeterminate state, only responsible for style control
  60. indeterminate: PropTypes.bool,
  61. // Callback function when changing
  62. onChange: PropTypes.func,
  63. value: PropTypes.any,
  64. style: PropTypes.object,
  65. className: PropTypes.string,
  66. prefixCls: PropTypes.string,
  67. onMouseEnter: PropTypes.func,
  68. onMouseLeave: PropTypes.func,
  69. extra: PropTypes.node,
  70. index: PropTypes.number,
  71. 'aria-label': PropTypes.string,
  72. tabIndex: PropTypes.number,
  73. preventScroll: PropTypes.bool,
  74. type: PropTypes.string,
  75. };
  76. static defaultProps = {
  77. defaultChecked: false,
  78. indeterminate: false,
  79. onChange: noop,
  80. onMouseEnter: noop,
  81. onMouseLeave: noop,
  82. type: 'default',
  83. };
  84. static elementType: string;
  85. checkboxEntity: CheckboxInner;
  86. context: CheckboxContextType;
  87. get adapter(): CheckboxAdapter<CheckboxProps, CheckboxState> {
  88. return {
  89. ...super.adapter,
  90. setNativeControlChecked: checked => {
  91. this.setState({ checked });
  92. },
  93. notifyChange: cbContent => {
  94. const { onChange } = this.props;
  95. onChange && onChange(cbContent);
  96. },
  97. generateEvent: (checked, e) => {
  98. const { props } = this;
  99. const cbValue = {
  100. target: {
  101. ...props,
  102. checked,
  103. },
  104. stopPropagation: () => {
  105. e.stopPropagation();
  106. },
  107. preventDefault: () => {
  108. e.preventDefault();
  109. },
  110. nativeEvent: {
  111. stopImmediatePropagation: () => {
  112. if (e.nativeEvent && typeof e.nativeEvent.stopImmediatePropagation === 'function') {
  113. e.nativeEvent.stopImmediatePropagation();
  114. }
  115. }
  116. },
  117. };
  118. return cbValue;
  119. },
  120. getIsInGroup: () => this.isInGroup(),
  121. getGroupValue: () => (this.context && this.context.checkboxGroup.value) || [],
  122. notifyGroupChange: cbContent => {
  123. this.context.checkboxGroup.onChange(cbContent);
  124. },
  125. getGroupDisabled: () => (this.context && this.context.checkboxGroup.disabled),
  126. setAddonId: () => {
  127. this.setState({ addonId: getUuidShort({ prefix: 'addon' }) });
  128. },
  129. setExtraId: () => {
  130. this.setState({ extraId: getUuidShort({ prefix: 'extra' }) });
  131. },
  132. setFocusVisible: (focusVisible: boolean): void => {
  133. this.setState({ focusVisible });
  134. },
  135. focusCheckboxEntity: () => {
  136. this.focus();
  137. },
  138. };
  139. }
  140. foundation: CheckboxFoundation;
  141. constructor(props: CheckboxProps) {
  142. super(props);
  143. const checked = false;
  144. this.state = {
  145. checked: props.checked || props.defaultChecked || checked,
  146. addonId: props.addonId,
  147. extraId: props.extraId,
  148. focusVisible: false
  149. };
  150. this.checkboxEntity = null;
  151. this.foundation = new CheckboxFoundation(this.adapter);
  152. }
  153. componentDidUpdate(prevProps: CheckboxProps) {
  154. if (this.props.checked !== prevProps.checked) {
  155. if (isUndefined(this.props.checked)) {
  156. this.foundation.setChecked(false);
  157. } else if (isBoolean(this.props.checked)) {
  158. this.foundation.setChecked(this.props.checked);
  159. }
  160. }
  161. }
  162. isInGroup() {
  163. // Why do we need to determine whether there is a value in props?
  164. // If there is no value in the props of the checkbox in the context of the checkboxGroup,
  165. // it will be considered not to belong to the checkboxGroup。
  166. return Boolean(this.context && this.context.checkboxGroup && ('value' in this.props));
  167. }
  168. focus() {
  169. this.checkboxEntity && this.checkboxEntity.focus();
  170. }
  171. blur() {
  172. this.checkboxEntity && this.checkboxEntity.blur();
  173. }
  174. handleChange: React.MouseEventHandler<HTMLSpanElement> = e => this.foundation.handleChange(e);
  175. handleEnterPress = (e: React.KeyboardEvent<HTMLSpanElement>) => this.foundation.handleEnterPress(e);
  176. handleFocusVisible = (event: React.FocusEvent) => {
  177. this.foundation.handleFocusVisible(event);
  178. }
  179. handleBlur = (event: React.FocusEvent) => {
  180. this.foundation.handleBlur();
  181. }
  182. render() {
  183. const {
  184. disabled,
  185. style,
  186. prefixCls,
  187. className,
  188. indeterminate,
  189. children,
  190. onMouseEnter,
  191. onMouseLeave,
  192. extra,
  193. value,
  194. role,
  195. tabIndex,
  196. id,
  197. type,
  198. } = this.props;
  199. const { checked, addonId, extraId, focusVisible } = this.state;
  200. const props: Record<string, any> = {
  201. checked,
  202. disabled,
  203. };
  204. const inGroup = this.isInGroup();
  205. if (inGroup) {
  206. if (this.context.checkboxGroup.value) {
  207. const realChecked = (this.context.checkboxGroup.value || []).includes(value);
  208. props.checked = realChecked;
  209. }
  210. if (this.context.checkboxGroup.disabled) {
  211. props.disabled = this.context.checkboxGroup.disabled || this.props.disabled;
  212. }
  213. const { isCardType, isPureCardType } = this.context.checkboxGroup;
  214. props.isCardType = isCardType;
  215. props.isPureCardType = isPureCardType;
  216. props['name'] = this.context.checkboxGroup.name;
  217. } else {
  218. props.isPureCardType = type === strings.TYPE_PURECARD;
  219. props.isCardType = type === strings.TYPE_CARD || props.isPureCardType;
  220. }
  221. const prefix = prefixCls || css.PREFIX;
  222. const focusOuter = props.isCardType || props.isPureCardType;
  223. const wrapper = classnames(prefix, {
  224. [`${prefix}-disabled`]: props.disabled,
  225. [`${prefix}-indeterminate`]: indeterminate,
  226. [`${prefix}-checked`]: props.checked,
  227. [`${prefix}-unChecked`]: !props.checked,
  228. [`${prefix}-cardType`]: props.isCardType,
  229. [`${prefix}-cardType_disabled`]: props.disabled && props.isCardType,
  230. [`${prefix}-cardType_unDisabled`]: !(props.disabled && props.isCardType),
  231. [`${prefix}-cardType_checked`]: props.isCardType && props.checked && !props.disabled,
  232. [`${prefix}-cardType_checked_disabled`]: props.isCardType && props.checked && props.disabled,
  233. [className]: Boolean(className),
  234. [`${prefix}-focus`]: focusVisible && focusOuter,
  235. });
  236. const extraCls = classnames(`${prefix}-extra`, {
  237. [`${prefix}-cardType_extra_noChildren`]: props.isCardType && !children,
  238. });
  239. const name = inGroup && this.context.checkboxGroup.name;
  240. const xSemiPropChildren = this.props['x-semi-children-alias'] || 'children';
  241. const renderContent = () => {
  242. if (!children && !extra) {
  243. return null;
  244. }
  245. return (
  246. <div className={`${prefix}-content`}>
  247. {children ? (
  248. <span id={addonId} className={`${prefix}-addon`} x-semi-prop={xSemiPropChildren}>
  249. {children}
  250. </span>
  251. ) : null}
  252. {extra ? (
  253. <div id={extraId} className={extraCls} x-semi-prop="extra">
  254. {extra}
  255. </div>
  256. ) : null}
  257. </div>
  258. );
  259. };
  260. return (
  261. // label is better than span, however span is here which is to solve gitlab issue #364
  262. <span
  263. role={role}
  264. tabIndex={tabIndex}
  265. style={style}
  266. className={wrapper}
  267. id={id}
  268. onMouseEnter={onMouseEnter}
  269. onMouseLeave={onMouseLeave}
  270. onClick={this.handleChange}
  271. onKeyPress={this.handleEnterPress}
  272. aria-labelledby={this.props['aria-labelledby']}
  273. {...this.getDataAttr(this.props)}
  274. >
  275. <CheckboxInner
  276. {...this.props}
  277. {...props}
  278. addonId={children && addonId}
  279. extraId={extra && extraId}
  280. isPureCardType={props.isPureCardType}
  281. ref={ref => {
  282. this.checkboxEntity = ref;
  283. }}
  284. focusInner={focusVisible && !focusOuter}
  285. onInputFocus={this.handleFocusVisible}
  286. onInputBlur={this.handleBlur}
  287. />
  288. {renderContent()}
  289. </span>
  290. );
  291. }
  292. }
  293. Checkbox.elementType = 'Checkbox';
  294. export default Checkbox;