├── .dumirc.ts ├── .editorconfig ├── .eslintrc.js ├── .fatherrc.ts ├── .github ├── dependabot.yml └── workflows │ ├── codeql.yml │ └── react-component-ci.yml ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── assets └── index.less ├── bunfig.toml ├── docs ├── changelog.md ├── demo │ ├── characterRender.md │ └── simple.md ├── examples │ ├── characterRender.tsx │ └── simple.tsx └── index.md ├── index.js ├── jest.config.js ├── now.json ├── package.json ├── src ├── Rate.tsx ├── Star.tsx ├── index.tsx ├── useRefs.ts └── util.ts ├── tests ├── __snapshots__ │ └── simple.spec.js.snap ├── props.spec.js ├── setup.js └── simple.spec.js ├── tsconfig.json └── typeings.d.ts /.dumirc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'dumi'; 2 | import path from 'path'; 3 | 4 | export default defineConfig({ 5 | alias: { 6 | 'rc-rate$': path.resolve('src'), 7 | 'rc-rate/es': path.resolve('src'), 8 | }, 9 | favicons: ['https://avatars0.githubusercontent.com/u/9441414?s=200&v=4'], 10 | themeConfig: { 11 | name: 'Rate', 12 | logo: 'https://avatars0.githubusercontent.com/u/9441414?s=200&v=4', 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [require.resolve('@umijs/fabric/dist/eslint')], 3 | rules: { 4 | 'jsx-a11y/no-autofocus': 0, 5 | }, 6 | overrides: [ 7 | { 8 | files: ['docs/**/*.tsx'], 9 | rules: { 10 | 'no-console': 0, 11 | }, 12 | }, 13 | ], 14 | }; 15 | -------------------------------------------------------------------------------- /.fatherrc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'father'; 2 | 3 | export default defineConfig({ 4 | plugins: ['@rc-component/father-plugin'], 5 | }); 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "21:00" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: "@types/react" 11 | versions: 12 | - 17.0.0 13 | - 17.0.1 14 | - 17.0.2 15 | - 17.0.3 16 | - dependency-name: "@types/react-dom" 17 | versions: 18 | - 17.0.0 19 | - 17.0.1 20 | - 17.0.2 21 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | schedule: 9 | - cron: "7 4 * * 2" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ javascript ] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v3 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v2 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: +security-and-quality 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v2 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v2 40 | with: 41 | category: "/language:${{ matrix.language }}" 42 | -------------------------------------------------------------------------------- /.github/workflows/react-component-ci.yml: -------------------------------------------------------------------------------- 1 | name: ✅ test 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | uses: react-component/rc-test/.github/workflows/test.yml@main 6 | secrets: inherit -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.log 3 | .idea 4 | .ipr 5 | .iws 6 | *~ 7 | ~* 8 | *.diff 9 | *.patch 10 | *.bak 11 | .DS_Store 12 | Thumbs.db 13 | .project 14 | .*proj 15 | .svn 16 | *.swp 17 | *.swo 18 | *.pyc 19 | *.pyo 20 | node_modules 21 | .cache 22 | *.css 23 | build 24 | lib 25 | es 26 | coverage 27 | yarn.lock 28 | package-lock.json 29 | .doc/ 30 | 31 | .doc 32 | # dumi 33 | .dumi/tmp 34 | .dumi/tmp-test 35 | .dumi/tmp-production 36 | 37 | bun.lockb -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "proseWrap": "never", 5 | "printWidth": 100 6 | } 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.9.1 4 | 5 | `2020-11-17` 6 | 7 | - fix: character type [#102](https://github.com/react-component/rate/pull/102) 8 | 9 | ## 2.9.0 10 | 11 | `2020-11-02` 12 | 13 | - Update devDependencies include np/typescript/rc-tooltip 14 | - Add peerDependencies [#97](https://github.com/react-component/rate/pull/97) 15 | 16 | ## 2.8.2 17 | 18 | `2020-06-15` 19 | - fix: improve defaultvalue more than half [#86](https://github.com/react-component/rate/pull/86) 20 | 21 | - fix: star tabindex when disabled [#87](https://github.com/react-component/rate/pull/87) 22 | 23 | ## 2.8.1 24 | 25 | `2020-06-12` 26 | - feat: character support props [#85](https://github.com/react-component/rate/pull/85) 27 | 28 | ## 2.8.0 29 | 30 | `2020-06-10` 31 | - feat: expand character [#84](https://github.com/react-component/rate/pull/84) 32 | 33 | ## 2.7.0 34 | 35 | `2020-05-29` 36 | - 🆙 upgrade rc-util to 5.x 37 | 38 | ## 2.6.0 39 | 40 | `2020-04-16` 41 | - feat: add direction rtl [#80](https://github.com/react-component/rate/pull/80) 42 | - chore: use father [#81](https://github.com/react-component/rate/pull/81) 43 | 44 | ## 2.4.1 45 | 46 | - Better accessibility support. 47 | 48 | ## 2.4.0 49 | 50 | - Add allowClear support. 51 | 52 | ## 2.3.0 53 | 54 | - Add keyboard support. 55 | - Add focus() blur() and autoFocus. 56 | 57 | ## 2.1.0 58 | 59 | - Fix typo `charactor` to `character`. 60 | 61 | ## 2.0.0 62 | 63 | - Add `character`. 64 | - Add `className`. 65 | - Add `onHoverChange(value)`. 66 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-present yiminghe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rc-rate 2 | 3 | React Rate Component 4 | 5 | [![NPM version][npm-image]][npm-url] 6 | [![npm download][download-image]][download-url] 7 | [![build status][github-actions-image]][github-actions-url] 8 | [![Codecov][codecov-image]][codecov-url] 9 | [![bundle size][bundlephobia-image]][bundlephobia-url] 10 | [![dumi][dumi-image]][dumi-url] 11 | 12 | [npm-image]: http://img.shields.io/npm/v/rc-rate.svg?style=flat-square 13 | [npm-url]: http://npmjs.org/package/rc-rate 14 | [github-actions-image]: https://github.com/react-component/rate/workflows/CI/badge.svg 15 | [github-actions-url]: https://github.com/react-component/rate/actions 16 | [codecov-image]: https://img.shields.io/codecov/c/github/react-component/rate/master.svg?style=flat-square 17 | [codecov-url]: https://codecov.io/gh/react-component/rate/branch/master 18 | [david-url]: https://david-dm.org/react-component/rate 19 | [david-image]: https://david-dm.org/react-component/rate/status.svg?style=flat-square 20 | [david-dev-url]: https://david-dm.org/react-component/rate?type=dev 21 | [david-dev-image]: https://david-dm.org/react-component/rate/dev-status.svg?style=flat-square 22 | [download-image]: https://img.shields.io/npm/dm/rc-rate.svg?style=flat-square 23 | [download-url]: https://npmjs.org/package/rc-rate 24 | [bundlephobia-url]: https://bundlephobia.com/result?p=rc-rate 25 | [bundlephobia-image]: https://badgen.net/bundlephobia/minzip/rc-rate 26 | [dumi-url]: https://github.com/umijs/dumi 27 | [dumi-image]: https://img.shields.io/badge/docs%20by-dumi-blue?style=flat-square 28 | 29 | ## Screenshots 30 | 31 | 32 | 33 | ## Changelog 34 | 35 | - [CHANGELOG](./CHANGELOG.md) 36 | 37 | ## Development 38 | 39 | ``` 40 | npm install 41 | npm start 42 | ``` 43 | 44 | ## Example 45 | 46 | - Local: http://localhost:9001/ 47 | 48 | - Online: http://react-component.github.io/rate/ 49 | 50 | ## install 51 | 52 | [![rc-rate](https://nodei.co/npm/rc-rate.png)](https://npmjs.org/package/rc-rate) 53 | 54 | ## Usage 55 | 56 | ```js 57 | import React from 'react'; 58 | import ReactDOM from 'react-dom'; 59 | import Rate from 'rc-rate'; 60 | 61 | ReactDOM.render( 62 | , 63 | document.getElementById('root') 64 | ) 65 | ``` 66 | 67 | ### with [styled-components](https://github.com/styled-components/styled-components) 68 | ```js 69 | import React from 'react'; 70 | import ReactDOM from 'react-dom'; 71 | import Rate from 'rc-rate'; 72 | import styled from 'styled-components'; 73 | 74 | const StyledRate = styled(Rate)` 75 | &.rc-rate { 76 | font-size: ${({ size }) => size}px; 77 | } 78 | ` 79 | 80 | ReactDOM.render( 81 | , 82 | document.getElementById('root') 83 | ) 84 | ``` 85 | 86 | ## API 87 | 88 | ### props 89 | 90 | | name | type | default | description | 91 | | ------------- | --------------------------------- | ------------- | ----------------------------------------------------- | 92 | | count | number | 5 | Star numbers | 93 | | value | number | - | Controlled value | 94 | | defaultValue | number | 0 | Initial value | 95 | | allowHalf | boolean | false | Support half star | 96 | | allowClear | boolean | true | Reset when click again | 97 | | style | object | {} | | 98 | | onChange | function | (value) => {} | `onChange` will be triggered when click | 99 | | onHoverChange | function | (value) => {} | `onHoverChange` will be triggered when hover on stars | 100 | | character | ReactNode \| (props) => ReactNode | ★ | The each character of rate | 101 | | disabled | boolean | false | | 102 | | direction | string | `ltr` | The direction of rate | 103 | 104 | ## Test Case 105 | 106 | ``` 107 | npm test 108 | npm run chrome-test 109 | ``` 110 | 111 | ## Coverage 112 | 113 | ``` 114 | npm run coverage 115 | ``` 116 | 117 | open coverage/ dir 118 | 119 | ## License 120 | 121 | rc-rate is released under the MIT license. 122 | -------------------------------------------------------------------------------- /assets/index.less: -------------------------------------------------------------------------------- 1 | @rate-prefix-cls: rc-rate; 2 | @rate-star-color: #f5a623; 3 | @font-size-base: 13px; 4 | 5 | .@{rate-prefix-cls} { 6 | margin: 0; 7 | padding: 0; 8 | list-style: none; 9 | font-size: 18px; 10 | display: inline-block; 11 | vertical-align: middle; 12 | font-weight: normal; 13 | font-style: normal; 14 | outline: none; 15 | 16 | &-rtl { 17 | direction: rtl; 18 | } 19 | 20 | &-disabled &-star { 21 | cursor: default; 22 | &:before, 23 | &-content:before { 24 | cursor: default; 25 | } 26 | &:hover { 27 | transform: scale(1); 28 | } 29 | } 30 | 31 | &-star { 32 | margin: 0; 33 | padding: 0; 34 | display: inline-block; 35 | margin-right: 8px; 36 | position: relative; 37 | transition: all .3s; 38 | color: #e9e9e9; 39 | cursor: pointer; 40 | line-height: 1.5; 41 | 42 | .@{rate-prefix-cls}-rtl & { 43 | margin-right: 0; 44 | margin-left: 8px; 45 | float: right; 46 | } 47 | 48 | &-first, 49 | &-second { 50 | transition: all .3s; 51 | } 52 | 53 | &-focused, &:hover { 54 | transform: scale(1.1); 55 | } 56 | 57 | &-first { 58 | position: absolute; 59 | left: 0; 60 | top: 0; 61 | width: 50%; 62 | height: 100%; 63 | overflow: hidden; 64 | opacity: 0; 65 | 66 | .@{rate-prefix-cls}-rtl & { 67 | right: 0; 68 | left: auto; 69 | } 70 | } 71 | 72 | &-half &-first, 73 | &-half &-second { 74 | opacity: 1; 75 | } 76 | 77 | &-half &-first, 78 | &-full &-second { 79 | color: @rate-star-color; 80 | } 81 | 82 | &-half:hover &-first, 83 | &-full:hover &-second { 84 | color: tint(@rate-star-color,30%); 85 | } 86 | } 87 | } 88 | 89 | @icon-url: "//at.alicdn.com/t/font_r5u29ls31bgldi"; 90 | 91 | @font-face { 92 | font-family: 'anticon'; 93 | src: url('@{icon-url}.eot'); /* IE9*/ 94 | src: url('@{icon-url}.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ url('@{icon-url}.woff') format('woff'), /* chrome、firefox */ url('@{icon-url}.ttf') format('truetype'), /* chrome、firefox、opera、Safari, Android, iOS 4.2+*/ url('@{icon-url}.svg#iconfont') format('svg'); /* iOS 4.1- */ 95 | } 96 | 97 | .anticon { 98 | font-style: normal; 99 | vertical-align: baseline; 100 | text-align: center; 101 | text-transform: none; 102 | line-height: 1; 103 | text-rendering: optimizeLegibility; 104 | -webkit-font-smoothing: antialiased; 105 | -moz-osx-font-smoothing: grayscale; 106 | &:before { 107 | display: block; 108 | font-family: "anticon" !important; 109 | } 110 | } 111 | 112 | .anticon-star:before { content: "\e660"; }; 113 | -------------------------------------------------------------------------------- /bunfig.toml: -------------------------------------------------------------------------------- 1 | [install] 2 | peer = false -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/demo/characterRender.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: characterRender 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | -------------------------------------------------------------------------------- /docs/demo/simple.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: simple 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | -------------------------------------------------------------------------------- /docs/examples/characterRender.tsx: -------------------------------------------------------------------------------- 1 | /* eslint no-console: 0 */ 2 | import React from 'react'; 3 | import Tooltip from 'rc-tooltip'; 4 | import 'rc-tooltip/assets/bootstrap_white.css'; 5 | import Rate from 'rc-rate'; 6 | import '../../assets/index.less'; 7 | 8 | export default () => ( 9 |
10 | ( 13 | 14 | {node} 15 | 16 | )} 17 | /> 18 |
19 | ); 20 | -------------------------------------------------------------------------------- /docs/examples/simple.tsx: -------------------------------------------------------------------------------- 1 | /* eslint no-console: 0 */ 2 | import React from 'react'; 3 | import Rate from 'rc-rate'; 4 | import '../../assets/index.less'; 5 | 6 | function onChange(v: number) { 7 | console.log('selected star', v); 8 | } 9 | 10 | export default () => ( 11 |
12 |

Base

13 | 20 |
21 | 28 |
29 | { 34 | return index + 1; 35 | }} 36 | /> 37 |
38 | } 44 | /> 45 |

Disabled

46 | } 52 | /> 53 |

RTL

54 | } 61 | /> 62 |
63 | ); 64 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | hero: 3 | title: rc-rate 4 | description: React Rate Component 5 | --- 6 | 7 | 8 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // export this package's api 2 | import Rate from './src/'; 3 | export default Rate; 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | setupFiles: ["./tests/setup.js"], 3 | snapshotSerializers: [require.resolve("enzyme-to-json/serializer")], 4 | }; 5 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "name": "rc-rate", 4 | "builds": [ 5 | { 6 | "src": "package.json", 7 | "use": "@now/static-build", 8 | "config": { "distDir": "dist" } 9 | } 10 | ], 11 | "routes": [ 12 | { "src": "/(.*)", "dest": "/dist/$1" } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rc-rate", 3 | "version": "2.13.1", 4 | "description": "React Star Rate Component", 5 | "engines": { 6 | "node": ">=8.x" 7 | }, 8 | "keywords": [ 9 | "react", 10 | "react-component", 11 | "react-rate", 12 | "rate" 13 | ], 14 | "homepage": "https://github.com/react-component/rate", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/react-component/rate.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/react-component/rate/issues" 21 | }, 22 | "files": [ 23 | "lib", 24 | "es", 25 | "assets/*.css" 26 | ], 27 | "license": "MIT", 28 | "main": "./lib/index", 29 | "module": "./es/index", 30 | "scripts": { 31 | "start": "dumi dev", 32 | "docs:build": "dumi build", 33 | "docs:deploy": "gh-pages -d .doc", 34 | "compile": "father build && lessc assets/index.less assets/index.css", 35 | "prepare": "dumi setup", 36 | "prepublishOnly": "npm run compile && np --yolo --no-publish", 37 | "postpublish": "npm run docs:build && npm run docs:deploy", 38 | "lint": "eslint src/ --ext .ts,.tsx,.jsx,.js,.md", 39 | "prettier": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"", 40 | "test": "rc-test", 41 | "coverage": "rc-test --coverage", 42 | "now-build": "npm run docs:build" 43 | }, 44 | "dependencies": { 45 | "@babel/runtime": "^7.10.1", 46 | "classnames": "^2.2.5", 47 | "rc-util": "^5.0.1" 48 | }, 49 | "devDependencies": { 50 | "@rc-component/father-plugin": "^1.0.0", 51 | "@types/classnames": "^2.2.9", 52 | "@types/jest": "^29.5.1", 53 | "@types/react": "^17.0.15", 54 | "@types/react-dom": "^17.0.9", 55 | "@umijs/fabric": "^3.0.0", 56 | "cheerio": "1.0.0-rc.12", 57 | "cross-env": "^7.0.0", 58 | "dumi": "^2.1.2", 59 | "enzyme": "^3.1.1", 60 | "enzyme-adapter-react-16": "^1.15.6", 61 | "enzyme-to-json": "^3.1.2", 62 | "eslint": "^7.1.0", 63 | "father": "^4.0.0", 64 | "gh-pages": "^3.1.0", 65 | "less": "^3.0.0", 66 | "np": "^7.0.0", 67 | "rc-test": "^7.0.15", 68 | "rc-tooltip": "^5.0.1", 69 | "react": "^16.0.0", 70 | "react-dom": "^16.0.0", 71 | "typescript": "^5.0.4" 72 | }, 73 | "peerDependencies": { 74 | "react": ">=16.9.0", 75 | "react-dom": ">=16.9.0" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Rate.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import useMergedState from 'rc-util/lib/hooks/useMergedState'; 3 | import KeyCode from 'rc-util/lib/KeyCode'; 4 | import pickAttrs from 'rc-util/lib/pickAttrs'; 5 | import React from 'react'; 6 | import type { StarProps } from './Star'; 7 | import Star from './Star'; 8 | import useRefs from './useRefs'; 9 | import { getOffsetLeft } from './util'; 10 | 11 | export interface RateProps 12 | extends Pick { 13 | value?: number; 14 | defaultValue?: number; 15 | allowClear?: boolean; 16 | style?: React.CSSProperties; 17 | prefixCls?: string; 18 | onChange?: (value: number) => void; 19 | onHoverChange?: (value: number) => void; 20 | className?: string; 21 | tabIndex?: number; 22 | onFocus?: () => void; 23 | onBlur?: () => void; 24 | onKeyDown?: React.KeyboardEventHandler; 25 | onMouseEnter?: React.MouseEventHandler; 26 | onMouseLeave?: React.MouseEventHandler; 27 | id?: string; 28 | autoFocus?: boolean; 29 | direction?: string; 30 | /** 31 | * Is keyboard control enabled. 32 | * @default true 33 | */ 34 | keyboard?: boolean; 35 | } 36 | 37 | export interface RateRef { 38 | focus: VoidFunction; 39 | blur: VoidFunction; 40 | } 41 | 42 | function Rate(props: RateProps, ref: React.Ref) { 43 | const { 44 | // Base 45 | prefixCls = 'rc-rate', 46 | className, 47 | 48 | // Value 49 | defaultValue, 50 | value: propValue, 51 | count = 5, 52 | allowHalf = false, 53 | allowClear = true, 54 | keyboard = true, 55 | 56 | // Display 57 | character = '★', 58 | characterRender, 59 | 60 | // Meta 61 | disabled, 62 | direction = 'ltr', 63 | tabIndex = 0, 64 | autoFocus, 65 | 66 | // Events 67 | onHoverChange, 68 | onChange, 69 | onFocus, 70 | onBlur, 71 | onKeyDown, 72 | onMouseLeave, 73 | 74 | ...restProps 75 | } = props; 76 | 77 | const [getStarRef, setStarRef] = useRefs(); 78 | const rateRef = React.useRef(null); 79 | 80 | // ============================ Ref ============================= 81 | const triggerFocus = () => { 82 | if (!disabled) { 83 | rateRef.current?.focus(); 84 | } 85 | }; 86 | 87 | React.useImperativeHandle(ref, () => ({ 88 | focus: triggerFocus, 89 | blur: () => { 90 | if (!disabled) { 91 | rateRef.current?.blur(); 92 | } 93 | }, 94 | })); 95 | 96 | // =========================== Value ============================ 97 | const [value, setValue] = useMergedState(defaultValue || 0, { 98 | value: propValue, 99 | }); 100 | const [cleanedValue, setCleanedValue] = useMergedState(null); 101 | 102 | const getStarValue = (index: number, x: number) => { 103 | const reverse = direction === 'rtl'; 104 | let starValue = index + 1; 105 | if (allowHalf) { 106 | const starEle = getStarRef(index); 107 | const leftDis = getOffsetLeft(starEle); 108 | const width = starEle.clientWidth; 109 | if (reverse && x - leftDis > width / 2) { 110 | starValue -= 0.5; 111 | } else if (!reverse && x - leftDis < width / 2) { 112 | starValue -= 0.5; 113 | } 114 | } 115 | return starValue; 116 | }; 117 | 118 | // >>>>> Change 119 | const changeValue = (nextValue: number) => { 120 | setValue(nextValue); 121 | onChange?.(nextValue); 122 | }; 123 | 124 | // =========================== Focus ============================ 125 | const [focused, setFocused] = React.useState(false); 126 | 127 | const onInternalFocus = () => { 128 | setFocused(true); 129 | onFocus?.(); 130 | }; 131 | 132 | const onInternalBlur = () => { 133 | setFocused(false); 134 | onBlur?.(); 135 | }; 136 | 137 | // =========================== Hover ============================ 138 | const [hoverValue, setHoverValue] = React.useState(null); 139 | 140 | const onHover = (event: React.MouseEvent, index: number) => { 141 | const nextHoverValue = getStarValue(index, event.pageX); 142 | if (nextHoverValue !== cleanedValue) { 143 | setHoverValue(nextHoverValue); 144 | setCleanedValue(null); 145 | } 146 | onHoverChange?.(nextHoverValue); 147 | }; 148 | 149 | const onMouseLeaveCallback = (event?: React.MouseEvent) => { 150 | if (!disabled) { 151 | setHoverValue(null); 152 | setCleanedValue(null); 153 | onHoverChange?.(undefined); 154 | } 155 | if (event) { 156 | onMouseLeave?.(event); 157 | } 158 | }; 159 | 160 | // =========================== Click ============================ 161 | const onClick = (event: React.MouseEvent | React.KeyboardEvent, index: number) => { 162 | const newValue = getStarValue(index, (event as React.MouseEvent).pageX); 163 | let isReset = false; 164 | if (allowClear) { 165 | isReset = newValue === value; 166 | } 167 | onMouseLeaveCallback(); 168 | changeValue(isReset ? 0 : newValue); 169 | setCleanedValue(isReset ? newValue : null); 170 | }; 171 | 172 | const onInternalKeyDown: React.KeyboardEventHandler = (event) => { 173 | const { keyCode } = event; 174 | const reverse = direction === 'rtl'; 175 | const step = allowHalf ? 0.5 : 1; 176 | 177 | if (keyboard) { 178 | if (keyCode === KeyCode.RIGHT && value < count && !reverse) { 179 | changeValue(value + step); 180 | event.preventDefault(); 181 | } else if (keyCode === KeyCode.LEFT && value > 0 && !reverse) { 182 | changeValue(value - step); 183 | event.preventDefault(); 184 | } else if (keyCode === KeyCode.RIGHT && value > 0 && reverse) { 185 | changeValue(value - step); 186 | event.preventDefault(); 187 | } else if (keyCode === KeyCode.LEFT && value < count && reverse) { 188 | changeValue(value + step); 189 | event.preventDefault(); 190 | } 191 | } 192 | 193 | onKeyDown?.(event); 194 | }; 195 | 196 | // =========================== Effect =========================== 197 | 198 | React.useEffect(() => { 199 | if (autoFocus && !disabled) { 200 | triggerFocus(); 201 | } 202 | }, []); 203 | 204 | // =========================== Render =========================== 205 | // >>> Star 206 | const starNodes = new Array(count) 207 | .fill(0) 208 | .map((item, index) => ( 209 | 224 | )); 225 | 226 | const classString = classNames(prefixCls, className, { 227 | [`${prefixCls}-disabled`]: disabled, 228 | [`${prefixCls}-rtl`]: direction === 'rtl', 229 | }); 230 | 231 | // >>> Node 232 | return ( 233 |
    243 | {starNodes} 244 |
245 | ); 246 | } 247 | 248 | export default React.forwardRef(Rate); 249 | -------------------------------------------------------------------------------- /src/Star.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import KeyCode from 'rc-util/lib/KeyCode'; 3 | import classNames from 'classnames'; 4 | 5 | export interface StarProps { 6 | value?: number; 7 | index?: number; 8 | prefixCls?: string; 9 | allowHalf?: boolean; 10 | disabled?: boolean; 11 | onHover?: (e: React.MouseEvent, index: number) => void; 12 | onClick?: ( 13 | e: React.MouseEvent | React.KeyboardEvent, 14 | index: number, 15 | ) => void; 16 | character?: React.ReactNode | ((props: StarProps) => React.ReactNode); 17 | characterRender?: (origin: React.ReactElement, props: StarProps) => React.ReactNode; 18 | focused?: boolean; 19 | count?: number; 20 | } 21 | 22 | function Star(props: StarProps, ref: React.Ref) { 23 | const { 24 | disabled, 25 | prefixCls, 26 | character, 27 | characterRender, 28 | index, 29 | count, 30 | value, 31 | allowHalf, 32 | focused, 33 | onHover, 34 | onClick, 35 | } = props; 36 | 37 | // =========================== Events =========================== 38 | const onInternalHover: React.MouseEventHandler = (e) => { 39 | onHover(e, index); 40 | }; 41 | 42 | const onInternalClick: React.MouseEventHandler = (e) => { 43 | onClick(e, index); 44 | }; 45 | 46 | const onInternalKeyDown: React.KeyboardEventHandler = (e) => { 47 | if (e.keyCode === KeyCode.ENTER) { 48 | onClick(e, index); 49 | } 50 | }; 51 | 52 | // =========================== Render =========================== 53 | // >>>>> ClassName 54 | const starValue = index + 1; 55 | const classNameList = new Set([prefixCls]); 56 | 57 | // TODO: Current we just refactor from CC to FC. This logic seems can be optimized. 58 | if (value === 0 && index === 0 && focused) { 59 | classNameList.add(`${prefixCls}-focused`); 60 | } else if (allowHalf && value + 0.5 >= starValue && value < starValue) { 61 | classNameList.add(`${prefixCls}-half`); 62 | classNameList.add(`${prefixCls}-active`); 63 | if (focused) { 64 | classNameList.add(`${prefixCls}-focused`); 65 | } 66 | } else { 67 | if (starValue <= value) { 68 | classNameList.add(`${prefixCls}-full`); 69 | } else { 70 | classNameList.add(`${prefixCls}-zero`); 71 | } 72 | if (starValue === value && focused) { 73 | classNameList.add(`${prefixCls}-focused`); 74 | } 75 | } 76 | 77 | // >>>>> Node 78 | const characterNode = typeof character === 'function' ? character(props) : character; 79 | let start: React.ReactNode = ( 80 |
  • 81 |
    index ? 'true' : 'false'} 87 | aria-posinset={index + 1} 88 | aria-setsize={count} 89 | tabIndex={disabled ? -1 : 0} 90 | > 91 |
    {characterNode}
    92 |
    {characterNode}
    93 |
    94 |
  • 95 | ); 96 | 97 | if (characterRender) { 98 | start = characterRender(start as React.ReactElement, props); 99 | } 100 | 101 | return start as React.ReactElement; 102 | } 103 | 104 | export default React.forwardRef(Star); 105 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import Rate from './Rate'; 2 | 3 | export default Rate; 4 | -------------------------------------------------------------------------------- /src/useRefs.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export default function useRefs(): [ 4 | getRef: (index: number) => T, 5 | setRef: (index: number) => (instance: T) => void, 6 | ] { 7 | const nodeRef = React.useRef>({}); 8 | 9 | function getRef(index: number) { 10 | return nodeRef.current[index]; 11 | } 12 | 13 | function setRef(index: number) { 14 | return (node: T) => { 15 | nodeRef.current[index] = node; 16 | }; 17 | } 18 | 19 | return [getRef, setRef]; 20 | } 21 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | function getScroll(w: Window) { 2 | let ret = w.pageXOffset; 3 | const method = 'scrollLeft'; 4 | if (typeof ret !== 'number') { 5 | const d = w.document; 6 | // ie6,7,8 standard mode 7 | ret = d.documentElement[method]; 8 | if (typeof ret !== 'number') { 9 | // quirks mode 10 | ret = d.body[method]; 11 | } 12 | } 13 | return ret; 14 | } 15 | 16 | function getClientPosition(elem: HTMLElement) { 17 | let x: number; 18 | let y: number; 19 | const doc = elem.ownerDocument; 20 | const { body } = doc; 21 | const docElem = doc && doc.documentElement; 22 | const box = elem.getBoundingClientRect(); 23 | x = box.left; 24 | y = box.top; 25 | x -= docElem.clientLeft || body.clientLeft || 0; 26 | y -= docElem.clientTop || body.clientTop || 0; 27 | return { 28 | left: x, 29 | top: y, 30 | }; 31 | } 32 | 33 | export function getOffsetLeft(el: HTMLElement) { 34 | const pos = getClientPosition(el); 35 | const doc = el.ownerDocument; 36 | // Only IE use `parentWindow` 37 | const w: Window = doc.defaultView || (doc as any).parentWindow; 38 | pos.left += getScroll(w); 39 | return pos.left; 40 | } 41 | -------------------------------------------------------------------------------- /tests/__snapshots__/simple.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`rate allowHalf render works 1`] = ` 4 |
      8 |
    • 11 |
      18 |
      21 | ★ 22 |
      23 |
      26 | ★ 27 |
      28 |
      29 |
    • 30 |
    • 33 |
      40 |
      43 | ★ 44 |
      45 |
      48 | ★ 49 |
      50 |
      51 |
    • 52 |
    • 55 |
      62 |
      65 | ★ 66 |
      67 |
      70 | ★ 71 |
      72 |
      73 |
    • 74 |
    75 | `; 76 | 77 | exports[`rate allowHalf render works in RTL 1`] = ` 78 |
      82 |
    • 85 |
      92 |
      95 | ★ 96 |
      97 |
      100 | ★ 101 |
      102 |
      103 |
    • 104 |
    • 107 |
      114 |
      117 | ★ 118 |
      119 |
      122 | ★ 123 |
      124 |
      125 |
    • 126 |
    • 129 |
      136 |
      139 | ★ 140 |
      141 |
      144 | ★ 145 |
      146 |
      147 |
    • 148 |
    149 | `; 150 | 151 | exports[`rate allowHalf render works more than half 1`] = ` 152 |
      156 |
    • 159 |
      166 |
      169 | ★ 170 |
      171 |
      174 | ★ 175 |
      176 |
      177 |
    • 178 |
    • 181 |
      188 |
      191 | ★ 192 |
      193 |
      196 | ★ 197 |
      198 |
      199 |
    • 200 |
    • 203 |
      210 |
      213 | ★ 214 |
      215 |
      218 | ★ 219 |
      220 |
      221 |
    • 222 |
    223 | `; 224 | 225 | exports[`rate full render works 1`] = ` 226 |
      230 |
    • 233 |
      240 |
      243 | ★ 244 |
      245 |
      248 | ★ 249 |
      250 |
      251 |
    • 252 |
    • 255 |
      262 |
      265 | ★ 266 |
      267 |
      270 | ★ 271 |
      272 |
      273 |
    • 274 |
    • 277 |
      284 |
      287 | ★ 288 |
      289 |
      292 | ★ 293 |
      294 |
      295 |
    • 296 |
    297 | `; 298 | 299 | exports[`rate full render works in RTL 1`] = ` 300 |
      304 |
    • 307 |
      314 |
      317 | ★ 318 |
      319 |
      322 | ★ 323 |
      324 |
      325 |
    • 326 |
    • 329 |
      336 |
      339 | ★ 340 |
      341 |
      344 | ★ 345 |
      346 |
      347 |
    • 348 |
    • 351 |
      358 |
      361 | ★ 362 |
      363 |
      366 | ★ 367 |
      368 |
      369 |
    • 370 |
    371 | `; 372 | 373 | exports[`rate full render works with character function 1`] = ` 374 |
      378 |
    • 381 |
      388 |
      391 | 1 392 |
      393 |
      396 | 1 397 |
      398 |
      399 |
    • 400 |
    • 403 |
      410 |
      413 | 2 414 |
      415 |
      418 | 2 419 |
      420 |
      421 |
    • 422 |
    • 425 |
      432 |
      435 | 3 436 |
      437 |
      440 | 3 441 |
      442 |
      443 |
    • 444 |
    445 | `; 446 | 447 | exports[`rate full render works with character node 1`] = ` 448 |
      452 |
    • 455 |
      462 |
      465 | 1 466 |
      467 |
      470 | 1 471 |
      472 |
      473 |
    • 474 |
    • 477 |
      484 |
      487 | 1 488 |
      489 |
      492 | 1 493 |
      494 |
      495 |
    • 496 |
    • 499 |
      506 |
      509 | 1 510 |
      511 |
      514 | 1 515 |
      516 |
      517 |
    • 518 |
    519 | `; 520 | -------------------------------------------------------------------------------- /tests/props.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount } from 'enzyme'; 3 | import Rate from '../src'; 4 | 5 | describe('props', () => { 6 | it('characterRender', () => { 7 | const wrapper = mount( 8 | {index}} />, 9 | ); 10 | 11 | wrapper.find('li').forEach((li, index) => { 12 | expect(li.find('span.render-holder').length).toEqual(1); 13 | expect(li.text()).toEqual(index); 14 | }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/setup.js: -------------------------------------------------------------------------------- 1 | global.requestAnimationFrame = cb => setTimeout(cb, 0); 2 | 3 | const Enzyme = require('enzyme'); 4 | const Adapter = require('enzyme-adapter-react-16'); 5 | 6 | Enzyme.configure({ adapter: new Adapter() }); 7 | -------------------------------------------------------------------------------- /tests/simple.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, mount } from 'enzyme'; 3 | import KeyCode from 'rc-util/lib/KeyCode'; 4 | import Rate from '../src'; 5 | 6 | describe('rate', () => { 7 | describe('full', () => { 8 | it('render works', () => { 9 | const wrapper = render(); 10 | expect(wrapper).toMatchSnapshot(); 11 | }); 12 | 13 | it('render works in RTL', () => { 14 | const wrapper = render(); 15 | expect(wrapper).toMatchSnapshot(); 16 | }); 17 | 18 | it('render works with character node', () => { 19 | const wrapper = render(); 20 | expect(wrapper).toMatchSnapshot(); 21 | }); 22 | 23 | it('render works with character function', () => { 24 | const wrapper = render( 25 | { 29 | return index + 1; 30 | }} 31 | />, 32 | ); 33 | expect(wrapper).toMatchSnapshot(); 34 | }); 35 | 36 | it('click works', () => { 37 | const handleChange = jest.fn(); 38 | const wrapper = mount(); 39 | wrapper.find('li > div').at(1).simulate('click'); 40 | expect(handleChange).toBeCalledWith(2); 41 | }); 42 | 43 | it('click works in RTL', () => { 44 | const handleChange = jest.fn(); 45 | const wrapper = mount(); 46 | wrapper.find('li > div').at(1).simulate('click'); 47 | expect(handleChange).toBeCalledWith(2); 48 | }); 49 | 50 | it('support mouseMove', () => { 51 | const wrapper = mount(); 52 | wrapper.find('li > div').at(1).simulate('mouseMove'); 53 | expect(wrapper.find('li').at(1).hasClass('rc-rate-star-full')).toBe(true); 54 | }); 55 | 56 | it('support mouseMove in RTL', () => { 57 | const wrapper = mount(); 58 | wrapper.find('li > div').at(1).simulate('mouseMove'); 59 | expect(wrapper.find('li').at(1).hasClass('rc-rate-star-full')).toBe(true); 60 | }); 61 | 62 | it('support focus and blur', () => { 63 | const wrapper = mount(); 64 | wrapper.simulate('focus'); 65 | expect(wrapper.find('li').at(1).hasClass('rc-rate-star-focused')).toBe(true); 66 | 67 | wrapper.simulate('blur'); 68 | expect(wrapper.find('li').at(1).hasClass('rc-rate-star-focused')).toBe(false); 69 | }); 70 | 71 | it('support focus and blur in RTL', () => { 72 | const wrapper = mount(); 73 | wrapper.simulate('focus'); 74 | expect(wrapper.find('li').at(1).hasClass('rc-rate-star-focused')).toBe(true); 75 | 76 | wrapper.simulate('blur'); 77 | expect(wrapper.find('li').at(1).hasClass('rc-rate-star-focused')).toBe(false); 78 | }); 79 | 80 | describe('support keyboard', () => { 81 | it('left & right', () => { 82 | const handleChange = jest.fn(); 83 | const wrapper = mount(); 84 | wrapper.simulate('keyDown', { keyCode: KeyCode.LEFT }); 85 | expect(handleChange).toBeCalledWith(0); 86 | handleChange.mockReset(); 87 | wrapper.simulate('keyDown', { keyCode: KeyCode.RIGHT }); 88 | expect(handleChange).toBeCalledWith(2); 89 | }); 90 | 91 | it('enter', () => { 92 | const handleChange = jest.fn(); 93 | const wrapper = mount(); 94 | wrapper.find('li > div').at(2).simulate('keyDown', { keyCode: KeyCode.ENTER }); 95 | expect(handleChange).toBeCalledWith(3); 96 | }); 97 | }); 98 | 99 | describe('support keyboard in RTL', () => { 100 | it('left & right', () => { 101 | const handleChange = jest.fn(); 102 | const wrapper = mount(); 103 | wrapper.simulate('keyDown', { keyCode: KeyCode.LEFT }); 104 | expect(handleChange).toBeCalledWith(2); 105 | handleChange.mockReset(); 106 | wrapper.simulate('keyDown', { keyCode: KeyCode.RIGHT }); 107 | expect(handleChange).toBeCalledWith(0); 108 | }); 109 | 110 | it('enter', () => { 111 | const handleChange = jest.fn(); 112 | const wrapper = mount(); 113 | wrapper.find('li > div').at(2).simulate('keyDown', { keyCode: KeyCode.ENTER }); 114 | expect(handleChange).toBeCalledWith(3); 115 | }); 116 | }); 117 | }); 118 | 119 | describe('allowHalf', () => { 120 | it('render works', () => { 121 | const wrapper = render(); 122 | expect(wrapper).toMatchSnapshot(); 123 | }); 124 | 125 | it('render works more than half ', () => { 126 | const wrapper = render(); 127 | expect(wrapper).toMatchSnapshot(); 128 | }); 129 | 130 | it('render works in RTL', () => { 131 | const wrapper = render( 132 | , 133 | ); 134 | expect(wrapper).toMatchSnapshot(); 135 | }); 136 | 137 | it('click works', () => { 138 | const wrapper = mount(); 139 | wrapper.find('li > div').at(2).simulate('click'); 140 | expect(wrapper.find('li').at(4).hasClass('rc-rate-star-full')).toBe(false); 141 | }); 142 | 143 | it('support focus and blur', () => { 144 | const wrapper = mount(); 145 | wrapper.simulate('focus'); 146 | expect(wrapper.find('li').at(1).hasClass('rc-rate-star-focused')).toBe(true); 147 | 148 | wrapper.simulate('blur'); 149 | expect(wrapper.find('li').at(1).hasClass('rc-rate-star-focused')).toBe(false); 150 | }); 151 | 152 | it('support focus and blur in RTL', () => { 153 | const wrapper = mount(); 154 | wrapper.simulate('focus'); 155 | expect(wrapper.find('li').at(1).hasClass('rc-rate-star-focused')).toBe(true); 156 | 157 | wrapper.simulate('blur'); 158 | expect(wrapper.find('li').at(1).hasClass('rc-rate-star-focused')).toBe(false); 159 | }); 160 | 161 | it('support keyboard', () => { 162 | const handleChange = jest.fn(); 163 | const wrapper = mount(); 164 | wrapper.simulate('keyDown', { keyCode: KeyCode.LEFT }); 165 | expect(handleChange).toBeCalledWith(1); 166 | handleChange.mockReset(); 167 | wrapper.simulate('keyDown', { keyCode: KeyCode.RIGHT }); 168 | expect(handleChange).toBeCalledWith(2); 169 | }); 170 | 171 | it('support keyboard in RTL', () => { 172 | const handleChange = jest.fn(); 173 | const wrapper = mount( 174 | , 175 | ); 176 | wrapper.simulate('keyDown', { keyCode: KeyCode.LEFT }); 177 | expect(handleChange).toBeCalledWith(2); 178 | handleChange.mockReset(); 179 | wrapper.simulate('keyDown', { keyCode: KeyCode.RIGHT }); 180 | expect(handleChange).toBeCalledWith(1); 181 | }); 182 | 183 | it('hover Rate of allowHalf', () => { 184 | const onHoverChange = jest.fn(); 185 | const wrapper = mount(); 186 | wrapper.find('li > div').at(1).simulate('mouseMove', { 187 | pageX: -1, 188 | }); 189 | expect(onHoverChange).toHaveBeenCalledWith(1.5); 190 | }); 191 | 192 | it('hover Rate of allowHalf and rtl', () => { 193 | const onHoverChange = jest.fn(); 194 | const wrapper = mount( 195 | , 196 | ); 197 | wrapper.find('li > div').at(1).simulate('mouseMove', { 198 | pageX: 1, 199 | }); 200 | expect(onHoverChange).toHaveBeenCalledWith(1.5); 201 | }); 202 | }); 203 | 204 | describe('allowClear', () => { 205 | it('allowClear is false', () => { 206 | const handleChange = jest.fn(); 207 | const wrapper = mount( 208 | , 209 | ); 210 | wrapper.find('li > div').at(3).simulate('click'); 211 | wrapper.find('li > div').at(3).simulate('click'); 212 | expect(handleChange).toBeCalledWith(4); 213 | }); 214 | it('allowClear is true', () => { 215 | const handleChange = jest.fn(); 216 | const wrapper = mount(); 217 | wrapper.find('li > div').at(3).simulate('click'); 218 | expect(handleChange).toBeCalledWith(0); 219 | }); 220 | it('cleaned star disable hover', () => { 221 | const wrapper = mount(); 222 | wrapper.find('li > div').at(3).simulate('click'); 223 | wrapper.find('li > div').at(3).simulate('mouseMove'); 224 | expect(wrapper.find('li').at(3).hasClass('rc-rate-star-full')).toBe(false); 225 | }); 226 | it('cleaned star reset', () => { 227 | const wrapper = mount(); 228 | wrapper.find('li > div').at(3).simulate('click'); 229 | wrapper.find('ul').simulate('mouseLeave'); 230 | wrapper.find('li > div').at(3).simulate('mouseMove'); 231 | expect(wrapper.find('li').at(3).hasClass('rc-rate-star-full')).toBe(true); 232 | }); 233 | }); 234 | 235 | describe('focus & blur', () => { 236 | let container; 237 | beforeEach(() => { 238 | container = document.createElement('div'); 239 | document.body.appendChild(container); 240 | }); 241 | 242 | afterEach(() => { 243 | document.body.removeChild(container); 244 | }); 245 | 246 | it('focus()', () => { 247 | const handleFocus = jest.fn(); 248 | const rateRef = React.createRef(); 249 | mount(, { 250 | attachTo: container, 251 | }); 252 | rateRef.current.focus(); 253 | expect(handleFocus).toBeCalled(); 254 | }); 255 | 256 | it('blur()', () => { 257 | const handleBlur = jest.fn(); 258 | const rateRef = React.createRef(); 259 | mount(, { 260 | attachTo: container, 261 | }); 262 | rateRef.current.focus(); 263 | rateRef.current.blur(); 264 | expect(handleBlur).toBeCalled(); 265 | }); 266 | 267 | it('autoFocus', () => { 268 | const handleFocus = jest.fn(); 269 | mount(, { attachTo: container }); 270 | expect(handleFocus).toBeCalled(); 271 | }); 272 | }); 273 | 274 | describe('right class', () => { 275 | it('rtl', () => { 276 | const wrapper = mount(); 277 | expect(wrapper.find('.rc-rate-rtl').length).toBe(1); 278 | }); 279 | it('disabled', () => { 280 | const wrapper = mount(); 281 | expect(wrapper.find('.rc-rate-disabled').length).toBe(1); 282 | }); 283 | }); 284 | 285 | describe('events', () => { 286 | it('onKeyDown', () => { 287 | const onKeyDown = jest.fn(); 288 | const wrapper = mount(); 289 | wrapper.simulate('keydown'); 290 | expect(onKeyDown).toHaveBeenCalled(); 291 | }); 292 | 293 | // https://github.com/ant-design/ant-design/issues/30940 294 | it('range picker should accept onMouseEnter and onMouseLeave event when Rate component is diabled', () => { 295 | const handleMouseEnter = jest.fn(); 296 | const handleMouseLeave = jest.fn(); 297 | const wrapper = mount( 298 | , 299 | ); 300 | wrapper.simulate('mouseenter'); 301 | expect(handleMouseEnter).toHaveBeenCalled(); 302 | wrapper.simulate('mouseleave'); 303 | expect(handleMouseLeave).toHaveBeenCalled(); 304 | }); 305 | 306 | it('range picker should accept onMouseEnter and onMouseLeave event when Rate component is not diabled', () => { 307 | const handleMouseEnter = jest.fn(); 308 | const handleMouseLeave = jest.fn(); 309 | const wrapper = mount( 310 | , 311 | ); 312 | wrapper.simulate('mouseenter'); 313 | expect(handleMouseEnter).toHaveBeenCalled(); 314 | wrapper.simulate('mouseleave'); 315 | expect(handleMouseLeave).toHaveBeenCalled(); 316 | }); 317 | 318 | it('should ignore key presses when keyboard is false', () => { 319 | const mockChange = jest.fn(); 320 | const mockKeyDown = jest.fn(); 321 | const wrapper = mount( 322 | 328 | ); 329 | wrapper.simulate('keyDown', { keyCode: KeyCode.LEFT }); 330 | expect(mockChange).not.toHaveBeenCalled(); 331 | expect(mockKeyDown).toHaveBeenCalled(); 332 | }); 333 | }); 334 | 335 | describe('html attributes', () => { 336 | it('data-* and aria-* and role', () => { 337 | const wrapper = mount(); 338 | expect(wrapper.getDOMNode().getAttribute('data-number')).toBe('1'); 339 | expect(wrapper.getDOMNode().getAttribute('aria-label')).toBe('label'); 340 | expect(wrapper.getDOMNode().getAttribute('role')).toBe('button'); 341 | }); 342 | it('id', () => { 343 | const wrapper = mount(); 344 | expect(wrapper.getDOMNode().getAttribute('id')).toBe('myrate'); 345 | }); 346 | }); 347 | }); 348 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "baseUrl": "./", 6 | "jsx": "react", 7 | "declaration": true, 8 | "skipLibCheck": true, 9 | "esModuleInterop": true, 10 | "paths": { 11 | "@/*": [ 12 | "src/*" 13 | ], 14 | "@@/*": [ 15 | ".dumi/tmp/*" 16 | ], 17 | "rc-rate": [ 18 | "src/index.tsx" 19 | ] 20 | } 21 | }, 22 | "include": [ 23 | ".dumirc.ts", 24 | "./src/**/*.ts", 25 | "./src/**/*.tsx", 26 | "./docs/**/*.tsx" 27 | ] 28 | } -------------------------------------------------------------------------------- /typeings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css'; 2 | declare module '*.less'; --------------------------------------------------------------------------------