Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui): overflow-menu #306

Merged
merged 9 commits into from
Mar 14, 2019
16 changes: 16 additions & 0 deletions src/framework/ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,15 @@ import {
Placement as PopoverPlacement,
Placements as PopoverPlacements,
} from './popover/type';
import {
OverflowMenuItem as OverflowMenuItemComponent,
Props as OverflowMenuItemProps,
OverflowMenuItemType,
} from './overflowMenu/overflowMenuItem.component';
import {
OverflowMenu as OverflowMenuComponent,
Props as OverflowMenuProps,
} from './overflowMenu/overflowMenu.component';

const Avatar = styled<AvatarComponent, AvatarProps>(AvatarComponent);
const BottomNavigatorTab = styled<BottomNavigatorTabComponent, BottomNavigatorTabProps>(BottomNavigatorTabComponent);
Expand All @@ -122,6 +131,8 @@ const Tooltip = styled<TooltipComponent, TooltipProps>(TooltipComponent);
const TopNavigationBar = styled<TopNavigationBarComponent, TopNavigationBarProps>(TopNavigationBarComponent);
const TopNavigationBarAction =
styled<TopNavigationBarActionComponent, TopNavigationBarActionProps>(TopNavigationBarActionComponent);
const OverflowMenuItem = styled<OverflowMenuItemComponent, OverflowMenuItemProps>(OverflowMenuItemComponent);
const OverflowMenu = styled<OverflowMenuComponent, OverflowMenuProps>(OverflowMenuComponent);

export {
Avatar,
Expand All @@ -147,6 +158,8 @@ export {
TopNavigationBar,
TopNavigationBarAction,
ViewPager,
OverflowMenu,
OverflowMenuItem,
};

export {
Expand All @@ -172,11 +185,14 @@ export {
TopNavigationBarProps,
TopNavigationBarActionProps,
ViewPagerProps,
OverflowMenuProps,
OverflowMenuItemProps,
};

export {
ButtonAlignment,
ButtonAlignments,
PopoverPlacement,
PopoverPlacements,
OverflowMenuItemType,
};
5 changes: 3 additions & 2 deletions src/framework/ui/list/list.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import {
GestureResponderEvent,
Image,
ImageProps,
TouchableOpacity,
Expand Down Expand Up @@ -145,8 +146,8 @@ describe('@list-item: component checks', () => {
it('* emits onPress with correct args', async () => {
const pressIndex: number = 0;

const onPress = jest.fn((args: any[]) => {
expect(args).toEqual(pressIndex);
const onPress = jest.fn((event: GestureResponderEvent, index: number) => {
expect(index).toEqual(pressIndex);
});

const item = () => (
Expand Down
17 changes: 5 additions & 12 deletions src/framework/ui/list/listItem.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
Text as TextComponent,
Props as TextProps,
} from '../text/text.component';
import { TouchableOpacityIndexedProps } from '../service/type';

interface ListDerivedProps {
index?: number;
Expand All @@ -39,14 +40,6 @@ interface TemplateDescriptionProps extends Partial<TemplateBaseProps> {
description: string;
}

// @ts-ignore: props override
interface TouchableOpacityIndexedProps extends TouchableOpacityProps {
onPress?: (index: number, event: GestureResponderEvent) => void;
onPressIn?: (index: number, event: GestureResponderEvent) => void;
onPressOut?: (index: number, event: GestureResponderEvent) => void;
onLongPress?: (index: number, event: GestureResponderEvent) => void;
}

type ListItemProps = (TemplateTitleProps | TemplateDescriptionProps) & ListDerivedProps;

const Text = styled<TextComponent, TextProps>(TextComponent);
Expand All @@ -57,29 +50,29 @@ export class ListItem extends React.Component<Props> {

private onPress = (event: GestureResponderEvent) => {
if (this.props.onPress) {
this.props.onPress(this.props.index, event);
this.props.onPress(event, this.props.index);
}
};

private onPressIn = (event: GestureResponderEvent) => {
this.props.dispatch([Interaction.ACTIVE]);

if (this.props.onPressIn) {
this.props.onPressIn(this.props.index, event);
this.props.onPressIn(event, this.props.index);
}
};

private onPressOut = (event: GestureResponderEvent) => {
this.props.dispatch([]);

if (this.props.onPressOut) {
this.props.onPressOut(this.props.index, event);
this.props.onPressOut(event, this.props.index);
}
};

private onLongPress = (event: GestureResponderEvent) => {
if (this.props.onLongPress) {
this.props.onLongPress(this.props.index, event);
this.props.onLongPress(event, this.props.index);
}
};

Expand Down
5 changes: 4 additions & 1 deletion src/framework/ui/modal/modal.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ export class Modal extends React.Component<Props> {
const dialog: React.ReactElement<ViewProps> =
<Animated.View
{...derivedProps}
style={animationStyle}
style={[animationStyle, styles.animatedWrapper]}
onStartShouldSetResponder={this.onStartShouldSetResponder}
onResponderRelease={this.onResponderRelease}
onStartShouldSetResponderCapture={this.onStartShouldSetResponderCapture}
Expand All @@ -177,4 +177,7 @@ const styles = StyleSheet.create({
width: 0,
height: 0,
},
animatedWrapper: {
alignSelf: 'flex-start',
},
});
38 changes: 32 additions & 6 deletions src/framework/ui/modal/modal.spec.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ exports[`@modal component checks * component styled with mappings 1`] = `
onStartShouldSetResponder={[Function]}
onStartShouldSetResponderCapture={[Function]}
pointerEvents="box-none"
style={Object {}}
style={
Object {
"alignSelf": "flex-start",
}
}
theme={
Object {
"blue-dark": "#2541CC",
Expand Down Expand Up @@ -93,7 +97,11 @@ exports[`@modal component checks * modal closes on passed prop 1`] = `
onStartShouldSetResponder={[Function]}
onStartShouldSetResponderCapture={[Function]}
pointerEvents="box-none"
style={Object {}}
style={
Object {
"alignSelf": "flex-start",
}
}
visible={true}
>
<View
Expand Down Expand Up @@ -191,7 +199,11 @@ exports[`@modal component checks * modal closes on passed prop 2`] = `
onStartShouldSetResponder={[Function]}
onStartShouldSetResponderCapture={[Function]}
pointerEvents="box-none"
style={Object {}}
style={
Object {
"alignSelf": "flex-start",
}
}
visible={true}
>
<View
Expand Down Expand Up @@ -275,7 +287,11 @@ exports[`@modal component checks * modal component close on backDrop checks 1`]
onStartShouldSetResponder={[Function]}
onStartShouldSetResponderCapture={[Function]}
pointerEvents="box-none"
style={Object {}}
style={
Object {
"alignSelf": "flex-start",
}
}
visible={true}
>
<View
Expand Down Expand Up @@ -333,7 +349,11 @@ exports[`@modal component checks * modal component close on backDrop checks 2`]
onStartShouldSetResponder={[Function]}
onStartShouldSetResponderCapture={[Function]}
pointerEvents="box-none"
style={Object {}}
style={
Object {
"alignSelf": "flex-start",
}
}
visible={true}
>
<View
Expand Down Expand Up @@ -378,7 +398,11 @@ exports[`@modal component checks * modal component renders properly 1`] = `
onStartShouldSetResponder={[Function]}
onStartShouldSetResponderCapture={[Function]}
pointerEvents="box-none"
style={Object {}}
style={
Object {
"alignSelf": "flex-start",
}
}
visible={true}
>
<View
Expand Down Expand Up @@ -468,6 +492,7 @@ exports[`@modal component checks * with animations 1`] = `
pointerEvents="box-none"
style={
Object {
"alignSelf": "flex-start",
"transform": Array [
Object {
"translateY": 1334,
Expand Down Expand Up @@ -564,6 +589,7 @@ exports[`@modal component checks * with animations 2`] = `
pointerEvents="box-none"
style={
Object {
"alignSelf": "flex-start",
"opacity": 0,
}
}
Expand Down
133 changes: 133 additions & 0 deletions src/framework/ui/overflowMenu/overflowMenu.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import React from 'react';
import {
ViewProps,
View,
GestureResponderEvent,
} from 'react-native';
import {
StyledComponentProps,
StyleType,
styled,
} from '@kitten/theme';
import {
OverflowMenuItemType,
OverflowMenuItem as OverflowMenuItemComponent,
Props as OverflowMenuItemProps,
} from './overflowMenuItem.component';
import {
Popover as PopoverComponent,
Props as PopoverProps,
} from '../popover/popover.component';
import { Omit } from '../service/type';

type MenuItemElement = React.ReactElement<OverflowMenuItemProps>;

interface OverflowMenuProps {
children: React.ReactElement<any>;
items: OverflowMenuItemType[];
size?: string;
onSelect?: (event: GestureResponderEvent, index: number) => void;
}

const Popover = styled<PopoverComponent, PopoverProps>(PopoverComponent);
const OverflowMenuItem =
styled<OverflowMenuItemComponent, OverflowMenuItemProps>(OverflowMenuItemComponent);

export type Props = & StyledComponentProps & OverflowMenuProps & Omit<PopoverProps, 'content'>;

export class OverflowMenu extends React.Component<Props> {

private isFirstItem = (index: number): boolean => {
return index === 0;
};

private isLastItem = (index: number): boolean => {
return this.props.items.length - 1 === index;
};

private isSingleItem = (): boolean => {
return this.props.items.length === 1;
};

private getPopoverStyle = (style: StyleType): StyleType => ({
...style.popover,
borderRadius: style.borderRadius,
});

private onSelect = (event: GestureResponderEvent, index: number): void => {
const { visible, onSelect } = this.props;
// FIXME: this is due to popover renders always with the "opacity": 0,
// so popover can listen events even it's "invisible"
if (visible && onSelect) {
onSelect(event, index);
}
};

private getMenuItemStyle = (style: StyleType, index: number): StyleType => {
const borderRadius: number = style.menuItem.borderRadius;

if (this.isFirstItem(index) && !this.isSingleItem()) {
return {
borderTopLeftRadius: borderRadius,
borderTopRightRadius: borderRadius,
};
} else if (this.isLastItem(index) && !this.isSingleItem()) {
return {
borderBottomLeftRadius: borderRadius,
borderBottomRightRadius: borderRadius,
};
} else if (this.isSingleItem()) {
return {
borderRadius: borderRadius,
};
}
};

private renderMenuItem = (item: OverflowMenuItemType, index: number): MenuItemElement => {
const { size, themedStyle } = this.props;
const itemStyle: StyleType = this.getMenuItemStyle(themedStyle, index);

return (
<OverflowMenuItem
{...item}
size={size}
isLastItem={this.isLastItem(index)}
style={itemStyle}
key={index}
index={index}
onPress={this.onSelect}
/>
);
};

private renderComponentChildren = (): MenuItemElement[] => {
return this.props.items.map((item: OverflowMenuItemType, index: number) =>
this.renderMenuItem(item, index));
};

private renderMenuContent = (): React.ReactElement<ViewProps> => {
const menuItems: MenuItemElement[] = this.renderComponentChildren();

return (
<View style={this.props.style}>
{menuItems}
</View>
);
};

public render(): React.ReactNode {
const { children, themedStyle, ...restProps } = this.props;
32penkin marked this conversation as resolved.
Show resolved Hide resolved
const menu: React.ReactElement<ViewProps> = this.renderMenuContent();
const popoverStyle: StyleType = this.getPopoverStyle(themedStyle);

return (
<Popover
{...restProps}
indicatorOffset={2}
content={menu}
style={[popoverStyle, restProps.style]}>
{children}
</Popover>
);
}
}
Loading