radio.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. /* eslint-disable prefer-destructuring */
  2. import React from 'react';
  3. import PropTypes from 'prop-types';
  4. import cls from 'classnames';
  5. import { noop, isUndefined, isBoolean } from 'lodash';
  6. import RadioFoundation, { RadioAdapter } from '@douyinfe/semi-foundation/radio/radioFoundation';
  7. import { RadioChangeEvent } from '@douyinfe/semi-foundation/radio/radioInnerFoundation';
  8. import { strings, radioClasses as css } from '@douyinfe/semi-foundation/radio/constants';
  9. import { getUuidShort } from '@douyinfe/semi-foundation/utils/uuid';
  10. import '@douyinfe/semi-foundation/radio/radio.scss';
  11. import BaseComponent from '../_base/baseComponent';
  12. import RadioInner from './radioInner';
  13. import Context, { RadioContextValue, RadioMode } from './context';
  14. export type RadioDisplayMode = 'vertical' | '';
  15. export type RadioType =
  16. typeof strings.TYPE_DEFAULT |
  17. typeof strings.TYPE_BUTTON |
  18. typeof strings.TYPE_CARD |
  19. typeof strings.TYPE_PURECARD;
  20. export type RadioProps = {
  21. autoFocus?: boolean;
  22. checked?: boolean;
  23. children?: React.ReactNode;
  24. defaultChecked?: boolean;
  25. value?: string | number;
  26. disabled?: boolean;
  27. prefixCls?: string;
  28. displayMode?: RadioDisplayMode;
  29. onChange?: (e: RadioChangeEvent) => void;
  30. onMouseEnter?: (e: React.MouseEvent<HTMLLabelElement>) => void;
  31. onMouseLeave?: (e: React.MouseEvent<HTMLLabelElement>) => void;
  32. mode?: RadioMode;
  33. extra?: React.ReactNode;
  34. style?: React.CSSProperties;
  35. className?: string;
  36. addonStyle?: React.CSSProperties;
  37. addonClassName?: string;
  38. type?: RadioType;
  39. 'aria-label'?: React.AriaAttributes['aria-label'];
  40. addonId?: string;
  41. extraId?: string;
  42. name?: string;
  43. preventScroll?: boolean
  44. };
  45. export interface RadioState {
  46. hover?: boolean;
  47. addonId?: string;
  48. extraId?: string;
  49. focusVisible?: boolean;
  50. checked?: boolean
  51. }
  52. export type { RadioChangeEvent };
  53. class Radio extends BaseComponent<RadioProps, RadioState> {
  54. static contextType = Context;
  55. static propTypes = {
  56. autoFocus: PropTypes.bool,
  57. checked: PropTypes.bool,
  58. defaultChecked: PropTypes.bool,
  59. value: PropTypes.any, // Compare according to value to determine whether to select
  60. style: PropTypes.object,
  61. className: PropTypes.string,
  62. disabled: PropTypes.bool,
  63. prefixCls: PropTypes.string,
  64. displayMode: PropTypes.oneOf<RadioDisplayMode>(['vertical', '']),
  65. onChange: PropTypes.func,
  66. onMouseEnter: PropTypes.func,
  67. onMouseLeave: PropTypes.func,
  68. mode: PropTypes.oneOf(strings.MODE),
  69. extra: PropTypes.node, // extra info
  70. addonStyle: PropTypes.object,
  71. addonClassName: PropTypes.string,
  72. type: PropTypes.oneOf([strings.TYPE_DEFAULT, strings.TYPE_BUTTON, strings.TYPE_CARD, strings.TYPE_PURECARD]), // Button style type
  73. 'aria-label': PropTypes.string,
  74. preventScroll: PropTypes.bool,
  75. };
  76. static defaultProps: Partial<RadioProps> = {
  77. autoFocus: false,
  78. defaultChecked: false,
  79. value: undefined as undefined,
  80. style: undefined as undefined,
  81. onMouseEnter: noop,
  82. onMouseLeave: noop,
  83. mode: '',
  84. type: 'default'
  85. };
  86. static elementType: string;
  87. radioEntity: RadioInner;
  88. context!: RadioContextValue;
  89. foundation: RadioFoundation;
  90. addonId: string;
  91. extraId: string;
  92. constructor(props: RadioProps) {
  93. super(props);
  94. this.state = {
  95. hover: false,
  96. addonId: props.addonId,
  97. extraId: props.extraId,
  98. checked: props.checked || props.defaultChecked || false,
  99. };
  100. this.foundation = new RadioFoundation(this.adapter);
  101. this.radioEntity = null;
  102. }
  103. componentDidUpdate(prevProps: RadioProps) {
  104. if (this.props.checked !== prevProps.checked) {
  105. if (isUndefined(this.props.checked)) {
  106. this.foundation.setChecked(false);
  107. } else if (isBoolean(this.props.checked)) {
  108. this.foundation.setChecked(this.props.checked);
  109. }
  110. }
  111. }
  112. get adapter(): RadioAdapter {
  113. return {
  114. ...super.adapter,
  115. setHover: (hover: boolean) => {
  116. this.setState({ hover });
  117. },
  118. setAddonId: () => {
  119. this.setState({ addonId: getUuidShort({ prefix: 'addon' }) });
  120. },
  121. setChecked: (checked: boolean) => {
  122. this.setState({ checked });
  123. },
  124. setExtraId: () => {
  125. this.setState({ extraId: getUuidShort({ prefix: 'extra' }) });
  126. },
  127. setFocusVisible: (focusVisible: boolean): void => {
  128. this.setState({ focusVisible });
  129. },
  130. };
  131. }
  132. isInGroup() {
  133. // eslint-disable-next-line react/destructuring-assignment
  134. return this.context && this.context.radioGroup;
  135. }
  136. focus() {
  137. this.radioEntity.focus();
  138. }
  139. blur() {
  140. this.radioEntity.blur();
  141. }
  142. onChange = (e: RadioChangeEvent) => {
  143. const { onChange } = this.props;
  144. if (this.isInGroup()) {
  145. const { radioGroup } = this.context;
  146. radioGroup.onChange && radioGroup.onChange(e);
  147. }
  148. !('checked' in this.props) && this.foundation.setChecked(e.target.checked);
  149. onChange && onChange(e);
  150. };
  151. handleMouseEnter = (e: React.MouseEvent<HTMLLabelElement>) => {
  152. this.props.onMouseEnter(e);
  153. this.foundation.setHover(true);
  154. };
  155. handleMouseLeave = (e: React.MouseEvent<HTMLLabelElement>) => {
  156. this.props.onMouseLeave(e);
  157. this.foundation.setHover(false);
  158. };
  159. handleFocusVisible = (event: React.FocusEvent) => {
  160. this.foundation.handleFocusVisible(event);
  161. }
  162. handleBlur = (event: React.FocusEvent) => {
  163. this.foundation.handleBlur();
  164. }
  165. render() {
  166. const {
  167. addonClassName,
  168. addonStyle,
  169. disabled,
  170. style,
  171. className,
  172. prefixCls,
  173. displayMode,
  174. children,
  175. extra,
  176. mode,
  177. type,
  178. value: propValue,
  179. name,
  180. ...rest
  181. } = this.props;
  182. let realChecked,
  183. isDisabled,
  184. realMode,
  185. isButtonRadioGroup,
  186. isCardRadioGroup,
  187. isPureCardRadioGroup,
  188. isButtonRadioComponent,
  189. buttonSize,
  190. realPrefixCls;
  191. const { hover: isHover, addonId, extraId, focusVisible, checked, } = this.state;
  192. const props: Record<string, any> = {
  193. checked,
  194. disabled,
  195. };
  196. if (this.isInGroup()) {
  197. realChecked = this.context.radioGroup.value === propValue;
  198. isDisabled = disabled || this.context.radioGroup.disabled;
  199. realMode = this.context.mode;
  200. isButtonRadioGroup = this.context.radioGroup.isButtonRadio;
  201. isCardRadioGroup = this.context.radioGroup.isCardRadio;
  202. isPureCardRadioGroup = this.context.radioGroup.isPureCardRadio;
  203. buttonSize = this.context.radioGroup.buttonSize;
  204. realPrefixCls = prefixCls || this.context.radioGroup.prefixCls;
  205. props.checked = realChecked;
  206. props.disabled = isDisabled;
  207. } else {
  208. realChecked = checked;
  209. isDisabled = disabled;
  210. realMode = mode;
  211. isButtonRadioComponent = type === 'button';
  212. realPrefixCls = prefixCls;
  213. isButtonRadioGroup = type === strings.TYPE_BUTTON;
  214. isPureCardRadioGroup = type === strings.TYPE_PURECARD;
  215. isCardRadioGroup = type === strings.TYPE_CARD || isPureCardRadioGroup;
  216. }
  217. const isButtonRadio = typeof isButtonRadioGroup === 'undefined' ? isButtonRadioComponent : isButtonRadioGroup;
  218. const prefix = realPrefixCls || css.PREFIX;
  219. const focusOuter = isCardRadioGroup || isPureCardRadioGroup || isButtonRadio;
  220. const wrapper = cls(prefix, {
  221. [`${prefix}-disabled`]: isDisabled,
  222. [`${prefix}-checked`]: realChecked,
  223. [`${prefix}-${displayMode}`]: Boolean(displayMode),
  224. [`${prefix}-buttonRadioComponent`]: isButtonRadioComponent,
  225. [`${prefix}-buttonRadioGroup`]: isButtonRadioGroup,
  226. [`${prefix}-buttonRadioGroup-${buttonSize}`]: isButtonRadioGroup && buttonSize,
  227. [`${prefix}-cardRadioGroup`]: isCardRadioGroup,
  228. [`${prefix}-cardRadioGroup_disabled`]: isDisabled && isCardRadioGroup,
  229. [`${prefix}-cardRadioGroup_checked`]: isCardRadioGroup && realChecked && !isDisabled,
  230. [`${prefix}-cardRadioGroup_checked_disabled`]: isCardRadioGroup && realChecked && isDisabled,
  231. [`${prefix}-cardRadioGroup_hover`]: isCardRadioGroup && !realChecked && isHover && !isDisabled,
  232. [className]: Boolean(className),
  233. [`${prefix}-focus`]: focusVisible && (isCardRadioGroup || isPureCardRadioGroup),
  234. });
  235. const groupName = this.isInGroup() && this.context.radioGroup.name;
  236. const addonCls = cls({
  237. [`${prefix}-addon`]: !isButtonRadio,
  238. [`${prefix}-addon-buttonRadio`]: isButtonRadio,
  239. [`${prefix}-addon-buttonRadio-checked`]: isButtonRadio && realChecked,
  240. [`${prefix}-addon-buttonRadio-disabled`]: isButtonRadio && isDisabled,
  241. [`${prefix}-addon-buttonRadio-hover`]: isButtonRadio && !realChecked && !isDisabled && isHover,
  242. [`${prefix}-addon-buttonRadio-${buttonSize}`]: isButtonRadio && buttonSize,
  243. [`${prefix}-focus`]: focusVisible && isButtonRadio,
  244. }, addonClassName);
  245. const renderContent = () => {
  246. if (!children && !extra) {
  247. return null;
  248. }
  249. return (
  250. <div className={cls([`${prefix}-content`, { [`${prefix}-isCardRadioGroup_content`]: isCardRadioGroup }])}>
  251. {children ? (
  252. <span className={addonCls} style={addonStyle} id={addonId} x-semi-prop="children">
  253. {children}
  254. </span>
  255. ) : null}
  256. {extra && !isButtonRadio ? (
  257. <div className={`${prefix}-extra`} id={extraId} x-semi-prop="extra">
  258. {extra}
  259. </div>
  260. ) : null}
  261. </div>
  262. );
  263. };
  264. return (
  265. <label
  266. style={style}
  267. className={wrapper}
  268. onMouseEnter={this.handleMouseEnter}
  269. onMouseLeave={this.handleMouseLeave}
  270. {...this.getDataAttr(rest)}
  271. >
  272. <RadioInner
  273. {...this.props}
  274. {...props}
  275. mode={realMode}
  276. name={name ?? groupName}
  277. isButtonRadio={isButtonRadio}
  278. isPureCardRadioGroup={isPureCardRadioGroup}
  279. onChange={this.onChange}
  280. ref={(ref: RadioInner) => {
  281. this.radioEntity = ref;
  282. }}
  283. addonId={children && addonId}
  284. extraId={extra && extraId}
  285. focusInner={focusVisible && !focusOuter}
  286. onInputFocus={this.handleFocusVisible}
  287. onInputBlur={this.handleBlur}
  288. />
  289. {renderContent()}
  290. </label>
  291. );
  292. }
  293. }
  294. Radio.elementType = 'Radio';
  295. export default Radio;