├── .husky ├── .gitignore ├── pre-commit └── commit-msg ├── .gitattributes ├── tsconfig.build.json ├── babel.config.js ├── .yarnrc ├── src ├── index.ts ├── useDimensions.ts ├── ResponsiveComponent.tsx ├── __tests__ │ ├── ResponsiveComponent.test.tsx │ ├── useDimensions.test.tsx │ ├── useStylesheet.test.tsx │ ├── setup.ts │ └── MediaQuery.test.tsx ├── useStylesheet.ts └── MediaQuery.tsx ├── example ├── index.js ├── src │ └── App.tsx ├── app.json ├── babel.config.js ├── package.json ├── webpack.config.js └── metro.config.js ├── .eslintrc ├── scripts └── bootstrap.js ├── tsconfig.json ├── .gitignore ├── LICENSE ├── .circleci └── config.yml ├── package.json ├── README.md └── CONTRIBUTING.md /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | # specific for windows script files 3 | *.bat text eol=crlf -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "extends": "./tsconfig", 4 | "exclude": ["example"] 5 | } 6 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint && yarn typescript 5 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["module:metro-react-native-babel-preset"], 3 | }; 4 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn commitlint -E HUSKY_GIT_PARAMS 5 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | # Override Yarn command so we can automatically setup the repo on running `yarn` 2 | 3 | yarn-path "scripts/bootstrap.js" 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as useDimensions } from "./useDimensions"; 2 | export { useStylesheet, getStylesheet } from "./useStylesheet"; 3 | export { 4 | default as MediaQuery, 5 | MediaQuery as IMediaQuery, 6 | mediaQuery, 7 | } from "./MediaQuery"; 8 | export { default as ResponsiveComponent } from "./ResponsiveComponent"; 9 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import { registerRootComponent } from "expo"; 2 | 3 | import App from "./src/App"; 4 | 5 | // registerRootComponent calls AppRegistry.registerComponent('main', () => App); 6 | // It also ensures that whether you load the app in the Expo client or in a native build, 7 | // the environment is set up appropriately 8 | registerRootComponent(App); 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | }, 5 | "extends": "react-native-wcandillon", 6 | "rules": { 7 | 8 | }, 9 | "ignorePatterns": [ 10 | "node_modules/", 11 | "lib/", 12 | "example/babel.config.js", 13 | "example/metro.config.js", 14 | "example/webpack.config.js", 15 | "scripts/bootstrap.js" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /example/src/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { StyleSheet, View } from "react-native"; 3 | import { useStylesheet } from "react-native-responsive-ui"; 4 | 5 | export default function App() { 6 | useStylesheet([]); 7 | 8 | return ; 9 | } 10 | 11 | const styles = StyleSheet.create({ 12 | container: { 13 | flex: 1, 14 | alignItems: "center", 15 | justifyContent: "center", 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /example/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-responsive-ui-example", 3 | "displayName": "ResponsiveUi Example", 4 | "expo": { 5 | "name": "react-native-responsive-ui-example", 6 | "slug": "react-native-responsive-ui-example", 7 | "description": "Example app for react-native-responsive-ui", 8 | "privacy": "public", 9 | "version": "1.0.0", 10 | "platforms": [ 11 | "ios", 12 | "android", 13 | "web" 14 | ], 15 | "ios": { 16 | "supportsTablet": true 17 | }, 18 | "assetBundlePatterns": [ 19 | "**/*" 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const pak = require("../package.json"); 4 | 5 | module.exports = function (api) { 6 | api.cache(true); 7 | 8 | return { 9 | presets: ["babel-preset-expo"], 10 | plugins: [ 11 | [ 12 | "module-resolver", 13 | { 14 | extensions: [".tsx", ".ts", ".js", ".json"], 15 | alias: { 16 | // For development, we want to alias the library to the source 17 | [pak.name]: path.join(__dirname, "..", pak.source), 18 | }, 19 | }, 20 | ], 21 | ], 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /src/useDimensions.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { Dimensions, ScaledSize } from "react-native"; 3 | 4 | const useDimensions = () => { 5 | const [dimensions, setDimensions] = useState(Dimensions.get("window")); 6 | 7 | const onChange = ({ 8 | window: { width, height, scale, fontScale }, 9 | }: { 10 | window: ScaledSize; 11 | }) => setDimensions({ width, height, scale, fontScale }); 12 | 13 | useEffect(() => { 14 | Dimensions.addEventListener("change", onChange); 15 | return () => Dimensions.removeEventListener("change", onChange); 16 | }, []); 17 | 18 | return dimensions; 19 | }; 20 | 21 | export default useDimensions; 22 | -------------------------------------------------------------------------------- /scripts/bootstrap.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const child_process = require("child_process"); 3 | 4 | const root = path.resolve(__dirname, ".."); 5 | const args = process.argv.slice(2); 6 | const options = { 7 | cwd: process.cwd(), 8 | env: process.env, 9 | stdio: "inherit", 10 | encoding: "utf-8", 11 | }; 12 | 13 | let result; 14 | 15 | if (process.cwd() !== root || args.length) { 16 | // We're not in the root of the project, or additional arguments were passed 17 | // In this case, forward the command to `yarn` 18 | result = child_process.spawnSync("yarn", args, options); 19 | } else { 20 | // If `yarn` is run without arguments, perform bootstrap 21 | result = child_process.spawnSync("yarn", ["bootstrap"], options); 22 | } 23 | 24 | process.exitCode = result.status; 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "react-native-responsive-ui": ["./src/index"] 6 | }, 7 | "allowUnreachableCode": false, 8 | "allowUnusedLabels": false, 9 | "esModuleInterop": true, 10 | "importsNotUsedAsValues": "error", 11 | "forceConsistentCasingInFileNames": true, 12 | "jsx": "react", 13 | "lib": ["esnext"], 14 | "module": "esnext", 15 | "moduleResolution": "node", 16 | "noFallthroughCasesInSwitch": true, 17 | "noImplicitReturns": true, 18 | "noImplicitUseStrict": false, 19 | "noStrictGenericChecks": false, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "resolveJsonModule": true, 23 | "skipLibCheck": true, 24 | "strict": true, 25 | "target": "esnext" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ResponsiveComponent.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from "react"; 2 | import { Dimensions, ScaledSize } from "react-native"; 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 5 | interface ResponsiveComponentProps {} 6 | 7 | interface ResponsiveComponentState { 8 | window: ScaledSize; 9 | } 10 | 11 | export default abstract class ResponsiveComponent extends Component< 12 | ResponsiveComponentProps, 13 | ResponsiveComponentState 14 | > { 15 | state = { 16 | window: Dimensions.get("window"), 17 | }; 18 | 19 | componentDidMount() { 20 | Dimensions.addEventListener("change", this.onDimensionChange); 21 | } 22 | 23 | componentWillUnmount() { 24 | Dimensions.removeEventListener("change", this.onDimensionChange); 25 | } 26 | 27 | onDimensionChange = (dims: ResponsiveComponentState) => this.setState(dims); 28 | } 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # XDE 6 | .expo/ 7 | 8 | # VSCode 9 | .vscode/ 10 | jsconfig.json 11 | 12 | # Xcode 13 | # 14 | build/ 15 | *.pbxuser 16 | !default.pbxuser 17 | *.mode1v3 18 | !default.mode1v3 19 | *.mode2v3 20 | !default.mode2v3 21 | *.perspectivev3 22 | !default.perspectivev3 23 | xcuserdata 24 | *.xccheckout 25 | *.moved-aside 26 | DerivedData 27 | *.hmap 28 | *.ipa 29 | *.xcuserstate 30 | project.xcworkspace 31 | 32 | # Android/IJ 33 | # 34 | .idea 35 | .gradle 36 | local.properties 37 | android.iml 38 | 39 | # Cocoapods 40 | # 41 | example/ios/Pods 42 | 43 | # node.js 44 | # 45 | node_modules/ 46 | npm-debug.log 47 | yarn-debug.log 48 | yarn-error.log 49 | 50 | # BUCK 51 | buck-out/ 52 | \.buckd/ 53 | android/app/libs 54 | android/keystores/debug.keystore 55 | 56 | # Expo 57 | .expo/* 58 | 59 | # generated by bob 60 | lib/ 61 | dist/ 62 | 63 | .idea/ 64 | .env 65 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-responsive-ui-example", 3 | "description": "Example app for react-native-responsive-ui", 4 | "version": "0.0.1", 5 | "private": true, 6 | "main": "index", 7 | "scripts": { 8 | "android": "expo start --android", 9 | "ios": "expo start --ios", 10 | "web": "expo start --web", 11 | "start": "expo start", 12 | "test": "jest" 13 | }, 14 | "dependencies": { 15 | "expo": "^40.0.0", 16 | "expo-splash-screen": "~0.8.1", 17 | "react": "16.13.1", 18 | "react-dom": "16.13.1", 19 | "react-native": "0.63.4", 20 | "react-native-unimodules": "~0.12.0", 21 | "react-native-web": "~0.14.9" 22 | }, 23 | "devDependencies": { 24 | "@babel/core": "~7.12.10", 25 | "@babel/runtime": "^7.9.6", 26 | "babel-plugin-module-resolver": "^4.0.0", 27 | "babel-preset-expo": "8.3.0", 28 | "expo-cli": "^4.0.13" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const createExpoWebpackConfigAsync = require("@expo/webpack-config"); 4 | 5 | const { resolver } = require("./metro.config"); 6 | 7 | const root = path.resolve(__dirname, ".."); 8 | const node_modules = path.join(__dirname, "node_modules"); 9 | 10 | module.exports = async function (env, argv) { 11 | const config = await createExpoWebpackConfigAsync(env, argv); 12 | 13 | config.module.rules.push({ 14 | test: /\.(js|jsx|ts|tsx)$/, 15 | include: path.resolve(root, "src"), 16 | use: "babel-loader", 17 | }); 18 | 19 | // We need to make sure that only one version is loaded for peerDependencies 20 | // So we alias them to the versions in example's node_modules 21 | Object.assign(config.resolve.alias, { 22 | ...resolver.extraNodeModules, 23 | "react-native-web": path.join(node_modules, "react-native-web"), 24 | }); 25 | 26 | return config; 27 | }; 28 | -------------------------------------------------------------------------------- /src/__tests__/ResponsiveComponent.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Dimensions, Text, View } from "react-native"; 3 | import { render } from "@testing-library/react-native"; 4 | 5 | import ResponsiveComponent from "../ResponsiveComponent"; 6 | 7 | class RespTestComp extends ResponsiveComponent { 8 | render() { 9 | const { width, height } = this.state.window; 10 | return ( 11 | 12 | Width: {width} 13 | Height: {height} 14 | 15 | ); 16 | } 17 | } 18 | 19 | describe("ResponsiveComponent", () => { 20 | test("should display proper sizing", async () => { 21 | Dimensions.set({ height: 100, width: 100 }); 22 | 23 | const { queryByText } = render(); 24 | 25 | expect(queryByText("Width: 100")).toBeTruthy(); 26 | expect(queryByText("Height: 100")).toBeTruthy(); 27 | 28 | Dimensions.set({ height: 200, width: 200 }); 29 | 30 | expect(queryByText("Width: 200")).toBeTruthy(); 31 | expect(queryByText("Height: 200")).toBeTruthy(); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 William Candillon 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 | -------------------------------------------------------------------------------- /src/useStylesheet.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import type { ViewStyle, TextStyle, ImageStyle } from "react-native"; 3 | 4 | import { mediaQuery, MediaQuery } from "./MediaQuery"; 5 | import useDimensions from "./useDimensions"; 6 | 7 | type NamedStyles = { [P in keyof T]: ViewStyle | TextStyle | ImageStyle }; 8 | interface MediaQueryStyle { 9 | query: MediaQuery; 10 | style: NamedStyles; 11 | } 12 | 13 | export const getStylesheet = >( 14 | { width, height }: { width: number; height: number }, 15 | styles: MediaQueryStyle[] 16 | ) => { 17 | const selectedStyles: NamedStyles[] = []; 18 | styles.forEach((style) => 19 | mediaQuery(style.query, width, height) 20 | ? selectedStyles.push(_.cloneDeep(style.style)) 21 | : undefined 22 | ); 23 | return _.merge.apply[], NamedStyles>( 24 | null, 25 | selectedStyles 26 | ); 27 | }; 28 | 29 | export const useStylesheet = >( 30 | styles: MediaQueryStyle[] 31 | ) => { 32 | const dimensions = useDimensions(); 33 | return getStylesheet(dimensions, styles); 34 | }; 35 | -------------------------------------------------------------------------------- /src/__tests__/useDimensions.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Dimensions, Text, View } from "react-native"; 3 | import { render } from "@testing-library/react-native"; 4 | 5 | import useDimensions from "../useDimensions"; 6 | 7 | const DimensionsComp = () => { 8 | const { width, height } = useDimensions(); 9 | 10 | return ( 11 | 12 | Width: {width} 13 | Height: {height} 14 | 15 | ); 16 | }; 17 | 18 | test("useDimensions should get proper initial dimensions", async () => { 19 | const { queryByText } = render(); 20 | 21 | expect(queryByText("Width: 100")).toBeTruthy(); 22 | expect(queryByText("Height: 100")).toBeTruthy(); 23 | }); 24 | 25 | test("useDimensions should handle changes in dimention", async () => { 26 | const { queryByText } = render(); 27 | 28 | expect(queryByText("Width: 100")).toBeTruthy(); 29 | expect(queryByText("Height: 100")).toBeTruthy(); 30 | 31 | Dimensions.set({ height: 120, width: 120 }); 32 | 33 | expect(queryByText("Width: 120")).toBeTruthy(); 34 | expect(queryByText("Height: 120")).toBeTruthy(); 35 | }); 36 | -------------------------------------------------------------------------------- /example/metro.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const blacklist = require("metro-config/src/defaults/blacklist"); 4 | const escape = require("escape-string-regexp"); 5 | 6 | const pak = require("../package.json"); 7 | 8 | const root = path.resolve(__dirname, ".."); 9 | 10 | const modules = Object.keys({ 11 | ...pak.peerDependencies, 12 | }); 13 | 14 | module.exports = { 15 | projectRoot: __dirname, 16 | watchFolders: [root], 17 | 18 | // We need to make sure that only one version is loaded for peerDependencies 19 | // So we blacklist them at the root, and alias them to the versions in example's node_modules 20 | resolver: { 21 | blacklistRE: blacklist( 22 | modules.map( 23 | (m) => 24 | new RegExp(`^${escape(path.join(root, "node_modules", m))}\\/.*$`) 25 | ) 26 | ), 27 | 28 | extraNodeModules: modules.reduce((acc, name) => { 29 | acc[name] = path.join(__dirname, "node_modules", name); 30 | return acc; 31 | }, {}), 32 | }, 33 | 34 | transformer: { 35 | getTransformOptions: async () => ({ 36 | transform: { 37 | experimentalImportSupport: false, 38 | inlineRequires: true, 39 | }, 40 | }), 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /src/__tests__/useStylesheet.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Dimensions, Text } from "react-native"; 3 | import { render } from "@testing-library/react-native"; 4 | 5 | import { useStylesheet, getStylesheet } from "../useStylesheet"; 6 | 7 | const styles = [ 8 | { 9 | query: { minHeight: 500 }, 10 | style: { container: { backgroundColor: "red" } }, 11 | }, 12 | ]; 13 | 14 | const StyleSheetComp = () => { 15 | const style = useStylesheet(styles); 16 | 17 | return {JSON.stringify(style)}; 18 | }; 19 | 20 | describe("getStylesheet", () => { 21 | test("should return value when within constraints", () => { 22 | const height = 1000; 23 | const width = 1000; 24 | const style = getStylesheet({ width, height }, styles); 25 | expect(style.container.backgroundColor).toBe("red"); 26 | }); 27 | 28 | test("should not return value when not within constraints", () => { 29 | const height = 100; 30 | const width = 100; 31 | const style = getStylesheet({ width, height }, styles); 32 | expect(style?.container?.backgroundColor).not.toBe("red"); 33 | }); 34 | }); 35 | 36 | describe("useStylesheet", () => { 37 | test("should return style when matched", async () => { 38 | Dimensions.set({ height: 1000, width: 1000 }); 39 | 40 | const { queryByText } = render(); 41 | 42 | expect(queryByText(/red/)).toBeTruthy(); 43 | }); 44 | 45 | test("should not return style when matched", async () => { 46 | Dimensions.set({ height: 100, width: 100 }); 47 | 48 | const { queryByText } = render(); 49 | 50 | expect(queryByText(/red/)).toBeFalsy(); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/MediaQuery.tsx: -------------------------------------------------------------------------------- 1 | import type * as React from "react"; 2 | import { PixelRatio, Platform, PlatformOSType } from "react-native"; 3 | 4 | import useDimensions from "./useDimensions"; 5 | 6 | type Orientation = "landscape" | "portrait"; 7 | 8 | export interface MediaQuery { 9 | minHeight?: number; 10 | maxHeight?: number; 11 | minWidth?: number; 12 | maxWidth?: number; 13 | minAspectRatio?: number; 14 | maxAspectRatio?: number; 15 | minPixelRatio?: number; 16 | maxPixelRatio?: number; 17 | orientation?: Orientation; 18 | condition?: boolean; 19 | platform?: PlatformOSType; 20 | } 21 | 22 | export const isInInterval = ( 23 | value: number, 24 | min?: number, 25 | max?: number 26 | ): boolean => 27 | (min === undefined || value >= min) && (max === undefined || value <= max); 28 | 29 | export const mediaQuery = ( 30 | query: MediaQuery, 31 | width: number, 32 | height: number 33 | ): boolean => { 34 | const { 35 | minWidth, 36 | maxWidth, 37 | minHeight, 38 | maxHeight, 39 | minAspectRatio, 40 | maxAspectRatio, 41 | minPixelRatio, 42 | maxPixelRatio, 43 | orientation, 44 | platform, 45 | condition, 46 | } = query; 47 | const currentOrientation: Orientation = 48 | width > height ? "landscape" : "portrait"; 49 | return ( 50 | isInInterval(width, minWidth, maxWidth) && 51 | isInInterval(height, minHeight, maxHeight) && 52 | isInInterval(width / height, minAspectRatio, maxAspectRatio) && 53 | isInInterval(PixelRatio.get(), minPixelRatio, maxPixelRatio) && 54 | (orientation === undefined || orientation === currentOrientation) && 55 | (platform === undefined || platform === Platform.OS) && 56 | (condition === undefined || condition) 57 | ); 58 | }; 59 | 60 | const MediaQuery: React.FC = ({ children, ...props }) => { 61 | const { width, height } = useDimensions(); 62 | const val = mediaQuery(props, width, height); 63 | if (val) { 64 | return children as JSX.Element; 65 | } 66 | return null; 67 | }; 68 | 69 | export default MediaQuery; 70 | -------------------------------------------------------------------------------- /src/__tests__/setup.ts: -------------------------------------------------------------------------------- 1 | jest.mock("react-native", () => { 2 | const { act } = jest.requireActual("@testing-library/react-native"); 3 | const ReactNative = jest.requireActual("react-native"); 4 | 5 | type mockChangeListener = ({ 6 | window: { height, width, scale, fontSize, pixelRatio }, 7 | }: { 8 | window: { 9 | height: number; 10 | width: number; 11 | scale: number; 12 | fontSize: number; 13 | pixelRatio: number; 14 | }; 15 | }) => void; 16 | 17 | const eventListeners: Record = {}; 18 | 19 | const mockDimensions = { 20 | height: 100, 21 | width: 100, 22 | scale: 1, 23 | fontSize: 16, 24 | pixelRatio: 1, 25 | }; 26 | 27 | const Dimensions = { 28 | get: jest.fn().mockReturnValue(mockDimensions), 29 | addEventListener: (eventName: string, onChange: mockChangeListener) => { 30 | eventListeners[eventName] = onChange; 31 | }, 32 | removeEventListener: (eventName: string) => { 33 | delete eventListeners[eventName]; 34 | }, 35 | set: ({ 36 | height = mockDimensions.height, 37 | width = mockDimensions.width, 38 | pixelRatio = mockDimensions.pixelRatio, 39 | }: { 40 | height: number; 41 | width: number; 42 | pixelRatio?: number; 43 | }) => { 44 | act(() => { 45 | mockDimensions.height = height; 46 | mockDimensions.width = width; 47 | mockDimensions.pixelRatio = pixelRatio; 48 | }); 49 | if (eventListeners.change) { 50 | act(() => { 51 | eventListeners.change({ 52 | window: { height, width, scale: 1, fontSize: 16, pixelRatio }, 53 | }); 54 | }); 55 | } 56 | }, 57 | }; 58 | 59 | const PixelRatio = { 60 | get: () => mockDimensions.pixelRatio, 61 | startDetecting: jest.fn(), 62 | roundToNearestPixel: (val: number) => val, 63 | getPixelSizeForLayoutSize: (val: number) => val, 64 | getFontScale: 1, 65 | }; 66 | 67 | // Must use `setPrototypeOf` and not spread, otherwise we get errors 68 | return Object.setPrototypeOf( 69 | { 70 | Dimensions, 71 | PixelRatio, 72 | }, 73 | ReactNative 74 | ); 75 | }); 76 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | executors: 4 | default: 5 | docker: 6 | - image: circleci/node:12 7 | working_directory: ~/project 8 | 9 | commands: 10 | attach_project: 11 | steps: 12 | - attach_workspace: 13 | at: ~/project 14 | 15 | jobs: 16 | install-dependencies: 17 | executor: default 18 | steps: 19 | - checkout 20 | - attach_project 21 | - restore_cache: 22 | keys: 23 | - dependencies-{{ checksum "package.json" }} 24 | - dependencies- 25 | - restore_cache: 26 | keys: 27 | - dependencies-example-{{ checksum "example/package.json" }} 28 | - dependencies-example- 29 | - run: 30 | name: Install dependencies 31 | command: | 32 | yarn install --cwd example --frozen-lockfile 33 | yarn install --frozen-lockfile 34 | - save_cache: 35 | key: dependencies-{{ checksum "package.json" }} 36 | paths: node_modules 37 | - save_cache: 38 | key: dependencies-example-{{ checksum "example/package.json" }} 39 | paths: example/node_modules 40 | - persist_to_workspace: 41 | root: . 42 | paths: . 43 | 44 | lint: 45 | executor: default 46 | steps: 47 | - attach_project 48 | - run: 49 | name: Lint files 50 | command: | 51 | yarn lint 52 | 53 | typescript: 54 | executor: default 55 | steps: 56 | - attach_project 57 | - run: 58 | name: Typecheck files 59 | command: | 60 | yarn typescript 61 | 62 | unit-tests: 63 | executor: default 64 | steps: 65 | - attach_project 66 | - run: 67 | name: Run unit tests 68 | command: | 69 | yarn test --coverage 70 | - store_artifacts: 71 | path: coverage 72 | destination: coverage 73 | 74 | build: 75 | executor: default 76 | steps: 77 | - attach_project 78 | - run: 79 | name: Build package 80 | command: | 81 | yarn prepare 82 | 83 | workflows: 84 | build-and-test: 85 | jobs: 86 | - install-dependencies 87 | - lint: 88 | requires: 89 | - install-dependencies 90 | - typescript: 91 | requires: 92 | - install-dependencies 93 | - unit-tests: 94 | requires: 95 | - install-dependencies 96 | - build: 97 | requires: 98 | - install-dependencies 99 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-responsive-ui", 3 | "version": "2.1.5", 4 | "description": "Responsive UIs for React Native", 5 | "main": "lib/commonjs/index", 6 | "module": "lib/module/index", 7 | "types": "lib/typescript/index.d.ts", 8 | "react-native": "src/index", 9 | "source": "src/index", 10 | "files": [ 11 | "src", 12 | "lib", 13 | "android", 14 | "ios", 15 | "cpp", 16 | "react-native-responsive-ui.podspec", 17 | "!lib/typescript/example", 18 | "!android/build", 19 | "!ios/build", 20 | "!**/__tests__", 21 | "!**/__fixtures__", 22 | "!**/__mocks__" 23 | ], 24 | "scripts": { 25 | "test": "jest", 26 | "typescript": "tsc --noEmit", 27 | "lint": "eslint \"**/*.{js,ts,tsx}\"", 28 | "prepare": "bob build && husky install", 29 | "release": "dotenv release-it", 30 | "example": "yarn --cwd example", 31 | "pods": "cd example && pod-install --quiet", 32 | "bootstrap": "yarn example && yarn && yarn pods" 33 | }, 34 | "keywords": [ 35 | "react-native", 36 | "ios", 37 | "android" 38 | ], 39 | "repository": "https://github.com/wcandillon/react-native-responsive-ui", 40 | "author": "William Candillon (https://github.com/wcandillon)", 41 | "license": "MIT", 42 | "bugs": { 43 | "url": "https://github.com/wcandillon/react-native-responsive-ui/issues" 44 | }, 45 | "homepage": "https://github.com/wcandillon/react-native-responsive-ui#readme", 46 | "publishConfig": { 47 | "registry": "https://registry.npmjs.org/" 48 | }, 49 | "devDependencies": { 50 | "@commitlint/config-conventional": "^11.0.0", 51 | "@release-it/conventional-changelog": "^2.0.0", 52 | "@testing-library/react-native": "^8.0.0-rc.1", 53 | "@types/jest": "^26.0.0", 54 | "@types/lodash": "^4.14.168", 55 | "@types/react": "^16.9.19", 56 | "@types/react-native": "0.62.13", 57 | "commitlint": "^11.0.0", 58 | "dotenv-cli": "^4.0.0", 59 | "eslint": "^7.2.0", 60 | "eslint-config-react-native-wcandillon": "3.6.1", 61 | "husky": "^6.0.0", 62 | "jest": "^26.0.1", 63 | "pod-install": "^0.1.0", 64 | "prettier": "^2.0.5", 65 | "react": "16.13.1", 66 | "react-native": "0.63.4", 67 | "react-native-builder-bob": "^0.18.0", 68 | "react-test-renderer": "16.13.1", 69 | "release-it": "^14.2.2", 70 | "typescript": "^4.1.3" 71 | }, 72 | "peerDependencies": { 73 | "react": "*", 74 | "react-native": "*" 75 | }, 76 | "dependencies": { 77 | "lodash": "^4.17.21" 78 | }, 79 | "jest": { 80 | "preset": "react-native", 81 | "setupFilesAfterEnv": [ 82 | "./src/__tests__/setup.ts" 83 | ], 84 | "modulePathIgnorePatterns": [ 85 | "/example/node_modules", 86 | "/lib/", 87 | "/src/__tests__/setup.ts" 88 | ] 89 | }, 90 | "commitlint": { 91 | "extends": [ 92 | "@commitlint/config-conventional" 93 | ] 94 | }, 95 | "release-it": { 96 | "git": { 97 | "commitMessage": "chore: release ${version}", 98 | "tagName": "v${version}" 99 | }, 100 | "npm": { 101 | "publish": true 102 | }, 103 | "github": { 104 | "release": true 105 | }, 106 | "plugins": { 107 | "@release-it/conventional-changelog": { 108 | "preset": "angular" 109 | } 110 | } 111 | }, 112 | "react-native-builder-bob": { 113 | "source": "src", 114 | "output": "lib", 115 | "targets": [ 116 | "commonjs", 117 | "module", 118 | [ 119 | "typescript", 120 | { 121 | "project": "tsconfig.build.json" 122 | } 123 | ] 124 | ] 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Native Reponsive UI 2 | 3 | [![CircleCI](https://circleci.com/gh/wcandillon/react-native-responsive-ui.svg?style=svg)](https://circleci.com/gh/wcandillon/react-native-responsive-ui) 4 | [![npm version](https://badge.fury.io/js/react-native-responsive-ui.svg)](https://badge.fury.io/js/react-native-responsive-ui) 5 | 6 | 7 | Building responsive UIs in React Native. 8 | 9 | ![example](https://raw.githubusercontent.com/wcandillon/react-native-responsive-ui/4637085802323386110a6352929147d11e1ca83c/example/components/images/example.gif) 10 | 11 | An example is available via expo [here](https://expo.io/@wcandillon/react-native-responsive-ui). 12 | 13 | ## Installation 14 | 15 | ```bash 16 | npm install react-native-responsive-ui --save 17 | ``` 18 | 19 | ## Usage 20 | 21 | The `MediaQuery` component renders its children only if the query evaluates to true (see list of properties below). 22 | This component listens to changes in the window dimensions. 23 | In the example below, we render the `Logo` component if the window's height has a minimum size of `450dp` and if the device orientation is in portrait mode (height is larger than width). 24 | 25 | ### Media Queries 26 | 27 | ```jsx 28 | // @flow 29 | import React, {Component} from "react"; 30 | import {View} from "react-native"; 31 | import {MediaQuery} from "react-native-responsive-ui"; 32 | 33 | export default class Login extends Component { 34 | render(): React$Element<*> { 35 | return 36 | 37 | 38 | 39 | ; 40 | } 41 | } 42 | 43 | ``` 44 | 45 | #### Properties 46 | 47 | | Name | Type | Description | 48 | |----------------|--------|--------------------------------------------------------------------------------------| 49 | | minHeight | dp | Minimum height of the window. | 50 | | maxHeight | dp | Maximum height of the window. | 51 | | minWidth | dp | Minimum width of the window. | 52 | | maxWidth | dp | Maximum width of the window. | 53 | | minAspectRatio | number | Minimum aspect ration of the window (ratio of horizontal pixels to vertical pixels). | 54 | | maxAspectRatio | number | Maximum aspect ration of the window (ratio of horizontal pixels to vertical pixels). | 55 | | minPixelRatio | number | Minimum device pixel density. See [PixelRatio](https://facebook.github.io/react-native/docs/pixelratio.html). | 56 | | maxPixelRatio | number | Maximum device pixel density. See [PixelRatio](https://facebook.github.io/react-native/docs/pixelratio.html). | 57 | | orientation | `portrait` or `landspace` | Indicates whether the viewport is in landscape (the display is wider than it is tall) or portrait (the display is square or taller than it is wide) mode. | 58 | | platform | string | Platform of the device. See [Platform](https://facebook.github.io/react-native/docs/platform-specific-code.html#platform-module). | 59 | | condition | boolean | Abritrary boolean value that must be true for the media query to pass. | 60 | 61 | 62 | ### useDimensions 63 | 64 | ```jsx 65 | import React from "react"; 66 | import {useDimensions} from "react-native-responsive-ui"; 67 | 68 | export default ({ children }) => { 69 | const {width, height} = useDimensions(); 70 | console.log(`New window dimensions: ${width}x${height}`); 71 | return children; 72 | }; 73 | ``` 74 | 75 | ### useStylesheet 76 | 77 | ```jsx 78 | import React from "react"; 79 | import {useStylesheet} from "react-native-responsive-ui"; 80 | 81 | export default class Buttons extends ResponsiveComponent { 82 | render() { 83 | const style = useStylesheet(staticStyle) 84 | return 85 |