Merge pull request #59 from instea/menuoptions-improvement
Menuoptions improvement
This commit is contained in:
commit
56b773cfd4
|
@ -50,18 +50,22 @@ describe('Menu', () => {
|
|||
);
|
||||
expect(output.type).toEqual(View);
|
||||
expect(output.props.children.length).toEqual(3);
|
||||
expect(output.props.children[0]).toEqual(
|
||||
<Text>Some text</Text>
|
||||
);
|
||||
// React.Children.toArray modifies components keys
|
||||
// using the same function to create expected children
|
||||
const expectedChildren = React.Children.toArray([
|
||||
<Text>Some text</Text>,
|
||||
<MenuTrigger />, // trigger will be modified
|
||||
<MenuOptions />, // options will be removed
|
||||
<Text>Some other text</Text>,
|
||||
]);
|
||||
expect(output.props.children[0]).toEqual(expectedChildren[0]);
|
||||
expect(output.props.children[1]).toEqual(objectContaining({
|
||||
type: MenuTrigger,
|
||||
props: objectContaining({
|
||||
onRef: any(Function)
|
||||
})
|
||||
}));
|
||||
expect(output.props.children[2]).toEqual(
|
||||
<Text>Some other text</Text>
|
||||
);
|
||||
expect(output.props.children[2]).toEqual(expectedChildren[3]);
|
||||
});
|
||||
|
||||
it('should subscribe menu and notify context', () => {
|
||||
|
@ -87,12 +91,16 @@ describe('Menu', () => {
|
|||
expect(ctx.menuRegistry.subscribe).not.toHaveBeenCalled();
|
||||
const output = renderer.getRenderOutput();
|
||||
expect(output.type).toEqual(View);
|
||||
expect(output.props.children).toEqual([
|
||||
const expectedChildren = React.Children.toArray([
|
||||
<MenuTrigger />,
|
||||
<Text>Some text</Text>,
|
||||
]);
|
||||
expect(output.props.children[0]).toEqual(
|
||||
objectContaining({
|
||||
type: MenuTrigger
|
||||
}),
|
||||
<Text>Some text</Text>
|
||||
]);
|
||||
})
|
||||
);
|
||||
expect(output.props.children[1]).toEqual(expectedChildren[1]);
|
||||
});
|
||||
|
||||
it('should not subscribe menu because of missing trigger', () => {
|
||||
|
@ -106,9 +114,9 @@ describe('Menu', () => {
|
|||
expect(ctx.menuRegistry.subscribe).not.toHaveBeenCalled();
|
||||
const output = renderer.getRenderOutput();
|
||||
expect(output.type).toEqual(View);
|
||||
expect(output.props.children).toEqual([
|
||||
expect(output.props.children).toEqual(React.Children.toArray(
|
||||
<Text>Some text</Text>
|
||||
]);
|
||||
));
|
||||
});
|
||||
|
||||
it('should not fail without any children', () => {
|
||||
|
@ -158,7 +166,7 @@ describe('Menu', () => {
|
|||
expect(ctx.menuActions._notify).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should forward on select handler to menu options', () => {
|
||||
it('should get menu options', () => {
|
||||
const onSelect = () => 0;
|
||||
const { instance } = renderMenu(
|
||||
<Menu onSelect={ onSelect }>
|
||||
|
@ -168,7 +176,6 @@ describe('Menu', () => {
|
|||
);
|
||||
const options = instance._getOptions();
|
||||
expect(options.type).toEqual(MenuOptions);
|
||||
expect(options.props.onSelect).toEqual(onSelect);
|
||||
});
|
||||
|
||||
it('declarative opened takes precedence over imperative', () => {
|
||||
|
|
|
@ -9,11 +9,22 @@ const { createSpy, objectContaining } = jasmine;
|
|||
|
||||
describe('MenuOption', () => {
|
||||
|
||||
const makeMockContext = ({ optionsCustomStyles, onSelect, closeMenu } = {}) => ({
|
||||
menuActions: {
|
||||
_getOpenedMenu: () => ({
|
||||
optionsCustomStyles: optionsCustomStyles || {},
|
||||
instance: { props: { onSelect: onSelect } }
|
||||
}),
|
||||
closeMenu: closeMenu || createSpy(),
|
||||
},
|
||||
});
|
||||
|
||||
it('should render component', () => {
|
||||
const { output } = render(
|
||||
<MenuOption>
|
||||
<Text>Option 1</Text>
|
||||
</MenuOption>
|
||||
</MenuOption>,
|
||||
makeMockContext()
|
||||
);
|
||||
expect(output.type).toEqual(TouchableHighlight);
|
||||
expect(nthChild(output, 1).type).toEqual(View);
|
||||
|
@ -24,18 +35,30 @@ describe('MenuOption', () => {
|
|||
|
||||
it('should be enabled by default', () => {
|
||||
const { instance } = render(
|
||||
<MenuOption />
|
||||
<MenuOption />,
|
||||
makeMockContext()
|
||||
);
|
||||
expect(instance.props.disabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should trigger on select event with value', () => {
|
||||
const spy = createSpy();
|
||||
const { instance, renderer } = render(
|
||||
<MenuOption onSelect={spy} value='hello' />
|
||||
const { renderer } = render(
|
||||
<MenuOption onSelect={spy} value='hello' />,
|
||||
makeMockContext()
|
||||
);
|
||||
const touchable = renderer.getRenderOutput();
|
||||
touchable.props.onPress();
|
||||
expect(spy).toHaveBeenCalledWith('hello');
|
||||
expect(spy.calls.count()).toEqual(1);
|
||||
});
|
||||
|
||||
it('should trigger onSelect event from Menu', () => {
|
||||
const spy = createSpy();
|
||||
const { renderer } = render(
|
||||
<MenuOption value='hello' />,
|
||||
makeMockContext({ onSelect: spy })
|
||||
);
|
||||
const menuActions = { closeMenu: createSpy() };
|
||||
instance.context = { menuActions };
|
||||
const touchable = renderer.getRenderOutput();
|
||||
touchable.props.onPress();
|
||||
expect(spy).toHaveBeenCalledWith('hello');
|
||||
|
@ -44,34 +67,35 @@ describe('MenuOption', () => {
|
|||
|
||||
it('should close menu on select', () => {
|
||||
const spy = createSpy();
|
||||
const { instance, renderer } = render(
|
||||
<MenuOption onSelect={spy} value='hello' />
|
||||
const closeMenu = createSpy();
|
||||
const { renderer } = render(
|
||||
<MenuOption onSelect={spy} value='hello' />,
|
||||
makeMockContext({ closeMenu })
|
||||
);
|
||||
const menuActions = { closeMenu: createSpy() };
|
||||
instance.context = { menuActions };
|
||||
const touchable = renderer.getRenderOutput();
|
||||
touchable.props.onPress();
|
||||
expect(spy).toHaveBeenCalled();
|
||||
expect(menuActions.closeMenu).toHaveBeenCalled();
|
||||
expect(closeMenu).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not close menu on select', () => {
|
||||
const spy = createSpy().and.returnValue(false);
|
||||
const { instance, renderer } = render(
|
||||
<MenuOption onSelect={spy} value='hello' />
|
||||
const closeMenu = createSpy()
|
||||
const { renderer } = render(
|
||||
<MenuOption onSelect={spy} value='hello' />,
|
||||
makeMockContext({ closeMenu })
|
||||
);
|
||||
const menuActions = { closeMenu: createSpy() };
|
||||
instance.context = { menuActions };
|
||||
const touchable = renderer.getRenderOutput();
|
||||
touchable.props.onPress();
|
||||
expect(spy).toHaveBeenCalled();
|
||||
expect(menuActions.closeMenu).not.toHaveBeenCalled();
|
||||
expect(closeMenu).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not trigger event when disabled', () => {
|
||||
const spy = createSpy();
|
||||
const { output } = render(
|
||||
<MenuOption onSelect={spy} disabled={true} />
|
||||
<MenuOption onSelect={spy} disabled={true} />,
|
||||
makeMockContext()
|
||||
);
|
||||
expect(output.type).toBe(View);
|
||||
expect(output.props.onPress).toBeUndefined();
|
||||
|
@ -79,7 +103,8 @@ describe('MenuOption', () => {
|
|||
|
||||
it('should render text passed in props', () => {
|
||||
const { output } = render(
|
||||
<MenuOption text='Hello world' />
|
||||
<MenuOption text='Hello world' />,
|
||||
makeMockContext()
|
||||
);
|
||||
expect(output.type).toEqual(TouchableHighlight);
|
||||
expect(output.props.children.type).toEqual(View);
|
||||
|
@ -96,7 +121,8 @@ describe('MenuOption', () => {
|
|||
optionTouchable: { underlayColor: 'green' },
|
||||
};
|
||||
const { output } = render(
|
||||
<MenuOption text='some text' customStyles={customStyles} />
|
||||
<MenuOption text='some text' customStyles={customStyles} />,
|
||||
makeMockContext()
|
||||
);
|
||||
const touchable = output;
|
||||
const view = nthChild(output, 1);
|
||||
|
@ -109,4 +135,28 @@ describe('MenuOption', () => {
|
|||
.toEqual(objectContaining(customStyles.optionText));
|
||||
});
|
||||
|
||||
it('should render component with inherited custom styles', () => {
|
||||
const optionsCustomStyles = {
|
||||
optionWrapper: { backgroundColor: 'pink' },
|
||||
optionText: { color: 'yellow' },
|
||||
};
|
||||
const customStyles = {
|
||||
optionText: { color: 'blue' },
|
||||
optionTouchable: { underlayColor: 'green' },
|
||||
};
|
||||
const { output } = render(
|
||||
<MenuOption text='some text' customStyles={customStyles} />,
|
||||
makeMockContext({ optionsCustomStyles })
|
||||
);
|
||||
const touchable = output;
|
||||
const view = nthChild(output, 1);
|
||||
const text = nthChild(output, 2);
|
||||
expect(normalizeStyle(touchable.props))
|
||||
.toEqual(objectContaining({ underlayColor: 'green' }));
|
||||
expect(normalizeStyle(view.props.style))
|
||||
.toEqual(objectContaining(optionsCustomStyles.optionWrapper));
|
||||
expect(normalizeStyle(text.props.style))
|
||||
.toEqual(objectContaining(customStyles.optionText));
|
||||
})
|
||||
|
||||
});
|
||||
|
|
|
@ -1,29 +1,40 @@
|
|||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { render, normalizeStyle } from './helpers';
|
||||
import { render } from './helpers';
|
||||
import { MenuOption } from '../src/index';
|
||||
|
||||
jest.dontMock('../src/MenuOptions');
|
||||
const MenuOptions = require('../src/MenuOptions').default;
|
||||
const { objectContaining } = jasmine;
|
||||
|
||||
describe('MenuOptions', () => {
|
||||
|
||||
function mockCtx() {
|
||||
return {
|
||||
menuActions: {
|
||||
_getOpenedMenu: () => ({
|
||||
instance: { getName: () => 'menu1' }
|
||||
}),
|
||||
},
|
||||
menuRegistry: {
|
||||
setOptionsCustomStyles: jest.fn(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
it('should render component', () => {
|
||||
const onSelect = () => 0;
|
||||
const { output } = render(
|
||||
<MenuOptions onSelect={onSelect}>
|
||||
<MenuOptions>
|
||||
<MenuOption />
|
||||
<MenuOption />
|
||||
<MenuOption />
|
||||
</MenuOptions>
|
||||
</MenuOptions>,
|
||||
mockCtx()
|
||||
);
|
||||
expect(output.type).toEqual(View);
|
||||
const children = output.props.children;
|
||||
expect(children.length).toEqual(3);
|
||||
children.forEach(ch => {
|
||||
expect(ch.type).toBe(MenuOption);
|
||||
expect(ch.props.onSelect).toEqual(onSelect);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -34,70 +45,45 @@ describe('MenuOptions', () => {
|
|||
<MenuOption />
|
||||
{option ? <MenuOption />: null}
|
||||
<MenuOption />
|
||||
</MenuOptions>
|
||||
</MenuOptions>,
|
||||
mockCtx()
|
||||
);
|
||||
expect(output.type).toEqual(View);
|
||||
const children = output.props.children;
|
||||
expect(children.length).toEqual(2);
|
||||
});
|
||||
|
||||
|
||||
it("should prioritize option's onSelect handler", () => {
|
||||
const onSelect = () => 0;
|
||||
const onSelectOption = () => 1;
|
||||
const { output } = render(
|
||||
<MenuOptions onSelect={onSelect}>
|
||||
<MenuOption onSelect={onSelectOption} />
|
||||
<MenuOption />
|
||||
</MenuOptions>
|
||||
);
|
||||
expect(output.type).toEqual(View);
|
||||
const children = output.props.children;
|
||||
expect(children.length).toEqual(2);
|
||||
expect(children[0].type).toBe(MenuOption);
|
||||
expect(children[1].type).toBe(MenuOption);
|
||||
expect(children[0].props.onSelect).toEqual(onSelectOption);
|
||||
expect(children[1].props.onSelect).toEqual(onSelect);
|
||||
expect(children.length).toEqual(3);
|
||||
});
|
||||
|
||||
it('should work with user defined options', () => {
|
||||
const UserOption = (props) => <MenuOption {...props} text='user-defined' />;
|
||||
const onSelect = () => 0;
|
||||
const { output } = render(
|
||||
<MenuOptions onSelect={onSelect}>
|
||||
<MenuOptions>
|
||||
<UserOption />
|
||||
</MenuOptions>
|
||||
</MenuOptions>,
|
||||
mockCtx()
|
||||
);
|
||||
expect(output.type).toEqual(View);
|
||||
const children = output.props.children;
|
||||
expect(children.length).toEqual(1);
|
||||
const ch = children[0];
|
||||
expect(ch.type).toBe(UserOption);
|
||||
expect(ch.props.onSelect).toEqual(onSelect);
|
||||
expect(children.type).toBe(UserOption);
|
||||
});
|
||||
|
||||
it('should render options with custom styles', () => {
|
||||
const onSelect = () => 0;
|
||||
it('should register custom styles', () => {
|
||||
const customStyles = {
|
||||
optionsWrapper: { backgroundColor: 'red' },
|
||||
optionText: { color: 'blue' },
|
||||
};
|
||||
const customOptionStyles = {
|
||||
optionText: { color: 'pink' },
|
||||
const customStyles2 = {
|
||||
optionsWrapper: { backgroundColor: 'blue' },
|
||||
};
|
||||
const { output } = render(
|
||||
<MenuOptions onSelect={onSelect} customStyles={customStyles}>
|
||||
<MenuOption />
|
||||
<MenuOption customStyles={customOptionStyles} />
|
||||
<MenuOption />
|
||||
</MenuOptions>
|
||||
const ctx = mockCtx();
|
||||
const { instance } = render(
|
||||
<MenuOptions customStyles={customStyles} />,
|
||||
ctx
|
||||
);
|
||||
expect(normalizeStyle(output.props.style))
|
||||
.toEqual(objectContaining(customStyles.optionsWrapper));
|
||||
const options = output.props.children;
|
||||
expect(options[0].props.customStyles).toEqual(customStyles);
|
||||
expect(options[1].props.customStyles).toEqual(customOptionStyles);
|
||||
expect(options[2].props.customStyles).toEqual(customStyles);
|
||||
expect(ctx.menuRegistry.setOptionsCustomStyles)
|
||||
.toHaveBeenLastCalledWith('menu1', customStyles)
|
||||
instance.componentWillReceiveProps({ customStyles: customStyles2 })
|
||||
expect(ctx.menuRegistry.setOptionsCustomStyles)
|
||||
.toHaveBeenLastCalledWith('menu1', customStyles2)
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -148,6 +148,8 @@ To style `<MenuOptions />` and it's `<MenuOption />` components you can pass `cu
|
|||
|
||||
**Note:** `optionWrapper`, `optionTouchable` and `optionText` styles of particular menu option can be overriden by `customStyles` prop of `<MenuOption />` component.
|
||||
|
||||
**Note:** In order to change `customStyles` dynamically, it is required that no child of `MenuOptions` stops the update (e.g. `shouldComponentUpdate` returning `false`).
|
||||
|
||||
**Note:** `Style` type is any valid RN style parameter.
|
||||
|
||||
See more in custom [styling example](../examples/StylingExample.js) and [touchable example](../examples/TouchableExample.js).
|
||||
|
|
|
@ -80,11 +80,24 @@ Another nice use case is to have menu options with icons.
|
|||
</MenuOptions>
|
||||
...
|
||||
const CheckedOption = (props) => (
|
||||
<MenuOption {...props} text={(props.checked ? '\u2713 ' : '') + props.text} />
|
||||
<MenuOption value={props.value} text={(props.checked ? '\u2713 ' : '') + props.text} />
|
||||
)
|
||||
```
|
||||
It is important to pass all (other) props to underlaying `MenuOption`.
|
||||
For more details see [extensions](extensions.md) documentation.
|
||||
|
||||
## Menu within scroll view
|
||||
If you want to display menu options in scroll view, simply wrap all menu options with `<ScrollView />` component. For example:
|
||||
|
||||
```js
|
||||
<MenuOptions>
|
||||
<ScrollView style={{ maxHeight: 200 }}>
|
||||
<MenuOption value={1} text='One' />
|
||||
<MenuOption value={2} text='Two' />
|
||||
...
|
||||
</ScrollView>
|
||||
</MenuOptions>
|
||||
```
|
||||
|
||||
You can also check our [FlatListExample](../examples/FlatListExample.js).
|
||||
|
||||
## Styled menu
|
||||
[StylingExample](../examples/StylingExample.js):
|
||||
|
|
|
@ -6,14 +6,32 @@
|
|||
Simplest example that adds checkmark symbol (unicode 2713).
|
||||
```
|
||||
const CheckedOption = (props) => (
|
||||
<MenuOption {...props} text={'\u2713 ' + props.text} />
|
||||
<MenuOption value={props.value} text={'\u2713 ' + props.text} />
|
||||
)
|
||||
```
|
||||
|
||||
**Note:** It is important that you pass all properties to underlying `MenuOption`. We internally pass `onSelect` handler to all menu options so that we can react to user actions. Although for now it might suffice to pass only `onSelect` in addition to other standard props, we highly recommend to pass any properties (as in example) in order to stay compatible with any further versions of the library.
|
||||
**Note:** `MenuOption` can be placed anywhere inside of `MenuOptions` container. For example it can be rendered using `FlatList`.
|
||||
|
||||
## MenuOptions - renderOptionsContainer
|
||||
You can control rendering of `<MenuOptions />` component by passing rendering function into `renderOptionsContainer` property. It takes `<MenuOptions />` component as argument and it have to return react component. For example if you want to wrap options with custom component and add some text above options:
|
||||
## MenuOptions
|
||||
`<MenuOption />` components are not required to be direct children of `<MenuOptions />`. You can pass any children to `<MenuOptions />` component. For example if you want to wrap options with custom component and add some text above options:
|
||||
|
||||
```
|
||||
const menu = (props) => (
|
||||
<Menu>
|
||||
<MenuTrigger />
|
||||
<MenuOptions>
|
||||
<SomeCustomContainer>
|
||||
<Text>Some text</Text>
|
||||
<MenuOption value={1} text="value 1" />
|
||||
<MenuOption value={2} text="value 2" />
|
||||
</SomeCustomContainer>
|
||||
</MenuOptions>
|
||||
</Menu>
|
||||
);
|
||||
```
|
||||
|
||||
#### Using `renderOptionsContainer` prop (DEPRECATED)
|
||||
You can also control rendering of `<MenuOptions />` component by passing rendering function into `renderOptionsContainer` property. It takes `<MenuOptions />` component as argument and it have to return react component.
|
||||
|
||||
```
|
||||
const optionsRenderer = (options) => (
|
||||
|
@ -25,10 +43,14 @@ const optionsRenderer = (options) => (
|
|||
const menu = (props) => (
|
||||
<Menu>
|
||||
<MenuTrigger />
|
||||
<MenuOptions renderOptionsContainer={optionsRenderer} />
|
||||
<MenuOptions renderOptionsContainer={optionsRenderer}>
|
||||
<MenuOption value={1} text="value 1" />
|
||||
<MenuOption value={2} text="value 2" />
|
||||
</MenuOptions>
|
||||
</Menu>
|
||||
);
|
||||
```
|
||||
**Note:** It is highly recommended to use first approach to extend menu options. `renderOptionsContainer` property might be removed in the future versions of the library.
|
||||
|
||||
## Custom renderer
|
||||
It is possible to use different renderer to display menu. There are already few predefined renderers: e.g. `ContextMenu` and `SlideInMenu` (from the `renderers` module). To use it you need to pass it to the `<Menu />` props or use `setDefaultRenderer` (see [API](api.md#static-functions)):
|
||||
|
|
|
@ -14,6 +14,7 @@ import NavigatorExample from './NavigatorExample';
|
|||
import TouchableExample from './TouchableExample';
|
||||
import MenuMethodsExample from './MenuMethodsExample';
|
||||
import CloseOnBackExample from './CloseOnBackExample';
|
||||
import FlatListExample from './FlatListExample';
|
||||
|
||||
const demos = [
|
||||
{ Component: BasicExample, name: 'Basic example' },
|
||||
|
@ -28,6 +29,7 @@ const demos = [
|
|||
{ Component: NonRootExample, name: 'Non root example' },
|
||||
{ Component: NavigatorExample, name: 'Example with react-native-router-flux' },
|
||||
{ Component: CloseOnBackExample, name: 'Close on back button press example' },
|
||||
{ Component: FlatListExample, name: 'Using FlatList' },
|
||||
];
|
||||
|
||||
// show debug messages for demos.
|
||||
|
|
|
@ -10,11 +10,14 @@ import Menu, {
|
|||
import Icon from 'react-native-vector-icons/FontAwesome';
|
||||
|
||||
const CheckedOption = (props) => (
|
||||
<MenuOption {...props} text={(props.checked ? '\u2713 ' : '') + props.text} />
|
||||
<MenuOption
|
||||
value={props.value}
|
||||
text={(props.checked ? '\u2713 ' : '') + props.text}
|
||||
/>
|
||||
)
|
||||
|
||||
const IconOption = ({iconName, text, ...others}) => (
|
||||
<MenuOption {...others} >
|
||||
const IconOption = ({iconName, text, value}) => (
|
||||
<MenuOption value={value}>
|
||||
<Text>
|
||||
<Icon name={iconName} />
|
||||
{' ' + text}
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
import React, { Component } from 'react';
|
||||
import { FlatList, Alert, StyleSheet } from 'react-native';
|
||||
import {
|
||||
MenuContext,
|
||||
Menu,
|
||||
MenuTrigger,
|
||||
MenuOptions,
|
||||
MenuOption,
|
||||
} from 'react-native-popup-menu';
|
||||
|
||||
Menu.debug = true;
|
||||
|
||||
const data = new Array(500)
|
||||
.fill(0)
|
||||
.map((a, i) => ({ key: i, value: 'item' + i }));
|
||||
|
||||
export default class App extends Component {
|
||||
render() {
|
||||
return (
|
||||
<MenuContext style={styles.container}>
|
||||
<Menu onSelect={value => Alert.alert(value)}>
|
||||
<MenuTrigger text="Select option" />
|
||||
<MenuOptions>
|
||||
<FlatList
|
||||
data={data}
|
||||
renderItem={({ item }) => (
|
||||
<MenuOption value={item.value} text={item.value} />
|
||||
)}
|
||||
style={{ height: 200 }}
|
||||
/>
|
||||
</MenuOptions>
|
||||
</Menu>
|
||||
</MenuContext>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingTop: 20,
|
||||
},
|
||||
});
|
21
src/Menu.js
21
src/Menu.js
|
@ -10,13 +10,6 @@ const isRegularComponent = c => c.type !== MenuOptions && c.type !== MenuTrigger
|
|||
const isTrigger = c => c.type === MenuTrigger;
|
||||
const isMenuOptions = c => c.type === MenuOptions;
|
||||
|
||||
const childrenToArray = children => {
|
||||
if (children) {
|
||||
return Array.isArray(children) ? children : [ children ];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
export default class Menu extends Component {
|
||||
|
||||
constructor(props, ctx) {
|
||||
|
@ -53,6 +46,12 @@ export default class Menu extends Component {
|
|||
this.context.menuRegistry.unsubscribe(this);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.name !== nextProps.name) {
|
||||
console.warn('Menu name cannot be changed');
|
||||
}
|
||||
}
|
||||
|
||||
open() {
|
||||
this.context.menuActions.openMenu(this._name);
|
||||
}
|
||||
|
@ -76,7 +75,7 @@ export default class Menu extends Component {
|
|||
}
|
||||
|
||||
_reduceChildren() {
|
||||
return childrenToArray(this.props.children).reduce((r, child) => {
|
||||
return React.Children.toArray(this.props.children).reduce((r, child) => {
|
||||
if (isTrigger(child)) {
|
||||
r.push(React.cloneElement(child, {
|
||||
key: null,
|
||||
|
@ -103,9 +102,7 @@ export default class Menu extends Component {
|
|||
}
|
||||
|
||||
_getOptions() {
|
||||
const { children, onSelect } = this.props;
|
||||
const optionsElem = childrenToArray(children).find(isMenuOptions);
|
||||
return React.cloneElement(optionsElem, { onSelect });
|
||||
return React.Children.toArray(this.props.children).find(isMenuOptions);
|
||||
}
|
||||
|
||||
_getOpened() {
|
||||
|
@ -117,7 +114,7 @@ export default class Menu extends Component {
|
|||
}
|
||||
|
||||
_validateChildren() {
|
||||
const children = childrenToArray(this.props.children);
|
||||
const children = React.Children.toArray(this.props.children);
|
||||
const options = children.find(isMenuOptions);
|
||||
if (!options) {
|
||||
console.warn('Menu has to contain MenuOptions component');
|
||||
|
|
|
@ -27,7 +27,8 @@ export default class MenuContext extends Component {
|
|||
closeMenu: () => this.closeMenu(),
|
||||
toggleMenu: name => this.toggleMenu(name),
|
||||
isMenuOpen: () => this.isMenuOpen(),
|
||||
_notify: force => this._notify(force)
|
||||
_getOpenedMenu: () => this._getOpenedMenu(),
|
||||
_notify: force => this._notify(force),
|
||||
};
|
||||
const menuRegistry = this._menuRegistry;
|
||||
return { menuRegistry, menuActions };
|
||||
|
@ -155,7 +156,7 @@ export default class MenuContext extends Component {
|
|||
debug('setState ignored - maybe the context was unmounted')
|
||||
return
|
||||
}
|
||||
this._placeholderRef.setState({ openedMenu: this.openedMenu }, afterSetState);
|
||||
this._placeholderRef.setState({ openedMenuName: this.openedMenu && this.openedMenu.name }, afterSetState);
|
||||
debug('notify ended');
|
||||
});
|
||||
}
|
||||
|
@ -207,7 +208,8 @@ export default class MenuContext extends Component {
|
|||
_onPlaceholderRef = r => this._placeholderRef = r;
|
||||
|
||||
_getOpenedMenu() {
|
||||
return this._placeholderRef && this._placeholderRef.state.openedMenu
|
||||
const name = this._placeholderRef && this._placeholderRef.state.openedMenuName;
|
||||
return name ? this._menuRegistry.getMenu(name) : undefined;
|
||||
}
|
||||
|
||||
_onBackdropPress = () => {
|
||||
|
|
|
@ -7,7 +7,8 @@ import { makeTouchable } from './helpers';
|
|||
export default class MenuOption extends Component {
|
||||
|
||||
_onSelect() {
|
||||
const { value, onSelect } = this.props;
|
||||
const { value } = this.props;
|
||||
const onSelect = this.props.onSelect || this._getMenusOnSelect()
|
||||
const shouldClose = onSelect(value) !== false;
|
||||
debug('select option', value, shouldClose);
|
||||
if (shouldClose) {
|
||||
|
@ -15,8 +16,22 @@ export default class MenuOption extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
_getMenusOnSelect() {
|
||||
const menu = this.context.menuActions._getOpenedMenu();
|
||||
return menu.instance.props.onSelect;
|
||||
}
|
||||
|
||||
_getCustomStyles() {
|
||||
const { optionsCustomStyles } = this.context.menuActions._getOpenedMenu();
|
||||
return {
|
||||
...optionsCustomStyles,
|
||||
...this.props.customStyles,
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { text, disabled, disableTouchable, children, style, customStyles } = this.props;
|
||||
const { text, disabled, disableTouchable, children, style } = this.props;
|
||||
const customStyles = this._getCustomStyles()
|
||||
if (text && React.Children.count(children) > 0) {
|
||||
console.warn("MenuOption: Please don't use text property together with explicit children. Children are ignored.");
|
||||
}
|
||||
|
|
|
@ -2,22 +2,34 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { View } from 'react-native';
|
||||
|
||||
const MenuOptions = ({ style, children, onSelect, customStyles }) => (
|
||||
<View style={[customStyles.optionsWrapper, style]}>
|
||||
{
|
||||
React.Children.map(children, c =>
|
||||
React.isValidElement(c) ?
|
||||
React.cloneElement(c, {
|
||||
onSelect: c.props.onSelect || onSelect,
|
||||
customStyles: Object.keys(c.props.customStyles || {}).length ? c.props.customStyles : customStyles
|
||||
}) : c
|
||||
)
|
||||
}
|
||||
</View>
|
||||
);
|
||||
class MenuOptions extends React.Component {
|
||||
|
||||
updateCustomStyles(_props) {
|
||||
const { customStyles } = _props
|
||||
const menu = this.context.menuActions._getOpenedMenu()
|
||||
const menuName = menu.instance.getName()
|
||||
this.context.menuRegistry.setOptionsCustomStyles(menuName, customStyles)
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.updateCustomStyles(nextProps)
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.updateCustomStyles(this.props)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { customStyles, style, children } = this.props
|
||||
return (
|
||||
<View style={[customStyles.optionsWrapper, style]}>
|
||||
{children}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
MenuOptions.propTypes = {
|
||||
onSelect: PropTypes.func,
|
||||
customStyles: PropTypes.object,
|
||||
renderOptionsContainer: PropTypes.func,
|
||||
optionsContainerStyle: PropTypes.oneOfType([
|
||||
|
@ -31,4 +43,9 @@ MenuOptions.defaultProps = {
|
|||
customStyles: {},
|
||||
};
|
||||
|
||||
MenuOptions.contextTypes = {
|
||||
menuRegistry: PropTypes.object,
|
||||
menuActions: PropTypes.object,
|
||||
};
|
||||
|
||||
export default MenuOptions;
|
||||
|
|
|
@ -7,6 +7,7 @@ import { iterator2array } from './helpers';
|
|||
* instance: react instance
|
||||
* triggerLayout: Object - layout of menu trigger if known
|
||||
* optionsLayout: Object - layout of menu options if known
|
||||
* optionsCustomStyles: Object - custom styles of options
|
||||
* }
|
||||
*/
|
||||
export default function makeMenuRegistry(menus = new Map()) {
|
||||
|
@ -46,6 +47,14 @@ export default function makeMenuRegistry(menus = new Map()) {
|
|||
menus.set(name, menu);
|
||||
}
|
||||
|
||||
function setOptionsCustomStyles(name, optionsCustomStyles) {
|
||||
if (!menus.has(name)) {
|
||||
return;
|
||||
}
|
||||
const menu = { ...menus.get(name), optionsCustomStyles };
|
||||
menus.set(name, menu);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get `menu data` by name.
|
||||
*/
|
||||
|
@ -60,5 +69,5 @@ export default function makeMenuRegistry(menus = new Map()) {
|
|||
return iterator2array(menus.values());
|
||||
}
|
||||
|
||||
return { subscribe, unsubscribe, updateLayoutInfo, getMenu, getAll };
|
||||
return { subscribe, unsubscribe, updateLayoutInfo, getMenu, getAll, setOptionsCustomStyles };
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue