index.tsx 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. import React from 'react';
  2. import ReactDOM from 'react-dom';
  3. import BaseComponent from '../_base/baseComponent';
  4. import cls from 'classnames';
  5. import ConfigContext from '../configProvider/context';
  6. import { cssClasses, strings } from '@douyinfe/semi-foundation/rating/constants';
  7. import PropTypes from 'prop-types';
  8. import { noop } from '@douyinfe/semi-foundation/utils/function';
  9. import Item from './item';
  10. import Tooltip from '../tooltip';
  11. import RatingFoundation, { RatingAdapter } from '@douyinfe/semi-foundation/rating/foundation';
  12. import '@douyinfe/semi-foundation/rating/rating.scss';
  13. export { RatingItemProps } from './item';
  14. export interface RatingProps {
  15. disabled?: boolean;
  16. value?: number;
  17. defaultValue?: number;
  18. count?: number;
  19. allowHalf?: boolean;
  20. allowClear?: boolean;
  21. style?: React.CSSProperties;
  22. prefixCls?: string;
  23. onChange?: (value: number) => void;
  24. onHoverChange?: (value: number) => void;
  25. className?: string;
  26. character?: React.ReactNode;
  27. tabIndex?: number;
  28. onFocus?: (e: React.FocusEvent) => void;
  29. onBlur?: (e: React.FocusEvent) => void;
  30. onKeyDown?: (e: React.KeyboardEvent) => void;
  31. onClick?: (e: React.MouseEvent | React.KeyboardEvent, index: number) => void;
  32. autoFocus?: boolean;
  33. size?: 'small' | 'default' | number;
  34. tooltips?: string[];
  35. }
  36. export interface RatingState {
  37. value: number;
  38. hoverValue: number;
  39. focused: boolean;
  40. clearedValue: number;
  41. }
  42. export default class Rating extends BaseComponent<RatingProps, RatingState> {
  43. static contextType = ConfigContext;
  44. static propTypes = {
  45. disabled: PropTypes.bool,
  46. value: PropTypes.number,
  47. defaultValue: PropTypes.number,
  48. count: PropTypes.number,
  49. allowHalf: PropTypes.bool,
  50. allowClear: PropTypes.bool,
  51. style: PropTypes.object,
  52. prefixCls: PropTypes.string,
  53. onChange: PropTypes.func,
  54. onHoverChange: PropTypes.func,
  55. className: PropTypes.string,
  56. character: PropTypes.node,
  57. tabIndex: PropTypes.number,
  58. onFocus: PropTypes.func,
  59. onBlur: PropTypes.func,
  60. onKeyDown: PropTypes.func,
  61. autoFocus: PropTypes.bool,
  62. size: PropTypes.oneOfType([PropTypes.oneOf(strings.SIZE_SET), PropTypes.number]),
  63. tooltips: PropTypes.arrayOf(PropTypes.string),
  64. };
  65. static defaultProps = {
  66. defaultValue: 0,
  67. count: 5,
  68. allowHalf: false,
  69. allowClear: true,
  70. style: {},
  71. prefixCls: cssClasses.PREFIX,
  72. onChange: noop,
  73. onHoverChange: noop,
  74. tabIndex: 0,
  75. size: 'default' as const,
  76. };
  77. stars: Record<string, Item>;
  78. rate: HTMLUListElement = null;
  79. foundation: RatingFoundation;
  80. constructor(props: RatingProps) {
  81. super(props);
  82. const value = props.value === undefined ? props.defaultValue : props.value;
  83. this.stars = {};
  84. this.state = {
  85. value,
  86. focused: false,
  87. hoverValue: undefined,
  88. clearedValue: null,
  89. };
  90. this.foundation = new RatingFoundation(this.adapter);
  91. }
  92. static getDerivedStateFromProps(nextProps: RatingProps, state: RatingState) {
  93. if ('value' in nextProps && nextProps.value !== undefined) {
  94. return {
  95. ...state,
  96. value: nextProps.value,
  97. };
  98. }
  99. return state;
  100. }
  101. get adapter(): RatingAdapter<RatingProps, RatingState> {
  102. return {
  103. ...super.adapter,
  104. focus: () => {
  105. const { disabled } = this.props;
  106. if (!disabled) {
  107. this.rate.focus();
  108. }
  109. },
  110. getStarDOM: (index: number) => {
  111. const instance = this.stars && this.stars[index];
  112. // eslint-disable-next-line react/no-find-dom-node
  113. return ReactDOM.findDOMNode(instance) as Element;
  114. },
  115. notifyHoverChange: (hoverValue: number, clearedValue: number) => {
  116. const { onHoverChange } = this.props;
  117. this.setState({
  118. hoverValue,
  119. clearedValue,
  120. });
  121. onHoverChange(hoverValue);
  122. },
  123. updateValue: (value: number) => {
  124. const { onChange } = this.props;
  125. if (!('value' in this.props)) {
  126. this.setState({
  127. value,
  128. });
  129. }
  130. onChange(value);
  131. },
  132. clearValue: (clearedValue: number) => {
  133. this.setState({
  134. clearedValue,
  135. });
  136. },
  137. notifyFocus: (e: React.FocusEvent) => {
  138. const { onFocus } = this.props;
  139. this.setState({
  140. focused: true,
  141. });
  142. onFocus && onFocus(e);
  143. },
  144. notifyBlur: (e: React.FocusEvent) => {
  145. const { onBlur } = this.props;
  146. this.setState({
  147. focused: false,
  148. });
  149. onBlur && onBlur(e);
  150. },
  151. notifyKeyDown: (e: React.KeyboardEvent) => {
  152. const { onKeyDown } = this.props;
  153. this.setState({
  154. focused: false,
  155. });
  156. onKeyDown && onKeyDown(e);
  157. },
  158. };
  159. }
  160. componentDidMount() {
  161. this.foundation.init();
  162. }
  163. componentWillUnmount() {
  164. this.foundation.destroy();
  165. }
  166. onHover = (event: React.MouseEvent, index: number) => {
  167. this.foundation.handleHover(event, index);
  168. };
  169. onMouseLeave = () => {
  170. this.foundation.handleMouseLeave();
  171. };
  172. onClick: RatingProps['onClick'] = (event, index) => {
  173. this.foundation.handleClick(event, index);
  174. };
  175. onFocus: RatingProps['onFocus'] = e => {
  176. this.foundation.handleFocus(e);
  177. };
  178. onBlur: RatingProps['onBlur'] = e => {
  179. this.foundation.handleBlur(e);
  180. };
  181. onKeyDown: RatingProps['onKeyDown'] = event => {
  182. const { value } = this.state;
  183. this.foundation.handleKeyDown(event, value);
  184. };
  185. focus = () => {
  186. const { disabled } = this.props;
  187. if (!disabled) {
  188. this.rate.focus();
  189. }
  190. };
  191. blur = () => {
  192. const { disabled } = this.props;
  193. if (!disabled) {
  194. this.rate.blur();
  195. }
  196. };
  197. saveRef = (index: number) => (node: Item) => {
  198. this.stars[index] = node;
  199. };
  200. saveRate = (node: HTMLUListElement) => {
  201. this.rate = node;
  202. };
  203. render() {
  204. const { count, allowHalf, style, prefixCls, disabled, className, character, tabIndex, size, tooltips } =
  205. this.props;
  206. const { value, hoverValue, focused } = this.state;
  207. const itemList = [...Array(count).keys()].map(ind => {
  208. const content = (
  209. <Item
  210. ref={this.saveRef(ind)}
  211. index={ind}
  212. count={count}
  213. prefixCls={`${prefixCls}-star`}
  214. allowHalf={allowHalf}
  215. value={hoverValue === undefined ? value : hoverValue}
  216. onClick={this.onClick}
  217. onHover={this.onHover}
  218. key={ind}
  219. disabled={disabled}
  220. character={character}
  221. focused={focused}
  222. size={size}
  223. />
  224. );
  225. if (tooltips) {
  226. const text = tooltips[ind] ? tooltips[ind] : '';
  227. const showTips = hoverValue - 1 === ind;
  228. return (
  229. <Tooltip visible={showTips} trigger="custom" content={text} key={`${ind}-${showTips}`}>
  230. {content}
  231. </Tooltip>
  232. );
  233. }
  234. return content;
  235. });
  236. const listCls = cls(
  237. prefixCls,
  238. {
  239. [`${prefixCls}-disabled`]: disabled,
  240. },
  241. className
  242. );
  243. return (
  244. <ul
  245. className={listCls}
  246. style={style}
  247. onMouseLeave={disabled ? null : this.onMouseLeave}
  248. tabIndex={disabled ? -1 : tabIndex}
  249. onFocus={disabled ? null : this.onFocus}
  250. onBlur={disabled ? null : this.onBlur}
  251. onKeyDown={disabled ? null : this.onKeyDown}
  252. ref={this.saveRate as any}
  253. role="radiogroup"
  254. >
  255. {itemList}
  256. </ul>
  257. );
  258. }
  259. }