item.tsx 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. import React, { PureComponent } from 'react';
  2. import cls from 'classnames';
  3. import PropTypes from 'prop-types';
  4. import { cssClasses, strings } from '@douyinfe/semi-foundation/rating/constants';
  5. import '@douyinfe/semi-foundation/rating/rating.scss';
  6. import { IconStar } from '@douyinfe/semi-icons';
  7. import { RatingItemFoundation, RatingItemAdapter } from '@douyinfe/semi-foundation/rating/foundation';
  8. import BaseComponent, { BaseProps } from '../_base/baseComponent';
  9. type ArrayElement<ArrayType extends readonly unknown[]> =
  10. ArrayType extends readonly (infer ElementType)[] ? ElementType : never;
  11. export interface RatingItemProps extends BaseProps {
  12. value: number;
  13. index: number;
  14. prefixCls: string;
  15. allowHalf: boolean;
  16. onHover: (e: React.MouseEvent, index: number) => void;
  17. onClick: (e: React.MouseEvent | React.KeyboardEvent, index: number) => void;
  18. character: React.ReactNode;
  19. focused: boolean;
  20. disabled: boolean;
  21. count: number;
  22. ariaLabelPrefix: string;
  23. size: number | ArrayElement<typeof strings.SIZE_SET>;
  24. 'aria-describedby'?: React.AriaAttributes['aria-describedby'];
  25. onFocus?: (e: React.FocusEvent) => void;
  26. onBlur?: (e: React.FocusEvent) => void;
  27. preventScroll?: boolean
  28. }
  29. export interface RatingItemState {
  30. firstStarFocus: boolean;
  31. secondStarFocus: boolean
  32. }
  33. export default class Item extends BaseComponent<RatingItemProps, RatingItemState> {
  34. static propTypes = {
  35. value: PropTypes.number,
  36. index: PropTypes.number,
  37. prefixCls: PropTypes.string,
  38. allowHalf: PropTypes.bool,
  39. onHover: PropTypes.func,
  40. onClick: PropTypes.func,
  41. character: PropTypes.node,
  42. focused: PropTypes.bool,
  43. disabled: PropTypes.bool,
  44. count: PropTypes.number,
  45. ariaLabelPrefix: PropTypes.string,
  46. size: PropTypes.oneOfType([
  47. PropTypes.oneOf(strings.SIZE_SET),
  48. PropTypes.number,
  49. ]),
  50. 'aria-describedby': PropTypes.string,
  51. onFocus: PropTypes.func,
  52. onBlur: PropTypes.func,
  53. preventScroll: PropTypes.bool,
  54. };
  55. foundation: RatingItemFoundation;
  56. constructor(props: RatingItemProps) {
  57. super(props);
  58. this.state = {
  59. firstStarFocus: false,
  60. secondStarFocus: false,
  61. };
  62. this.foundation = new RatingItemFoundation(this.adapter);
  63. }
  64. get adapter(): RatingItemAdapter<RatingItemProps, RatingItemState> {
  65. return {
  66. ...super.adapter,
  67. setFirstStarFocus: (value) => {
  68. this.setState({
  69. firstStarFocus: value,
  70. });
  71. },
  72. setSecondStarFocus: (value) => {
  73. this.setState({
  74. secondStarFocus: value,
  75. });
  76. }
  77. };
  78. }
  79. firstStar: HTMLDivElement = null;
  80. secondStar: HTMLDivElement = null;
  81. onHover: React.MouseEventHandler = e => {
  82. const { onHover, index } = this.props;
  83. onHover(e, index);
  84. };
  85. onClick: React.MouseEventHandler = e => {
  86. const { onClick, index } = this.props;
  87. onClick(e, index);
  88. };
  89. onFocus = (e, star) => {
  90. const { onFocus } = this.props;
  91. onFocus && onFocus(e);
  92. this.foundation.handleFocusVisible(e, star);
  93. }
  94. onBlur = (e, star) => {
  95. const { onBlur } = this.props;
  96. onBlur && onBlur(e);
  97. this.foundation.handleBlur(e, star);
  98. }
  99. onKeyDown: React.KeyboardEventHandler = e => {
  100. const { onClick, index } = this.props;
  101. if (e.keyCode === 13) {
  102. onClick(e, index);
  103. }
  104. };
  105. starFocus = () => {
  106. const { value, index, preventScroll } = this.props;
  107. if (value - index === 0.5) {
  108. this.firstStar.focus({ preventScroll });
  109. } else {
  110. this.secondStar.focus({ preventScroll });
  111. }
  112. }
  113. saveFirstStar = (node: HTMLDivElement) => {
  114. this.firstStar = node;
  115. };
  116. saveSecondStar = (node: HTMLDivElement) => {
  117. this.secondStar = node;
  118. };
  119. render() {
  120. const {
  121. index,
  122. prefixCls,
  123. character,
  124. count,
  125. value,
  126. disabled,
  127. allowHalf,
  128. focused,
  129. size,
  130. ariaLabelPrefix,
  131. } = this.props;
  132. const { firstStarFocus, secondStarFocus } = this.state;
  133. const starValue = index + 1;
  134. const diff = starValue - value;
  135. // const isHalf = allowHalf && value + 0.5 === starValue;
  136. const isHalf = allowHalf && diff < 1 && diff > 0;
  137. const firstWidth = 1 - diff;
  138. const isFull = starValue <= value;
  139. const isCustomSize = typeof size === 'number';
  140. const starCls = cls(prefixCls, {
  141. [`${prefixCls}-half`]: isHalf,
  142. [`${prefixCls}-full`]: isFull,
  143. [`${prefixCls}-${size}`]: !isCustomSize,
  144. });
  145. const sizeStyle = isCustomSize ? {
  146. width: size,
  147. height: size,
  148. fontSize: size
  149. } : {};
  150. const iconSize = isCustomSize ? 'inherit' : (size === 'small' ? 'default' : 'extra-large');
  151. const content = character ? character : <IconStar size={iconSize} style={{ display: 'block' }}/>;
  152. const isEmpty = index === count;
  153. const starWrapCls = cls(`${prefixCls}-wrapper`, {
  154. [`${prefixCls}-disabled`]: disabled,
  155. [`${cssClasses.PREFIX}-focus`]: (firstStarFocus || secondStarFocus) && value !== 0,
  156. });
  157. const starWrapProps = {
  158. onClick: disabled ? null : this.onClick,
  159. onKeyDown: disabled ? null : this.onKeyDown,
  160. onMouseMove: disabled ? null : this.onHover,
  161. className: starWrapCls,
  162. };
  163. const AriaSetSize = allowHalf ? count * 2 + 1 : count + 1;
  164. const firstStarProps = {
  165. ref: this.saveFirstStar as any,
  166. role: "radio",
  167. 'aria-checked': value === index + 0.5,
  168. 'aria-posinset': 2 * index + 1,
  169. 'aria-setsize': AriaSetSize,
  170. 'aria-disabled': disabled,
  171. 'aria-label': `${index + 0.5} ${ariaLabelPrefix}s`,
  172. 'aria-labelledby': this.props['aria-describedby'],
  173. 'aria-describedby': this.props['aria-describedby'],
  174. className: cls(`${prefixCls}-first`, `${cssClasses.PREFIX}-no-focus`),
  175. tabIndex: !disabled && value === index + 0.5 ? 0 : -1,
  176. onFocus: (e) => {
  177. this.onFocus(e, 'first');
  178. },
  179. onBlur: (e) => {
  180. this.onBlur(e, 'first');
  181. },
  182. };
  183. const secondStarTabIndex = !disabled && ((value === index + 1) || (isEmpty && value === 0)) ? 0 : -1;
  184. const secondStarProps = {
  185. ref: this.saveSecondStar as any,
  186. role: "radio",
  187. 'aria-checked': isEmpty ? value === 0 : value === index + 1,
  188. 'aria-posinset': allowHalf ? 2 * (index + 1) : index + 1,
  189. 'aria-setsize': AriaSetSize,
  190. 'aria-disabled': disabled,
  191. 'aria-label': `${isEmpty ? 0 : index + 1} ${ariaLabelPrefix}${index === 0 ? '' : 's'}`,
  192. 'aria-labelledby': this.props['aria-describedby'],
  193. 'aria-describedby': this.props['aria-describedby'],
  194. className: cls(`${prefixCls}-second`, `${cssClasses.PREFIX}-no-focus`),
  195. tabIndex: secondStarTabIndex,
  196. onFocus: (e) => {
  197. this.onFocus(e, 'second');
  198. },
  199. onBlur: (e) => {
  200. this.onBlur(e, 'second');
  201. },
  202. };
  203. return (
  204. <li className={starCls} style={{ ...sizeStyle }} key={index} >
  205. <div {...(starWrapProps as any)}>
  206. {allowHalf && !isEmpty && <div {...firstStarProps} style={{ width: `${firstWidth * 100}%` }}>{content}</div>}
  207. <div {...secondStarProps} x-semi-prop="character">{content}</div>
  208. </div>
  209. </li>
  210. );
  211. }
  212. }