├── .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 | [](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 | 
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 |
10 | );
11 |
12 | const HeartHalf = (props: SvgProps) => (
13 |
16 | );
17 |
18 | const HeartFull = (props: SvgProps) => (
19 |
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 |
19 | );
20 |
21 | const StarFull = ({ size, color }: Omit) => (
22 |
30 | );
31 |
32 | const RTL_TRANSFORM: ViewStyle = {
33 | transform: [{ rotateY: '180deg' }],
34 | };
35 |
36 | const StarHalf = ({ size, color }: Omit) => (
37 |
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 |
--------------------------------------------------------------------------------