diff --git a/components/_util/hooks/useRef.ts b/components/_util/hooks/useRef.ts new file mode 100644 index 000000000..41216855b --- /dev/null +++ b/components/_util/hooks/useRef.ts @@ -0,0 +1,14 @@ +import { onBeforeUpdate, readonly, ref, DeepReadonly, UnwrapRef } from 'vue'; + +export type UseRef = [(el: any) => void, DeepReadonly>]; + +export const useRef = (): UseRef => { + const refs = ref([]); + const setRef = (el: any) => { + refs.value.push(el); + }; + onBeforeUpdate(() => { + refs.value = []; + }); + return [setRef, readonly(refs)]; +}; diff --git a/components/rate/Star.tsx b/components/rate/Star.tsx new file mode 100644 index 000000000..eb7dc5328 --- /dev/null +++ b/components/rate/Star.tsx @@ -0,0 +1,89 @@ +import { defineComponent, computed, ExtractPropTypes } from 'vue'; +import { getPropsSlot } from '../_util/props-util'; +import PropTypes from '../_util/vue-types'; + +export const starProps = { + value: PropTypes.number, + index: PropTypes.number, + prefixCls: PropTypes.string, + allowHalf: PropTypes.looseBool, + disabled: PropTypes.looseBool, + character: PropTypes.any, + characterRender: PropTypes.func, + focused: PropTypes.looseBool, + count: PropTypes.number, + + onClick: PropTypes.func, + onHover: PropTypes.func, +}; + +export type StarProps = Partial>; + +export default defineComponent({ + name: 'Star', + inheritAttrs: false, + props: starProps, + setup(props, { slots, emit }) { + const onHover = e => { + const { index } = props; + emit('hover', e, index); + }; + const onClick = e => { + const { index } = props; + emit('click', e, index); + }; + const onKeyDown = e => { + const { index } = props; + if (e.keyCode === 13) { + emit('click', e, index); + } + }; + + const getClassName = computed(() => { + const { prefixCls, index, value, allowHalf, focused } = props; + const starValue = index + 1; + let className = prefixCls; + if (value === 0 && index === 0 && focused) { + className += ` ${prefixCls}-focused`; + } else if (allowHalf && value + 0.5 >= starValue && value < starValue) { + className += ` ${prefixCls}-half ${prefixCls}-active`; + if (focused) { + className += ` ${prefixCls}-focused`; + } + } else { + className += starValue <= value ? ` ${prefixCls}-full` : ` ${prefixCls}-zero`; + if (starValue === value && focused) { + className += ` ${prefixCls}-focused`; + } + } + return className; + }); + + const character = getPropsSlot(slots, props, 'character'); + + return () => { + const { disabled, prefixCls, characterRender, index, count, value } = props; + let star = ( +
  • +
    index ? 'true' : 'false'} + aria-posinset={index + 1} + aria-setsize={count} + tabindex={disabled ? -1 : 0} + > +
    {character}
    +
    {character}
    +
    +
  • + ); + if (characterRender) { + star = characterRender(star, props); + } + return star; + }; + }, +}); diff --git a/components/rate/index.tsx b/components/rate/index.tsx index 2644f1f81..303b4c60a 100644 --- a/components/rate/index.tsx +++ b/components/rate/index.tsx @@ -1,62 +1,236 @@ -import { inject, defineComponent, VNode } from 'vue'; -import omit from 'omit.js'; +import { + defineComponent, + ExtractPropTypes, + ref, + reactive, + VNode, + onUpdated, + onBeforeUpdate, + onMounted, +} from 'vue'; +import { initDefaultProps, getPropsSlot, findDOMNode } from '../_util/props-util'; +import { withInstall } from '../_util/type'; +import { getOffsetLeft } from './util'; +import classNames from '../_util/classNames'; import PropTypes from '../_util/vue-types'; -import { getOptionProps, getComponent } from '../_util/props-util'; -import { defaultConfigProvider } from '../config-provider'; -import VcRate from '../vc-rate'; +import KeyCode from '../_util/KeyCode'; import StarFilled from '@ant-design/icons-vue/StarFilled'; import Tooltip from '../tooltip'; -import { withInstall } from '../_util/type'; +import useConfigInject from '../_util/hooks/useConfigInject'; -export const RateProps = { +import Star from './Star'; +import { useRef } from '../_util/hooks/useRef'; + +export const rateProps = { prefixCls: PropTypes.string, count: PropTypes.number, value: PropTypes.number, - defaultValue: PropTypes.number, allowHalf: PropTypes.looseBool, allowClear: PropTypes.looseBool, tooltips: PropTypes.arrayOf(PropTypes.string), disabled: PropTypes.looseBool, character: PropTypes.any, autofocus: PropTypes.looseBool, + tabindex: PropTypes.number, + direction: PropTypes.string, }; +export type RateProps = Partial>; + const Rate = defineComponent({ name: 'ARate', - props: RateProps, - setup() { - return { - configProvider: inject('configProvider', defaultConfigProvider), + props: initDefaultProps(rateProps, { + value: 0, + count: 5, + allowHalf: false, + allowClear: true, + prefixCls: 'ant-rate', + tabindex: 0, + character: '★', + direction: 'ltr', + }), + emits: ['hoverChange', 'update:value', 'change', 'focus', 'blur', 'keydown'], + setup(props, { slots, attrs, emit, expose }) { + const { prefixCls, direction } = useConfigInject('rate', props); + const rateRef = ref(); + const [setRef, starRefs] = useRef(); + const state = reactive({ + sValue: props.value, + focused: false, + cleanedValue: null, + hoverValue: undefined, + }); + + const getStarDOM = index => { + return findDOMNode(starRefs[index]); }; - }, - methods: { - characterRender(node: VNode, { index }) { - const { tooltips } = this.$props; + const getStarValue = (index, x) => { + const reverse = direction.value === 'rtl'; + let value = index + 1; + if (props.allowHalf) { + const starEle = getStarDOM(index); + const leftDis = getOffsetLeft(starEle); + const width = starEle.clientWidth; + if (reverse && x - leftDis > width / 2) { + value -= 0.5; + } else if (!reverse && x - leftDis < width / 2) { + value -= 0.5; + } + } + return value; + }; + const changeValue = (value: number) => { + state.sValue = value; + emit('update:value', value); + emit('change', value); + }; + + const onHover = (e: MouseEvent, index) => { + const hoverValue = getStarValue(index, e.pageX); + if (hoverValue !== state.cleanedValue) { + state.hoverValue = hoverValue; + state.cleanedValue = null; + } + emit('hoverChange', hoverValue); + }; + const onMouseLeave = () => { + state.hoverValue = undefined; + state.cleanedValue = null; + emit('hoverChange', undefined); + }; + const onClick = (event: MouseEvent, index) => { + const { allowClear } = props; + const newValue = getStarValue(index, event.pageX); + let isReset = false; + if (allowClear) { + isReset = newValue === state.sValue; + } + onMouseLeave(); + changeValue(isReset ? 0 : newValue); + state.cleanedValue = isReset ? newValue : null; + }; + const onFocus = () => { + state.focused = true; + emit('focus'); + }; + const onBlur = () => { + state.focused = false; + emit('blur'); + }; + const onKeyDown = event => { + const { keyCode } = event; + const { count, allowHalf } = props; + const reverse = direction.value === 'rtl'; + if (keyCode === KeyCode.RIGHT && state.sValue < count && !reverse) { + if (allowHalf) { + state.sValue += 0.5; + } else { + state.sValue += 1; + } + changeValue(state.sValue); + event.preventDefault(); + } else if (keyCode === KeyCode.LEFT && state.sValue > 0 && !reverse) { + if (allowHalf) { + state.sValue -= 0.5; + } else { + state.sValue -= 1; + } + changeValue(state.sValue); + event.preventDefault(); + } else if (keyCode === KeyCode.RIGHT && state.sValue > 0 && reverse) { + if (allowHalf) { + state.sValue -= 0.5; + } else { + state.sValue -= 1; + } + changeValue(state.sValue); + event.preventDefault(); + } else if (keyCode === KeyCode.LEFT && state.sValue < count && reverse) { + if (allowHalf) { + state.sValue += 0.5; + } else { + state.sValue += 1; + } + changeValue(state.sValue); + event.preventDefault(); + } + emit('keydown', event); + }; + + const focus = () => { + if (!props.disabled) { + rateRef.value.focus(); + } + }; + const blur = () => { + if (!props.disabled) { + rateRef.value.blur(); + } + }; + + expose({ + focus, + blur, + }); + + onMounted(() => { + const { autoFocus, disabled } = props; + if (autoFocus && !disabled) { + focus(); + } + }); + + const characterRender = (node: VNode, { index }) => { + const { tooltips } = props; if (!tooltips) return node; return {node}; - }, - focus() { - (this.$refs.refRate as HTMLUListElement).focus(); - }, - blur() { - (this.$refs.refRate as HTMLUListElement).blur(); - }, - }, - render() { - const { prefixCls: customizePrefixCls, ...restProps } = getOptionProps(this); - const { getPrefixCls } = this.configProvider; - const prefixCls = getPrefixCls('rate', customizePrefixCls); - - const character = getComponent(this, 'character') || ; - const rateProps = { - character, - characterRender: this.characterRender, - prefixCls, - ...omit(restProps, ['tooltips']), - ...this.$attrs, - ref: 'refRate', }; - return ; + const character = getPropsSlot(slots, props, 'character') || ; + + return () => { + const { count, allowHalf, disabled, tabindex } = props; + const { class: className, style } = attrs; + const stars = []; + const disabledClass = disabled ? `${prefixCls.value}-disabled` : ''; + for (let index = 0; index < count; index++) { + stars.push( + , + ); + } + const rateClassName = classNames(prefixCls.value, disabledClass, className, { + [`${prefixCls.value}-rtl`]: direction.value === 'rtl', + }); + return ( +
      + {stars} +
    + ); + }; }, }); diff --git a/components/rate/style/index.less b/components/rate/style/index.less index c730871c5..5c93a12d9 100644 --- a/components/rate/style/index.less +++ b/components/rate/style/index.less @@ -10,7 +10,7 @@ margin: 0; padding: 0; color: @rate-star-color; - font-size: 20px; + font-size: @rate-star-size; line-height: unset; list-style: none; outline: none; @@ -25,24 +25,23 @@ &-star { position: relative; display: inline-block; - margin: 0; - padding: 0; color: inherit; cursor: pointer; - transition: all 0.3s; &:not(:last-child) { margin-right: 8px; } > div { - &:focus { - outline: 0; - } + transition: all 0.3s; &:hover, - &:focus { - transform: scale(1.1); + &:focus-visible { + transform: @rate-star-hover-scale; + } + + &:focus:not(:focus-visible) { + outline: 0; } } @@ -79,7 +78,9 @@ &-text { display: inline-block; - margin-left: 8px; + margin: 0 8px; font-size: @font-size-base; } } + +@import './rtl'; diff --git a/components/rate/style/rtl.less b/components/rate/style/rtl.less new file mode 100644 index 000000000..6a997955e --- /dev/null +++ b/components/rate/style/rtl.less @@ -0,0 +1,21 @@ +.@{rate-prefix-cls} { + &-rtl { + direction: rtl; + } + + &-star { + &:not(:last-child) { + .@{rate-prefix-cls}-rtl & { + margin-right: 0; + margin-left: 8px; + } + } + + &-first { + .@{rate-prefix-cls}-rtl & { + right: 0; + left: auto; + } + } + } +} diff --git a/components/rate/util.ts b/components/rate/util.ts new file mode 100644 index 000000000..42730abb4 --- /dev/null +++ b/components/rate/util.ts @@ -0,0 +1,42 @@ +/* eslint-disable import/prefer-default-export */ + +function getScroll(w: Window) { + let ret = w.pageXOffset; + const method = 'scrollLeft'; + if (typeof ret !== 'number') { + const d = w.document; + // ie6,7,8 standard mode + ret = d.documentElement[method]; + if (typeof ret !== 'number') { + // quirks mode + ret = d.body[method]; + } + } + return ret; +} + +function getClientPosition(elem: HTMLElement) { + let x: number; + let y: number; + const doc = elem.ownerDocument; + const { body } = doc; + const docElem = doc && doc.documentElement; + const box = elem.getBoundingClientRect(); + x = box.left; + y = box.top; + x -= docElem.clientLeft || body.clientLeft || 0; + y -= docElem.clientTop || body.clientTop || 0; + return { + left: x, + top: y, + }; +} + +export function getOffsetLeft(el: HTMLElement) { + const pos = getClientPosition(el); + const doc = el.ownerDocument; + // Only IE use `parentWindow` + const w: Window = doc.defaultView || (doc as any).parentWindow; + pos.left += getScroll(w); + return pos.left; +} diff --git a/components/style/themes/default.less b/components/style/themes/default.less index cc8ebad1f..76d4c9cf8 100644 --- a/components/style/themes/default.less +++ b/components/style/themes/default.less @@ -598,6 +598,8 @@ // --- @rate-star-color: @yellow-6; @rate-star-bg: @border-color-split; +@rate-star-size: 20px; +@rate-star-hover-scale: scale(1.1); // Card // --- diff --git a/v2-doc b/v2-doc index 001bf204e..0f6d531d0 160000 --- a/v2-doc +++ b/v2-doc @@ -1 +1 @@ -Subproject commit 001bf204ea9b389f1ab7ec1ce23cd6243db64251 +Subproject commit 0f6d531d088d5283250c8cec1c7e8be0e0d36a36