├── .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 |
5 |
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 | 
2 | [](https://www.npmjs.com/package/@sekizlipenguen/react-native-scroll-menu)
3 | [](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 | |  |
32 | |  |
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 |
--------------------------------------------------------------------------------