Skip to content

Commit 05dfdf6

Browse files
committed
fix(menu): dropdown position with translucent status bar
1 parent 9162a1f commit 05dfdf6

File tree

9 files changed

+203
-1761
lines changed

9 files changed

+203
-1761
lines changed

Diff for: example/metro.config.js

-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ const defaultConfig = getDefaultConfig(__dirname);
1313

1414
const modules = [
1515
'@expo/vector-icons',
16-
'expo-constants',
1716
...Object.keys(pak.peerDependencies),
1817
'@react-navigation/native',
1918
];

Diff for: example/src/Examples/MenuExample.tsx

+23-6
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ const MenuExample = ({ navigation }: Props) => {
4848
x: nativeEvent.pageX,
4949
y: nativeEvent.pageY,
5050
});
51-
setVisible({ menu3: true });
51+
setVisible({ menu4: true });
5252
};
5353

5454
React.useLayoutEffect(() => {
@@ -127,9 +127,25 @@ const MenuExample = ({ navigation }: Props) => {
127127
)}
128128
</Menu>
129129
</View>
130+
<View style={styles.alignCenter}>
131+
<Menu
132+
visible={_getVisible('menu3')}
133+
onDismiss={_toggleMenu('menu3')}
134+
anchor={
135+
<Button mode="outlined" onPress={_toggleMenu('menu3')}>
136+
Menu with anchorPosition bottom
137+
</Button>
138+
}
139+
anchorPosition="bottom"
140+
>
141+
<Menu.Item onPress={() => {}} title="One" />
142+
<Menu.Item onPress={() => {}} title="Two" />
143+
<Menu.Item onPress={() => {}} title="Three" />
144+
</Menu>
145+
</View>
130146
<Menu
131-
visible={_getVisible('menu3')}
132-
onDismiss={_toggleMenu('menu3')}
147+
visible={_getVisible('menu4')}
148+
onDismiss={_toggleMenu('menu4')}
133149
anchor={contextualMenuCoord}
134150
>
135151
<Menu.Item onPress={() => {}} title="Item 1" />
@@ -149,10 +165,10 @@ const MenuExample = ({ navigation }: Props) => {
149165

150166
<View style={styles.bottomMenu}>
151167
<Menu
152-
visible={_getVisible('menu4')}
153-
onDismiss={_toggleMenu('menu4')}
168+
visible={_getVisible('menu5')}
169+
onDismiss={_toggleMenu('menu5')}
154170
anchor={
155-
<Button mode="outlined" onPress={_toggleMenu('menu4')}>
171+
<Button mode="outlined" onPress={_toggleMenu('menu5')}>
156172
Menu at bottom
157173
</Button>
158174
}
@@ -181,6 +197,7 @@ const styles = StyleSheet.create({
181197
},
182198
alignCenter: {
183199
alignItems: 'center',
200+
marginVertical: 8,
184201
},
185202
md3Divider: {
186203
marginVertical: 8,

Diff for: example/yarn.lock

+29-598
Large diffs are not rendered by default.

Diff for: package.json

-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,6 @@
8383
"eslint": "8.31.0",
8484
"eslint-plugin-flowtype": "^8.0.3",
8585
"eslint-plugin-local-rules": "^1.3.2",
86-
"expo-constants": "^9.3.5",
8786
"glob": "^7.1.3",
8887
"husky": "^1.3.1",
8988
"jest": "^29.2.1",

Diff for: src/components/Menu/Menu.tsx

+21-9
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,12 @@ import {
2020
Pressable,
2121
} from 'react-native';
2222

23+
import {
24+
withSafeAreaInsets,
25+
WithSafeAreaInsetsProps,
26+
} from 'react-native-safe-area-context';
27+
2328
import MenuItem from './MenuItem';
24-
import { APPROX_STATUSBAR_HEIGHT } from '../../constants';
2529
import { withInternalTheme } from '../../core/theming';
2630
import type { $Omit, InternalTheme, MD3Elevation } from '../../types';
2731
import { ElevationLevels } from '../../types';
@@ -30,7 +34,7 @@ import { BackHandler } from '../../utils/BackHandler/BackHandler';
3034
import Portal from '../Portal/Portal';
3135
import Surface from '../Surface';
3236

33-
export type Props = {
37+
export type Props = WithSafeAreaInsetsProps & {
3438
/**
3539
* Whether the Menu is currently visible.
3640
*/
@@ -45,10 +49,9 @@ export type Props = {
4549
*/
4650
anchorPosition?: 'top' | 'bottom';
4751
/**
48-
* Extra margin to add at the top of the menu to account for translucent status bar on Android.
49-
* If you are using Expo, we assume translucent status bar and set a height for status bar automatically.
50-
* Pass `0` or a custom value to and customize it.
52+
* Extra margin to add at the top of the menu to handle specific cases with the status bar on Android.
5153
* This is automatically handled on iOS.
54+
* Pass `0` or a custom value to customize it.
5255
*/
5356
statusBarHeight?: number;
5457
/**
@@ -170,12 +173,11 @@ const DEFAULT_MODE = 'elevated';
170173
* `Modal` contents within a `PaperProvider` in order for the menu to show. This
171174
* wrapping is not necessary if you use Paper's `Modal` instead.
172175
*/
173-
class Menu extends React.Component<Props, State> {
176+
class MenuComponent extends React.Component<Props, State> {
174177
// @component ./MenuItem.tsx
175178
static Item = MenuItem;
176179

177180
static defaultProps = {
178-
statusBarHeight: APPROX_STATUSBAR_HEIGHT,
179181
overlayAccessibilityLabel: 'Close menu',
180182
testID: 'menu',
181183
};
@@ -436,6 +438,7 @@ class Menu extends React.Component<Props, State> {
436438
overlayAccessibilityLabel,
437439
keyboardShouldPersistTaps,
438440
testID,
441+
insets,
439442
} = this.props;
440443

441444
const {
@@ -455,7 +458,7 @@ class Menu extends React.Component<Props, State> {
455458

456459
// I don't know why but on Android measure function is wrong by 24
457460
const additionalVerticalValue = Platform.select({
458-
android: statusBarHeight,
461+
android: statusBarHeight ?? insets.top,
459462
default: 0,
460463
});
461464

@@ -706,4 +709,13 @@ const styles = StyleSheet.create({
706709
},
707710
});
708711

709-
export default withInternalTheme(Menu);
712+
const MenuWithHOC = withInternalTheme(withSafeAreaInsets(MenuComponent));
713+
714+
const Menu = MenuWithHOC as typeof MenuWithHOC & {
715+
Item: typeof MenuItem;
716+
};
717+
718+
// we need to attach again MenuItem as it is lost after using withSafeAreaInsets
719+
Menu.Item = MenuItem;
720+
721+
export default Menu;

Diff for: src/components/__tests__/DataTable.test.tsx

+16-11
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import * as React from 'react';
22

33
import { render } from '@testing-library/react-native';
4+
import mockSafeAreaContext from 'react-native-safe-area-context/jest/mock';
45

56
import Checkbox from '../Checkbox';
67
import DataTable from '../DataTable/DataTable';
78

9+
jest.mock('react-native-safe-area-context', () => mockSafeAreaContext);
10+
811
describe('DataTable.Header', () => {
912
it('renders data table header', () => {
1013
const tree = render(
@@ -133,17 +136,19 @@ describe('DataTable.Pagination', () => {
133136

134137
it('renders data table pagination with options select', () => {
135138
const { getByLabelText, toJSON } = render(
136-
<DataTable.Pagination
137-
page={3}
138-
numberOfPages={15}
139-
onPageChange={() => {}}
140-
label="11-20 of 150"
141-
showFastPaginationControls
142-
numberOfItemsPerPageList={[2, 4, 6]}
143-
numberOfItemsPerPage={2}
144-
onItemsPerPageChange={() => {}}
145-
selectPageDropdownLabel={'Rows per page'}
146-
/>
139+
<mockSafeAreaContext.SafeAreaProvider>
140+
<DataTable.Pagination
141+
page={3}
142+
numberOfPages={15}
143+
onPageChange={() => {}}
144+
label="11-20 of 150"
145+
showFastPaginationControls
146+
numberOfItemsPerPageList={[2, 4, 6]}
147+
numberOfItemsPerPage={2}
148+
onItemsPerPageChange={() => {}}
149+
selectPageDropdownLabel={'Rows per page'}
150+
/>
151+
</mockSafeAreaContext.SafeAreaProvider>
147152
);
148153

149154
expect(getByLabelText('Options Select')).toBeTruthy();

Diff for: src/components/__tests__/Menu.test.tsx

+29-18
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,34 @@ import * as React from 'react';
22
import { Animated, StyleSheet, View } from 'react-native';
33

44
import { render, waitFor, screen } from '@testing-library/react-native';
5+
import mockSafeAreaContext from 'react-native-safe-area-context/jest/mock';
56

67
import { getTheme } from '../../core/theming';
78
import { MD3Elevation } from '../../types';
89
import Button from '../Button/Button';
910
import Menu, { ELEVATION_LEVELS_MAP } from '../Menu/Menu';
1011
import Portal from '../Portal/Portal';
1112

13+
jest.mock('react-native-safe-area-context', () => mockSafeAreaContext);
14+
1215
const styles = StyleSheet.create({
1316
contentStyle: {
1417
borderTopLeftRadius: 0,
1518
borderTopRightRadius: 0,
1619
},
1720
});
1821

22+
const TestContextProvider: React.FC<{ children: React.ReactNode }> = ({
23+
children,
24+
}) => (
25+
<mockSafeAreaContext.SafeAreaProvider>
26+
<Portal.Host>{children}</Portal.Host>
27+
</mockSafeAreaContext.SafeAreaProvider>
28+
);
29+
1930
it('renders visible menu', () => {
2031
const tree = render(
21-
<Portal.Host>
32+
<TestContextProvider>
2233
<Menu
2334
visible
2435
onDismiss={jest.fn()}
@@ -27,15 +38,15 @@ it('renders visible menu', () => {
2738
<Menu.Item onPress={jest.fn()} title="Undo" />
2839
<Menu.Item onPress={jest.fn()} title="Redo" />
2940
</Menu>
30-
</Portal.Host>
41+
</TestContextProvider>
3142
).toJSON();
3243

3344
expect(tree).toMatchSnapshot();
3445
});
3546

3647
it('renders not visible menu', () => {
3748
const tree = render(
38-
<Portal.Host>
49+
<TestContextProvider>
3950
<Menu
4051
visible={false}
4152
onDismiss={jest.fn()}
@@ -44,15 +55,15 @@ it('renders not visible menu', () => {
4455
<Menu.Item onPress={jest.fn()} title="Undo" />
4556
<Menu.Item onPress={jest.fn()} title="Redo" />
4657
</Menu>
47-
</Portal.Host>
58+
</TestContextProvider>
4859
).toJSON();
4960

5061
expect(tree).toMatchSnapshot();
5162
});
5263

5364
it('renders menu with content styles', () => {
5465
const tree = render(
55-
<Portal.Host>
66+
<TestContextProvider>
5667
<Menu
5768
visible
5869
onDismiss={jest.fn()}
@@ -62,7 +73,7 @@ it('renders menu with content styles', () => {
6273
<Menu.Item onPress={jest.fn()} title="Undo" />
6374
<Menu.Item onPress={jest.fn()} title="Redo" />
6475
</Menu>
65-
</Portal.Host>
76+
</TestContextProvider>
6677
).toJSON();
6778

6879
expect(tree).toMatchSnapshot();
@@ -73,7 +84,7 @@ it('renders menu with content styles', () => {
7384
const theme = getTheme(false, true);
7485

7586
const { getByTestId } = render(
76-
<Portal.Host>
87+
<TestContextProvider>
7788
<Menu
7889
visible
7990
onDismiss={jest.fn()}
@@ -83,7 +94,7 @@ it('renders menu with content styles', () => {
8394
<Menu.Item onPress={jest.fn()} title="Undo" />
8495
<Menu.Item onPress={jest.fn()} title="Redo" />
8596
</Menu>
86-
</Portal.Host>
97+
</TestContextProvider>
8798
);
8899

89100
expect(getByTestId('menu-surface')).toHaveStyle({
@@ -95,7 +106,7 @@ it('renders menu with content styles', () => {
95106
it('uses the default anchorPosition of top', async () => {
96107
function makeMenu(visible: boolean) {
97108
return (
98-
<Portal.Host>
109+
<TestContextProvider>
99110
<Menu
100111
visible={visible}
101112
onDismiss={jest.fn()}
@@ -109,7 +120,7 @@ it('uses the default anchorPosition of top', async () => {
109120
<Menu.Item onPress={jest.fn()} title="Undo" />
110121
<Menu.Item onPress={jest.fn()} title="Redo" />
111122
</Menu>
112-
</Portal.Host>
123+
</TestContextProvider>
113124
);
114125
}
115126

@@ -138,7 +149,7 @@ it('uses the default anchorPosition of top', async () => {
138149
it('respects anchorPosition bottom', async () => {
139150
function makeMenu(visible: boolean) {
140151
return (
141-
<Portal.Host>
152+
<TestContextProvider>
142153
<Menu
143154
visible={visible}
144155
onDismiss={jest.fn()}
@@ -153,7 +164,7 @@ it('respects anchorPosition bottom', async () => {
153164
<Menu.Item onPress={jest.fn()} title="Undo" />
154165
<Menu.Item onPress={jest.fn()} title="Redo" />
155166
</Menu>
156-
</Portal.Host>
167+
</TestContextProvider>
157168
);
158169
}
159170

@@ -178,7 +189,7 @@ it('respects anchorPosition bottom', async () => {
178189
it('animated value changes correctly', () => {
179190
const value = new Animated.Value(1);
180191
const { getByTestId } = render(
181-
<Portal.Host>
192+
<TestContextProvider>
182193
<Menu
183194
visible
184195
onDismiss={jest.fn()}
@@ -188,7 +199,7 @@ it('animated value changes correctly', () => {
188199
>
189200
<Menu.Item onPress={jest.fn()} title="Test" />
190201
</Menu>
191-
</Portal.Host>
202+
</TestContextProvider>
192203
);
193204
expect(getByTestId('menu-surface-outer-layer')).toHaveStyle({
194205
transform: [{ scale: 1 }],
@@ -209,7 +220,7 @@ it('animated value changes correctly', () => {
209220

210221
it('renders menu with mode "elevated"', () => {
211222
const { getByTestId } = render(
212-
<Portal.Host>
223+
<TestContextProvider>
213224
<Menu
214225
visible
215226
onDismiss={jest.fn()}
@@ -219,7 +230,7 @@ it('renders menu with mode "elevated"', () => {
219230
<Menu.Item onPress={jest.fn()} title="Undo" />
220231
<Menu.Item onPress={jest.fn()} title="Redo" />
221232
</Menu>
222-
</Portal.Host>
233+
</TestContextProvider>
223234
);
224235

225236
const menuSurface = getByTestId('menu-surface');
@@ -233,7 +244,7 @@ it('renders menu with mode "elevated"', () => {
233244

234245
it('renders menu with mode "flat"', () => {
235246
const { getByTestId } = render(
236-
<Portal.Host>
247+
<TestContextProvider>
237248
<Menu
238249
visible
239250
onDismiss={jest.fn()}
@@ -243,7 +254,7 @@ it('renders menu with mode "flat"', () => {
243254
<Menu.Item onPress={jest.fn()} title="Undo" />
244255
<Menu.Item onPress={jest.fn()} title="Redo" />
245256
</Menu>
246-
</Portal.Host>
257+
</TestContextProvider>
247258
);
248259

249260
const menuSurface = getByTestId('menu-surface');

0 commit comments

Comments
 (0)