├── .idea ├── .gitignore ├── misc.xml ├── vcs.xml ├── react-native-scrolling-menu.iml └── modules.xml ├── assets ├── 1.gif └── 2.gif ├── .gitignore ├── package.json ├── lib ├── index.tsx └── index.js └── README.md /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /workspace.xml -------------------------------------------------------------------------------- /assets/1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sekizlipenguen/react-native-scroll-menu/HEAD/assets/1.gif -------------------------------------------------------------------------------- /assets/2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sekizlipenguen/react-native-scroll-menu/HEAD/assets/2.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | .idea/ 3 | node_modules/ 4 | .DS_Store 5 | yarn-error.log 6 | package-lock.json 7 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/react-native-scrolling-menu.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sekizlipenguen/react-native-scroll-menu", 3 | "version": "1.8.3", 4 | "description": "A customizable and easy-to-use horizontal scrolling menu for React Native.", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/sekizlipenguen/react-native-scroll-menu.git" 10 | }, 11 | "keywords": [ 12 | "react-native", 13 | "scroll menu", 14 | "horizontal menu", 15 | "react native menu", 16 | "react native horizontal menu", 17 | "scrolling menu", 18 | "react native components" 19 | ], 20 | "author": "SekizliPenguen", 21 | "license": "MIT", 22 | "licenseFilename": "LICENSE", 23 | "readmeFilename": "README.md", 24 | "peerDependencies": { 25 | "react": ">=17.0.0", 26 | "react-native": ">=0.60.0" 27 | }, 28 | "bugs": { 29 | "url": "https://github.com/sekizlipenguen/react-native-scroll-menu/issues" 30 | }, 31 | "homepage": "https://github.com/sekizlipenguen/react-native-scroll-menu#readme" 32 | } 33 | -------------------------------------------------------------------------------- /lib/index.tsx: -------------------------------------------------------------------------------- 1 | declare module "react-native-scroll-menu" { 2 | import {StyleProp, TextStyle, ViewStyle} from "react-native" 3 | 4 | type routeProps = { 5 | id: string; 6 | name: string; 7 | }; 8 | 9 | export type NavigationTabsProps = { 10 | id: string; 11 | name?: string; 12 | component?: React.ReactNode; 13 | onPress?: (route: routeProps) => void; 14 | }; 15 | 16 | type ScrollingButtonMenuProps = { 17 | items: Array; 18 | upperCase?: boolean; 19 | textStyle?: StyleProp; 20 | activeTextStyle?: StyleProp; 21 | buttonStyle?: StyleProp; 22 | firstButtonStyle?: StyleProp; 23 | lastButtonStyle?: StyleProp; 24 | activeButtonStyle?: StyleProp; 25 | activeColor?: string; 26 | activeBackgroundColor?: string; 27 | selected: string; 28 | selectedOpacity?: number; 29 | containerStyle?: object; 30 | contentContainerStyle?: object; 31 | scrollStyle?: object; 32 | keyboardShouldPersistTaps?: boolean | 'always' | 'never' | 'handled'; 33 | }; 34 | export const ScrollingButtonMenu: React.FC 35 | } 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![platforms](https://img.shields.io/badge/platforms-Android%20%7C%20iOS-brightgreen.svg?style=flat-square&colorB=191A17) 2 | [![npm](https://img.shields.io/npm/v/@sekizlipenguen/react-native-scroll-menu.svg?style=flat-square)](https://www.npmjs.com/package/@sekizlipenguen/react-native-scroll-menu) 3 | [![npm](https://img.shields.io/npm/dm/@sekizlipenguen/react-native-scroll-menu.svg?style=flat-square&colorB=007ec6)](https://www.npmjs.com/package/@sekizlipenguen/react-native-scroll-menu) 4 | 5 | # @sekizlipenguen/react-native-scroll-menu 6 | 7 | A lightweight and customizable horizontal scrolling menu component for React Native. Designed for simplicity and flexibility, this package helps you create smooth and interactive menus for your mobile applications. 8 | 9 | **Note:** This package is now part of the `@sekizlipenguen` scope to improve maintainability and provide better support for future updates. 10 | 11 | --- 12 | 13 | ## Installation 14 | 15 | Install the package using npm or yarn: 16 | 17 | ```bash 18 | npm install @sekizlipenguen/react-native-scroll-menu 19 | ``` 20 | 21 | ```bash 22 | yarn add @sekizlipenguen/react-native-scroll-menu 23 | ``` 24 | 25 | --- 26 | 27 | ## Example 28 | 29 | | Example | 30 | |:----------------------------------------------------------:| 31 | | ![](assets/1.gif) | 32 | | ![](assets/2.gif) | 33 | 34 | --- 35 | 36 | ## Usage 37 | 38 | Here’s a simple example of how to use the `@sekizlipenguen/react-native-scroll-menu`: 39 | 40 | ### Class Component Example 41 | 42 | ```javascript 43 | import React, { Component } from 'react'; 44 | import { View } from 'react-native'; 45 | 46 | // Import the component 47 | import ScrollingButtonMenu from '@sekizlipenguen/react-native-scroll-menu'; 48 | 49 | export default class Example extends Component { 50 | render() { 51 | return ( 52 | console.log(e)} 61 | selected={"1"} 62 | /> 63 | ); 64 | } 65 | } 66 | ``` 67 | 68 | ### Functional Component with Hook Example 69 | 70 | ```javascript 71 | import React, { useState } from 'react'; 72 | import { View, Text } from 'react-native'; 73 | 74 | // Import the component 75 | import ScrollingButtonMenu from '@sekizlipenguen/react-native-scroll-menu'; 76 | 77 | const ExampleWithHook = () => { 78 | const [selectedItem, setSelectedItem] = useState("1"); 79 | 80 | const handlePress = (item) => { 81 | console.log(item); 82 | setSelectedItem(item.id); 83 | }; 84 | 85 | return ( 86 | 87 | 98 | Selected Item: {selectedItem} 99 | 100 | ); 101 | }; 102 | 103 | export default ExampleWithHook; 104 | ``` 105 | 106 | ### Using Custom Components and Images in Items 107 | 108 | You can now use custom React components (e.g. images, icons, or any JSX) instead of just plain text labels. 109 | To do this, use the `component` property instead of `name`: 110 | 111 | ```jsx 112 | items = { 113 | [ 114 | {id: "1", name: 'Life'}, 115 | { 116 | id: "2", 117 | component 118 | : 119 | ( 120 | 121 | 122 | Icon 123 | 124 | ), 125 | } 126 | , 127 | { 128 | id: "3", name 129 | : 130 | 'Faith' 131 | } 132 | , 133 | ] 134 | } 135 | ``` 136 | 137 | > You can combine images with labels using any layout (e.g. `View`, `Text`, etc.) to create richer item content. 138 | 139 | If the `component` field is present, the `name` field will be ignored and only the custom component will be rendered. 140 | 141 | ### Per-Item `onPress` Function 142 | 143 | You can define a custom `onPress` function for each item individually. If provided, it will override the global `onPress` for that specific item. 144 | 145 | ```jsx 146 | items = { 147 | [ 148 | {id: "1", name: 'Home'}, 149 | { 150 | id: "2", 151 | name 152 | : 153 | 'Profile', 154 | onPress 155 | : 156 | () => { 157 | console.log("Profile pressed!"); 158 | }, 159 | } 160 | , 161 | { 162 | id: "3", name 163 | : 164 | 'Settings' 165 | } 166 | , 167 | ] 168 | } 169 | ``` 170 | 171 | > If `onPress` is provided on an item, it will be executed instead of the global `onPress` passed to the component. 172 | 173 | ### Optional Button Style (No Default) 174 | 175 | If you want full control over styling and don't want the default button style, you can omit it by passing an empty `buttonStyle`: 176 | 177 | ```jsx 178 | 182 | ``` 183 | 184 | When `buttonStyle` is set, the default internal style will not be applied. This allows full customization from the outside. 185 | 186 | --- 187 | 188 | ## Props 189 | 190 | | Key | Type | Description | 191 | |-----------------------------|------------------|-------------------------------------------------------| 192 | | `items` | Array | Array for button menu (required). | 193 | | `onPress` | Function(menu) | Function triggered on button press (required). | 194 | | `upperCase` | Boolean | Convert text to uppercase. Default: `false`. | 195 | | `selectedOpacity` | Number | Opacity when button is pressed. Default: `0.7`. | 196 | | `containerStyle` | Object | Style for the container. | 197 | | `contentContainerStyle` | Object | Style for the content container. | 198 | | `scrollStyle` | Object | Style for the scroll view. | 199 | | `textStyle` | Object | Style for the text. | 200 | | `buttonStyle` | Object | Style for the button. | 201 | | `activeButtonStyle` | Object | Style for the active button. | 202 | | `firstButtonStyle` | Object | Style for the first button. | 203 | | `lastButtonStyle` | Object | Style for the last button. | 204 | | `activeTextStyle` | Object | Style for the active text. | 205 | | `activeColor` | String | Active button text color. Default: `"#ffffff"`. | 206 | | `activeBackgroundColor` | String | Active button background color. Default: `"#ffffff"`. | 207 | | `selected` | String or Number | Selected item id. Default: `1`. | 208 | | `keyboardShouldPersistTaps` | String | Default: `"always"`. | 209 | 210 | --- 211 | 212 | ## Thank You! 213 | 214 | We appreciate your support and feedback! If you encounter any issues, feel free to [open an issue](https://github.com/sekizlipenguen/react-native-scroll-menu/issues). 215 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {Dimensions, ScrollView, StyleSheet, Text, TouchableOpacity, View} from 'react-native'; 4 | 5 | export const {width: screenWidth, height: screenHeight} = Dimensions.get('window'); 6 | 7 | export default class ScrollingButtonMenu extends React.Component { 8 | 9 | constructor(props) { 10 | super(props); 11 | 12 | this.scroll = React.createRef(); 13 | this.dataSourceCords = []; 14 | 15 | this.state = { 16 | index: '', 17 | }; 18 | } 19 | 20 | componentDidUpdate(prevProps) { 21 | const {selected} = this.props; 22 | if (this.props.selected != this.state.index) { 23 | this.setState({index: selected}, () => { 24 | this._scrollTo(); 25 | }); 26 | } 27 | } 28 | 29 | componentDidMount() { 30 | const {selected} = this.props; 31 | this.setState({index: selected}, () => { 32 | setTimeout(() => { 33 | this._scrollTo(); 34 | }, 600); 35 | }); 36 | } 37 | 38 | _scrollTo() { 39 | const {index} = this.state; 40 | const screen1 = screenWidth / 2; 41 | const elementOffset = this.dataSourceCords[index]; 42 | if (elementOffset !== undefined && this.scroll && typeof this.scroll.scrollTo == 'function') { 43 | let x = elementOffset.x - (screen1 - (elementOffset.width / 2)); 44 | this.scroll.scrollTo({ 45 | y: 0, 46 | x: x, 47 | animated: true, 48 | }); 49 | } 50 | } 51 | 52 | render() { 53 | const { 54 | items, 55 | upperCase, 56 | selectedOpacity, 57 | activeBackgroundColor, 58 | activeColor, 59 | textStyle, 60 | activeTextStyle, 61 | buttonStyle, 62 | firstButtonStyle, 63 | lastButtonStyle, 64 | activeButtonStyle, 65 | containerStyle, 66 | contentContainerStyle, 67 | scrollStyle, 68 | keyboardShouldPersistTaps, 69 | } = this.props; 70 | const {index} = this.state; 71 | 72 | return ( 73 | 77 | this.scroll = node} 82 | style={[styles.scroll,scrollStyle]} 83 | contentContainerStyle={[styles.scrollContainer,contentContainerStyle]} 84 | scrollEventThrottle={200} 85 | lazy={false} 86 | keyboardShouldPersistTaps={keyboardShouldPersistTaps} 87 | > 88 | { 89 | items.map((route, i) => ( 90 | this.setState({index: route.id}, () => setTimeout(() => { 103 | this._scrollTo(); 104 | if (typeof route.onPress === 'function') { 105 | return route.onPress(route); 106 | } else { 107 | return this.props.onPress(route); 108 | } 109 | }, 50))} 110 | onLayout={(event) => { 111 | const layout = event.nativeEvent.layout; 112 | this.dataSourceCords[route.id] = layout; 113 | }} 114 | activeOpacity={selectedOpacity} 115 | > 116 | { 117 | route.component 118 | ? route.component 119 | : 126 | {upperCase ? route.name.toUpperCase() : route.name} 127 | 128 | } 129 | 130 | )) 131 | } 132 | 133 | 134 | ); 135 | } 136 | } 137 | 138 | // Not: item objesi artık isteğe bağlı olarak component (React öğesi) alabilir. 139 | // Eğer bu alan sağlanırsa name alanı render edilmez, yerine component gösterilir. 140 | ScrollingButtonMenu.propTypes = { 141 | items: PropTypes.arrayOf(PropTypes.shape({ 142 | id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, 143 | name: PropTypes.string, 144 | component: PropTypes.element, 145 | onPress: PropTypes.func, 146 | })).isRequired, 147 | onPress: PropTypes.func.isRequired, 148 | upperCase: PropTypes.bool, 149 | textStyle: PropTypes.object, 150 | activeTextStyle: PropTypes.object, 151 | buttonStyle: PropTypes.object, 152 | firstButtonStyle: PropTypes.object, 153 | lastButtonStyle: PropTypes.object, 154 | activeButtonStyle: PropTypes.object, 155 | activeColor: PropTypes.string, 156 | activeBackgroundColor: PropTypes.string, 157 | selected: PropTypes.number, 158 | selectedOpacity: PropTypes.number, 159 | containerStyle: PropTypes.object, 160 | contentContainerStyle: PropTypes.object, 161 | keyboardShouldPersistTaps: PropTypes.string, 162 | }; 163 | 164 | ScrollingButtonMenu.defaultProps = { 165 | upperCase: false, 166 | textStyle: {}, 167 | activeTextStyle: {}, 168 | buttonStyle: { 169 | borderRadius: 4, 170 | marginRight: 10, 171 | }, 172 | activeColor: '', 173 | activeBackgroundColor: '#1e1e1e', 174 | selected: '', 175 | onPress: () => { 176 | 177 | }, 178 | selectedOpacity: 0.7, 179 | containerStyle: {}, 180 | contentContainerStyle:{}, 181 | scrollStyle:{}, 182 | keyboardShouldPersistTaps: 'always', 183 | lastButtonStyle: {}, 184 | firstButtonStyle: {}, 185 | activeButtonStyle: {}, 186 | }; 187 | 188 | const styles = StyleSheet.create({ 189 | scrollArea: { 190 | paddingLeft: 20, 191 | paddingTop: 20, 192 | }, 193 | scroll: {}, 194 | scrollContainer: {}, 195 | tabItem: { 196 | borderRadius: 18, 197 | borderColor: '#858585', 198 | borderStyle: 'solid', 199 | borderWidth: 1, 200 | padding: 6, 201 | paddingLeft: 15, 202 | paddingRight: 15, 203 | marginRight: 10, 204 | }, 205 | tabItemText: { 206 | color: '#5d5d5d', 207 | fontSize: 14, 208 | fontWeight: '500', 209 | fontStyle: 'normal', 210 | textAlign: 'left', 211 | lineHeight: 20, 212 | }, 213 | tabItemFocused: { 214 | borderWidth: 0, 215 | }, 216 | tabItemTextFocused: { 217 | color: '#fff', 218 | }, 219 | }); 220 | --------------------------------------------------------------------------------