radio.tsx 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. /* eslint-disable prefer-destructuring */
  2. import React from 'react';
  3. import PropTypes from 'prop-types';
  4. import cls from 'classnames';
  5. import { noop } 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. defaultChecked?: boolean;
  24. value?: string | number;
  25. disabled?: boolean;
  26. prefixCls?: string;
  27. displayMode?: RadioDisplayMode;
  28. onChange?: (e: RadioChangeEvent) => void;
  29. onMouseEnter?: (e: React.MouseEvent<HTMLLabelElement>) => void;
  30. onMouseLeave?: (e: React.MouseEvent<HTMLLabelElement>) => void;
  31. mode?: RadioMode;
  32. extra?: React.ReactNode;
  33. style?: React.CSSProperties;
  34. className?: string;
  35. addonStyle?: React.CSSProperties;
  36. addonClassName?: string;
  37. type?: RadioType;
  38. 'aria-label'?: React.AriaAttributes['aria-label'];
  39. };
  40. export interface RadioState {
  41. hover?: boolean;
  42. }
  43. export { RadioChangeEvent };
  44. class Radio extends BaseComponent<RadioProps, RadioState> {
  45. static contextType = Context;
  46. static propTypes = {
  47. autoFocus: PropTypes.bool,
  48. checked: PropTypes.bool,
  49. defaultChecked: PropTypes.bool,
  50. value: PropTypes.any, // Compare according to value to determine whether to select
  51. style: PropTypes.object,
  52. className: PropTypes.string,
  53. disabled: PropTypes.bool,
  54. prefixCls: PropTypes.string,
  55. displayMode: PropTypes.oneOf<RadioDisplayMode>(['vertical', '']),
  56. onChange: PropTypes.func,
  57. onMouseEnter: PropTypes.func,
  58. onMouseLeave: PropTypes.func,
  59. mode: PropTypes.oneOf(strings.MODE),
  60. extra: PropTypes.node, // extra info
  61. addonStyle: PropTypes.object,
  62. addonClassName: PropTypes.string,
  63. type: PropTypes.oneOf([strings.TYPE_DEFAULT, strings.TYPE_BUTTON, strings.TYPE_CARD, strings.TYPE_PURECARD]), // Button style type
  64. 'aria-label': PropTypes.string,
  65. };
  66. static defaultProps: Partial<RadioProps> = {
  67. autoFocus: false,
  68. defaultChecked: false,
  69. value: undefined as undefined,
  70. style: undefined as undefined,
  71. onMouseEnter: noop,
  72. onMouseLeave: noop,
  73. mode: '',
  74. type: 'default'
  75. };
  76. radioEntity: RadioInner;
  77. context!: RadioContextValue;
  78. foundation: RadioFoundation;
  79. addonId: string;
  80. extraId: string;
  81. constructor(props: RadioProps) {
  82. super(props);
  83. this.state = {
  84. hover: false,
  85. };
  86. this.foundation = new RadioFoundation(this.adapter);
  87. this.radioEntity = null;
  88. this.addonId = getUuidShort({ prefix: 'addon' });
  89. this.extraId = getUuidShort({ prefix: 'extra' });
  90. }
  91. get adapter(): RadioAdapter {
  92. return {
  93. ...super.adapter,
  94. setHover: (hover: boolean) => {
  95. this.setState({ hover });
  96. }
  97. };
  98. }
  99. isInGroup() {
  100. // eslint-disable-next-line react/destructuring-assignment
  101. return this.context && this.context.radioGroup;
  102. }
  103. focus() {
  104. this.radioEntity.focus();
  105. }
  106. blur() {
  107. this.radioEntity.blur();
  108. }
  109. onChange = (e: RadioChangeEvent) => {
  110. const { onChange } = this.props;
  111. if (this.isInGroup()) {
  112. const { radioGroup } = this.context;
  113. radioGroup.onChange && radioGroup.onChange(e);
  114. }
  115. onChange && onChange(e);
  116. };
  117. handleMouseEnter = (e: React.MouseEvent<HTMLLabelElement>) => {
  118. this.props.onMouseEnter(e);
  119. this.foundation.setHover(true);
  120. };
  121. handleMouseLeave = (e: React.MouseEvent<HTMLLabelElement>) => {
  122. this.props.onMouseLeave(e);
  123. this.foundation.setHover(false);
  124. };
  125. render() {
  126. const {
  127. addonClassName,
  128. addonStyle,
  129. checked,
  130. disabled,
  131. style,
  132. className,
  133. prefixCls,
  134. displayMode,
  135. children,
  136. extra,
  137. mode,
  138. type,
  139. value: propValue
  140. } = this.props;
  141. let realChecked,
  142. isDisabled,
  143. realMode,
  144. isButtonRadioGroup,
  145. isCardRadioGroup,
  146. isPureCardRadioGroup,
  147. isButtonRadioComponent,
  148. buttonSize,
  149. realPrefixCls;
  150. const isHover = this.state.hover;
  151. let props = {};
  152. if (this.isInGroup()) {
  153. realChecked = this.context.radioGroup.value === propValue;
  154. isDisabled = disabled || this.context.radioGroup.disabled;
  155. realMode = this.context.mode;
  156. isButtonRadioGroup = this.context.radioGroup.isButtonRadio;
  157. isCardRadioGroup = this.context.radioGroup.isCardRadio;
  158. isPureCardRadioGroup = this.context.radioGroup.isPureCardRadio;
  159. buttonSize = this.context.radioGroup.buttonSize;
  160. realPrefixCls = prefixCls || this.context.radioGroup.prefixCls;
  161. props = { checked: realChecked, disabled: isDisabled };
  162. } else {
  163. realChecked = checked;
  164. isDisabled = disabled;
  165. realMode = mode;
  166. isButtonRadioComponent = type === 'button';
  167. realPrefixCls = prefixCls;
  168. }
  169. const isButtonRadio = typeof isButtonRadioGroup === 'undefined' ? isButtonRadioComponent : isButtonRadioGroup;
  170. const prefix = realPrefixCls || css.PREFIX;
  171. const wrapper = cls(prefix, {
  172. [`${prefix}-disabled`]: isDisabled,
  173. [`${prefix}-checked`]: realChecked,
  174. [`${prefix}-${displayMode}`]: Boolean(displayMode),
  175. [`${prefix}-buttonRadioComponent`]: isButtonRadioComponent,
  176. [`${prefix}-buttonRadioGroup`]: isButtonRadioGroup,
  177. [`${prefix}-buttonRadioGroup-${buttonSize}`]: isButtonRadioGroup && buttonSize,
  178. [`${prefix}-cardRadioGroup`]: isCardRadioGroup,
  179. [`${prefix}-cardRadioGroup_disabled`]: isDisabled && isCardRadioGroup,
  180. [`${prefix}-cardRadioGroup_checked`]: isCardRadioGroup && realChecked && !isDisabled,
  181. [`${prefix}-cardRadioGroup_checked_disabled`]: isCardRadioGroup && realChecked && isDisabled,
  182. [`${prefix}-cardRadioGroup_hover`]: isCardRadioGroup && !realChecked && isHover && !isDisabled,
  183. [className]: Boolean(className),
  184. });
  185. const name = this.isInGroup() && this.context.radioGroup.name;
  186. const addonCls = cls({
  187. [`${prefix}-addon`]: !isButtonRadio,
  188. [`${prefix}-addon-buttonRadio`]: isButtonRadio,
  189. [`${prefix}-addon-buttonRadio-checked`]: isButtonRadio && realChecked,
  190. [`${prefix}-addon-buttonRadio-disabled`]: isButtonRadio && isDisabled,
  191. [`${prefix}-addon-buttonRadio-hover`]: isButtonRadio && !realChecked && !isDisabled && isHover,
  192. [`${prefix}-addon-buttonRadio-${buttonSize}`]: isButtonRadio && buttonSize,
  193. }, addonClassName);
  194. const renderContent = () => (
  195. <>
  196. {children ? <span className={addonCls} style={addonStyle} id={this.addonId}>{children}</span> : null}
  197. {extra && !isButtonRadio ? <div className={`${prefix}-extra`} id={this.extraId}>{extra}</div> : null}
  198. </>
  199. );
  200. return (
  201. <label
  202. style={style}
  203. className={wrapper}
  204. onMouseEnter={this.handleMouseEnter}
  205. onMouseLeave={this.handleMouseLeave}
  206. >
  207. <RadioInner
  208. {...this.props}
  209. {...props}
  210. mode={realMode}
  211. name={name}
  212. isButtonRadio={isButtonRadio}
  213. isPureCardRadioGroup={isPureCardRadioGroup}
  214. onChange={this.onChange}
  215. ref={(ref: RadioInner) => {
  216. this.radioEntity = ref;
  217. }}
  218. addonId={children && this.addonId}
  219. extraId={extra && this.extraId}
  220. />
  221. {
  222. isCardRadioGroup ?
  223. <div className={`${prefix}-isCardRadioGroup_content`}>{renderContent()}</div> :
  224. renderContent()
  225. }
  226. </label>
  227. );
  228. }
  229. }
  230. export default Radio;