diff --git a/components/rating/__docs__/adaptor/index.tsx b/components/rating/__docs__/adaptor/index.tsx index 75cd249e39..a57b10744b 100644 --- a/components/rating/__docs__/adaptor/index.tsx +++ b/components/rating/__docs__/adaptor/index.tsx @@ -25,7 +25,7 @@ export default { }, ], }), - adaptor: ({ type, size, value, ...others }) => { + adaptor: ({ type, size, value, ...others }: any) => { return ; }, }; diff --git a/components/rating/__docs__/demo/accessibility/index.tsx b/components/rating/__docs__/demo/accessibility/index.tsx index 33d82af569..fe2ad914e6 100644 --- a/components/rating/__docs__/demo/accessibility/index.tsx +++ b/components/rating/__docs__/demo/accessibility/index.tsx @@ -2,7 +2,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Rating } from '@alifd/next'; -const starMap = { +const starMap: Record = { 1: 'Bad', 2: 'OK', 3: 'Good', diff --git a/components/rating/__docs__/demo/grade/index.tsx b/components/rating/__docs__/demo/grade/index.tsx index 6d70583784..0d7d6091ca 100644 --- a/components/rating/__docs__/demo/grade/index.tsx +++ b/components/rating/__docs__/demo/grade/index.tsx @@ -2,7 +2,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Rating } from '@alifd/next'; -const starMap = { +const starMap: Record = { 1: 'Bad', 2: 'OK', 3: 'Good', diff --git a/components/rating/__docs__/demo/render-preview/index.tsx b/components/rating/__docs__/demo/render-preview/index.tsx index 82cfe45ce8..5f58189680 100644 --- a/components/rating/__docs__/demo/render-preview/index.tsx +++ b/components/rating/__docs__/demo/render-preview/index.tsx @@ -3,10 +3,7 @@ import ReactDOM from 'react-dom'; import { Rating, Button } from '@alifd/next'; class Preview extends React.Component { - constructor(props) { - super(props); - this.state = { isPreview: false }; - } + state = { isPreview: false }; render() { return ( diff --git a/components/rating/__docs__/index.en-us.md b/components/rating/__docs__/index.en-us.md index 82d40c1c3c..3a68fe4445 100644 --- a/components/rating/__docs__/index.en-us.md +++ b/components/rating/__docs__/index.en-us.md @@ -15,25 +15,28 @@ Rating component is usually used for customer feedback. ### Rating -| Param | Descripiton | Type | Default Value | -| ------------ | -------------------------------------------------------------------------------------------------- | -------- | --------- | -| defaultValue | default value | Number | 0 | -| size | size

**options**:
'small', 'medium', 'large' | Enum | 'medium' | -| value | value | Number | - | -| count | full mark of rating | Number | 5 | -| showGrade | display grade or not | Boolean | false | -| allowHalf | allow half start or not | Boolean | false | -| allowClear | Whether to allow clear when click again | Boolean | false | -| onChange | callback function on click star

**signatures**:
Function(value: String) => void
**params**:
_value_: {String} score | Function | func.noop | -| onHoverChange | callback function on hover star

**signatures**:
Function(value: String) => void
**params**:
_value_: {String} score | Function | func.noop | -| disabled | disabled rate or not | Boolean | false | -| readAs | custom display of grade

**signatures**:
Function() => void | Function | val => val | +| Param | Description | Type | Default Value | Required | +| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | ------------- | -------- | +| defaultValue | Default value | number | - | | +| value | Value(controlled mode) | number | - | | +| size | Size | 'small' \| 'medium' \| 'large' | 'medium' | | +| count | Full mark of rating | number | 5 | | +| showGrade | Display grade or not | boolean | false | | +| allowHalf | Allow half star or not | boolean | - | | +| allowClear | Whether to allow clear when click again | boolean | false | | +| onChange | Callback function on click star | (value: number) => void | - | | +| onHoverChange | Callback function on hover star | (value?: number) => void | - | | +| disabled | Disabled or not | boolean | false | | +| isPreview | Is preview mode or not | boolean | false | | +| renderPreview | Render method when is preview mode.(Required when isPreview=true)

**signature**:
**params**:
_value_: Score
_props_: The props of rating
**return**:
The render content when preview | (value: number, props: RatingProps) => React.ReactNode | - | | +| readAs | Custom display of grade

**signature**:
**params**:
_val_: Score value
**return**:
Score label | (val: number) => React.ReactNode | - | | +| type | - | string | - | | ## ARIA and KeyBoard -| KeyBoard | Descripiton | -| :---------- | :------------------------------ | -| Up Arrow | increase star rating | -| Down Arrow | decrease star rating | +| KeyBoard | Descripiton | +| :---------- | :------------------- | +| Up Arrow | increase star rating | +| Down Arrow | decrease star rating | | Right Arrow | increase star rating | | Left Arrow | decrease star rating | diff --git a/components/rating/__docs__/index.md b/components/rating/__docs__/index.md index 64d799fae4..edff8fff27 100644 --- a/components/rating/__docs__/index.md +++ b/components/rating/__docs__/index.md @@ -13,27 +13,27 @@ ### Rating -| 参数 | 说明 | 类型 | 默认值 | -| ------------- | ---------------------------------------------------------------------------------------------------------- | -------- | ---------- | -| defaultValue | 默认值 | Number | 0 | -| size | 尺寸

**可选值**:
'small', 'medium', 'large' | Enum | 'medium' | -| value | 值 | Number | - | -| count | 评分的总数 | Number | 5 | -| showGrade | 是否显示 grade | Boolean | false | -| allowHalf | 是否允许半星评分 | Boolean | false | -| allowClear | 是否允许再次点击后清除 | Boolean | false | -| onChange | 用户点击评分时触发的回调

**签名**:
Function(value: Number) => void
**参数**:
_value_: {Number} 评分值 | Function | func.noop | -| onHoverChange | 用户hover评分时触发的回调

**签名**:
Function(value: Number) => void
**参数**:
_value_: {Number} 评分值 | Function | func.noop | -| disabled | 是否禁用 | Boolean | false | -| readAs | 评分文案生成方法,传入id支持无障碍时,读屏软件可读

**签名**:
Function() => void | Function | val => val | -| isPreview | 是否为预览态 | Boolean | false | -| renderPreview | 预览态模式下渲染的内容

**签名**:
Function(value: number) => void
**参数**:
_value_: {number} 评分值 | Function | - | -| readOnly | 是否为只读态,效果上同 disabeld | Boolean | false | +| 参数 | 说明 | 类型 | 默认值 | 是否必填 | +| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | -------- | -------- | +| defaultValue | 默认值 | number | - | | +| value | 值(受控模式) | number | - | | +| size | 尺寸 | 'small' \| 'medium' \| 'large' | 'medium' | | +| count | 评分的总数 | number | 5 | | +| showGrade | 是否显示 grade | boolean | false | | +| allowHalf | 是否允许半星评分 | boolean | - | | +| allowClear | 是否允许再次点击后清除 | boolean | false | | +| onChange | 用户点击评分时触发的回调 | (value: number) => void | - | | +| onHoverChange | 用户 hover 评分时触发的回调 | (value?: number) => void | - | | +| disabled | 是否禁用 | boolean | false | | +| isPreview | 是否为预览态 | boolean | false | | +| renderPreview | 预览态模式下渲染的内容(isPreview 时必传,否则预览不生效)

**签名**:
**参数**:
_value_: 评分值
_props_: 组件参数对象
**返回值**:
预览模式下的渲染内容 | (value: number, props: RatingProps) => React.ReactNode | - | | +| readAs | 评分文案生成方法,传入 id 支持无障碍时,读屏软件可读

**签名**:
**参数**:
_val_: 当前分值
**返回值**:
该分值的渲染文案 | (val: number) => React.ReactNode | - | | +| type | - | string | - | | ## 无障碍键盘操作指南 -| 按键 | 说明 | -| :---------- | :----- | +| 按键 | 说明 | +| :---------- | :----------- | | Up Arrow | 增加星级评分 | | Down Arrow | 减少星级评分 | | Right Arrow | 增加星级评分 | diff --git a/components/rating/__docs__/theme/index.tsx b/components/rating/__docs__/theme/index.tsx index d0825dc3d2..3c590f563b 100644 --- a/components/rating/__docs__/theme/index.tsx +++ b/components/rating/__docs__/theme/index.tsx @@ -1,3 +1,5 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; import '../../../demo-helper/style'; import { Demo, DemoGroup, DemoHead, initDemo } from '../../../demo-helper'; import ConfigProvider from '../../../config-provider'; @@ -33,16 +35,17 @@ const i18nMap = { }, }; -function render(i18n, lang) { +function render(i18n: (typeof i18nMap)[keyof typeof i18nMap], lang: 'zh-cn' | 'en-us') { + // eslint-disable-next-line react/no-render-return-value return ReactDOM.render(
- - - + + + diff --git a/components/rating/__tests__/a11y-spec.tsx b/components/rating/__tests__/a11y-spec.tsx index 80d508a4a1..18c7e7be83 100644 --- a/components/rating/__tests__/a11y-spec.tsx +++ b/components/rating/__tests__/a11y-spec.tsx @@ -1,30 +1,14 @@ import React from 'react'; -import Enzyme from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; -import { unmount, testReact } from '../../util/__tests__/legacy/a11y/validate'; +import { testReact } from '../../util/__tests__/a11y/validate'; import Rating from '../index'; import '../style'; -Enzyme.configure({ adapter: new Adapter() }); - -/* eslint-disable no-undef, react/jsx-filename-extension */ describe('Rating A11y', () => { - let wrapper; - - afterEach(() => { - if (wrapper) { - wrapper.unmount(); - wrapper = null; - } - unmount(); - }); - it('should not have any violations', async () => { - wrapper = await testReact( + await testReact(
); - return wrapper; }); }); diff --git a/components/rating/__tests__/index-spec.tsx b/components/rating/__tests__/index-spec.tsx index 33bbf412a4..31221a9477 100644 --- a/components/rating/__tests__/index-spec.tsx +++ b/components/rating/__tests__/index-spec.tsx @@ -1,539 +1,240 @@ import React from 'react'; -import ReactDOM from 'react-dom'; -import ReactTestUtils from 'react-dom/test-utils'; -import Enzyme, { mount } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; -import assert from 'power-assert'; -import { setTimeout } from 'timers'; import Rating from '../index'; import '../style'; import { KEYCODE } from '../../util'; -Enzyme.configure({ adapter: new Adapter() }); +function triggerPosition(x: number, y: number, type: 'click' | 'mouseover') { + cy.get(`.next-rating`).then(dom => { + const rect = dom[0].getBoundingClientRect(); + cy.get('.next-rating-base').trigger(type, { + pageX: rect.left + x, + pageY: rect.top + y, + }); + }); +} -/* eslint-disable no-undef, react/jsx-filename-extension */ describe('Rating', () => { describe('render', () => { - let wrapper, parent; - - beforeEach(() => { - wrapper = mount(); - - parent = document.createElement('div'); - document.body.appendChild(parent); - }); - - afterEach(() => { - wrapper.unmount(); - wrapper = null; - - document.body.removeChild(parent); - parent = null; - }); - it('should render correct when value < min value', () => { - ReactDOM.render(, parent); + cy.mount(); // 16 x 0 + 4 x (0 + 1) = 0 + 4 = 4 - assert( - document.querySelectorAll('#render-test-0 .next-rating-overlay')[0].style.width <= - '4px' - ); + cy.get('.next-rating-overlay').should('have.css', 'width', '4px'); }); it('should render correct rating', () => { - ReactDOM.render(, parent); + cy.mount(); // 16 x 3 + 4 x (3 + 1) = 48 + 16 = 64 - assert( - document.querySelectorAll('#render-test-1 .next-rating-overlay')[0].style.width === - '64px' - ); + cy.get('.next-rating-overlay').should('have.css', 'width', '64px'); }); it('should render correct when value > count', () => { - ReactDOM.render(, parent); + cy.mount(); // 16 x 5 + 4 x (5 + 1) = 80 + 24 = 104 - assert( - parseInt( - document.querySelectorAll('#render-test-2 .next-rating-overlay')[0].style.width - ) >= 104 - ); + cy.get('.next-rating-overlay').should('have.css', 'width', '104px'); }); it('should render with different types', () => { - // 覆盖componentWillReceiveProps的setState - wrapper.setProps({ value: 3.3 }); - wrapper.setProps({ showGrade: true }); - assert(wrapper.find('.next-rating-grade-high').length === 1); + // 覆盖 componentWillReceiveProps 的 setState + cy.mount().as('x'); + cy.rerender('x', { value: 3.3, showGrade: true }); + cy.get('.next-rating-grade-high').should('have.length', 1); }); }); - describe('action', () => { - let ret, hval, parent, rect, onChange, onHoverChange; - - beforeEach(() => { - ret = -1; - hval = -1; - parent = document.createElement('div'); - document.body.appendChild(parent); - onChange = function (val) { - return (ret = val); - }; - onHoverChange = function (val) { - return (hval = val); - }; - }); - - afterEach(() => { - document.body.removeChild(parent); - parent = null; - rect = null; - onChange = null; - onHoverChange = null; - }); + describe.only('action', () => { + // let ret, hval, parent, rect, onChange, onHoverChange; + + // beforeEach(() => { + // ret = -1; + // hval = -1; + // parent = document.createElement('div'); + // document.body.appendChild(parent); + // onChange = function (val) { + // return (ret = val); + // }; + // onHoverChange = function (val) { + // return (hval = val); + // }; + // }); + + // afterEach(() => { + // document.body.removeChild(parent); + // parent = null; + // rect = null; + // onChange = null; + // onHoverChange = null; + // }); it('should be controlled ', () => { - ReactDOM.render(, parent); - rect = document.querySelectorAll('#action-test-0')[0].getBoundingClientRect(); - - ReactTestUtils.Simulate.click( - document.querySelectorAll('#action-test-0 .next-rating-base')[0], - { - pageX: rect.left + 8, - pageY: rect.top + 8, - } - ); - - assert(ret === 1); - assert( - document.querySelectorAll('#action-test-0 .next-rating-overlay')[0].style.width === - '64px' - ); + cy.mount(); + cy.get('.next-rating-overlay').should('have.css', 'width', '64px'); + triggerPosition(8, 8, 'click'); + cy.get('.next-rating-overlay').should('have.css', 'width', '64px'); }); it('should trigger click event', () => { - ReactDOM.render( - , - parent - ); - rect = document - .querySelectorAll('#action-test-1 .next-rating-icon')[0] - .getBoundingClientRect(); - - ReactTestUtils.Simulate.click( - document.querySelectorAll('#action-test-1 .next-rating-base')[0], - { - pageX: rect.left + 8, - pageY: rect.top + 8, - } - ); - - assert(ret === 1); + const onChange = cy.spy(); + cy.mount(); + triggerPosition(8, 8, 'click'); + cy.wrap(onChange).should('be.calledOnceWith', 1); }); it('should allowClear={false} work', () => { - ReactDOM.render( - , - parent - ); - rect = document - .querySelectorAll('#action-test-1 .next-rating-icon')[0] - .getBoundingClientRect(); - - ReactTestUtils.Simulate.click( - document.querySelectorAll('#action-test-1 .next-rating-base')[0], - { - pageX: rect.left + 8, - pageY: rect.top + 8, - } - ); - - assert(ret === 1); - - ReactTestUtils.Simulate.click( - document.querySelectorAll('#action-test-1 .next-rating-base')[0], - { - pageX: rect.left + 8, - pageY: rect.top + 8, - } - ); - - assert(ret === 1); + const onChange = cy.spy(); + cy.mount(); + triggerPosition(8, 8, 'click'); + triggerPosition(8, 8, 'click'); + cy.wrap(onChange).should('be.calledOnceWith', 1); }); it('should allowClear={true} work', () => { - ReactDOM.render( - , - parent - ); - rect = document - .querySelectorAll('#action-test-1 .next-rating-icon')[0] - .getBoundingClientRect(); - - ReactTestUtils.Simulate.click( - document.querySelectorAll('#action-test-1 .next-rating-base')[0], - { - pageX: rect.left + 8, - pageY: rect.top + 8, - } - ); - - assert(ret === 1); - - ReactTestUtils.Simulate.click( - document.querySelectorAll('#action-test-1 .next-rating-base')[0], - { - pageX: rect.left + 8, - pageY: rect.top + 8, - } - ); - - assert(ret === 0); + const onChange = cy.spy(); + cy.mount(); + triggerPosition(8, 8, 'click'); + triggerPosition(8, 8, 'click'); + cy.wrap(onChange).should('be.calledTwice'); + cy.wrap(onChange).should('be.calledWith', 1); + cy.wrap(onChange).should('be.calledWith', 0); }); it('should trigger click event correct', () => { - ReactDOM.render( - , - parent - ); - rect = document - .querySelectorAll('#action-test-1 .next-rating-icon')[0] - .getBoundingClientRect(); - - ReactTestUtils.Simulate.click( - document.querySelectorAll('#action-test-1 .next-rating-base')[0], - { - pageX: rect.left + 1000, - pageY: rect.top + 8, - } - ); - - assert(ret === 5); + const onChange = cy.spy(); + cy.mount(); + triggerPosition(1000, 8, 'click'); + cy.wrap(onChange).should('be.calledOnceWith', 5); }); - it('should trigger mouse event', done => { - ReactDOM.render( - , - parent - ); - rect = document - .querySelectorAll('#action-test-2 .next-rating-icon')[0] - .getBoundingClientRect(); - - ReactTestUtils.Simulate.mouseOver( - document.querySelectorAll('#action-test-2 .next-rating-base')[0], - { - pageX: rect.left + 8, - pageY: rect.top + 8, - } - ); - - setTimeout(() => { - // 16 x 1 + 4 x (1 + 1) = 24 - assert( - document.querySelectorAll('#action-test-2 .next-rating-overlay')[0].style - .width === '24px' - ); - done(); - }, 200); + it('should trigger mouse event', () => { + cy.mount(); + triggerPosition(8, 8, 'mouseover'); + cy.get('.next-rating-overlay').should('have.css', 'width', '24px'); }); - it('should trigger mouse event when allow half', done => { - ReactDOM.render( - , - parent - ); - rect = document - .querySelectorAll('#action-test-3 .next-rating-icon')[0] - .getBoundingClientRect(); - - ReactTestUtils.Simulate.mouseOver( - document.querySelectorAll('#action-test-3 .next-rating-base')[0], - { - pageX: rect.left + 2, - pageY: rect.top + 8, - } - ); - - setTimeout(() => { - // 16 * 0.5 + 4 x (0 + 1) = 12 - assert( - document.querySelectorAll('#action-test-3 .next-rating-overlay')[0].style - .width === '12px' - ); - done(); - }, 200); + it('should trigger mouse event when allow half', () => { + cy.mount(); + triggerPosition(4, 8, 'mouseover'); + cy.get('.next-rating-overlay').should('have.css', 'width', '12px'); }); - it('should trigger twice mouse over', done => { - ReactDOM.render(, parent); - rect = document - .querySelectorAll('#action-test-4 .next-rating-icon')[0] - .getBoundingClientRect(); - - ReactTestUtils.Simulate.mouseOver( - document.querySelectorAll('#action-test-4 .next-rating-base')[0], - { - pageX: rect.left + 4, - pageY: rect.top + 8, - } - ); - - ReactTestUtils.Simulate.mouseOver( - document.querySelectorAll('#action-test-4 .next-rating-base')[0], - { - pageX: rect.left + 48, - pageY: rect.top + 8, - } - ); - - setTimeout(() => { - assert( - document.querySelectorAll('#action-test-4 .next-rating-overlay')[0].style - .width === '64px' - ); - done(); - }, 300); + it('should trigger twice mouse over', () => { + cy.mount(); + triggerPosition(4, 8, 'mouseover'); + triggerPosition(48, 8, 'mouseover'); + cy.get('.next-rating-overlay').should('have.css', 'width', '64px'); }); - it('should trigger mouse leave', done => { - ReactDOM.render( - , - parent - ); - rect = document - .querySelectorAll('#action-test-5 .next-rating-icon')[0] - .getBoundingClientRect(); - - ReactTestUtils.Simulate.mouseOver( - document.querySelectorAll('#action-test-5 .next-rating-base')[0], - { - pageX: rect.left + 4, - pageY: rect.top + 8, - } - ); - ReactTestUtils.Simulate.mouseLeave( - document.querySelectorAll('#action-test-5 .next-rating-base')[0] - ); - - setTimeout(() => { - // 16 x 3 + 4 x (3 + 1) = 48 + 16 = 64 - assert( - document.querySelectorAll('#action-test-5 .next-rating-overlay')[0].style - .width === '64px' - ); - done(); - }, 100); + it('should trigger mouse leave', () => { + cy.mount(); + triggerPosition(4, 8, 'mouseover'); + cy.get('.next-rating-overlay').should('have.css', 'width', '24px'); + cy.get('.next-rating-base').trigger('mouseout'); + cy.get('.next-rating-overlay').should('have.css', 'width', '64px'); }); it('should render disabled rating', () => { - ReactDOM.render( - , - parent - ); - rect = document - .querySelectorAll('#action-test-6 .next-rating-icon')[0] - .getBoundingClientRect(); - - ReactTestUtils.Simulate.click( - document.querySelectorAll('#action-test-6 .next-rating-base')[0], - { - pageX: rect.left + 8, - pageY: rect.top + 8, - } - ); - - assert(ret === -1); - assert( - document.querySelectorAll('#action-test-6 .next-rating-overlay')[0].style.width === - '64px' - ); + const onChange = cy.spy(); + cy.mount(); + triggerPosition(8, 8, 'click'); + cy.wrap(onChange).should('not.be.called'); + cy.get('.next-rating-overlay').should('have.css', 'width', '64px'); }); it('should render readonly rating', () => { - ReactDOM.render( - , - parent - ); - rect = document - .querySelectorAll('#action-test-6 .next-rating-icon')[0] - .getBoundingClientRect(); - - ReactTestUtils.Simulate.click( - document.querySelectorAll('#action-test-6 .next-rating-base')[0], - { - pageX: rect.left + 8, - pageY: rect.top + 8, - } - ); - - assert(ret === -1); - assert( - document.querySelectorAll('#action-test-6 .next-rating-overlay')[0].style.width === - '64px' - ); + const onChange = cy.spy(); + cy.mount(); + triggerPosition(8, 8, 'click'); + cy.wrap(onChange).should('not.be.called'); + cy.get('.next-rating-overlay').should('have.css', 'width', '64px'); }); it('should renderPreview', () => { - ReactDOM.render( - 'preview rating'} - />, - parent - ); - - assert(document.querySelectorAll('#action-preview')[0].innerText === 'preview rating'); + cy.mount( 'preview rating'} />); + cy.get('.next-form-preview').should('have.text', 'preview rating'); }); - it('should trigger mouse over', done => { - ReactDOM.render(, parent); - rect = document - .querySelectorAll('#action-test-7 .next-rating-icon')[0] - .getBoundingClientRect(); - - ReactTestUtils.Simulate.mouseOver( - document.querySelectorAll('#action-test-7 .next-rating-base')[0], - { - pageX: rect.left + 4, - pageY: rect.top + 8, - } - ); - - ReactTestUtils.Simulate.mouseOver( - document.querySelectorAll('#action-test-7 .next-rating-base')[0], - { - pageX: rect.left + 48, - pageY: rect.top + 8, - } - ); - - setTimeout(() => { - assert(hval === 3); - done(); - }, 300); + it('should trigger mouse over', () => { + const onHoverChange = cy.spy(); + cy.mount(); + triggerPosition(4, 8, 'mouseover'); + triggerPosition(48, 8, 'mouseover'); + cy.wrap(onHoverChange).should('be.calledTwice'); + cy.wrap(onHoverChange).should('be.calledWith', 1); + cy.wrap(onHoverChange).should('be.calledWith', 3); }); it('should trigger keyboard right event correct', () => { - ReactDOM.render( - , - parent - ); - rect = document.querySelector('#action-test-1'); - ReactTestUtils.Simulate.keyDown(rect, { keyCode: KEYCODE.RIGHT }); - ReactTestUtils.Simulate.keyDown(rect, { keyCode: KEYCODE.ENTER }); - - assert(ret === 4); + const onChange = cy.spy(); + cy.mount(); + cy.get('.next-rating').trigger('keydown', { keyCode: KEYCODE.RIGHT }); + cy.get('.next-rating').trigger('keydown', { keyCode: KEYCODE.ENTER }); + cy.wrap(onChange).should('be.calledOnceWith', 4); }); it('should trigger keyboard left event correct', () => { - ReactDOM.render( - , - parent - ); - rect = document.querySelector('#action-test-1'); - ReactTestUtils.Simulate.keyDown(rect, { keyCode: KEYCODE.LEFT }); - ReactTestUtils.Simulate.keyDown(rect, { keyCode: KEYCODE.ENTER }); - - assert(ret === 2); + const onChange = cy.spy(); + cy.mount(); + cy.get('.next-rating').trigger('keydown', { keyCode: KEYCODE.LEFT }); + cy.get('.next-rating').trigger('keydown', { keyCode: KEYCODE.ENTER }); + cy.wrap(onChange).should('be.calledOnceWith', 2); }); it('should trigger keyboard down event correct', () => { - ReactDOM.render( - , - parent - ); - rect = document.querySelector('#action-test-1'); - ReactTestUtils.Simulate.keyDown(rect, { keyCode: KEYCODE.DOWN }); - ReactTestUtils.Simulate.keyDown(rect, { keyCode: KEYCODE.ENTER }); - - assert(ret === 4); + const onChange = cy.spy(); + cy.mount(); + cy.get('.next-rating').trigger('keydown', { keyCode: KEYCODE.DOWN }); + cy.get('.next-rating').trigger('keydown', { keyCode: KEYCODE.ENTER }); + cy.wrap(onChange).should('be.calledOnceWith', 4); }); it('should trigger keyboard up event correct', () => { - ReactDOM.render( - , - parent - ); - rect = document.querySelector('#action-test-1'); - ReactTestUtils.Simulate.keyDown(rect, { keyCode: KEYCODE.UP }); - ReactTestUtils.Simulate.keyDown(rect, { keyCode: KEYCODE.ENTER }); - - assert(ret === 2); + const onChange = cy.spy(); + cy.mount(); + cy.get('.next-rating').trigger('keydown', { keyCode: KEYCODE.UP }); + cy.get('.next-rating').trigger('keydown', { keyCode: KEYCODE.ENTER }); + cy.wrap(onChange).should('be.calledOnceWith', 2); }); it('should ignore unsupported keyboard event correct', () => { - ReactDOM.render( - , - parent - ); - rect = document.querySelector('#action-test-1'); - ReactTestUtils.Simulate.keyDown(rect, { keyCode: KEYCODE.SPACE }); - ReactTestUtils.Simulate.keyDown(rect, { keyCode: KEYCODE.ENTER }); - - assert(ret === 3); + const onChange = cy.spy(); + cy.mount(); + cy.get('.next-rating').trigger('keydown', { keyCode: KEYCODE.SPACE }); + cy.get('.next-rating').trigger('keydown', { keyCode: KEYCODE.ENTER }); + cy.wrap(onChange).should('be.calledOnceWith', 3); }); it('should trigger keyboard left event correct when value is 0', () => { - ReactDOM.render( - , - parent - ); - rect = document.querySelector('#action-test-1'); - ReactTestUtils.Simulate.keyDown(rect, { keyCode: KEYCODE.LEFT }); - ReactTestUtils.Simulate.keyDown(rect, { keyCode: KEYCODE.ENTER }); - - assert(ret === 5); + const onChange = cy.spy(); + cy.mount(); + cy.get('.next-rating').trigger('keydown', { keyCode: KEYCODE.LEFT }); + cy.get('.next-rating').trigger('keydown', { keyCode: KEYCODE.ENTER }); + cy.wrap(onChange).should('be.calledOnceWith', 5); }); it('should trigger keyboard right event correct when value is the count of rating', () => { - ReactDOM.render( - , - parent - ); - rect = document.querySelector('#action-test-1'); - ReactTestUtils.Simulate.keyDown(rect, { keyCode: KEYCODE.RIGHT }); - ReactTestUtils.Simulate.keyDown(rect, { keyCode: KEYCODE.ENTER }); - - assert(ret === 1); + const onChange = cy.spy(); + cy.mount(); + cy.get('.next-rating').trigger('keydown', { keyCode: KEYCODE.RIGHT }); + cy.get('.next-rating').trigger('keydown', { keyCode: KEYCODE.ENTER }); + cy.wrap(onChange).should('be.calledOnceWith', 1); }); it('should behave correct when click outside rating', () => { - ReactDOM.render( - , - parent - ); - rect = document - .querySelectorAll('#action-test-1 .next-rating-icon')[0] - .getBoundingClientRect(); - - ReactTestUtils.Simulate.click( - document.querySelectorAll('#action-test-1 .next-rating-base')[0], - { - pageX: rect.left - 1000, - pageY: rect.top + 8, - } - ); - - assert(ret === -1); + const onChange = cy.spy(); + cy.mount(); + triggerPosition(-1000, 8, 'click'); + cy.wrap(onChange).should('not.be.called'); }); it('should support rtl', () => { - const wrapper = mount( - - ); - assert(wrapper.find('.next-rating').props().dir === 'rtl'); + cy.mount(); + cy.get('.next-rating[dir="rtl"]').should('exist'); }); it('should support rtl in half mode', () => { - const wrapper = mount( - - ); - assert(wrapper.find('.next-rating').props().dir === 'rtl'); + cy.mount(); + cy.get('.next-rating[dir="rtl"]').should('exist'); }); }); }); diff --git a/components/rating/index.tsx b/components/rating/index.tsx index 483e0d075b..54f51b43ee 100644 --- a/components/rating/index.tsx +++ b/components/rating/index.tsx @@ -1,8 +1,10 @@ import ConfigProvider from '../config-provider'; import Rating from './rating'; +export type { RatingProps, RatingLocale } from './types'; + export default ConfigProvider.config(Rating, { - transform: /* istanbul ignore next */ (props, deprecated) => { + transform: (props, deprecated) => { if ('type' in props) { deprecated('type', 'showGrade', 'Rating'); diff --git a/components/rating/rating.tsx b/components/rating/rating.tsx index 34917659e7..a083ba9ff7 100644 --- a/components/rating/rating.tsx +++ b/components/rating/rating.tsx @@ -1,96 +1,47 @@ -import React, { Component } from 'react'; +import React, { Component, type KeyboardEvent, type MouseEvent } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { polyfill } from 'react-lifecycles-compat'; - import Icon from '../icon'; -import { func, KEYCODE, obj } from '../util'; +import ConfigProvider from '../config-provider'; +import { func, KEYCODE, obj, type ClassPropsWithDefault } from '../util'; import zhCN from '../locale/zh-cn'; +import type { RatingProps, RatingState } from './types'; const { noop, bindCtx } = func; const { ENTER, LEFT, UP, RIGHT, DOWN } = KEYCODE; const supportKeys = [ENTER, LEFT, UP, RIGHT, DOWN]; -// 评分组件的大小与icon的大小映射关系 +// 评分组件的大小与 icon 的大小映射关系 const ICON_SIZE_MAP = { small: 'xs', medium: 'small', large: 'medium', -}; +} as const; -/** Rating */ -class Rating extends Component { +class Rating extends Component { static propTypes = { + ...ConfigProvider.propTypes, prefix: PropTypes.string, - /** - * 默认值 - */ defaultValue: PropTypes.number, - /** - * 值 - */ value: PropTypes.number, - /** - * 评分的总数 - */ count: PropTypes.number, - /** - * 是否显示 grade - */ showGrade: PropTypes.bool, - /** - * 尺寸 - */ size: PropTypes.oneOf(['small', 'medium', 'large']), - /** - * 是否允许半星评分 - */ allowHalf: PropTypes.bool, - /** - * 是否允许再次点击后清除 - */ allowClear: PropTypes.bool, - /** - * 用户点击评分时触发的回调 - * @param {Number} value 评分值 - */ onChange: PropTypes.func, - /** - * 用户hover评分时触发的回调 - * @param {Number} value 评分值 - */ onHoverChange: PropTypes.func, - /** - * 是否禁用 - */ disabled: PropTypes.bool, - /** - * 评分文案生成方法,传入id支持无障碍时,读屏软件可读 - */ readAs: PropTypes.func, - // 实验属性: 自定义评分icon iconType: PropTypes.string, - // 实验属性: 开启 `-webkit-text-stroke` 显示边框颜色,在IE中无效 strokeMode: PropTypes.bool, className: PropTypes.string, id: PropTypes.string, rtl: PropTypes.bool, - /** - * 自定义国际化文案对象 - */ locale: PropTypes.object, - /** - * 是否为预览态 - */ isPreview: PropTypes.bool, - /** - * 预览态模式下渲染的内容 - * @param {number} value 评分值 - */ renderPreview: PropTypes.func, - /** - * 是否为只读态,效果上同 disabeld - */ readOnly: PropTypes.bool, }; @@ -103,7 +54,7 @@ class Rating extends Component { count: 5, showGrade: false, defaultValue: 0, - readAs: val => val, + readAs: (val: number) => val, allowHalf: false, allowClear: false, onChange: noop, @@ -111,7 +62,7 @@ class Rating extends Component { locale: zhCN.Rating, }; - static currentValue(min, max, hoverValue, stateValue) { + static currentValue(min: number, max: number, hoverValue: number, stateValue: number) { let value = hoverValue ? hoverValue : stateValue; value = value >= max ? max : value; @@ -120,24 +71,8 @@ class Rating extends Component { return value || 0; } - constructor(props) { - super(props); - - this.state = { - value: 'value' in props ? props.value : props.defaultValue, - hoverValue: 0, - cleanedValue: null, - iconSpace: 0, - iconSize: 0, - clicked: false, // 标记组件是否被点击过 - }; - this.timer = null; - - bindCtx(this, ['handleClick', 'handleHover', 'handleLeave', 'onKeyDown']); - } - - static getDerivedStateFromProps(nextProps, prevState) { - const state = {}; + static getDerivedStateFromProps(nextProps: RatingProps) { + const state: Partial = {}; if ('value' in nextProps) { state.value = nextProps.value || 0; } @@ -157,6 +92,26 @@ class Rating extends Component { return state; } + timer: ReturnType | null; + underlayNode: HTMLDivElement | null = null; + readonly props: ClassPropsWithDefault; + + constructor(props: RatingProps) { + super(props); + + this.state = { + // @ts-expect-error FIXME 这里没有像 getDerivedStateFromProps 内那样处理 props.value 为 undefined 时的情况,先标记 + value: 'value' in props ? props.value : props.defaultValue, + hoverValue: 0, + cleanedValue: null, + iconSpace: 0, + iconSize: 0, + clicked: false, // 标记组件是否被点击过 + }; + this.timer = null; + bindCtx(this, ['handleClick', 'handleHover', 'handleLeave', 'onKeyDown']); + } + componentDidMount() { this.getRenderResult(); } @@ -165,6 +120,8 @@ class Rating extends Component { this.clearTimer(); } + [key: `refs-rating-icon-${number}`]: HTMLSpanElement | null; + // 清除延时 clearTimer() { if (this.timer) { @@ -192,14 +149,14 @@ class Rating extends Component { } } - getValue(e) { + getValue(e: MouseEvent) { // 如定位不准,优先纠正定位 this.getRenderResult(); const { allowHalf, count, rtl } = this.props; const { iconSpace, iconSize } = this.state; - const pos = e.pageX - this.underlayNode.getBoundingClientRect().left; + const pos = e.pageX - this.underlayNode!.getBoundingClientRect().left; const fullNum = Math.floor(pos / (iconSpace + iconSize)); const surplusNum = (pos - fullNum * (iconSpace + iconSize) - iconSpace) / iconSize; let value = Number(fullNum) + Number(surplusNum.toFixed(1)); @@ -219,7 +176,7 @@ class Rating extends Component { return rtl ? count - value + 1 : value; } - handleHover(e) { + handleHover(e: MouseEvent) { if (this.state.disabled) { return; } @@ -253,7 +210,7 @@ class Rating extends Component { onHoverChange(undefined); } - onKeyDown(e) { + onKeyDown(e: KeyboardEvent) { if (this.state.disabled) { return; } @@ -300,7 +257,7 @@ class Rating extends Component { return !onKeyDown || onKeyDown(e); } - handleChecked(index) { + handleChecked(index: number) { if (this.state.disabled) { return; } @@ -308,7 +265,7 @@ class Rating extends Component { this.setState({ hoverValue: index }); } - handleClick(e) { + handleClick(e: MouseEvent) { if (this.state.disabled) { return; } @@ -361,7 +318,7 @@ class Rating extends Component { return iconSize * (ceilValue - 1) + ceilValue * iconSpace; } - saveRef = (ref, i) => { + saveRef = (ref: HTMLSpanElement | null, i: number) => { this[`refs-rating-icon-${i}`] = ref; }; @@ -390,10 +347,10 @@ class Rating extends Component { const enableA11y = !!id; - // 获得Value + // 获得 Value const value = Rating.currentValue(0, count, hoverValue, this.state.value); - // icon的sizeMap + // icon 的 sizeMap const sizeMap = ICON_SIZE_MAP[size]; for (let i = 0; i < count; i++) { @@ -409,7 +366,7 @@ class Rating extends Component { ); - const saveRefs = ref => { + const saveRefs = (ref: HTMLSpanElement | null) => { this.saveRef(ref, i); }; @@ -424,7 +381,9 @@ class Rating extends Component { id={`${id}-${prefix}star${i + 1}`} key={`input-${i}`} className={`${prefix}sr-only`} + // @ts-expect-error FIXME parseInt require number arg aria-checked={i + 1 === parseInt(hoverValue)} + // @ts-expect-error FIXME parseInt require number arg checked={i + 1 === parseInt(hoverValue)} onChange={this.handleChecked.bind(this, i + 1)} type="radio" @@ -436,7 +395,7 @@ class Rating extends Component { overlay.push(