From 2b78d2dbc07f9b15f94e5b102c097ce916f3d85b Mon Sep 17 00:00:00 2001 From: Sendya <18x@loacg.com> Date: Fri, 28 May 2021 16:16:37 +0800 Subject: [PATCH 1/2] refactor(v3/rate): use composition api --- components/rate/Star.tsx | 89 ++++++++++ components/rate/index.tsx | 257 +++++++++++++++++++++++---- components/rate/style/index.less | 21 +-- components/rate/style/rtl.less | 21 +++ components/rate/util.ts | 42 +++++ components/style/themes/default.less | 2 + 6 files changed, 383 insertions(+), 49 deletions(-) create mode 100644 components/rate/Star.tsx create mode 100644 components/rate/style/rtl.less create mode 100644 components/rate/util.ts 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..c480dc264 100644 --- a/components/rate/index.tsx +++ b/components/rate/index.tsx @@ -1,62 +1,241 @@ -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'; + +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 starRefs = ref([]); + const state = reactive({ + sValue: props.value, + focused: false, + cleanedValue: null, + hoverValue: undefined, + }); + const saveRef = (el: any) => { + starRefs.value.push(el); }; - }, - methods: { - characterRender(node: VNode, { index }) { - const { tooltips } = this.$props; + onBeforeUpdate(() => { + starRefs.value = []; + }); + + const getStarDOM = index => { + return findDOMNode(starRefs.value[index]); + }; + 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 70726c6e6..56adab6ff 100644 --- a/components/style/themes/default.less +++ b/components/style/themes/default.less @@ -597,6 +597,8 @@ // --- @rate-star-color: @yellow-6; @rate-star-bg: @border-color-split; +@rate-star-size: 20px; +@rate-star-hover-scale: scale(1.1); // Card // --- From 7ec594c5fc2249f1073382d4a75e4f90af7a8953 Mon Sep 17 00:00:00 2001 From: Sendya <18x@loacg.com> Date: Fri, 28 May 2021 16:47:43 +0800 Subject: [PATCH 2/2] fix: add useRef hook --- components/_util/hooks/useRef.ts | 14 ++++++++++++++ components/rate/index.tsx | 13 ++++--------- v2-doc | 2 +- 3 files changed, 19 insertions(+), 10 deletions(-) create mode 100644 components/_util/hooks/useRef.ts 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/index.tsx b/components/rate/index.tsx index c480dc264..303b4c60a 100644 --- a/components/rate/index.tsx +++ b/components/rate/index.tsx @@ -19,6 +19,7 @@ import Tooltip from '../tooltip'; import useConfigInject from '../_util/hooks/useConfigInject'; import Star from './Star'; +import { useRef } from '../_util/hooks/useRef'; export const rateProps = { prefixCls: PropTypes.string, @@ -52,22 +53,16 @@ const Rate = defineComponent({ setup(props, { slots, attrs, emit, expose }) { const { prefixCls, direction } = useConfigInject('rate', props); const rateRef = ref(); - const starRefs = ref([]); + const [setRef, starRefs] = useRef(); const state = reactive({ sValue: props.value, focused: false, cleanedValue: null, hoverValue: undefined, }); - const saveRef = (el: any) => { - starRefs.value.push(el); - }; - onBeforeUpdate(() => { - starRefs.value = []; - }); const getStarDOM = index => { - return findDOMNode(starRefs.value[index]); + return findDOMNode(starRefs[index]); }; const getStarValue = (index, x) => { const reverse = direction.value === 'rtl'; @@ -200,7 +195,7 @@ const Rate = defineComponent({ for (let index = 0; index < count; index++) { stars.push(