├── .editorconfig ├── .gitattributes ├── .github ├── actions │ └── setup │ │ └── action.yml └── workflows │ └── ci.yml ├── .gitignore ├── .nvmrc ├── .watchmanconfig ├── .yarnrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── babel.config.js ├── example ├── App.js ├── app.json ├── assets │ ├── adaptive-icon.png │ ├── favicon.png │ ├── icon.png │ └── splash.png ├── babel.config.js ├── metro.config.js ├── package.json ├── src │ ├── App.tsx │ ├── BasicExample.tsx │ ├── ClearOnCurrentRatingTapExample.tsx │ ├── CustomIconExample.tsx │ ├── ExampleContainer.tsx │ └── StarRatingDisplayExample.tsx ├── tsconfig.json ├── webpack.config.js └── yarn.lock ├── lefthook.yml ├── media └── demo.gif ├── package.json ├── scripts └── bootstrap.js ├── src ├── StarIcon.tsx ├── StarRating.tsx ├── StarRatingDisplay.tsx ├── __tests__ │ └── utils.test.ts ├── index.ts └── utils.ts ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | indent_style = space 10 | indent_size = 2 11 | 12 | end_of_line = lf 13 | charset = utf-8 14 | trim_trailing_whitespace = true 15 | insert_final_newline = true 16 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | # specific for windows script files 3 | *.bat text eol=crlf -------------------------------------------------------------------------------- /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup 2 | description: Setup Node.js and install dependencies 3 | 4 | runs: 5 | using: composite 6 | steps: 7 | - name: Setup Node.js 8 | uses: actions/setup-node@v3 9 | with: 10 | node-version-file: .nvmrc 11 | 12 | - name: Cache dependencies 13 | id: yarn-cache 14 | uses: actions/cache@v3 15 | with: 16 | path: | 17 | **/node_modules 18 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 19 | restore-keys: | 20 | ${{ runner.os }}-yarn- 21 | 22 | - name: Install dependencies 23 | if: steps.yarn-cache.outputs.cache-hit != 'true' 24 | run: | 25 | yarn install --cwd example --frozen-lockfile 26 | yarn install --frozen-lockfile 27 | shell: bash 28 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | 10 | jobs: 11 | lint: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | 17 | - name: Setup 18 | uses: ./.github/actions/setup 19 | 20 | - name: Lint files 21 | run: yarn lint 22 | 23 | - name: Typecheck files 24 | run: yarn typecheck 25 | 26 | test: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v3 31 | 32 | - name: Setup 33 | uses: ./.github/actions/setup 34 | 35 | - name: Run unit tests 36 | run: yarn test --maxWorkers=2 --coverage 37 | 38 | build: 39 | runs-on: ubuntu-latest 40 | steps: 41 | - name: Checkout 42 | uses: actions/checkout@v3 43 | 44 | - name: Setup 45 | uses: ./.github/actions/setup 46 | 47 | - name: Build package 48 | run: yarn prepack 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # XDE 6 | .expo/ 7 | 8 | # VSCode 9 | .vscode/ 10 | jsconfig.json 11 | 12 | # Xcode 13 | # 14 | build/ 15 | *.pbxuser 16 | !default.pbxuser 17 | *.mode1v3 18 | !default.mode1v3 19 | *.mode2v3 20 | !default.mode2v3 21 | *.perspectivev3 22 | !default.perspectivev3 23 | xcuserdata 24 | *.xccheckout 25 | *.moved-aside 26 | DerivedData 27 | *.hmap 28 | *.ipa 29 | *.xcuserstate 30 | project.xcworkspace 31 | 32 | # Android/IJ 33 | # 34 | .classpath 35 | .cxx 36 | .gradle 37 | .idea 38 | .project 39 | .settings 40 | local.properties 41 | android.iml 42 | 43 | # Cocoapods 44 | # 45 | example/ios/Pods 46 | 47 | # Ruby 48 | example/vendor/ 49 | 50 | # node.js 51 | # 52 | node_modules/ 53 | npm-debug.log 54 | yarn-debug.log 55 | yarn-error.log 56 | 57 | # BUCK 58 | buck-out/ 59 | \.buckd/ 60 | android/app/libs 61 | android/keystores/debug.keystore 62 | 63 | # Expo 64 | .expo/ 65 | 66 | # Turborepo 67 | .turbo/ 68 | 69 | # generated by bob 70 | lib/ 71 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16.18.1 2 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | # Override Yarn command so we can automatically setup the repo on running `yarn` 2 | 3 | yarn-path "scripts/bootstrap.js" 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual 11 | identity and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the overall 27 | community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or advances of 32 | any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email address, 36 | without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | [INSERT CONTACT METHOD]. 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series of 87 | actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or permanent 94 | ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within the 114 | community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.1, available at 120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 127 | [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are always welcome, no matter how large or small! 4 | 5 | We want this community to be friendly and respectful to each other. Please follow it in all your interactions with the project. Before contributing, please read the [code of conduct](./CODE_OF_CONDUCT.md). 6 | 7 | ## Development workflow 8 | 9 | To get started with the project, run `yarn` in the root directory to install the required dependencies for each package: 10 | 11 | ```sh 12 | yarn 13 | ``` 14 | 15 | > While it's possible to use [`npm`](https://github.com/npm/cli), the tooling is built around [`yarn`](https://classic.yarnpkg.com/), so you'll have an easier time if you use `yarn` for development. 16 | 17 | While developing, you can run the [example app](/example/) to test your changes. Any changes you make in your library's JavaScript code will be reflected in the example app without a rebuild. If you change any native code, then you'll need to rebuild the example app. 18 | 19 | To start the packager: 20 | 21 | ```sh 22 | yarn example start 23 | ``` 24 | 25 | To run the example app on Android: 26 | 27 | ```sh 28 | yarn example android 29 | ``` 30 | 31 | To run the example app on iOS: 32 | 33 | ```sh 34 | yarn example ios 35 | ``` 36 | 37 | To run the example app on Web: 38 | 39 | ```sh 40 | yarn example web 41 | ``` 42 | 43 | Make sure your code passes TypeScript and ESLint. Run the following to verify: 44 | 45 | ```sh 46 | yarn typecheck 47 | yarn lint 48 | ``` 49 | 50 | To fix formatting errors, run the following: 51 | 52 | ```sh 53 | yarn lint --fix 54 | ``` 55 | 56 | Remember to add tests for your change if possible. Run the unit tests by: 57 | 58 | ```sh 59 | yarn test 60 | ``` 61 | 62 | 63 | ### Commit message convention 64 | 65 | We follow the [conventional commits specification](https://www.conventionalcommits.org/en) for our commit messages: 66 | 67 | - `fix`: bug fixes, e.g. fix crash due to deprecated method. 68 | - `feat`: new features, e.g. add new method to the module. 69 | - `refactor`: code refactor, e.g. migrate from class components to hooks. 70 | - `docs`: changes into documentation, e.g. add usage example for the module.. 71 | - `test`: adding or updating tests, e.g. add integration tests using detox. 72 | - `chore`: tooling changes, e.g. change CI config. 73 | 74 | Our pre-commit hooks verify that your commit message matches this format when committing. 75 | 76 | ### Linting and tests 77 | 78 | [ESLint](https://eslint.org/), [Prettier](https://prettier.io/), [TypeScript](https://www.typescriptlang.org/) 79 | 80 | We use [TypeScript](https://www.typescriptlang.org/) for type checking, [ESLint](https://eslint.org/) with [Prettier](https://prettier.io/) for linting and formatting the code, and [Jest](https://jestjs.io/) for testing. 81 | 82 | Our pre-commit hooks verify that the linter and tests pass when committing. 83 | 84 | ### Publishing to npm 85 | 86 | We use [release-it](https://github.com/release-it/release-it) to make it easier to publish new versions. It handles common tasks like bumping version based on semver, creating tags and releases etc. 87 | 88 | To publish new versions, run the following: 89 | 90 | ```sh 91 | yarn release 92 | ``` 93 | 94 | ### Scripts 95 | 96 | The `package.json` file contains various scripts for common tasks: 97 | 98 | - `yarn bootstrap`: setup project by installing all dependencies and pods. 99 | - `yarn typecheck`: type-check files with TypeScript. 100 | - `yarn lint`: lint files with ESLint. 101 | - `yarn test`: run unit tests with Jest. 102 | - `yarn example start`: start the Metro server for the example app. 103 | - `yarn example android`: run the example app on Android. 104 | - `yarn example ios`: run the example app on iOS. 105 | 106 | ### Sending a pull request 107 | 108 | > **Working on your first pull request?** You can learn how from this _free_ series: [How to Contribute to an Open Source Project on GitHub](https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github). 109 | 110 | When you're sending a pull request: 111 | 112 | - Prefer small pull requests focused on one change. 113 | - Verify that linters and tests are passing. 114 | - Review the documentation to make sure it looks good. 115 | - Follow the pull request template when opening a pull request. 116 | - For pull requests that change the API or implementation, discuss with maintainers first by opening an issue. 117 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Benedikt Viebahn 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-star-rating-widget 2 | 3 | [![npm version](https://badge.fury.io/js/react-native-star-rating-widget.svg)](https://badge.fury.io/js/react-native-star-rating-widget) 4 | 5 | A customizable, animated star rating component for React Native. Compatible with iOS, Android and Web. Written in Typescript. 6 | 7 | ![Demo](https://github.com/benediktviebahn/react-native-star-rating-widget/raw/master/media/demo.gif) 8 | 9 | ## Installation 10 | 1. install react-native-star-rating-widget 11 | `npm install react-native-star-rating-widget --save` or `yarn add react-native-star-rating-widget` 12 | 2. if not already installed, add [react-native-svg](https://github.com/react-native-community/react-native-svg) 13 | 14 | ## Usage 15 | This package exports an 16 | 17 | ### Interactive `StarRating` component 18 | ```js 19 | import StarRating from 'react-native-star-rating-widget'; 20 | 21 | const Example = () => { 22 | const [rating, setRating] = useState(0); 23 | return ( 24 | 28 | ); 29 | }; 30 | ``` 31 | 32 | ### Non-Interactive `StarRatingDisplay` component 33 | ```js 34 | import { StarRatingDisplay } from 'react-native-star-rating-widget'; 35 | 36 | const Example = () => { 37 | return ( 38 | 41 | ); 42 | }; 43 | ``` 44 | 45 | See [example/src](example/src) for more examples. 46 | 47 | ## Props 48 | ### `StarRating` Props 49 | | Name | Type | Default | Description | 50 | | ----------------- | ----------------------- | ---------------- | ----------------------------------------------------- | 51 | | rating | number | **REQUIRED** | Rating Value. Should be between 0 and `maxStars` | 52 | | onChange | (rating: number) => void | **REQUIRED** | called when rating changes | 53 | | maxStars | number | 5 | number of stars | 54 | | starSize | number | 32 | star size | 55 | | color | string | "#fdd835" | star color | 56 | | emptyColor | string | same as `color` | empty star color | 57 | | style | object | undefined | optional style | 58 | | starStyle | object | undefined | optional star style | 59 | | enableHalfStar | boolean | true | enable or disable display of half stars | 60 | | enableSwiping | boolean | true | enable or disable swiping | 61 | | onRatingStart | (rating: number) => void | undefined | called when the interaction starts, before `onChange` | 62 | | onRatingEnd | (rating: number) => void | undefined | called when the interaction starts, after `onChange` | 63 | | animationConfig | see [AnimationConfig](#animationConfig) | see [AnimationConfig](#animationConfig) | animation configuration object | 64 | | StarIconComponent | (props: { index: number; size: number; color: string; type: "full" \| "half" \| "empty"; }) => JSX.Element | [StarIcon](https://github.com/bviebahn/react-native-star-rating-widget/blob/master/src/StarIcon.tsx) | Icon component | 65 | | accessibilityLabel | string | star rating. %value% stars. use custom actions to set rating. | The label used on the star component | 66 | | accessabilityIncrementLabel | string | increment | The label for the increment action | 67 | | accessabilityDecrementLabel | string | decrement | The label for the decrement action. | 68 | | accessabilityActivateLabel | string | activate (default) | The label for the activate action. | 69 | | accessibilityAdjustmentLabel | string | %value% stars | The label that is announced after adjustment action | 70 | 71 | ### `StarRatingDisplay` Props 72 | The `StarRatingDisplay` component accepts mostly the same props as `StarRating` except those that are interaction related props such as `onChange`, `enableSwiping`, `onRatingStart` etc. 73 | 74 | ### AnimationConfig 75 | | Name | Type | Default | Description | 76 | | -------- | ------------------ | ----------------- | ------------------------------------------ | 77 | | scale | number | 1.2 | star animation scale value | 78 | | duration | number | 300 | animation duration | 79 | | delay | number | 300 | animation delay when interaction has ended | 80 | | easing | (number) => number | Easing.elastic(2) | animation easing function | 81 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /example/App.js: -------------------------------------------------------------------------------- 1 | export { default } from './src/App'; 2 | -------------------------------------------------------------------------------- /example/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "example", 4 | "slug": "example", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "userInterfaceStyle": "light", 9 | "splash": { 10 | "image": "./assets/splash.png", 11 | "resizeMode": "contain", 12 | "backgroundColor": "#ffffff" 13 | }, 14 | "updates": { 15 | "fallbackToCacheTimeout": 0 16 | }, 17 | "assetBundlePatterns": [ 18 | "**/*" 19 | ], 20 | "ios": { 21 | "supportsTablet": true 22 | }, 23 | "android": { 24 | "adaptiveIcon": { 25 | "foregroundImage": "./assets/adaptive-icon.png", 26 | "backgroundColor": "#FFFFFF" 27 | } 28 | }, 29 | "web": { 30 | "favicon": "./assets/favicon.png" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /example/assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bviebahn/react-native-star-rating-widget/028b43da27ea70a792b208a2d518f7c14d66338d/example/assets/adaptive-icon.png -------------------------------------------------------------------------------- /example/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bviebahn/react-native-star-rating-widget/028b43da27ea70a792b208a2d518f7c14d66338d/example/assets/favicon.png -------------------------------------------------------------------------------- /example/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bviebahn/react-native-star-rating-widget/028b43da27ea70a792b208a2d518f7c14d66338d/example/assets/icon.png -------------------------------------------------------------------------------- /example/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bviebahn/react-native-star-rating-widget/028b43da27ea70a792b208a2d518f7c14d66338d/example/assets/splash.png -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const pak = require('../package.json'); 3 | 4 | module.exports = function (api) { 5 | api.cache(true); 6 | 7 | return { 8 | presets: ['babel-preset-expo'], 9 | plugins: [ 10 | [ 11 | 'module-resolver', 12 | { 13 | extensions: ['.tsx', '.ts', '.js', '.json'], 14 | alias: { 15 | // For development, we want to alias the library to the source 16 | [pak.name]: path.join(__dirname, '..', pak.source), 17 | }, 18 | }, 19 | ], 20 | ], 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /example/metro.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const escape = require('escape-string-regexp'); 3 | const { getDefaultConfig } = require('@expo/metro-config'); 4 | const exclusionList = require('metro-config/src/defaults/exclusionList'); 5 | const pak = require('../package.json'); 6 | 7 | const root = path.resolve(__dirname, '..'); 8 | 9 | const modules = Object.keys({ 10 | ...pak.peerDependencies, 11 | }); 12 | 13 | const defaultConfig = getDefaultConfig(__dirname); 14 | 15 | module.exports = { 16 | ...defaultConfig, 17 | 18 | projectRoot: __dirname, 19 | watchFolders: [root], 20 | 21 | // We need to make sure that only one version is loaded for peerDependencies 22 | // So we block them at the root, and alias them to the versions in example's node_modules 23 | resolver: { 24 | ...defaultConfig.resolver, 25 | 26 | blacklistRE: exclusionList( 27 | modules.map( 28 | (m) => 29 | new RegExp(`^${escape(path.join(root, 'node_modules', m))}\\/.*$`) 30 | ) 31 | ), 32 | 33 | extraNodeModules: modules.reduce((acc, name) => { 34 | acc[name] = path.join(__dirname, 'node_modules', name); 35 | return acc; 36 | }, {}), 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "main": "node_modules/expo/AppEntry.js", 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo start --android", 8 | "ios": "expo start --ios", 9 | "web": "expo start --web" 10 | }, 11 | "dependencies": { 12 | "expo": "~47.0.13", 13 | "expo-status-bar": "~1.4.2", 14 | "react": "18.1.0", 15 | "react-dom": "18.1.0", 16 | "react-native": "0.70.8", 17 | "react-native-svg": "13.4.0", 18 | "react-native-web": "~0.18.7" 19 | }, 20 | "devDependencies": { 21 | "@babel/core": "^7.19.3", 22 | "@expo/webpack-config": "^18.0.1", 23 | "babel-loader": "^9.1.2", 24 | "babel-plugin-module-resolver": "^5.0.0" 25 | }, 26 | "private": true 27 | } 28 | -------------------------------------------------------------------------------- /example/src/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { ScrollView, StyleSheet } from 'react-native'; 4 | import BasicExample from './BasicExample'; 5 | import CustomIconExample from './CustomIconExample'; 6 | import StarRatingDisplayExample from './StarRatingDisplayExample'; 7 | import ClearOnCurrentRatingTapExample from './ClearOnCurrentRatingTapExample'; 8 | 9 | export default function App() { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | } 19 | 20 | const styles = StyleSheet.create({ 21 | container: { 22 | backgroundColor: '#ddd', 23 | }, 24 | content: { 25 | padding: 32, 26 | paddingTop: 64, 27 | alignItems: 'center', 28 | }, 29 | }); 30 | -------------------------------------------------------------------------------- /example/src/BasicExample.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import StarRating from 'react-native-star-rating-widget'; 3 | import ExampleContainer from './ExampleContainer'; 4 | 5 | export default function BasicExample() { 6 | const [rating, setRating] = React.useState(3); 7 | return ( 8 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /example/src/ClearOnCurrentRatingTapExample.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import StarRating from 'react-native-star-rating-widget'; 3 | import ExampleContainer from './ExampleContainer'; 4 | 5 | export default function ClearOnCurrentRatingTapExample() { 6 | const [rating, setRating] = React.useState(3); 7 | 8 | const beforeStartRating = React.useRef(rating); 9 | const preventClear = React.useRef(false); 10 | 11 | const handleChange = (newRating: number) => { 12 | setRating(newRating); 13 | 14 | // You likely only want to clear the rating if the user taps on the current rating, 15 | // and prevent clearing the rating when the user swipes to the current rating. 16 | // If onChange is called it means the user did not tap on the current rating, 17 | // so we don't want to clear it when the interaction ends. 18 | preventClear.current = true; 19 | }; 20 | 21 | const handleRatingStart = () => { 22 | beforeStartRating.current = rating; 23 | preventClear.current = false; 24 | }; 25 | 26 | const handleRatingEnd = (endRating: number) => { 27 | if (!preventClear.current && endRating === beforeStartRating.current) { 28 | setRating(0); 29 | } 30 | }; 31 | 32 | return ( 33 | 34 | 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /example/src/CustomIconExample.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Svg, { SvgProps, Path } from 'react-native-svg'; 3 | import StarRating, { StarIconProps } from 'react-native-star-rating-widget'; 4 | import ExampleContainer from './ExampleContainer'; 5 | 6 | const HeartEmpty = (props: SvgProps) => ( 7 | 8 | 9 | 10 | ); 11 | 12 | const HeartHalf = (props: SvgProps) => ( 13 | 14 | 15 | 16 | ); 17 | 18 | const HeartFull = (props: SvgProps) => ( 19 | 20 | 21 | 22 | ); 23 | 24 | const HeartIcon = ({ color, size, type }: StarIconProps) => { 25 | if (type === 'empty') { 26 | return ; 27 | } 28 | 29 | if (type === 'half') { 30 | return ; 31 | } 32 | 33 | return ; 34 | }; 35 | 36 | const CustomIconExample = () => { 37 | const [rating, setRating] = React.useState(2.5); 38 | 39 | return ( 40 | 41 | 47 | 48 | ); 49 | }; 50 | 51 | export default CustomIconExample; 52 | -------------------------------------------------------------------------------- /example/src/ExampleContainer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, Text, View } from 'react-native'; 3 | 4 | export default function ExampleContainer({ 5 | title, 6 | children, 7 | }: { 8 | title: string; 9 | children: React.ReactNode; 10 | }) { 11 | return ( 12 | 13 | {title} 14 | {children} 15 | 16 | ); 17 | } 18 | 19 | const styles = StyleSheet.create({ 20 | container: { 21 | width: '100%', 22 | marginBottom: 32, 23 | padding: 16, 24 | backgroundColor: '#fff', 25 | alignItems: 'center', 26 | borderRadius: 16, 27 | shadowColor: '#000', 28 | shadowOffset: { 29 | width: 0, 30 | height: 1, 31 | }, 32 | shadowOpacity: 0.22, 33 | shadowRadius: 2.22, 34 | 35 | elevation: 3, 36 | }, 37 | title: { fontSize: 18, marginBottom: 8, color: '#222', textAlign: 'center' }, 38 | }); 39 | -------------------------------------------------------------------------------- /example/src/StarRatingDisplayExample.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StarRatingDisplay } from 'react-native-star-rating-widget'; 3 | import ExampleContainer from './ExampleContainer'; 4 | 5 | export default function StarRatingDisplayExample() { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "compilerOptions": { 4 | // Avoid expo-cli auto-generating a tsconfig 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const createExpoWebpackConfigAsync = require('@expo/webpack-config'); 3 | const { resolver } = require('./metro.config'); 4 | 5 | const root = path.resolve(__dirname, '..'); 6 | const node_modules = path.join(__dirname, 'node_modules'); 7 | 8 | module.exports = async function (env, argv) { 9 | const config = await createExpoWebpackConfigAsync(env, argv); 10 | 11 | config.module.rules.push({ 12 | test: /\.(js|jsx|ts|tsx)$/, 13 | include: path.resolve(root, 'src'), 14 | use: 'babel-loader', 15 | }); 16 | 17 | // We need to make sure that only one version is loaded for peerDependencies 18 | // So we alias them to the versions in example's node_modules 19 | Object.assign(config.resolve.alias, { 20 | ...resolver.extraNodeModules, 21 | 'react-native-web': path.join(node_modules, 'react-native-web'), 22 | }); 23 | 24 | return config; 25 | }; 26 | -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | pre-commit: 2 | parallel: true 3 | commands: 4 | lint: 5 | files: git diff --name-only @{push} 6 | glob: "*.{js,ts,jsx,tsx}" 7 | run: npx eslint {files} 8 | types: 9 | files: git diff --name-only @{push} 10 | glob: "*.{js,ts, jsx, tsx}" 11 | run: npx tsc --noEmit 12 | commit-msg: 13 | parallel: true 14 | commands: 15 | commitlint: 16 | run: npx commitlint --edit 17 | -------------------------------------------------------------------------------- /media/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bviebahn/react-native-star-rating-widget/028b43da27ea70a792b208a2d518f7c14d66338d/media/demo.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-star-rating-widget", 3 | "version": "1.9.2", 4 | "description": "A star rating widget for react native", 5 | "main": "lib/commonjs/index", 6 | "module": "lib/module/index", 7 | "types": "lib/typescript/index.d.ts", 8 | "react-native": "src/index", 9 | "source": "src/index", 10 | "files": [ 11 | "src", 12 | "lib", 13 | "android", 14 | "ios", 15 | "cpp", 16 | "*.podspec", 17 | "!lib/typescript/example", 18 | "!ios/build", 19 | "!android/build", 20 | "!android/gradle", 21 | "!android/gradlew", 22 | "!android/gradlew.bat", 23 | "!android/local.properties", 24 | "!**/__tests__", 25 | "!**/__fixtures__", 26 | "!**/__mocks__", 27 | "!**/.*" 28 | ], 29 | "scripts": { 30 | "test": "jest", 31 | "typecheck": "tsc --noEmit", 32 | "lint": "eslint \"**/*.{js,ts,tsx}\"", 33 | "prepack": "bob build", 34 | "release": "release-it", 35 | "example": "yarn --cwd example", 36 | "bootstrap": "yarn example && yarn install" 37 | }, 38 | "keywords": [ 39 | "react-native", 40 | "ios", 41 | "android" 42 | ], 43 | "repository": "https://github.com/bviebahn/react-native-star-rating-widget", 44 | "author": "Benedikt Viebahn (https://github.com/bviebahn)", 45 | "license": "MIT", 46 | "bugs": { 47 | "url": "https://github.com/bviebahn/react-native-star-rating-widget/issues" 48 | }, 49 | "homepage": "https://github.com/bviebahn/react-native-star-rating-widget#readme", 50 | "publishConfig": { 51 | "registry": "https://registry.npmjs.org/" 52 | }, 53 | "devDependencies": { 54 | "@commitlint/config-conventional": "^17.0.2", 55 | "@evilmartians/lefthook": "^1.2.2", 56 | "@react-native-community/eslint-config": "^3.0.2", 57 | "@release-it/conventional-changelog": "^5.0.0", 58 | "@types/jest": "^28.1.2", 59 | "@types/react": "~17.0.21", 60 | "@types/react-native": "0.70.0", 61 | "commitlint": "^17.0.2", 62 | "del-cli": "^5.0.0", 63 | "eslint": "^8.4.1", 64 | "eslint-config-prettier": "^8.5.0", 65 | "eslint-plugin-prettier": "^4.0.0", 66 | "jest": "^28.1.1", 67 | "pod-install": "^0.1.0", 68 | "prettier": "^2.0.5", 69 | "react": "18.1.0", 70 | "react-native": "0.70.5", 71 | "react-native-builder-bob": "^0.20.0", 72 | "react-native-svg": "^13.6.0", 73 | "release-it": "^15.0.0", 74 | "typescript": "^5.5.4" 75 | }, 76 | "resolutions": { 77 | "@types/react": "17.0.21" 78 | }, 79 | "peerDependencies": { 80 | "react": "*", 81 | "react-native": "*", 82 | "react-native-svg": "*" 83 | }, 84 | "engines": { 85 | "node": ">= 16.0.0" 86 | }, 87 | "jest": { 88 | "preset": "react-native", 89 | "modulePathIgnorePatterns": [ 90 | "/example/node_modules", 91 | "/lib/" 92 | ] 93 | }, 94 | "commitlint": { 95 | "extends": [ 96 | "@commitlint/config-conventional" 97 | ] 98 | }, 99 | "release-it": { 100 | "git": { 101 | "commitMessage": "chore: release ${version}", 102 | "tagName": "v${version}" 103 | }, 104 | "npm": { 105 | "publish": true 106 | }, 107 | "github": { 108 | "release": true 109 | }, 110 | "plugins": { 111 | "@release-it/conventional-changelog": { 112 | "preset": "angular" 113 | } 114 | } 115 | }, 116 | "eslintConfig": { 117 | "root": true, 118 | "extends": [ 119 | "@react-native-community", 120 | "prettier" 121 | ], 122 | "rules": { 123 | "prettier/prettier": [ 124 | "error", 125 | { 126 | "quoteProps": "consistent", 127 | "singleQuote": true, 128 | "tabWidth": 2, 129 | "trailingComma": "es5", 130 | "useTabs": false 131 | } 132 | ] 133 | } 134 | }, 135 | "eslintIgnore": [ 136 | "node_modules/", 137 | "lib/" 138 | ], 139 | "prettier": { 140 | "quoteProps": "consistent", 141 | "singleQuote": true, 142 | "tabWidth": 2, 143 | "trailingComma": "es5", 144 | "useTabs": false 145 | }, 146 | "react-native-builder-bob": { 147 | "source": "src", 148 | "output": "lib", 149 | "targets": [ 150 | "commonjs", 151 | "module", 152 | [ 153 | "typescript", 154 | { 155 | "project": "tsconfig.build.json" 156 | } 157 | ] 158 | ] 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /scripts/bootstrap.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const path = require('path'); 3 | const child_process = require('child_process'); 4 | 5 | const root = path.resolve(__dirname, '..'); 6 | const args = process.argv.slice(2); 7 | const options = { 8 | cwd: process.cwd(), 9 | env: process.env, 10 | stdio: 'inherit', 11 | encoding: 'utf-8', 12 | }; 13 | 14 | if (os.type() === 'Windows_NT') { 15 | options.shell = true; 16 | } 17 | 18 | let result; 19 | 20 | if (process.cwd() !== root || args.length) { 21 | // We're not in the root of the project, or additional arguments were passed 22 | // In this case, forward the command to `yarn` 23 | result = child_process.spawnSync('yarn', args, options); 24 | } else { 25 | // If `yarn` is run without arguments, perform bootstrap 26 | result = child_process.spawnSync('yarn', ['bootstrap'], options); 27 | } 28 | 29 | process.exitCode = result.status; 30 | -------------------------------------------------------------------------------- /src/StarIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { I18nManager, ViewStyle } from 'react-native'; 3 | import Svg, { Path, Rect } from 'react-native-svg'; 4 | 5 | export type StarIconProps = { 6 | index: number; 7 | size: number; 8 | color: string; 9 | type: 'full' | 'half' | 'empty'; 10 | }; 11 | 12 | const StarBorder = ({ size, color }: Omit) => ( 13 | 14 | 18 | 19 | ); 20 | 21 | const StarFull = ({ size, color }: Omit) => ( 22 | 23 | 24 | 25 | 29 | 30 | ); 31 | 32 | const RTL_TRANSFORM: ViewStyle = { 33 | transform: [{ rotateY: '180deg' }], 34 | }; 35 | 36 | const StarHalf = ({ size, color }: Omit) => ( 37 | 43 | 44 | 48 | 49 | ); 50 | 51 | const StarIcon = ({ index, type, size, color }: StarIconProps) => { 52 | const Component = 53 | type === 'full' ? StarFull : type === 'half' ? StarHalf : StarBorder; 54 | 55 | return ; 56 | }; 57 | 58 | export default StarIcon; 59 | -------------------------------------------------------------------------------- /src/StarRating.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | PanResponder, 4 | StyleSheet, 5 | View, 6 | StyleProp, 7 | ViewStyle, 8 | Animated, 9 | Easing, 10 | I18nManager, 11 | AccessibilityInfo, 12 | AccessibilityActionEvent, 13 | } from 'react-native'; 14 | import StarIcon, { StarIconProps } from './StarIcon'; 15 | import { getStars } from './utils'; 16 | 17 | type AnimationConfig = { 18 | easing?: (value: number) => number; 19 | duration?: number; 20 | delay?: number; 21 | scale?: number; 22 | }; 23 | 24 | type StarRatingProps = { 25 | /** 26 | * Rating Value. Should be between 0 and `maxStars`. 27 | */ 28 | rating: number; 29 | 30 | /** 31 | * Change listener that gets called when rating changes. 32 | */ 33 | onChange: (rating: number) => void; 34 | 35 | /** 36 | * Custom color for the filled stars. 37 | * 38 | * @default '#fdd835' 39 | */ 40 | color?: string; 41 | 42 | /** 43 | * Custom color for the empty stars. 44 | * 45 | * @default color 46 | */ 47 | emptyColor?: string; 48 | 49 | /** 50 | * Total amount of stars to display. 51 | * 52 | * @default 5 53 | */ 54 | maxStars?: number; 55 | 56 | /** 57 | * Size of the stars. 58 | * 59 | * @default 32 60 | */ 61 | starSize?: number; 62 | 63 | /** 64 | * Enable half star ratings. 65 | * 66 | * @default true 67 | */ 68 | enableHalfStar?: boolean; 69 | 70 | /** 71 | * Enable swiping to rate. 72 | * 73 | * @default true 74 | */ 75 | enableSwiping?: boolean; 76 | 77 | /** 78 | * Callback that gets called when the interaction starts, before `onChange`. 79 | * 80 | * @param rating The rating value at the start of the interaction. 81 | */ 82 | onRatingStart?: (rating: number) => void; 83 | 84 | /** 85 | * Callback that gets called when the interaction ends, after `onChange`. 86 | * 87 | * @param rating The rating value at the end of the interaction. 88 | */ 89 | onRatingEnd?: (rating: number) => void; 90 | 91 | /** 92 | * Custom style for the component. 93 | */ 94 | style?: StyleProp; 95 | 96 | /** 97 | * Custom style for the star component. 98 | */ 99 | starStyle?: StyleProp; 100 | 101 | /** 102 | * Custom animation configuration. 103 | * 104 | * @default 105 | * { 106 | * easing: Easing.elastic(2), 107 | * duration: 300, 108 | * scale: 1.2, 109 | * delay: 300 110 | * } 111 | */ 112 | animationConfig?: AnimationConfig; 113 | 114 | /** 115 | * Custom star icon component. 116 | * 117 | * @default StarIcon 118 | */ 119 | StarIconComponent?: (props: StarIconProps) => JSX.Element; 120 | 121 | testID?: string; 122 | 123 | /** 124 | * The accessibility label used on the star component. If you want to include the staged star value, then 125 | * include the token, %value%, in your label. 126 | * 127 | * @default 'star rating. %value% stars. use custom actions to set rating.' 128 | */ 129 | accessibilityLabel?: string; 130 | 131 | /** 132 | * The accessibility label for the increment action. 133 | * 134 | * @default 'increment' 135 | */ 136 | accessabilityIncrementLabel?: string; 137 | 138 | /** 139 | * The accessibility label for the decrement action. 140 | * 141 | * @default 'decrement' 142 | */ 143 | accessabilityDecrementLabel?: string; 144 | 145 | /** 146 | * The accessibility label for the activate action. 147 | * 148 | * @default 'activate (default)' 149 | */ 150 | accessabilityActivateLabel?: string; 151 | 152 | /** 153 | * When the user is adjusting the amount of stars, the voiceover reads as "n stars". This property will override 154 | * that label. Use the token, %value%, in your label to specify where the staged value should go. 155 | * 156 | * @default '%value% stars' 157 | */ 158 | accessibilityAdjustmentLabel?: string; 159 | }; 160 | 161 | const defaultColor = '#fdd835'; 162 | const defaultAnimationConfig: Required = { 163 | easing: Easing.elastic(2), 164 | duration: 300, 165 | scale: 1.2, 166 | delay: 300, 167 | }; 168 | 169 | const StarRating = ({ 170 | rating, 171 | maxStars = 5, 172 | starSize = 32, 173 | onChange, 174 | color = defaultColor, 175 | emptyColor = color, 176 | enableHalfStar = true, 177 | enableSwiping = true, 178 | onRatingStart, 179 | onRatingEnd, 180 | animationConfig = defaultAnimationConfig, 181 | style, 182 | starStyle, 183 | StarIconComponent = StarIcon, 184 | testID, 185 | accessibilityLabel = 'star rating. %value% stars. use custom actions to set rating.', 186 | accessabilityIncrementLabel = 'increment', 187 | accessabilityDecrementLabel = 'decrement', 188 | accessabilityActivateLabel = 'activate (default)', 189 | accessibilityAdjustmentLabel = '%value% stars', 190 | }: StarRatingProps) => { 191 | const width = React.useRef(); 192 | const [isInteracting, setInteracting] = React.useState(false); 193 | const [stagedRating, setStagedRating] = React.useState(rating); 194 | 195 | const panResponder = React.useMemo(() => { 196 | const calculateRating = (x: number, isRTL = I18nManager.isRTL) => { 197 | if (!width.current) return rating; 198 | 199 | if (isRTL) { 200 | return calculateRating(width.current - x, false); 201 | } 202 | const newRating = Math.max( 203 | 0, 204 | Math.min( 205 | Math.round((x / width.current) * maxStars * 2 + 0.2) / 2, 206 | maxStars 207 | ) 208 | ); 209 | 210 | return enableHalfStar ? newRating : Math.ceil(newRating); 211 | }; 212 | 213 | const handleChange = (newRating: number) => { 214 | if (newRating !== rating) { 215 | onChange(newRating); 216 | } 217 | }; 218 | 219 | return PanResponder.create({ 220 | onStartShouldSetPanResponder: () => true, 221 | onStartShouldSetPanResponderCapture: () => true, 222 | onMoveShouldSetPanResponder: () => true, 223 | onMoveShouldSetPanResponderCapture: () => true, 224 | onPanResponderMove: (e) => { 225 | if (enableSwiping) { 226 | const newRating = calculateRating(e.nativeEvent.locationX); 227 | handleChange(newRating); 228 | } 229 | }, 230 | onPanResponderStart: (e) => { 231 | const newRating = calculateRating(e.nativeEvent.locationX); 232 | onRatingStart?.(newRating); 233 | handleChange(newRating); 234 | setInteracting(true); 235 | }, 236 | onPanResponderEnd: (e) => { 237 | const newRating = calculateRating(e.nativeEvent.locationX); 238 | handleChange(newRating); 239 | onRatingEnd?.(newRating); 240 | 241 | setTimeout(() => { 242 | setInteracting(false); 243 | }, animationConfig.delay || defaultAnimationConfig.delay); 244 | }, 245 | onPanResponderTerminate: () => { 246 | // called when user drags outside of the component 247 | setTimeout(() => { 248 | setInteracting(false); 249 | }, animationConfig.delay || defaultAnimationConfig.delay); 250 | }, 251 | }); 252 | }, [ 253 | rating, 254 | maxStars, 255 | enableHalfStar, 256 | onChange, 257 | enableSwiping, 258 | onRatingStart, 259 | onRatingEnd, 260 | animationConfig.delay, 261 | ]); 262 | 263 | return ( 264 | 265 | { 269 | width.current = e.nativeEvent.layout.width; 270 | }} 271 | testID={testID} 272 | accessible={true} 273 | accessibilityRole="adjustable" 274 | accessibilityLabel={accessibilityLabel.replace( 275 | /%value%/g, 276 | stagedRating.toString() 277 | )} 278 | accessibilityValue={{ 279 | min: 0, 280 | max: enableHalfStar ? maxStars * 2 : maxStars, 281 | now: enableHalfStar ? rating * 2 : rating, // this has to be an integer 282 | }} 283 | accessibilityActions={[ 284 | { name: 'increment', label: accessabilityIncrementLabel }, 285 | { name: 'decrement', label: accessabilityDecrementLabel }, 286 | { name: 'activate', label: accessabilityActivateLabel }, 287 | ]} 288 | onAccessibilityAction={(event: AccessibilityActionEvent) => { 289 | const incrementor = enableHalfStar ? 0.5 : 1; 290 | switch (event.nativeEvent.actionName) { 291 | case 'increment': 292 | if (stagedRating >= maxStars) { 293 | AccessibilityInfo.announceForAccessibility( 294 | accessibilityAdjustmentLabel.replace( 295 | /%value%/g, 296 | `${maxStars}` 297 | ) 298 | ); 299 | } else { 300 | AccessibilityInfo.announceForAccessibility( 301 | accessibilityAdjustmentLabel.replace( 302 | /%value%/g, 303 | `${stagedRating + incrementor}` 304 | ) 305 | ); 306 | setStagedRating(stagedRating + incrementor); 307 | } 308 | 309 | break; 310 | case 'decrement': 311 | if (stagedRating <= 0) { 312 | AccessibilityInfo.announceForAccessibility( 313 | accessibilityAdjustmentLabel.replace(/%value%/g, `${0}`) 314 | ); 315 | } else { 316 | AccessibilityInfo.announceForAccessibility( 317 | accessibilityAdjustmentLabel.replace( 318 | /%value%/g, 319 | `${stagedRating - incrementor}` 320 | ) 321 | ); 322 | setStagedRating(stagedRating - incrementor); 323 | } 324 | 325 | break; 326 | case 'activate': 327 | onChange(stagedRating); 328 | break; 329 | } 330 | }} 331 | > 332 | {getStars(rating, maxStars).map((starType, i) => { 333 | return ( 334 | = 0.5} 337 | animationConfig={animationConfig} 338 | style={starStyle} 339 | > 340 | 346 | 347 | ); 348 | })} 349 | 350 | 351 | ); 352 | }; 353 | 354 | type AnimatedIconProps = { 355 | active: boolean; 356 | children: React.ReactElement; 357 | animationConfig: AnimationConfig; 358 | style?: StyleProp; 359 | }; 360 | 361 | const AnimatedIcon: React.FC = ({ 362 | active, 363 | animationConfig, 364 | children, 365 | style, 366 | }) => { 367 | const { 368 | scale = defaultAnimationConfig.scale, 369 | easing = defaultAnimationConfig.easing, 370 | duration = defaultAnimationConfig.duration, 371 | } = animationConfig; 372 | 373 | const animatedSize = React.useRef(new Animated.Value(active ? scale : 1)); 374 | 375 | React.useEffect(() => { 376 | const animation = Animated.timing(animatedSize.current, { 377 | toValue: active ? scale : 1, 378 | useNativeDriver: true, 379 | easing, 380 | duration, 381 | }); 382 | 383 | animation.start(); 384 | return animation.stop; 385 | }, [active, scale, easing, duration]); 386 | 387 | return ( 388 | 402 | {children} 403 | 404 | ); 405 | }; 406 | 407 | const styles = StyleSheet.create({ 408 | starRating: { 409 | flexDirection: 'row', 410 | alignSelf: 'flex-start', 411 | }, 412 | star: { 413 | marginHorizontal: 5, 414 | }, 415 | }); 416 | 417 | export default StarRating; 418 | -------------------------------------------------------------------------------- /src/StarRatingDisplay.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, View, StyleProp, ViewStyle } from 'react-native'; 3 | import StarIcon, { StarIconProps } from './StarIcon'; 4 | import { getStars } from './utils'; 5 | 6 | type Props = { 7 | /** 8 | * Rating Value. Should be between 0 and `maxStars`. 9 | */ 10 | rating: number; 11 | 12 | /** 13 | * Custom color for the filled stars. 14 | * 15 | * @default '#fdd835' 16 | */ 17 | color?: string; 18 | 19 | /** 20 | * Custom color for the empty stars. 21 | * 22 | * @default color 23 | */ 24 | emptyColor?: string; 25 | 26 | /** 27 | * Total amount of stars to display. 28 | * 29 | * @default 5 30 | */ 31 | maxStars?: number; 32 | 33 | /** 34 | * Size of the stars. 35 | * 36 | * @default 32 37 | */ 38 | starSize?: number; 39 | 40 | /** 41 | * Custom style for the component. 42 | */ 43 | style?: StyleProp; 44 | 45 | /** 46 | * Custom style for the star component. 47 | */ 48 | starStyle?: StyleProp; 49 | 50 | /** 51 | * Custom star icon component. 52 | * 53 | * @default StarIcon 54 | */ 55 | StarIconComponent?: (props: StarIconProps) => JSX.Element; 56 | 57 | /** 58 | * The accessibility label used on the star component. 59 | * 60 | * @default `star rating. ${rating} stars.` 61 | */ 62 | accessibilityLabel?: string; 63 | testID?: string; 64 | }; 65 | 66 | const defaultColor = '#fdd835'; 67 | 68 | const StarRatingDisplay = ({ 69 | rating, 70 | maxStars = 5, 71 | starSize = 32, 72 | color = defaultColor, 73 | emptyColor = color, 74 | style, 75 | starStyle, 76 | StarIconComponent = StarIcon, 77 | testID, 78 | accessibilityLabel = `star rating. ${rating} stars.`, 79 | }: Props) => { 80 | return ( 81 | 86 | {getStars(rating, maxStars).map((starType, i) => { 87 | return ( 88 | 89 | 95 | 96 | ); 97 | })} 98 | 99 | ); 100 | }; 101 | 102 | const styles = StyleSheet.create({ 103 | starRating: { 104 | flexDirection: 'row', 105 | }, 106 | star: { 107 | marginHorizontal: 5, 108 | }, 109 | }); 110 | 111 | export default StarRatingDisplay; 112 | -------------------------------------------------------------------------------- /src/__tests__/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { getStars } from '../utils'; 2 | 3 | test('returns stars array', () => { 4 | expect(getStars(3.5, 5)).toEqual(['full', 'full', 'full', 'half', 'empty']); 5 | }); 6 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import StarRating from './StarRating'; 2 | export { default as StarRatingDisplay } from './StarRatingDisplay'; 3 | export { default as StarIcon } from './StarIcon'; 4 | export type { StarIconProps } from './StarIcon'; 5 | 6 | export default StarRating; 7 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export function getStars(rating: number, maxStars: number) { 2 | return [...Array(maxStars)].map((_, i) => { 3 | if (rating - i >= 1) { 4 | return 'full'; 5 | } 6 | 7 | return rating - i >= 0.5 ? 'half' : 'empty'; 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "extends": "./tsconfig", 4 | "exclude": ["example"] 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "react-native-star-rating-widget": ["./src/index"] 6 | }, 7 | "allowUnreachableCode": false, 8 | "allowUnusedLabels": false, 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "jsx": "react", 12 | "lib": ["esnext"], 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "noFallthroughCasesInSwitch": true, 16 | "noImplicitReturns": true, 17 | "noImplicitUseStrict": false, 18 | "noStrictGenericChecks": false, 19 | "noUncheckedIndexedAccess": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "resolveJsonModule": true, 23 | "skipLibCheck": true, 24 | "strict": true, 25 | "target": "esnext" 26 | } 27 | } 28 | --------------------------------------------------------------------------------