Skip to content

Commit

Permalink
feat(ui): popover - autoplacement
Browse files Browse the repository at this point in the history
  • Loading branch information
artyorsh committed Jul 22, 2019
1 parent 9a93a70 commit 27eac63
Show file tree
Hide file tree
Showing 4 changed files with 364 additions and 144 deletions.
63 changes: 63 additions & 0 deletions src/framework/ui/popover/placement.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {
Frame,
PlacementOptions,
PopoverPlacement,
PopoverPlacements,
} from './type';

const PLACEMENT_FAMILIES: string[] = [
PopoverPlacements.BOTTOM.rawValue,
PopoverPlacements.TOP.rawValue,
PopoverPlacements.LEFT.rawValue,
PopoverPlacements.RIGHT.rawValue,
];

export class PopoverPlacementService {

public find(preferredValue: PopoverPlacement, options: PlacementOptions): PopoverPlacement {
const placement: PopoverPlacement = this.findRecursive(preferredValue, PLACEMENT_FAMILIES, options);

return placement || preferredValue;
}

private findRecursive(placement: PopoverPlacement, families: string[], options: PlacementOptions): PopoverPlacement {
const oneOfCurrentFamily: PopoverPlacement = this.findFromFamily(placement, options);

if (oneOfCurrentFamily) {
return oneOfCurrentFamily;
}

const oneOfReversedFamily: PopoverPlacement = this.findFromFamily(placement.reverse(), options);

if (oneOfReversedFamily) {
return oneOfReversedFamily;
}

delete families[families.indexOf(placement.parent().rawValue)];
delete families[families.indexOf(placement.reverse().parent().rawValue)];

const firstTruthy: string = families.filter(Boolean)[0];

if (firstTruthy) {
const nextPlacement: PopoverPlacement = PopoverPlacements.parse(firstTruthy);

return this.findRecursive(nextPlacement, families, options);
}

return null;
}

private findFromFamily(placement: PopoverPlacement, options: PlacementOptions): PopoverPlacement {
const preferredFrame: Frame = placement.frame(options);

if (placement.fits(preferredFrame, options.bounds)) {
return placement;
}

return placement.family().find((familyValue: PopoverPlacement): boolean => {
const familyFrame = familyValue.frame(options);

return familyValue.fits(familyFrame, options.bounds);
});
}
}
129 changes: 65 additions & 64 deletions src/framework/ui/popover/popover.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

import React from 'react';
import {
Dimensions,
ScaledSize,
StyleProp,
StyleSheet,
View,
Expand All @@ -32,11 +34,12 @@ import {
} from './measure.component';
import {
Frame,
OffsetRect,
Offsets,
PlacementOptions,
PopoverPlacement,
PopoverPlacements,
} from './type';
import { PopoverPlacementService } from './placement.service';
import { ModalPresentingBased } from '../support/typings';

type ContentElement = React.ReactElement<any>;
Expand All @@ -50,6 +53,9 @@ interface ComponentProps extends PopoverViewProps, ModalPresentingBased {

export type PopoverProps = StyledComponentProps & ViewProps & ComponentProps;

const WINDOW: ScaledSize = Dimensions.get('window');
const WINDOW_BOUNDS: Frame = new Frame(0, 0, WINDOW.width, WINDOW.height);

const TAG_CHILD: number = 0;
const TAG_CONTENT: number = 1;
const PLACEMENT_DEFAULT: PopoverPlacement = PopoverPlacements.BOTTOM;
Expand Down Expand Up @@ -137,26 +143,21 @@ export class PopoverComponent extends React.Component<PopoverProps> {
onBackdropPress: () => null,
};

private popoverElement: MeasuredElement;
private popoverModalId: string = '';
private popoverId: string;
private placementService: PopoverPlacementService = new PopoverPlacementService();
private popoverPlacement: PopoverPlacement;

public componentDidUpdate(prevProps: PopoverProps) {
const { visible } = this.props;

if (prevProps.visible !== visible) {
if (visible) {
// Toggles re-measuring
if (prevProps.visible !== this.props.visible) {
if (this.props.visible) {
// Toggles re-measuring. This is needed for dynamic containers like ScrollView
this.setState({ layout: undefined });
} else {
this.popoverModalId = ModalService.hide(this.popoverModalId);
this.popoverId = ModalService.hide(this.popoverId);
}
}
}

public componentWillUnmount() {
this.popoverModalId = '';
}

private getComponentStyle = (source: StyleType): StyleType => {
const {
indicatorWidth,
Expand All @@ -172,85 +173,82 @@ export class PopoverComponent extends React.Component<PopoverProps> {
height: indicatorHeight,
backgroundColor: indicatorBackgroundColor,
},
child: {},
};
};

private onMeasure = (layout: MeasureResult) => {
const { visible } = this.props;
if (this.props.visible) {
const placementOptions: PlacementOptions = this.createPlacementOptions(layout);
const popoverPlacement = this.placementService.find(this.popoverPlacement, placementOptions);

if (visible) {
this.popoverModalId = this.showPopoverModal(this.popoverElement, layout);
this.popoverId = this.showPopoverModal(popoverPlacement, placementOptions);
}
};

private showPopoverModal = (element: MeasuredElement, layout: MeasureResult): string => {
const { placement, allowBackdrop, onBackdropPress } = this.props;
private createPlacement = (value: string | PopoverPlacement): PopoverPlacement => {
return PopoverPlacements.parse(value, PLACEMENT_DEFAULT);
};

private createPlacementOptions = (layout: MeasureResult): PlacementOptions => {
const { children } = this.props;

const popoverFrame: Frame = this.getPopoverFrame(layout, placement);
return {
source: layout[TAG_CONTENT],
other: layout[TAG_CHILD],
bounds: WINDOW_BOUNDS,
offsets: Offsets.find(children.props.style),
};
};

const { origin: popoverPosition } = popoverFrame;
private showPopoverModal = (placement: PopoverPlacement, options: PlacementOptions): string => {
const popoverFrame: Frame = placement.frame(options);
const popoverElement: MeasuredElement = this.renderPopoverElement(this.props.content, placement);

const additionalStyle: ViewStyle = {
left: popoverPosition.x,
top: popoverPosition.y,
opacity: 1,
const positionStyle: ViewStyle = {
left: popoverFrame.origin.x,
top: popoverFrame.origin.y,
};

const popover: React.ReactElement<ModalPresentingBased> = React.cloneElement(element, {
style: additionalStyle,
const positionedPopoverElement: React.ReactElement<ModalPresentingBased> = React.cloneElement(popoverElement, {
style: [styles.popoverVisible, positionStyle],
});

return ModalService.show(popover, {
allowBackdrop,
onBackdropPress,
return ModalService.show(positionedPopoverElement, {
allowBackdrop: this.props.allowBackdrop,
onBackdropPress: this.props.onBackdropPress,
});
};

private getPopoverFrame = (layout: MeasureResult, rawPlacement: string | PopoverPlacement): Frame => {
const { children } = this.props;
const { [TAG_CONTENT]: popoverFrame, [TAG_CHILD]: childFrame } = layout;

const offsetRect: OffsetRect = Offsets.find(children.props.style);
const placement: PopoverPlacement = PopoverPlacements.parse(rawPlacement, PLACEMENT_DEFAULT);

return placement.frame(popoverFrame, childFrame, offsetRect);
};

private renderPopoverElement = (children: ContentElement, popoverStyle: Partial<StyleType>): MeasuringElement => {
const { style, placement, indicatorStyle, ...derivedProps } = this.props;
private renderPopoverElement = (children: ContentElement, placement: PopoverPlacement): ContentElement => {
const { style: derivedStyle, themedStyle, indicatorStyle, ...derivedProps } = this.props;
const { container, indicator } = this.getComponentStyle(themedStyle);

const measuringProps: MeasuringElementProps = {
tag: TAG_CONTENT,
};

const popoverPlacement: PopoverPlacement = PopoverPlacements.parse(placement, PLACEMENT_DEFAULT);
const indicatorPlacement: PopoverPlacement = popoverPlacement.reverse();
const measuringProps: MeasuringElementProps = { tag: TAG_CONTENT };

return (
<View
{...measuringProps}
key={TAG_CONTENT}
style={styles.container}>
style={[styles.popover, styles.popoverInvisible]}>
<PopoverView
{...derivedProps}
style={[popoverStyle.container, style]}
indicatorStyle={[popoverStyle.indicator, styles.indicator, indicatorStyle]}
placement={indicatorPlacement.rawValue}>
style={[container, derivedStyle]}
indicatorStyle={[indicator, styles.indicator, indicatorStyle]}
placement={placement.reverse().rawValue}>
{children}
</PopoverView>
</View>
);
};

private renderChildElement = (source: ChildElement, style: StyleProp<ViewStyle>): MeasuringElement => {
private renderChildElement = (source: ChildElement): MeasuringElement => {
const measuringProps: MeasuringElementProps = { tag: TAG_CHILD };

return (
<View
{...measuringProps}
key={TAG_CHILD}
style={[style, styles.child]}>
style={styles.child}>
{source}
</View>
);
Expand All @@ -265,26 +263,29 @@ export class PopoverComponent extends React.Component<PopoverProps> {
);
};

public render(): MeasuringNode | React.ReactNode {
const { themedStyle, content, visible, children } = this.props;
const { child, ...popoverStyles } = this.getComponentStyle(themedStyle);

if (visible) {
this.popoverElement = this.renderPopoverElement(content, popoverStyles);
const childElement: MeasuringElement = this.renderChildElement(children, child);
public render(): React.ReactNode {
if (this.props.visible) {
this.popoverPlacement = this.createPlacement(this.props.placement);
const popoverElement: ContentElement = this.renderPopoverElement(this.props.content, this.popoverPlacement);
const childElement: ChildElement = this.renderChildElement(this.props.children);

return this.renderMeasuringElement(childElement, this.popoverElement);
return this.renderMeasuringElement(childElement, popoverElement);
}

return children;
return this.props.children;
}
}

const styles = StyleSheet.create({
container: {
popover: {
position: 'absolute',
},
popoverInvisible: {
opacity: 0,
},
popoverVisible: {
opacity: 1,
},
indicator: {},
child: {},
});
Expand Down

0 comments on commit 27eac63

Please sign in to comment.