Skip to content

Commit

Permalink
feat(ui): overflow-menu (#306)
Browse files Browse the repository at this point in the history
  • Loading branch information
32penkin authored Mar 14, 2019
1 parent 35c35f4 commit f396ba7
Show file tree
Hide file tree
Showing 15 changed files with 1,992 additions and 25 deletions.
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;
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

0 comments on commit f396ba7

Please sign in to comment.