|
@@ -1,14 +1,17 @@
|
|
|
import React, { PureComponent } from 'react';
|
|
|
import cls from 'classnames';
|
|
|
import PropTypes from 'prop-types';
|
|
|
-import { strings } from '@douyinfe/semi-foundation/rating/constants';
|
|
|
+import { cssClasses, strings } from '@douyinfe/semi-foundation/rating/constants';
|
|
|
import '@douyinfe/semi-foundation/rating/rating.scss';
|
|
|
import { IconStar } from '@douyinfe/semi-icons';
|
|
|
+import { RatingItemFoundation, RatingItemAdapter } from '@douyinfe/semi-foundation/rating/foundation';
|
|
|
+import BaseComponent, { BaseProps } from '../_base/baseComponent';
|
|
|
+
|
|
|
|
|
|
type ArrayElement<ArrayType extends readonly unknown[]> =
|
|
|
ArrayType extends readonly (infer ElementType)[] ? ElementType : never;
|
|
|
|
|
|
-export interface RatingItemProps {
|
|
|
+export interface RatingItemProps extends BaseProps {
|
|
|
value: number;
|
|
|
index: number;
|
|
|
prefixCls: string;
|
|
@@ -19,11 +22,19 @@ export interface RatingItemProps {
|
|
|
focused: boolean;
|
|
|
disabled: boolean;
|
|
|
count: number;
|
|
|
+ ariaLabelPrefix: string;
|
|
|
size: number | ArrayElement<typeof strings.SIZE_SET>;
|
|
|
'aria-describedby'?: React.AriaAttributes['aria-describedby'];
|
|
|
+ onFocus?: (e: React.FocusEvent) => void;
|
|
|
+ onBlur?: (e: React.FocusEvent) => void;
|
|
|
+}
|
|
|
+
|
|
|
+export interface RatingItemState {
|
|
|
+ firstStarFocus: boolean,
|
|
|
+ secondStarFocus: boolean,
|
|
|
}
|
|
|
|
|
|
-export default class Item extends PureComponent<RatingItemProps> {
|
|
|
+export default class Item extends BaseComponent<RatingItemProps, RatingItemState> {
|
|
|
static propTypes = {
|
|
|
value: PropTypes.number,
|
|
|
index: PropTypes.number,
|
|
@@ -35,13 +46,46 @@ export default class Item extends PureComponent<RatingItemProps> {
|
|
|
focused: PropTypes.bool,
|
|
|
disabled: PropTypes.bool,
|
|
|
count: PropTypes.number,
|
|
|
+ ariaLabelPrefix: PropTypes.string,
|
|
|
size: PropTypes.oneOfType([
|
|
|
PropTypes.oneOf(strings.SIZE_SET),
|
|
|
PropTypes.number,
|
|
|
]),
|
|
|
'aria-describedby': PropTypes.string,
|
|
|
+ onFocus: PropTypes.func,
|
|
|
+ onBlur: PropTypes.func,
|
|
|
};
|
|
|
|
|
|
+ foundation: RatingItemFoundation;
|
|
|
+
|
|
|
+ constructor(props: RatingItemProps) {
|
|
|
+ super(props);
|
|
|
+ this.state = {
|
|
|
+ firstStarFocus: false,
|
|
|
+ secondStarFocus: false,
|
|
|
+ };
|
|
|
+ this.foundation = new RatingItemFoundation(this.adapter);
|
|
|
+ }
|
|
|
+
|
|
|
+ get adapter(): RatingItemAdapter<RatingItemProps, RatingItemState> {
|
|
|
+ return {
|
|
|
+ ...super.adapter,
|
|
|
+ setFirstStarFocus: (value) => {
|
|
|
+ this.setState({
|
|
|
+ firstStarFocus: value,
|
|
|
+ });
|
|
|
+ },
|
|
|
+ setSecondStarFocus: (value) => {
|
|
|
+ this.setState({
|
|
|
+ secondStarFocus: value,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ firstStar: HTMLDivElement = null;
|
|
|
+ secondStar: HTMLDivElement = null;
|
|
|
+
|
|
|
onHover: React.MouseEventHandler = e => {
|
|
|
const { onHover, index } = this.props;
|
|
|
onHover(e, index);
|
|
@@ -52,6 +96,19 @@ export default class Item extends PureComponent<RatingItemProps> {
|
|
|
onClick(e, index);
|
|
|
};
|
|
|
|
|
|
+ onFocus = (e, star) => {
|
|
|
+ const { onFocus } = this.props;
|
|
|
+ onFocus && onFocus(e);
|
|
|
+ this.foundation.handleFocusVisible(e, star);
|
|
|
+ }
|
|
|
+
|
|
|
+ onBlur = (e, star) => {
|
|
|
+ const { onBlur } = this.props;
|
|
|
+ onBlur && onBlur(e);
|
|
|
+ this.foundation.handleBlur(e, star);
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
onKeyDown: React.KeyboardEventHandler = e => {
|
|
|
const { onClick, index } = this.props;
|
|
|
if (e.keyCode === 13) {
|
|
@@ -59,6 +116,23 @@ export default class Item extends PureComponent<RatingItemProps> {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+ starFocus = () => {
|
|
|
+ const { value, index } = this.props;
|
|
|
+ if (value - index === 0.5) {
|
|
|
+ this.firstStar.focus();
|
|
|
+ } else {
|
|
|
+ this.secondStar.focus();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ saveFirstStar = (node: HTMLDivElement) => {
|
|
|
+ this.firstStar = node;
|
|
|
+ };
|
|
|
+
|
|
|
+ saveSecondStar = (node: HTMLDivElement) => {
|
|
|
+ this.secondStar = node;
|
|
|
+ };
|
|
|
+
|
|
|
render() {
|
|
|
const {
|
|
|
index,
|
|
@@ -69,18 +143,18 @@ export default class Item extends PureComponent<RatingItemProps> {
|
|
|
disabled,
|
|
|
allowHalf,
|
|
|
focused,
|
|
|
- size
|
|
|
+ size,
|
|
|
+ ariaLabelPrefix,
|
|
|
} = this.props;
|
|
|
+ const { firstStarFocus, secondStarFocus } = this.state;
|
|
|
const starValue = index + 1;
|
|
|
const diff = starValue - value;
|
|
|
- const isFocused = value === 0 && index === 0 && focused;
|
|
|
// const isHalf = allowHalf && value + 0.5 === starValue;
|
|
|
const isHalf = allowHalf && diff < 1 && diff > 0;
|
|
|
- const firstWidth = isHalf ? 1 - diff : 0.5;
|
|
|
+ const firstWidth = 1 - diff;
|
|
|
const isFull = starValue <= value;
|
|
|
const isCustomSize = typeof size === 'number';
|
|
|
const starCls = cls(prefixCls, {
|
|
|
- [`${prefixCls}-focused`]: isFocused,
|
|
|
[`${prefixCls}-half`]: isHalf,
|
|
|
[`${prefixCls}-full`]: isFull,
|
|
|
[`${prefixCls}-${size}`]: !isCustomSize,
|
|
@@ -91,26 +165,65 @@ export default class Item extends PureComponent<RatingItemProps> {
|
|
|
fontSize: size
|
|
|
} : {};
|
|
|
const iconSize = isCustomSize ? 'inherit' : (size === 'small' ? 'default' : 'extra-large');
|
|
|
- const content = character ? character : <IconStar size={iconSize} />;
|
|
|
+ const content = character ? character : <IconStar size={iconSize} style={{ display: 'block' }}/>;
|
|
|
+ const isEmpty = index === count;
|
|
|
+ const starWrapCls = cls(`${prefixCls}-wrapper`,{
|
|
|
+ [`${prefixCls}-disabled`]: disabled,
|
|
|
+ [`${cssClasses.PREFIX}-focus`]: (firstStarFocus || secondStarFocus) && value !== 0,
|
|
|
+ });
|
|
|
+ const starWrapProps = {
|
|
|
+ onClick: disabled ? null : this.onClick ,
|
|
|
+ onKeyDown: disabled ? null : this.onKeyDown,
|
|
|
+ onMouseMove: disabled ? null : this.onHover,
|
|
|
+ className: starWrapCls,
|
|
|
+ };
|
|
|
+ const AriaSetSize = allowHalf ? count * 2 + 1 : count + 1;
|
|
|
+ const firstStarProps = {
|
|
|
+ ref: this.saveFirstStar as any,
|
|
|
+ role:"radio",
|
|
|
+ 'aria-checked': value === index + 0.5,
|
|
|
+ 'aria-posinset': 2 * index + 1,
|
|
|
+ 'aria-setsize': AriaSetSize,
|
|
|
+ 'aria-disabled': disabled,
|
|
|
+ 'aria-label': `${index + 0.5} ${ariaLabelPrefix}s`,
|
|
|
+ 'aria-labelledby': this.props['aria-describedby'],
|
|
|
+ 'aria-describedby': this.props['aria-describedby'],
|
|
|
+ className: cls(`${prefixCls}-first`,`${cssClasses.PREFIX}-no-focus`),
|
|
|
+ tabIndex: !disabled && value === index + 0.5 ? 0 : -1,
|
|
|
+ onFocus: (e) => {
|
|
|
+ this.onFocus(e, 'first');
|
|
|
+ },
|
|
|
+ onBlur:(e) => {
|
|
|
+ this.onBlur(e, 'first');
|
|
|
+ },
|
|
|
+ };
|
|
|
+
|
|
|
+ const secondStarTabIndex = !disabled && ((value === index + 1) || (isEmpty && value === 0)) ? 0 : -1;
|
|
|
+ const secondStarProps = {
|
|
|
+ ref: this.saveSecondStar as any,
|
|
|
+ role:"radio",
|
|
|
+ 'aria-checked': isEmpty ? value === 0 : value === index + 1,
|
|
|
+ 'aria-posinset': allowHalf ? 2 * (index + 1) : index + 1,
|
|
|
+ 'aria-setsize': AriaSetSize,
|
|
|
+ 'aria-disabled': disabled,
|
|
|
+ 'aria-label': `${isEmpty ? 0 : index + 1} ${ariaLabelPrefix}${index === 0 ? '' : 's'}`,
|
|
|
+ 'aria-labelledby': this.props['aria-describedby'],
|
|
|
+ 'aria-describedby': this.props['aria-describedby'],
|
|
|
+ className: cls(`${prefixCls}-second`,`${cssClasses.PREFIX}-no-focus`),
|
|
|
+ tabIndex: secondStarTabIndex,
|
|
|
+ onFocus: (e) => {
|
|
|
+ this.onFocus(e, 'second');
|
|
|
+ },
|
|
|
+ onBlur:(e) => {
|
|
|
+ this.onBlur(e, 'second');
|
|
|
+ },
|
|
|
+ };
|
|
|
+
|
|
|
return (
|
|
|
- <li className={starCls} style={{ ...sizeStyle }}>
|
|
|
- <div
|
|
|
- onClick={disabled ? null : this.onClick}
|
|
|
- onKeyDown={disabled ? null : this.onKeyDown}
|
|
|
- onMouseMove={disabled ? null : this.onHover}
|
|
|
- role="radio"
|
|
|
- aria-checked={value > index ? 'true' : 'false'}
|
|
|
- aria-posinset={index + 1}
|
|
|
- aria-setsize={count}
|
|
|
- aria-disabled={disabled}
|
|
|
- aria-label={`Rating ${index + (isHalf ? 0.5 : 1)}`}
|
|
|
- aria-labelledby={this.props['aria-describedby']} // screen reader will read labelledby instead of describedby
|
|
|
- aria-describedby={this.props['aria-describedby']}
|
|
|
- tabIndex={0}
|
|
|
- className={`${prefixCls}-wrapper`}
|
|
|
- >
|
|
|
- <div className={`${prefixCls}-first`} style={{ width: `${firstWidth * 100}%` }}>{content}</div>
|
|
|
- <div className={`${prefixCls}-second`} x-semi-prop="character">{content}</div>
|
|
|
+ <li className={starCls} style={{ ...sizeStyle }} key={index} >
|
|
|
+ <div {...(starWrapProps as any)}>
|
|
|
+ {allowHalf && !isEmpty && <div {...firstStarProps} style={{ width: `${firstWidth * 100}%` }}>{content}</div>}
|
|
|
+ <div {...secondStarProps} x-semi-prop="character">{content}</div>
|
|
|
</div>
|
|
|
</li>
|
|
|
);
|