├── .nvmrc ├── .watchmanconfig ├── example ├── App.js ├── src │ ├── tab-flash-list │ │ ├── index.tsx │ │ ├── tab-flash-list-scroll-view.tsx │ │ └── tab-flash-list.tsx │ ├── App.web.tsx │ ├── App.tsx │ └── example.tsx ├── assets │ ├── icon.png │ ├── favicon.png │ ├── splash.png │ └── adaptive-icon.png ├── tsconfig.json ├── index.js ├── babel.config.js ├── patches │ └── react-native-reanimated+3.1.0.patch ├── webpack.config.js ├── app.json ├── package.json └── metro.config.js ├── src ├── __tests__ │ └── index.test.tsx ├── scrollable-view │ ├── index.tsx │ ├── flat-list.tsx │ ├── scroll-view.tsx │ └── section-list.tsx ├── hooks │ ├── index.tsx │ ├── use-shared-scrollable-ref.tsx │ ├── use-refresh-value.tsx │ ├── use-sync-initial-position.tsx │ └── use-scene-info.tsx ├── context.tsx ├── index.tsx ├── tab-flash-list-scroll-view.tsx ├── create-collapsible-scroll-view.tsx ├── utils.tsx ├── scene.web.tsx ├── create-collapsible-tabs.tsx ├── types.tsx ├── scene.tsx ├── refresh-control.tsx ├── create-collapsible-tabs.web.tsx └── gesture-container.tsx ├── .gitattributes ├── tsconfig.build.json ├── babel.config.js ├── .yarnrc.yml ├── .editorconfig ├── lefthook.yml ├── .github ├── actions │ └── setup │ │ └── action.yml └── workflows │ └── ci.yml ├── tsconfig.json ├── LICENSE ├── .gitignore ├── CONTRIBUTING.md ├── package.json ├── CODE_OF_CONDUCT.md └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 16.18.1 2 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /example/App.js: -------------------------------------------------------------------------------- 1 | export { default } from "./src/App"; 2 | -------------------------------------------------------------------------------- /src/__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | it.todo('write a test'); 2 | -------------------------------------------------------------------------------- /example/src/tab-flash-list/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./tab-flash-list"; 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 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /example/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/showtime-xyz/showtime-tab-view/HEAD/example/assets/icon.png -------------------------------------------------------------------------------- /example/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/showtime-xyz/showtime-tab-view/HEAD/example/assets/favicon.png -------------------------------------------------------------------------------- /example/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/showtime-xyz/showtime-tab-view/HEAD/example/assets/splash.png -------------------------------------------------------------------------------- /example/assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/showtime-xyz/showtime-tab-view/HEAD/example/assets/adaptive-icon.png -------------------------------------------------------------------------------- /src/scrollable-view/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./flat-list"; 2 | export * from "./scroll-view"; 3 | export * from "./section-list"; 4 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "compilerOptions": { 4 | // Avoid expo-cli auto-generating a tsconfig 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /example/src/App.web.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { Example } from "./example"; 4 | 5 | export default function App() { 6 | return ; 7 | } 8 | -------------------------------------------------------------------------------- /src/hooks/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./use-refresh-value"; 2 | export * from "./use-scene-info"; 3 | export * from "./use-shared-scrollable-ref"; 4 | export * from "./use-sync-initial-position"; 5 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | plugins: 4 | - path: .yarn/plugins/@yarnpkg/plugin-version.cjs 5 | spec: "@yarnpkg/plugin-version" 6 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 7 | spec: "@yarnpkg/plugin-interactive-tools" 8 | 9 | yarnPath: .yarn/releases/yarn-3.5.0.cjs 10 | -------------------------------------------------------------------------------- /example/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { GestureHandlerRootView } from "react-native-gesture-handler"; 3 | 4 | import { Example } from "./example"; 5 | 6 | export default function App() { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | indent_style = space 10 | indent_size = 2 11 | 12 | end_of_line = lf 13 | charset = utf-8 14 | trim_trailing_whitespace = true 15 | insert_final_newline = true 16 | -------------------------------------------------------------------------------- /src/context.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | 3 | import type { TabHeaderContext } from "./types"; 4 | 5 | export const HeaderTabContext = React.createContext(null); 6 | 7 | export const useHeaderTabContext = () => { 8 | const ctx = useContext(HeaderTabContext); 9 | if (!ctx) throw new Error("HeaderTabContext not found"); 10 | return ctx; 11 | }; 12 | -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | pre-commit: 2 | parallel: true 3 | commands: 4 | lint: 5 | files: git diff --name-only @{push} 6 | glob: "*.{js,ts,jsx,tsx}" 7 | run: npx eslint {files} 8 | types: 9 | files: git diff --name-only @{push} 10 | glob: "*.{js,ts, jsx, tsx}" 11 | run: npx tsc --noEmit 12 | commit-msg: 13 | parallel: true 14 | commands: 15 | commitlint: 16 | run: npx commitlint --edit 17 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import { registerRootComponent } from "expo"; 2 | import { LogBox } from "react-native"; 3 | 4 | import App from "./App"; 5 | LogBox.ignoreLogs([ 6 | "Warning: Failed prop type: Invalid prop `externalScrollView` of type `object`", // recyclerlistview type issue 7 | ]); 8 | 9 | // registerRootComponent calls AppRegistry.registerComponent('main', () => App); 10 | // It also ensures that whether you load the app in Expo Go or in a native build, 11 | // the environment is set up appropriately 12 | registerRootComponent(App); 13 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { TabView } from "react-native-tab-view"; 2 | 3 | import { createCollapsibleTabsComponent } from "./create-collapsible-tabs"; 4 | 5 | export { TabFlatList, TabScrollView, TabSectionList } from "./scrollable-view"; 6 | 7 | export * from "./create-collapsible-tabs"; 8 | export * from "./scene"; 9 | export * from "./types"; 10 | export type { 11 | TabScrollViewProps, 12 | TabFlatListProps, 13 | TabSectionListProps, 14 | } from "./scrollable-view"; 15 | export { useHeaderTabContext } from "./context"; 16 | export const CollapsibleTabView = createCollapsibleTabsComponent(TabView); 17 | -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const pak = require("../package.json"); 3 | 4 | module.exports = function (api) { 5 | api.cache(true); 6 | 7 | return { 8 | presets: ["babel-preset-expo"], 9 | plugins: [ 10 | [ 11 | "module-resolver", 12 | { 13 | extensions: [".tsx", ".ts", ".js", ".json"], 14 | alias: { 15 | // For development, we want to alias the library to the source 16 | [pak.name]: path.join(__dirname, "..", pak.source), 17 | }, 18 | }, 19 | ], 20 | "react-native-reanimated/plugin", 21 | ], 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /src/hooks/use-shared-scrollable-ref.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | 3 | import { useAnimatedRef } from "react-native-reanimated"; 4 | 5 | import type { ForwardRefType } from "../types"; 6 | 7 | export function useSharedScrollableRef( 8 | forwardRef: ForwardRefType 9 | ) { 10 | const ref = useAnimatedRef(); 11 | 12 | useEffect(() => { 13 | if (!forwardRef) { 14 | return; 15 | } 16 | if (typeof forwardRef === "function") { 17 | forwardRef(ref.current); 18 | } else { 19 | forwardRef.current = ref.current; 20 | } 21 | }); 22 | 23 | return ref; 24 | } 25 | -------------------------------------------------------------------------------- /src/tab-flash-list-scroll-view.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import type { ScrollViewProps } from "react-native"; 3 | 4 | import Animated from "react-native-reanimated"; 5 | import { SceneComponent } from "./scene"; 6 | 7 | type TabScrollViewProps = ScrollViewProps & { 8 | index: number; 9 | }; 10 | 11 | function TabFlashListScrollViewComponent( 12 | props: TabScrollViewProps, 13 | ref: React.Ref 14 | ) { 15 | return ( 16 | 21 | ); 22 | } 23 | 24 | export const TabFlashListScrollView = React.forwardRef( 25 | TabFlashListScrollViewComponent 26 | ); 27 | -------------------------------------------------------------------------------- /example/src/tab-flash-list/tab-flash-list-scroll-view.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import type { ScrollViewProps } from "react-native"; 3 | 4 | import Animated from "react-native-reanimated"; 5 | import { SceneComponent } from "@showtime-xyz/tab-view"; 6 | 7 | type TabScrollViewProps = ScrollViewProps & { 8 | index: number; 9 | }; 10 | function TabFlashListScrollViewComponent(props: TabScrollViewProps, ref: any) { 11 | return ( 12 | 18 | ); 19 | } 20 | 21 | export const TabFlashListScrollView = React.forwardRef( 22 | TabFlashListScrollViewComponent 23 | ); 24 | -------------------------------------------------------------------------------- /example/patches/react-native-reanimated+3.1.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/react-native-reanimated/src/reanimated2/mappers.ts b/node_modules/react-native-reanimated/src/reanimated2/mappers.ts 2 | index 4e66106..bc8712f 100644 3 | --- a/node_modules/react-native-reanimated/src/reanimated2/mappers.ts 4 | +++ b/node_modules/react-native-reanimated/src/reanimated2/mappers.ts 5 | @@ -80,8 +80,11 @@ export function createMapperRegistry() { 6 | } 7 | 8 | function mapperRun() { 9 | - processingMappers = true; 10 | runRequested = false; 11 | + if (processingMappers) { 12 | + return; 13 | + } 14 | + processingMappers = true; 15 | if (mappers.size !== sortedMappers.length) { 16 | updateMappersOrder(); 17 | } 18 | -------------------------------------------------------------------------------- /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup 2 | description: Setup Node.js and install dependencies 3 | 4 | runs: 5 | using: composite 6 | steps: 7 | - name: Setup Node.js 8 | uses: actions/setup-node@v3 9 | with: 10 | node-version-file: .nvmrc 11 | 12 | - name: Cache dependencies 13 | id: yarn-cache 14 | uses: actions/cache@v3 15 | with: 16 | path: | 17 | **/node_modules 18 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 19 | restore-keys: | 20 | ${{ runner.os }}-yarn- 21 | 22 | - name: Install dependencies 23 | if: steps.yarn-cache.outputs.cache-hit != 'true' 24 | run: | 25 | yarn install --cwd example --frozen-lockfile 26 | yarn install --frozen-lockfile 27 | shell: bash 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "@showtime-xyz/tab-view": ["./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 | "noUncheckedIndexedAccess": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "resolveJsonModule": true, 24 | "skipLibCheck": true, 25 | "strict": true, 26 | "target": "esnext" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/scrollable-view/flat-list.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FlatList as RNFlatList, FlatListProps, Platform } from "react-native"; 3 | 4 | import Animated from "react-native-reanimated"; 5 | 6 | import { SceneComponent } from "../scene"; 7 | 8 | const AnimatePageView = 9 | Platform.OS === "web" 10 | ? RNFlatList 11 | : Animated.createAnimatedComponent(RNFlatList); 12 | export type TabFlatListProps = FlatListProps & { 13 | index: number; 14 | }; 15 | 16 | function FlatList(props: TabFlatListProps, ref: any) { 17 | return ( 18 | 23 | ); 24 | } 25 | 26 | export const TabFlatList = React.forwardRef(FlatList) as ( 27 | props: TabFlatListProps & { 28 | ref?: React.Ref>; 29 | } 30 | ) => React.ReactElement; 31 | -------------------------------------------------------------------------------- /src/hooks/use-refresh-value.tsx: -------------------------------------------------------------------------------- 1 | import Animated, { 2 | interpolate, 3 | useDerivedValue, 4 | } from "react-native-reanimated"; 5 | 6 | export const useRefreshDerivedValue = ( 7 | translateYValue: Animated.SharedValue, 8 | { 9 | refreshHeight, 10 | overflowPull, 11 | animatedValue, 12 | pullExtendedCoefficient, 13 | }: { 14 | refreshHeight: number; 15 | overflowPull: number; 16 | animatedValue: Animated.SharedValue; 17 | pullExtendedCoefficient: number; 18 | } 19 | ) => { 20 | return useDerivedValue(() => { 21 | translateYValue.value = interpolate( 22 | animatedValue.value, 23 | [0, refreshHeight + overflowPull, refreshHeight + overflowPull + 1], 24 | [ 25 | 0, 26 | refreshHeight + overflowPull, 27 | refreshHeight + overflowPull + pullExtendedCoefficient, 28 | ] 29 | ); 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const createExpoWebpackConfigAsync = require('@expo/webpack-config'); 3 | const { resolver } = require('./metro.config'); 4 | 5 | const root = path.resolve(__dirname, '..'); 6 | const node_modules = path.join(__dirname, 'node_modules'); 7 | 8 | module.exports = async function (env, argv) { 9 | const config = await createExpoWebpackConfigAsync(env, argv); 10 | 11 | config.module.rules.push({ 12 | test: /\.(js|jsx|ts|tsx)$/, 13 | include: path.resolve(root, 'src'), 14 | use: 'babel-loader', 15 | }); 16 | 17 | // We need to make sure that only one version is loaded for peerDependencies 18 | // So we alias them to the versions in example's node_modules 19 | Object.assign(config.resolve.alias, { 20 | ...resolver.extraNodeModules, 21 | 'react-native-web': path.join(node_modules, 'react-native-web'), 22 | }); 23 | 24 | return config; 25 | }; 26 | -------------------------------------------------------------------------------- /src/scrollable-view/scroll-view.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Platform, 4 | ScrollView as RNScrollView, 5 | ScrollViewProps, 6 | } from "react-native"; 7 | 8 | import Animated from "react-native-reanimated"; 9 | 10 | import { SceneComponent } from "../scene"; 11 | 12 | const AnimatePageView = 13 | Platform.OS === "web" 14 | ? RNScrollView 15 | : Animated.createAnimatedComponent(RNScrollView); 16 | 17 | export type TabScrollViewProps = ScrollViewProps & { 18 | index: number; 19 | }; 20 | 21 | function ScrollView(props: TabScrollViewProps, ref: any) { 22 | return ( 23 | 28 | ); 29 | } 30 | 31 | export const TabScrollView = React.forwardRef(ScrollView) as ( 32 | props: TabScrollViewProps & { 33 | ref?: React.Ref; 34 | } 35 | ) => React.ReactElement; 36 | -------------------------------------------------------------------------------- /example/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "example", 4 | "slug": "example", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "userInterfaceStyle": "light", 9 | "splash": { 10 | "image": "./assets/splash.png", 11 | "resizeMode": "contain", 12 | "backgroundColor": "#ffffff" 13 | }, 14 | "updates": { 15 | "fallbackToCacheTimeout": 0 16 | }, 17 | "assetBundlePatterns": [ 18 | "**/*" 19 | ], 20 | "ios": { 21 | "supportsTablet": true, 22 | "bundleIdentifier": "com.showtime.tabview.example" 23 | }, 24 | "android": { 25 | "adaptiveIcon": { 26 | "foregroundImage": "./assets/adaptive-icon.png", 27 | "backgroundColor": "#FFFFFF" 28 | }, 29 | "package": "com.showtime.tabview.example" 30 | }, 31 | "web": { 32 | "favicon": "./assets/favicon.png" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/create-collapsible-scroll-view.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | type ComponentClass, 3 | type ForwardRefExoticComponent, 4 | type RefAttributes, 5 | } from 'react' 6 | import Animated from 'react-native-reanimated' 7 | import { SceneComponent } from './scene' 8 | 9 | export function createCollapsibleScrollView

(Component: ComponentClass

) { 10 | // Use type assertion here if you're sure about the compatibility 11 | const AnimatePageView = Animated.createAnimatedComponent( 12 | Component as unknown as ComponentClass 13 | ) 14 | 15 | type CollapsibleScrollViewProps = P & { 16 | index: number 17 | } 18 | 19 | return React.forwardRef< 20 | ForwardRefExoticComponent>>, 21 | CollapsibleScrollViewProps 22 | >(function TabViewScene(props, ref) { 23 | return 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /src/scrollable-view/section-list.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Platform, 4 | SectionList as RNSectionList, 5 | SectionListProps, 6 | } from "react-native"; 7 | 8 | import Animated from "react-native-reanimated"; 9 | 10 | import { SceneComponent } from "../scene"; 11 | 12 | const AnimatePageView = 13 | Platform.OS === "web" 14 | ? RNSectionList 15 | : Animated.createAnimatedComponent(RNSectionList); 16 | export type TabSectionListProps = SectionListProps & { 17 | index: number; 18 | }; 19 | 20 | function SectionList( 21 | props: TabSectionListProps, 22 | ref: any 23 | ) { 24 | return ( 25 | 30 | ); 31 | } 32 | 33 | export const TabSectionList = React.forwardRef(SectionList) as ( 34 | props: TabSectionListProps & { 35 | ref?: React.Ref>; 36 | } 37 | ) => React.ReactElement; 38 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | 10 | jobs: 11 | lint: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | 17 | - name: Setup 18 | uses: ./.github/actions/setup 19 | 20 | - name: Lint files 21 | run: yarn lint 22 | 23 | - name: Typecheck files 24 | run: yarn typecheck 25 | 26 | test: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v3 31 | 32 | - name: Setup 33 | uses: ./.github/actions/setup 34 | 35 | - name: Run unit tests 36 | run: yarn test --maxWorkers=2 --coverage 37 | 38 | build: 39 | runs-on: ubuntu-latest 40 | steps: 41 | - name: Checkout 42 | uses: actions/checkout@v3 43 | 44 | - name: Setup 45 | uses: ./.github/actions/setup 46 | 47 | - name: Build package 48 | run: yarn prepack 49 | -------------------------------------------------------------------------------- /example/src/tab-flash-list/tab-flash-list.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { FlashList, FlashListProps } from "@shopify/flash-list"; 4 | 5 | import { useHeaderTabContext } from "@showtime-xyz/tab-view"; 6 | 7 | import { TabFlashListScrollView } from "./tab-flash-list-scroll-view"; 8 | 9 | export type TabFlashListProps = Omit< 10 | FlashListProps, 11 | "renderScrollComponent" 12 | > & { 13 | index: number; 14 | }; 15 | 16 | function TabFlashListComponent( 17 | props: TabFlashListProps, 18 | ref: React.Ref> 19 | ) { 20 | const { scrollViewPaddingTop } = useHeaderTabContext(); 21 | return ( 22 | 28 | ); 29 | } 30 | 31 | export const TabFlashList = React.forwardRef(TabFlashListComponent) as ( 32 | props: TabFlashListProps & { 33 | ref?: React.Ref>; 34 | } 35 | ) => React.ReactElement; 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 alantoa 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "start": "expo start --dev-client", 6 | "android": "expo run:android", 7 | "ios": "expo run:ios", 8 | "web": "expo start --web", 9 | "postinstall": "patch-package" 10 | }, 11 | "dependencies": { 12 | "@shopify/flash-list": "1.7.3", 13 | "expo": "^52.0.35", 14 | "expo-splash-screen": "~0.29.22", 15 | "expo-status-bar": "~2.0.1", 16 | "fix": "^0.0.6", 17 | "node-libs-react-native": "^1.2.1", 18 | "patch-package": "^7.0.0", 19 | "postinstall-postinstall": "^2.1.0", 20 | "react": "18.3.1", 21 | "react-dom": "18.3.1", 22 | "react-native": "0.76.7", 23 | "react-native-gesture-handler": "~2.20.2", 24 | "react-native-pager-view": "6.5.1", 25 | "react-native-reanimated": "~3.16.1", 26 | "react-native-tab-view": "4.0.5", 27 | "react-native-web": "~0.19.13" 28 | }, 29 | "devDependencies": { 30 | "@babel/core": "^7.25.2", 31 | "@expo/webpack-config": "^18.0.1", 32 | "babel-loader": "^8.1.0", 33 | "babel-plugin-module-resolver": "^4.1.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/hooks/use-sync-initial-position.tsx: -------------------------------------------------------------------------------- 1 | import type React from "react"; 2 | import { useCallback, useRef } from "react"; 3 | 4 | import type Animated from "react-native-reanimated"; 5 | import { runOnUI, useSharedValue } from "react-native-reanimated"; 6 | 7 | import { useHeaderTabContext } from "../context"; 8 | import { _ScrollTo } from "../utils"; 9 | 10 | export const useSyncInitialPosition = ( 11 | ref: React.RefObject 12 | ) => { 13 | const opacityValue = useSharedValue(0); 14 | const isInitiated = useRef(true); 15 | const { headerHeight, minHeaderHeight } = useHeaderTabContext(); 16 | 17 | const initialPosition = useCallback( 18 | (position: number) => { 19 | if (!isInitiated.current) return; 20 | isInitiated.current = false; 21 | runOnUI(_ScrollTo)( 22 | ref, 23 | 0, 24 | Math.min(position, headerHeight - minHeaderHeight), 25 | false 26 | ); 27 | opacityValue.value = 1; 28 | }, 29 | // eslint-disable-next-line react-hooks/exhaustive-deps 30 | [headerHeight, minHeaderHeight, ref] 31 | ); 32 | 33 | return { 34 | opacityValue, 35 | initialPosition, 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /example/metro.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const escape = require("escape-string-regexp"); 3 | const { getDefaultConfig } = require("@expo/metro-config"); 4 | const exclusionList = require("metro-config/src/defaults/exclusionList"); 5 | const pak = require("../package.json"); 6 | 7 | const root = path.resolve(__dirname, ".."); 8 | 9 | const modules = Object.keys({ 10 | ...pak.peerDependencies, 11 | }); 12 | 13 | const defaultConfig = getDefaultConfig(__dirname); 14 | 15 | module.exports = { 16 | ...defaultConfig, 17 | 18 | projectRoot: __dirname, 19 | watchFolders: [root], 20 | 21 | // We need to make sure that only one version is loaded for peerDependencies 22 | // So we block them at the root, and alias them to the versions in example's node_modules 23 | resolver: { 24 | ...defaultConfig.resolver, 25 | nodeModulesPaths: [ 26 | path.resolve(__dirname, "node_modules"), 27 | path.resolve(root, "node_modules"), 28 | ], 29 | blacklistRE: exclusionList( 30 | modules.map( 31 | (m) => 32 | new RegExp(`^${escape(path.join(root, "node_modules", m))}\\/.*$`) 33 | ) 34 | ), 35 | extraNodeModules: modules.reduce((acc, name) => { 36 | acc[name] = path.join(__dirname, "node_modules", name); 37 | return acc; 38 | }, {}), 39 | sourceExts: ["ts", "tsx", "js", "jsx", "json", "cjs"], 40 | assetExts: ["glb", "gltf", "png", "jpg", "svg"], 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /src/utils.tsx: -------------------------------------------------------------------------------- 1 | import { Platform } from "react-native"; 2 | 3 | import { 4 | type SharedValue, 5 | runOnJS, 6 | scrollTo, 7 | withTiming, 8 | } from "react-native-reanimated"; 9 | 10 | export function _ScrollTo(ref: never, x: number, y: number, animated: boolean) { 11 | "worklet"; 12 | if (!ref) return; 13 | scrollTo(ref, x, y, animated); 14 | } 15 | 16 | export const isIOS = Platform.OS === "ios"; 17 | 18 | export const animateToRefresh = ({ 19 | transRefreshing, 20 | isRefreshing, 21 | isRefreshingWithAnimation, 22 | isToRefresh, 23 | destPoi, 24 | onStartRefresh, 25 | }: { 26 | transRefreshing: SharedValue; 27 | isRefreshing: SharedValue; 28 | isRefreshingWithAnimation: SharedValue; 29 | isToRefresh: boolean; 30 | destPoi: number; 31 | onStartRefresh?: () => void; 32 | }) => { 33 | "worklet"; 34 | 35 | if (isToRefresh === true && isRefreshing.value === true) return; 36 | if ( 37 | isToRefresh === false && 38 | isRefreshing.value === false && 39 | transRefreshing.value === destPoi 40 | ) 41 | return; 42 | isRefreshing.value = isToRefresh; 43 | if (isToRefresh && onStartRefresh) { 44 | runOnJS(onStartRefresh)(); 45 | } 46 | 47 | if (transRefreshing.value === destPoi) { 48 | isRefreshingWithAnimation.value = isToRefresh; 49 | return; 50 | } 51 | transRefreshing.value = withTiming(destPoi, undefined, () => { 52 | isRefreshingWithAnimation.value = isToRefresh; 53 | }); 54 | }; 55 | -------------------------------------------------------------------------------- /src/scene.web.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { ScrollViewProps, StyleSheet } from "react-native"; 3 | 4 | import Animated, { useSharedValue } from "react-native-reanimated"; 5 | 6 | import { useHeaderTabContext } from "./context"; 7 | import { useSharedScrollableRef } from "./hooks"; 8 | import type { SceneProps } from "./types"; 9 | 10 | export function SceneComponent

({ 11 | index, 12 | onScroll, 13 | ContainerView, 14 | contentContainerStyle, 15 | forwardedRef, 16 | style, 17 | ...restProps 18 | }: SceneProps

) { 19 | const { updateSceneInfo } = useHeaderTabContext(); 20 | const scollViewRef = 21 | useSharedScrollableRef(forwardedRef); 22 | const scrollY = useSharedValue(0); 23 | 24 | useEffect(() => { 25 | if (scollViewRef && scollViewRef.current) { 26 | updateSceneInfo({ 27 | scrollRef: scollViewRef, 28 | index, 29 | scrollY, 30 | }); 31 | } 32 | }, [scollViewRef, index, scrollY, updateSceneInfo]); 33 | 34 | return ( 35 | 45 | ); 46 | } 47 | 48 | const styles = StyleSheet.create({ 49 | container: { 50 | flex: 1, 51 | }, 52 | }); 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # XDE 6 | .expo/ 7 | 8 | # VSCode 9 | .vscode/ 10 | jsconfig.json 11 | 12 | # Xcode 13 | # 14 | build/ 15 | *.pbxuser 16 | !default.pbxuser 17 | *.mode1v3 18 | !default.mode1v3 19 | *.mode2v3 20 | !default.mode2v3 21 | *.perspectivev3 22 | !default.perspectivev3 23 | xcuserdata 24 | *.xccheckout 25 | *.moved-aside 26 | DerivedData 27 | *.hmap 28 | *.ipa 29 | *.xcuserstate 30 | project.xcworkspace 31 | 32 | # Android/IJ 33 | # 34 | .classpath 35 | .cxx 36 | .gradle 37 | .idea 38 | .project 39 | .settings 40 | local.properties 41 | android.iml 42 | 43 | # Cocoapods 44 | # 45 | example/ios/Pods 46 | 47 | # Ruby 48 | example/vendor/ 49 | 50 | # node.js 51 | # 52 | node_modules/ 53 | npm-debug.log 54 | yarn-debug.log 55 | yarn-error.log 56 | 57 | # BUCK 58 | buck-out/ 59 | \.buckd/ 60 | android/app/libs 61 | android/keystores/debug.keystore 62 | 63 | # Expo 64 | .expo/ 65 | 66 | # Turborepo 67 | .turbo/ 68 | 69 | # generated by bob 70 | lib/ 71 | 72 | 73 | # Native 74 | example/ios/ 75 | example/android/ 76 | 77 | 78 | # Yarn Berry 79 | .pnp.* 80 | .yarn/* 81 | !.yarn/patches 82 | !.yarn/plugins 83 | !.yarn/releases 84 | !.yarn/sdks 85 | !.yarn/versions 86 | 87 | 88 | .yarn/* 89 | !.yarn/patches 90 | !.yarn/plugins 91 | !.yarn/releases 92 | !.yarn/sdks 93 | !.yarn/versions 94 | 95 | example/.yarn/* 96 | !example/.yarn/patches 97 | !example/.yarn/plugins 98 | !example/.yarn/releases 99 | !example/.yarn/sdks 100 | !example/.yarn/versions -------------------------------------------------------------------------------- /src/hooks/use-scene-info.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | import { useCallback, useEffect, useState } from "react"; 3 | 4 | import Animated, { 5 | runOnJS, 6 | useAnimatedReaction, 7 | useSharedValue, 8 | } from "react-native-reanimated"; 9 | 10 | import type { UpdateSceneInfoParams } from "../types"; 11 | 12 | export const useSceneInfo = (curIndexValue: Animated.SharedValue) => { 13 | // Are all the fields on the scene ready 14 | const sceneIsReady = useSharedValue<{ [index: number]: boolean }>({}); 15 | 16 | const [childScrollYTrans, setChildScrollYTrans] = useState<{ 17 | [index: number]: Animated.SharedValue; 18 | }>({}); 19 | const [childScrollRef, setChildScrollRef] = useState<{ 20 | [index: number]: any; 21 | }>({}); 22 | 23 | const updateSceneInfo = useCallback( 24 | ({ index, scrollRef, scrollY }: UpdateSceneInfoParams) => { 25 | if (scrollRef && childScrollRef[index] !== scrollRef) { 26 | setChildScrollRef((preChildRef) => { 27 | return { ...preChildRef, [index]: scrollRef }; 28 | }); 29 | } 30 | 31 | if (scrollY && childScrollYTrans[index] !== scrollY) { 32 | setChildScrollYTrans((_p) => { 33 | return { ..._p, [index]: scrollY }; 34 | }); 35 | } 36 | }, 37 | [] 38 | ); 39 | 40 | const aArray = [childScrollRef, childScrollYTrans]; 41 | 42 | const updateIsReady = useCallback(() => { 43 | const mIndex = curIndexValue.value; 44 | const isReady = aArray.every((item) => 45 | Object.prototype.hasOwnProperty.call(item, mIndex) 46 | ); 47 | 48 | if (isReady) { 49 | sceneIsReady.value = { 50 | ...sceneIsReady.value, 51 | [mIndex]: isReady, 52 | }; 53 | } 54 | }, [curIndexValue, sceneIsReady, ...aArray]); 55 | 56 | // We should call function updateIsReady when the elements in the aArray change 57 | useEffect(() => { 58 | updateIsReady(); 59 | }, [updateIsReady, ...aArray]); 60 | 61 | /** 62 | * If all of the elements in the Aarray have changed, the tabIndex is switched. 63 | * At this point the above useEffect will not be called again, 64 | * and we will have to call the updateisReady function again. 65 | */ 66 | useAnimatedReaction( 67 | () => { 68 | return curIndexValue.value; 69 | }, 70 | () => { 71 | runOnJS(updateIsReady)(); 72 | }, 73 | [updateIsReady] 74 | ); 75 | 76 | return { 77 | childScrollRef, 78 | childScrollYTrans, 79 | sceneIsReady, 80 | updateSceneInfo, 81 | }; 82 | }; 83 | -------------------------------------------------------------------------------- /example/src/example.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from "react"; 2 | import { StatusBar, Text, View } from "react-native"; 3 | import { useSharedValue } from "react-native-reanimated"; 4 | import { Route, CollapsibleTabView } from "@showtime-xyz/tab-view"; 5 | import { TabFlashList } from "./tab-flash-list/index"; 6 | const StatusBarHeight = StatusBar.currentHeight ?? 0; 7 | const TabScene = ({ route }: any) => { 8 | return ( 9 | { 14 | return ( 15 | 24 | {`${route.title}-Item-${index}`} 25 | 26 | ); 27 | }} 28 | /> 29 | ); 30 | }; 31 | 32 | export function Example() { 33 | const [isRefreshing, setIsRefreshing] = useState(false); 34 | const [routes] = useState([ 35 | { key: "like", title: "Like", index: 0 }, 36 | { key: "owner", title: "Owner", index: 1 }, 37 | { key: "created", title: "Created", index: 2 }, 38 | ]); 39 | const [index, setIndex] = useState(0); 40 | const animationHeaderPosition = useSharedValue(0); 41 | const animationHeaderHeight = useSharedValue(0); 42 | 43 | const renderScene = useCallback(({ route }: any) => { 44 | switch (route.key) { 45 | case "like": 46 | return ; 47 | case "owner": 48 | return ; 49 | case "created": 50 | return ; 51 | default: 52 | return null; 53 | } 54 | }, []); 55 | 56 | const onStartRefresh = async () => { 57 | setIsRefreshing(true); 58 | setTimeout(() => { 59 | console.log("onStartRefresh"); 60 | setIsRefreshing(false); 61 | }, 300); 62 | }; 63 | const renderHeader = () => ( 64 | 65 | ); 66 | return ( 67 | 80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /src/create-collapsible-tabs.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | useCallback, 3 | useEffect, 4 | useImperativeHandle, 5 | useRef, 6 | } from "react"; 7 | 8 | import { 9 | type NavigationState, 10 | type SceneRendererProps, 11 | TabBar, 12 | type TabDescriptor, 13 | type TabView, 14 | type TabViewProps, 15 | } from "react-native-tab-view"; 16 | 17 | import { 18 | GestureContainer, 19 | type GestureContainerRef, 20 | } from "./gesture-container"; 21 | import type { 22 | CollapsibleHeaderProps, 23 | Route, 24 | TabViewCustomRenders, 25 | } from "./types"; 26 | 27 | export type CollapsibleTabViewRef = object; 28 | export type CollapsibleTabViewProps = Partial< 29 | TabViewProps 30 | > & 31 | Pick, "onIndexChange" | "navigationState" | "renderScene"> & 32 | CollapsibleHeaderProps; 33 | 34 | export type ForwardTabViewProps = 35 | CollapsibleTabViewProps & { 36 | forwardedRef: React.ForwardedRef; 37 | Component: React.PropsWithRef; 38 | }; 39 | 40 | export function createCollapsibleTabsComponent( 41 | Component: React.PropsWithRef 42 | ) { 43 | return React.forwardRef>( 44 | function tabView(props, ref) { 45 | return ( 46 | 51 | ); 52 | } 53 | ); 54 | } 55 | 56 | function CollapsibleHeaderTabView({ 57 | forwardedRef, 58 | ...props 59 | }: ForwardTabViewProps) { 60 | const gestureContainerRef = useRef(null); 61 | const initialPageRef = useRef(props.navigationState.index); 62 | 63 | useEffect(() => { 64 | gestureContainerRef.current?.setCurrentIndex(props.navigationState.index); 65 | }, [props.navigationState.index]); 66 | 67 | useImperativeHandle( 68 | forwardedRef, 69 | () => ({ 70 | // Todo: add snapTo tab view content method 71 | }), 72 | [] 73 | ); 74 | const renderTabBar = useCallback( 75 | ( 76 | tabbarProps: SceneRendererProps & { 77 | navigationState: NavigationState; 78 | options: Record> | undefined; 79 | } 80 | ) => { 81 | return props?.renderTabBar ? ( 82 | props.renderTabBar(tabbarProps) 83 | ) : ( 84 | 85 | ); 86 | }, 87 | [props] 88 | ); 89 | 90 | const renderTabView = (e: TabViewCustomRenders) => { 91 | const { Component, renderScene, ...restProps } = props; 92 | 93 | return ( 94 | ; 99 | options: Record> | undefined; 100 | } 101 | ) => e.renderTabBarContainer(renderTabBar(tabbarProps))} 102 | renderScene={(props) => e.renderSceneHeader(renderScene(props), props)} 103 | /> 104 | ); 105 | }; 106 | 107 | return ( 108 | // @ts-expect-error Needs fixing 109 | 115 | ); 116 | } 117 | -------------------------------------------------------------------------------- /src/types.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import type React from "react"; 3 | import type { ComponentClass } from "react"; 4 | import type { ScrollViewProps } from "react-native"; 5 | 6 | import type { NativeGesture } from "react-native-gesture-handler"; 7 | import type { SharedValue } from "react-native-reanimated"; 8 | import type { 9 | TabViewProps, 10 | Route as TabViewRoute, 11 | } from "react-native-tab-view"; 12 | 13 | export type Route = TabViewRoute & { 14 | key?: string; 15 | index: number; 16 | subtitle?: string | number; 17 | }; 18 | 19 | export enum RefreshTypeEnum { 20 | Idle = 0, 21 | Pending = 1, 22 | Success = 2, 23 | Refreshing = 3, 24 | Finish = 4, 25 | Cancel = 5, 26 | } 27 | 28 | export type CollapsibleHeaderProps = { 29 | initHeaderHeight?: number; 30 | renderScrollHeader?: () => React.ReactElement | null; 31 | overridenShareAnimatedValue?: SharedValue; 32 | overridenTranslateYValue?: SharedValue; 33 | initTabbarHeight?: number; 34 | minHeaderHeight?: number; 35 | overflowHeight?: number; 36 | headerRespond?: boolean; 37 | scrollEnabled?: boolean; 38 | isRefreshing?: boolean; 39 | onStartRefresh?: () => void; 40 | renderRefreshControl?: ( 41 | refreshProps: RefreshControlProps 42 | ) => React.ReactElement; 43 | refreshHeight?: number; 44 | overflowPull?: number; 45 | pullExtendedCoefficient?: number; 46 | animationHeaderPosition?: SharedValue; 47 | animationHeaderHeight?: SharedValue; 48 | panHeaderMaxOffset?: number; 49 | onPullEnough?: () => void; 50 | refreshControlColor?: string; 51 | refreshControlTop?: number; 52 | emptyBodyComponent?: JSX.Element | null; 53 | renderSceneHeader?: (props: T) => JSX.Element | null; 54 | /** 55 | * Enabling this option will prevent Reanimated & GestureHandler from crashing sometimes in debug mode. 56 | */ 57 | enableGestureRunOnJS?: boolean; 58 | }; 59 | 60 | export type TabViewCustomRenders = { 61 | // biome-ignore lint/suspicious/noExplicitAny: 62 | renderTabBarContainer: (children: any) => JSX.Element; 63 | // biome-ignore lint/suspicious/noExplicitAny: 64 | renderSceneHeader: (children: any, props: any) => JSX.Element; 65 | }; 66 | 67 | export type GestureContainerProps = Pick< 68 | TabViewProps, 69 | "navigationState" 70 | > & 71 | CollapsibleHeaderProps & { 72 | initialPage: number; 73 | renderTabView: (e: TabViewCustomRenders) => JSX.Element; 74 | }; 75 | 76 | export interface RefreshControlProps { 77 | refreshValue: SharedValue; 78 | refreshType: SharedValue; 79 | progress: SharedValue; 80 | refreshControlColor?: string; 81 | } 82 | export type SceneProps

= P & { 83 | // biome-ignore lint/suspicious/noExplicitAny: 84 | ContainerView: any; 85 | // biome-ignore lint/suspicious/noExplicitAny: 86 | forwardedRef: any; 87 | index: number; 88 | useExternalScrollView?: boolean; 89 | } & ScrollViewProps; 90 | 91 | export type UpdateSceneInfoParams = { 92 | // biome-ignore lint/suspicious/noExplicitAny: 93 | scrollRef: any; 94 | index: number; 95 | scrollY: SharedValue; 96 | }; 97 | 98 | export type ScrollableView = ComponentClass; 99 | 100 | export type ForwardRefType = 101 | | ((instance: T | null) => void) 102 | | React.MutableRefObject 103 | | null; 104 | 105 | export type GesturePanContext = { 106 | startY: SharedValue; 107 | basyY: SharedValue; 108 | }; 109 | export type TabHeaderContext = { 110 | isSlidingHeader: SharedValue; 111 | shareAnimatedValue: SharedValue; 112 | isStartRefreshing: SharedValue; 113 | minHeaderHeight: number; 114 | tabbarHeight: number; 115 | headerHeight: number; 116 | scrollStickyHeaderHeight: number; 117 | refreshHeight: number; 118 | overflowPull: number; 119 | pullExtendedCoefficient: number; 120 | headerTrans: SharedValue; 121 | expectHeight: number; 122 | refHasChanged: (ref: NativeGesture) => void; 123 | curIndexValue: SharedValue; 124 | updateSceneInfo: (e: UpdateSceneInfoParams) => void; 125 | scrollViewPaddingTop: number; 126 | } | null; 127 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are always welcome, no matter how large or small! 4 | 5 | We want this community to be friendly and respectful to each other. Please follow it in all your interactions with the project. Before contributing, please read the [code of conduct](./CODE_OF_CONDUCT.md). 6 | 7 | ## Development workflow 8 | 9 | To get started with the project, run `yarn` in the root directory to install the required dependencies for each package: 10 | 11 | ```sh 12 | yarn 13 | ``` 14 | 15 | > While it's possible to use [`npm`](https://github.com/npm/cli), the tooling is built around [`yarn`](https://classic.yarnpkg.com/), so you'll have an easier time if you use `yarn` for development. 16 | 17 | While developing, you can run the [example app](/example/) to test your changes. Any changes you make in your library's JavaScript code will be reflected in the example app without a rebuild. If you change any native code, then you'll need to rebuild the example app. 18 | 19 | To start the packager: 20 | 21 | ```sh 22 | yarn example start 23 | ``` 24 | 25 | To run the example app on Android: 26 | 27 | ```sh 28 | yarn example android 29 | ``` 30 | 31 | To run the example app on iOS: 32 | 33 | ```sh 34 | yarn example ios 35 | ``` 36 | 37 | To run the example app on Web: 38 | 39 | ```sh 40 | yarn example web 41 | ``` 42 | 43 | Make sure your code passes TypeScript and ESLint. Run the following to verify: 44 | 45 | ```sh 46 | yarn typecheck 47 | yarn lint 48 | ``` 49 | 50 | To fix formatting errors, run the following: 51 | 52 | ```sh 53 | yarn lint --fix 54 | ``` 55 | 56 | Remember to add tests for your change if possible. Run the unit tests by: 57 | 58 | ```sh 59 | yarn test 60 | ``` 61 | 62 | 63 | ### Commit message convention 64 | 65 | We follow the [conventional commits specification](https://www.conventionalcommits.org/en) for our commit messages: 66 | 67 | - `fix`: bug fixes, e.g. fix crash due to deprecated method. 68 | - `feat`: new features, e.g. add new method to the module. 69 | - `refactor`: code refactor, e.g. migrate from class components to hooks. 70 | - `docs`: changes into documentation, e.g. add usage example for the module.. 71 | - `test`: adding or updating tests, e.g. add integration tests using detox. 72 | - `chore`: tooling changes, e.g. change CI config. 73 | 74 | Our pre-commit hooks verify that your commit message matches this format when committing. 75 | 76 | ### Linting and tests 77 | 78 | [ESLint](https://eslint.org/), [Prettier](https://prettier.io/), [TypeScript](https://www.typescriptlang.org/) 79 | 80 | We use [TypeScript](https://www.typescriptlang.org/) for type checking, [ESLint](https://eslint.org/) with [Prettier](https://prettier.io/) for linting and formatting the code, and [Jest](https://jestjs.io/) for testing. 81 | 82 | Our pre-commit hooks verify that the linter and tests pass when committing. 83 | 84 | ### Publishing to npm 85 | 86 | We use [release-it](https://github.com/release-it/release-it) to make it easier to publish new versions. It handles common tasks like bumping version based on semver, creating tags and releases etc. 87 | 88 | To publish new versions, run the following: 89 | 90 | ```sh 91 | yarn release 92 | ``` 93 | 94 | ### Scripts 95 | 96 | The `package.json` file contains various scripts for common tasks: 97 | 98 | - `yarn bootstrap`: setup project by installing all dependencies and pods. 99 | - `yarn typecheck`: type-check files with TypeScript. 100 | - `yarn lint`: lint files with ESLint. 101 | - `yarn test`: run unit tests with Jest. 102 | - `yarn example start`: start the Metro server for the example app. 103 | - `yarn example android`: run the example app on Android. 104 | - `yarn example ios`: run the example app on iOS. 105 | 106 | ### Sending a pull request 107 | 108 | > **Working on your first pull request?** You can learn how from this _free_ series: [How to Contribute to an Open Source Project on GitHub](https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github). 109 | 110 | When you're sending a pull request: 111 | 112 | - Prefer small pull requests focused on one change. 113 | - Verify that linters and tests are passing. 114 | - Review the documentation to make sure it looks good. 115 | - Follow the pull request template when opening a pull request. 116 | - For pull requests that change the API or implementation, discuss with maintainers first by opening an issue. 117 | -------------------------------------------------------------------------------- /src/scene.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useRef } from "react"; 2 | import { StyleSheet } from "react-native"; 3 | 4 | import { Gesture, GestureDetector } from "react-native-gesture-handler"; 5 | import Animated, { 6 | runOnJS, 7 | scrollTo, 8 | useAnimatedRef, 9 | useAnimatedScrollHandler, 10 | useAnimatedStyle, 11 | useSharedValue, 12 | withTiming, 13 | } from "react-native-reanimated"; 14 | 15 | import { useSyncInitialPosition } from "./hooks/use-sync-initial-position"; 16 | import { useHeaderTabContext } from "./context"; 17 | import type { SceneProps } from "./types"; 18 | import { SCROLLABLE_STATE } from "./contants"; 19 | 20 | function mergeRefs( 21 | ...inputRefs: (React.Ref | undefined)[] 22 | ): React.Ref | React.RefCallback { 23 | const filteredInputRefs = inputRefs.filter(Boolean); 24 | 25 | if (filteredInputRefs.length <= 1) { 26 | const firstRef = filteredInputRefs[0]; 27 | 28 | return firstRef || null; 29 | } 30 | 31 | return function mergedRefs(ref) { 32 | for (const inputRef of filteredInputRefs) { 33 | if (typeof inputRef === "function") { 34 | inputRef(ref); 35 | } else if (inputRef) { 36 | (inputRef as React.MutableRefObject).current = ref; 37 | } 38 | } 39 | }; 40 | } 41 | 42 | export function SceneComponent

({ 43 | index, 44 | onScroll: propOnScroll, 45 | onContentSizeChange, 46 | ContainerView, 47 | contentContainerStyle, 48 | scrollIndicatorInsets, 49 | forwardedRef, 50 | useExternalScrollView = false, 51 | ...restProps 52 | }: SceneProps

) { 53 | //#region refs 54 | const nativeGestureRef = useRef(Gesture.Native()); 55 | const scollViewRef = useAnimatedRef(); 56 | //#endregion 57 | 58 | //#region hooks 59 | const { 60 | shareAnimatedValue, 61 | expectHeight, 62 | curIndexValue, 63 | refHasChanged, 64 | updateSceneInfo, 65 | scrollViewPaddingTop, 66 | } = useHeaderTabContext(); 67 | //#endregion 68 | 69 | //#region animations/style 70 | const scrollY = useSharedValue(0); 71 | const { opacityValue, initialPosition } = 72 | useSyncInitialPosition(scollViewRef); 73 | const sceneStyle = useAnimatedStyle(() => { 74 | return { 75 | opacity: withTiming(opacityValue.value), 76 | }; 77 | }, [opacityValue]); 78 | 79 | //#endregion 80 | 81 | //#region methods 82 | const onScrollAnimateEvent = useAnimatedScrollHandler( 83 | { 84 | onScroll: (e) => { 85 | scrollY.value = e.contentOffset.y; 86 | shareAnimatedValue.value = e.contentOffset.y; 87 | if (propOnScroll) { 88 | runOnJS(propOnScroll as never)({ nativeEvent: e }); 89 | } 90 | }, 91 | }, 92 | [] 93 | ); 94 | 95 | // adjust the scene size 96 | const _onContentSizeChange = useCallback( 97 | (contentWidth: number, contentHeight: number) => { 98 | onContentSizeChange?.(contentWidth, contentHeight); 99 | if (Math.ceil(contentHeight) >= expectHeight) { 100 | initialPosition(shareAnimatedValue.value); 101 | } 102 | }, 103 | [onContentSizeChange, initialPosition, expectHeight, shareAnimatedValue] 104 | ); 105 | //#endregion 106 | 107 | useEffect(() => { 108 | refHasChanged?.(nativeGestureRef.current); 109 | }, [refHasChanged]); 110 | useEffect(() => { 111 | if (scollViewRef?.current) { 112 | updateSceneInfo({ 113 | scrollRef: scollViewRef, 114 | index, 115 | scrollY, 116 | }); 117 | } 118 | }, [scollViewRef, index, scrollY, updateSceneInfo]); 119 | 120 | return ( 121 | 122 | 123 | 143 | 144 | 145 | ); 146 | } 147 | 148 | const styles = StyleSheet.create({ 149 | container: { 150 | flex: 1, 151 | }, 152 | }); 153 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@showtime-xyz/tab-view", 3 | "version": "0.1.7", 4 | "description": "A react native component, support collapse header and custom refresh control, power by Reanimated v2 & GestureHandler V2.", 5 | "main": "lib/commonjs/index", 6 | "module": "lib/module/index", 7 | "types": "lib/typescript/index.d.ts", 8 | "react-native": "src/index", 9 | "source": "src/index", 10 | "files": [ 11 | "src", 12 | "lib", 13 | "android", 14 | "ios", 15 | "cpp", 16 | "*.podspec", 17 | "!lib/typescript/example", 18 | "!ios/build", 19 | "!android/build", 20 | "!android/gradle", 21 | "!android/gradlew", 22 | "!android/gradlew.bat", 23 | "!android/local.properties", 24 | "!**/__tests__", 25 | "!**/__fixtures__", 26 | "!**/__mocks__", 27 | "!**/.*" 28 | ], 29 | "workspaces": [ 30 | "example" 31 | ], 32 | "scripts": { 33 | "test": "jest", 34 | "typecheck": "tsc --noEmit", 35 | "lint": "eslint \"**/*.{js,ts,tsx}\"", 36 | "prepack": "bob build", 37 | "release": "release-it", 38 | "example": "yarn --cwd example", 39 | "bootstrap": "yarn example && yarn install" 40 | }, 41 | "keywords": [ 42 | "react-native", 43 | "ios", 44 | "android" 45 | ], 46 | "repository": "https://github.com/showtime-xyz/showtime-tab-view", 47 | "author": "alantoa (https://github.com/showtime-xyz)", 48 | "license": "MIT", 49 | "bugs": { 50 | "url": "https://github.com/showtime-xyz/showtime-tab-view/issues" 51 | }, 52 | "homepage": "https://github.com/showtime-xyz/showtime-tab-view#readme", 53 | "publishConfig": { 54 | "registry": "https://registry.npmjs.org/" 55 | }, 56 | "devDependencies": { 57 | "@commitlint/config-conventional": "^17.0.2", 58 | "@evilmartians/lefthook": "^1.2.2", 59 | "@react-native-community/eslint-config": "^3.0.2", 60 | "@release-it/conventional-changelog": "^5.0.0", 61 | "@types/jest": "^28.1.2", 62 | "@types/react": "~17.0.21", 63 | "@types/react-native": "0.70.0", 64 | "commitlint": "^17.0.2", 65 | "del-cli": "^5.0.0", 66 | "eslint": "^8.4.1", 67 | "eslint-config-prettier": "^8.5.0", 68 | "eslint-plugin-prettier": "^4.0.0", 69 | "jest": "^28.1.1", 70 | "pod-install": "^0.1.0", 71 | "prettier": "^2.0.5", 72 | "react": "18.2.0", 73 | "react-native": "^0.71.8", 74 | "react-native-builder-bob": "^0.20.0", 75 | "react-native-gesture-handler": "~2.20.2", 76 | "react-native-pager-view": "6.5.1", 77 | "react-native-reanimated": "~3.16.1", 78 | "react-native-tab-view": "4.0.5", 79 | "release-it": "^15.0.0", 80 | "typescript": "^4.5.2" 81 | }, 82 | "resolutions": { 83 | "@types/react": "18.2.6", 84 | "@types/react-native": "0.72.0" 85 | }, 86 | "peerDependencies": { 87 | "react": "*", 88 | "react-native": "*", 89 | "react-native-gesture-handler": ">=2.0.0", 90 | "react-native-pager-view": ">=5.0.0", 91 | "react-native-reanimated": ">=2.0.0", 92 | "react-native-tab-view": ">3.3.0" 93 | }, 94 | "engines": { 95 | "node": ">= 16.0.0" 96 | }, 97 | "packageManager": "^yarn@1.22.15", 98 | "jest": { 99 | "preset": "react-native", 100 | "modulePathIgnorePatterns": [ 101 | "/example/node_modules", 102 | "/lib/" 103 | ] 104 | }, 105 | "commitlint": { 106 | "extends": [ 107 | "@commitlint/config-conventional" 108 | ] 109 | }, 110 | "release-it": { 111 | "git": { 112 | "commitMessage": "chore: release ${version}", 113 | "tagName": "v${version}" 114 | }, 115 | "npm": { 116 | "publish": true 117 | }, 118 | "github": { 119 | "release": true 120 | }, 121 | "plugins": { 122 | "@release-it/conventional-changelog": { 123 | "preset": "angular" 124 | } 125 | } 126 | }, 127 | "eslintConfig": { 128 | "root": true, 129 | "extends": [ 130 | "@react-native-community", 131 | "prettier" 132 | ], 133 | "rules": { 134 | "prettier/prettier": [ 135 | "error", 136 | { 137 | "quoteProps": "consistent", 138 | "singleQuote": false, 139 | "tabWidth": 2, 140 | "trailingComma": "es5", 141 | "useTabs": false 142 | } 143 | ], 144 | "no-unused-vars": "off", 145 | "@typescript-eslint/no-unused-vars": "warn" 146 | } 147 | }, 148 | "eslintIgnore": [ 149 | "node_modules/", 150 | "lib/" 151 | ], 152 | "prettier": { 153 | "quoteProps": "consistent", 154 | "singleQuote": false, 155 | "tabWidth": 2, 156 | "trailingComma": "es5", 157 | "useTabs": false 158 | }, 159 | "react-native-builder-bob": { 160 | "source": "src", 161 | "output": "lib", 162 | "targets": [ 163 | "commonjs", 164 | "module", 165 | [ 166 | "typescript", 167 | { 168 | "project": "tsconfig.build.json" 169 | } 170 | ] 171 | ] 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/refresh-control.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useCallback, useMemo } from "react"; 2 | import { ActivityIndicator, StyleSheet } from "react-native"; 3 | 4 | import Animated, { 5 | useAnimatedReaction, 6 | useAnimatedStyle, 7 | useDerivedValue, 8 | useSharedValue, 9 | withSpring, 10 | type SharedValue, 11 | } from "react-native-reanimated"; 12 | 13 | import { useRefreshDerivedValue } from "./hooks/use-refresh-value"; 14 | import { RefreshTypeEnum, type RefreshControlProps } from "./types"; 15 | 16 | type RefreshControlContainerProps = { 17 | top: number; 18 | refreshHeight: number; 19 | overflowPull: number; 20 | opacityValue: SharedValue; 21 | refreshValue: SharedValue; 22 | isRefreshing: SharedValue; 23 | isRefreshingWithAnimation: SharedValue; 24 | pullExtendedCoefficient: number; 25 | renderContent?: (refreshProps: RefreshControlProps) => React.ReactElement; 26 | refreshControlColor?: string; 27 | }; 28 | 29 | const RefreshControlContainer = memo( 30 | ({ 31 | top, 32 | refreshHeight, 33 | overflowPull, 34 | opacityValue, 35 | refreshValue, 36 | isRefreshing, 37 | isRefreshingWithAnimation, 38 | pullExtendedCoefficient, 39 | renderContent, 40 | refreshControlColor = "#999999", 41 | }) => { 42 | const refreshType = useSharedValue(RefreshTypeEnum.Idle); 43 | 44 | const progress = useDerivedValue(() => { 45 | "worklet"; 46 | if (isRefreshingWithAnimation.value) return 1; 47 | return Math.min(refreshValue.value / refreshHeight, 1); 48 | }, [refreshHeight]); 49 | 50 | const tranYValue = useSharedValue(0); 51 | 52 | useRefreshDerivedValue(tranYValue, { 53 | animatedValue: refreshValue, 54 | refreshHeight, 55 | overflowPull, 56 | pullExtendedCoefficient, 57 | }); 58 | 59 | useAnimatedReaction( 60 | () => { 61 | "worklet"; 62 | return { 63 | _progress: progress.value, 64 | _isRefreshing: isRefreshing.value, 65 | _isRefreshingWithAnimation: isRefreshingWithAnimation.value, 66 | }; 67 | }, 68 | ({ _progress, _isRefreshing, _isRefreshingWithAnimation }) => { 69 | "worklet"; 70 | if (_isRefreshing !== _isRefreshingWithAnimation) { 71 | refreshType.value = _isRefreshing 72 | ? RefreshTypeEnum.Pending 73 | : RefreshTypeEnum.Finish; 74 | return; 75 | } 76 | if (_isRefreshing) { 77 | refreshType.value = RefreshTypeEnum.Refreshing; 78 | } else { 79 | refreshType.value = 80 | _progress < 1 ? RefreshTypeEnum.Cancel : RefreshTypeEnum.Success; 81 | } 82 | }, 83 | [refreshHeight] 84 | ); 85 | 86 | const animatedStyle = useAnimatedStyle(() => { 87 | "worklet"; 88 | return { 89 | opacity: withSpring(opacityValue.value, { 90 | mass: 1, 91 | damping: 15, 92 | stiffness: 120, 93 | }), 94 | transform: [ 95 | { 96 | translateY: tranYValue.value, 97 | }, 98 | ], 99 | }; 100 | }, []); 101 | 102 | const childProps = useMemo( 103 | () => ({ 104 | refreshValue, 105 | refreshType, 106 | progress, 107 | }), 108 | [refreshValue, refreshType, progress] 109 | ); 110 | 111 | const _renderContent = useCallback(() => { 112 | if (renderContent) { 113 | return React.cloneElement(renderContent(childProps), childProps); 114 | } 115 | return ( 116 | 120 | ); 121 | }, [renderContent, childProps, refreshControlColor]); 122 | 123 | const containerStyle = useMemo( 124 | () => [ 125 | styles.container, 126 | { top: top - refreshHeight, height: refreshHeight }, 127 | animatedStyle, 128 | ], 129 | [top, refreshHeight, animatedStyle] 130 | ); 131 | 132 | return ( 133 | {_renderContent()} 134 | ); 135 | } 136 | ); 137 | 138 | const styles = StyleSheet.create({ 139 | baseControl: { 140 | alignItems: "center", 141 | flex: 1, 142 | justifyContent: "center", 143 | paddingTop: 10, 144 | }, 145 | container: { 146 | left: 0, 147 | position: "absolute", 148 | right: 0, 149 | width: "100%", 150 | }, 151 | textStyle: { 152 | marginTop: 4, 153 | fontSize: 13, 154 | textAlign: "center", 155 | }, 156 | }); 157 | 158 | const RefreshControlNormal = memo< 159 | RefreshControlProps & { refreshControlColor?: string } 160 | >(function RefreshControlNormal({ refreshControlColor }) { 161 | return ( 162 | 163 | 164 | 165 | ); 166 | }); 167 | 168 | export default RefreshControlContainer; 169 | -------------------------------------------------------------------------------- /src/create-collapsible-tabs.web.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | useCallback, 3 | useImperativeHandle, 4 | useState, 5 | useRef, 6 | } from "react"; 7 | import { LayoutChangeEvent, StyleSheet, View } from "react-native"; 8 | 9 | import { 10 | NavigationState, 11 | SceneRendererProps, 12 | TabBar, 13 | TabView, 14 | TabViewProps, 15 | } from "react-native-tab-view"; 16 | 17 | import { HeaderTabContext } from "./context"; 18 | import { useSceneInfo } from "./hooks"; 19 | import type { 20 | CollapsibleHeaderProps, 21 | Route, 22 | TabViewCustomRenders, 23 | } from "./types"; 24 | 25 | export { 26 | TabFlatList, 27 | TabScrollView, 28 | TabSectionList, 29 | TabScrollViewProps, 30 | TabFlatListProps, 31 | TabSectionListProps, 32 | } from "./scrollable-view"; 33 | 34 | export type HeaderTabViewRef = {}; 35 | export type HeaderTabViewProps = Partial> & 36 | Pick, "onIndexChange" | "navigationState" | "renderScene"> & 37 | CollapsibleHeaderProps; 38 | 39 | export function createCollapsibleTabsComponent() { 40 | return React.forwardRef(CollapsibleHeaderTabView); 41 | } 42 | 43 | function CollapsibleHeaderTabView( 44 | { 45 | renderTabBar: renderTabBarProp, 46 | renderScrollHeader, 47 | initTabbarHeight = 44, 48 | minHeaderHeight = 0, 49 | navigationState, 50 | emptyBodyComponent, 51 | renderScene, 52 | renderSceneHeader: renderSceneHeaderProp, 53 | ...restProps 54 | }: HeaderTabViewProps, 55 | ref?: any 56 | ) { 57 | const shareAnimatedValue = { value: 0 }; 58 | const headerTrans = { value: 0 }; 59 | const curIndexValue = { value: 0 }; 60 | const isSlidingHeader = { value: false }; 61 | const isStartRefreshing = { value: false }; 62 | 63 | // layout 64 | const [tabbarHeight, setTabbarHeight] = useState(initTabbarHeight); 65 | 66 | const containeRef = useRef(null); 67 | useImperativeHandle(ref, () => ({}), []); 68 | const tabbarOnLayout = useCallback( 69 | ({ 70 | nativeEvent: { 71 | layout: { height }, 72 | }, 73 | }: LayoutChangeEvent) => { 74 | if (Math.abs(tabbarHeight - height) < 1) return; 75 | setTabbarHeight(height); 76 | }, 77 | [tabbarHeight] 78 | ); 79 | const renderTabBar = useCallback( 80 | ( 81 | tabbarProps: SceneRendererProps & { 82 | navigationState: NavigationState; 83 | } 84 | ) => { 85 | return renderTabBarProp ? ( 86 | renderTabBarProp(tabbarProps as any) 87 | ) : ( 88 | 89 | ); 90 | }, 91 | [renderTabBarProp] 92 | ); 93 | 94 | const renderTabView = (e: TabViewCustomRenders) => { 95 | return ( 96 | ; 102 | } 103 | ) => e.renderTabBarContainer(renderTabBar(tabbarProps))} 104 | renderScene={(props: any) => 105 | e.renderSceneHeader(renderScene(props), props) 106 | } 107 | /> 108 | ); 109 | }; 110 | 111 | const renderTabBarContainer = (children: React.ReactElement) => { 112 | return ( 113 | 114 | {children} 115 | 116 | ); 117 | }; 118 | const renderSceneHeader = ( 119 | children: React.ReactElement, 120 | props: SceneRendererProps & { route: T } 121 | ) => { 122 | return ( 123 | 124 | {renderSceneHeaderProp?.(props.route)} 125 | {children} 126 | 127 | ); 128 | }; 129 | 130 | const { updateSceneInfo } = useSceneInfo(curIndexValue); 131 | return ( 132 | false, 143 | curIndexValue, 144 | minHeaderHeight, 145 | updateSceneInfo, 146 | isSlidingHeader, 147 | isStartRefreshing, 148 | scrollStickyHeaderHeight: 0, 149 | scrollViewPaddingTop: 0, 150 | }} 151 | > 152 | 153 | {renderScrollHeader && renderScrollHeader()} 154 | {navigationState.routes.length === 0 && emptyBodyComponent ? ( 155 | {emptyBodyComponent} 156 | ) : ( 157 | renderTabView({ 158 | renderTabBarContainer: renderTabBarContainer, 159 | renderSceneHeader: renderSceneHeader, 160 | }) 161 | )} 162 | 163 | 164 | ); 165 | } 166 | 167 | const styles = StyleSheet.create({ 168 | full: { 169 | flex: 1, 170 | }, 171 | tabbarStyle: { 172 | zIndex: 1, 173 | }, 174 | }); 175 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual 11 | identity and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the overall 27 | community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or advances of 32 | any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email address, 36 | without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | [INSERT CONTACT METHOD]. 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series of 87 | actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or permanent 94 | ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within the 114 | community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.1, available at 120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 127 | [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | showtime tab view logo 2 |

3 |

Showtime Tab View

4 |
5 | 6 | A React Native component that supports a collapsible header and custom refresh control, powered by [Reanimated v2](https://docs.swmansion.com/react-native-reanimated/) and [GestureHandler V2](https://docs.swmansion.com/react-native-gesture-handler/docs/). 7 |