index.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  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. 'aria-describedby'?: string;
  16. 'aria-errormessage'?: string;
  17. 'aria-invalid'?: boolean;
  18. 'aria-label'?: string;
  19. 'aria-labelledby'?: string;
  20. 'aria-required'?: boolean;
  21. disabled?: boolean;
  22. value?: number;
  23. defaultValue?: number;
  24. count?: number;
  25. allowHalf?: boolean;
  26. allowClear?: boolean;
  27. style?: React.CSSProperties;
  28. prefixCls?: string;
  29. onChange?: (value: number) => void;
  30. onHoverChange?: (value: number) => void;
  31. className?: string;
  32. character?: React.ReactNode;
  33. tabIndex?: number;
  34. onFocus?: (e: React.FocusEvent) => void;
  35. onBlur?: (e: React.FocusEvent) => void;
  36. onKeyDown?: (e: React.KeyboardEvent) => void;
  37. onClick?: (e: React.MouseEvent | React.KeyboardEvent, index: number) => void;
  38. autoFocus?: boolean;
  39. size?: 'small' | 'default' | number;
  40. tooltips?: string[];
  41. id?: string;
  42. }
  43. export interface RatingState {
  44. value: number;
  45. hoverValue: number;
  46. focused: boolean;
  47. clearedValue: number;
  48. emptyStarFocusVisible: boolean;
  49. }
  50. export default class Rating extends BaseComponent<RatingProps, RatingState> {
  51. static contextType = ConfigContext;
  52. static propTypes = {
  53. 'aria-describedby': PropTypes.string,
  54. 'aria-errormessage': PropTypes.string,
  55. 'aria-invalid': PropTypes.bool,
  56. 'aria-label': PropTypes.string,
  57. 'aria-labelledby': PropTypes.string,
  58. 'aria-required': PropTypes.bool,
  59. disabled: PropTypes.bool,
  60. value: PropTypes.number,
  61. defaultValue: PropTypes.number,
  62. count: PropTypes.number,
  63. allowHalf: PropTypes.bool,
  64. allowClear: PropTypes.bool,
  65. style: PropTypes.object,
  66. prefixCls: PropTypes.string,
  67. onChange: PropTypes.func,
  68. onHoverChange: PropTypes.func,
  69. className: PropTypes.string,
  70. character: PropTypes.node,
  71. tabIndex: PropTypes.number,
  72. onFocus: PropTypes.func,
  73. onBlur: PropTypes.func,
  74. onKeyDown: PropTypes.func,
  75. autoFocus: PropTypes.bool,
  76. size: PropTypes.oneOfType([PropTypes.oneOf(strings.SIZE_SET), PropTypes.number]),
  77. tooltips: PropTypes.arrayOf(PropTypes.string),
  78. id: PropTypes.string,
  79. };
  80. static defaultProps = {
  81. defaultValue: 0,
  82. count: 5,
  83. allowHalf: false,
  84. allowClear: true,
  85. style: {},
  86. prefixCls: cssClasses.PREFIX,
  87. onChange: noop,
  88. onHoverChange: noop,
  89. tabIndex: -1,
  90. size: 'default' as const,
  91. };
  92. stars: Record<string, Item>;
  93. rate: HTMLUListElement = null;
  94. foundation: RatingFoundation;
  95. constructor(props: RatingProps) {
  96. super(props);
  97. const value = props.value === undefined ? props.defaultValue : props.value;
  98. this.stars = {};
  99. this.state = {
  100. value,
  101. focused: false,
  102. hoverValue: undefined,
  103. clearedValue: null,
  104. emptyStarFocusVisible: false,
  105. };
  106. this.foundation = new RatingFoundation(this.adapter);
  107. }
  108. static getDerivedStateFromProps(nextProps: RatingProps, state: RatingState) {
  109. if ('value' in nextProps && nextProps.value !== undefined) {
  110. return {
  111. ...state,
  112. value: nextProps.value,
  113. };
  114. }
  115. return state;
  116. }
  117. get adapter(): RatingAdapter<RatingProps, RatingState> {
  118. return {
  119. ...super.adapter,
  120. focus: () => {
  121. const { disabled, count } = this.props;
  122. const { value } = this.state;
  123. if (!disabled) {
  124. const index = Math.ceil(value) - 1;
  125. this.stars[index < 0 ? count : index].starFocus();
  126. }
  127. },
  128. getStarDOM: (index: number) => {
  129. const instance = this.stars && this.stars[index];
  130. // eslint-disable-next-line react/no-find-dom-node
  131. return ReactDOM.findDOMNode(instance) as Element;
  132. },
  133. notifyHoverChange: (hoverValue: number, clearedValue: number) => {
  134. const { onHoverChange } = this.props;
  135. this.setState({
  136. hoverValue,
  137. clearedValue,
  138. });
  139. onHoverChange(hoverValue);
  140. },
  141. updateValue: (value: number) => {
  142. const { onChange } = this.props;
  143. if (!('value' in this.props)) {
  144. this.setState({
  145. value,
  146. });
  147. }
  148. onChange(value);
  149. },
  150. clearValue: (clearedValue: number) => {
  151. this.setState({
  152. clearedValue,
  153. });
  154. },
  155. notifyFocus: (e: React.FocusEvent) => {
  156. const { onFocus } = this.props;
  157. this.setState({
  158. focused: true,
  159. });
  160. onFocus && onFocus(e);
  161. },
  162. notifyBlur: (e: React.FocusEvent) => {
  163. const { onBlur } = this.props;
  164. this.setState({
  165. focused: false,
  166. });
  167. onBlur && onBlur(e);
  168. },
  169. notifyKeyDown: (e: React.KeyboardEvent) => {
  170. const { onKeyDown } = this.props;
  171. this.setState({
  172. focused: false,
  173. });
  174. onKeyDown && onKeyDown(e);
  175. },
  176. setEmptyStarFocusVisible: (focusVisible: boolean): void => {
  177. this.setState({
  178. emptyStarFocusVisible: focusVisible,
  179. });
  180. },
  181. };
  182. }
  183. componentDidMount() {
  184. this.foundation.init();
  185. }
  186. componentWillUnmount() {
  187. this.foundation.destroy();
  188. }
  189. onHover = (event: React.MouseEvent, index: number) => {
  190. this.foundation.handleHover(event, index);
  191. };
  192. onMouseLeave = () => {
  193. this.foundation.handleMouseLeave();
  194. };
  195. onClick: RatingProps['onClick'] = (event, index) => {
  196. this.foundation.handleClick(event, index);
  197. };
  198. onFocus: RatingProps['onFocus'] = e => {
  199. this.foundation.handleFocus(e);
  200. };
  201. onBlur: RatingProps['onBlur'] = e => {
  202. this.foundation.handleBlur(e);
  203. };
  204. onKeyDown: RatingProps['onKeyDown'] = event => {
  205. const { value } = this.state;
  206. this.foundation.handleKeyDown(event, value);
  207. };
  208. focus = () => {
  209. const { disabled } = this.props;
  210. if (!disabled) {
  211. this.rate.focus();
  212. }
  213. };
  214. blur = () => {
  215. const { disabled } = this.props;
  216. if (!disabled) {
  217. this.rate.blur();
  218. }
  219. };
  220. saveRef = (index: number) => (node: Item) => {
  221. this.stars[index] = node;
  222. };
  223. saveRate = (node: HTMLUListElement) => {
  224. this.rate = node;
  225. };
  226. handleStarFocusVisible = (event: React.FocusEvent) => {
  227. this.foundation.handleStarFocusVisible(event);
  228. }
  229. handleStarBlur = (event: React.FocusEvent) => {
  230. this.foundation.handleStarBlur(event);
  231. }
  232. getAriaLabelPrefix = () => {
  233. if (this.props['aria-label']) {
  234. return this.props['aria-label'];
  235. }
  236. let prefix = 'star';
  237. const { character } = this.props;
  238. if (typeof character === 'string') {
  239. prefix = character;
  240. }
  241. return prefix;
  242. }
  243. getItemList = (ariaLabelPrefix: string) => {
  244. const { count, allowHalf, prefixCls, disabled, character, size, tooltips } =this.props;
  245. const { value, hoverValue, focused } = this.state;
  246. // index == count is for Empty rating
  247. const itemList = [...Array(count + 1).keys()].map(ind => {
  248. const content = (
  249. <Item
  250. ref={this.saveRef(ind)}
  251. index={ind}
  252. count={count}
  253. prefixCls={`${prefixCls}-star`}
  254. allowHalf={allowHalf}
  255. value={hoverValue === undefined ? value : hoverValue}
  256. onClick={disabled ? noop : this.onClick}
  257. onHover={disabled ? noop : this.onHover}
  258. key={ind}
  259. disabled={disabled}
  260. character={character}
  261. focused={focused}
  262. size={ind === count ? 0 : size}
  263. ariaLabelPrefix={ariaLabelPrefix}
  264. onFocus={disabled || count !== ind ? noop : this.handleStarFocusVisible}
  265. onBlur={disabled || count !== ind ? noop : this.handleStarBlur}
  266. />
  267. );
  268. if (tooltips) {
  269. const text = tooltips[ind] ? tooltips[ind] : '';
  270. const showTips = hoverValue - 1 === ind;
  271. return (
  272. <Tooltip visible={showTips} trigger="custom" content={text} key={`${ind}-${showTips}`}>
  273. {content}
  274. </Tooltip>
  275. );
  276. }
  277. return content;
  278. });
  279. return itemList;
  280. }
  281. render() {
  282. const { style, prefixCls, disabled, className, id, count, tabIndex } = this.props;
  283. const { value, emptyStarFocusVisible } = this.state;
  284. const ariaLabelPrefix = this.getAriaLabelPrefix();
  285. const ariaLabel = `Rating: ${value} of ${count} ${ariaLabelPrefix}${value === 1 ? '' : 's'},`;
  286. const itemList = this.getItemList(ariaLabelPrefix);
  287. const listCls = cls(
  288. prefixCls,
  289. {
  290. [`${prefixCls}-disabled`]: disabled,
  291. [`${prefixCls}-focus`]: emptyStarFocusVisible,
  292. },
  293. className
  294. );
  295. return (
  296. // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
  297. <ul
  298. aria-label={ariaLabel}
  299. aria-labelledby={this.props['aria-labelledby']}
  300. aria-describedby={this.props['aria-describedby']}
  301. className={listCls}
  302. style={style}
  303. onMouseLeave={disabled ? noop : this.onMouseLeave}
  304. tabIndex={disabled ? -1 : tabIndex}
  305. onFocus={disabled ? noop : this.onFocus}
  306. onBlur={disabled ? noop : this.onBlur}
  307. onKeyDown={disabled ? noop : this.onKeyDown}
  308. ref={this.saveRate as any}
  309. id={id}
  310. >
  311. {itemList}
  312. </ul>
  313. );
  314. }
  315. }