Skip to content

Commit

Permalink
Scroll to expose expanded items in location list
Browse files Browse the repository at this point in the history
  • Loading branch information
raksooo committed May 6, 2020
1 parent eacc22b commit f41d697
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 52 deletions.
36 changes: 28 additions & 8 deletions gui/src/renderer/components/Accordion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ interface IProps {
expanded: boolean;
animationDuration: number;
children?: React.ReactNode;
onWillExpand?: (nextHeight: number) => void;
onWillCollapse?: () => void;
}

interface IState {
Expand All @@ -28,6 +30,7 @@ const Content = styled.div({

export default class Accordion extends React.Component<IProps, IState> {
private containerRef = React.createRef<HTMLDivElement>();
private contentRef = React.createRef<HTMLDivElement>();

public static defaultProps = {
expanded: true,
Expand All @@ -54,34 +57,51 @@ export default class Accordion extends React.Component<IProps, IState> {
height={this.state.containerHeight}
animationDuration={this.props.animationDuration}
onTransitionEnd={this.onTransitionEnd}>
<Content>{this.state.mountChildren && this.props.children}</Content>
<Content ref={this.contentRef}>{this.state.mountChildren && this.props.children}</Content>
</Container>
);
}

private expand() {
// Make sure the children are mounted first before expanding the accordion
this.mountChildren(() => {
this.onWillExpand();
this.setState({ containerHeight: this.getContentHeightWithUnit() });
});
}

private mountChildren(childrenDidMount: () => void) {
if (!this.state.mountChildren) {
this.setState({ mountChildren: true }, () => {
this.setState({ containerHeight: this.getContentHeight() });
});
this.setState({ mountChildren: true }, childrenDidMount);
} else {
this.setState({ containerHeight: this.getContentHeight() });
childrenDidMount();
}
}

private collapse() {
// First change height to height in px since it's not possible to transition to/from auto
this.setState({ containerHeight: this.getContentHeight() }, () => {
this.setState({ containerHeight: this.getContentHeightWithUnit() }, () => {
this.props.onWillCollapse?.();
// Make sure new height has been applied
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
this.containerRef.current?.offsetHeight;
this.setState({ containerHeight: '0' });
});
}

private getContentHeight(): string {
return (this.containerRef.current?.scrollHeight ?? 0) + 'px';
private getContentHeightWithUnit(): string {
return (this.getContentHeight() ?? 0) + 'px';
}

private getContentHeight(): number | undefined {
return this.contentRef.current?.offsetHeight;
}

private onWillExpand() {
const nextHeight = this.getContentHeight();
if (nextHeight) {
this.props.onWillExpand?.(nextHeight);
}
}

private onTransitionEnd = () => {
Expand Down
8 changes: 7 additions & 1 deletion gui/src/renderer/components/BridgeLocations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ interface IBridgeLocationsProps {
selectedValue?: LiftedConstraint<RelayLocation>;
selectedElementRef?: React.Ref<React.ReactInstance>;
onSelect?: (value: LocationSelection<SpecialBridgeLocationType>) => void;
onWillExpand?: (locationRect: DOMRect, expandedContentHeight: number) => void;
onWillCollapse?: () => void;
}

const BridgeLocations = React.forwardRef(function BridgeLocationsT(
Expand Down Expand Up @@ -49,7 +51,11 @@ const BridgeLocations = React.forwardRef(function BridgeLocationsT(
{messages.pgettext('select-location-view', 'Closest to exit server')}
</SpecialLocation>
</SpecialLocations>
<RelayLocations source={props.source} />
<RelayLocations
source={props.source}
onWillExpand={props.onWillExpand}
onWillCollapse={props.onWillCollapse}
/>
</LocationList>
);
});
Expand Down
24 changes: 23 additions & 1 deletion gui/src/renderer/components/CityRow.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from 'react';
import ReactDOM from 'react-dom';
import { Component, Styles, Types, View } from 'reactxp';
import { colors } from '../../config.json';
import { compareRelayLocation, RelayLocation } from '../../shared/daemon-rpc-types';
Expand All @@ -18,6 +19,8 @@ interface IProps {
expanded: boolean;
onSelect?: (location: RelayLocation) => void;
onExpand?: (location: RelayLocation, value: boolean) => void;
onWillExpand?: (locationRect: DOMRect, expandedContentHeight: number) => void;
onWillCollapse?: () => void;
children?: RelayRowElement | RelayRowElement[];
}

Expand All @@ -30,6 +33,8 @@ const styles = {
};

export default class CityRow extends Component<IProps> {
private buttonRef = React.createRef<Cell.CellButton>();

public static compareProps(oldProps: IProps, nextProps: IProps): boolean {
if (React.Children.count(oldProps.children) !== React.Children.count(nextProps.children)) {
return false;
Expand Down Expand Up @@ -70,6 +75,7 @@ export default class CityRow extends Component<IProps> {
return (
<View>
<Cell.CellButton
ref={this.buttonRef}
onPress={this.handlePress}
disabled={!this.props.hasActiveRelays}
selected={this.props.selected}
Expand All @@ -83,7 +89,15 @@ export default class CityRow extends Component<IProps> {
{hasChildren && <ChevronButton onPress={this.toggleCollapse} up={this.props.expanded} />}
</Cell.CellButton>

{hasChildren && <Accordion expanded={this.props.expanded}>{this.props.children}</Accordion>}
{hasChildren && (
<Accordion
expanded={this.props.expanded}
onWillExpand={this.onWillExpand}
onWillCollapse={this.props.onWillCollapse}
animationDuration={150}>
{this.props.children}
</Accordion>
)}
</View>
);
}
Expand All @@ -100,4 +114,12 @@ export default class CityRow extends Component<IProps> {
this.props.onSelect(this.props.location);
}
};

private onWillExpand = (nextHeight: number) => {
const buttonNode = ReactDOM.findDOMNode(this.buttonRef.current);
if (buttonNode instanceof HTMLElement) {
const buttonRect = buttonNode.getBoundingClientRect();
this.props.onWillExpand?.(buttonRect, nextHeight);
}
};
}
24 changes: 23 additions & 1 deletion gui/src/renderer/components/CountryRow.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from 'react';
import ReactDOM from 'react-dom';
import { Component, Styles, Types, View } from 'reactxp';
import { compareRelayLocation, RelayLocation } from '../../shared/daemon-rpc-types';
import Accordion from './Accordion';
Expand All @@ -17,6 +18,8 @@ interface IProps {
expanded: boolean;
onSelect?: (location: RelayLocation) => void;
onExpand?: (location: RelayLocation, value: boolean) => void;
onWillExpand?: (locationRect: DOMRect, expandedContentHeight: number) => void;
onWillCollapse?: () => void;
children?: CityRowElement | CityRowElement[];
}

Expand All @@ -32,6 +35,8 @@ const styles = {
};

export default class CountryRow extends Component<IProps> {
private buttonRef = React.createRef<Cell.CellButton>();

public static compareProps(oldProps: IProps, nextProps: IProps) {
if (React.Children.count(oldProps.children) !== React.Children.count(nextProps.children)) {
return false;
Expand Down Expand Up @@ -78,6 +83,7 @@ export default class CountryRow extends Component<IProps> {
return (
<View style={styles.container}>
<Cell.CellButton
ref={this.buttonRef}
style={styles.base}
onPress={this.handlePress}
disabled={!this.props.hasActiveRelays}
Expand All @@ -92,7 +98,15 @@ export default class CountryRow extends Component<IProps> {
) : null}
</Cell.CellButton>

{hasChildren && <Accordion expanded={this.props.expanded}>{this.props.children}</Accordion>}
{hasChildren && (
<Accordion
expanded={this.props.expanded}
onWillExpand={this.onWillExpand}
onWillCollapse={this.props.onWillCollapse}
animationDuration={150}>
{this.props.children}
</Accordion>
)}
</View>
);
}
Expand All @@ -109,4 +123,12 @@ export default class CountryRow extends Component<IProps> {
this.props.onSelect(this.props.location);
}
};

private onWillExpand = (nextHeight: number) => {
const buttonNode = ReactDOM.findDOMNode(this.buttonRef.current);
if (buttonNode instanceof HTMLElement) {
const buttonRect = buttonNode.getBoundingClientRect();
this.props.onWillExpand?.(buttonRect, nextHeight);
}
};
}
48 changes: 29 additions & 19 deletions gui/src/renderer/components/CustomScrollbars.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from 'react';
import { Scheduler } from '../../shared/scheduler';

const AUTOHIDE_TIMEOUT = 1000;

Expand Down Expand Up @@ -54,7 +55,7 @@ export default class CustomScrollbars extends React.Component<IProps, IState> {
private scrollableRef = React.createRef<HTMLDivElement>();
private trackRef = React.createRef<HTMLDivElement>();
private thumbRef = React.createRef<HTMLDivElement>();
private autoHideTimer?: NodeJS.Timeout;
private autoHideScheduler = new Scheduler();

public scrollToTop() {
const scrollable = this.scrollableRef.current;
Expand Down Expand Up @@ -86,6 +87,29 @@ export default class CustomScrollbars extends React.Component<IProps, IState> {
}
}

public scrollIntoView(elementRect: DOMRect) {
const scrollable = this.scrollableRef.current;
if (scrollable) {
const scrollableRect = scrollable.getBoundingClientRect();
// The element position needs to be relative to the parent, not the document
const elementTop = elementRect.top - scrollableRect.top;
const bottomOverflow = elementTop + elementRect.height - scrollableRect.height;

let scrollDistance = 0;
if (elementTop < 0) {
scrollDistance = elementTop;
} else if (bottomOverflow > 0) {
// Prevent the elements top from being scrolled out of the visible area
scrollDistance = Math.min(bottomOverflow, elementTop);
}

scrollable.scrollBy({
top: scrollDistance,
behavior: 'smooth',
});
}
}

public getScrollPosition(): [number, number] {
const scroll = this.scrollableRef.current;
if (scroll) {
Expand Down Expand Up @@ -130,7 +154,7 @@ export default class CustomScrollbars extends React.Component<IProps, IState> {
}

public componentWillUnmount() {
this.stopAutoHide();
this.autoHideScheduler.cancel();

document.removeEventListener('mousemove', this.handleMouseMove);
document.removeEventListener('mouseup', this.handleMouseUp);
Expand Down Expand Up @@ -206,7 +230,7 @@ export default class CustomScrollbars extends React.Component<IProps, IState> {
};

private handleEnterTrack = () => {
this.stopAutoHide();
this.autoHideScheduler.cancel();
this.setState({
isTrackHovered: true,
showScrollIndicators: true,
Expand Down Expand Up @@ -326,10 +350,7 @@ export default class CustomScrollbars extends React.Component<IProps, IState> {
}

private startAutoHide() {
if (this.autoHideTimer) {
clearTimeout(this.autoHideTimer);
}
this.autoHideTimer = global.setTimeout(() => {
this.autoHideScheduler.schedule(() => {
this.setState({
showScrollIndicators: false,
showTrack: false,
Expand All @@ -339,25 +360,14 @@ export default class CustomScrollbars extends React.Component<IProps, IState> {
}

private startAutoShrink() {
if (this.autoHideTimer) {
clearTimeout(this.autoHideTimer);
}

this.autoHideTimer = global.setTimeout(() => {
this.autoHideScheduler.schedule(() => {
this.setState({
showTrack: false,
isWide: false,
});
}, AUTOHIDE_TIMEOUT);
}

private stopAutoHide() {
if (this.autoHideTimer) {
clearTimeout(this.autoHideTimer);
this.autoHideTimer = undefined;
}
}

private isPointInsideOfElement(element: HTMLElement, point: { x: number; y: number }) {
const rect = element.getBoundingClientRect();
return (
Expand Down
8 changes: 7 additions & 1 deletion gui/src/renderer/components/ExitLocations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ interface IExitLocationsProps {
selectedValue?: RelayLocation;
selectedElementRef?: React.Ref<React.ReactInstance>;
onSelect?: (value: LocationSelection<never>) => void;
onWillExpand?: (locationRect: DOMRect, expandedContentHeight: number) => void;
onWillCollapse?: () => void;
}

const ExitLocations = React.forwardRef(function ExitLocationsT(
Expand All @@ -30,7 +32,11 @@ const ExitLocations = React.forwardRef(function ExitLocationsT(
selectedValue={selectedValue}
selectedElementRef={props.selectedElementRef}
onSelect={props.onSelect}>
<RelayLocations source={props.source} />
<RelayLocations
source={props.source}
onWillExpand={props.onWillExpand}
onWillCollapse={props.onWillCollapse}
/>
</LocationList>
);
});
Expand Down
6 changes: 6 additions & 0 deletions gui/src/renderer/components/LocationList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,8 @@ interface IRelayLocationsProps {
expandedItems?: RelayLocation[];
onSelect?: (location: RelayLocation) => void;
onExpand?: (location: RelayLocation, expand: boolean) => void;
onWillExpand?: (locationRect: DOMRect, expandedContentHeight: number) => void;
onWillCollapse?: () => void;
}

interface ICommonCellProps<T> {
Expand All @@ -269,6 +271,8 @@ export class RelayLocations extends Component<IRelayLocationsProps> {
expanded={this.isExpanded(countryLocation)}
onSelect={this.handleSelection}
onExpand={this.handleExpand}
onWillExpand={this.props.onWillExpand}
onWillCollapse={this.props.onWillCollapse}
{...this.getCommonCellProps<CountryRow>(countryLocation)}>
{relayCountry.cities.map((relayCity) => {
const cityLocation: RelayLocation = {
Expand All @@ -283,6 +287,8 @@ export class RelayLocations extends Component<IRelayLocationsProps> {
expanded={this.isExpanded(cityLocation)}
onSelect={this.handleSelection}
onExpand={this.handleExpand}
onWillExpand={this.props.onWillExpand}
onWillCollapse={this.props.onWillCollapse}
{...this.getCommonCellProps<CityRow>(cityLocation)}>
{relayCity.relays.map((relay) => {
const relayLocation: RelayLocation = {
Expand Down
Loading

0 comments on commit f41d697

Please sign in to comment.