├── .eslintrc.js ├── .github └── workflows │ └── publish.yml ├── .gitignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── resources └── README │ ├── react-native-shadow-2-ex-1.png │ └── react-native-shadow-2-ex-2.png ├── sandbox ├── .eslintrc.js ├── .gitignore ├── App.tsx ├── README.md ├── app.json ├── babel.config.js ├── package-lock.json ├── package.json ├── src │ ├── App.tsx │ └── shadow │ │ ├── index.tsx │ │ └── utils.tsx └── tsconfig.json ├── src ├── index.tsx └── utils.tsx ├── tsconfig.json └── tsconfig.lint.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // This is a workaround for https://github.com/eslint/eslint/issues/3458 2 | require('@rushstack/eslint-patch/modern-module-resolution'); 3 | 4 | module.exports = { 5 | root: true, 6 | env: { 7 | es2021: true, 8 | node: true, 9 | 'react-native/react-native': true, // *1 10 | }, 11 | extends: ['eslint-config-gev/react-native-js'], 12 | overrides: [ 13 | { 14 | files: ['*.ts', '*.tsx'], 15 | extends: ['eslint-config-gev/react-native'], 16 | parser: '@typescript-eslint/parser', 17 | parserOptions: { 18 | tsconfigRootDir: __dirname, 19 | project: ['./tsconfig.json'], 20 | ecmaVersion: 12, 21 | sourceType: 'module', 22 | ecmaFeatures: { 23 | jsx: true, // *1 24 | }, 25 | }, 26 | }, 27 | ], 28 | ignorePatterns: ['**/lib/**/*', '**/dist/**/*', '**/node_modules/**/*', '.eslintrc.js'], 29 | rules: {}, 30 | }; 31 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-node@v2 12 | with: 13 | node-version: 12 14 | - run: npm install 15 | - run: npm test 16 | - uses: JS-DevTools/npm-publish@v1 17 | with: 18 | token: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /lib 3 | docs 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": "explicit" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 7.1.1 - 2024-10-26 2 | 3 | - Fix `corners` and `sides` properties not being Partial. [#83](https://github.com/SrBrahma/react-native-shadow-2/issues/83). 4 | - Also, [read the README](https://github.com/SrBrahma/react-native-shadow-2) about React Native 0.76 brand new built-in shadow! ;) 5 | 6 | ### 7.1.0 - 2024-05-26 7 | 8 | - Fixed pixel gap from top and bottom in rtl mode [#73](https://github.com/SrBrahma/react-native-shadow-2/pull/73). Thanks, [numandev1](https://github.com/numandev1)! 9 | 10 | ### 7.0.8 - 2023-05-15 11 | 12 | - Fixed issue when the child size would change only one of its axis. [#72](https://github.com/SrBrahma/react-native-shadow-2/issues/72). 13 | - As a minor performance improvement, now sides will only be rendered if they are known to be visible. Before, if your height was X and the topStart and bottomStart radii were each X/2, the left side would still be rendered even it having the size 0. 14 | 15 | ### 7.0.7 - 2023-04-14 16 | 17 | - Fixed X offset not working in iOS. [#65](https://github.com/SrBrahma/react-native-shadow-2/issues/65), [#67](https://github.com/SrBrahma/react-native-shadow-2/issues/67). Many thanks, [dmdmd](https://github.com/dmdmd) and [Youssef Henna](https://github.com/YoussefHenna)! 18 | 19 | ### 7.0.6 - 2022-09-26 20 | 21 | - Add react-native-svg v13 support. [#58](https://github.com/SrBrahma/react-native-shadow-2/pull/58). Thanks, [@Vin-Xi](https://github.com/Vin-Xi)! 22 | 23 | ### 7.0.5 - 2022-08-15 24 | 25 | - Fixed inner circle of corners being black. [#56 (comment)](https://github.com/SrBrahma/react-native-shadow-2/issues/56#issuecomment-1214805252). 26 | - Fixed the outer part of corners not being cropped. [#56 (comment)](https://github.com/SrBrahma/react-native-shadow-2/issues/56#issuecomment-1214805252). Thanks once again, [alexco2](https://github.com/alexco2)! 27 | - setChildLayoutWidth/height will now only be called if the sizes **in pixels** changed, ignoring useless re-renders due to very small changes in the values that wouldn't change the result. 28 | 29 | ### 7.0.4 - 2022-08-14 30 | 31 | - Fixed Android's `The argument must be a React element, but you passed null.` error. [#56](https://github.com/SrBrahma/react-native-shadow-2/issues/56). Thanks again, [alexco2](https://github.com/alexco2)! 32 | 33 | ### 7.0.3 - 2022-08-14 34 | 35 | - Fixed `undefined is not an object (evaluating 'd.width')` error. [#56](https://github.com/SrBrahma/react-native-shadow-2/issues/56). Thanks, [alexco2](https://github.com/alexco2)! 36 | - Fixed `flex-start` not being the default `alignSelf` style as it was before. 37 | 38 | ### 7.0.2 - 2022-08-10 39 | 40 | - Fixed missing `version` const export in `index.tsx`. 41 | 42 | ### 7.0.1 - 2022-08-10 43 | 44 | - Added missing `import React from 'react'`. 45 | 46 | # 7.0.0 - 2022-08-10 47 | 48 | > Major changes to improve the performance, simplify the library usage and improve the Developer Experience. An important update that consolidates this library's maturity. 49 | 50 | ### Features 51 | 52 | - `stretch` property - [#7](https://github.com/SrBrahma/react-native-shadow-2/issues/7#issuecomment-899784537). 53 | - `disabled` property - Easy and performance-friendly way to disable the shadow (but to keep rendering the children). 54 | - `containerViewProps` property. 55 | - `childrenViewProps` property. 56 | 57 | ### Changes 58 | 59 | - Renamed `viewStyle` to `style`. 60 | - Renamed `containerViewStyle` to `containerStyle`. 61 | - Renamed `finalColor` to `endColor`, to follow the `start/end` pattern of the following change. 62 | - `left` / `right` in `sides` and `corners` were changed to `start` / `end` for [RTL friendliness](https://reactnative.dev/blog/2016/08/19/right-to-left-support-for-react-native-apps) 63 | - `sides` and `corners` properties are now objects instead of arrays. 64 | 65 | > Note that you may still use `borderTopLeftRadius` etc in `style` besides `borderTopStartRadius` if you want to. 66 | 67 | ### Removals 68 | 69 | 79 | 80 | ### Improvements 81 | 82 | - Significant performance and RAM usage due to general refactorings, SVGs' simplification (with the same appearance), improved memoizations and micro performance improvements. 83 | - Now using `colord` package instead of `polished` to deal with colors' alpha. 84 | 85 | ### Fixes 86 | 87 | - [RTL in web](https://github.com/necolas/react-native-web/issues/2350#issuecomment-1193642853). 88 | - Error when there is more than a child. [#38](https://github.com/SrBrahma/react-native-shadow-2/issues/38) 89 | - Error when there isn't a child. [#38 (comment)](https://github.com/SrBrahma/react-native-shadow-2/issues/38#issuecomment-1059716569) 90 | - Situational 1-pixel overlap of corners. 91 | 92 |


93 | 94 | ## 6.0.6 - 2022-07-21 95 | 96 | - Fixed Web Shadow when there are more than one being rendered. [#53](https://github.com/SrBrahma/react-native-shadow-2/issues/53). Many thanks, [@GreyJohnsonGit](https://github.com/GreyJohnsonGit)! 97 | 98 | ## 6.0.5 - 2022-05-01 99 | 100 | - Fixed Android crash when `distance` and a radius are both 0. [#47](https://github.com/SrBrahma/react-native-shadow-2/issues/47). Many thanks, [@Czino](https://github.com/Czino)! 101 | 102 | ## 6.0.4 - 2022-04-12 103 | 104 | - Fixed missed children type when using the new @types/react 18. [#44](https://github.com/SrBrahma/react-native-shadow-2/issues/44). Thanks [@Czino](https://github.com/Czino) and [@hcharley](https://github.com/hcharley)! 105 | 106 | ## 6.0.3 - 2022-02-11 107 | 108 | - Fixed paintInside gaps on iOS. [#36](https://github.com/SrBrahma/react-native-shadow-2/issues/36). Thanks, [@walterholohan](https://github.com/walterholohan)! 109 | 110 | ## 6.0.2 - 2022-01-26 111 | 112 | - Changed `react-native-svg` peerDep version from `'*'` to `'^12.1.0'` to warn users using older and certainly incompatible versions. 113 | - Added `version` export, the package semver version. 114 | 115 | ## 6.0.1 - 2022-01-16 116 | 117 | - Fixed topRight corner using bottomRight radius on its positioning instead its own radius. [#33](https://github.com/SrBrahma/react-native-shadow-2/pull/33). Thanks, [@timqha](https://github.com/timqha)! 118 | 119 | ## 6.0.0 - 2022-01-03 120 | 121 | - Changed finalColor default to **transparent startColor** instead of `#000`. This results in a better gradient when startColor isn't black. [Explanation](https://github.com/SrBrahma/react-native-shadow-2/issues/31#issuecomment-985578972). As this (unlikely) may lead to unexpected visual results in your app, made this version a major. 122 | - Changed shadow wrapping View style from `width: '100%', height: '100%', position: 'absolute',` to `...StyleSheet.absoluteFillObject`. This fixed a strange overflowing shadow on the first render that happened in some specific case. 123 | - Added `pointerEvents='none'` to the shadow wrapping view. Thanks, [OriErel](https://github.com/OriErel)! [#32](https://github.com/SrBrahma/react-native-shadow-2/pull/32) 124 | - Added `shadowViewProps` property to set additional properties to the shadow wrapping view. [#32](https://github.com/SrBrahma/react-native-shadow-2/pull/32) 125 | - **tsconfig** - `module` from `commonjs` to `es6`, `jsx` from `react` to `react-native`, added `esModuleInterop: true` 126 | 127 | ## 5.1.2 - 2021-11-07 128 | 129 | - Changed tsconfig target from `esnext` to `es6`. [#29](https://github.com/SrBrahma/react-native-shadow-2/issues/29) 130 | 131 | ## 5.1.1 - 2021-10-27 132 | 133 | - Fixed the multi-children error being raised when > 1 child but `radius={0}`. 134 | 135 | ## 5.1.0 - 2021-10-02 136 | 137 | - This package now supports [RTL](https://reactnative.dev/blog/2016/08/19/right-to-left-support-for-react-native-apps). [#26](https://github.com/SrBrahma/react-native-shadow-2/issues/26). Thanks [@abdullahkn287](abdullahkn287) and [@serzh-f](https://github.com/serzh-f)! 138 | - [Web] - Removed `shape-rendering: 'crispEdges'` from the SVG parts as since 5.0.0 the Web shadow is pixel perfect (by properly rounding the sizes) and this previous semi-solution is no longer needed. 139 | 140 | ## 5.0.0 - 2021-09-19 141 | 142 | - Renamed `getChildRadiusStyle` property to `getChildRadius`. 143 | - Each corner radius is now limited using [this](https://css-tricks.com/what-happens-when-border-radii-overlap/). The link is for CSS but works in the same way for mobile. [#15](https://github.com/SrBrahma/react-native-shadow-2/issues/15). Thanks for the limit suggestion, [@jimmi-joensson](https://github.com/jimmi-joensson)! 144 | - Added safeRender property to only render on the 2nd render and beyond -- so, no relative rendering on the first render. This is useful when you want a pill/circle like shadow and you are inputting a radius greater than the corresponding sizes. On the future there may be a prop specific for those cases to have them working right on the first render. 145 | - In web looks like it's now completly free from the pixel gaps/overlaps on the 2nd render and beyond! 146 | - Added pointerEvents='box-none' to the container and content views, so clicks/presses go through them and your child may receive them. [#24](https://github.com/SrBrahma/react-native-shadow-2/issues/24). Thanks, [@AdamSheaHewett](https://github.com/AdamSheaHewett) 147 | - Fixed onLayout changes not taking effect when `size` prop was true then switched to false and then a new render was made. 148 | 149 | ## 4.1.0 - 2021-08-28 150 | 151 | - Added `getViewStyleRadius`. [#19](https://github.com/SrBrahma/react-native-shadow-2/issues/19). Thanks [@rbozan](https://github.com/rbozan)! 152 | - Added support for `borderTopStartRadius`, `borderTopEndRadius`, `borderBottomStartRadius`, `borderTopStartRadius` in `getChildRadiusStyle` and in the new `getViewStyleRadius`. Before, only `left, right, top, bottom` combinations were supported. 153 | - Code improvements. 154 | - Added `types` to package.json to display `TypeScript Types` in https://reactnative.directory. - [react-native-community/directory #707](https://github.com/react-native-community/directory/pull/707#issuecomment-906719165). Thanks, [@Simek](https://github.com/Simek)! 155 | 156 | ### NOTE: `getChildRadiusStyle` will be renamed to `getChildRadius` at the next major. 157 | 158 | ### Sandbox 159 | 160 | - Now using @sharcoux/slider instead of @react-native-community/slider 161 | 162 | ## 4.0.0 - 2021-08-16 163 | 164 | Not too many changes here, but they require a major semver change. It implements [#13](https://github.com/SrBrahma/react-native-shadow-2/issues/13) 165 | 166 | - `paintInside` now defaults to true **if** the `offset` property is defined. Else, it keeps its default to false; 167 | - Changed `viewStyle` type from `ViewStyle` to `StyleProp`. Thanks, [jimmi-joensson](https://github.com/jimmi-joensson)! 168 | - Renamed ShadowI to ShadowProps 169 | 170 | ## 3.0.0 - 2021-07-17 171 | 172 | - **Shadow with automatic size is applied on the same render!**. Lib rewritten to allow it. 1 month of pure suffering and despair trying to find new html/svg/react hacks to do what I wanted. :') #7, #8, #9, 173 | - Now it works on Web (React Native Web / Expo) 174 | - Added `getChildRadius` prop. 175 | - Added `paintInside` prop. 176 | - Added `viewStyle` prop. 177 | - Removed `contentViewStyle` prop. 178 | - Changed default `startColor` from `'#00000010'` to `'#00000020'`, so it's more noticeable when first trying the lib. 179 | - Looks like the pixel gaps/overlaps are all solved. It was a very long and frustrating marathon to achieve this! 180 | - [code] Added a partial README filler, using [this](https://github.com/tgreyuk/typedoc-plugin-markdown/issues/59#issuecomment-867300957) (mine!) 181 | - Added Sandbox! Used it a lot while developing this lib. 182 | - Changed minor functionalities 183 | - Minor fixes 184 | 185 | > We are calling this 3.0.0 because 2.0.0 would be ambiguous. One could think that the 1.0.0 is a reference to the original react-native-shadow package and the 2.0.0 would just be the react-native-shadow-2. 186 | 187 | ### 1.1.1 - 2021-03-23 188 | 189 | - Fixed sides shadow position when not having top/left side shadow. 190 | 191 | ## 1.1.0 - 2021-03-06 192 | 193 | - Added offset 194 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Henrique Bruno Fantauzzi de Almeida 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!IMPORTANT] 2 | > The new React Native **0.76**, released on **Oct 23, 2024**, [finally supports a cross-platform shadow](https://reactnative.dev/blog/2024/10/23/release-0.76-new-architecture#box-shadow-and-filter-style-props)! 3 | > 4 | > This new feature should be preferred over this library. 5 | > 6 | > **I can only wholeheartedly appreciate everyone's support and kindness over the past almost 4 years and celebrate having reached 40k weekly and 2M+ total downloads** 🤗 7 | 8 | --- 9 | 10 |
11 | 12 | ## 🚀 Check out my latest project — [NextStack](https://www.nextstack.gg)! 13 | 14 | 15 | NextStack OpenGraph Image 16 | 17 | 18 | --- 19 | 20 |
21 | 22 | # react-native-shadow-2 23 | 24 |
25 | 26 | [![npm](https://img.shields.io/npm/v/react-native-shadow-2)](https://www.npmjs.com/package/react-native-shadow-2) 27 | [![npm](https://img.shields.io/npm/dt/react-native-shadow-2)](https://www.npmjs.com/package/react-native-shadow-2) 28 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) 29 | 30 |
31 | 32 | [react-native-shadow](https://github.com/879479119/react-native-shadow) is dead for years. This is an improved version with more functionalities, TypeScript support, and written from scratch. It's not required to define its size: the shadow is smartly applied on the first render and then precisely reapplied on the following ones. 33 | 34 | It solves the old React Native issue of not having the same shadow appearance and usage for Android, iOS, and Web. 35 | 36 | Compatible with Android, iO,S and Web. **And Expo!** 37 | 38 | Supports [RTL](https://reactnative.dev/blog/2016/08/19/right-to-left-support-for-react-native-apps). 39 | 40 | 44 | 45 | ## [🍟 Demo - Expo Snack Sandbox](https://snack.expo.io/@srbrahma/react-native-shadow-2-sandbox) 46 | > Give this library a quick try! 47 | 48 | ## [📰 Changelog](./CHANGELOG.md) 49 | 50 | ### [❗ Read the FAQ below!](#️-faq) 51 | 52 | ## 💿 Installation 53 | 54 | #### 1. First, install [react-native-svg](https://github.com/react-native-svg/react-native-svg#installation). 55 | 56 | > The latest `react-native-svg` version is recommended, including if using Expo. 57 | 58 | #### 2. Then install react-native-shadow-2: 59 | 60 | ```bash 61 | npm i react-native-shadow-2 62 | # or 63 | yarn add react-native-shadow-2 64 | ``` 65 | 66 | 67 | ## 📖 Usage 68 | 69 | ```tsx 70 | import { Shadow } from 'react-native-shadow-2'; 71 | 72 | 73 | 🙂 74 | 75 | ``` 76 | 77 | ![Example 1](./resources/README/react-native-shadow-2-ex-1.png) 78 | 79 |
80 | 81 | ```tsx 82 | 83 | 84 | 🤯 85 | 86 | 87 | ``` 88 | 89 | ![Example 2](./resources/README/react-native-shadow-2-ex-2.png) 90 | 91 | ## Properties 92 | 93 | #### All properties are optional. 94 | | Property | Description | Type | Default 95 | | --- | --- | --- | --- 96 | | **startColor** | The initial gradient color of the shadow. | `string` | `'#00000020'` 97 | | **endColor** | The final gradient color of the shadow. | `string` | Transparent startColor. [Explanation](https://github.com/SrBrahma/react-native-shadow-2/issues/31#issuecomment-985578972). 98 | | **distance** | How far the shadow goes. | `number` | `10` 99 | | **offset** | Moves the shadow. Negative `x` moves it left/start, negative `y` moves it up.

Accepts `'x%'` values.

Defining this will default `paintInside` to **true**, as it's the usual desired behaviour. | `[x: string \| number, y: string \| number]` | `[0, 0]` 100 | | **paintInside** | Apply the shadow below/inside the content. `startColor` is used as fill color, without a gradient.

Useful when using `offset` or if your child has some transparency. | `boolean` | `false`, but `true` if `offset` is defined 101 | | **sides** | The sides that will have their shadows drawn. Doesn't include corners. Undefined sides fallbacks to **true**. [Explanation](https://github.com/SrBrahma/react-native-shadow-2/issues/76#issuecomment-1563276588). | `Record<'start' \| 'end' \| 'top' \| 'bottom', boolean>` | `undefined` 102 | | **corners** | The corners that will have their shadows drawn. Undefined corners fallbacks to **true**. [Explanation](https://github.com/SrBrahma/react-native-shadow-2/issues/76#issuecomment-1563276588). | `Record<'topStart' \| 'topEnd' \| 'bottomStart' \| 'bottomEnd', boolean>` | `undefined` 103 | | **style** | The style of the View that wraps your children. Read the [Notes](https://github.com/SrBrahma/react-native-shadow-2/edit/main/README.md#notes) below. | `StyleProp` | `undefined` 104 | | **containerStyle** | The style of the View that wraps the Shadow and your children. Useful for margins. | `StyleProp` | `undefined` 105 | | **stretch** | Make your children ocuppy all available horizontal space. [Explanation](https://github.com/SrBrahma/react-native-shadow-2/issues/7#issuecomment-899784537). | `boolean` | `false` 106 | | **safeRender** | Won't use the relative sizing and positioning on the 1st render but on the following renders with the exact onLayout sizes. Useful if dealing with radii greater than the sides sizes (like a circle) to avoid visual artifacts on the 1st render.

If `true`, the Shadow won't appear on the 1st render. | `boolean` | `false` 107 | | **disabled** | Disables the Shadow. Useful for easily reusing components as sometimes shadows are not desired.

`containerStyle` and `style` are still applied. | `boolean` | `false` 108 | 109 | ## Notes 110 | 111 | * If the Shadow has a single child, it will get the `width`, `height` and all of the `borderRadius` properties from the children's `style` property, if defined. 112 | 113 | * You may also define those properties in the Shadow's `style`. The defined properties here will have priority over the ones defined in the child's `style`. 114 | 115 | * If the `width` **and** `height` are defined in any of those, there will be only a single render, as the first automatic sizing won't happen, only the precise render. 116 | 117 | * You can use either the `'borderTopLeftRadius'` or `'borderTopStartRadius'` and their variations to define the corners radii, although I recommend the latter as it's the RTL standard. 118 | 119 | * [Having a radius greater than its side will mess the shadow if the sizes aren't defined](https://github.com/SrBrahma/react-native-shadow-2/issues/15). **You can use the `safeRender` property** to only show the shadow on the 2nd render and beyond, when we have the exact component size and the radii are properly limited. 120 | 121 | * [Radii greater than 2000 (Tailwind's `rounded-full` is 9999) may crash Android](https://github.com/SrBrahma/react-native-shadow-2/issues/46). 122 | 123 | 124 | 125 | ## ⁉️ FAQ 126 | 127 | * #### How to set the Shadow opacity? 128 | 129 | The opacity is set directly in the `startColor` and `endColor` properties, in the alpha channel. E.g.: `'#0001'` is an almost transparent black. You may also use `'#rrggbbaa'`, `'rgba()'`, `'hsla()'` etc. [All patterns in this link, but not int colors, are accepted](https://reactnative.dev/docs/colors). 130 | 131 | 132 | * #### My component is no longer using the available parent width after applying the Shadow! What to do? 133 | 134 | Use the `stretch` property or `style={{alignSelf: 'stretch'}}` in your Shadow component. [Explanation](https://github.com/SrBrahma/react-native-shadow-2/issues/7#issuecomment-899764882)! 135 | 136 | 137 | * #### I want a preset for my Shadows! 138 | 139 | It's exported the `ShadowProps` type, the props of the Shadow component. You may do the following: 140 | ```tsx 141 | const ShadowPresets = { 142 | button: { 143 | offset: [0, 1], distance: 1, startColor: '#0003', 144 | } as ShadowProps, 145 | }; 146 | 147 | 148 | ``` 149 | 150 | * #### The `offset` and `size` properties are throwing Typescript errors! 151 | 152 | Upgrade your Typescript to at least 4.0.0! Those two properties use [**labeled tuples**](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-0.html#labeled-tuple-elements). If you don't use Typescript, this won't happen. 153 | 154 | 155 | 162 | 163 | ## 📰 Popularly seen on 164 | * ### [LogRocket - Applying box shadows in React Native](https://blog.logrocket.com/applying-box-shadows-in-react-native/) 165 | * ### [V. Petrachin - Top 10 Libraries You Should Know for React Native in 2022](https://viniciuspetrachin.medium.com/top-10-libraries-you-should-know-for-react-native-d435e5209c96) 166 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-shadow-2", 3 | "version": "7.1.1", 4 | "description": "Cross-platform shadow for React Native. Improved version of the abandoned react-native-shadow package", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "files": [ 8 | "/lib" 9 | ], 10 | "scripts": { 11 | "test": "", 12 | "clean": "rimraf lib", 13 | "build": "npm run clean && tsc", 14 | "prepare": "npm run build", 15 | "prepublishOnly": "npm run test", 16 | "sandbox": "npm run --prefix ./sandbox web" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/SrBrahma/react-native-shadow-2.git" 21 | }, 22 | "keywords": [ 23 | "shadow", 24 | "shadows", 25 | "react-native", 26 | "native", 27 | "gradient", 28 | "ios", 29 | "android", 30 | "react", 31 | "cross-platform", 32 | "automatic", 33 | "typescript", 34 | "cross", 35 | "blur", 36 | "shadow-2", 37 | "native-shadow", 38 | "svg" 39 | ], 40 | "author": "Henrique Bruno (aka SrBrahma)", 41 | "license": "ISC", 42 | "bugs": { 43 | "url": "https://github.com/SrBrahma/react-native-shadow-2/issues" 44 | }, 45 | "homepage": "https://github.com/SrBrahma/react-native-shadow-2#readme", 46 | "dependencies": { 47 | "colord": "2.9.2" 48 | }, 49 | "devDependencies": { 50 | "@types/react-native": "*", 51 | "eslint-config-gev": "^3.2.0", 52 | "prettier": "^2.8.6", 53 | "prettier-config-gev": "^1.1.2", 54 | "react": "*", 55 | "react-native": "*", 56 | "react-native-svg": ">=12.1.0", 57 | "rimraf": "^3.0.0", 58 | "typescript": "^4.0.0" 59 | }, 60 | "peerDependencies": { 61 | "react-native": "*", 62 | "react-native-svg": ">=12.1.0" 63 | }, 64 | "prettier": "prettier-config-gev" 65 | } 66 | -------------------------------------------------------------------------------- /resources/README/react-native-shadow-2-ex-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ftzi/react-native-shadow-2/873fafd130cd97f9140a602fca28d7b55cea723f/resources/README/react-native-shadow-2-ex-1.png -------------------------------------------------------------------------------- /resources/README/react-native-shadow-2-ex-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ftzi/react-native-shadow-2/873fafd130cd97f9140a602fca28d7b55cea723f/resources/README/react-native-shadow-2-ex-2.png -------------------------------------------------------------------------------- /sandbox/.eslintrc.js: -------------------------------------------------------------------------------- 1 | // This is a workaround for https://github.com/eslint/eslint/issues/3458 2 | require('@rushstack/eslint-patch/modern-module-resolution'); 3 | 4 | module.exports = { 5 | root: true, 6 | env: { 7 | es2021: true, 8 | node: true, 9 | 'react-native/react-native': true, // *1 10 | }, 11 | extends: ['eslint-config-gev/react-native-js'], 12 | overrides: [ 13 | { 14 | files: ['*.ts', '*.tsx'], 15 | extends: ['eslint-config-gev/react-native'], 16 | parser: '@typescript-eslint/parser', 17 | parserOptions: { 18 | tsconfigRootDir: __dirname, 19 | project: ['./tsconfig.json'], 20 | ecmaVersion: 12, 21 | sourceType: 'module', 22 | ecmaFeatures: { 23 | jsx: true, // *1 24 | }, 25 | }, 26 | }, 27 | ], 28 | ignorePatterns: ['**/lib/**/*', '**/dist/**/*', '**/node_modules/**/*', '.eslintrc.js'], 29 | rules: {}, 30 | }; 31 | -------------------------------------------------------------------------------- /sandbox/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | npm-debug.* 4 | *.jks 5 | *.p8 6 | *.p12 7 | *.key 8 | *.mobileprovision 9 | *.orig.* 10 | web-build/ 11 | 12 | # macOS 13 | .DS_Store 14 | 15 | # gev 16 | *.env -------------------------------------------------------------------------------- /sandbox/App.tsx: -------------------------------------------------------------------------------- 1 | import { App } from './src/App'; 2 | 3 | export default App; 4 | -------------------------------------------------------------------------------- /sandbox/README.md: -------------------------------------------------------------------------------- 1 | Accessible in: 2 | ## [🍟 Expo Snack Sandbox](https://snack.expo.io/@srbrahma/react-native-shadow-2-sandbox) 3 | 4 | -=-=-=-=- 5 | 6 | To run your project, navigate to the directory and run one of the following npm commands: 7 | - npm start # you can open iOS, Android, or web from here, or run them directly with the commands below. 8 | - npm run android 9 | - npm run ios # requires an iOS device or macOS for access to an iOS simulator 10 | - npm run web 11 | 12 | 13 | Did https://reactnative.dev/docs/typescript#using-custom-path-aliases-with-typescript to alias to ../src/index.tsx -------------------------------------------------------------------------------- /sandbox/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "sandbox", 4 | "slug": "sandbox", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "splash": { 8 | "resizeMode": "contain", 9 | "backgroundColor": "#ffffff" 10 | }, 11 | "updates": { 12 | "fallbackToCacheTimeout": 0 13 | }, 14 | "assetBundlePatterns": ["**/*"], 15 | "ios": { 16 | "supportsTablet": true 17 | }, 18 | "android": { 19 | "adaptiveIcon": { 20 | "backgroundColor": "#FFFFFF" 21 | } 22 | }, 23 | "web": { 24 | // https://github.com/akveo/react-native-ui-kitten/issues/996#issuecomment-790975093 25 | "build": { "babel": { "include": ["pagescrollview"] } } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /sandbox/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /sandbox/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "node_modules/expo/AppEntry.js", 3 | "scripts": { 4 | "start": "expo start --web", 5 | "android": "expo start --android", 6 | "ios": "expo start --ios", 7 | "web": "expo start --web" 8 | }, 9 | "dependencies": { 10 | "@sharcoux/slider": "^6.1.1", 11 | "colord": "2.9.2", 12 | "expo": "^48.0.0", 13 | "pagescrollview": "^2.2.0", 14 | "react": "18.2.0", 15 | "react-dom": "18.2.0", 16 | "react-native": "0.71.6", 17 | "react-native-safe-area-context": "4.5.0", 18 | "react-native-svg": "13.4.0", 19 | "react-native-web": "~0.18.11", 20 | "tinycolor2": "1.4.2" 21 | }, 22 | "devDependencies": { 23 | "@babel/core": "^7.20.0", 24 | "@types/react": "~18.0.27", 25 | "@types/react-native": "*", 26 | "@types/tinycolor2": "1.4.3", 27 | "eslint-config-gev": "^3.2.0", 28 | "expo-cli": "^6.0.1", 29 | "prettier": "^2.8.6", 30 | "prettier-config-gev": "^1.1.2", 31 | "typescript": "^4.9.4" 32 | }, 33 | "private": true 34 | } 35 | -------------------------------------------------------------------------------- /sandbox/src/App.tsx: -------------------------------------------------------------------------------- 1 | // Sandbox to test the library. 2 | // Using a copy of the lib code here. 3 | import type React from 'react'; 4 | import { useEffect, useState } from 'react'; 5 | import { 6 | I18nManager, 7 | Platform, 8 | Pressable, 9 | StatusBar, 10 | StyleSheet, 11 | Switch, 12 | Text, 13 | TextInput, 14 | View, 15 | } from 'react-native'; 16 | import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context'; 17 | import { Slider } from '@sharcoux/slider'; 18 | import { PageScrollView } from 'pagescrollview'; 19 | import tinycolor from 'tinycolor2'; 20 | import { Shadow } from './shadow/index'; // Aliased in Sandbox in dev. 21 | 22 | const defaults = { 23 | distace: 50, 24 | borderRadius: 30, 25 | width: 200, 26 | height: 200, 27 | startColor: tinycolor('#00000020').toHex8String(), 28 | finalColor: tinycolor('#0000').toHex8String(), 29 | childColor: tinycolor('#fff').toHex8String(), // tinycolor('#fff').toHex8String(), 30 | }; 31 | 32 | export const App: React.FC = () => { 33 | const [distance, setDistance] = useState(defaults.distace); 34 | 35 | const [borderRadii, setBorderRadii] = useState<{ 36 | borderBottomLeftRadius: number; 37 | borderBottomRightRadius: number; 38 | borderTopLeftRadius: number; 39 | borderTopRightRadius: number; 40 | }>({ 41 | borderBottomLeftRadius: defaults.borderRadius, 42 | borderBottomRightRadius: defaults.borderRadius, 43 | borderTopLeftRadius: defaults.borderRadius, 44 | borderTopRightRadius: defaults.borderRadius, 45 | }); 46 | 47 | const [offsetX, setOffsetX] = useState(0); 48 | const [offsetY, setOffsetY] = useState(0); 49 | 50 | const [size, setSize] = useState([defaults.width, defaults.height] as [number, number]); 51 | const [doUseSizeStyle, setDoUseSizeProp] = useState(true); 52 | 53 | const [childWidth, setChildWidth] = useState(defaults.width); 54 | const [childHeight, setChildHeight] = useState(defaults.height); 55 | 56 | const [startColor, setStartColor] = useState(defaults.startColor); 57 | const [finalColor, setFinalColor] = useState(defaults.finalColor); 58 | const [childColor, setChildColor] = useState(defaults.childColor); 59 | 60 | const [disabled, setDisabled] = useState(false); 61 | 62 | const [rtl, setRtl] = useState(false); 63 | 64 | useEffect(() => I18nManager.forceRTL(rtl), [rtl]); 65 | 66 | return ( 67 | 68 | 69 | 70 | 71 | {`react-native-shadow-2 sandbox`} 72 | {`By SrBrahma @ https://github.com/SrBrahma/react-native-shadow-2`} 75 | 76 | 77 | {/** Can't get this scroll to work properly in web */} 78 | 79 | {/** View necessary so the settings won't grow too large in width */} 80 | 81 | setSize([v, size[1]])} 87 | /> 88 | setSize([size[0], v])} 94 | /> 95 | 103 | 110 | 117 | 124 | {/* */} 127 | 131 | setBorderRadii({ ...borderRadii, borderTopLeftRadius }) 132 | } 133 | range={[-10, 100]} 134 | step={0.1} 135 | /> 136 | 140 | setBorderRadii({ ...borderRadii, borderTopRightRadius }) 141 | } 142 | range={[-10, 100]} 143 | step={0.1} 144 | /> 145 | 149 | setBorderRadii({ ...borderRadii, borderBottomLeftRadius }) 150 | } 151 | range={[-10, 100]} 152 | step={0.1} 153 | /> 154 | 158 | setBorderRadii({ ...borderRadii, borderBottomRightRadius }) 159 | } 160 | range={[-10, 100]} 161 | step={0.1} 162 | /> 163 | 169 | 175 | 176 | 177 | { 182 | const color = tinycolor(text); 183 | if (color.isValid()) 184 | // Only change if valid input 185 | setStartColor(color.toHex8String()); 186 | }} 187 | /> 188 | 189 | 190 | { 195 | const color = tinycolor(text); 196 | if (color.isValid()) setFinalColor(color.toHex8String()); 197 | }} 198 | /> 199 | 200 | 201 | { 206 | const color = tinycolor(text); 207 | if (color.isValid()) setChildColor(color.toHex8String()); 208 | }} 209 | /> 210 | 211 | 212 | 213 | 214 | {/* Max child width is 200 and max dist is 100. Total max is 400. */} 215 | 218 | 233 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | ); 249 | }; 250 | 251 | const NameValue: React.FC<{ 252 | name: string; 253 | value: string | number | boolean | undefined; 254 | valueMonospace?: boolean; 255 | }> = ({ name, value, valueMonospace = false }) => { 256 | const prettyValue = 257 | typeof value === 'number' ? value.toFixed(1).replace(/[.,]0+$/, '') : String(value); // https://stackoverflow.com/a/5623195/10247962 258 | return ( 259 | 266 | {name} 267 | 274 | {prettyValue} 275 | 276 | 277 | ); 278 | }; 279 | 280 | const MySlider: React.FC<{ 281 | name: string; 282 | step?: number; 283 | range: [min: number, max: number]; 284 | value: number; 285 | onValueChange: (value: number) => void; 286 | }> = ({ name, step = 1, range, value, onValueChange }) => { 287 | return ( 288 | 289 | 290 | 291 | onValueChange(value - step)} 293 | style={({ pressed }) => [styles.decIncButton, pressed && { backgroundColor: '#bbb' }]} 294 | > 295 | 296 | {'-'} 297 | 298 | 299 | 305 | onValueChange(value + step)} 307 | style={({ pressed }) => [styles.decIncButton, pressed && { backgroundColor: '#bbb' }]} 308 | > 309 | 310 | {'+'} 311 | 312 | 313 | 314 | 315 | ); 316 | }; 317 | 318 | const MySwitch: React.FC<{ 319 | name: string; 320 | value: boolean; 321 | description?: string; 322 | onValueChange: (value: boolean) => void; 323 | }> = ({ name, onValueChange, value, description }) => { 324 | return ( 325 | 326 | 327 | {name} 328 | 329 | 330 | 331 | {/* I couldn't fking stop the text from growing the settings view, so I made this workaround. */} 332 | {description?.split('\n')?.map((t) => ( 333 | 334 | {t} 335 | 336 | ))} 337 | 338 | 339 | ); 340 | }; 341 | 342 | // Flex all the way up to settings ScrollView: https://necolas.github.io/react-native-web/docs/scroll-view/ 343 | const styles = StyleSheet.create({ 344 | container: { 345 | flex: 1, 346 | paddingHorizontal: 30, 347 | backgroundColor: '#f0f0f0', 348 | alignItems: 'center', 349 | justifyContent: 'center', 350 | }, 351 | title: { 352 | textAlign: 'center', 353 | fontWeight: 'bold', 354 | fontSize: 30, 355 | marginTop: 20, 356 | marginBottom: 20, 357 | }, 358 | subtitle: { 359 | textAlign: 'center', 360 | fontWeight: 'bold', 361 | fontSize: 14, 362 | color: '#444', 363 | marginBottom: 10, 364 | }, 365 | sandbox: { 366 | flex: 1, 367 | flexWrap: 'wrap-reverse', // to make it responsive, with the shadow component being above the settings on small screens 368 | flexDirection: 'row', 369 | alignItems: 'flex-end', 370 | alignContent: 'space-between', 371 | justifyContent: 'space-evenly', 372 | paddingBottom: 40, 373 | }, 374 | settings: { 375 | borderRadius: 8, 376 | backgroundColor: '#e5e5e5', 377 | paddingHorizontal: 40, 378 | paddingVertical: 30, 379 | marginTop: 20, 380 | alignItems: 'center', 381 | justifyContent: 'center', 382 | }, 383 | description: { 384 | color: '#222', 385 | fontStyle: 'italic', 386 | includeFontPadding: false, 387 | }, 388 | textInput: { 389 | backgroundColor: '#fff', 390 | borderColor: '#222', 391 | borderRadius: 3, 392 | paddingVertical: 3, 393 | textAlign: 'center', 394 | textAlignVertical: 'center', 395 | paddingHorizontal: 8, 396 | fontSize: 14, 397 | borderWidth: StyleSheet.hairlineWidth, 398 | marginBottom: 12, 399 | fontFamily: Platform.OS === 'android' ? 'monospace' : 'Courier', 400 | }, 401 | decIncButton: { 402 | backgroundColor: '#fff', 403 | padding: 5, 404 | paddingHorizontal: 12, 405 | borderRadius: 4, 406 | }, 407 | }); 408 | -------------------------------------------------------------------------------- /sandbox/src/shadow/index.tsx: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/consistent-type-imports 2 | import React, { Children, useMemo, useState } from 'react'; 3 | import type { StyleProp, ViewProps, ViewStyle } from 'react-native'; 4 | import { I18nManager, StyleSheet, View } from 'react-native'; 5 | import { Defs, LinearGradient, Mask, Path, Rect, Stop, Svg } from 'react-native-svg'; 6 | import { colord } from 'colord'; 7 | import type { 8 | Corner, 9 | CornerRadius, 10 | CornerRadiusShadow, 11 | RadialGradientPropsOmited, 12 | Side, 13 | Size, 14 | } from './utils'; 15 | import { 16 | additional, 17 | cornersArray, 18 | divDps, 19 | generateGradientIdSuffix, 20 | objFromKeys, 21 | P, 22 | R, 23 | radialGradient, 24 | rtlAbsoluteFillObject, 25 | rtlScaleX, 26 | scale, 27 | sumDps, 28 | } from './utils'; 29 | 30 | /** Package Semver. Used on the [Snack](https://snack.expo.dev/@srbrahma/react-native-shadow-2-sandbox). */ 31 | export const version = '7.0.7'; 32 | 33 | export interface ShadowProps { 34 | /** The color of the shadow when it's right next to the given content, leaving it. 35 | * Accepts alpha channel. 36 | * 37 | * @default '#00000020' */ 38 | startColor?: string; 39 | /** The color of the shadow at the maximum distance from the content. Accepts alpha channel. 40 | * 41 | * It defaults to a transparent color of `startColor`. E.g.: `startColor` is `#f00`, so it defaults to `#f000`. [Reason here](https://github.com/SrBrahma/react-native-shadow-2/issues/31#issuecomment-985578972). 42 | * 43 | * @default Transparent startColor */ 44 | endColor?: string; 45 | /** How far the shadow goes. 46 | * @default 10 */ 47 | distance?: number; 48 | /** The sides that have the shadows drawn. Doesn't include corners. 49 | * 50 | * Undefined sides fallbacks to true. 51 | * 52 | * @default undefined */ 53 | // We are using the raw type here instead of Side/Corner so TypeDoc/Readme output is better for the users, won't be just `Side`. 54 | sides?: Record<'start' | 'end' | 'top' | 'bottom', boolean>; 55 | /** The corners that have the shadows drawn. 56 | * 57 | * Undefined corners fallbacks to true. 58 | * 59 | * @default undefined */ 60 | corners?: Record<'topStart' | 'topEnd' | 'bottomStart' | 'bottomEnd', boolean>; 61 | /** Moves the shadow. Negative x moves it to the left, negative y moves it up. 62 | * 63 | * Accepts `'x%'` values, in relation to the child's size. 64 | * 65 | * Setting an offset will default `paintInside` to true. 66 | * 67 | * @default [0, 0] */ 68 | offset?: [x: number | string, y: number | string]; 69 | /** If the shadow should be applied inside the external shadows, below the child. `startColor` is used as fill color. 70 | * 71 | * You may want this as true when using offset or if your child have some transparency. 72 | * 73 | * **The default changes to true if `offset` property is defined.** 74 | * 75 | * @default false */ 76 | paintInside?: boolean; 77 | /** Style of the View that wraps your child component. 78 | * 79 | * You may set here the corners radii (e.g. borderTopLeftRadius) and the width/height. */ 80 | style?: StyleProp; 81 | /** Style of the view that wraps the shadow and your child component. */ 82 | containerStyle?: StyleProp; 83 | /** If you don't want the relative sizing and positioning of the shadow on the first render, but only on the second render and 84 | * beyond with the exact onLayout's sizes. This is useful if dealing with radius greater than the sizes, to assure 85 | * the fully round corners when the sides sizes are unknown and to avoid weird and overflowing shadows on the first render. 86 | * 87 | * Note that when true, the shadow won't appear on the first render. 88 | * 89 | * @default false */ 90 | safeRender?: boolean; 91 | /** Use this when you want your children to ocuppy all available cross-axis/horizontal space. 92 | * 93 | * Shortcut to `style={{alignSelf: 'stretch'}}. 94 | * 95 | * [Explanation](https://github.com/SrBrahma/react-native-shadow-2/issues/7#issuecomment-899784537) 96 | * 97 | * @default false */ 98 | stretch?: boolean; 99 | /** Won't render the Shadow. Useful for reusing components as sometimes shadows are not wanted. 100 | * 101 | * The children will be wrapped by two Views. `containerStyle` and `style` are still applied. 102 | * 103 | * For performance, contrary to `disabled={false}`, the children's corners radii aren't set in `style`. 104 | * This is done in "enabled" to limit Pressable's ripple as we already obtain those values. 105 | * 106 | * @default false */ 107 | disabled?: boolean; 108 | /** Props for the container's wrapping View. You probably don't need to use this. */ 109 | containerViewProps?: ViewProps; 110 | /** Props for the shadow's wrapping View. You probably don't need to use this. You may pass `style` to this. */ 111 | shadowViewProps?: ViewProps; 112 | /** Props for the children's wrapping View. You probably't don't need to use this. */ 113 | childrenViewProps?: ViewProps; 114 | /** Your child component. */ 115 | children?: React.ReactNode; 116 | } 117 | 118 | // For better memoization and performance. 119 | const emptyObj: Record = {}; 120 | const defaultOffset = [0, 0] as [x: number | string, y: number | string]; 121 | 122 | export function Shadow(props: ShadowProps): JSX.Element { 123 | return props.disabled ? : ; 124 | } 125 | 126 | function ShadowInner(props: ShadowProps): JSX.Element { 127 | /** getConstants().isRTL instead of just isRTL due to Web https://github.com/necolas/react-native-web/issues/2350#issuecomment-1193642853 */ 128 | const isRTL = I18nManager.getConstants().isRTL; 129 | const [childLayout, setChildLayout] = useState(); 130 | const [idSuffix] = useState(generateGradientIdSuffix); 131 | 132 | const { 133 | sides, 134 | corners, 135 | startColor: startColorProp, 136 | endColor: endColorProp, 137 | distance: distanceProp, 138 | style: styleProp, 139 | safeRender, 140 | stretch, 141 | /** Defaults to true if offset is defined, else defaults to false */ 142 | paintInside = props.offset ? true : false, 143 | offset = defaultOffset, 144 | children, 145 | containerStyle, 146 | shadowViewProps, 147 | childrenViewProps, 148 | containerViewProps, 149 | } = props; 150 | 151 | /** `s` is a shortcut for `style` I am using in another lib of mine (react-native-gev). While currently no one uses it besides me, 152 | * I believe it may come to be a popular pattern eventually :) */ 153 | const childProps: { style?: ViewStyle; s?: ViewStyle } = 154 | Children.count(children) === 1 155 | ? (Children.only(children) as JSX.Element).props ?? emptyObj 156 | : emptyObj; 157 | 158 | const childStyleStr: string | null = useMemo( 159 | () => (childProps.style ? JSON.stringify(childProps.style) : null), 160 | [childProps.style], 161 | ); 162 | const childSStr: string | null = useMemo( 163 | () => (childProps.s ? JSON.stringify(childProps.s) : null), 164 | [childProps.s], 165 | ); 166 | 167 | /** Child's style. */ 168 | const cStyle: ViewStyle = useMemo(() => { 169 | const cStyle = StyleSheet.flatten([ 170 | childStyleStr && JSON.parse(childStyleStr), 171 | childSStr && JSON.parse(childSStr), 172 | ]); 173 | if (typeof cStyle.width === 'number') cStyle.width = R(cStyle.width); 174 | if (typeof cStyle.height === 'number') cStyle.height = R(cStyle.height); 175 | return cStyle; 176 | }, [childSStr, childStyleStr]); 177 | 178 | /** Child's Radii. */ 179 | const cRadii: Record = useMemo(() => { 180 | return { 181 | topStart: cStyle.borderTopStartRadius ?? cStyle.borderTopLeftRadius ?? cStyle.borderRadius, 182 | topEnd: cStyle.borderTopEndRadius ?? cStyle.borderTopRightRadius ?? cStyle.borderRadius, 183 | bottomStart: 184 | cStyle.borderBottomStartRadius ?? cStyle.borderBottomLeftRadius ?? cStyle.borderRadius, 185 | bottomEnd: 186 | cStyle.borderBottomEndRadius ?? cStyle.borderBottomRightRadius ?? cStyle.borderRadius, 187 | }; 188 | }, [cStyle]); 189 | 190 | const styleStr: string | null = useMemo( 191 | () => (styleProp ? JSON.stringify(styleProp) : null), 192 | [styleProp], 193 | ); 194 | 195 | /** Flattened style. */ 196 | const { style, sRadii }: { style: ViewStyle; sRadii: Record } = 197 | useMemo(() => { 198 | const style = styleStr ? StyleSheet.flatten(JSON.parse(styleStr)) : {}; 199 | if (typeof style.width === 'number') style.width = R(style.width); 200 | if (typeof style.height === 'number') style.height = R(style.height); 201 | return { 202 | style, 203 | sRadii: { 204 | topStart: style.borderTopStartRadius ?? style.borderTopLeftRadius ?? style.borderRadius, 205 | topEnd: style.borderTopEndRadius ?? style.borderTopRightRadius ?? style.borderRadius, 206 | bottomStart: 207 | style.borderBottomStartRadius ?? style.borderBottomLeftRadius ?? style.borderRadius, 208 | bottomEnd: 209 | style.borderBottomEndRadius ?? style.borderBottomRightRadius ?? style.borderRadius, 210 | }, 211 | }; 212 | }, [styleStr]); 213 | 214 | const styleWidth = style.width ?? cStyle.width; 215 | const width = styleWidth ?? childLayout?.width ?? '100%'; // '100%' sometimes will lead to gaps. Child's size don't lie. 216 | const styleHeight = style.height ?? cStyle.height; 217 | const height = styleHeight ?? childLayout?.height ?? '100%'; 218 | 219 | const radii: CornerRadius = useMemo( 220 | () => 221 | sanitizeRadii({ 222 | width, 223 | height, 224 | radii: { 225 | topStart: sRadii.topStart ?? cRadii.topStart, 226 | topEnd: sRadii.topEnd ?? cRadii.topEnd, 227 | bottomStart: sRadii.bottomStart ?? cRadii.bottomStart, 228 | bottomEnd: sRadii.bottomEnd ?? cRadii.bottomEnd, 229 | }, 230 | }), 231 | [ 232 | width, 233 | height, 234 | sRadii.topStart, 235 | sRadii.topEnd, 236 | sRadii.bottomStart, 237 | sRadii.bottomEnd, 238 | cRadii.topStart, 239 | cRadii.topEnd, 240 | cRadii.bottomStart, 241 | cRadii.bottomEnd, 242 | ], 243 | ); 244 | 245 | const { topStart, topEnd, bottomStart, bottomEnd } = radii; 246 | 247 | const shadow = useMemo( 248 | () => 249 | getShadow({ 250 | topStart, 251 | topEnd, 252 | bottomStart, 253 | bottomEnd, 254 | width, 255 | height, 256 | isRTL, 257 | distanceProp, 258 | startColorProp, 259 | endColorProp, 260 | paintInside, 261 | safeRender, 262 | activeSides: { 263 | bottom: sides?.bottom ?? true, 264 | top: sides?.top ?? true, 265 | start: sides?.start ?? true, 266 | end: sides?.end ?? true, 267 | }, 268 | activeCorners: { 269 | topStart: corners?.topStart ?? true, 270 | topEnd: corners?.topEnd ?? true, 271 | bottomStart: corners?.bottomStart ?? true, 272 | bottomEnd: corners?.bottomEnd ?? true, 273 | }, 274 | idSuffix, 275 | }), 276 | [ 277 | width, 278 | height, 279 | distanceProp, 280 | startColorProp, 281 | endColorProp, 282 | topStart, 283 | topEnd, 284 | bottomStart, 285 | bottomEnd, 286 | paintInside, 287 | sides?.bottom, 288 | sides?.top, 289 | sides?.start, 290 | sides?.end, 291 | corners?.topStart, 292 | corners?.topEnd, 293 | corners?.bottomStart, 294 | corners?.bottomEnd, 295 | safeRender, 296 | isRTL, 297 | idSuffix, 298 | ], 299 | ); 300 | 301 | // Not yet sure if we should memo this. 302 | return getResult({ 303 | shadow, 304 | children, 305 | stretch, 306 | offset, 307 | radii, 308 | containerStyle, 309 | style, 310 | shadowViewProps, 311 | childrenViewProps, 312 | containerViewProps, 313 | styleWidth, 314 | styleHeight, 315 | childLayout, 316 | setChildLayout, 317 | }); 318 | } 319 | 320 | /** We make some effort for this to be likely memoized */ 321 | function sanitizeRadii({ 322 | width, 323 | height, 324 | radii, 325 | }: { 326 | width: string | number; 327 | height: string | number; 328 | /** Not yet treated. May be negative / undefined */ 329 | radii: Partial; 330 | }): CornerRadius { 331 | /** Round and zero negative radius values */ 332 | let radiiSanitized = objFromKeys(cornersArray, (k) => R(Math.max(radii[k] ?? 0, 0))); 333 | 334 | if (typeof width === 'number' && typeof height === 'number') { 335 | // https://css-tricks.com/what-happens-when-border-radii-overlap/ 336 | // Note that the tutorial above doesn't mention the specification of minRatio < 1 but it's required as said on spec and will malfunction without it. 337 | const minRatio = Math.min( 338 | divDps(width, sumDps(radiiSanitized.topStart, radiiSanitized.topEnd)), 339 | divDps(height, sumDps(radiiSanitized.topEnd, radiiSanitized.bottomEnd)), 340 | divDps(width, sumDps(radiiSanitized.bottomStart, radiiSanitized.bottomEnd)), 341 | divDps(height, sumDps(radiiSanitized.topStart, radiiSanitized.bottomStart)), 342 | ); 343 | 344 | if (minRatio < 1) 345 | // We ensure to use the .floor instead of the R else we could have the following case: 346 | // A topStart=3, topEnd=3 and width=5. This would cause a pixel overlap between those 2 corners. 347 | // The .floor ensures that the radii sum will be below the adjacent border length. 348 | radiiSanitized = objFromKeys( 349 | cornersArray, 350 | (k) => Math.floor(P(radiiSanitized[k]) * minRatio) / scale, 351 | ); 352 | } 353 | 354 | return radiiSanitized; 355 | } 356 | 357 | /** The SVG parts. */ 358 | // We default the props here for a micro improvement in performance. endColorProp default value was the main reason. 359 | function getShadow({ 360 | safeRender, 361 | width, 362 | height, 363 | isRTL, 364 | distanceProp = 10, 365 | startColorProp = '#00000020', 366 | endColorProp = colord(startColorProp).alpha(0).toHex(), 367 | topStart, 368 | topEnd, 369 | bottomStart, 370 | bottomEnd, 371 | activeSides, 372 | activeCorners, 373 | paintInside, 374 | idSuffix, 375 | }: { 376 | safeRender: boolean | undefined; 377 | width: string | number; 378 | height: string | number; 379 | isRTL: boolean; 380 | distanceProp?: number; 381 | startColorProp?: string; 382 | endColorProp?: string; 383 | topStart: number; 384 | topEnd: number; 385 | bottomStart: number; 386 | bottomEnd: number; 387 | activeSides: Record; 388 | activeCorners: Record; 389 | paintInside: boolean; 390 | idSuffix: string; 391 | }): JSX.Element | null { 392 | // Skip if using safeRender and we still don't have the exact sizes, if we are still on the first render using the relative sizes. 393 | if (safeRender && (typeof width === 'string' || typeof height === 'string')) return null; 394 | 395 | const distance = R(Math.max(distanceProp, 0)); // Min val as 0 396 | 397 | // Quick return if not going to show up anything 398 | if (!distance && !paintInside) return null; 399 | 400 | const distanceWithAdditional = distance + additional; 401 | 402 | /** Will (+ additional), only if its value isn't '100%'. [*4] */ 403 | const widthWithAdditional = typeof width === 'string' ? width : width + additional; 404 | /** Will (+ additional), only if its value isn't '100%'. [*4] */ 405 | const heightWithAdditional = typeof height === 'string' ? height : height + additional; 406 | 407 | const startColord = colord(startColorProp); 408 | const endColord = colord(endColorProp); 409 | 410 | // [*1]: Seems that SVG in web accepts opacity in hex color, but in mobile gradient doesn't. 411 | // So we remove the opacity from the color, and only apply the opacity in stopOpacity, so in web 412 | // it isn't applied twice. 413 | const startColorWoOpacity = startColord.alpha(1).toHex(); 414 | const endColorWoOpacity = endColord.alpha(1).toHex(); 415 | 416 | const startColorOpacity = startColord.alpha(); 417 | const endColorOpacity = endColord.alpha(); 418 | 419 | // Fragment wasn't working for some reason, so, using array. 420 | const linearGradient = [ 421 | // [*1] In mobile, it's required for the alpha to be set in opacity prop to work. 422 | // In web, smaller offsets needs to come before, so offset={0} definition comes first. 423 | , 424 | , 425 | ]; 426 | 427 | const radialGradient2 = (p: RadialGradientPropsOmited) => 428 | radialGradient({ 429 | ...p, 430 | startColorWoOpacity, 431 | startColorOpacity, 432 | endColorWoOpacity, 433 | endColorOpacity, 434 | paintInside, 435 | }); 436 | 437 | const cornerShadowRadius: CornerRadiusShadow = { 438 | topStartShadow: sumDps(topStart, distance), 439 | topEndShadow: sumDps(topEnd, distance), 440 | bottomStartShadow: sumDps(bottomStart, distance), 441 | bottomEndShadow: sumDps(bottomEnd, distance), 442 | }; 443 | 444 | const { topStartShadow, topEndShadow, bottomStartShadow, bottomEndShadow } = cornerShadowRadius; 445 | 446 | /* Skip sides if we don't have a distance. */ 447 | const sides = distance > 0 && ( 448 | <> 449 | {/* Skip side if adjacents corners use its size already */} 450 | {activeSides.start && 451 | (typeof height === 'number' ? height > topStart + bottomStart : true) && ( 452 | 457 | 458 | 465 | {linearGradient} 466 | 467 | 468 | {/* I was using a Mask here to remove part of each side (same size as now, sum of related corners), but, 469 | just moving the rectangle outside its viewbox is already a mask!! -> svg overflow is cutten away. <- */} 470 | 476 | 477 | )} 478 | {activeSides.end && (typeof height === 'number' ? height > topEnd + bottomEnd : true) && ( 479 | 484 | 485 | 492 | {linearGradient} 493 | 494 | 495 | 501 | 502 | )} 503 | {activeSides.top && (typeof width === 'number' ? width > topStart + topEnd : true) && ( 504 | 514 | 515 | 516 | {linearGradient} 517 | 518 | 519 | 525 | 526 | )} 527 | {activeSides.bottom && 528 | (typeof width === 'number' ? width > bottomStart + bottomEnd : true) && ( 529 | 539 | 540 | 541 | {linearGradient} 542 | 543 | 544 | 550 | 551 | )} 552 | 553 | ); 554 | 555 | /* The anchor for the svgs path is the top left point in the corner square. 556 | The starting point is the clockwise external arc init point. */ 557 | /* Checking topLeftShadowEtc > 0 due to https://github.com/SrBrahma/react-native-shadow-2/issues/47. */ 558 | const corners = ( 559 | <> 560 | {activeCorners.topStart && topStartShadow > 0 && ( 561 | 566 | 567 | {radialGradient2({ 568 | id: `topStart.${idSuffix}`, 569 | top: true, 570 | left: !isRTL, 571 | radius: topStart, 572 | shadowRadius: topStartShadow, 573 | })} 574 | 575 | 580 | 581 | )} 582 | {activeCorners.topEnd && topEndShadow > 0 && ( 583 | 593 | 594 | {radialGradient2({ 595 | id: `topEnd.${idSuffix}`, 596 | top: true, 597 | left: isRTL, 598 | radius: topEnd, 599 | shadowRadius: topEndShadow, 600 | })} 601 | 602 | 603 | 604 | )} 605 | {activeCorners.bottomStart && bottomStartShadow > 0 && ( 606 | 616 | 617 | {radialGradient2({ 618 | id: `bottomStart.${idSuffix}`, 619 | top: false, 620 | left: !isRTL, 621 | radius: bottomStart, 622 | shadowRadius: bottomStartShadow, 623 | })} 624 | 625 | 630 | 631 | )} 632 | {activeCorners.bottomEnd && bottomEndShadow > 0 && ( 633 | 643 | 644 | {radialGradient2({ 645 | id: `bottomEnd.${idSuffix}`, 646 | top: false, 647 | left: isRTL, 648 | radius: bottomEnd, 649 | shadowRadius: bottomEndShadow, 650 | })} 651 | 652 | 657 | 658 | )} 659 | 660 | ); 661 | 662 | /** 663 | * Paint the inner area, so we can offset it. 664 | * [*2]: I tried redrawing the inner corner arc, but there would always be a small gap between the external shadows 665 | * and this internal shadow along the curve. So, instead we dont specify the inner arc on the corners when 666 | * paintBelow, but just use a square inner corner. And here we will just mask those squares in each corner. 667 | */ 668 | const inner = paintInside && ( 669 | 674 | {typeof width === 'number' && typeof height === 'number' ? ( 675 | // Maybe due to how react-native-svg handles masks in iOS, the paintInside would have gaps: https://github.com/SrBrahma/react-native-shadow-2/issues/36 676 | // We use Path as workaround to it. 677 | 686 | ) : ( 687 | <> 688 | 689 | 690 | {/* Paint all white, then black on border external areas to erase them */} 691 | 692 | {/* Remove the corners */} 693 | 694 | 701 | 708 | 716 | 717 | 718 | 725 | 726 | )} 727 | 728 | ); 729 | 730 | return ( 731 | <> 732 | {sides} 733 | {corners} 734 | {inner} 735 | 736 | ); 737 | } 738 | 739 | function getResult({ 740 | shadow, 741 | stretch, 742 | containerStyle, 743 | children, 744 | style, 745 | radii, 746 | offset, 747 | containerViewProps, 748 | shadowViewProps, 749 | childrenViewProps, 750 | styleWidth, 751 | styleHeight, 752 | childLayout, 753 | setChildLayout, 754 | }: { 755 | radii: CornerRadius; 756 | containerStyle: StyleProp; 757 | shadow: JSX.Element | null; 758 | children: any; 759 | style: ViewStyle; // Already flattened 760 | stretch: boolean | undefined; 761 | offset: [x: number | string, y: number | string]; 762 | containerViewProps: ViewProps | undefined; 763 | shadowViewProps: ViewProps | undefined; 764 | childrenViewProps: ViewProps | undefined; 765 | /** The style width. Tries to use the style prop then the child's style. */ 766 | styleWidth: string | number | undefined; 767 | /** The style height. Tries to use the style prop then the child's style. */ 768 | styleHeight: string | number | undefined; 769 | childLayout: Size | undefined; 770 | setChildLayout: React.Dispatch>; 771 | }): JSX.Element { 772 | // const isWidthPrecise = styleWidth; 773 | 774 | return ( 775 | // pointerEvents: https://github.com/SrBrahma/react-native-shadow-2/issues/24 776 | 777 | 786 | {shadow} 787 | 788 | { 806 | // For some reason, conditionally setting the onLayout wasn't working on condition change. 807 | // [web] [*3]: the width/height we get here is already rounded by RN, even if the real size according to the browser 808 | // inspector is decimal. It will round up if (>= .5), else, down. 809 | const eventLayout = e.nativeEvent.layout; 810 | // Change layout state if the style width/height is undefined or 'x%', or the sizes in pixels are different. 811 | if ( 812 | (typeof styleWidth !== 'number' && 813 | (childLayout?.width === undefined || 814 | P(eventLayout.width) !== P(childLayout.width))) || 815 | (typeof styleHeight !== 'number' && 816 | (childLayout?.height === undefined || 817 | P(eventLayout.height) !== P(childLayout.height))) 818 | ) 819 | setChildLayout({ width: eventLayout.width, height: eventLayout.height }); 820 | }} 821 | {...childrenViewProps} 822 | > 823 | {children} 824 | 825 | 826 | ); 827 | } 828 | 829 | function DisabledShadow({ 830 | stretch, 831 | containerStyle, 832 | children, 833 | style, 834 | childrenViewProps, 835 | containerViewProps, 836 | }: { 837 | containerStyle?: StyleProp; 838 | children?: any; 839 | style?: StyleProp; 840 | stretch?: boolean; 841 | containerViewProps?: ViewProps; 842 | childrenViewProps?: ViewProps; 843 | }): JSX.Element { 844 | return ( 845 | 846 | 851 | {children} 852 | 853 | 854 | ); 855 | } 856 | -------------------------------------------------------------------------------- /sandbox/src/shadow/utils.tsx: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-imports 2 | import React from 'react'; 3 | import { PixelRatio, Platform } from 'react-native'; 4 | import { RadialGradient, Stop } from 'react-native-svg'; 5 | 6 | export type Side = 'start' | 'end' | 'top' | 'bottom'; 7 | export type Corner = 'topStart' | 'topEnd' | 'bottomStart' | 'bottomEnd'; 8 | export type Size = { width: number | undefined; height: number | undefined }; 9 | export type CornerRadius = Record; 10 | 11 | // Add Shadow to the corner names 12 | export type CornerRadiusShadow = Record<`${Corner}Shadow`, number>; 13 | 14 | export const cornersArray = ['topStart', 'topEnd', 'bottomStart', 'bottomEnd'] as const; 15 | 16 | const isWeb = Platform.OS === 'web'; 17 | 18 | /** Rounds the given size to a pixel perfect size. */ 19 | export const R: (value: number) => number = isWeb 20 | ? // In Web, 1dp=1px. But it accepts decimal sizes, and it's somewhat problematic. 21 | // The size rounding is browser-dependent, so we do the decimal rounding for web by ourselves to have a 22 | // consistent behavior. We floor it, because it's better for the child to overlap by a pixel the right/bottom shadow part 23 | // than to have a pixel wide gap between them. 24 | Math.floor 25 | : PixelRatio.roundToNearestPixel; 26 | 27 | /** Converts dp to pixels. */ 28 | export const P: (value: number) => number = isWeb ? (v) => v : PixelRatio.getPixelSizeForLayoutSize; 29 | 30 | /** How many pixels for each dp. scale = pixels/dp */ 31 | export const scale = isWeb ? 1 : PixelRatio.get(); 32 | 33 | /** Converts two sizes to pixel for perfect math, sums them and converts the result back to dp. */ 34 | export const sumDps: (a: number, b: number) => number = isWeb 35 | ? (a, b) => a + b 36 | : (a, b) => R((P(a) + P(b)) / scale); 37 | 38 | /** Converts two sizes to pixel for perfect math, divides them and converts the result back to dp. */ 39 | export const divDps: (a: number, b: number) => number = isWeb 40 | ? (a, b) => a / b 41 | : (a, b) => P(a) / P(b); 42 | 43 | /** 44 | * [Android/ios?] [*4] A small safe margin for the svg sizes. 45 | * 46 | * It fixes some gaps that we had, as even that the svg size and the svg rect for example size were the same, this rect 47 | * would still strangely be cropped/clipped. We give this additional size to the svg so our rect/etc won't be unintendedly clipped. 48 | * 49 | * It doesn't mean 1 pixel, as RN uses dp sizing, it's just an arbitrary and big enough number. 50 | * */ 51 | export const additional = isWeb ? 0 : 1; 52 | 53 | /** Auxilary function to shorten code */ 54 | export function objFromKeys, Rtn>( 55 | keys: KeysArray, 56 | fun: (key: KeysArray[number]) => Rtn, 57 | ): Record { 58 | const result: Record = {} as any; 59 | for (const key of keys) result[key as KeysArray[number]] = fun(key); 60 | return result; 61 | } 62 | 63 | export const cornerToStyle = { 64 | topLeft: ['borderTopLeftRadius', 'borderTopStartRadius'], 65 | topRight: ['borderTopRightRadius', 'borderTopEndRadius'], 66 | bottomLeft: ['borderBottomLeftRadius', 'borderBottomStartRadius'], 67 | bottomRight: ['borderBottomRightRadius', 'borderBottomEndRadius'], 68 | } as const; 69 | 70 | type RadialGradientProps = { 71 | id: string; 72 | top: boolean; 73 | left: boolean; 74 | radius: number; 75 | shadowRadius: number; 76 | startColorWoOpacity: string; 77 | startColorOpacity: number; 78 | endColorWoOpacity: string; 79 | endColorOpacity: number; 80 | paintInside: boolean; 81 | }; 82 | export type RadialGradientPropsOmited = Omit< 83 | RadialGradientProps, 84 | `${'start' | 'end' | 'paintInside'}${string}` 85 | >; 86 | 87 | /** 88 | For iOS this is the last value before rounding to 1. 89 | We do this because react-native-svg in iOS won't consider Stops after the one with offset=1. 90 | This doesn't seem to affect the look of the corners on iOS. 91 | If it does, we will need to go back to the previous ( would throw [#56](https://github.com/SrBrahma/react-native-shadow-2/issues/56). 109 | I tried {paintInside ? : <>}, but it caused the another reported bug in the same issue. 110 | This if/else solution solves those react-native-svg strange limitations. 111 | I could try to have a wrapper function / dynamic children but those bugs were very unexpected, so I chose the Will-Work solution. 112 | */ 113 | if (paintInside) 114 | return ( 115 | 122 | 127 | 132 | {/* Ensure it stops painting after the radius if endColorOpacity isn't 0. */} 133 | 134 | 135 | ); 136 | else 137 | return ( 138 | 145 | {/* Don't paint the inner circle if not paintInside */} 146 | 147 | 152 | 157 | 158 | 159 | ); 160 | } 161 | 162 | /** 163 | * Generates a sufficiently unique suffix to add to gradient ids and prevent collisions. 164 | * 165 | * https://github.com/SrBrahma/react-native-shadow-2/pull/54 166 | */ 167 | export const generateGradientIdSuffix = (() => { 168 | let shadowGradientIdCounter = 0; 169 | return () => String(shadowGradientIdCounter++); 170 | })(); 171 | 172 | export const rtlScaleX = { transform: [{ scaleX: -1 }] }; 173 | 174 | /** 175 | * https://github.com/SrBrahma/react-native-shadow-2/issues/67 176 | */ 177 | export const rtlAbsoluteFillObject = { 178 | position: 'absolute', 179 | start: 0, 180 | end: 0, 181 | top: 0, 182 | bottom: 0, 183 | } as const; 184 | -------------------------------------------------------------------------------- /sandbox/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Expo", 4 | "extends": "expo/tsconfig.base", 5 | "compilerOptions": { 6 | "incremental": true, 7 | "strict": true, 8 | "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 9 | "useUnknownInCatchVariables": false, 10 | "allowJs": true 11 | }, 12 | "include": ["App.tsx", "src/**/*"], 13 | "exclude": ["node_modules/**/*", "*.json", ".*"] 14 | } 15 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/consistent-type-imports 2 | import React, { Children, useMemo, useState } from 'react'; 3 | import type { StyleProp, ViewProps, ViewStyle } from 'react-native'; 4 | import { I18nManager, StyleSheet, View } from 'react-native'; 5 | import { Defs, LinearGradient, Mask, Path, Rect, Stop, Svg } from 'react-native-svg'; 6 | import { colord } from 'colord'; 7 | import type { 8 | Corner, 9 | CornerRadius, 10 | CornerRadiusShadow, 11 | RadialGradientPropsOmited, 12 | Side, 13 | Size, 14 | } from './utils'; 15 | import { 16 | additional, 17 | cornersArray, 18 | divDps, 19 | generateGradientIdSuffix, 20 | objFromKeys, 21 | P, 22 | R, 23 | radialGradient, 24 | rtlAbsoluteFillObject, 25 | rtlScaleX, 26 | scale, 27 | sumDps, 28 | } from './utils'; 29 | 30 | /** Package Semver. Used on the [Snack](https://snack.expo.dev/@srbrahma/react-native-shadow-2-sandbox). */ 31 | export const version = '7.1.1'; 32 | 33 | export interface ShadowProps { 34 | /** The color of the shadow when it's right next to the given content, leaving it. 35 | * Accepts alpha channel. 36 | * 37 | * @default '#00000020' */ 38 | startColor?: string; 39 | /** The color of the shadow at the maximum distance from the content. Accepts alpha channel. 40 | * 41 | * It defaults to a transparent color of `startColor`. E.g.: `startColor` is `#f00`, so it defaults to `#f000`. [Reason here](https://github.com/SrBrahma/react-native-shadow-2/issues/31#issuecomment-985578972). 42 | * 43 | * @default Transparent startColor */ 44 | endColor?: string; 45 | /** How far the shadow goes. 46 | * @default 10 */ 47 | distance?: number; 48 | /** The sides that have the shadows drawn. Doesn't include corners. 49 | * 50 | * Undefined sides fallbacks to true. 51 | * 52 | * @default undefined */ 53 | // We are using the raw type here instead of Side/Corner so TypeDoc/Readme output is better for the users, won't be just `Side`. 54 | sides?: Partial>; 55 | /** The corners that have the shadows drawn. 56 | * 57 | * Undefined corners fallbacks to true. 58 | * 59 | * @default undefined */ 60 | corners?: Partial>; 61 | /** Moves the shadow. Negative x moves it to the left, negative y moves it up. 62 | * 63 | * Accepts `'x%'` values, in relation to the child's size. 64 | * 65 | * Setting an offset will default `paintInside` to true. 66 | * 67 | * @default [0, 0] */ 68 | offset?: [x: number | string, y: number | string]; 69 | /** If the shadow should be applied inside the external shadows, below the child. `startColor` is used as fill color. 70 | * 71 | * You may want this as true when using offset or if your child have some transparency. 72 | * 73 | * **The default changes to true if `offset` property is defined.** 74 | * 75 | * @default false */ 76 | paintInside?: boolean; 77 | /** Style of the View that wraps your child component. 78 | * 79 | * You may set here the corners radii (e.g. borderTopLeftRadius) and the width/height. */ 80 | style?: StyleProp; 81 | /** Style of the view that wraps the shadow and your child component. */ 82 | containerStyle?: StyleProp; 83 | /** If you don't want the relative sizing and positioning of the shadow on the first render, but only on the second render and 84 | * beyond with the exact onLayout's sizes. This is useful if dealing with radius greater than the sizes, to assure 85 | * the fully round corners when the sides sizes are unknown and to avoid weird and overflowing shadows on the first render. 86 | * 87 | * Note that when true, the shadow won't appear on the first render. 88 | * 89 | * @default false */ 90 | safeRender?: boolean; 91 | /** Use this when you want your children to ocuppy all available cross-axis/horizontal space. 92 | * 93 | * Shortcut to `style={{alignSelf: 'stretch'}}. 94 | * 95 | * [Explanation](https://github.com/SrBrahma/react-native-shadow-2/issues/7#issuecomment-899784537) 96 | * 97 | * @default false */ 98 | stretch?: boolean; 99 | /** Won't render the Shadow. Useful for reusing components as sometimes shadows are not wanted. 100 | * 101 | * The children will be wrapped by two Views. `containerStyle` and `style` are still applied. 102 | * 103 | * For performance, contrary to `disabled={false}`, the children's corners radii aren't set in `style`. 104 | * This is done in "enabled" to limit Pressable's ripple as we already obtain those values. 105 | * 106 | * @default false */ 107 | disabled?: boolean; 108 | /** Props for the container's wrapping View. You probably don't need to use this. */ 109 | containerViewProps?: ViewProps; 110 | /** Props for the shadow's wrapping View. You probably don't need to use this. You may pass `style` to this. */ 111 | shadowViewProps?: ViewProps; 112 | /** Props for the children's wrapping View. You probably't don't need to use this. */ 113 | childrenViewProps?: ViewProps; 114 | /** Your child component. */ 115 | children?: React.ReactNode; 116 | } 117 | 118 | // For better memoization and performance. 119 | const emptyObj: Record = {}; 120 | const defaultOffset = [0, 0] as [x: number | string, y: number | string]; 121 | 122 | export function Shadow(props: ShadowProps): JSX.Element { 123 | return props.disabled ? : ; 124 | } 125 | 126 | function ShadowInner(props: ShadowProps): JSX.Element { 127 | /** getConstants().isRTL instead of just isRTL due to Web https://github.com/necolas/react-native-web/issues/2350#issuecomment-1193642853 */ 128 | const isRTL = I18nManager.getConstants().isRTL; 129 | const [childLayout, setChildLayout] = useState(); 130 | const [idSuffix] = useState(generateGradientIdSuffix); 131 | 132 | const { 133 | sides, 134 | corners, 135 | startColor: startColorProp, 136 | endColor: endColorProp, 137 | distance: distanceProp, 138 | style: styleProp, 139 | safeRender, 140 | stretch, 141 | /** Defaults to true if offset is defined, else defaults to false */ 142 | paintInside = props.offset ? true : false, 143 | offset = defaultOffset, 144 | children, 145 | containerStyle, 146 | shadowViewProps, 147 | childrenViewProps, 148 | containerViewProps, 149 | } = props; 150 | 151 | /** `s` is a shortcut for `style` I am using in another lib of mine (react-native-gev). While currently no one uses it besides me, 152 | * I believe it may come to be a popular pattern eventually :) */ 153 | const childProps: { style?: ViewStyle; s?: ViewStyle } = 154 | Children.count(children) === 1 155 | ? (Children.only(children) as JSX.Element).props ?? emptyObj 156 | : emptyObj; 157 | 158 | const childStyleStr: string | null = useMemo( 159 | () => (childProps.style ? JSON.stringify(childProps.style) : null), 160 | [childProps.style], 161 | ); 162 | const childSStr: string | null = useMemo( 163 | () => (childProps.s ? JSON.stringify(childProps.s) : null), 164 | [childProps.s], 165 | ); 166 | 167 | /** Child's style. */ 168 | const cStyle: ViewStyle = useMemo(() => { 169 | const cStyle = StyleSheet.flatten([ 170 | childStyleStr && JSON.parse(childStyleStr), 171 | childSStr && JSON.parse(childSStr), 172 | ]); 173 | if (typeof cStyle.width === 'number') cStyle.width = R(cStyle.width); 174 | if (typeof cStyle.height === 'number') cStyle.height = R(cStyle.height); 175 | return cStyle; 176 | }, [childSStr, childStyleStr]); 177 | 178 | /** Child's Radii. */ 179 | const cRadii: Record = useMemo(() => { 180 | return { 181 | topStart: cStyle.borderTopStartRadius ?? cStyle.borderTopLeftRadius ?? cStyle.borderRadius, 182 | topEnd: cStyle.borderTopEndRadius ?? cStyle.borderTopRightRadius ?? cStyle.borderRadius, 183 | bottomStart: 184 | cStyle.borderBottomStartRadius ?? cStyle.borderBottomLeftRadius ?? cStyle.borderRadius, 185 | bottomEnd: 186 | cStyle.borderBottomEndRadius ?? cStyle.borderBottomRightRadius ?? cStyle.borderRadius, 187 | }; 188 | }, [cStyle]); 189 | 190 | const styleStr: string | null = useMemo( 191 | () => (styleProp ? JSON.stringify(styleProp) : null), 192 | [styleProp], 193 | ); 194 | 195 | /** Flattened style. */ 196 | const { style, sRadii }: { style: ViewStyle; sRadii: Record } = 197 | useMemo(() => { 198 | const style = styleStr ? StyleSheet.flatten(JSON.parse(styleStr)) : {}; 199 | if (typeof style.width === 'number') style.width = R(style.width); 200 | if (typeof style.height === 'number') style.height = R(style.height); 201 | return { 202 | style, 203 | sRadii: { 204 | topStart: style.borderTopStartRadius ?? style.borderTopLeftRadius ?? style.borderRadius, 205 | topEnd: style.borderTopEndRadius ?? style.borderTopRightRadius ?? style.borderRadius, 206 | bottomStart: 207 | style.borderBottomStartRadius ?? style.borderBottomLeftRadius ?? style.borderRadius, 208 | bottomEnd: 209 | style.borderBottomEndRadius ?? style.borderBottomRightRadius ?? style.borderRadius, 210 | }, 211 | }; 212 | }, [styleStr]); 213 | 214 | const styleWidth = style.width ?? cStyle.width; 215 | const width = styleWidth ?? childLayout?.width ?? '100%'; // '100%' sometimes will lead to gaps. Child's size don't lie. 216 | const styleHeight = style.height ?? cStyle.height; 217 | const height = styleHeight ?? childLayout?.height ?? '100%'; 218 | 219 | const radii: CornerRadius = useMemo( 220 | () => 221 | sanitizeRadii({ 222 | width, 223 | height, 224 | radii: { 225 | topStart: sRadii.topStart ?? cRadii.topStart, 226 | topEnd: sRadii.topEnd ?? cRadii.topEnd, 227 | bottomStart: sRadii.bottomStart ?? cRadii.bottomStart, 228 | bottomEnd: sRadii.bottomEnd ?? cRadii.bottomEnd, 229 | }, 230 | }), 231 | [ 232 | width, 233 | height, 234 | sRadii.topStart, 235 | sRadii.topEnd, 236 | sRadii.bottomStart, 237 | sRadii.bottomEnd, 238 | cRadii.topStart, 239 | cRadii.topEnd, 240 | cRadii.bottomStart, 241 | cRadii.bottomEnd, 242 | ], 243 | ); 244 | 245 | const { topStart, topEnd, bottomStart, bottomEnd } = radii; 246 | 247 | const shadow = useMemo( 248 | () => 249 | getShadow({ 250 | topStart, 251 | topEnd, 252 | bottomStart, 253 | bottomEnd, 254 | width, 255 | height, 256 | isRTL, 257 | distanceProp, 258 | startColorProp, 259 | endColorProp, 260 | paintInside, 261 | safeRender, 262 | activeSides: { 263 | bottom: sides?.bottom ?? true, 264 | top: sides?.top ?? true, 265 | start: sides?.start ?? true, 266 | end: sides?.end ?? true, 267 | }, 268 | activeCorners: { 269 | topStart: corners?.topStart ?? true, 270 | topEnd: corners?.topEnd ?? true, 271 | bottomStart: corners?.bottomStart ?? true, 272 | bottomEnd: corners?.bottomEnd ?? true, 273 | }, 274 | idSuffix, 275 | }), 276 | [ 277 | width, 278 | height, 279 | distanceProp, 280 | startColorProp, 281 | endColorProp, 282 | topStart, 283 | topEnd, 284 | bottomStart, 285 | bottomEnd, 286 | paintInside, 287 | sides?.bottom, 288 | sides?.top, 289 | sides?.start, 290 | sides?.end, 291 | corners?.topStart, 292 | corners?.topEnd, 293 | corners?.bottomStart, 294 | corners?.bottomEnd, 295 | safeRender, 296 | isRTL, 297 | idSuffix, 298 | ], 299 | ); 300 | 301 | // Not yet sure if we should memo this. 302 | return getResult({ 303 | shadow, 304 | children, 305 | stretch, 306 | offset, 307 | radii, 308 | containerStyle, 309 | style, 310 | shadowViewProps, 311 | childrenViewProps, 312 | containerViewProps, 313 | styleWidth, 314 | styleHeight, 315 | childLayout, 316 | setChildLayout, 317 | }); 318 | } 319 | 320 | /** We make some effort for this to be likely memoized */ 321 | function sanitizeRadii(props: { 322 | width: string | number; 323 | height: string | number; 324 | /** Not yet treated. May be negative / undefined */ 325 | radii: Partial; 326 | }): CornerRadius { 327 | /** Round and zero negative radius values */ 328 | let radiiSanitized = objFromKeys(cornersArray, (k) => R(Math.max(props.radii[k] ?? 0, 0))); 329 | 330 | if (typeof props.width === 'number' && typeof props.height === 'number') { 331 | // https://css-tricks.com/what-happens-when-border-radii-overlap/ 332 | // Note that the tutorial above doesn't mention the specification of minRatio < 1 but it's required as said on spec and will malfunction without it. 333 | const minRatio = Math.min( 334 | divDps(props.width, sumDps(radiiSanitized.topStart, radiiSanitized.topEnd)), 335 | divDps(props.height, sumDps(radiiSanitized.topEnd, radiiSanitized.bottomEnd)), 336 | divDps(props.width, sumDps(radiiSanitized.bottomStart, radiiSanitized.bottomEnd)), 337 | divDps(props.height, sumDps(radiiSanitized.topStart, radiiSanitized.bottomStart)), 338 | ); 339 | 340 | if (minRatio < 1) 341 | // We ensure to use the .floor instead of the R else we could have the following case: 342 | // A topStart=3, topEnd=3 and width=5. This would cause a pixel overlap between those 2 corners. 343 | // The .floor ensures that the radii sum will be below the adjacent border length. 344 | radiiSanitized = objFromKeys( 345 | cornersArray, 346 | (k) => Math.floor(P(radiiSanitized[k]) * minRatio) / scale, 347 | ); 348 | } 349 | 350 | return radiiSanitized; 351 | } 352 | 353 | /** The SVG parts. */ 354 | // We default the props here for a micro improvement in performance. endColorProp default value was the main reason. 355 | function getShadow({ 356 | safeRender, 357 | width, 358 | height, 359 | isRTL, 360 | distanceProp = 10, 361 | startColorProp = '#00000020', 362 | endColorProp, 363 | topStart, 364 | topEnd, 365 | bottomStart, 366 | bottomEnd, 367 | activeSides, 368 | activeCorners, 369 | paintInside, 370 | idSuffix, 371 | }: { 372 | safeRender: boolean | undefined; 373 | width: string | number; 374 | height: string | number; 375 | isRTL: boolean; 376 | distanceProp?: number; 377 | startColorProp?: string; 378 | endColorProp?: string; 379 | topStart: number; 380 | topEnd: number; 381 | bottomStart: number; 382 | bottomEnd: number; 383 | activeSides: Record; 384 | activeCorners: Record; 385 | paintInside: boolean; 386 | idSuffix: string; 387 | }): JSX.Element | null { 388 | // Skip if using safeRender and we still don't have the exact sizes, if we are still on the first render using the relative sizes. 389 | if (safeRender && (typeof width === 'string' || typeof height === 'string')) return null; 390 | 391 | const distance = R(Math.max(distanceProp, 0)); // Min val as 0 392 | 393 | // Quick return if not going to show up anything 394 | if (!distance && !paintInside) return null; 395 | 396 | const distanceWithAdditional = distance + additional; 397 | 398 | /** Will (+ additional), only if its value isn't '100%'. [*4] */ 399 | const widthWithAdditional = typeof width === 'string' ? width : width + additional; 400 | /** Will (+ additional), only if its value isn't '100%'. [*4] */ 401 | const heightWithAdditional = typeof height === 'string' ? height : height + additional; 402 | 403 | const startColord = colord(startColorProp); 404 | const endColord = endColorProp ? colord(endColorProp) : startColord.alpha(0); 405 | 406 | // [*1]: Seems that SVG in web accepts opacity in hex color, but in mobile gradient doesn't. 407 | // So we remove the opacity from the color, and only apply the opacity in stopOpacity, so in web 408 | // it isn't applied twice. 409 | const startColorWoOpacity = startColord.alpha(1).toHex(); 410 | const endColorWoOpacity = endColord.alpha(1).toHex(); 411 | 412 | const startColorOpacity = startColord.alpha(); 413 | const endColorOpacity = endColord.alpha(); 414 | 415 | // Fragment wasn't working for some reason, so, using array. 416 | const linearGradient = [ 417 | // [*1] In mobile, it's required for the alpha to be set in opacity prop to work. 418 | // In web, smaller offsets needs to come before, so offset={0} definition comes first. 419 | , 420 | , 421 | ]; 422 | 423 | const radialGradient2 = (p: RadialGradientPropsOmited) => 424 | radialGradient({ 425 | ...p, 426 | startColorWoOpacity, 427 | startColorOpacity, 428 | endColorWoOpacity, 429 | endColorOpacity, 430 | paintInside, 431 | }); 432 | 433 | const cornerShadowRadius: CornerRadiusShadow = { 434 | topStartShadow: sumDps(topStart, distance), 435 | topEndShadow: sumDps(topEnd, distance), 436 | bottomStartShadow: sumDps(bottomStart, distance), 437 | bottomEndShadow: sumDps(bottomEnd, distance), 438 | }; 439 | 440 | const { topStartShadow, topEndShadow, bottomStartShadow, bottomEndShadow } = cornerShadowRadius; 441 | 442 | /* Skip sides if we don't have a distance. */ 443 | const sides = distance > 0 && ( 444 | <> 445 | {/* Skip side if adjacents corners use its size already */} 446 | {activeSides.start && 447 | (typeof height === 'number' ? height > topStart + bottomStart : true) && ( 448 | 453 | 454 | 461 | {linearGradient} 462 | 463 | 464 | {/* I was using a Mask here to remove part of each side (same size as now, sum of related corners), but, 465 | just moving the rectangle outside its viewbox is already a mask!! -> svg overflow is cutten away. <- */} 466 | 472 | 473 | )} 474 | {activeSides.end && (typeof height === 'number' ? height > topEnd + bottomEnd : true) && ( 475 | 480 | 481 | 488 | {linearGradient} 489 | 490 | 491 | 497 | 498 | )} 499 | {activeSides.top && (typeof width === 'number' ? width > topStart + topEnd : true) && ( 500 | 510 | 511 | 512 | {linearGradient} 513 | 514 | 515 | 521 | 522 | )} 523 | {activeSides.bottom && 524 | (typeof width === 'number' ? width > bottomStart + bottomEnd : true) && ( 525 | 535 | 536 | 537 | {linearGradient} 538 | 539 | 540 | 546 | 547 | )} 548 | 549 | ); 550 | 551 | /* The anchor for the svgs path is the top left point in the corner square. 552 | The starting point is the clockwise external arc init point. */ 553 | /* Checking topLeftShadowEtc > 0 due to https://github.com/SrBrahma/react-native-shadow-2/issues/47. */ 554 | const corners = ( 555 | <> 556 | {activeCorners.topStart && topStartShadow > 0 && ( 557 | 562 | 563 | {radialGradient2({ 564 | id: `topStart.${idSuffix}`, 565 | top: true, 566 | left: !isRTL, 567 | radius: topStart, 568 | shadowRadius: topStartShadow, 569 | })} 570 | 571 | 576 | 577 | )} 578 | {activeCorners.topEnd && topEndShadow > 0 && ( 579 | 589 | 590 | {radialGradient2({ 591 | id: `topEnd.${idSuffix}`, 592 | top: true, 593 | left: isRTL, 594 | radius: topEnd, 595 | shadowRadius: topEndShadow, 596 | })} 597 | 598 | 599 | 600 | )} 601 | {activeCorners.bottomStart && bottomStartShadow > 0 && ( 602 | 612 | 613 | {radialGradient2({ 614 | id: `bottomStart.${idSuffix}`, 615 | top: false, 616 | left: !isRTL, 617 | radius: bottomStart, 618 | shadowRadius: bottomStartShadow, 619 | })} 620 | 621 | 626 | 627 | )} 628 | {activeCorners.bottomEnd && bottomEndShadow > 0 && ( 629 | 639 | 640 | {radialGradient2({ 641 | id: `bottomEnd.${idSuffix}`, 642 | top: false, 643 | left: isRTL, 644 | radius: bottomEnd, 645 | shadowRadius: bottomEndShadow, 646 | })} 647 | 648 | 653 | 654 | )} 655 | 656 | ); 657 | 658 | /** 659 | * Paint the inner area, so we can offset it. 660 | * [*2]: I tried redrawing the inner corner arc, but there would always be a small gap between the external shadows 661 | * and this internal shadow along the curve. So, instead we dont specify the inner arc on the corners when 662 | * paintBelow, but just use a square inner corner. And here we will just mask those squares in each corner. 663 | */ 664 | const inner = paintInside && ( 665 | 670 | {typeof width === 'number' && typeof height === 'number' ? ( 671 | // Maybe due to how react-native-svg handles masks in iOS, the paintInside would have gaps: https://github.com/SrBrahma/react-native-shadow-2/issues/36 672 | // We use Path as workaround to it. 673 | 680 | ) : ( 681 | <> 682 | 683 | 684 | {/* Paint all white, then black on border external areas to erase them */} 685 | 686 | {/* Remove the corners */} 687 | 688 | 695 | 702 | 710 | 711 | 712 | 719 | 720 | )} 721 | 722 | ); 723 | 724 | return ( 725 | <> 726 | {sides} 727 | {corners} 728 | {inner} 729 | 730 | ); 731 | } 732 | 733 | function getResult(props: { 734 | radii: CornerRadius; 735 | containerStyle: StyleProp; 736 | shadow: JSX.Element | null; 737 | children: any; 738 | style: ViewStyle; // Already flattened 739 | stretch: boolean | undefined; 740 | offset: [x: number | string, y: number | string]; 741 | containerViewProps: ViewProps | undefined; 742 | shadowViewProps: ViewProps | undefined; 743 | childrenViewProps: ViewProps | undefined; 744 | /** The style width. Tries to use the style prop then the child's style. */ 745 | styleWidth: string | number | undefined; 746 | /** The style height. Tries to use the style prop then the child's style. */ 747 | styleHeight: string | number | undefined; 748 | childLayout: Size | undefined; 749 | setChildLayout: React.Dispatch>; 750 | }): JSX.Element { 751 | // const isWidthPrecise = styleWidth; 752 | 753 | return ( 754 | // pointerEvents: https://github.com/SrBrahma/react-native-shadow-2/issues/24 755 | 756 | 765 | {props.shadow} 766 | 767 | { 785 | // For some reason, conditionally setting the onLayout wasn't working on condition change. 786 | // [web] [*3]: the width/height we get here is already rounded by RN, even if the real size according to the browser 787 | // inspector is decimal. It will round up if (>= .5), else, down. 788 | const eventLayout = e.nativeEvent.layout; 789 | // Change layout state if the style width/height is undefined or 'x%', or the sizes in pixels are different. 790 | if ( 791 | (typeof props.styleWidth !== 'number' && 792 | (props.childLayout?.width === undefined || 793 | P(eventLayout.width) !== P(props.childLayout.width))) || 794 | (typeof props.styleHeight !== 'number' && 795 | (props.childLayout?.height === undefined || 796 | P(eventLayout.height) !== P(props.childLayout.height))) 797 | ) 798 | props.setChildLayout({ width: eventLayout.width, height: eventLayout.height }); 799 | }} 800 | {...props.childrenViewProps} 801 | > 802 | {props.children} 803 | 804 | 805 | ); 806 | } 807 | 808 | function DisabledShadow(props: { 809 | containerStyle?: StyleProp; 810 | children?: any; 811 | style?: StyleProp; 812 | stretch?: boolean; 813 | containerViewProps?: ViewProps; 814 | childrenViewProps?: ViewProps; 815 | }): JSX.Element { 816 | return ( 817 | 818 | 823 | {props.children} 824 | 825 | 826 | ); 827 | } 828 | -------------------------------------------------------------------------------- /src/utils.tsx: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-imports 2 | import React from 'react'; 3 | import { PixelRatio, Platform } from 'react-native'; 4 | import { RadialGradient, Stop } from 'react-native-svg'; 5 | 6 | export type Side = 'start' | 'end' | 'top' | 'bottom'; 7 | export type Corner = 'topStart' | 'topEnd' | 'bottomStart' | 'bottomEnd'; 8 | export type Size = { width: number | undefined; height: number | undefined }; 9 | export type CornerRadius = Record; 10 | 11 | // Add Shadow to the corner names 12 | export type CornerRadiusShadow = Record<`${Corner}Shadow`, number>; 13 | 14 | export const cornersArray = ['topStart', 'topEnd', 'bottomStart', 'bottomEnd'] as const; 15 | 16 | const isWeb = Platform.OS === 'web'; 17 | 18 | /** Rounds the given size to a pixel perfect size. */ 19 | export const R: (value: number) => number = isWeb 20 | ? // In Web, 1dp=1px. But it accepts decimal sizes, and it's somewhat problematic. 21 | // The size rounding is browser-dependent, so we do the decimal rounding for web by ourselves to have a 22 | // consistent behavior. We floor it, because it's better for the child to overlap by a pixel the right/bottom shadow part 23 | // than to have a pixel wide gap between them. 24 | Math.floor 25 | : PixelRatio.roundToNearestPixel; 26 | 27 | /** Converts dp to pixels. */ 28 | export const P: (value: number) => number = isWeb ? (v) => v : PixelRatio.getPixelSizeForLayoutSize; 29 | 30 | /** How many pixels for each dp. scale = pixels/dp */ 31 | export const scale = isWeb ? 1 : PixelRatio.get(); 32 | 33 | /** Converts two sizes to pixel for perfect math, sums them and converts the result back to dp. */ 34 | export const sumDps: (a: number, b: number) => number = isWeb 35 | ? (a, b) => a + b 36 | : (a, b) => R((P(a) + P(b)) / scale); 37 | 38 | /** Converts two sizes to pixel for perfect math, divides them and converts the result back to dp. */ 39 | export const divDps: (a: number, b: number) => number = isWeb 40 | ? (a, b) => a / b 41 | : (a, b) => P(a) / P(b); 42 | 43 | /** 44 | * [Android/ios?] [*4] A small safe margin for the svg sizes. 45 | * 46 | * It fixes some gaps that we had, as even that the svg size and the svg rect for example size were the same, this rect 47 | * would still strangely be cropped/clipped. We give this additional size to the svg so our rect/etc won't be unintendedly clipped. 48 | * 49 | * It doesn't mean 1 pixel, as RN uses dp sizing, it's just an arbitrary and big enough number. 50 | * */ 51 | export const additional = isWeb ? 0 : 1; 52 | 53 | /** Auxilary function to shorten code */ 54 | export function objFromKeys, Rtn>( 55 | keys: KeysArray, 56 | fun: (key: KeysArray[number]) => Rtn, 57 | ): Record { 58 | const result: Record = {} as any; 59 | for (const key of keys) result[key as KeysArray[number]] = fun(key); 60 | return result; 61 | } 62 | 63 | export const cornerToStyle = { 64 | topLeft: ['borderTopLeftRadius', 'borderTopStartRadius'], 65 | topRight: ['borderTopRightRadius', 'borderTopEndRadius'], 66 | bottomLeft: ['borderBottomLeftRadius', 'borderBottomStartRadius'], 67 | bottomRight: ['borderBottomRightRadius', 'borderBottomEndRadius'], 68 | } as const; 69 | 70 | type RadialGradientProps = { 71 | id: string; 72 | top: boolean; 73 | left: boolean; 74 | radius: number; 75 | shadowRadius: number; 76 | startColorWoOpacity: string; 77 | startColorOpacity: number; 78 | endColorWoOpacity: string; 79 | endColorOpacity: number; 80 | paintInside: boolean; 81 | }; 82 | export type RadialGradientPropsOmited = Omit< 83 | RadialGradientProps, 84 | `${'start' | 'end' | 'paintInside'}${string}` 85 | >; 86 | 87 | /** 88 | For iOS this is the last value before rounding to 1. 89 | We do this because react-native-svg in iOS won't consider Stops after the one with offset=1. 90 | This doesn't seem to affect the look of the corners on iOS. 91 | If it does, we will need to go back to the previous ( would throw [#56](https://github.com/SrBrahma/react-native-shadow-2/issues/56). 109 | I tried {paintInside ? : <>}, but it caused the another reported bug in the same issue. 110 | This if/else solution solves those react-native-svg strange limitations. 111 | I could try to have a wrapper function / dynamic children but those bugs were very unexpected, so I chose the Will-Work solution. 112 | */ 113 | if (paintInside) 114 | return ( 115 | 122 | 127 | 132 | {/* Ensure it stops painting after the radius if endColorOpacity isn't 0. */} 133 | 134 | 135 | ); 136 | else 137 | return ( 138 | 145 | {/* Don't paint the inner circle if not paintInside */} 146 | 147 | 152 | 157 | 158 | 159 | ); 160 | } 161 | 162 | /** 163 | * Generates a sufficiently unique suffix to add to gradient ids and prevent collisions. 164 | * 165 | * https://github.com/SrBrahma/react-native-shadow-2/pull/54 166 | */ 167 | export const generateGradientIdSuffix = (() => { 168 | let shadowGradientIdCounter = 0; 169 | return () => String(shadowGradientIdCounter++); 170 | })(); 171 | 172 | export const rtlScaleX = { transform: [{ scaleX: -1 }] }; 173 | 174 | /** 175 | * https://github.com/SrBrahma/react-native-shadow-2/issues/67 176 | */ 177 | export const rtlAbsoluteFillObject = { 178 | position: 'absolute', 179 | start: 0, 180 | end: 0, 181 | top: 0, 182 | bottom: 0, 183 | } as const; 184 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 4 | "module": "es6", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 5 | "jsx": "react-native", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 6 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 7 | "outDir": "lib", /* Redirect output structure to the directory. */ 8 | "rootDir": "src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 9 | "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 10 | "strict": true, /* Enable all strict type-checking options. */ 11 | "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 12 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 13 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 14 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 15 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 16 | }, 17 | "include": ["src/**/*"], 18 | "exclude": ["src/**/*.test.*"] 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.lint.json: -------------------------------------------------------------------------------- 1 | // We need this file for eslint, as in tsconfig.json we exclude the tests from the compilation, 2 | // but then, jest will complain the test files aren't included in tsconfig. 3 | { 4 | "extends": "./tsconfig.json", 5 | "exclude": [] // Withour *.test.* 6 | } --------------------------------------------------------------------------------