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 5, 2020
1 parent 7faba9d commit bda43ce
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 45 deletions.
14 changes: 13 additions & 1 deletion 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,7 +57,7 @@ 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>
);
}
Expand All @@ -63,16 +66,19 @@ export default class Accordion extends React.Component<IProps, IState> {
// Make sure the children are mounted first before expanding the accordion
if (!this.state.mountChildren) {
this.setState({ mountChildren: true }, () => {
this.onWillExpand();
this.setState({ containerHeight: this.getContentHeight() });
});
} else {
this.onWillExpand();
this.setState({ containerHeight: this.getContentHeight() });
}
}

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.props.onWillCollapse?.();
// Make sure new height has been applied
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
this.containerRef.current?.offsetHeight;
Expand All @@ -84,6 +90,12 @@ export default class Accordion extends React.Component<IProps, IState> {
return (this.containerRef.current?.scrollHeight ?? 0) + 'px';
}

private onWillExpand() {
if (this.contentRef.current) {
this.props.onWillExpand?.(this.contentRef.current.offsetHeight);
}
}

private onTransitionEnd = () => {
if (this.props.expanded) {
// Height auto enables the container to grow if the content changes size
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?: (buttonDimensions: DOMRect, additionalHeight: 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?: (buttonDimensions: DOMRect, additionalHeight: 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 buttonDimensions = buttonNode.getBoundingClientRect();
this.props.onWillExpand?.(buttonDimensions, 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?: (buttonDimensions: DOMRect, additionalHeight: 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 buttonDimensions = buttonNode.getBoundingClientRect();
this.props.onWillExpand?.(buttonDimensions, nextHeight);
}
};
}
43 changes: 24 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,24 @@ export default class CustomScrollbars extends React.Component<IProps, IState> {
}
}

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

if (bottomOverflow > 0) {
scrollable.scrollBy({
// Prevent the elements top from being scrolled out of the visible area
top: Math.min(bottomOverflow, elementDimensions.top),
behavior: 'smooth',
});
}
}
}

public getScrollPosition(): [number, number] {
const scroll = this.scrollableRef.current;
if (scroll) {
Expand Down Expand Up @@ -130,7 +149,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 +225,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 +345,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 +355,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?: (buttonDimensions: DOMRect, additionalHeight: 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?: (buttonDimensions: DOMRect, additionalHeight: 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 bda43ce

Please sign in to comment.