├── .editorconfig
├── .gitattributes
├── .github
├── FUNDING.yml
├── actions
│ └── setup
│ │ └── action.yml
├── images
│ ├── modes-screenshot.png
│ ├── react-native-ui-datepicker-example.gif
│ └── rnui-datepicker.png
└── workflows
│ └── ci.yml
├── .gitignore
├── .nvmrc
├── .prettierrc.json
├── .watchmanconfig
├── .yarnrc
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── babel.config.js
├── demo
├── .gitignore
├── README.md
├── app.json
├── app
│ ├── +html.tsx
│ ├── +not-found.tsx
│ ├── _layout.tsx
│ ├── examples.tsx
│ ├── index.tsx
│ └── playground.tsx
├── assets
│ ├── fonts
│ │ └── SpaceMono-Regular.ttf
│ └── images
│ │ ├── adaptive-icon.png
│ │ ├── calendar.png
│ │ ├── favicon.png
│ │ ├── github-logo.png
│ │ ├── icon.png
│ │ ├── partial-react-logo.png
│ │ ├── react-logo.png
│ │ ├── react-logo@2x.png
│ │ ├── react-logo@3x.png
│ │ └── splash-icon.png
├── babel.config.js
├── components
│ ├── date-input.tsx
│ ├── examples
│ │ ├── custom-datepicker-1.tsx
│ │ ├── custom-datepicker-2.tsx
│ │ ├── custom-datepicker-3.tsx
│ │ ├── index.ts
│ │ ├── multiple-datepicker.tsx
│ │ ├── range-datepicker.tsx
│ │ └── single-datepicker.tsx
│ ├── feature-card.tsx
│ ├── github-link.tsx
│ ├── locale-selector.tsx
│ ├── package-manager.tsx
│ ├── theme-selector.tsx
│ └── ui
│ │ ├── button.tsx
│ │ ├── calendar.tsx
│ │ ├── separator.tsx
│ │ ├── tabs.tsx
│ │ └── text.tsx
├── constants
│ └── Colors.ts
├── global.css
├── hooks
│ ├── use-active-link.ts
│ ├── useColorScheme.ts
│ ├── useColorScheme.web.ts
│ └── useThemeColor.ts
├── layouts
│ ├── common
│ │ ├── github-stats.tsx
│ │ └── theme-toggle.tsx
│ ├── header.tsx
│ └── nav
│ │ ├── config-navigation.ts
│ │ ├── nav-item.tsx
│ │ ├── nav-menu.tsx
│ │ └── types.ts
├── lib
│ ├── constants.ts
│ ├── generate-dates.ts
│ ├── useColorScheme.tsx
│ └── utils.ts
├── metro.config.js
├── nativewind-env.d.ts
├── package.json
├── scripts
│ └── reset-project.js
├── tailwind.config.js
├── tsconfig.json
└── yarn.lock
├── eslint.config.js
├── example
├── .gitignore
├── README.md
├── app.json
├── app
│ ├── +not-found.tsx
│ ├── _layout.tsx
│ ├── bottom-sheet.tsx
│ └── index.tsx
├── assets
│ ├── fonts
│ │ └── SpaceMono-Regular.ttf
│ └── images
│ │ ├── adaptive-icon.png
│ │ ├── favicon.png
│ │ ├── icon.png
│ │ ├── partial-react-logo.png
│ │ ├── react-logo.png
│ │ ├── react-logo@2x.png
│ │ ├── react-logo@3x.png
│ │ └── splash-icon.png
├── components
│ ├── Collapsible.tsx
│ ├── ExternalLink.tsx
│ ├── HapticTab.tsx
│ ├── HelloWave.tsx
│ ├── ParallaxScrollView.tsx
│ ├── ThemedText.tsx
│ ├── ThemedView.tsx
│ └── ui
│ │ ├── IconSymbol.ios.tsx
│ │ ├── IconSymbol.tsx
│ │ ├── TabBarBackground.ios.tsx
│ │ └── TabBarBackground.tsx
├── constants
│ └── Colors.ts
├── hooks
│ ├── useColorScheme.ts
│ ├── useColorScheme.web.ts
│ └── useThemeColor.ts
├── metro.config.js
├── package.json
├── scripts
│ └── reset-project.js
├── tsconfig.json
└── yarn.lock
├── jest.config.ts
├── jest.setup.ts
├── lefthook.yml
├── package.json
├── scripts
└── bootstrap.js
├── src
├── __tests__
│ ├── __snapshots__
│ │ └── common.test.tsx.snap
│ ├── api.test.tsx
│ └── common.test.tsx
├── assets
│ └── images
│ │ ├── arrow_left.png
│ │ └── arrow_right.png
├── calendar-context.ts
├── components
│ ├── calendar.tsx
│ ├── day.tsx
│ ├── days.tsx
│ ├── header
│ │ ├── index.tsx
│ │ ├── month-button.tsx
│ │ ├── next-button.tsx
│ │ ├── prev-button.tsx
│ │ ├── selectors.tsx
│ │ ├── time-button.tsx
│ │ ├── types.ts
│ │ └── year-button.tsx
│ ├── months.tsx
│ ├── time-picker.tsx
│ ├── time-picker
│ │ ├── animated-math.ts
│ │ ├── period-native.tsx
│ │ ├── period-picker.tsx
│ │ ├── period-web.tsx
│ │ ├── wheel-native.tsx
│ │ ├── wheel-picker
│ │ │ ├── index.ts
│ │ │ ├── wheel-picker-item.tsx
│ │ │ ├── wheel-picker.style.ts
│ │ │ └── wheel-picker.tsx
│ │ ├── wheel-web.tsx
│ │ └── wheel.tsx
│ ├── weekdays.tsx
│ └── years.tsx
├── datetime-picker.tsx
├── enums.ts
├── hooks
│ └── use-previous.ts
├── index.ts
├── locales.ts
├── numerals.ts
├── polyfill.ts
├── react-native-extended.d.ts
├── theme.ts
├── types.ts
├── ui.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/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [farhoudshapouran]
4 | buy_me_a_coffee: farhoudshapouran
5 |
--------------------------------------------------------------------------------
/.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: 18 # Force Node.js 18 instead of using .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/images/modes-screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/farhoudshapouran/react-native-ui-datepicker/9b317eba399144944ac5235e2d151036456c5786/.github/images/modes-screenshot.png
--------------------------------------------------------------------------------
/.github/images/react-native-ui-datepicker-example.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/farhoudshapouran/react-native-ui-datepicker/9b317eba399144944ac5235e2d151036456c5786/.github/images/react-native-ui-datepicker-example.gif
--------------------------------------------------------------------------------
/.github/images/rnui-datepicker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/farhoudshapouran/react-native-ui-datepicker/9b317eba399144944ac5235e2d151036456c5786/.github/images/rnui-datepicker.png
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | branches:
8 | - main
9 |
10 | jobs:
11 | # disabled because the latest eslint version "ignores" option does not work as expected
12 | # lint:
13 | # runs-on: ubuntu-latest
14 | # steps:
15 | # - name: Checkout repository
16 | # uses: actions/checkout@v3
17 |
18 | # - name: Setup project
19 | # uses: ./.github/actions/setup
20 |
21 | # - name: Lint files
22 | # run: yarn lint
23 |
24 | # - name: Typecheck files
25 | # run: yarn typecheck
26 |
27 | test:
28 | runs-on: ubuntu-latest
29 | steps:
30 | - name: Checkout repository
31 | uses: actions/checkout@v3
32 |
33 | - name: Setup project
34 | uses: ./.github/actions/setup
35 |
36 | - name: Run unit tests
37 | run: yarn test
38 |
39 | # build:
40 | # runs-on: ubuntu-latest
41 | # steps:
42 | # - name: Checkout repository
43 | # uses: actions/checkout@v3
44 |
45 | # - name: Setup project
46 | # uses: ./.github/actions/setup
47 |
48 | # - name: Build package
49 | # run: yarn prepack
50 |
--------------------------------------------------------------------------------
/.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 | demo/ios/Pods
46 | example/ios/Pods
47 |
48 | # Ruby
49 | demo/vendor/
50 | example/vendor/
51 |
52 | # node.js
53 | #
54 | node_modules/
55 | npm-debug.log
56 | yarn-debug.log
57 | yarn-error.log
58 |
59 | # BUCK
60 | buck-out/
61 | \.buckd/
62 | android/app/libs
63 | android/keystores/debug.keystore
64 |
65 | # Expo
66 | .expo/
67 |
68 | # Turborepo
69 | .turbo/
70 |
71 | # generated by bob
72 | /lib/
73 |
74 | .env
75 | web-build
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 16.18.1
2 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "quoteProps": "consistent",
3 | "trailingComma": "es5",
4 | "tabWidth": 2,
5 | "semi": true,
6 | "singleQuote": true,
7 | "useTabs": false,
8 | "plugins": ["prettier-plugin-tailwindcss"]
9 | }
10 |
--------------------------------------------------------------------------------
/.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) 2025 Farhoud Shapouran
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['module:metro-react-native-babel-preset'],
3 | };
4 |
--------------------------------------------------------------------------------
/demo/.gitignore:
--------------------------------------------------------------------------------
1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
2 |
3 | # dependencies
4 | node_modules/
5 |
6 | # Expo
7 | .expo/
8 | dist/
9 | web-build/
10 | expo-env.d.ts
11 |
12 | # Native
13 | *.orig.*
14 | *.jks
15 | *.p8
16 | *.p12
17 | *.key
18 | *.mobileprovision
19 |
20 | # Metro
21 | .metro-health-check*
22 |
23 | # debug
24 | npm-debug.*
25 | yarn-debug.*
26 | yarn-error.*
27 |
28 | # macOS
29 | .DS_Store
30 | *.pem
31 |
32 | # local env files
33 | .env*.local
34 |
35 | # typescript
36 | *.tsbuildinfo
37 |
38 | app-example
39 |
--------------------------------------------------------------------------------
/demo/README.md:
--------------------------------------------------------------------------------
1 | # Welcome to your Expo app 👋
2 |
3 | This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).
4 |
5 | ## Get started
6 |
7 | 1. Install dependencies
8 |
9 | ```bash
10 | npm install
11 | ```
12 |
13 | 2. Start the app
14 |
15 | ```bash
16 | npx expo start
17 | ```
18 |
19 | In the output, you'll find options to open the app in a
20 |
21 | - [development build](https://docs.expo.dev/develop/development-builds/introduction/)
22 | - [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
23 | - [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
24 | - [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo
25 |
26 | You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
27 |
28 | ## Get a fresh project
29 |
30 | When you're ready, run:
31 |
32 | ```bash
33 | npm run reset-project
34 | ```
35 |
36 | This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing.
37 |
38 | ## Learn more
39 |
40 | To learn more about developing your project with Expo, look at the following resources:
41 |
42 | - [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides).
43 | - [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web.
44 |
45 | ## Join the community
46 |
47 | Join our community of developers creating universal apps.
48 |
49 | - [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
50 | - [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.
51 |
--------------------------------------------------------------------------------
/demo/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "React Native UI DatePicker Demo",
4 | "slug": "react-native-ui-datepicker-demo",
5 | "description": "Customizable React Native DateTime Picker component for Android, iOS, and Web. It includes date, time, and datetime modes and supports different locales.",
6 | "version": "1.0.0",
7 | "githubUrl": "https://github.com/farhoudshapouran/react-native-ui-datepicker",
8 | "orientation": "portrait",
9 | "icon": "./assets/images/icon.png",
10 | "scheme": "myapp",
11 | "userInterfaceStyle": "automatic",
12 | "newArchEnabled": true,
13 | "ios": {
14 | "userInterfaceStyle": "automatic",
15 | "supportsTablet": true
16 | },
17 | "android": {
18 | "userInterfaceStyle": "automatic",
19 | "adaptiveIcon": {
20 | "foregroundImage": "./assets/images/adaptive-icon.png",
21 | "backgroundColor": "#ffffff"
22 | }
23 | },
24 | "web": {
25 | "bundler": "metro",
26 | "output": "static",
27 | "favicon": "./assets/images/favicon.png"
28 | },
29 | "plugins": [
30 | "expo-router",
31 | [
32 | "expo-splash-screen",
33 | {
34 | "image": "./assets/images/splash-icon.png",
35 | "imageWidth": 200,
36 | "resizeMode": "contain",
37 | "backgroundColor": "#ffffff"
38 | }
39 | ],
40 | "expo-font"
41 | ],
42 | "experiments": {
43 | "typedRoutes": true,
44 | "baseUrl": "/react-native-ui-datepicker"
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/demo/app/+html.tsx:
--------------------------------------------------------------------------------
1 | import { ScrollViewStyleReset } from 'expo-router/html';
2 | import type { PropsWithChildren } from 'react';
3 |
4 | // This file is web-only and used to configure the root HTML for every
5 | // web page during static rendering.
6 | // The contents of this function only run in Node.js environments and
7 | // do not have access to the DOM or browser APIs.
8 | export default function Root({ children }: PropsWithChildren) {
9 | return (
10 |
11 |
12 |
13 |
14 |
18 |
19 | {/*
20 | Disable body scrolling on web. This makes ScrollView components work closer to how they do on native.
21 | However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line.
22 | */}
23 |
24 |
25 | {/* Add any additional elements that you want globally available on web... */}
26 |
27 | {children}
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/demo/app/+not-found.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link, Stack } from 'expo-router';
3 | import { StyleSheet, View, Text } from 'react-native';
4 |
5 | export default function NotFoundScreen() {
6 | return (
7 | <>
8 |
9 |
10 | This screen doesn't exist.
11 |
12 | Go to home screen!
13 |
14 |
15 | >
16 | );
17 | }
18 |
19 | const styles = StyleSheet.create({
20 | container: {
21 | flex: 1,
22 | alignItems: 'center',
23 | justifyContent: 'center',
24 | padding: 20,
25 | },
26 | link: {
27 | marginTop: 15,
28 | paddingVertical: 15,
29 | },
30 | });
31 |
--------------------------------------------------------------------------------
/demo/app/_layout.tsx:
--------------------------------------------------------------------------------
1 | import '@/global.css';
2 |
3 | import React, { useEffect } from 'react';
4 | import { View, SafeAreaView, ScrollView, Platform } from 'react-native';
5 | import { ThemeProvider } from '@react-navigation/native';
6 | import { Slot } from 'expo-router';
7 | import * as SplashScreen from 'expo-splash-screen';
8 | import { StatusBar } from 'expo-status-bar';
9 | import { GestureHandlerRootView } from 'react-native-gesture-handler';
10 | import { useColorScheme } from '@/lib/useColorScheme';
11 | import { LIGHT_THEME, DARK_THEME } from '@/lib/constants';
12 | import { Header } from '@/layouts/header';
13 | import 'react-native-gesture-handler';
14 | import 'react-native-reanimated';
15 | import {
16 | useFonts,
17 | Inter_300Light,
18 | Inter_400Regular,
19 | Inter_500Medium,
20 | Inter_600SemiBold,
21 | Inter_700Bold,
22 | } from '@expo-google-fonts/inter';
23 | import {
24 | Archivo_300Light,
25 | Archivo_400Regular,
26 | Archivo_500Medium,
27 | Archivo_600SemiBold,
28 | Archivo_700Bold,
29 | } from '@expo-google-fonts/archivo';
30 | import {
31 | Poppins_300Light,
32 | Poppins_400Regular,
33 | Poppins_500Medium,
34 | Poppins_600SemiBold,
35 | Poppins_700Bold,
36 | } from '@expo-google-fonts/poppins';
37 |
38 | SplashScreen.preventAutoHideAsync();
39 |
40 | export default function RootLayout() {
41 | const { isDarkColorScheme } = useColorScheme();
42 |
43 | const [fontsLoaded] = useFonts({
44 | Inter_300Light,
45 | Inter_400Regular,
46 | Inter_500Medium,
47 | Inter_600SemiBold,
48 | Inter_700Bold,
49 | Archivo_300Light,
50 | Archivo_400Regular,
51 | Archivo_500Medium,
52 | Archivo_600SemiBold,
53 | Archivo_700Bold,
54 | Poppins_300Light,
55 | Poppins_400Regular,
56 | Poppins_500Medium,
57 | Poppins_600SemiBold,
58 | Poppins_700Bold,
59 | });
60 |
61 | useEffect(() => {
62 | if (fontsLoaded) {
63 | SplashScreen.hideAsync();
64 | }
65 | }, [fontsLoaded]);
66 |
67 | if (!fontsLoaded) {
68 | return null;
69 | }
70 |
71 | return (
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 | );
86 | }
87 |
--------------------------------------------------------------------------------
/demo/app/examples.tsx:
--------------------------------------------------------------------------------
1 | import { View } from 'react-native';
2 | import { Text } from '@/components/ui/text';
3 | import {
4 | CustomDatePicker1,
5 | CustomDatePicker2,
6 | CustomDatePicker3,
7 | } from '@/components/examples';
8 | import { Separator } from '@/components/ui/separator';
9 |
10 | export default function ExamplesPage() {
11 | return (
12 |
13 |
14 |
15 | Examples
16 |
17 | Inspired by the stunning designs found on Dribbble.
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/demo/app/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { View } from 'react-native';
3 | import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
4 | import { Text } from '@/components/ui/text';
5 | import {
6 | MultipleDatePicker,
7 | RangeDatePicker,
8 | SingleDatePicker,
9 | } from '@/components/examples';
10 | import { PackageManager } from '@/components/package-manager';
11 | import { FeatureCard } from '@/components/feature-card';
12 | import { GithubLink } from '@/components/github-link';
13 |
14 | export default function MainPage() {
15 | const [exampleTab, setExampleTab] = useState('single');
16 | const [packageTab, setPackageTab] = useState('npm');
17 |
18 | return (
19 |
20 |
21 |
22 |
23 | React Native UI DatePicker
24 |
25 |
26 | Customizable date picker for React Native
27 |
28 |
29 |
30 |
31 |
36 |
37 |
38 | Single
39 |
40 |
41 | Range
42 |
43 |
44 | Multiple
45 |
46 |
47 |
51 |
52 |
53 |
54 |
55 |
56 |
60 |
61 |
62 |
63 |
64 |
65 | Installation
66 |
71 |
72 |
73 | npm
74 |
75 |
76 | yarn
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 | Features
89 |
90 |
91 |
95 |
99 |
100 |
101 |
105 |
109 |
110 |
111 |
115 |
119 |
120 |
121 |
125 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 | );
137 | }
138 |
--------------------------------------------------------------------------------
/demo/app/playground.tsx:
--------------------------------------------------------------------------------
1 | import { View } from 'react-native';
2 | import { Text } from '@/components/ui/text';
3 |
4 | export default function PlaygroundPage() {
5 | return (
6 |
7 |
8 | This section will be completed soon...
9 |
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/demo/assets/fonts/SpaceMono-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/farhoudshapouran/react-native-ui-datepicker/9b317eba399144944ac5235e2d151036456c5786/demo/assets/fonts/SpaceMono-Regular.ttf
--------------------------------------------------------------------------------
/demo/assets/images/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/farhoudshapouran/react-native-ui-datepicker/9b317eba399144944ac5235e2d151036456c5786/demo/assets/images/adaptive-icon.png
--------------------------------------------------------------------------------
/demo/assets/images/calendar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/farhoudshapouran/react-native-ui-datepicker/9b317eba399144944ac5235e2d151036456c5786/demo/assets/images/calendar.png
--------------------------------------------------------------------------------
/demo/assets/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/farhoudshapouran/react-native-ui-datepicker/9b317eba399144944ac5235e2d151036456c5786/demo/assets/images/favicon.png
--------------------------------------------------------------------------------
/demo/assets/images/github-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/farhoudshapouran/react-native-ui-datepicker/9b317eba399144944ac5235e2d151036456c5786/demo/assets/images/github-logo.png
--------------------------------------------------------------------------------
/demo/assets/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/farhoudshapouran/react-native-ui-datepicker/9b317eba399144944ac5235e2d151036456c5786/demo/assets/images/icon.png
--------------------------------------------------------------------------------
/demo/assets/images/partial-react-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/farhoudshapouran/react-native-ui-datepicker/9b317eba399144944ac5235e2d151036456c5786/demo/assets/images/partial-react-logo.png
--------------------------------------------------------------------------------
/demo/assets/images/react-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/farhoudshapouran/react-native-ui-datepicker/9b317eba399144944ac5235e2d151036456c5786/demo/assets/images/react-logo.png
--------------------------------------------------------------------------------
/demo/assets/images/react-logo@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/farhoudshapouran/react-native-ui-datepicker/9b317eba399144944ac5235e2d151036456c5786/demo/assets/images/react-logo@2x.png
--------------------------------------------------------------------------------
/demo/assets/images/react-logo@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/farhoudshapouran/react-native-ui-datepicker/9b317eba399144944ac5235e2d151036456c5786/demo/assets/images/react-logo@3x.png
--------------------------------------------------------------------------------
/demo/assets/images/splash-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/farhoudshapouran/react-native-ui-datepicker/9b317eba399144944ac5235e2d151036456c5786/demo/assets/images/splash-icon.png
--------------------------------------------------------------------------------
/demo/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 | api.cache(true);
3 | return {
4 | presets: [
5 | ['babel-preset-expo', { jsxImportSource: 'nativewind' }],
6 | 'nativewind/babel',
7 | ],
8 | plugins: ['react-native-reanimated/plugin'],
9 | };
10 | };
11 |
--------------------------------------------------------------------------------
/demo/components/date-input.tsx:
--------------------------------------------------------------------------------
1 | import { View } from 'react-native';
2 | import Feather from '@expo/vector-icons/Feather';
3 | import { cssInterop } from 'nativewind';
4 | import { Text } from './ui/text';
5 | import { cn } from '@/lib/utils';
6 |
7 | cssInterop(Feather, {
8 | className: {
9 | target: 'style',
10 | },
11 | });
12 |
13 | type Props = {
14 | value: string | null;
15 | placeholder: string;
16 | };
17 |
18 | export function DateInput({ value, placeholder }: Props) {
19 | return (
20 |
21 |
22 |
29 | {value || placeholder}
30 |
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/demo/components/examples/custom-datepicker-1.tsx:
--------------------------------------------------------------------------------
1 | import { useMemo, useState } from 'react';
2 | import { View } from 'react-native';
3 | import { Text } from '../ui/text';
4 | import { Calendar } from '../ui/calendar';
5 | import {
6 | DateType,
7 | CalendarDay,
8 | CalendarComponents,
9 | } from 'react-native-ui-datepicker';
10 | import { cn } from '@/lib/utils';
11 | import dayjs from 'dayjs';
12 | import Feather from '@expo/vector-icons/Feather';
13 | import { cssInterop } from 'nativewind';
14 | import {
15 | currentMonthDates,
16 | nextMonthDates,
17 | previousMonthDates,
18 | } from '@/lib/generate-dates';
19 | import { Link } from 'expo-router';
20 |
21 | cssInterop(Feather, {
22 | className: {
23 | target: 'style',
24 | },
25 | });
26 |
27 | const components: CalendarComponents = {
28 | IconPrev: (
29 |
30 | ),
31 | IconNext: (
32 |
33 | ),
34 | Day: (day: CalendarDay) => ,
35 | };
36 |
37 | export default function CustomDatePicker1() {
38 | const [dates, setDates] = useState();
39 |
40 | return (
41 |
42 | setDates(dates)}
46 | containerHeight={320}
47 | className="bg-card border-muted mb-4 w-[450px] rounded-3xl px-4 pb-4 shadow-xl dark:border-slate-900 dark:bg-slate-950"
48 | firstDayOfWeek={1}
49 | weekdaysFormat="short"
50 | weekdaysHeight={36}
51 | multiRangeMode
52 | classNames={{
53 | day_cell: 'p-[1px]',
54 | header: 'px-2 mb-2',
55 | weekdays: 'pb-2',
56 | month_selector_label: 'font-archivoSemiBold text-foreground text-lg',
57 | year_selector_label: 'font-archivoSemiBold text-foreground text-lg',
58 | weekday_label: 'font-archivoLight text-foreground',
59 | range_fill: 'my-[1px] bg-slate-200 dark:bg-slate-700/60',
60 | range_fill_weekstart: 'rounded-s-2xl',
61 | range_fill_weekend: 'rounded-e-2xl',
62 | outside_label: 'opacity-70',
63 | month:
64 | 'rounded-2xl web:hover:bg-muted web:dark:hover:bg-slate-700/60',
65 | month_label: 'text-foreground',
66 | selected_month: 'bg-slate-500 web:hover:bg-slate-500',
67 | selected_month_label: 'text-slate-100',
68 | year: 'rounded-2xl web:hover:bg-muted web:dark:hover:bg-slate-700/60',
69 | year_label: 'text-foreground',
70 | active_year: 'bg-slate-700/60',
71 | selected_year: 'bg-slate-500 web:hover:bg-slate-500',
72 | selected_year_label: 'text-slate-100',
73 | }}
74 | components={components}
75 | />
76 |
77 |
81 |
82 |
83 |
84 | Designed by Roman Kamushken
85 |
86 |
87 |
88 |
89 |
90 | );
91 | }
92 |
93 | const markedDates = [
94 | ...previousMonthDates(6),
95 | ...currentMonthDates(6),
96 | ...nextMonthDates(6),
97 | ];
98 |
99 | type DayProps = {
100 | day: CalendarDay;
101 | };
102 |
103 | const Day = ({ day }: DayProps) => {
104 | const { isSelected, isToday, isCurrentMonth } = day;
105 | const formatedDate = dayjs(day.date).format('YYYY-MM-DD');
106 | const isMarked = useMemo(
107 | () => markedDates.includes(formatedDate),
108 | [formatedDate]
109 | );
110 | const isGreen = day.number % 2 === 0;
111 |
112 | return (
113 |
119 |
126 | {day.text}
127 |
128 | {isToday ? (
129 |
130 | ) : isMarked ? (
131 |
137 | {isGreen ? '1141' : '1610'}
138 |
139 | ) : null}
140 |
141 | );
142 | };
143 |
--------------------------------------------------------------------------------
/demo/components/examples/custom-datepicker-2.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { View } from 'react-native';
3 | import { Text } from '../ui/text';
4 | import { Calendar } from '../ui/calendar';
5 | import {
6 | DateType,
7 | CalendarWeek,
8 | CalendarComponents,
9 | } from 'react-native-ui-datepicker';
10 | import dayjs from 'dayjs';
11 | import Feather from '@expo/vector-icons/Feather';
12 | import { cssInterop } from 'nativewind';
13 | import { Link } from 'expo-router';
14 | import { Button } from '../ui/button';
15 |
16 | cssInterop(Feather, {
17 | className: {
18 | target: 'style',
19 | },
20 | });
21 |
22 | const components: CalendarComponents = {
23 | IconPrev: (
24 |
25 | ),
26 | IconNext: (
27 |
28 | ),
29 | Weekday: (weekday: CalendarWeek) => ,
30 | };
31 |
32 | export default function CustomDatePicker2() {
33 | const [date, setDate] = useState();
34 |
35 | return (
36 |
37 |
38 | setDate(date)}
42 | containerHeight={305}
43 | className="w-full border-none bg-transparent px-0 shadow-none"
44 | firstDayOfWeek={1}
45 | weekdaysHeight={36}
46 | showOutsideDays={false}
47 | navigationPosition="right"
48 | classNames={{
49 | day_cell: 'p-1.5',
50 | day: 'rounded-full',
51 | day_label: 'font-poppinsMedium text-foreground',
52 | today: 'bg-blue-500/10',
53 | today_label: 'text-blue-500',
54 | selected: 'bg-blue-500 shadow-lg shadow-blue-500',
55 | selected_label: 'text-white',
56 | header: 'px-2.5 -me-3',
57 | weekdays: 'pb-2',
58 | month_selector_label: 'font-poppinsMedium text-foreground text-xl',
59 | year_selector_label: 'font-poppinsMedium text-foreground text-xl',
60 | range_fill: 'my-[1px] bg-slate-700/60',
61 | range_fill_weekstart: 'rounded-s-2xl',
62 | range_fill_weekend: 'rounded-e-2xl',
63 | outside_label: 'opacity-70',
64 | month: 'rounded-full web:hover:bg-slate-400/60',
65 | month_label: 'font-poppinsMedium text-foreground',
66 | selected_month: 'bg-blue-500 web:hover:bg-blue-500/80',
67 | selected_month_label: 'text-white',
68 | year: 'rounded-full web:hover:bg-slate-400/60',
69 | year_label: 'font-poppinsMedium text-foreground',
70 | selected_year: 'bg-blue-500 web:hover:bg-blue-500/80',
71 | selected_year_label: 'text-white',
72 | }}
73 | components={components}
74 | />
75 |
76 |
82 |
88 |
89 |
90 |
91 |
95 |
96 |
97 |
98 | Designed by Michał Masiak
99 |
100 |
101 |
102 |
103 |
104 | );
105 | }
106 |
107 | type WeekdayProps = {
108 | weekday: CalendarWeek;
109 | };
110 |
111 | const Weekday = ({ weekday }: WeekdayProps) => {
112 | return (
113 | {weekday.name.min[0]}
114 | );
115 | };
116 |
--------------------------------------------------------------------------------
/demo/components/examples/custom-datepicker-3.tsx:
--------------------------------------------------------------------------------
1 | import { useMemo, useState } from 'react';
2 | import { View } from 'react-native';
3 | import { Text } from '../ui/text';
4 | import { Calendar } from '../ui/calendar';
5 | import {
6 | DateType,
7 | CalendarDay,
8 | CalendarWeek,
9 | CalendarComponents,
10 | } from 'react-native-ui-datepicker';
11 | import { cn } from '@/lib/utils';
12 | import Feather from '@expo/vector-icons/Feather';
13 | import Entypo from '@expo/vector-icons/Entypo';
14 | import { cssInterop } from 'nativewind';
15 | import { Link } from 'expo-router';
16 |
17 | cssInterop(Feather, {
18 | className: {
19 | target: 'style',
20 | },
21 | });
22 |
23 | cssInterop(Entypo, {
24 | className: {
25 | target: 'style',
26 | },
27 | });
28 |
29 | const components: CalendarComponents = {
30 | IconPrev: (
31 |
32 | ),
33 | IconNext: (
34 |
35 | ),
36 | Weekday: (weekday: CalendarWeek) => ,
37 | Day: (day: CalendarDay) => ,
38 | };
39 |
40 | export default function CustomDatePicker3() {
41 | const [date, setDate] = useState();
42 |
43 | return (
44 |
45 |
46 |
47 |
48 | Calendar
49 |
50 |
55 |
56 |
57 | setDate(date)}
61 | containerHeight={400}
62 | weekdaysHeight={44}
63 | className="w-full border-none bg-transparent px-0 pb-2 pt-0 shadow-none"
64 | firstDayOfWeek={1}
65 | monthCaptionFormat="short"
66 | multiRangeMode
67 | classNames={{
68 | day_cell: 'p-1',
69 | header: 'px-[65px] mb-2',
70 | weekdays: 'py-2 mx-1 bg-blue-500/10 rounded dark:bg-slate-800',
71 | month_selector_label:
72 | 'font-archivoMedium text-muted-foreground text-lg',
73 | year_selector_label:
74 | 'font-archivoMedium text-muted-foreground text-lg',
75 | month: 'rounded web:hover:bg-muted web:dark:hover:bg-slate-700/60',
76 | month_label: 'font-archivo text-lg text-foreground',
77 | selected_month: 'bg-slate-500 web:hover:bg-slate-500',
78 | selected_month_label: 'text-slate-100',
79 | year: 'rounded web:hover:bg-muted web:dark:hover:bg-slate-700/60',
80 | year_label: 'font-archivo text-lg text-foreground',
81 | active_year: 'bg-slate-700/60',
82 | selected_year: 'bg-slate-500 web:hover:bg-slate-500',
83 | selected_year_label: 'text-slate-100',
84 | }}
85 | components={components}
86 | />
87 |
88 |
89 |
93 |
94 |
95 |
96 | Designed by Roman Kamushken
97 |
98 |
99 |
100 |
101 |
102 | );
103 | }
104 |
105 | type WeekdayProps = {
106 | weekday: CalendarWeek;
107 | };
108 |
109 | const Weekday = ({ weekday }: WeekdayProps) => {
110 | return (
111 | {weekday.name.min[0]}
112 | );
113 | };
114 |
115 | type DayProps = {
116 | day: CalendarDay;
117 | };
118 |
119 | const Day = ({ day }: DayProps) => {
120 | const { isSelected, isToday, isCurrentMonth } = day;
121 | const length =
122 | day.number % 3 === 0
123 | ? 1
124 | : day.number % 4 === 2
125 | ? 2
126 | : day.number % 5 === 0
127 | ? 3
128 | : 0;
129 |
130 | const dots = useMemo(() => , [length]);
131 |
132 | return (
133 |
140 |
147 | {day.text}
148 |
149 | {dots}
150 |
151 | );
152 | };
153 |
154 | const colors = ['bg-green-500', 'bg-red-500', 'bg-yellow-500'];
155 |
156 | const Dots = ({ length }: { length: number }) => {
157 | const shuffledColors = shuffleArray(colors);
158 |
159 | return (
160 |
161 | {Array.from({ length }, (_, index) => (
162 |
166 | ))}
167 |
168 | );
169 | };
170 |
171 | function shuffleArray(array: string[]) {
172 | for (let i = array.length - 1; i > 0; i--) {
173 | const j = Math.floor(Math.random() * (i + 1));
174 | [array[i], array[j]] = [array[j], array[i]];
175 | }
176 | return array;
177 | }
178 |
--------------------------------------------------------------------------------
/demo/components/examples/index.ts:
--------------------------------------------------------------------------------
1 | export { default as SingleDatePicker } from './single-datepicker';
2 | export { default as RangeDatePicker } from './range-datepicker';
3 | export { default as MultipleDatePicker } from './multiple-datepicker';
4 | export { default as CustomDatePicker1 } from './custom-datepicker-1';
5 | export { default as CustomDatePicker2 } from './custom-datepicker-2';
6 | export { default as CustomDatePicker3 } from './custom-datepicker-3';
7 |
--------------------------------------------------------------------------------
/demo/components/examples/multiple-datepicker.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { View } from 'react-native';
3 | import { Calendar, DateType } from '../ui/calendar';
4 | import { DateInput } from '../date-input';
5 | import dayjs from 'dayjs';
6 |
7 | export default function MultipleDatePicker() {
8 | const [dates, setDates] = useState();
9 |
10 | const selectedDates = dates
11 | ?.map((date) => dayjs(date).format('MMM DD, YYYY'))
12 | .join(' - ');
13 |
14 | return (
15 |
16 | setDates(dates)}
20 | />
21 |
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/demo/components/examples/range-datepicker.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { View } from 'react-native';
3 | import { Calendar, DateType } from '../ui/calendar';
4 | import dayjs from 'dayjs';
5 | import { DateInput } from '../date-input';
6 |
7 | export default function RangeDatePicker() {
8 | const [range, setRange] = useState<{
9 | startDate: DateType;
10 | endDate: DateType;
11 | }>({ startDate: undefined, endDate: undefined });
12 |
13 | const from = range.startDate
14 | ? dayjs(range.startDate).format('MMM DD, YYYY')
15 | : '';
16 | const to = range.endDate ? dayjs(range.endDate).format('MMM DD, YYYY') : '';
17 |
18 | return (
19 |
20 | setRange(params)}
25 | />
26 |
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/demo/components/examples/single-datepicker.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { View } from 'react-native';
3 | import { Calendar, DateType } from '../ui/calendar';
4 | import dayjs from 'dayjs';
5 | import { DateInput } from '../date-input';
6 |
7 | export default function SingleDatePicker() {
8 | const [date, setDate] = useState();
9 |
10 | return (
11 |
12 | setDate(date)}
16 | timePicker
17 | //use12Hours
18 | //minDate={new Date()}
19 | //maxDate={new Date(new Date().getFullYear(), 11, 31)} // end of the year
20 | />
21 |
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/demo/components/feature-card.tsx:
--------------------------------------------------------------------------------
1 | import { View } from 'react-native';
2 | import { Text } from './ui/text';
3 |
4 | type Props = {
5 | title: string;
6 | description: string;
7 | };
8 |
9 | export function FeatureCard({ title, description }: Props) {
10 | return (
11 |
12 | {title}
13 | {description}
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/demo/components/github-link.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View } from 'react-native';
3 | import AntDesign from '@expo/vector-icons/AntDesign';
4 | import { cssInterop } from 'nativewind';
5 | import { Text } from './ui/text';
6 | import { Link } from 'expo-router';
7 |
8 | cssInterop(AntDesign, {
9 | className: {
10 | target: 'style',
11 | },
12 | });
13 |
14 | export function GithubLink() {
15 | return (
16 |
17 |
21 |
22 |
23 | Check repository on GitHub
24 |
25 |
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/demo/components/locale-selector.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react';
2 | import { StyleSheet, View, Text, Pressable } from 'react-native';
3 |
4 | const Locales = ['en', 'de', 'es', 'fr', 'tr', 'fa'];
5 |
6 | type Props = {
7 | locale: string;
8 | setLocale: (locale: string) => void;
9 | mainColor?: string;
10 | activeTextColor?: string;
11 | };
12 |
13 | export const LocaleSelector = ({
14 | locale,
15 | setLocale,
16 | mainColor,
17 | activeTextColor,
18 | }: Props) => {
19 | return (
20 |
21 |
27 | Locale:
28 |
29 | {Locales.map((item, index) => (
30 | setLocale(item)}
39 | accessibilityRole="button"
40 | accessibilityLabel={item.toUpperCase()}
41 | >
42 |
52 | {item.toUpperCase()}
53 |
54 |
55 | ))}
56 |
57 | );
58 | };
59 |
60 | const styles = StyleSheet.create({
61 | localeContainer: {
62 | flexDirection: 'row',
63 | alignItems: 'center',
64 | marginBottom: 20,
65 | },
66 | localeButton: {
67 | alignItems: 'center',
68 | justifyContent: 'center',
69 | width: 36,
70 | height: 36,
71 | borderRadius: 36,
72 | margin: 2,
73 | },
74 | localeButtonText: {
75 | fontSize: 15,
76 | },
77 | });
78 |
--------------------------------------------------------------------------------
/demo/components/package-manager.tsx:
--------------------------------------------------------------------------------
1 | import { View } from 'react-native';
2 | import { Text } from './ui/text';
3 | import { Button } from './ui/button';
4 | import Feather from '@expo/vector-icons/Feather';
5 | import { cssInterop } from 'nativewind';
6 | import * as Clipboard from 'expo-clipboard';
7 |
8 | cssInterop(Feather, {
9 | className: {
10 | target: 'style',
11 | },
12 | });
13 |
14 | type Props = {
15 | command: string;
16 | };
17 |
18 | export function PackageManager({ command }: Props) {
19 | const copyToClipboard = async () => {
20 | await Clipboard.setStringAsync(command);
21 | };
22 |
23 | return (
24 |
25 | {command}
26 |
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/demo/components/theme-selector.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react';
2 | import { StyleSheet, View, Pressable } from 'react-native';
3 |
4 | export interface ITheme {
5 | mainColor: string;
6 | activeTextColor: string;
7 | }
8 |
9 | type Props = {
10 | themes: ITheme[];
11 | setTheme: (theme: ITheme) => void;
12 | };
13 |
14 | export const ThemeSelector = ({ themes = [], setTheme }: Props) => {
15 | return (
16 |
17 | {themes.map((item, index) => (
18 | setTheme(item)}
28 | accessibilityRole="button"
29 | accessibilityLabel="Set Active Theme"
30 | />
31 | ))}
32 |
33 | );
34 | };
35 |
36 | const styles = StyleSheet.create({
37 | themeContainer: {
38 | flexDirection: 'row',
39 | alignItems: 'center',
40 | justifyContent: 'space-around',
41 | marginBottom: 10,
42 | width: 330,
43 | },
44 | themeButton: {
45 | borderWidth: 4,
46 | width: 32,
47 | height: 32,
48 | borderRadius: 32,
49 | margin: 5,
50 | shadowRadius: 20,
51 | shadowColor: '#000',
52 | shadowOpacity: 0.1,
53 | shadowOffset: { width: 0, height: 0 },
54 | },
55 | });
56 |
--------------------------------------------------------------------------------
/demo/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import { cva, type VariantProps } from 'class-variance-authority';
2 | import * as React from 'react';
3 | import { Pressable } from 'react-native';
4 | import { cn } from '@/lib/utils';
5 | import { TextClassContext } from './text';
6 |
7 | const buttonVariants = cva(
8 | 'group flex items-center justify-center rounded-md web:ring-offset-background web:transition-colors web:focus-visible:outline-none web:focus-visible:ring-2 web:focus-visible:ring-ring web:focus-visible:ring-offset-2',
9 | {
10 | variants: {
11 | variant: {
12 | default: 'bg-primary web:hover:opacity-90 active:opacity-90',
13 | destructive: 'bg-destructive web:hover:opacity-90 active:opacity-90',
14 | outline:
15 | 'border border-input bg-background web:hover:bg-accent web:hover:text-accent-foreground active:bg-accent',
16 | secondary: 'bg-secondary web:hover:opacity-80 active:opacity-80',
17 | ghost:
18 | 'web:hover:bg-accent web:hover:text-accent-foreground active:bg-accent',
19 | link: 'web:underline-offset-4 web:hover:underline web:focus:underline',
20 | },
21 | size: {
22 | default: 'h-10 px-4 py-2 native:h-12 native:px-5 native:py-3',
23 | sm: 'h-9 rounded-md px-3',
24 | lg: 'h-11 rounded-md px-8 native:h-14',
25 | icon: 'h-10 w-10',
26 | },
27 | },
28 | defaultVariants: {
29 | variant: 'default',
30 | size: 'default',
31 | },
32 | }
33 | );
34 |
35 | const buttonTextVariants = cva(
36 | 'web:whitespace-nowrap text-sm native:text-base font-medium text-foreground web:transition-colors',
37 | {
38 | variants: {
39 | variant: {
40 | default: 'text-primary-foreground',
41 | destructive: 'text-destructive-foreground',
42 | outline: 'group-active:text-accent-foreground',
43 | secondary:
44 | 'text-secondary-foreground group-active:text-secondary-foreground',
45 | ghost: 'group-active:text-accent-foreground',
46 | link: 'text-primary group-active:underline',
47 | },
48 | size: {
49 | default: '',
50 | sm: '',
51 | lg: 'native:text-lg',
52 | icon: '',
53 | },
54 | },
55 | defaultVariants: {
56 | variant: 'default',
57 | size: 'default',
58 | },
59 | }
60 | );
61 |
62 | type ButtonProps = React.ComponentPropsWithoutRef &
63 | VariantProps;
64 |
65 | const Button = React.forwardRef<
66 | React.ElementRef,
67 | ButtonProps
68 | >(({ className, variant, size, ...props }, ref) => {
69 | return (
70 |
77 |
86 |
87 | );
88 | });
89 | Button.displayName = 'Button';
90 |
91 | export { Button, buttonTextVariants, buttonVariants };
92 | export type { ButtonProps };
93 |
--------------------------------------------------------------------------------
/demo/components/ui/calendar.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 | import DateTimePicker, {
3 | DateType,
4 | CalendarDay,
5 | CalendarComponents,
6 | useDefaultClassNames,
7 | } from 'react-native-ui-datepicker';
8 | import Feather from '@expo/vector-icons/Feather';
9 | import { cssInterop } from 'nativewind';
10 |
11 | cssInterop(Feather, {
12 | className: {
13 | target: 'style',
14 | },
15 | });
16 |
17 | const icons: CalendarComponents = {
18 | IconPrev: (
19 |
20 | ),
21 | IconNext: (
22 |
23 | ),
24 | };
25 |
26 | export type CalendarProps = React.ComponentProps;
27 |
28 | function Calendar({
29 | className,
30 | classNames,
31 | showOutsideDays = true,
32 | containerHeight = 280,
33 | components,
34 | ...props
35 | }: React.ComponentProps) {
36 | const defaultClassNames = useDefaultClassNames();
37 |
38 | return (
39 |
86 | );
87 | }
88 |
89 | export { Calendar, DateType, CalendarDay };
90 |
--------------------------------------------------------------------------------
/demo/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | import * as SeparatorPrimitive from '@rn-primitives/separator';
2 | import * as React from 'react';
3 | import { cn } from '@/lib/utils';
4 |
5 | const Separator = React.forwardRef<
6 | SeparatorPrimitive.RootRef,
7 | SeparatorPrimitive.RootProps
8 | >(
9 | (
10 | { className, orientation = 'horizontal', decorative = true, ...props },
11 | ref
12 | ) => (
13 |
24 | )
25 | );
26 | Separator.displayName = SeparatorPrimitive.Root.displayName;
27 |
28 | export { Separator };
29 |
--------------------------------------------------------------------------------
/demo/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | import * as TabsPrimitive from '@rn-primitives/tabs';
2 | import * as React from 'react';
3 | import { cn } from '@/lib/utils';
4 | import { TextClassContext } from './text';
5 |
6 | const Tabs = TabsPrimitive.Root;
7 |
8 | const TabsList = React.forwardRef<
9 | TabsPrimitive.ListRef,
10 | TabsPrimitive.ListProps
11 | >(({ className, ...props }, ref) => (
12 |
20 | ));
21 | TabsList.displayName = TabsPrimitive.List.displayName;
22 |
23 | const TabsTrigger = React.forwardRef<
24 | TabsPrimitive.TriggerRef,
25 | TabsPrimitive.TriggerProps
26 | >(({ className, ...props }, ref) => {
27 | const { value } = TabsPrimitive.useRootContext();
28 | return (
29 |
35 |
46 |
47 | );
48 | });
49 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
50 |
51 | const TabsContent = React.forwardRef<
52 | TabsPrimitive.ContentRef,
53 | TabsPrimitive.ContentProps
54 | >(({ className, ...props }, ref) => (
55 |
63 | ));
64 | TabsContent.displayName = TabsPrimitive.Content.displayName;
65 |
66 | export { Tabs, TabsContent, TabsList, TabsTrigger };
67 |
--------------------------------------------------------------------------------
/demo/components/ui/text.tsx:
--------------------------------------------------------------------------------
1 | import * as Slot from '@rn-primitives/slot';
2 | import type { SlottableTextProps, TextRef } from '@rn-primitives/types';
3 | import * as React from 'react';
4 | import { Text as RNText } from 'react-native';
5 | import { cn } from '@/lib/utils';
6 |
7 | const TextClassContext = React.createContext(undefined);
8 |
9 | const Text = React.forwardRef(
10 | ({ className, asChild = false, ...props }, ref) => {
11 | const textClass = React.useContext(TextClassContext);
12 | const Component = asChild ? Slot.Text : RNText;
13 | return (
14 |
23 | );
24 | }
25 | );
26 | Text.displayName = 'Text';
27 |
28 | export { Text, TextClassContext };
29 |
--------------------------------------------------------------------------------
/demo/constants/Colors.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Below are the colors that are used in the app. The colors are defined in the light and dark mode.
3 | * There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc.
4 | */
5 |
6 | const tintColorLight = '#0a7ea4';
7 | const tintColorDark = '#fff';
8 |
9 | export const Colors = {
10 | light: {
11 | text: '#11181C',
12 | background: '#fff',
13 | tint: tintColorLight,
14 | icon: '#687076',
15 | tabIconDefault: '#687076',
16 | tabIconSelected: tintColorLight,
17 | },
18 | dark: {
19 | text: '#ECEDEE',
20 | background: '#151718',
21 | tint: tintColorDark,
22 | icon: '#9BA1A6',
23 | tabIconDefault: '#9BA1A6',
24 | tabIconSelected: tintColorDark,
25 | },
26 | };
27 |
--------------------------------------------------------------------------------
/demo/global.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 240 10% 3.9%;
9 | --card: 0 0% 100%;
10 | --card-foreground: 240 10% 3.9%;
11 | --popover: 0 0% 100%;
12 | --popover-foreground: 240 10% 3.9%;
13 | --primary: 240 5.9% 10%;
14 | --primary-foreground: 0 0% 98%;
15 | --secondary: 240 4.8% 95.9%;
16 | --secondary-foreground: 240 5.9% 10%;
17 | --muted: 240 4.8% 95.9%;
18 | --muted-foreground: 240 3.8% 46.1%;
19 | --accent: 240 4.8% 95.9%;
20 | --accent-foreground: 240 5.9% 10%;
21 | --destructive: 0 84.2% 60.2%;
22 | --destructive-foreground: 0 0% 98%;
23 | --border: 240 5.9% 90%;
24 | --input: 240 5.9% 90%;
25 | --ring: 240 5.9% 10%;
26 | --radius: 0.5rem;
27 | }
28 |
29 | .dark:root {
30 | --background: 240 10% 3.9%;
31 | --foreground: 0 0% 98%;
32 | --card: 240 10% 3.9%;
33 | --card-foreground: 0 0% 98%;
34 | --popover: 240 10% 3.9%;
35 | --popover-foreground: 0 0% 98%;
36 | --primary: 0 0% 98%;
37 | --primary-foreground: 240 5.9% 10%;
38 | --secondary: 240 3.7% 15.9%;
39 | --secondary-foreground: 0 0% 98%;
40 | --muted: 240 3.7% 15.9%;
41 | --muted-foreground: 240 5% 64.9%;
42 | --accent: 240 3.7% 15.9%;
43 | --accent-foreground: 0 0% 98%;
44 | --destructive: 0 62.8% 30.6%;
45 | --destructive-foreground: 0 0% 98%;
46 | --border: 240 3.7% 15.9%;
47 | --input: 240 3.7% 15.9%;
48 | --ring: 240 4.9% 83.9%;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/demo/hooks/use-active-link.ts:
--------------------------------------------------------------------------------
1 | import { usePathname } from 'expo-router';
2 |
3 | // ----------------------------------------------------------------------
4 |
5 | type ReturnType = boolean;
6 |
7 | export function useActiveLink(path: string, deep = false): ReturnType {
8 | const pathname = usePathname();
9 |
10 | const checkPath = path.startsWith('#');
11 |
12 | const currentPath = path === '/' ? '/' : `${path}`;
13 |
14 | const normalActive = !checkPath && pathname === currentPath;
15 |
16 | const rootPath = pathname.split('/')[1] || '/';
17 | const currentPathPath = currentPath.split('/')[1] || '/';
18 |
19 | const deepActive = !checkPath && currentPathPath === rootPath;
20 |
21 | return deep ? deepActive : normalActive;
22 | }
23 |
--------------------------------------------------------------------------------
/demo/hooks/useColorScheme.ts:
--------------------------------------------------------------------------------
1 | export { useColorScheme } from 'react-native';
2 |
--------------------------------------------------------------------------------
/demo/hooks/useColorScheme.web.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { useColorScheme as useRNColorScheme } from 'react-native';
3 |
4 | /**
5 | * To support static rendering, this value needs to be re-calculated on the client side for web
6 | */
7 | export function useColorScheme() {
8 | const [hasHydrated, setHasHydrated] = useState(false);
9 |
10 | useEffect(() => {
11 | setHasHydrated(true);
12 | }, []);
13 |
14 | const colorScheme = useRNColorScheme();
15 |
16 | if (hasHydrated) {
17 | return colorScheme;
18 | }
19 |
20 | return 'light';
21 | }
22 |
--------------------------------------------------------------------------------
/demo/hooks/useThemeColor.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Learn more about light and dark modes:
3 | * https://docs.expo.dev/guides/color-schemes/
4 | */
5 |
6 | import { Colors } from '@/constants/Colors';
7 | import { useColorScheme } from '@/hooks/useColorScheme';
8 |
9 | export function useThemeColor(
10 | props: { light?: string; dark?: string },
11 | colorName: keyof typeof Colors.light & keyof typeof Colors.dark
12 | ) {
13 | const theme = useColorScheme() ?? 'light';
14 | const colorFromProps = props[theme];
15 |
16 | if (colorFromProps) {
17 | return colorFromProps;
18 | } else {
19 | return Colors[theme][colorName];
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/demo/layouts/common/github-stats.tsx:
--------------------------------------------------------------------------------
1 | import { View } from 'react-native';
2 | import { Link } from 'expo-router';
3 | import AntDesign from '@expo/vector-icons/AntDesign';
4 | import Feather from '@expo/vector-icons/Feather';
5 | import { cssInterop } from 'nativewind';
6 | import { Text } from '@/components/ui/text';
7 | import { Button } from '@/components/ui/button';
8 | import { useEffect, useState } from 'react';
9 | import { formatNumber } from '@/lib/utils';
10 |
11 | cssInterop(AntDesign, {
12 | className: {
13 | target: 'style',
14 | },
15 | });
16 |
17 | cssInterop(Feather, {
18 | className: {
19 | target: 'style',
20 | },
21 | });
22 |
23 | type DataType = {
24 | downloads: number;
25 | start: string;
26 | end: string;
27 | package: string;
28 | };
29 |
30 | export function GithubStats() {
31 | const [loading, setLoading] = useState(true);
32 | const [data, setData] = useState();
33 |
34 | const getStats = async () => {
35 | try {
36 | const response = await fetch(
37 | 'https://api.npmjs.org/downloads/point/last-month/react-native-ui-datepicker'
38 | );
39 | const json = await response.json();
40 | setData(json);
41 | } catch (error) {
42 | console.error(error);
43 | } finally {
44 | setLoading(false);
45 | }
46 | };
47 |
48 | useEffect(() => {
49 | getStats();
50 | }, []);
51 |
52 | return (
53 |
57 |
69 |
70 | );
71 | }
72 |
--------------------------------------------------------------------------------
/demo/layouts/common/theme-toggle.tsx:
--------------------------------------------------------------------------------
1 | import { cssInterop } from 'nativewind';
2 | import { Button } from '@/components/ui/button';
3 | import { useColorScheme } from '@/lib/useColorScheme';
4 | import Feather from '@expo/vector-icons/Feather';
5 |
6 | cssInterop(Feather, {
7 | className: {
8 | target: 'style',
9 | },
10 | });
11 |
12 | export const ThemeToggle = () => {
13 | const { toggleColorScheme, isDarkColorScheme } = useColorScheme();
14 |
15 | return (
16 |
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/demo/layouts/header.tsx:
--------------------------------------------------------------------------------
1 | import { View } from 'react-native';
2 | import { NavMenu } from './nav/nav-menu';
3 | import { ThemeToggle } from './common/theme-toggle';
4 | import { GithubStats } from './common/github-stats';
5 |
6 | export const Header = () => {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/demo/layouts/nav/config-navigation.ts:
--------------------------------------------------------------------------------
1 | import { NavItemProps } from './types';
2 |
3 | export const navConfig: NavItemProps[] = [
4 | { title: 'Home', path: '/' },
5 | { title: 'Examples', path: '/examples' },
6 | { title: 'Playground', path: '/playground' },
7 | ];
8 |
--------------------------------------------------------------------------------
/demo/layouts/nav/nav-item.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'expo-router';
2 | import { useActiveLink } from '@/hooks/use-active-link';
3 | import { NavItemProps } from './types';
4 | import { Button } from '@/components/ui/button';
5 | import { Text } from '@/components/ui/text';
6 | import { cn } from '@/lib/utils';
7 |
8 | type Props = {
9 | item: NavItemProps;
10 | };
11 |
12 | export function NavItem({ item }: Props) {
13 | const active = useActiveLink(item.path);
14 |
15 | return (
16 |
17 |
23 | {item.title}
24 |
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/demo/layouts/nav/nav-menu.tsx:
--------------------------------------------------------------------------------
1 | import { View } from 'react-native';
2 | import { navConfig } from './config-navigation';
3 | import { NavItem } from './nav-item';
4 |
5 | export function NavMenu() {
6 | return (
7 |
8 | {navConfig.map((item, index) => (
9 |
10 | ))}
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/demo/layouts/nav/types.ts:
--------------------------------------------------------------------------------
1 | export type NavItemProps = {
2 | title: string;
3 | path: string;
4 | };
5 |
--------------------------------------------------------------------------------
/demo/lib/constants.ts:
--------------------------------------------------------------------------------
1 | import { DarkTheme, DefaultTheme } from '@react-navigation/native';
2 |
3 | const LIGHT_THEME = {
4 | ...DefaultTheme,
5 | colors: {
6 | ...DefaultTheme.colors,
7 | background: 'hsl(0 0% 100%)', // background
8 | border: 'hsl(240 5.9% 90%)', // border
9 | card: 'hsl(0 0% 100%)', // card
10 | notification: 'hsl(0 84.2% 60.2%)', // destructive
11 | primary: 'hsl(240 5.9% 10%)', // primary
12 | text: 'hsl(240 10% 3.9%)', // foreground
13 | backdrop: 'rgba(91, 91, 91, 0.8)',
14 | },
15 | };
16 |
17 | const DARK_THEME = {
18 | ...DarkTheme,
19 | colors: {
20 | ...DarkTheme.colors,
21 | background: 'hsl(240 10% 3.9%)', // background
22 | border: 'hsl(240 3.7% 15.9%)', // border
23 | card: 'hsl(240 10% 3.9%)', // card
24 | notification: 'hsl(0 72% 51%)', // destructive
25 | primary: 'hsl(0 0% 98%)', // primary
26 | text: 'hsl(0 0% 98%)', // foreground
27 | backdrop: 'rgba(91, 91, 91, 0.8)',
28 | },
29 | };
30 |
31 | export { LIGHT_THEME, DARK_THEME };
32 |
--------------------------------------------------------------------------------
/demo/lib/generate-dates.ts:
--------------------------------------------------------------------------------
1 | import dayjs from 'dayjs';
2 |
3 | const currentDate = dayjs();
4 | const currentYear = currentDate.year();
5 | const currentMonth = currentDate.month();
6 |
7 | export function getRandomDateInMonth(year: number, month: number) {
8 | const startOfMonth = dayjs().year(year).month(month).startOf('month');
9 | const endOfMonth = dayjs().year(year).month(month).endOf('month');
10 | const randomDays = Math.floor(
11 | Math.random() * endOfMonth.diff(startOfMonth, 'day')
12 | );
13 | return startOfMonth.add(randomDays, 'day');
14 | }
15 |
16 | export const previousMonthDates = (length: number) => {
17 | return Array.from({ length }, () =>
18 | getRandomDateInMonth(currentYear, currentMonth - 1).format('YYYY-MM-DD')
19 | );
20 | };
21 |
22 | export const currentMonthDates = (length: number) => {
23 | return Array.from({ length }, () =>
24 | getRandomDateInMonth(currentYear, currentMonth).format('YYYY-MM-DD')
25 | );
26 | };
27 |
28 | export const nextMonthDates = (length: number) => {
29 | return Array.from({ length }, () =>
30 | getRandomDateInMonth(currentYear, currentMonth + 1).format('YYYY-MM-DD')
31 | );
32 | };
33 |
--------------------------------------------------------------------------------
/demo/lib/useColorScheme.tsx:
--------------------------------------------------------------------------------
1 | import { useColorScheme as useNativewindColorScheme } from 'nativewind';
2 |
3 | export function useColorScheme() {
4 | const { colorScheme, setColorScheme, toggleColorScheme } =
5 | useNativewindColorScheme();
6 |
7 | return {
8 | colorScheme: colorScheme ?? 'dark',
9 | isDarkColorScheme: colorScheme === 'dark',
10 | setColorScheme,
11 | toggleColorScheme,
12 | };
13 | }
14 |
--------------------------------------------------------------------------------
/demo/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from 'clsx';
2 | import { twMerge } from 'tailwind-merge';
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
8 | export function formatNumber(num: number) {
9 | if (num >= 1e9) {
10 | return (num / 1e9).toFixed(1) + 'B'; // Billions
11 | } else if (num >= 1e6) {
12 | return (num / 1e6).toFixed(1) + 'M'; // Millions
13 | } else if (num >= 1e3) {
14 | return (num / 1e3).toFixed(1) + 'K'; // Thousands
15 | } else {
16 | return num.toString(); // Less than 1000
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/demo/metro.config.js:
--------------------------------------------------------------------------------
1 | const { getDefaultConfig } = require('expo/metro-config');
2 | const { withNativeWind } = require('nativewind/metro');
3 | const path = require('path');
4 |
5 | const projectRoot = __dirname;
6 | const libraryRoot = path.resolve(projectRoot, '..');
7 |
8 | const config = getDefaultConfig(projectRoot);
9 |
10 | if (config.resolver) {
11 | // 1. Watch all files within the Repository
12 | config.watchFolders = [libraryRoot];
13 | // 2. Let Metro know where to resolve packages, and in what order
14 | config.resolver.nodeModulesPaths = [
15 | path.resolve(projectRoot, 'node_modules'),
16 | path.resolve(libraryRoot, 'node_modules'),
17 | ];
18 | // 3. Force Metro to resolve (sub)dependencies only from the `nodeModulesPaths`
19 | config.resolver.disableHierarchicalLookup = true;
20 | }
21 |
22 | module.exports = withNativeWind(config, { input: './global.css' });
23 |
--------------------------------------------------------------------------------
/demo/nativewind-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | // NOTE: This file should not be edited and should be committed with your source code. It is generated by NativeWind.åå
4 |
--------------------------------------------------------------------------------
/demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-ui-datepicker-demo",
3 | "main": "expo-router/entry",
4 | "version": "1.0.0",
5 | "homepage": "https://farhoudshapouran.github.io/react-native-ui-datepicker",
6 | "scripts": {
7 | "start": "expo start -c",
8 | "reset-project": "node ./scripts/reset-project.js",
9 | "android": "expo start --android",
10 | "ios": "expo start --ios",
11 | "web": "expo start --web",
12 | "test": "jest --watchAll",
13 | "lint": "expo lint",
14 | "predeploy": "expo export -p web",
15 | "deploy": "gh-pages --nojekyll -d dist"
16 | },
17 | "jest": {
18 | "preset": "jest-expo"
19 | },
20 | "dependencies": {
21 | "@expo-google-fonts/archivo": "^0.2.3",
22 | "@expo-google-fonts/inter": "^0.2.3",
23 | "@expo-google-fonts/poppins": "^0.2.3",
24 | "@expo/vector-icons": "^14.0.2",
25 | "@react-navigation/bottom-tabs": "^7.2.0",
26 | "@react-navigation/native": "^7.0.14",
27 | "@rn-primitives/separator": "^1.1.0",
28 | "@rn-primitives/slot": "^1.1.0",
29 | "@rn-primitives/tabs": "^1.1.0",
30 | "@rn-primitives/types": "^1.1.0",
31 | "@shopify/react-native-skia": "1.5.0",
32 | "class-variance-authority": "^0.7.1",
33 | "clsx": "^2.1.1",
34 | "dayjs": "^1.11.13",
35 | "expo": "^52.0.35",
36 | "expo-blur": "~14.0.3",
37 | "expo-clipboard": "~7.0.1",
38 | "expo-constants": "~17.0.6",
39 | "expo-font": "~13.0.3",
40 | "expo-haptics": "~14.0.1",
41 | "expo-linking": "~7.0.5",
42 | "expo-router": "~4.0.17",
43 | "expo-splash-screen": "~0.29.22",
44 | "expo-status-bar": "~2.0.1",
45 | "expo-symbols": "~0.2.2",
46 | "expo-system-ui": "~4.0.8",
47 | "expo-web-browser": "~14.0.2",
48 | "nativewind": "^4.1.23",
49 | "react": "18.3.1",
50 | "react-dom": "18.3.1",
51 | "react-native": "0.76.7",
52 | "react-native-bouncy-checkbox": "^4.1.2",
53 | "react-native-gesture-handler": "~2.20.2",
54 | "react-native-reanimated": "~3.16.1",
55 | "react-native-safe-area-context": "4.12.0",
56 | "react-native-screens": "~4.4.0",
57 | "react-native-web": "~0.19.13",
58 | "react-native-webview": "13.12.5",
59 | "react-scan": "^0.0.36-native",
60 | "tailwind-merge": "^3.0.1",
61 | "tailwindcss": "^3.4.3"
62 | },
63 | "devDependencies": {
64 | "@babel/core": "^7.25.2",
65 | "@types/jest": "^29.5.12",
66 | "@types/react": "~18.3.12",
67 | "@types/react-test-renderer": "^18.3.0",
68 | "gh-pages": "^6.3.0",
69 | "jest": "^29.2.1",
70 | "jest-expo": "~52.0.4",
71 | "prettier": "^3.4.2",
72 | "react-test-renderer": "18.3.1",
73 | "typescript": "^5.3.3"
74 | },
75 | "private": true
76 | }
77 |
--------------------------------------------------------------------------------
/demo/scripts/reset-project.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * This script is used to reset the project to a blank state.
5 | * It moves the /app, /components, /hooks, /scripts, and /constants directories to /app-example and creates a new /app directory with an index.tsx and _layout.tsx file.
6 | * You can remove the `reset-project` script from package.json and safely delete this file after running it.
7 | */
8 |
9 | const fs = require("fs");
10 | const path = require("path");
11 |
12 | const root = process.cwd();
13 | const oldDirs = ["app", "components", "hooks", "constants", "scripts"];
14 | const newDir = "app-example";
15 | const newAppDir = "app";
16 | const newDirPath = path.join(root, newDir);
17 |
18 | const indexContent = `import { Text, View } from "react-native";
19 |
20 | export default function Index() {
21 | return (
22 |
29 | Edit app/index.tsx to edit this screen.
30 |
31 | );
32 | }
33 | `;
34 |
35 | const layoutContent = `import { Stack } from "expo-router";
36 |
37 | export default function RootLayout() {
38 | return ;
39 | }
40 | `;
41 |
42 | const moveDirectories = async () => {
43 | try {
44 | // Create the app-example directory
45 | await fs.promises.mkdir(newDirPath, { recursive: true });
46 | console.log(`📁 /${newDir} directory created.`);
47 |
48 | // Move old directories to new app-example directory
49 | for (const dir of oldDirs) {
50 | const oldDirPath = path.join(root, dir);
51 | const newDirPath = path.join(root, newDir, dir);
52 | if (fs.existsSync(oldDirPath)) {
53 | await fs.promises.rename(oldDirPath, newDirPath);
54 | console.log(`➡️ /${dir} moved to /${newDir}/${dir}.`);
55 | } else {
56 | console.log(`➡️ /${dir} does not exist, skipping.`);
57 | }
58 | }
59 |
60 | // Create new /app directory
61 | const newAppDirPath = path.join(root, newAppDir);
62 | await fs.promises.mkdir(newAppDirPath, { recursive: true });
63 | console.log("\n📁 New /app directory created.");
64 |
65 | // Create index.tsx
66 | const indexPath = path.join(newAppDirPath, "index.tsx");
67 | await fs.promises.writeFile(indexPath, indexContent);
68 | console.log("📄 app/index.tsx created.");
69 |
70 | // Create _layout.tsx
71 | const layoutPath = path.join(newAppDirPath, "_layout.tsx");
72 | await fs.promises.writeFile(layoutPath, layoutContent);
73 | console.log("📄 app/_layout.tsx created.");
74 |
75 | console.log("\n✅ Project reset complete. Next steps:");
76 | console.log(
77 | "1. Run `npx expo start` to start a development server.\n2. Edit app/index.tsx to edit the main screen.\n3. Delete the /app-example directory when you're done referencing it."
78 | );
79 | } catch (error) {
80 | console.error(`Error during script execution: ${error}`);
81 | }
82 | };
83 |
84 | moveDirectories();
85 |
--------------------------------------------------------------------------------
/demo/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const { hairlineWidth } = require('nativewind/theme');
2 |
3 | /** @type {import('tailwindcss').Config} */
4 | module.exports = {
5 | darkMode: 'class',
6 | content: [
7 | './app/**/*.{js,jsx,ts,tsx}',
8 | './components/**/*.{ts,tsx}',
9 | './layouts/**/*.{ts,tsx}',
10 | ],
11 | presets: [require('nativewind/preset')],
12 | theme: {
13 | extend: {
14 | fontFamily: {
15 | interLight: ['Inter_300Light'],
16 | inter: ['Inter_400Regular'],
17 | interMedium: ['Inter_500Medium'],
18 | interSemiBold: ['Inter_600SemiBold'],
19 | interBold: ['Inter_700Bold'],
20 | archivoLight: ['Archivo_300Light'],
21 | archivo: ['Archivo_400Regular'],
22 | archivoMedium: ['Archivo_500Medium'],
23 | archivoSemiBold: ['Archivo_600SemiBold'],
24 | archivoBold: ['Archivo_700Bold'],
25 | poppinsLight: ['Poppins_300Light'],
26 | poppins: ['Poppins_400Regular'],
27 | poppinsMedium: ['Poppins_500Medium'],
28 | poppinsSemiBold: ['Poppins_600SemiBold'],
29 | poppinsBold: ['Poppins_700Bold'],
30 | },
31 | colors: {
32 | border: 'hsl(var(--border))',
33 | input: 'hsl(var(--input))',
34 | ring: 'hsl(var(--ring))',
35 | background: 'hsl(var(--background))',
36 | foreground: 'hsl(var(--foreground))',
37 | primary: {
38 | DEFAULT: 'hsl(var(--primary))',
39 | foreground: 'hsl(var(--primary-foreground))',
40 | },
41 | secondary: {
42 | DEFAULT: 'hsl(var(--secondary))',
43 | foreground: 'hsl(var(--secondary-foreground))',
44 | },
45 | destructive: {
46 | DEFAULT: 'hsl(var(--destructive))',
47 | foreground: 'hsl(var(--destructive-foreground))',
48 | },
49 | success: {
50 | DEFAULT: 'hsl(var(--success))',
51 | foreground: 'hsl(var(--success-foreground))',
52 | },
53 | warning: {
54 | DEFAULT: 'hsl(var(--warning))',
55 | foreground: 'hsl(var(--warning-foreground))',
56 | },
57 | muted: {
58 | DEFAULT: 'hsl(var(--muted))',
59 | foreground: 'hsl(var(--muted-foreground))',
60 | },
61 | accent: {
62 | DEFAULT: 'hsl(var(--accent))',
63 | foreground: 'hsl(var(--accent-foreground))',
64 | },
65 | popover: {
66 | DEFAULT: 'hsl(var(--popover))',
67 | foreground: 'hsl(var(--popover-foreground))',
68 | },
69 | card: {
70 | DEFAULT: 'hsl(var(--card))',
71 | foreground: 'hsl(var(--card-foreground))',
72 | },
73 | },
74 | borderWidth: {
75 | hairline: hairlineWidth(),
76 | },
77 | borderRadius: {
78 | '3xl': 'calc(var(--radius) + 16px)',
79 | '2xl': 'calc(var(--radius) + 10px)',
80 | 'xl': 'calc(var(--radius) + 4px)',
81 | 'lg': 'var(--radius)',
82 | 'md': 'calc(var(--radius) - 2px)',
83 | 'sm': 'calc(var(--radius) - 4px)',
84 | },
85 | },
86 | },
87 | plugins: [],
88 | };
89 |
--------------------------------------------------------------------------------
/demo/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo/tsconfig.base",
3 | "compilerOptions": {
4 | "strict": true,
5 | "paths": {
6 | "@/*": ["./*"],
7 | "react-native-ui-datepicker": ["../src"]
8 | }
9 | },
10 | "include": [
11 | "**/*.ts",
12 | "**/*.tsx",
13 | ".expo/types/**/*.ts",
14 | "expo-env.d.ts",
15 | "nativewind-env.d.ts"
16 | ],
17 | "exclude": ["node_modules", "build", "dist", ".expo"]
18 | }
19 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | const prettier = require('eslint-plugin-prettier');
2 | const reactHooks = require('eslint-plugin-react-hooks');
3 | const reactNative = require('eslint-plugin-react-native');
4 |
5 | module.exports = [
6 | {
7 | files: ['src/**/*.{js,jsx,ts,tsx}'],
8 | ignores: [
9 | 'node_modules/',
10 | 'lib/',
11 | 'example/',
12 | 'demo/',
13 | '*.config.js',
14 | 'tsconfig.json',
15 | ],
16 | plugins: {
17 | prettier,
18 | 'react-hooks': reactHooks,
19 | 'react-native': reactNative,
20 | },
21 | rules: {
22 | 'react/react-in-jsx-scope': 'off',
23 | 'prettier/prettier': 'warn',
24 | 'react-hooks/rules-of-hooks': 'error',
25 | 'react-hooks/exhaustive-deps': 'warn',
26 | 'react-native/no-inline-styles': 'warn',
27 | },
28 | languageOptions: {
29 | parser: require('@typescript-eslint/parser'),
30 | ecmaVersion: 'latest',
31 | sourceType: 'module',
32 | parserOptions: {
33 | ecmaFeatures: {
34 | jsx: true,
35 | },
36 | project: './tsconfig.json',
37 | },
38 | },
39 | },
40 | ];
41 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
2 |
3 | # dependencies
4 | node_modules/
5 |
6 | # Expo
7 | .expo/
8 | dist/
9 | web-build/
10 | expo-env.d.ts
11 |
12 | # Native
13 | *.orig.*
14 | *.jks
15 | *.p8
16 | *.p12
17 | *.key
18 | *.mobileprovision
19 |
20 | # Metro
21 | .metro-health-check*
22 |
23 | # debug
24 | npm-debug.*
25 | yarn-debug.*
26 | yarn-error.*
27 |
28 | # macOS
29 | .DS_Store
30 | *.pem
31 |
32 | # local env files
33 | .env*.local
34 |
35 | # typescript
36 | *.tsbuildinfo
37 |
38 | app-example
39 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | # Welcome to your Expo app 👋
2 |
3 | This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).
4 |
5 | ## Get started
6 |
7 | 1. Install dependencies
8 |
9 | ```bash
10 | npm install
11 | ```
12 |
13 | 2. Start the app
14 |
15 | ```bash
16 | npx expo start
17 | ```
18 |
19 | In the output, you'll find options to open the app in a
20 |
21 | - [development build](https://docs.expo.dev/develop/development-builds/introduction/)
22 | - [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
23 | - [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
24 | - [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo
25 |
26 | You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
27 |
28 | ## Get a fresh project
29 |
30 | When you're ready, run:
31 |
32 | ```bash
33 | npm run reset-project
34 | ```
35 |
36 | This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing.
37 |
38 | ## Learn more
39 |
40 | To learn more about developing your project with Expo, look at the following resources:
41 |
42 | - [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides).
43 | - [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web.
44 |
45 | ## Join the community
46 |
47 | Join our community of developers creating universal apps.
48 |
49 | - [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
50 | - [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.
51 |
--------------------------------------------------------------------------------
/example/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "React Native UI DatePicker Example",
4 | "slug": "react-native-ui-datepicker-example",
5 | "version": "1.0.0",
6 | "orientation": "portrait",
7 | "icon": "./assets/images/icon.png",
8 | "scheme": "myapp",
9 | "userInterfaceStyle": "automatic",
10 | "newArchEnabled": true,
11 | "ios": {
12 | "supportsTablet": true
13 | },
14 | "android": {
15 | "adaptiveIcon": {
16 | "foregroundImage": "./assets/images/adaptive-icon.png",
17 | "backgroundColor": "#ffffff"
18 | }
19 | },
20 | "web": {
21 | "bundler": "metro",
22 | "output": "static",
23 | "favicon": "./assets/images/favicon.png"
24 | },
25 | "plugins": [
26 | "expo-router",
27 | [
28 | "expo-splash-screen",
29 | {
30 | "image": "./assets/images/splash-icon.png",
31 | "imageWidth": 200,
32 | "resizeMode": "contain",
33 | "backgroundColor": "#ffffff"
34 | }
35 | ]
36 | ],
37 | "experiments": {
38 | "typedRoutes": true
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/example/app/+not-found.tsx:
--------------------------------------------------------------------------------
1 | import { Link, Stack } from 'expo-router';
2 | import { StyleSheet } from 'react-native';
3 |
4 | import { ThemedText } from '@/components/ThemedText';
5 | import { ThemedView } from '@/components/ThemedView';
6 |
7 | export default function NotFoundScreen() {
8 | return (
9 | <>
10 |
11 |
12 | This screen doesn't exist.
13 |
14 | Go to home screen!
15 |
16 |
17 | >
18 | );
19 | }
20 |
21 | const styles = StyleSheet.create({
22 | container: {
23 | flex: 1,
24 | alignItems: 'center',
25 | justifyContent: 'center',
26 | padding: 20,
27 | },
28 | link: {
29 | marginTop: 15,
30 | paddingVertical: 15,
31 | },
32 | });
33 |
--------------------------------------------------------------------------------
/example/app/_layout.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | DarkTheme,
3 | DefaultTheme,
4 | ThemeProvider,
5 | } from '@react-navigation/native';
6 | import { useFonts } from 'expo-font';
7 | import { Stack } from 'expo-router';
8 | import * as SplashScreen from 'expo-splash-screen';
9 | import { StatusBar } from 'expo-status-bar';
10 | import { useEffect } from 'react';
11 | import 'react-native-reanimated';
12 |
13 | import { useColorScheme } from '@/hooks/useColorScheme';
14 | import { I18nManager } from 'react-native';
15 |
16 | // Prevent the splash screen from auto-hiding before asset loading is complete.
17 | SplashScreen.preventAutoHideAsync();
18 |
19 | //I18nManager.forceRTL(true);
20 |
21 | export default function RootLayout() {
22 | const colorScheme = useColorScheme();
23 | const [loaded] = useFonts({
24 | SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
25 | });
26 |
27 | useEffect(() => {
28 | if (loaded) {
29 | SplashScreen.hideAsync();
30 | }
31 | }, [loaded]);
32 |
33 | if (!loaded) {
34 | return null;
35 | }
36 |
37 | return (
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/example/app/bottom-sheet.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useRef, useState } from 'react';
2 | import { Button, StyleSheet } from 'react-native';
3 | import { GestureHandlerRootView } from 'react-native-gesture-handler';
4 | import {
5 | BottomSheetModal,
6 | BottomSheetView,
7 | BottomSheetModalProvider,
8 | } from '@gorhom/bottom-sheet';
9 | import DateTimePicker, {
10 | DateType,
11 | useDefaultStyles,
12 | } from 'react-native-ui-datepicker';
13 |
14 | export default function BottomSheetScreen() {
15 | const defaultStyles = useDefaultStyles();
16 | const [date, setDate] = useState();
17 | const bottomSheetModalRef = useRef(null);
18 | const [dates, setDates] = useState();
19 | const [range, setRange] = useState<{
20 | startDate: DateType;
21 | endDate: DateType;
22 | }>({ startDate: undefined, endDate: undefined });
23 |
24 | const handlePresentModalPress = useCallback(() => {
25 | bottomSheetModalRef.current?.present();
26 | }, []);
27 |
28 | // const handleSheetChanges = useCallback((index: number) => {
29 | // console.log('handleSheetChanges', index);
30 | // }, []);
31 |
32 | return (
33 |
34 |
35 |
36 |
40 |
41 | setDate(params.date)}
46 | firstDayOfWeek={6}
47 | multiRangeMode
48 | showOutsideDays
49 | timePicker
50 | //calendar="jalali"
51 | //locale="en"
52 | //numerals="arabext"
53 | />
54 |
55 |
56 |
57 |
58 | );
59 | }
60 |
61 | const styles = StyleSheet.create({
62 | container: {
63 | flex: 1,
64 | alignItems: 'center',
65 | justifyContent: 'center',
66 | },
67 | contentContainer: {
68 | flex: 1,
69 | paddingHorizontal: 30,
70 | height: 400,
71 | },
72 | });
73 |
--------------------------------------------------------------------------------
/example/app/index.tsx:
--------------------------------------------------------------------------------
1 | import { HelloWave } from '@/components/HelloWave';
2 | import { ThemedText } from '@/components/ThemedText';
3 | import { ThemedView } from '@/components/ThemedView';
4 | import { Link } from 'expo-router';
5 | import { ScrollView, StyleSheet } from 'react-native';
6 |
7 | export default function MainPage() {
8 | return (
9 |
10 |
11 | Bottom Sheet
12 |
13 |
14 | );
15 | }
16 |
17 | const styles = StyleSheet.create({
18 | container: {
19 | flex: 1,
20 | },
21 | contentContainer: {
22 | flex: 1,
23 | paddingHorizontal: 30,
24 | height: 400,
25 | },
26 | });
27 |
--------------------------------------------------------------------------------
/example/assets/fonts/SpaceMono-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/farhoudshapouran/react-native-ui-datepicker/9b317eba399144944ac5235e2d151036456c5786/example/assets/fonts/SpaceMono-Regular.ttf
--------------------------------------------------------------------------------
/example/assets/images/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/farhoudshapouran/react-native-ui-datepicker/9b317eba399144944ac5235e2d151036456c5786/example/assets/images/adaptive-icon.png
--------------------------------------------------------------------------------
/example/assets/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/farhoudshapouran/react-native-ui-datepicker/9b317eba399144944ac5235e2d151036456c5786/example/assets/images/favicon.png
--------------------------------------------------------------------------------
/example/assets/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/farhoudshapouran/react-native-ui-datepicker/9b317eba399144944ac5235e2d151036456c5786/example/assets/images/icon.png
--------------------------------------------------------------------------------
/example/assets/images/partial-react-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/farhoudshapouran/react-native-ui-datepicker/9b317eba399144944ac5235e2d151036456c5786/example/assets/images/partial-react-logo.png
--------------------------------------------------------------------------------
/example/assets/images/react-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/farhoudshapouran/react-native-ui-datepicker/9b317eba399144944ac5235e2d151036456c5786/example/assets/images/react-logo.png
--------------------------------------------------------------------------------
/example/assets/images/react-logo@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/farhoudshapouran/react-native-ui-datepicker/9b317eba399144944ac5235e2d151036456c5786/example/assets/images/react-logo@2x.png
--------------------------------------------------------------------------------
/example/assets/images/react-logo@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/farhoudshapouran/react-native-ui-datepicker/9b317eba399144944ac5235e2d151036456c5786/example/assets/images/react-logo@3x.png
--------------------------------------------------------------------------------
/example/assets/images/splash-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/farhoudshapouran/react-native-ui-datepicker/9b317eba399144944ac5235e2d151036456c5786/example/assets/images/splash-icon.png
--------------------------------------------------------------------------------
/example/components/Collapsible.tsx:
--------------------------------------------------------------------------------
1 | import { PropsWithChildren, useState } from 'react';
2 | import { StyleSheet, TouchableOpacity } from 'react-native';
3 |
4 | import { ThemedText } from '@/components/ThemedText';
5 | import { ThemedView } from '@/components/ThemedView';
6 | import { IconSymbol } from '@/components/ui/IconSymbol';
7 | import { Colors } from '@/constants/Colors';
8 | import { useColorScheme } from '@/hooks/useColorScheme';
9 |
10 | export function Collapsible({ children, title }: PropsWithChildren & { title: string }) {
11 | const [isOpen, setIsOpen] = useState(false);
12 | const theme = useColorScheme() ?? 'light';
13 |
14 | return (
15 |
16 | setIsOpen((value) => !value)}
19 | activeOpacity={0.8}>
20 |
27 |
28 | {title}
29 |
30 | {isOpen && {children}}
31 |
32 | );
33 | }
34 |
35 | const styles = StyleSheet.create({
36 | heading: {
37 | flexDirection: 'row',
38 | alignItems: 'center',
39 | gap: 6,
40 | },
41 | content: {
42 | marginTop: 6,
43 | marginLeft: 24,
44 | },
45 | });
46 |
--------------------------------------------------------------------------------
/example/components/ExternalLink.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'expo-router';
2 | import { openBrowserAsync } from 'expo-web-browser';
3 | import { type ComponentProps } from 'react';
4 | import { Platform } from 'react-native';
5 |
6 | type Props = Omit, 'href'> & { href: string };
7 |
8 | export function ExternalLink({ href, ...rest }: Props) {
9 | return (
10 | {
15 | if (Platform.OS !== 'web') {
16 | // Prevent the default behavior of linking to the default browser on native.
17 | event.preventDefault();
18 | // Open the link in an in-app browser.
19 | await openBrowserAsync(href);
20 | }
21 | }}
22 | />
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/example/components/HapticTab.tsx:
--------------------------------------------------------------------------------
1 | import { BottomTabBarButtonProps } from '@react-navigation/bottom-tabs';
2 | import { PlatformPressable } from '@react-navigation/elements';
3 | import * as Haptics from 'expo-haptics';
4 |
5 | export function HapticTab(props: BottomTabBarButtonProps) {
6 | return (
7 | {
10 | if (process.env.EXPO_OS === 'ios') {
11 | // Add a soft haptic feedback when pressing down on the tabs.
12 | Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
13 | }
14 | props.onPressIn?.(ev);
15 | }}
16 | />
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/example/components/HelloWave.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { StyleSheet } from 'react-native';
3 | import Animated, {
4 | useSharedValue,
5 | useAnimatedStyle,
6 | withTiming,
7 | withRepeat,
8 | withSequence,
9 | } from 'react-native-reanimated';
10 |
11 | import { ThemedText } from '@/components/ThemedText';
12 |
13 | export function HelloWave() {
14 | const rotationAnimation = useSharedValue(0);
15 |
16 | useEffect(() => {
17 | rotationAnimation.value = withRepeat(
18 | withSequence(withTiming(25, { duration: 150 }), withTiming(0, { duration: 150 })),
19 | 4 // Run the animation 4 times
20 | );
21 | }, []);
22 |
23 | const animatedStyle = useAnimatedStyle(() => ({
24 | transform: [{ rotate: `${rotationAnimation.value}deg` }],
25 | }));
26 |
27 | return (
28 |
29 | 👋
30 |
31 | );
32 | }
33 |
34 | const styles = StyleSheet.create({
35 | text: {
36 | fontSize: 28,
37 | lineHeight: 32,
38 | marginTop: -6,
39 | },
40 | });
41 |
--------------------------------------------------------------------------------
/example/components/ParallaxScrollView.tsx:
--------------------------------------------------------------------------------
1 | import type { PropsWithChildren, ReactElement } from 'react';
2 | import { StyleSheet } from 'react-native';
3 | import Animated, {
4 | interpolate,
5 | useAnimatedRef,
6 | useAnimatedStyle,
7 | useScrollViewOffset,
8 | } from 'react-native-reanimated';
9 |
10 | import { ThemedView } from '@/components/ThemedView';
11 | import { useBottomTabOverflow } from '@/components/ui/TabBarBackground';
12 | import { useColorScheme } from '@/hooks/useColorScheme';
13 |
14 | const HEADER_HEIGHT = 250;
15 |
16 | type Props = PropsWithChildren<{
17 | headerImage: ReactElement;
18 | headerBackgroundColor: { dark: string; light: string };
19 | }>;
20 |
21 | export default function ParallaxScrollView({
22 | children,
23 | headerImage,
24 | headerBackgroundColor,
25 | }: Props) {
26 | const colorScheme = useColorScheme() ?? 'light';
27 | const scrollRef = useAnimatedRef();
28 | const scrollOffset = useScrollViewOffset(scrollRef);
29 | const bottom = useBottomTabOverflow();
30 | const headerAnimatedStyle = useAnimatedStyle(() => {
31 | return {
32 | transform: [
33 | {
34 | translateY: interpolate(
35 | scrollOffset.value,
36 | [-HEADER_HEIGHT, 0, HEADER_HEIGHT],
37 | [-HEADER_HEIGHT / 2, 0, HEADER_HEIGHT * 0.75]
38 | ),
39 | },
40 | {
41 | scale: interpolate(scrollOffset.value, [-HEADER_HEIGHT, 0, HEADER_HEIGHT], [2, 1, 1]),
42 | },
43 | ],
44 | };
45 | });
46 |
47 | return (
48 |
49 |
54 |
60 | {headerImage}
61 |
62 | {children}
63 |
64 |
65 | );
66 | }
67 |
68 | const styles = StyleSheet.create({
69 | container: {
70 | flex: 1,
71 | },
72 | header: {
73 | height: HEADER_HEIGHT,
74 | overflow: 'hidden',
75 | },
76 | content: {
77 | flex: 1,
78 | padding: 32,
79 | gap: 16,
80 | overflow: 'hidden',
81 | },
82 | });
83 |
--------------------------------------------------------------------------------
/example/components/ThemedText.tsx:
--------------------------------------------------------------------------------
1 | import { Text, type TextProps, StyleSheet } from 'react-native';
2 |
3 | import { useThemeColor } from '@/hooks/useThemeColor';
4 |
5 | export type ThemedTextProps = TextProps & {
6 | lightColor?: string;
7 | darkColor?: string;
8 | type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link';
9 | };
10 |
11 | export function ThemedText({
12 | style,
13 | lightColor,
14 | darkColor,
15 | type = 'default',
16 | ...rest
17 | }: ThemedTextProps) {
18 | const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text');
19 |
20 | return (
21 |
33 | );
34 | }
35 |
36 | const styles = StyleSheet.create({
37 | default: {
38 | fontSize: 16,
39 | lineHeight: 24,
40 | },
41 | defaultSemiBold: {
42 | fontSize: 16,
43 | lineHeight: 24,
44 | fontWeight: '600',
45 | },
46 | title: {
47 | fontSize: 32,
48 | fontWeight: 'bold',
49 | lineHeight: 32,
50 | },
51 | subtitle: {
52 | fontSize: 20,
53 | fontWeight: 'bold',
54 | },
55 | link: {
56 | lineHeight: 30,
57 | fontSize: 16,
58 | color: '#0a7ea4',
59 | },
60 | });
61 |
--------------------------------------------------------------------------------
/example/components/ThemedView.tsx:
--------------------------------------------------------------------------------
1 | import { View, type ViewProps } from 'react-native';
2 |
3 | import { useThemeColor } from '@/hooks/useThemeColor';
4 |
5 | export type ThemedViewProps = ViewProps & {
6 | lightColor?: string;
7 | darkColor?: string;
8 | };
9 |
10 | export function ThemedView({ style, lightColor, darkColor, ...otherProps }: ThemedViewProps) {
11 | const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background');
12 |
13 | return ;
14 | }
15 |
--------------------------------------------------------------------------------
/example/components/ui/IconSymbol.ios.tsx:
--------------------------------------------------------------------------------
1 | import { SymbolView, SymbolViewProps, SymbolWeight } from 'expo-symbols';
2 | import { StyleProp, ViewStyle } from 'react-native';
3 |
4 | export function IconSymbol({
5 | name,
6 | size = 24,
7 | color,
8 | style,
9 | weight = 'regular',
10 | }: {
11 | name: SymbolViewProps['name'];
12 | size?: number;
13 | color: string;
14 | style?: StyleProp;
15 | weight?: SymbolWeight;
16 | }) {
17 | return (
18 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/example/components/ui/IconSymbol.tsx:
--------------------------------------------------------------------------------
1 | // This file is a fallback for using MaterialIcons on Android and web.
2 |
3 | import MaterialIcons from '@expo/vector-icons/MaterialIcons';
4 | import { SymbolWeight } from 'expo-symbols';
5 | import React from 'react';
6 | import { OpaqueColorValue, StyleProp, ViewStyle } from 'react-native';
7 |
8 | // Add your SFSymbol to MaterialIcons mappings here.
9 | const MAPPING = {
10 | // See MaterialIcons here: https://icons.expo.fyi
11 | // See SF Symbols in the SF Symbols app on Mac.
12 | 'house.fill': 'home',
13 | 'paperplane.fill': 'send',
14 | 'chevron.left.forwardslash.chevron.right': 'code',
15 | 'chevron.right': 'chevron-right',
16 | } as Partial<
17 | Record<
18 | import('expo-symbols').SymbolViewProps['name'],
19 | React.ComponentProps['name']
20 | >
21 | >;
22 |
23 | export type IconSymbolName = keyof typeof MAPPING;
24 |
25 | /**
26 | * An icon component that uses native SFSymbols on iOS, and MaterialIcons on Android and web. This ensures a consistent look across platforms, and optimal resource usage.
27 | *
28 | * Icon `name`s are based on SFSymbols and require manual mapping to MaterialIcons.
29 | */
30 | export function IconSymbol({
31 | name,
32 | size = 24,
33 | color,
34 | style,
35 | }: {
36 | name: IconSymbolName;
37 | size?: number;
38 | color: string | OpaqueColorValue;
39 | style?: StyleProp;
40 | weight?: SymbolWeight;
41 | }) {
42 | return ;
43 | }
44 |
--------------------------------------------------------------------------------
/example/components/ui/TabBarBackground.ios.tsx:
--------------------------------------------------------------------------------
1 | import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
2 | import { BlurView } from 'expo-blur';
3 | import { StyleSheet } from 'react-native';
4 | import { useSafeAreaInsets } from 'react-native-safe-area-context';
5 |
6 | export default function BlurTabBarBackground() {
7 | return (
8 |
15 | );
16 | }
17 |
18 | export function useBottomTabOverflow() {
19 | const tabHeight = useBottomTabBarHeight();
20 | const { bottom } = useSafeAreaInsets();
21 | return tabHeight - bottom;
22 | }
23 |
--------------------------------------------------------------------------------
/example/components/ui/TabBarBackground.tsx:
--------------------------------------------------------------------------------
1 | // This is a shim for web and Android where the tab bar is generally opaque.
2 | export default undefined;
3 |
4 | export function useBottomTabOverflow() {
5 | return 0;
6 | }
7 |
--------------------------------------------------------------------------------
/example/constants/Colors.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Below are the colors that are used in the app. The colors are defined in the light and dark mode.
3 | * There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc.
4 | */
5 |
6 | const tintColorLight = '#0a7ea4';
7 | const tintColorDark = '#fff';
8 |
9 | export const Colors = {
10 | light: {
11 | text: '#11181C',
12 | background: '#fff',
13 | tint: tintColorLight,
14 | icon: '#687076',
15 | tabIconDefault: '#687076',
16 | tabIconSelected: tintColorLight,
17 | },
18 | dark: {
19 | text: '#ECEDEE',
20 | background: '#151718',
21 | tint: tintColorDark,
22 | icon: '#9BA1A6',
23 | tabIconDefault: '#9BA1A6',
24 | tabIconSelected: tintColorDark,
25 | },
26 | };
27 |
--------------------------------------------------------------------------------
/example/hooks/useColorScheme.ts:
--------------------------------------------------------------------------------
1 | export { useColorScheme } from 'react-native';
2 |
--------------------------------------------------------------------------------
/example/hooks/useColorScheme.web.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { useColorScheme as useRNColorScheme } from 'react-native';
3 |
4 | /**
5 | * To support static rendering, this value needs to be re-calculated on the client side for web
6 | */
7 | export function useColorScheme() {
8 | const [hasHydrated, setHasHydrated] = useState(false);
9 |
10 | useEffect(() => {
11 | setHasHydrated(true);
12 | }, []);
13 |
14 | const colorScheme = useRNColorScheme();
15 |
16 | if (hasHydrated) {
17 | return colorScheme;
18 | }
19 |
20 | return 'light';
21 | }
22 |
--------------------------------------------------------------------------------
/example/hooks/useThemeColor.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Learn more about light and dark modes:
3 | * https://docs.expo.dev/guides/color-schemes/
4 | */
5 |
6 | import { Colors } from '@/constants/Colors';
7 | import { useColorScheme } from '@/hooks/useColorScheme';
8 |
9 | export function useThemeColor(
10 | props: { light?: string; dark?: string },
11 | colorName: keyof typeof Colors.light & keyof typeof Colors.dark
12 | ) {
13 | const theme = useColorScheme() ?? 'light';
14 | const colorFromProps = props[theme];
15 |
16 | if (colorFromProps) {
17 | return colorFromProps;
18 | } else {
19 | return Colors[theme][colorName];
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/example/metro.config.js:
--------------------------------------------------------------------------------
1 | const { getDefaultConfig } = require('expo/metro-config');
2 | const path = require('path');
3 |
4 | const projectRoot = __dirname;
5 | const libraryRoot = path.resolve(projectRoot, '..');
6 |
7 | const config = getDefaultConfig(projectRoot);
8 |
9 | if (config.resolver) {
10 | // 1. Watch all files within the Repository
11 | config.watchFolders = [libraryRoot];
12 | // 2. Let Metro know where to resolve packages, and in what order
13 | config.resolver.nodeModulesPaths = [
14 | path.resolve(projectRoot, 'node_modules'),
15 | path.resolve(libraryRoot, 'node_modules'),
16 | ];
17 | // 3. Force Metro to resolve (sub)dependencies only from the `nodeModulesPaths`
18 | config.resolver.disableHierarchicalLookup = true;
19 | }
20 |
21 | module.exports = config;
22 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-ui-datepicker-example",
3 | "main": "expo-router/entry",
4 | "version": "1.0.0",
5 | "scripts": {
6 | "start": "expo start",
7 | "reset-project": "node ./scripts/reset-project.js",
8 | "android": "expo start --android",
9 | "ios": "expo start --ios",
10 | "web": "expo start --web",
11 | "test": "jest --watchAll",
12 | "lint": "expo lint"
13 | },
14 | "jest": {
15 | "preset": "jest-expo"
16 | },
17 | "dependencies": {
18 | "@expo/vector-icons": "^14.0.2",
19 | "@gorhom/bottom-sheet": "^5",
20 | "@react-navigation/bottom-tabs": "^7.2.0",
21 | "@react-navigation/native": "^7.0.14",
22 | "expo": "~52.0.37",
23 | "expo-blur": "~14.0.3",
24 | "expo-constants": "~17.0.7",
25 | "expo-font": "~13.0.4",
26 | "expo-haptics": "~14.0.1",
27 | "expo-linking": "~7.0.5",
28 | "expo-router": "~4.0.17",
29 | "expo-splash-screen": "~0.29.22",
30 | "expo-status-bar": "~2.0.1",
31 | "expo-symbols": "~0.2.2",
32 | "expo-system-ui": "~4.0.8",
33 | "expo-web-browser": "~14.0.2",
34 | "react": "18.3.1",
35 | "react-dom": "18.3.1",
36 | "react-native": "0.76.7",
37 | "react-native-gesture-handler": "~2.20.2",
38 | "react-native-reanimated": "~3.16.1",
39 | "react-native-safe-area-context": "4.12.0",
40 | "react-native-screens": "~4.4.0",
41 | "react-native-web": "~0.19.13",
42 | "react-native-webview": "13.12.5"
43 | },
44 | "devDependencies": {
45 | "@babel/core": "^7.25.2",
46 | "@types/jest": "^29.5.12",
47 | "@types/react": "~18.3.12",
48 | "@types/react-test-renderer": "^18.3.0",
49 | "jest": "^29.2.1",
50 | "jest-expo": "~52.0.4",
51 | "react-test-renderer": "18.3.1",
52 | "typescript": "^5.3.3"
53 | },
54 | "private": true
55 | }
56 |
--------------------------------------------------------------------------------
/example/scripts/reset-project.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * This script is used to reset the project to a blank state.
5 | * It deletes or moves the /app, /components, /hooks, /scripts, and /constants directories to /app-example based on user input and creates a new /app directory with an index.tsx and _layout.tsx file.
6 | * You can remove the `reset-project` script from package.json and safely delete this file after running it.
7 | */
8 |
9 | const fs = require("fs");
10 | const path = require("path");
11 | const readline = require("readline");
12 |
13 | const root = process.cwd();
14 | const oldDirs = ["app", "components", "hooks", "constants", "scripts"];
15 | const exampleDir = "app-example";
16 | const newAppDir = "app";
17 | const exampleDirPath = path.join(root, exampleDir);
18 |
19 | const indexContent = `import { Text, View } from "react-native";
20 |
21 | export default function Index() {
22 | return (
23 |
30 | Edit app/index.tsx to edit this screen.
31 |
32 | );
33 | }
34 | `;
35 |
36 | const layoutContent = `import { Stack } from "expo-router";
37 |
38 | export default function RootLayout() {
39 | return ;
40 | }
41 | `;
42 |
43 | const rl = readline.createInterface({
44 | input: process.stdin,
45 | output: process.stdout,
46 | });
47 |
48 | const moveDirectories = async (userInput) => {
49 | try {
50 | if (userInput === "y") {
51 | // Create the app-example directory
52 | await fs.promises.mkdir(exampleDirPath, { recursive: true });
53 | console.log(`📁 /${exampleDir} directory created.`);
54 | }
55 |
56 | // Move old directories to new app-example directory or delete them
57 | for (const dir of oldDirs) {
58 | const oldDirPath = path.join(root, dir);
59 | if (fs.existsSync(oldDirPath)) {
60 | if (userInput === "y") {
61 | const newDirPath = path.join(root, exampleDir, dir);
62 | await fs.promises.rename(oldDirPath, newDirPath);
63 | console.log(`➡️ /${dir} moved to /${exampleDir}/${dir}.`);
64 | } else {
65 | await fs.promises.rm(oldDirPath, { recursive: true, force: true });
66 | console.log(`❌ /${dir} deleted.`);
67 | }
68 | } else {
69 | console.log(`➡️ /${dir} does not exist, skipping.`);
70 | }
71 | }
72 |
73 | // Create new /app directory
74 | const newAppDirPath = path.join(root, newAppDir);
75 | await fs.promises.mkdir(newAppDirPath, { recursive: true });
76 | console.log("\n📁 New /app directory created.");
77 |
78 | // Create index.tsx
79 | const indexPath = path.join(newAppDirPath, "index.tsx");
80 | await fs.promises.writeFile(indexPath, indexContent);
81 | console.log("📄 app/index.tsx created.");
82 |
83 | // Create _layout.tsx
84 | const layoutPath = path.join(newAppDirPath, "_layout.tsx");
85 | await fs.promises.writeFile(layoutPath, layoutContent);
86 | console.log("📄 app/_layout.tsx created.");
87 |
88 | console.log("\n✅ Project reset complete. Next steps:");
89 | console.log(
90 | `1. Run \`npx expo start\` to start a development server.\n2. Edit app/index.tsx to edit the main screen.${
91 | userInput === "y"
92 | ? `\n3. Delete the /${exampleDir} directory when you're done referencing it.`
93 | : ""
94 | }`
95 | );
96 | } catch (error) {
97 | console.error(`❌ Error during script execution: ${error.message}`);
98 | }
99 | };
100 |
101 | rl.question(
102 | "Do you want to move existing files to /app-example instead of deleting them? (Y/n): ",
103 | (answer) => {
104 | const userInput = answer.trim().toLowerCase() || "y";
105 | if (userInput === "y" || userInput === "n") {
106 | moveDirectories(userInput).finally(() => rl.close());
107 | } else {
108 | console.log("❌ Invalid input. Please enter 'Y' or 'N'.");
109 | rl.close();
110 | }
111 | }
112 | );
113 |
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo/tsconfig.base",
3 | "compilerOptions": {
4 | "strict": true,
5 | "paths": {
6 | "@/*": ["./*"],
7 | "react-native-ui-datepicker": ["../src"]
8 | }
9 | },
10 | "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'jest';
2 |
3 | const config: Config = {
4 | preset: 'react-native',
5 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
6 | setupFilesAfterEnv: ['./jest.setup.ts'],
7 | modulePathIgnorePatterns: [
8 | './demo/node_modules',
9 | './example/node_modules',
10 | './lib/',
11 | ],
12 | };
13 |
14 | export default config;
15 |
--------------------------------------------------------------------------------
/jest.setup.ts:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-native/extend-expect';
2 |
--------------------------------------------------------------------------------
/lefthook.yml:
--------------------------------------------------------------------------------
1 | pre-commit:
2 | parallel: true
3 | commands:
4 | lint:
5 | glob: "*.{js,ts,jsx,tsx}"
6 | run: npx eslint {files}
7 | types:
8 | glob: "*.{js,ts,jsx,tsx}"
9 | run: npx tsc --noEmit
10 | commit-msg:
11 | parallel: true
12 | commands:
13 | commitlint:
14 | run: npx commitlint --edit
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-ui-datepicker",
3 | "version": "3.1.2",
4 | "description": "Customizable date picker 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 | "exports": {
11 | "import": "./lib/module/index.js",
12 | "require": "./lib/commonjs/index.js",
13 | "types": "./lib/typescript/index.d.ts"
14 | },
15 | "files": [
16 | "src",
17 | "lib",
18 | "android",
19 | "ios",
20 | "cpp",
21 | "*.podspec",
22 | "!lib/typescript/example",
23 | "!ios/build",
24 | "!android/build",
25 | "!android/gradle",
26 | "!android/gradlew",
27 | "!android/gradlew.bat",
28 | "!android/local.properties",
29 | "!**/__tests__",
30 | "!**/__fixtures__",
31 | "!**/__mocks__",
32 | "!**/.*"
33 | ],
34 | "scripts": {
35 | "test": "jest",
36 | "typecheck": "npx tsc --noEmit",
37 | "lint": "eslint .",
38 | "prepack": "bob build",
39 | "release": "release-it",
40 | "example": "yarn --cwd example",
41 | "bootstrap": "yarn example && yarn install"
42 | },
43 | "keywords": [
44 | "react-native",
45 | "ios",
46 | "android",
47 | "react-native-ui-datetpicker",
48 | "react-native-datetime",
49 | "react-native-datetime-picker",
50 | "react-native-datetpicker",
51 | "react-native-datet-picker",
52 | "react-native-timepicker",
53 | "react-native-time-picker",
54 | "react-native-calendar",
55 | "react-native-daterange",
56 | "datetime",
57 | "datetime-picker",
58 | "datepicker",
59 | "datet-picker",
60 | "timepicker",
61 | "time-picker",
62 | "calendar",
63 | "daterange"
64 | ],
65 | "repository": "https://github.com/farhoudshapouran/react-native-ui-datepicker",
66 | "author": "Farhoud Shapouran (https://github.com/farhoudshapouran)",
67 | "license": "MIT",
68 | "bugs": {
69 | "url": "https://github.com/farhoudshapouran/react-native-ui-datepicker/issues"
70 | },
71 | "homepage": "https://github.com/farhoudshapouran/react-native-ui-datepicker#readme",
72 | "publishConfig": {
73 | "registry": "https://registry.npmjs.org/"
74 | },
75 | "devDependencies": {
76 | "@commitlint/config-conventional": "^17.0.2",
77 | "@evilmartians/lefthook": "^1.2.2",
78 | "@react-native-community/eslint-config": "^3.0.2",
79 | "@react-native/eslint-config": "^0.77.0",
80 | "@release-it/conventional-changelog": "^5.0.0",
81 | "@testing-library/jest-native": "^5.4.3",
82 | "@testing-library/react-native": "^12.3.2",
83 | "@types/jest": "^29.5.14",
84 | "@types/lodash": "^4.14.202",
85 | "@types/react": "18.2.0",
86 | "@types/react-native": "0.72.0",
87 | "@types/react-test-renderer": "^18.0.0",
88 | "@typescript-eslint/parser": "^8.24.1",
89 | "commitlint": "^17.0.2",
90 | "del-cli": "^5.0.0",
91 | "eslint": "^9.20.1",
92 | "eslint-config-prettier": "^10.0.1",
93 | "eslint-plugin-prettier": "^5.2.3",
94 | "eslint-plugin-react-hooks": "^5.1.0",
95 | "eslint-plugin-react-native": "^5.0.0",
96 | "jest": "^28.1.3",
97 | "pod-install": "^0.1.0",
98 | "prettier": "^3.4.2",
99 | "prettier-plugin-tailwindcss": "^0.6.11",
100 | "react": "18.2.0",
101 | "react-native": "0.72.0",
102 | "react-native-builder-bob": "^0.20.0",
103 | "react-test-renderer": "18.2.0",
104 | "ts-node": "^10.9.1",
105 | "typescript": "^5.7.3"
106 | },
107 | "resolutions": {
108 | "@types/react": "18.2.0",
109 | "@types/react-native": "0.72.0"
110 | },
111 | "peerDependencies": {
112 | "react": "*",
113 | "react-native": "*"
114 | },
115 | "engines": {
116 | "node": ">= 18"
117 | },
118 | "commitlint": {
119 | "extends": [
120 | "@commitlint/config-conventional"
121 | ]
122 | },
123 | "eslintConfig": {
124 | "root": true,
125 | "extends": [
126 | "@react-native-community",
127 | "prettier"
128 | ],
129 | "plugins": [
130 | "prettier"
131 | ],
132 | "rules": {
133 | "prettier/prettier": [
134 | "error",
135 | {
136 | "quoteProps": "consistent",
137 | "trailingComma": "es5",
138 | "tabWidth": 2,
139 | "semi": true,
140 | "singleQuote": true,
141 | "useTabs": false
142 | }
143 | ]
144 | }
145 | },
146 | "eslintIgnore": [
147 | "node_modules/",
148 | "lib/",
149 | "example/",
150 | "demo/",
151 | "*.config.js",
152 | "tsconfig.json"
153 | ],
154 | "react-native-builder-bob": {
155 | "source": "src",
156 | "output": "lib",
157 | "targets": [
158 | "commonjs",
159 | "module",
160 | [
161 | "typescript",
162 | {
163 | "project": "tsconfig.build.json"
164 | }
165 | ]
166 | ]
167 | },
168 | "dependencies": {
169 | "clsx": "^2.1.1",
170 | "dayjs": "^1.11.13",
171 | "jalali-plugin-dayjs": "^1.1.4",
172 | "lodash": "^4.17.21",
173 | "tailwind-merge": "^3.0.1"
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/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/__tests__/api.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen } from '@testing-library/react-native';
3 | import DateTimePicker from '../datetime-picker';
4 | //import dayjs from 'dayjs';
5 | import 'dayjs/locale/en';
6 | import 'dayjs/locale/de';
7 | import 'dayjs/locale/es';
8 | import 'dayjs/locale/fr';
9 | import 'dayjs/locale/tr';
10 |
11 | describe('API TESTS', () => {
12 | test('should display the passed date', () => {
13 | const selectedDate = new Date(2020, 11, 19);
14 | const month = selectedDate.toLocaleString('en-US', { month: 'long' });
15 |
16 | render();
17 | expect(screen.getByText(month)).toBeVisible();
18 | expect(screen.getByText('19')).toBeVisible();
19 | expect(screen.getByText('2020')).toBeVisible();
20 | });
21 |
22 | // test('minDate should be applied after init', () => {
23 | // const minDate = new Date();
24 |
25 | // render();
26 | // expect(
27 | // screen.getByTestId(dayjs(minDate).add(-1, 'day').format('YYYY/MM/DD'))
28 | // ).toBeDisabled();
29 | // });
30 | });
31 |
--------------------------------------------------------------------------------
/src/__tests__/common.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen } from '@testing-library/react-native';
3 | import DateTimePicker from '../datetime-picker';
4 |
5 | describe('COMMON TESTS', () => {
6 | test('should render with default options', () => {
7 | render();
8 | expect(screen.toJSON()).toMatchSnapshot();
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/src/assets/images/arrow_left.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/farhoudshapouran/react-native-ui-datepicker/9b317eba399144944ac5235e2d151036456c5786/src/assets/images/arrow_left.png
--------------------------------------------------------------------------------
/src/assets/images/arrow_right.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/farhoudshapouran/react-native-ui-datepicker/9b317eba399144944ac5235e2d151036456c5786/src/assets/images/arrow_right.png
--------------------------------------------------------------------------------
/src/calendar-context.ts:
--------------------------------------------------------------------------------
1 | import { createContext, useContext } from 'react';
2 | import { CalendarViews } from './enums';
3 | import type { DateType, DatePickerBaseProps } from './types';
4 |
5 | export interface CalendarContextType extends DatePickerBaseProps {
6 | locale: string;
7 | showOutsideDays: boolean;
8 | firstDayOfWeek: number;
9 | calendarView: CalendarViews;
10 | currentDate: DateType; // used for latest state of calendar based on Month and Year
11 | currentYear: number;
12 | isRTL: boolean;
13 | setCalendarView: (value: CalendarViews) => void;
14 | onSelectDate: (date: DateType) => void;
15 | onSelectMonth: (month: number) => void;
16 | onSelectYear: (year: number) => void;
17 | onChangeMonth: (value: number) => void;
18 | onChangeYear: (value: number) => void;
19 | }
20 |
21 | export const CalendarContext = createContext({} as CalendarContextType);
22 |
23 | export const useCalendarContext = () => useContext(CalendarContext);
24 |
--------------------------------------------------------------------------------
/src/components/calendar.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode, useMemo } from 'react';
2 | import { View, ViewStyle } from 'react-native';
3 | import { useCalendarContext } from '../calendar-context';
4 | import type { CalendarViews } from '../enums';
5 | import Header from './header';
6 | import Years from './years';
7 | import Months from './months';
8 | import Days from './days';
9 | import TimePicker from './time-picker';
10 |
11 | const CalendarView: Record = {
12 | year: ,
13 | month: ,
14 | day: ,
15 | time: ,
16 | };
17 |
18 | const Calendar = () => {
19 | const {
20 | hideHeader,
21 | calendarView,
22 | style = {},
23 | className = '',
24 | styles = {},
25 | classNames = {},
26 | containerHeight,
27 | navigationPosition,
28 | isRTL,
29 | } = useCalendarContext();
30 |
31 | const containerStyle: ViewStyle = useMemo(
32 | () => ({
33 | height: containerHeight,
34 | }),
35 | [containerHeight]
36 | );
37 |
38 | return (
39 |
40 | {!hideHeader ? (
41 |
47 | ) : null}
48 | {CalendarView[calendarView]}
49 |
50 | );
51 | };
52 |
53 | export default Calendar;
54 |
--------------------------------------------------------------------------------
/src/components/day.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useMemo } from 'react';
2 | import { View, Pressable, StyleSheet, Text } from 'react-native';
3 | import {
4 | ClassNames,
5 | CalendarDay,
6 | Styles,
7 | CalendarComponents,
8 | DateType,
9 | } from '../types';
10 | import { CONTAINER_HEIGHT, WEEKDAYS_HEIGHT } from '../enums';
11 | import { cn } from '../utils';
12 | import { isEqual } from 'lodash';
13 |
14 | interface Props {
15 | day: CalendarDay;
16 | onSelectDate: (date: DateType) => void;
17 | containerHeight?: number;
18 | weekdaysHeight?: number;
19 | styles?: Styles;
20 | classNames?: ClassNames;
21 | components?: CalendarComponents;
22 | }
23 |
24 | export const EmptyDay = React.memo(() => {
25 | return ;
26 | });
27 |
28 | const Day = ({
29 | day,
30 | onSelectDate,
31 | containerHeight = CONTAINER_HEIGHT,
32 | weekdaysHeight = WEEKDAYS_HEIGHT,
33 | styles = {},
34 | classNames = {},
35 | components = {},
36 | }: Props) => {
37 | const style = useMemo(
38 | () => createDefaultStyles(containerHeight, weekdaysHeight),
39 | [containerHeight, weekdaysHeight]
40 | );
41 |
42 | const {
43 | text,
44 | date,
45 | isDisabled,
46 | isCurrentMonth,
47 | isToday,
48 | isSelected,
49 | inRange,
50 | leftCrop,
51 | rightCrop,
52 | isStartOfWeek,
53 | isEndOfWeek,
54 | isCrop,
55 | inMiddle,
56 | rangeStart,
57 | rangeEnd,
58 | } = day;
59 |
60 | const containerStyle = StyleSheet.flatten([
61 | defaultStyles.dayContainer,
62 | styles.day,
63 | isToday && styles.today,
64 | !isCurrentMonth && styles.outside,
65 | isSelected && styles.selected,
66 | isDisabled && styles.disabled,
67 | inMiddle && styles.range_middle,
68 | rangeStart && styles.range_start,
69 | rangeEnd && styles.range_end,
70 | ]);
71 |
72 | const textStyle = StyleSheet.flatten([
73 | styles.day_label,
74 | isToday && styles.today_label,
75 | !isCurrentMonth && styles.outside_label,
76 | isSelected && styles.selected_label,
77 | isDisabled && styles.disabled_label,
78 | inMiddle && styles.range_middle_label,
79 | rangeStart && styles.range_start_label,
80 | rangeEnd && styles.range_end_label,
81 | ]);
82 |
83 | const containerClassName = cn(
84 | classNames.day,
85 | isToday && classNames.today,
86 | !isCurrentMonth && classNames.outside,
87 | isSelected && classNames.selected,
88 | isDisabled && classNames.disabled,
89 | inMiddle && classNames.range_middle,
90 | rangeStart && classNames.range_start,
91 | rangeEnd && classNames.range_end
92 | );
93 |
94 | const textClassName = cn(
95 | classNames.day_label,
96 | isToday && classNames.today_label,
97 | !isCurrentMonth && classNames.outside_label,
98 | isSelected && classNames.selected_label,
99 | isDisabled && classNames.disabled_label,
100 | inMiddle && classNames.range_middle_label,
101 | rangeStart && classNames.range_start_label,
102 | rangeEnd && classNames.range_end_label
103 | );
104 |
105 | const RangeFill = useMemo(() => {
106 | if (!inRange) return null;
107 | if (!isCrop) {
108 | return (
109 |
122 | );
123 | }
124 | return (
125 | <>
126 | {leftCrop && (
127 |
135 | )}
136 | {rightCrop && (
137 |
145 | )}
146 | >
147 | );
148 | }, [
149 | inRange,
150 | isCrop,
151 | leftCrop,
152 | rightCrop,
153 | defaultStyles.rangeRoot,
154 | styles.range_fill,
155 | styles.range_fill_weekstart,
156 | styles.range_fill_weekend,
157 | classNames.range_fill,
158 | classNames.range_fill_weekstart,
159 | classNames.range_fill_weekend,
160 | ]);
161 |
162 | return (
163 |
164 |
168 | {RangeFill}
169 | {components.Day ? (
170 | onSelectDate(date)}
173 | accessibilityRole="button"
174 | accessibilityLabel={text}
175 | style={containerStyle}
176 | >
177 | {components.Day(day)}
178 |
179 | ) : (
180 | onSelectDate(date)}
183 | accessibilityRole="button"
184 | accessibilityLabel={text}
185 | style={containerStyle}
186 | className={containerClassName}
187 | >
188 |
189 | {text}
190 |
191 |
192 | )}
193 |
194 |
195 | );
196 | };
197 |
198 | const defaultStyles = StyleSheet.create({
199 | dayWrapper: {
200 | width: `${99.9 / 7}%`,
201 | },
202 | dayContainer: { flex: 1, justifyContent: 'center', alignItems: 'center' },
203 | rangeWrapper: {
204 | flex: 1,
205 | },
206 | rangeRoot: {
207 | position: 'absolute',
208 | top: 0,
209 | left: 0,
210 | right: 0,
211 | bottom: 0,
212 | },
213 | leftCrop: { left: '50%' },
214 | rightCrop: { right: '50%' },
215 | });
216 |
217 | const createDefaultStyles = (containerHeight: number, weekdaysHeight: number) =>
218 | StyleSheet.create({
219 | dayCell: {
220 | minHeight: (containerHeight - weekdaysHeight) / 6,
221 | },
222 | });
223 |
224 | const customComparator = (prev: Readonly, next: Readonly) => {
225 | const areEqual =
226 | isEqual(prev.day, next.day) &&
227 | prev.onSelectDate === next.onSelectDate &&
228 | prev.containerHeight === next.containerHeight &&
229 | isEqual(prev.styles, next.styles) &&
230 | isEqual(prev.classNames, next.classNames) &&
231 | isEqual(prev.components, next.components);
232 |
233 | return areEqual;
234 | };
235 |
236 | export default memo(Day, customComparator);
237 |
--------------------------------------------------------------------------------
/src/components/header/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useMemo } from 'react';
2 | import { View, StyleSheet } from 'react-native';
3 | import type { HeaderProps, NavigationProps } from './types';
4 | import PrevButton from './prev-button';
5 | import NextButton from './next-button';
6 | import Selectors from './selectors';
7 | import { isEqual } from 'lodash';
8 |
9 | const createDefaultStyles = (isRTL: boolean) =>
10 | StyleSheet.create({
11 | headerContainer: {
12 | paddingVertical: 3,
13 | },
14 | container: {
15 | padding: 5,
16 | gap: 20,
17 | flexDirection: isRTL ? 'row-reverse' : 'row',
18 | alignItems: 'center',
19 | justifyContent: 'space-between',
20 | },
21 | navigation: {
22 | flexDirection: isRTL ? 'row-reverse' : 'row',
23 | },
24 | });
25 |
26 | const NavigationButtons = ({ styles, classNames, isRTL }: NavigationProps) => {
27 | const style = useMemo(() => createDefaultStyles(isRTL), [isRTL]);
28 |
29 | return (
30 |
31 |
37 |
43 |
44 | );
45 | };
46 |
47 | const Header = ({
48 | navigationPosition = 'around',
49 | styles = {},
50 | classNames = {},
51 | isRTL,
52 | }: HeaderProps) => {
53 | const style = useMemo(() => createDefaultStyles(isRTL), [isRTL]);
54 |
55 | return (
56 |
60 |
61 | {navigationPosition === 'left' ? (
62 | <>
63 |
68 |
69 | >
70 | ) : navigationPosition === 'right' ? (
71 | <>
72 |
73 |
78 | >
79 | ) : (
80 | <>
81 |
87 |
88 |
94 | >
95 | )}
96 |
97 |
98 | );
99 | };
100 |
101 | const customComparator = (
102 | prev: Readonly,
103 | next: Readonly
104 | ) => {
105 | const areEqual =
106 | prev.PrevIcon === next.PrevIcon &&
107 | prev.NextIcon === next.NextIcon &&
108 | prev.navigationPosition === next.navigationPosition &&
109 | prev.isRTL === next.isRTL &&
110 | isEqual(prev.styles, next.styles) &&
111 | isEqual(prev.classNames, next.classNames);
112 |
113 | return areEqual;
114 | };
115 |
116 | export default memo(Header, customComparator);
117 |
--------------------------------------------------------------------------------
/src/components/header/month-button.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react';
2 | import dayjs from 'dayjs';
3 | import { Pressable, Text, View } from 'react-native';
4 | import { useCalendarContext } from '../../calendar-context';
5 | import { isValidJalaliLocale } from '../../utils';
6 |
7 | const MonthButton = () => {
8 | const {
9 | currentDate,
10 | calendarView,
11 | setCalendarView,
12 | calendar = 'gregory',
13 | locale,
14 | styles,
15 | classNames,
16 | disableMonthPicker,
17 | monthCaptionFormat,
18 | } = useCalendarContext();
19 |
20 | const currentMonthText = dayjs(currentDate)
21 | .calendar(calendar)
22 | .locale(
23 | calendar === 'jalali' && !isValidJalaliLocale(locale) ? 'en' : locale
24 | )
25 | .format(monthCaptionFormat === 'full' ? 'MMMM' : 'MMM');
26 |
27 | return (
28 |
31 | setCalendarView(calendarView === 'month' ? 'day' : 'month')
32 | }
33 | testID="btn-month"
34 | accessibilityRole="button"
35 | accessibilityLabel={currentMonthText}
36 | >
37 |
41 |
45 | {currentMonthText}
46 |
47 |
48 |
49 | );
50 | };
51 |
52 | export default memo(MonthButton);
53 |
--------------------------------------------------------------------------------
/src/components/header/next-button.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useCallback, useMemo } from 'react';
2 | import {
3 | Image,
4 | ImageStyle,
5 | Pressable,
6 | StyleSheet,
7 | useColorScheme,
8 | View,
9 | } from 'react-native';
10 | import { useCalendarContext } from '../../calendar-context';
11 | import { YEAR_PAGE_SIZE } from '../../utils';
12 | import { ClassNames, Styles } from '../../types';
13 | import { UI } from '../../ui';
14 | import { isEqual } from 'lodash';
15 | import { COLORS } from '../../theme';
16 |
17 | const arrow_right = require('../../assets/images/arrow_right.png');
18 |
19 | type NextButtonProps = {
20 | style?: Styles[UI.button_next];
21 | imageStyle?: Styles[UI.button_next_image];
22 | className?: ClassNames[UI.button_next];
23 | imageClassName?: ClassNames[UI.button_next_image];
24 | };
25 |
26 | const NextButton = ({
27 | style,
28 | imageStyle,
29 | className,
30 | imageClassName,
31 | }: NextButtonProps) => {
32 | const {
33 | currentYear,
34 | onChangeMonth,
35 | onChangeYear,
36 | calendarView,
37 | components = {},
38 | isRTL,
39 | } = useCalendarContext();
40 |
41 | const colorScheme = useColorScheme();
42 | const theme = colorScheme ?? 'light';
43 | const defaultStyles = useMemo(() => createDefaultStyles(isRTL), [isRTL]);
44 |
45 | const onPress = useCallback(() => {
46 | switch (calendarView) {
47 | case 'day':
48 | return onChangeMonth(1);
49 | case 'month':
50 | return onChangeYear(currentYear + 1);
51 | case 'year':
52 | return onChangeYear(currentYear + YEAR_PAGE_SIZE);
53 | default:
54 | return {};
55 | }
56 | }, [calendarView, currentYear, onChangeMonth, onChangeYear]);
57 |
58 | const iconStyle: ImageStyle = useMemo(
59 | () => ({
60 | ...defaultStyles.icon,
61 | tintColor: COLORS[theme].foreground,
62 | ...(imageStyle as ImageStyle),
63 | }),
64 | [imageStyle, theme, defaultStyles.icon]
65 | );
66 |
67 | return (
68 |
75 |
79 | {components.IconNext || (
80 |
85 | )}
86 |
87 |
88 | );
89 | };
90 |
91 | const customComparator = (
92 | prev: Readonly,
93 | next: Readonly
94 | ) => {
95 | const areEqual =
96 | prev.className === next.className &&
97 | isEqual(prev.style, next.style) &&
98 | isEqual(prev.imageStyle, next.imageStyle) &&
99 | isEqual(prev.imageClassName, next.imageClassName);
100 |
101 | return areEqual;
102 | };
103 |
104 | export default memo(NextButton, customComparator);
105 |
106 | const createDefaultStyles = (isRTL: boolean) =>
107 | StyleSheet.create({
108 | iconContainer: {
109 | padding: 4,
110 | },
111 | next: {
112 | marginLeft: isRTL ? 0 : 3,
113 | marginRight: isRTL ? 3 : 0,
114 | },
115 | icon: {
116 | width: 14,
117 | height: 14,
118 | transform: [{ rotate: isRTL ? '180deg' : '0deg' }],
119 | },
120 | });
121 |
--------------------------------------------------------------------------------
/src/components/header/prev-button.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useCallback, useMemo } from 'react';
2 | import {
3 | Image,
4 | ImageStyle,
5 | Pressable,
6 | StyleSheet,
7 | useColorScheme,
8 | View,
9 | } from 'react-native';
10 | import { useCalendarContext } from '../../calendar-context';
11 | import { YEAR_PAGE_SIZE } from '../../utils';
12 | import { ClassNames, Styles } from '../../types';
13 | import { UI } from '../../ui';
14 | import { isEqual } from 'lodash';
15 | import { COLORS } from '../../theme';
16 |
17 | const arrow_left = require('../../assets/images/arrow_left.png');
18 |
19 | type PrevButtonProps = {
20 | style?: Styles[UI.button_prev];
21 | imageStyle?: Styles[UI.button_prev_image];
22 | className?: ClassNames[UI.button_prev];
23 | imageClassName?: ClassNames[UI.button_prev_image];
24 | };
25 |
26 | const PrevButton = ({
27 | style,
28 | imageStyle,
29 | className,
30 | imageClassName,
31 | }: PrevButtonProps) => {
32 | const {
33 | currentYear,
34 | calendarView,
35 | onChangeMonth,
36 | onChangeYear,
37 | components = {},
38 | isRTL,
39 | } = useCalendarContext();
40 |
41 | const colorScheme = useColorScheme();
42 | const theme = colorScheme ?? 'light';
43 | const defaultStyles = useMemo(() => createDefaultStyles(isRTL), [isRTL]);
44 |
45 | const onPress = useCallback(() => {
46 | switch (calendarView) {
47 | case 'day':
48 | return onChangeMonth(-1);
49 | case 'month':
50 | return onChangeYear(currentYear - 1);
51 | case 'year':
52 | return onChangeYear(currentYear - YEAR_PAGE_SIZE);
53 | default:
54 | return {};
55 | }
56 | }, [calendarView, currentYear, onChangeMonth, onChangeYear]);
57 |
58 | const iconStyle: ImageStyle = useMemo(
59 | () => ({
60 | ...defaultStyles.icon,
61 | tintColor: COLORS[theme].foreground,
62 | ...(imageStyle as ImageStyle),
63 | }),
64 | [imageStyle, theme, defaultStyles.icon]
65 | );
66 |
67 | return (
68 |
75 |
79 | {components.IconPrev || (
80 |
85 | )}
86 |
87 |
88 | );
89 | };
90 |
91 | const customComparator = (
92 | prev: Readonly,
93 | next: Readonly
94 | ) => {
95 | const areEqual =
96 | prev.className === next.className &&
97 | isEqual(prev.style, next.style) &&
98 | isEqual(prev.imageStyle, next.imageStyle) &&
99 | isEqual(prev.imageClassName, next.imageClassName);
100 |
101 | return areEqual;
102 | };
103 |
104 | export default memo(PrevButton, customComparator);
105 |
106 | const createDefaultStyles = (isRTL: boolean) =>
107 | StyleSheet.create({
108 | iconContainer: {
109 | padding: 4,
110 | },
111 | prev: {
112 | marginRight: isRTL ? 0 : 3,
113 | marginLeft: isRTL ? 3 : 0,
114 | },
115 | icon: {
116 | width: 14,
117 | height: 14,
118 | transform: [{ rotate: isRTL ? '180deg' : '0deg' }],
119 | },
120 | });
121 |
--------------------------------------------------------------------------------
/src/components/header/selectors.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react';
2 | import { StyleSheet, View } from 'react-native';
3 | import { useCalendarContext } from '../../calendar-context';
4 | import MonthButton from './month-button';
5 | import YearButton from './year-button';
6 | import { TimeButton } from './time-button';
7 | import { NavigationPosition } from '../../types';
8 |
9 | type Props = {
10 | position: NavigationPosition;
11 | };
12 |
13 | const Selectors = ({ position }: Props) => {
14 | const { mode, calendarView, timePicker } = useCalendarContext();
15 |
16 | return (
17 |
29 |
30 | {calendarView !== 'year' ? : null}
31 |
32 |
33 | {timePicker && mode === 'single' && calendarView !== 'year' ? (
34 |
35 | ) : null}
36 |
37 | );
38 | };
39 |
40 | export default memo(Selectors);
41 |
42 | const defaultStyles = StyleSheet.create({
43 | container: {
44 | flex: 1,
45 | flexDirection: 'row',
46 | alignItems: 'center',
47 | },
48 | monthAndYear: {
49 | gap: 5,
50 | flexDirection: 'row',
51 | alignItems: 'center',
52 | },
53 | });
54 |
--------------------------------------------------------------------------------
/src/components/header/time-button.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import dayjs from 'dayjs';
3 | import { useMemo } from 'react';
4 | import { Pressable, Text, View } from 'react-native';
5 | import { useCalendarContext } from '../../calendar-context';
6 | import { formatNumber, getParsedDate } from '../../utils';
7 |
8 | export const TimeButton = () => {
9 | const {
10 | currentDate,
11 | date,
12 | calendarView,
13 | setCalendarView,
14 | styles,
15 | classNames,
16 | numerals = 'latn',
17 | use12Hours,
18 | } = useCalendarContext();
19 |
20 | const { hour, hour12, minute, period } = useMemo(
21 | () => getParsedDate(date || currentDate),
22 | [date, currentDate]
23 | );
24 |
25 | const labelText = useMemo(() => {
26 | const hourValue = use12Hours ? hour12 : hour;
27 |
28 | const hourLabel =
29 | hourValue < 10
30 | ? `${formatNumber(0, numerals)}${formatNumber(hourValue, numerals)}`
31 | : `${formatNumber(hourValue, numerals)}`;
32 |
33 | const minuteLabel =
34 | minute < 10
35 | ? `${formatNumber(0, numerals)}${formatNumber(minute, numerals)}`
36 | : `${formatNumber(minute, numerals)}`;
37 |
38 | return `${hourLabel}:${minuteLabel} ${use12Hours ? period : ''}`.trim();
39 | }, [numerals, hour, hour12, minute, use12Hours, period]);
40 |
41 | return (
42 | setCalendarView(calendarView === 'time' ? 'day' : 'time')}
44 | accessibilityRole="button"
45 | accessibilityLabel={dayjs(date || currentDate).format('HH:mm')}
46 | >
47 |
48 |
52 | {labelText}
53 |
54 |
55 |
56 | );
57 | };
58 |
--------------------------------------------------------------------------------
/src/components/header/types.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ClassNames, Styles, NavigationPosition } from '../../types';
3 |
4 | export type HeaderProps = {
5 | PrevIcon?: React.ReactNode;
6 | NextIcon?: React.ReactNode;
7 | navigationPosition?: NavigationPosition;
8 | styles?: Styles;
9 | classNames?: ClassNames;
10 | isRTL: boolean;
11 | };
12 |
13 | export type NavigationProps = {
14 | styles?: Styles;
15 | classNames?: ClassNames;
16 | isRTL: boolean;
17 | };
18 |
--------------------------------------------------------------------------------
/src/components/header/year-button.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react';
2 | import { Pressable, StyleSheet, Text, View } from 'react-native';
3 | import { useCalendarContext } from '../../calendar-context';
4 | import { formatNumber, getDateYear, getYearRange } from '../../utils';
5 | import dayjs from 'dayjs';
6 |
7 | const YearButton = () => {
8 | const {
9 | currentDate,
10 | calendarView,
11 | setCalendarView,
12 | currentYear,
13 | onChangeYear,
14 | styles,
15 | classNames,
16 | disableYearPicker,
17 | calendar = 'gregory',
18 | numerals = 'latn',
19 | } = useCalendarContext();
20 |
21 | const years = getYearRange(currentYear);
22 | return (
23 | {
26 | setCalendarView(calendarView === 'year' ? 'day' : 'year');
27 | onChangeYear(getDateYear(currentDate));
28 | }}
29 | testID="btn-year"
30 | accessibilityRole="button"
31 | accessibilityLabel={dayjs(currentDate).calendar(calendar).format('YYYY')}
32 | >
33 |
37 |
41 | {calendarView === 'year'
42 | ? `${formatNumber(years[0] || 0, numerals)} - ${formatNumber(years[years.length - 1] || 0, numerals)}`
43 | : formatNumber(
44 | parseInt(dayjs(currentDate).calendar(calendar).format('YYYY')),
45 | numerals
46 | )}
47 |
48 |
49 |
50 | );
51 | };
52 |
53 | export default memo(YearButton);
54 |
55 | const defaultStyles = StyleSheet.create({
56 | container: {
57 | alignItems: 'center',
58 | justifyContent: 'center',
59 | },
60 | });
61 |
--------------------------------------------------------------------------------
/src/components/months.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react';
2 | import { View, StyleSheet, Pressable, Text } from 'react-native';
3 | import { useCalendarContext } from '../calendar-context';
4 | import { getParsedDate, getMonthsArray, cn, isMonthDisabled } from '../utils';
5 | import { CONTAINER_HEIGHT } from '../enums';
6 |
7 | const Months = () => {
8 | const {
9 | currentDate,
10 | onSelectMonth,
11 | styles = {},
12 | classNames = {},
13 | components = {},
14 | containerHeight = CONTAINER_HEIGHT,
15 | monthsFormat = 'full',
16 | minDate,
17 | maxDate,
18 | calendar = 'gregory',
19 | locale,
20 | isRTL,
21 | } = useCalendarContext();
22 |
23 | const style = useMemo(
24 | () => createDefaultStyles(containerHeight, isRTL),
25 | [containerHeight, isRTL]
26 | );
27 |
28 | const { month } = getParsedDate(currentDate);
29 |
30 | const containerStyle = StyleSheet.flatten([style.container, styles?.months]);
31 |
32 | return (
33 |
34 |
35 | {getMonthsArray({ calendar, locale }).map((item, index) => {
36 | const isSelected = index === month;
37 |
38 | const isDisabled = isMonthDisabled(index, currentDate, {
39 | minDate,
40 | maxDate,
41 | });
42 |
43 | const itemStyle = StyleSheet.flatten([
44 | style.month,
45 | styles.month,
46 | isSelected && styles.selected_month,
47 | isDisabled && styles.disabled,
48 | ]);
49 |
50 | const textStyle = StyleSheet.flatten([
51 | styles.month_label,
52 | isSelected && styles.selected_month_label,
53 | isDisabled && styles.disabled_label,
54 | ]);
55 |
56 | const containerClassName = cn(
57 | classNames.month,
58 | isSelected && classNames.selected_month,
59 | isDisabled && classNames.disabled
60 | );
61 |
62 | const textClassName = cn(
63 | classNames.month_label,
64 | isSelected && classNames.selected_month_label,
65 | isDisabled && classNames.disabled_label
66 | );
67 |
68 | return (
69 |
70 | {components.Month ? (
71 | onSelectMonth(index)}
74 | accessibilityRole="button"
75 | accessibilityLabel={item.name.full}
76 | style={style.month}
77 | >
78 | {components.Month({ ...item, isSelected })}
79 |
80 | ) : (
81 | onSelectMonth(index)}
84 | accessibilityRole="button"
85 | accessibilityLabel={item.name.full}
86 | style={itemStyle}
87 | className={containerClassName}
88 | >
89 |
90 | {item.name[monthsFormat]}
91 |
92 |
93 | )}
94 |
95 | );
96 | })}
97 |
98 |
99 | );
100 | };
101 |
102 | const createDefaultStyles = (containerHeight: number, isRTL: boolean) =>
103 | StyleSheet.create({
104 | container: {
105 | flex: 1,
106 | alignItems: 'center',
107 | justifyContent: 'center',
108 | },
109 | months: {
110 | flexDirection: isRTL ? 'row-reverse' : 'row',
111 | flexWrap: 'wrap',
112 | },
113 | month: {
114 | flex: 1,
115 | margin: 2,
116 | alignItems: 'center',
117 | justifyContent: 'center',
118 | },
119 | monthCell: {
120 | width: `${99.9 / 3}%`,
121 | height: containerHeight / 6,
122 | },
123 | });
124 |
125 | export default Months;
126 |
--------------------------------------------------------------------------------
/src/components/time-picker.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useMemo } from 'react';
2 | import {
3 | View,
4 | StyleSheet,
5 | ViewStyle,
6 | TextStyle,
7 | ScrollView,
8 | Text,
9 | I18nManager,
10 | } from 'react-native';
11 | import { useCalendarContext } from '../calendar-context';
12 | import Wheel from './time-picker/wheel';
13 | import { CONTAINER_HEIGHT } from '../enums';
14 | import { getParsedDate, formatNumber } from '../utils';
15 | import { Numerals, PickerOption } from '../types';
16 | import dayjs from 'dayjs';
17 | import PeriodPicker from './time-picker/period-picker';
18 |
19 | export type Period = 'AM' | 'PM';
20 |
21 | const createNumberList = (
22 | num: number,
23 | numerals: Numerals,
24 | startFrom: number = 0
25 | ): PickerOption[] => {
26 | return Array.from({ length: num }, (_, i) => ({
27 | value: i + startFrom,
28 | text:
29 | i + startFrom < 10
30 | ? `${formatNumber(0, numerals)}${formatNumber(i + startFrom, numerals)}`
31 | : `${formatNumber(i + startFrom, numerals)}`,
32 | }));
33 | };
34 |
35 | const TimePicker = () => {
36 | const {
37 | currentDate,
38 | date,
39 | onSelectDate,
40 | styles,
41 | classNames,
42 | timeZone,
43 | numerals = 'latn',
44 | use12Hours,
45 | } = useCalendarContext();
46 |
47 | const hours = useMemo(
48 | () => createNumberList(use12Hours ? 12 : 24, numerals, use12Hours ? 1 : 0),
49 | [numerals, use12Hours]
50 | );
51 |
52 | const minutes = useMemo(() => createNumberList(60, numerals), [numerals]);
53 |
54 | const { hour, hour12, minute, period } = getParsedDate(date || currentDate);
55 |
56 | const handleChangeHour = useCallback(
57 | (value: number) => {
58 | let hour24 = value;
59 |
60 | if (use12Hours) {
61 | if (period === 'PM' && value < 12) {
62 | hour24 = value + 12;
63 | } else if (period === 'PM' && value === 12) {
64 | hour24 = 0;
65 | }
66 | }
67 | const newDate = dayjs.tz(date, timeZone).hour(hour24).minute(minute);
68 | onSelectDate(newDate);
69 | },
70 | [date, onSelectDate, timeZone, use12Hours, period, minute]
71 | );
72 |
73 | const handleChangeMinute = useCallback(
74 | (value: number) => {
75 | const newDate = dayjs.tz(date, timeZone).minute(value);
76 | onSelectDate(newDate);
77 | },
78 | [date, onSelectDate, timeZone]
79 | );
80 |
81 | const handlePeriodChange = useCallback(
82 | (newPeriod: Period) => {
83 | let newHour = hour12;
84 | if (newPeriod === 'PM' && hour12 < 12) {
85 | newHour = hour12 + 12;
86 | } else if (newPeriod === 'AM' && hour12 === 12) {
87 | newHour = 0;
88 | } else if (newPeriod === 'AM' && hour >= 12) {
89 | newHour = hour12;
90 | }
91 |
92 | const newDate = dayjs.tz(date || currentDate, timeZone).hour(newHour);
93 | onSelectDate(newDate);
94 | },
95 | [date, currentDate, onSelectDate, timeZone, hour, hour12]
96 | );
97 |
98 | const timePickerContainerStyle: ViewStyle = useMemo(
99 | () => ({
100 | ...defaultStyles.timePickerContainer,
101 | flexDirection: I18nManager.isRTL ? 'row-reverse' : 'row',
102 | }),
103 | [I18nManager.isRTL]
104 | );
105 |
106 | const timePickerTextStyle: TextStyle = useMemo(
107 | () => ({ ...defaultStyles.timeSeparator, ...styles?.time_label }),
108 | [styles?.time_label]
109 | );
110 |
111 | return (
112 |
118 |
119 |
120 |
127 |
128 |
129 | :
130 |
131 |
132 |
139 |
140 |
141 | {use12Hours && period ? (
142 |
143 |
149 |
150 | ) : null}
151 |
152 | );
153 | };
154 |
155 | const defaultStyles = StyleSheet.create({
156 | container: {
157 | flex: 1,
158 | alignItems: 'center',
159 | justifyContent: 'center',
160 | },
161 | wheelContainer: {
162 | flex: 1,
163 | },
164 | timePickerContainer: {
165 | alignItems: 'center',
166 | justifyContent: 'center',
167 | width: CONTAINER_HEIGHT / 2,
168 | height: CONTAINER_HEIGHT / 2,
169 | },
170 | timeSeparator: {
171 | marginHorizontal: 5,
172 | },
173 | periodContainer: {
174 | marginLeft: 10,
175 | },
176 | });
177 |
178 | export default TimePicker;
179 |
--------------------------------------------------------------------------------
/src/components/time-picker/animated-math.ts:
--------------------------------------------------------------------------------
1 | import { Animated } from 'react-native';
2 |
3 | const FACTORIAL_3 = 3 * 2;
4 | const FACTORIAL_5 = 5 * 4 * FACTORIAL_3;
5 | const FACTORIAL_7 = 7 * 6 * FACTORIAL_5;
6 |
7 | function sin(animated: Animated.Animated) {
8 | const normalized = normalize(animated);
9 | const square = Animated.multiply(normalized, normalized);
10 | const pow3 = Animated.multiply(normalized, square);
11 | const pow5 = Animated.multiply(pow3, square);
12 | const pow7 = Animated.multiply(pow5, square);
13 |
14 | return Animated.add(
15 | Animated.add(normalized, Animated.multiply(pow3, -1 / FACTORIAL_3)),
16 | Animated.add(
17 | Animated.multiply(pow5, 1 / FACTORIAL_5),
18 | Animated.multiply(pow7, -1 / FACTORIAL_7)
19 | )
20 | );
21 | }
22 |
23 | function normalize(animated: Animated.Animated): Animated.Animated {
24 | return Animated.add(
25 | Animated.modulo(Animated.add(animated, Math.PI), Math.PI * 2),
26 | -Math.PI
27 | ).interpolate({
28 | inputRange: [-Math.PI, -Math.PI / 2, Math.PI / 2, Math.PI],
29 | outputRange: [0, -Math.PI / 2, Math.PI / 2, 0],
30 | });
31 | }
32 |
33 | export { sin, normalize };
34 |
--------------------------------------------------------------------------------
/src/components/time-picker/period-native.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react';
2 | import WheelPicker from './wheel-picker';
3 | import { ClassNames, PickerOption, Styles } from '../../types';
4 | import { isEqual } from 'lodash';
5 |
6 | interface PeriodProps {
7 | value: string;
8 | setValue?: (value: any) => void;
9 | styles?: Styles;
10 | classNames?: ClassNames;
11 | }
12 |
13 | const options: PickerOption[] = [
14 | { value: 'AM', text: 'AM' },
15 | { value: 'PM', text: 'PM' },
16 | ];
17 |
18 | const PeriodNative = ({
19 | value,
20 | setValue = () => {},
21 | styles,
22 | classNames,
23 | }: PeriodProps) => {
24 | return (
25 |
37 | );
38 | };
39 |
40 | const customComparator = (
41 | prev: Readonly,
42 | next: Readonly
43 | ) => {
44 | const areEqual =
45 | prev.value === next.value &&
46 | prev.setValue === next.setValue &&
47 | isEqual(prev.styles, next.styles) &&
48 | isEqual(prev.classNames, next.classNames);
49 |
50 | return areEqual;
51 | };
52 |
53 | export default memo(PeriodNative, customComparator);
54 |
--------------------------------------------------------------------------------
/src/components/time-picker/period-picker.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react';
2 | import { Platform } from 'react-native';
3 | import PeriodNative from './period-native';
4 | import PeriodWeb from './period-web';
5 | import { ClassNames, Styles } from '../../types';
6 |
7 | type PeriodProps = {
8 | value: string;
9 | setValue?: (value: any) => void;
10 | styles?: Styles;
11 | classNames?: ClassNames;
12 | };
13 |
14 | const PeriodPicker = (props: PeriodProps) => {
15 | const Component = Platform.OS === 'web' ? PeriodWeb : PeriodNative;
16 | return ;
17 | };
18 |
19 | export default memo(PeriodPicker);
20 |
--------------------------------------------------------------------------------
/src/components/time-picker/period-web.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react';
2 | import { Pressable, StyleSheet, Text, View } from 'react-native';
3 | import { ClassNames, Styles } from '../../types';
4 | import { isEqual } from 'lodash';
5 |
6 | interface PeriodProps {
7 | value: string;
8 | setValue?: (value: any) => void;
9 | styles?: Styles;
10 | classNames?: ClassNames;
11 | }
12 |
13 | const PeriodWeb = ({
14 | value,
15 | setValue = () => {},
16 | styles,
17 | classNames,
18 | }: PeriodProps) => {
19 | return (
20 | setValue(value == 'AM' ? 'PM' : 'AM')}>
21 |
25 |
26 | {value}
27 |
28 |
29 |
30 | );
31 | };
32 |
33 | const defaultStyles = StyleSheet.create({
34 | period: {
35 | width: 65,
36 | height: 44,
37 | alignItems: 'center',
38 | justifyContent: 'center',
39 | },
40 | });
41 |
42 | const customComparator = (
43 | prev: Readonly,
44 | next: Readonly
45 | ) => {
46 | const areEqual =
47 | prev.value === next.value &&
48 | prev.setValue === next.setValue &&
49 | isEqual(prev.styles, next.styles) &&
50 | isEqual(prev.classNames, next.classNames);
51 |
52 | return areEqual;
53 | };
54 |
55 | export default memo(PeriodWeb, customComparator);
56 |
--------------------------------------------------------------------------------
/src/components/time-picker/wheel-native.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react';
2 | import { StyleSheet, Platform } from 'react-native';
3 | import WheelPicker from './wheel-picker';
4 | import { ClassNames, PickerOption, Styles } from '../../types';
5 |
6 | interface WheelProps {
7 | value: number | string;
8 | setValue?: (value: any) => void;
9 | items: PickerOption[];
10 | styles?: Styles;
11 | classNames?: ClassNames;
12 | }
13 |
14 | const WheelNative = ({
15 | value,
16 | setValue = () => {},
17 | items,
18 | styles,
19 | classNames,
20 | }: WheelProps) => {
21 | return (
22 |
34 | );
35 | };
36 |
37 | export default memo(WheelNative);
38 |
39 | const defaultStyles = StyleSheet.create({
40 | container: {
41 | display: 'flex',
42 | ...Platform.select({
43 | web: {
44 | userSelect: 'none',
45 | },
46 | }),
47 | },
48 | });
49 |
--------------------------------------------------------------------------------
/src/components/time-picker/wheel-picker/index.ts:
--------------------------------------------------------------------------------
1 | import WheelPicker from './wheel-picker';
2 |
3 | export default WheelPicker;
4 |
--------------------------------------------------------------------------------
/src/components/time-picker/wheel-picker/wheel-picker-item.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleProp, TextStyle, Animated, ViewStyle, Text } from 'react-native';
3 | import styles from './wheel-picker.style';
4 | import { isEqual } from 'lodash';
5 | import { PickerOption } from 'src/types';
6 |
7 | interface ItemProps {
8 | textStyle: StyleProp;
9 | textClassName: string;
10 | style: StyleProp;
11 | option: PickerOption | null;
12 | height: number;
13 | index: number;
14 | currentScrollIndex: Animated.AnimatedAddition;
15 | visibleRest: number;
16 | rotationFunction: (x: number) => number;
17 | opacityFunction: (x: number) => number;
18 | scaleFunction: (x: number) => number;
19 | }
20 |
21 | const WheelPickerItem: React.FC = ({
22 | textStyle,
23 | textClassName,
24 | style,
25 | height,
26 | option,
27 | index,
28 | visibleRest,
29 | currentScrollIndex,
30 | opacityFunction,
31 | rotationFunction,
32 | scaleFunction,
33 | }) => {
34 | const relativeScrollIndex = Animated.subtract(index, currentScrollIndex);
35 |
36 | const translateY = relativeScrollIndex.interpolate({
37 | inputRange: (() => {
38 | const range = [0];
39 | for (let i = 1; i <= visibleRest + 1; i++) {
40 | range.unshift(-i);
41 | range.push(i);
42 | }
43 | return range;
44 | })(),
45 | outputRange: (() => {
46 | const range = [0];
47 | for (let i = 1; i <= visibleRest + 1; i++) {
48 | let y =
49 | (height / 2) * (1 - Math.sin(Math.PI / 2 - rotationFunction(i)));
50 | for (let j = 1; j < i; j++) {
51 | y += height * (1 - Math.sin(Math.PI / 2 - rotationFunction(j)));
52 | }
53 | range.unshift(y);
54 | range.push(-y);
55 | }
56 | return range;
57 | })(),
58 | });
59 |
60 | const opacity = relativeScrollIndex.interpolate({
61 | inputRange: (() => {
62 | const range = [0];
63 | for (let i = 1; i <= visibleRest + 1; i++) {
64 | range.unshift(-i);
65 | range.push(i);
66 | }
67 | return range;
68 | })(),
69 | outputRange: (() => {
70 | const range = [1];
71 | for (let x = 1; x <= visibleRest + 1; x++) {
72 | const y = opacityFunction(x);
73 | range.unshift(y);
74 | range.push(y);
75 | }
76 | return range;
77 | })(),
78 | });
79 |
80 | const scale = relativeScrollIndex.interpolate({
81 | inputRange: (() => {
82 | const range = [0];
83 | for (let i = 1; i <= visibleRest + 1; i++) {
84 | range.unshift(-i);
85 | range.push(i);
86 | }
87 | return range;
88 | })(),
89 | outputRange: (() => {
90 | const range = [1.0];
91 | for (let x = 1; x <= visibleRest + 1; x++) {
92 | const y = scaleFunction(x);
93 | range.unshift(y);
94 | range.push(y);
95 | }
96 | return range;
97 | })(),
98 | });
99 |
100 | const rotateX = relativeScrollIndex.interpolate({
101 | inputRange: (() => {
102 | const range = [0];
103 | for (let i = 1; i <= visibleRest + 1; i++) {
104 | range.unshift(-i);
105 | range.push(i);
106 | }
107 | return range;
108 | })(),
109 | outputRange: (() => {
110 | const range = ['0deg'];
111 | for (let x = 1; x <= visibleRest + 1; x++) {
112 | const y = rotationFunction(x);
113 | range.unshift(`${y}deg`);
114 | range.push(`${y}deg`);
115 | }
116 | return range;
117 | })(),
118 | });
119 |
120 | return (
121 |
132 |
133 | {option?.text}
134 |
135 |
136 | );
137 | };
138 |
139 | const customComparator = (
140 | prevProps: Readonly,
141 | nextProps: Readonly
142 | ) => {
143 | return (
144 | prevProps.textClassName === nextProps.textClassName &&
145 | isEqual(prevProps.textStyle, nextProps.textStyle)
146 | );
147 | };
148 |
149 | export default React.memo(WheelPickerItem, customComparator);
150 |
--------------------------------------------------------------------------------
/src/components/time-picker/wheel-picker/wheel-picker.style.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 |
3 | export default StyleSheet.create({
4 | container: {
5 | position: 'relative',
6 | },
7 | selectedIndicator: {
8 | position: 'absolute',
9 | width: '100%',
10 | top: '50%',
11 | },
12 | scrollView: {
13 | overflow: 'hidden',
14 | flex: 1,
15 | },
16 | option: {
17 | alignItems: 'center',
18 | justifyContent: 'center',
19 | paddingHorizontal: 16,
20 | zIndex: 100,
21 | },
22 | });
23 |
--------------------------------------------------------------------------------
/src/components/time-picker/wheel.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react';
2 | import { Platform } from 'react-native';
3 | import WheelNative from './wheel-native';
4 | import WheelWeb from './wheel-web';
5 | import { ClassNames, PickerOption, Styles } from '../../types';
6 |
7 | type WheelProps = {
8 | value: number | string;
9 | setValue?: (value: any) => void;
10 | items: PickerOption[];
11 | styles?: Styles;
12 | classNames?: ClassNames;
13 | };
14 |
15 | const Wheel = (props: WheelProps) => {
16 | const Component = Platform.OS === 'web' ? WheelWeb : WheelNative;
17 | return ;
18 | };
19 |
20 | export default memo(Wheel);
21 |
--------------------------------------------------------------------------------
/src/components/weekdays.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useMemo } from 'react';
2 | import { StyleSheet, Text, View } from 'react-native';
3 | import { getWeekdays } from '../utils';
4 | import {
5 | Styles,
6 | ClassNames,
7 | WeekdayFormat,
8 | CalendarComponents,
9 | } from '../types';
10 | import { WEEKDAYS_HEIGHT } from '../enums';
11 |
12 | type WeekdaysProps = {
13 | locale: string;
14 | firstDayOfWeek: number;
15 | styles?: Styles;
16 | classNames?: ClassNames;
17 | weekdaysFormat?: WeekdayFormat;
18 | weekdaysHeight?: number;
19 | components?: CalendarComponents;
20 | isRTL: boolean;
21 | };
22 |
23 | const Weekdays = ({
24 | locale,
25 | firstDayOfWeek,
26 | styles = {},
27 | classNames = {},
28 | weekdaysFormat = 'min',
29 | weekdaysHeight = WEEKDAYS_HEIGHT,
30 | components = {},
31 | isRTL,
32 | }: WeekdaysProps) => {
33 | const style = useMemo(
34 | () => createDefaultStyles(weekdaysHeight, isRTL),
35 | [weekdaysHeight, isRTL]
36 | );
37 |
38 | return (
39 |
44 | {getWeekdays(locale, firstDayOfWeek)?.map((weekday, index) => (
45 |
50 | {components.Weekday ? (
51 | components.Weekday(weekday)
52 | ) : (
53 |
57 | {weekday.name[weekdaysFormat]}
58 |
59 | )}
60 |
61 | ))}
62 |
63 | );
64 | };
65 |
66 | export default memo(Weekdays);
67 |
68 | const createDefaultStyles = (weekdaysHeight: number, isRTL: boolean) =>
69 | StyleSheet.create({
70 | container: {
71 | height: weekdaysHeight,
72 | flexDirection: isRTL ? 'row-reverse' : 'row',
73 | alignItems: 'center',
74 | },
75 | weekday: {
76 | width: `${99.9 / 7}%`,
77 | alignItems: 'center',
78 | justifyContent: 'center',
79 | },
80 | });
81 |
--------------------------------------------------------------------------------
/src/components/years.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useMemo } from 'react';
2 | import { View, StyleSheet, Pressable, Text } from 'react-native';
3 | import { useCalendarContext } from '../calendar-context';
4 | import {
5 | cn,
6 | formatNumber,
7 | getDateYear,
8 | getYearRange,
9 | isYearDisabled,
10 | } from '../utils';
11 | import { CONTAINER_HEIGHT } from '../enums';
12 |
13 | const Years = () => {
14 | const {
15 | mode,
16 | calendar = 'gregory',
17 | numerals = 'latn',
18 | currentDate,
19 | currentYear,
20 | date,
21 | onSelectYear,
22 | styles = {},
23 | classNames = {},
24 | components = {},
25 | containerHeight = CONTAINER_HEIGHT,
26 | minDate,
27 | maxDate,
28 | isRTL,
29 | } = useCalendarContext();
30 |
31 | const style = useMemo(
32 | () => createDefaultStyles(containerHeight, isRTL),
33 | [containerHeight, isRTL]
34 | );
35 |
36 | const selectedYear = getDateYear(date);
37 |
38 | const generateCells = useCallback(() => {
39 | const years = getYearRange(currentYear);
40 | const activeYear = getDateYear(currentDate);
41 | const column = years.map((year) => {
42 | const isSelected = year === selectedYear;
43 | const isActivated = year === activeYear;
44 |
45 | const isDisabled = isYearDisabled(year, { minDate, maxDate });
46 |
47 | const containerStyle = StyleSheet.flatten([
48 | style.year,
49 | styles.year,
50 | isActivated && styles.active_year,
51 | isSelected && styles.selected_year,
52 | isDisabled && styles.disabled,
53 | ]);
54 |
55 | const textStyle = StyleSheet.flatten([
56 | styles.year_label,
57 | isActivated && styles.active_year_label,
58 | isSelected && styles.selected_year_label,
59 | isDisabled && styles.disabled_label,
60 | ]);
61 |
62 | const containerClassName = cn(
63 | classNames.year,
64 | isActivated && classNames.active_year,
65 | isSelected && classNames.selected_year,
66 | isDisabled && classNames.disabled
67 | );
68 |
69 | const textClassName = cn(
70 | classNames.year_label,
71 | isActivated && classNames.active_year_label,
72 | isSelected && classNames.selected_year_label,
73 | isDisabled && classNames.disabled_label
74 | );
75 |
76 | return (
77 |
78 | {components.Year ? (
79 | onSelectYear(year)}
82 | accessibilityRole="button"
83 | accessibilityLabel={year.toString()}
84 | style={style.year}
85 | >
86 | {components.Year({
87 | number: year,
88 | text: formatNumber(year, numerals),
89 | isSelected,
90 | isActivated,
91 | })}
92 |
93 | ) : (
94 | onSelectYear(year)}
97 | accessibilityRole="button"
98 | accessibilityLabel={year.toString()}
99 | style={containerStyle}
100 | className={containerClassName}
101 | >
102 |
103 | {formatNumber(year, numerals)}
104 |
105 |
106 | )}
107 |
108 | );
109 | });
110 | return column;
111 | }, [
112 | onSelectYear,
113 | selectedYear,
114 | currentYear,
115 | currentDate,
116 | styles,
117 | mode,
118 | classNames,
119 | components?.Year,
120 | minDate,
121 | maxDate,
122 | numerals,
123 | style.year,
124 | style.yearCell,
125 | calendar,
126 | ]);
127 |
128 | return (
129 |
130 | {generateCells()}
131 |
132 | );
133 | };
134 |
135 | const createDefaultStyles = (containerHeight: number, isRTL: boolean) =>
136 | StyleSheet.create({
137 | container: {
138 | flex: 1,
139 | alignItems: 'center',
140 | justifyContent: 'center',
141 | },
142 | years: {
143 | flexDirection: isRTL ? 'row-reverse' : 'row',
144 | flexWrap: 'wrap',
145 | },
146 | year: {
147 | flex: 1,
148 | margin: 2,
149 | alignItems: 'center',
150 | justifyContent: 'center',
151 | },
152 | yearCell: {
153 | width: `${99.9 / 3}%`,
154 | height: containerHeight / 6,
155 | },
156 | });
157 |
158 | export default Years;
159 |
--------------------------------------------------------------------------------
/src/enums.ts:
--------------------------------------------------------------------------------
1 | export type CalendarViews = 'day' | 'month' | 'year' | 'time';
2 |
3 | export enum CalendarActionKind {
4 | SET_CALENDAR_VIEW = 'SET_CALENDAR_VIEW',
5 | CHANGE_CURRENT_DATE = 'CHANGE_CURRENT_DATE',
6 | CHANGE_CURRENT_YEAR = 'CHANGE_CURRENT_YEAR',
7 | CHANGE_SELECTED_DATE = 'CHANGE_SELECTED_DATE',
8 | CHANGE_SELECTED_RANGE = 'CHANGE_SELECTED_RANGE',
9 | CHANGE_SELECTED_MULTIPLE = 'CHANGE_SELECTED_MULTIPLE',
10 | SET_IS_RTL = 'SET_IS_RTL',
11 | RESET_STATE = 'RESET_STATE',
12 | }
13 |
14 | export const CONTAINER_HEIGHT = 300;
15 | export const WEEKDAYS_HEIGHT = 25;
16 |
--------------------------------------------------------------------------------
/src/hooks/use-previous.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react';
2 |
3 | export const usePrevious = (value: any) => {
4 | const ref = useRef();
5 | useEffect(() => {
6 | ref.current = value;
7 | });
8 | return ref.current;
9 | };
10 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import './locales';
2 | import './polyfill';
3 | import DateTimePicker from './datetime-picker';
4 |
5 | export type {
6 | DateType,
7 | CalendarMode,
8 | CalendarDay,
9 | CalendarWeek,
10 | CalendarMonth,
11 | CalendarYear,
12 | CalendarComponents,
13 | } from './types';
14 | export { useDefaultClassNames, useDefaultStyles } from './theme';
15 |
16 | export default DateTimePicker;
17 |
--------------------------------------------------------------------------------
/src/locales.ts:
--------------------------------------------------------------------------------
1 | import 'dayjs/locale/af'; // Afrikaans
2 | import 'dayjs/locale/ar'; // Arabic
3 | import 'dayjs/locale/az'; // Azerbaijani
4 | import 'dayjs/locale/bg'; // Bulgarian
5 | import 'dayjs/locale/bn'; // Bengali
6 | import 'dayjs/locale/ca'; // Catalan
7 | import 'dayjs/locale/cs'; // Czech
8 | import 'dayjs/locale/da'; // Danish
9 | import 'dayjs/locale/de'; // German
10 | import 'dayjs/locale/el'; // Greek
11 | import 'dayjs/locale/en'; // English
12 | import 'dayjs/locale/es'; // Spanish
13 | import 'dayjs/locale/et'; // Estonian
14 | import 'dayjs/locale/fa'; // Persian
15 | import 'dayjs/locale/fi'; // Finnish
16 | import 'dayjs/locale/fr'; // French
17 | import 'dayjs/locale/gl'; // Galician
18 | import 'dayjs/locale/he'; // Hebrew
19 | import 'dayjs/locale/hi'; // Hindi
20 | import 'dayjs/locale/hr'; // Croatian
21 | import 'dayjs/locale/hu'; // Hungarian
22 | import 'dayjs/locale/id'; // Indonesian
23 | import 'dayjs/locale/it'; // Italian
24 | import 'dayjs/locale/ja'; // Japanese
25 | import 'dayjs/locale/ka'; // Georgian
26 | import 'dayjs/locale/kk'; // Kazakh
27 | import 'dayjs/locale/ko'; // Korean
28 | import 'dayjs/locale/lt'; // Lithuanian
29 | import 'dayjs/locale/lv'; // Latvian
30 | import 'dayjs/locale/ms'; // Malay
31 | import 'dayjs/locale/nb'; // Norwegian Bokmål
32 | import 'dayjs/locale/nl'; // Dutch
33 | import 'dayjs/locale/pl'; // Polish
34 | import 'dayjs/locale/pt'; // Portuguese
35 | import 'dayjs/locale/ro'; // Romanian
36 | import 'dayjs/locale/ru'; // Russian
37 | import 'dayjs/locale/sk'; // Slovak
38 | import 'dayjs/locale/sl'; // Slovenian
39 | import 'dayjs/locale/sr'; // Serbian
40 | import 'dayjs/locale/sv'; // Swedish
41 | import 'dayjs/locale/th'; // Thai
42 | import 'dayjs/locale/tr'; // Turkish
43 | import 'dayjs/locale/uk'; // Ukrainian
44 | import 'dayjs/locale/vi'; // Vietnamese
45 | import 'dayjs/locale/zh'; // Chinese
46 |
--------------------------------------------------------------------------------
/src/numerals.ts:
--------------------------------------------------------------------------------
1 | import { Numerals } from './types';
2 |
3 | export const numeralSystems: Record = {
4 | latn: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
5 | arab: ['٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩'],
6 | arabext: ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹'],
7 | deva: ['०', '१', '२', '३', '४', '५', '६', '७', '८', '९'],
8 | beng: ['০', '১', '২', '৩', '৪', '৫', '৬', '৭', '৮', '৯'],
9 | guru: ['੦', '੧', '੨', '੩', '੪', '੫', '੬', '੭', '੮', '੯'],
10 | gujr: ['૦', '૧', '૨', '૩', '૪', '૫', '૬', '૭', '૮', '૯'],
11 | orya: ['୦', '୧', '୨', '୩', '୪', '୫', '୬', '୭', '୮', '୯'],
12 | tamldec: ['௦', '௧', '௨', '௩', '௪', '௫', '௬', '௭', '௮', '௯'],
13 | telu: ['౦', '౧', '౨', '౩', '౪', '౫', '౬', '౭', '౮', '౯'],
14 | knda: ['೦', '೧', '೨', '೩', '೪', '೫', '೬', '೭', '೮', '೯'],
15 | mlym: ['൦', '൧', '൨', '൩', '൪', '൫', '൬', '൭', '൮', '൯'],
16 | } as const;
17 |
--------------------------------------------------------------------------------
/src/polyfill.ts:
--------------------------------------------------------------------------------
1 | // @ts-ignore
2 | Date.prototype._toLocaleString = Date.prototype.toLocaleString;
3 | // @ts-ignore
4 | Date.prototype.toLocaleString = function (a, b) {
5 | if (b && Object.keys(b).length === 1 && 'timeZone' in b && a === 'en-US') {
6 | return Intl.DateTimeFormat('en-us', {
7 | year: 'numeric',
8 | month: '2-digit',
9 | day: '2-digit',
10 | hour: '2-digit',
11 | minute: '2-digit',
12 | second: '2-digit',
13 | hour12: false,
14 | timeZone: b.timeZone,
15 | })
16 | .format(this)
17 | .replace(/(\d{2})\/(\d{2})\/(\d{4}),/g, '$3-$1-$2');
18 | }
19 | // @ts-ignore
20 | return this._toLocaleString(a, b);
21 | };
22 |
--------------------------------------------------------------------------------
/src/react-native-extended.d.ts:
--------------------------------------------------------------------------------
1 | import 'react-native';
2 |
3 | declare module 'react-native' {
4 | interface ViewProps {
5 | className?: string;
6 | }
7 |
8 | interface TextProps {
9 | className?: string;
10 | }
11 |
12 | interface PressableProps {
13 | className?: string;
14 | }
15 |
16 | interface ImageProps {
17 | className?: string;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import type { Dayjs } from 'dayjs';
2 | import type { CalendarActionKind, CalendarViews } from './enums';
3 | import type { ImageStyle, TextStyle, ViewStyle } from 'react-native';
4 | import {
5 | UI,
6 | SelectionState,
7 | DayFlag,
8 | MonthState,
9 | YearState,
10 | CalenderFlag,
11 | } from './ui';
12 |
13 | export type DateType = string | number | Dayjs | Date | null | undefined;
14 |
15 | export type CalendarType = 'gregory' | 'jalali';
16 |
17 | export type CalendarMode = 'single' | 'range' | 'multiple';
18 |
19 | export type NavigationPosition = 'around' | 'right' | 'left';
20 |
21 | export type WeekdayFormat = 'min' | 'short' | 'full';
22 |
23 | export type MonthFormat = 'short' | 'full';
24 |
25 | export type LocalState = {
26 | date: DateType;
27 | startDate: DateType;
28 | endDate: DateType;
29 | dates?: DateType[];
30 | calendarView: CalendarViews;
31 | currentDate: DateType; // used for latest state of calendar based on Month and Year
32 | currentYear: number;
33 | isRTL: boolean; // used for jalali or i18n RTL detection
34 | };
35 |
36 | export type CalendarAction = {
37 | type: CalendarActionKind;
38 | payload: any;
39 | };
40 |
41 | export type CalendarDay = {
42 | number: number;
43 | text: string;
44 | date: string;
45 | isDisabled: boolean;
46 | isCurrentMonth: boolean;
47 | dayOfMonth?: number;
48 | isToday: boolean;
49 | isSelected: boolean;
50 | inRange: boolean;
51 | leftCrop: boolean;
52 | rightCrop: boolean;
53 | isStartOfWeek: boolean;
54 | isEndOfWeek: boolean;
55 | isCrop: boolean;
56 | inMiddle: boolean;
57 | rangeStart: boolean;
58 | rangeEnd: boolean;
59 | };
60 |
61 | export type CalendarWeek = {
62 | index: number;
63 | name: {
64 | full: string;
65 | short: string;
66 | min: string;
67 | };
68 | };
69 |
70 | export type CalendarMonth = {
71 | index: number;
72 | name: {
73 | full: string;
74 | short: string;
75 | };
76 | isSelected: boolean;
77 | };
78 |
79 | export type CalendarYear = {
80 | number: number;
81 | text: string;
82 | isSelected: boolean;
83 | isActivated: boolean;
84 | };
85 |
86 | export type SingleChange = (params: { date: DateType }) => void;
87 |
88 | export type RangeChange = (params: {
89 | startDate: DateType;
90 | endDate: DateType;
91 | }) => void;
92 |
93 | export type MultiChange = (params: {
94 | dates: DateType[];
95 | datePressed?: DateType;
96 | change: 'added' | 'removed' | 'updated';
97 | }) => void;
98 |
99 | export type ClassNames = Partial<{
100 | [key in
101 | | UI
102 | | SelectionState
103 | | DayFlag
104 | | MonthState
105 | | YearState
106 | | CalenderFlag]: string;
107 | }>;
108 |
109 | export type Styles = Partial<{
110 | [key in
111 | | UI
112 | | SelectionState
113 | | DayFlag
114 | | MonthState
115 | | YearState
116 | | CalenderFlag]: ViewStyle | TextStyle | ImageStyle;
117 | }>;
118 |
119 | export type CalendarComponents = Partial<{
120 | /** The component containing the day in the days grid */
121 | Day: (day: CalendarDay) => React.ReactNode;
122 | /** The component containing the month in the months grid */
123 | Month: (month: CalendarMonth) => React.ReactNode;
124 | /** The component containing the year in the years grid */
125 | Year: (year: CalendarYear) => React.ReactNode;
126 | /** The component containing the weekday in the header */
127 | Weekday: (weekday: CalendarWeek) => React.ReactNode;
128 | /** The previous month/year button icon in the header */
129 | IconPrev: React.ReactNode;
130 | /** The next month button/year icon in the header */
131 | IconNext: React.ReactNode;
132 | }>;
133 |
134 | export interface DatePickerBaseProps {
135 | mode?: CalendarMode;
136 | calendar?: CalendarType;
137 | locale?: string;
138 | numerals?: Numerals;
139 | timeZone?: string;
140 | date?: DateType;
141 | startDate?: DateType;
142 | endDate?: DateType;
143 | dates?: DateType[];
144 | min?: number;
145 | max?: number;
146 | onChange?: SingleChange | RangeChange | MultiChange;
147 | startYear?: number;
148 | endYear?: number;
149 | minDate?: DateType;
150 | maxDate?: DateType;
151 | enabledDates?: DateType[] | ((date: DateType) => boolean);
152 | disabledDates?: DateType[] | ((date: DateType) => boolean);
153 | firstDayOfWeek?: number;
154 | showOutsideDays?: boolean;
155 | timePicker?: boolean;
156 | use12Hours?: boolean;
157 | initialView?: CalendarViews;
158 | containerHeight?: number;
159 | weekdaysHeight?: number;
160 | style?: ViewStyle;
161 | className?: string;
162 | styles?: Styles;
163 | classNames?: ClassNames;
164 | navigationPosition?: NavigationPosition;
165 | weekdaysFormat?: WeekdayFormat;
166 | monthsFormat?: MonthFormat;
167 | monthCaptionFormat?: MonthFormat;
168 | multiRangeMode?: boolean;
169 | hideHeader?: boolean;
170 | hideWeekdays?: boolean;
171 | disableMonthPicker?: boolean;
172 | disableYearPicker?: boolean;
173 | components?: CalendarComponents;
174 | /** use to handle month and year selectors */
175 | month?: number;
176 | year?: number;
177 | onMonthChange?: (month: number) => void;
178 | onYearChange?: (year: number) => void;
179 | }
180 |
181 | export type Numerals =
182 | | 'latn'
183 | | 'arab'
184 | | 'arabext'
185 | | 'deva'
186 | | 'beng'
187 | | 'guru'
188 | | 'gujr'
189 | | 'orya'
190 | | 'tamldec'
191 | | 'telu'
192 | | 'knda'
193 | | 'mlym';
194 |
195 | export type PickerOption = {
196 | value: number | string;
197 | text: string;
198 | };
199 |
--------------------------------------------------------------------------------
/src/ui.ts:
--------------------------------------------------------------------------------
1 | export enum UI {
2 | /** The container of the displayed days. */
3 | days = 'days',
4 | /** Wrapper of the day cell in the days grid. */
5 | day_cell = 'day_cell',
6 | /** The day cell in the days grid. */
7 | day = 'day',
8 | /** The label of the day cell in the days grid. */
9 | day_label = 'day_label',
10 | /** The container of the displayed months. */
11 | months = 'months',
12 | /** Wrapper of the month cell in the months grid. */
13 | month = 'month',
14 | /** The label of the month cell in the months grid. */
15 | month_label = 'month_label',
16 | /** The container of the displayed years. */
17 | years = 'years',
18 | /** Wrapper of the year cell in the years grid. */
19 | year = 'year',
20 | /** The label of the year cell in the years grid. */
21 | year_label = 'year_label',
22 | /** The filled background for the selected range. */
23 | range_fill = 'range_fill',
24 | /** The background for the start days of each week within the selected range. */
25 | range_fill_weekstart = 'range_fill_weekstart',
26 | /** The background for the end days of each week within the selected range. */
27 | range_fill_weekend = 'range_fill_weekend',
28 | /** The calendar header with the previous and next buttons and selectors. */
29 | header = 'header',
30 | /** The cell containing the month selector in the header. */
31 | month_selector = 'month_selector',
32 | /** The label of the month selector cell in the header. */
33 | month_selector_label = 'month_selector_label',
34 | /** The cell containing the year selector in the header. */
35 | year_selector = 'year_selector',
36 | /** The label of the year selector cell in the header. */
37 | year_selector_label = 'year_selector_label',
38 | /** The cell containing the time selector in the header. */
39 | time_selector = 'time_selector',
40 | /** The label of the time selector cell in the header. */
41 | time_selector_label = 'time_selector_label',
42 | /** The row grouping the weekdays in the header. */
43 | weekdays = 'weekdays',
44 | /** The cell with the weekday in the header. */
45 | weekday = 'weekday',
46 | /** The label of the weekday in the header. */
47 | weekday_label = 'weekday_label',
48 | /** The next button in the header. */
49 | button_next = 'button_next',
50 | /** The previous button in the header. */
51 | button_prev = 'button_prev',
52 | /** The next button image in the header. */
53 | button_next_image = 'button_next_image',
54 | /** The previous button image in the header. */
55 | button_prev_image = 'button_prev_image',
56 | /** The label of the hour and minutes. */
57 | time_label = 'time_label',
58 | /** The indicator of the selected hour and minutes. */
59 | time_selected_indicator = 'time_selected_indicator',
60 | }
61 |
62 | export enum SelectionState {
63 | /** The day is at the end of a selected range. */
64 | range_end = 'range_end',
65 | /** The label of the end range cell within a range. */
66 | range_end_label = 'range_end_label',
67 | /** The day is at the middle of a selected range. */
68 | range_middle = 'range_middle',
69 | /** The label of the middle range cell within a range. */
70 | range_middle_label = 'range_middle_label',
71 | /** The day is at the start of a selected range. */
72 | range_start = 'range_start',
73 | /** The label of the start range cell within a range. */
74 | range_start_label = 'range_start_label',
75 | /** The day is selected. */
76 | selected = 'selected',
77 | /** The label of the selected day. */
78 | selected_label = 'selected_label',
79 | }
80 |
81 | export enum CalenderFlag {
82 | /** The day/month/year is disabled. */
83 | disabled = 'disabled',
84 | /** The label of the disabled day/month/year. */
85 | disabled_label = 'disabled_label',
86 | }
87 |
88 | export enum DayFlag {
89 | /** The day is hidden. */
90 | hidden = 'hidden',
91 | /** The day is outside the current month. */
92 | outside = 'outside',
93 | /** The label of the outsided day. */
94 | outside_label = 'outside_label',
95 | /** The day is today. */
96 | today = 'today',
97 | /** The label of the today. */
98 | today_label = 'today_label',
99 | }
100 |
101 | export enum MonthState {
102 | /** The month is selected. */
103 | selected_month = 'selected_month',
104 | /** The label of the selected month. */
105 | selected_month_label = 'selected_month_label',
106 | }
107 |
108 | export enum YearState {
109 | /** The year is selected. (for single mode) */
110 | selected_year = 'selected_year',
111 | /** The label of the selected year. */
112 | selected_year_label = 'selected_year_label',
113 | /** The year is activated (not selected). */
114 | active_year = 'active_year',
115 | /** The label of the activated year. */
116 | active_year_label = 'active_year_label',
117 | }
118 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": [
4 | "demo",
5 | "example",
6 | "lib",
7 | "node_modules",
8 | "**/__tests__/*",
9 | "**/__mocks__/*"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "react-native-ui-datepicker": ["./src/index"]
6 | },
7 | "allowUnreachableCode": false,
8 | "allowUnusedLabels": false,
9 | "esModuleInterop": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "jsx": "react-native",
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 | "allowJs": true,
27 | "types": ["react-native", "jest", "node", "@testing-library/jest-native"]
28 | },
29 | "include": ["src/**/*"],
30 | "exclude": ["node_modules"]
31 | }
32 |
--------------------------------------------------------------------------------