Skip to content

Commit 181b365

Browse files
authored
feat: allow overflow button customization (vonovak#116)
1 parent 5b3e24f commit 181b365

19 files changed

+107
-91
lines changed

.flowconfig

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ emoji=true
3232
esproposal.optional_chaining=enable
3333
esproposal.nullish_coalescing=enable
3434

35+
exact_by_default=true
36+
3537
module.file_ext=.js
3638
module.file_ext=.json
3739
module.file_ext=.ios.js
@@ -46,9 +48,6 @@ suppress_type=$FlowFixMe
4648
suppress_type=$FlowFixMeProps
4749
suppress_type=$FlowFixMeState
4850

49-
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)
50-
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)?:? #[0-9]+
51-
suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError
5251

5352
[lints]
5453
sketchy-null-number=warn
@@ -72,4 +71,4 @@ untyped-import
7271
untyped-type-import
7372

7473
[version]
75-
^0.122.0
74+
^0.137.0

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ The package exports common handlers you can use, but you can provide your own to
133133
| accessibilityLabel?: string | | 'More options' by default |
134134
| left?: boolean | whether the `OverflowMenu` is on the left from header title | false by default, it just influences styling. No need to pass this if you passed it to `HeaderButtons` |
135135
| children: React.Node | the overflow items | typically `HiddenItem`s, please read the note below |
136+
| other props | props passed to the nested Touchable | pass eg. `pressColor` to control ripple color on Android |
136137

137138
##### Important note
138139

@@ -207,7 +208,7 @@ On Android, `OverflowMenuProvider` accepts an optional `spaceAboveMenu` prop, wh
207208

208209
#### `HeaderButton`
209210

210-
You will typically not use `HeaderButton` directly. `HeaderButton` is where all the `onPress`, `title` and Icon-related props meet to render actual button.
211+
You will typically not use `HeaderButton` directly. `HeaderButton` is where all the `onPress`, `title` and Icon-related props (color, size) meet to render actual button.
211212
See the source if you want to customize it.
212213

213214
### Recipes

example/screens/UsageWithIcons.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,28 @@ const ReusableCapitalizedEditItem = ({ onPress }) => (
2121

2222
const ReusableItem = ({ onPress }) => <HiddenItem title="hidden2" onPress={onPress} />;
2323

24+
const rippleColorAndroidProps = {
25+
pressColor: 'red',
26+
};
27+
2428
export function UsageWithIcons({ navigation }) {
2529
React.useLayoutEffect(() => {
2630
navigation.setOptions({
2731
// in your app, extract the arrow function into a separate component
2832
// to avoid creating a new one every time
2933
headerRight: () => (
3034
<HeaderButtons HeaderButtonComponent={IoniconsHeaderButton}>
31-
<Item title="search" iconName="ios-search" onPress={() => alert('search')} />
35+
<Item
36+
title="search"
37+
iconName="ios-search"
38+
onPress={() => alert('search')}
39+
{...rippleColorAndroidProps}
40+
/>
3241
<ReusableCapitalizedEditItem onPress={() => alert('Edit')} />
3342
<OverflowMenu
3443
style={{ marginHorizontal: 10 }}
3544
OverflowIcon={<Ionicons name="ios-more" size={23} color="blue" />}
45+
{...rippleColorAndroidProps}
3646
>
3747
<HiddenItem title="hidden1" onPress={() => alert('hidden1')} />
3848
<ReusableItem onPress={() => alert('hidden2')} />

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
"@testing-library/react-native": "^7.0.1",
4343
"eslint": "^7.11.0",
4444
"eslint-plugin-prettier": "^3.1.2",
45-
"flow-bin": "0.122",
45+
"flow-bin": "^0.137.0",
4646
"jest": "^26.5.3",
4747
"metro-react-native-babel-preset": "^0.59.0",
4848
"prettier": "^2.1.2",

src/ButtonsWrapper.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ const ButtonsExtraMarginContext = React.createContext<
1212
'toBeHandledByOverflowMenu' | 'alreadyHandledByHeaderButtons'
1313
>('toBeHandledByOverflowMenu');
1414

15-
type Props = {|
15+
type Props = {
1616
left: boolean,
1717
children: React.Node,
18-
|};
18+
};
1919

20-
export const ButtonsWrapper = ({ left, children }: Props) => {
20+
export const ButtonsWrapper = ({ left, children }: Props): any => {
2121
const marginStatus = React.useContext(ButtonsExtraMarginContext);
2222
const valueOfLeft = marginStatus === 'alreadyHandledByHeaderButtons' ? null : left;
2323
const extraSideMargin = getMargin(valueOfLeft);

src/HeaderButton.js

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,35 +9,33 @@ import type { ViewStyleProp } from 'react-native/Libraries/StyleSheet/StyleSheet
99
const BUTTON_HIT_SLOP = Object.freeze({ top: 5, bottom: 5, left: 5, right: 5 });
1010

1111
// for renderVisibleButton() function
12-
export type VisibleButtonProps = {|
12+
export type VisibleButtonProps = {
1313
IconComponent?: React.ComponentType<any>,
1414
iconSize?: number,
1515
color?: string,
1616
iconName?: string,
1717
title: string,
1818
buttonStyle?: ViewStyleProp,
19-
|};
19+
};
2020

2121
// from <Item />
22-
export type ItemProps = {|
22+
export type ItemProps = {
2323
...$Exact<React.ElementConfig<typeof TouchableWithoutFeedback>>,
2424
...VisibleButtonProps,
2525
onPress: ?() => any,
2626
style?: ViewStyleProp,
27-
|};
27+
};
2828

29-
type OtherProps = {|
29+
type OtherProps = {
3030
background?: any,
3131
foreground?: any,
3232
renderButtonElement: (VisibleButtonProps) => React.Element<any>,
33-
|};
33+
...
34+
};
3435

35-
export type HeaderButtonProps = {|
36-
...ItemProps,
37-
...OtherProps,
38-
|};
36+
export type HeaderButtonProps = ItemProps & OtherProps;
3937

40-
export function HeaderButton(props: HeaderButtonProps) {
38+
export function HeaderButton(props: HeaderButtonProps): React.Node {
4139
const {
4240
onPress,
4341
style,

src/HeaderButtons.js

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,20 @@ import { HeaderButton } from './HeaderButton';
66
import { HeaderButtonsContext } from './HeaderButtonsContext';
77
import { ButtonsWrapper } from './ButtonsWrapper';
88

9-
type HeaderButtonsProps = {|
9+
type HeaderButtonsProps = {
1010
children: React.Node,
11-
left: boolean,
12-
HeaderButtonComponent: React.ComponentType<any>,
13-
|};
11+
left?: boolean,
12+
HeaderButtonComponent?: React.ComponentType<any>,
13+
};
1414

15-
export function HeaderButtons({ HeaderButtonComponent, children, left }: HeaderButtonsProps) {
15+
export const HeaderButtons = ({
16+
children,
17+
HeaderButtonComponent = HeaderButton,
18+
left = false,
19+
}: HeaderButtonsProps): React.Element<typeof HeaderButtons> => {
1620
return (
1721
<HeaderButtonsContext.Provider value={HeaderButtonComponent}>
1822
<ButtonsWrapper left={left}>{children}</ButtonsWrapper>
1923
</HeaderButtonsContext.Provider>
2024
);
21-
}
22-
23-
HeaderButtons.defaultProps = {
24-
left: false,
25-
HeaderButtonComponent: HeaderButton,
2625
};

src/HeaderButtonsContext.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@
22
import * as React from 'react';
33
import { HeaderButton } from './HeaderButton';
44

5-
export const HeaderButtonsContext = React.createContext<React.ComponentType<any>>(HeaderButton);
5+
export const HeaderButtonsContext: any = React.createContext<React.ComponentType<any>>(
6+
HeaderButton
7+
);

src/HeaderItems.js

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@ import { Text, StyleSheet, Platform } from 'react-native';
77
import { OverflowMenuContext } from './overflowMenu/OverflowMenuContext';
88
import { MenuItem } from './overflowMenu/vendor/MenuItem';
99

10-
type HiddenItemProps = {|
10+
type HiddenItemProps = {
1111
...MenuItemProps,
1212
destructive?: boolean,
13-
|};
13+
};
1414

15-
export function HiddenItem({ destructive, onPress, ...otherProps }: HiddenItemProps) {
15+
export function HiddenItem({
16+
onPress,
17+
...otherProps
18+
}: HiddenItemProps): React.Element<typeof HiddenItem> {
1619
const toggleMenu = React.useContext(OverflowMenuContext);
1720

1821
// when rendering dropdown menu (e.g. android default) the return value is actually rendered
@@ -23,14 +26,15 @@ export function HiddenItem({ destructive, onPress, ...otherProps }: HiddenItemPr
2326
onPress && onPress();
2427
};
2528

29+
// $FlowFixMeProps
2630
return <MenuItem {...otherProps} onPress={onMenuItemPress} />;
2731
}
2832

2933
// TODO check RTL
30-
export function Item(props: ItemProps) {
34+
export function Item(props: ItemProps): React.Element<typeof Item> {
3135
const HeaderButtonComponent = React.useContext(HeaderButtonsContext);
32-
// HeaderButtonComponent knows iconSize, icon color and etc.
33-
// Item itself will likely only have title and onPress
36+
// HeaderButtonComponent already knows iconSize, icon color and etc.
37+
// Item itself will likely only have title and onPress but can override iconSize, icon color and etc. if needed
3438
return <HeaderButtonComponent {...props} renderButtonElement={renderVisibleButton} />;
3539
}
3640

src/TouchableItem.ios.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ import { BaseButton } from 'react-native-gesture-handler';
44

55
const AnimatedBaseButton = Animated.createAnimatedComponent(BaseButton);
66

7-
type Props = {|
7+
export type Props = {
88
...ViewProps,
99
disabled?: boolean,
1010
delayPressIn?: number,
1111
onPress?: () => void,
1212
children: React.Node,
13-
1413
activeOpacity: number,
15-
|};
14+
...
15+
};
1616

1717
const useNativeDriver = Platform.OS !== 'web';
1818

src/TouchableItem.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@ import {
77
type ViewProps,
88
} from 'react-native';
99

10-
type Props = {|
10+
type Props = {
1111
...ViewProps,
1212
pressColor?: string,
1313
disabled?: boolean,
1414
borderless?: boolean,
1515
delayPressIn?: number,
1616
onPress?: () => void,
1717
children: React.Node,
18-
|};
18+
};
1919

2020
const ANDROID_VERSION_LOLLIPOP = 21;
2121

src/__tests__/overflowMenuPressHandlers.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ describe('overflowMenuPressHandlers', () => {
148148
{ title: 'three', onPress: jest.fn(), destructive: true },
149149
];
150150
beforeEach(() => {
151+
// $FlowExpectedError
151152
ActionSheetIOS.showActionSheetWithOptions = jest.fn();
152153
// $FlowExpectedError
153154
UIManager.showPopupMenu = jest.fn();

src/overflowMenu/OverflowMenu.js

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,27 @@ import type { ViewStyleProp } from 'react-native/Libraries/StyleSheet/StyleSheet
1313
import { OVERFLOW_BUTTON_TEST_ID } from '../e2e';
1414
import { ButtonsWrapper } from '../ButtonsWrapper';
1515

16-
export type OverflowMenuProps = {|
16+
export type OverflowMenuProps = {
1717
children: React.Node,
1818
OverflowIcon: React.Element<any>,
1919
style?: ViewStyleProp,
20-
testID: string,
21-
accessibilityLabel: string,
20+
testID?: string,
21+
accessibilityLabel?: string,
2222
onPress: (OnOverflowMenuPressParams) => any,
23-
left: boolean,
24-
|};
23+
left?: boolean,
24+
...
25+
};
2526

2627
export const OverflowMenu = ({
2728
children,
28-
OverflowIcon,
29-
accessibilityLabel,
30-
testID,
3129
style,
32-
onPress,
33-
left,
34-
}: OverflowMenuProps) => {
30+
OverflowIcon = <View />,
31+
accessibilityLabel = 'More options',
32+
testID = OVERFLOW_BUTTON_TEST_ID,
33+
onPress = defaultOnOverflowMenuPress,
34+
left = false, // this is needed only when OverflowMenu is rendered without HeaderButtons,
35+
...other
36+
}: OverflowMenuProps): React.Node => {
3537
const toggleMenu = React.useContext(OverflowMenuContext);
3638
const btnRef = React.useRef<typeof View | null>(null);
3739
const renderButtonElement = React.useCallback(() => OverflowIcon, [OverflowIcon]);
@@ -55,26 +57,20 @@ export const OverflowMenu = ({
5557
return (
5658
<ButtonsWrapper left={left}>
5759
<View ref={btnRef} collapsable={false} style={styles.overflowMenuView} />
60+
{/* $FlowFixMeProps yeaah, this is boring :/ */}
5861
<HeaderButton
5962
title="overflow menu"
6063
renderButtonElement={renderButtonElement}
6164
style={[styles.icon, style]}
6265
onPress={usedOnPress}
6366
accessibilityLabel={accessibilityLabel}
6467
testID={testID}
68+
{...other}
6569
/>
6670
</ButtonsWrapper>
6771
);
6872
};
6973

70-
OverflowMenu.defaultProps = {
71-
accessibilityLabel: 'More options',
72-
OverflowIcon: <View />,
73-
onPress: defaultOnOverflowMenuPress,
74-
testID: OVERFLOW_BUTTON_TEST_ID,
75-
left: false, // this is needed only when OverflowMenu is rendered without HeaderButtons
76-
};
77-
7874
const styles = StyleSheet.create({
7975
icon: {
8076
...Platform.select({

src/overflowMenu/OverflowMenuContext.js

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import { Dimensions, Platform } from 'react-native';
44
import { getDefaultSpaceAboveMenu } from './statusBarUtils';
55
import { Menu } from './vendor/Menu';
66

7-
export type ToggleMenuParam = ?{|
7+
export type ToggleMenuParam = ?{
88
elements: React.ChildrenArray<any>,
99
x: number,
1010
y: number,
11-
|};
11+
};
1212

1313
export const OVERFLOW_TOP = 15;
1414

@@ -20,14 +20,17 @@ const warn = () => {
2020
'Please check the installation instructions in the react-navigation-header-buttons readme!'
2121
);
2222
};
23-
export const OverflowMenuContext = React.createContext<(ToggleMenuParam) => void>(warn);
23+
export const OverflowMenuContext: any = React.createContext<(ToggleMenuParam) => void>(warn);
2424

25-
type Props = {|
25+
type Props = {
2626
children: React.Element<any>,
2727
spaceAboveMenu?: number,
28-
|};
28+
};
2929

30-
export const OverflowMenuProvider = ({ children, spaceAboveMenu }: Props) => {
30+
export const OverflowMenuProvider = ({
31+
children,
32+
spaceAboveMenu,
33+
}: Props): React.Element<typeof OverflowMenuProvider> => {
3134
const [visible, setVisible] = React.useState(false);
3235
const [position, setPosition] = React.useState({ x: Dimensions.get('window').width - 10, y: 40 });
3336
const [elements, setElements] = React.useState(null);

src/overflowMenu/vendor/Divider.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ import * as React from 'react';
33
import { StyleSheet, View } from 'react-native';
44
import type { ViewProps } from 'react-native/Libraries/Components/View/ViewPropTypes';
55

6-
type Props = {|
6+
type Props = {
77
...ViewProps,
88
inset?: boolean,
9-
|};
9+
};
1010

1111
/**
1212
* A divider is a thin, lightweight separator that groups content in lists and page layouts.
@@ -28,7 +28,7 @@ type Props = {|
2828
*
2929
* ```
3030
*/
31-
export function Divider(props: Props) {
31+
export function Divider(props: Props): React.Element<typeof View> {
3232
const { inset, style, ...rest } = props;
3333
return <View {...rest} style={[styles.light, inset && styles.inset, style]} />;
3434
}

0 commit comments

Comments
 (0)