1
0

checkbox.tsx 12 KB

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