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

[Core] Add overflow list component #2537

Merged
merged 30 commits into from
Jun 1, 2018
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
6397f7b
Add overflow list component
invliD May 22, 2018
c898836
Refactor throttle
invliD May 22, 2018
a4c2732
Add iso test props
invliD May 22, 2018
3d8b20d
Unnest spacer css
invliD May 22, 2018
877917f
Remove constructor
invliD May 22, 2018
7cd0698
Clarify overflow
invliD May 22, 2018
26aea39
Improve legibility of repartition method
invliD May 22, 2018
9ae9eb2
Remove extraneous element measuring
invliD May 22, 2018
4a58756
Improve classes const definition
invliD May 22, 2018
fcec829
Stop measuring all elements on mount
invliD May 22, 2018
0fddce3
stuff
invliD May 22, 2018
412aef3
Add tests
invliD May 22, 2018
68d54e9
Add props to docs
invliD May 22, 2018
67706f1
Use breadcrumb component
invliD May 22, 2018
cd37872
Add props validation in willReceiveProps
invliD May 23, 2018
fd7ebdc
Fix comment
invliD May 23, 2018
6092dc2
Fix tests
invliD May 23, 2018
593b552
:(
invliD May 23, 2018
2fa5074
Update _examples.scss
invliD May 27, 2018
6b5590d
add some docs
llorca May 30, 2018
fe43b06
Update _overflow-list.scss
invliD May 31, 2018
e1712bd
Merge branch 'develop' into sb/overflow-list
giladgray May 31, 2018
ddd3253
no need for constructor
May 31, 2018
97632df
inline renderItems
May 31, 2018
f009f30
observer.disconnect() instead of manually unobserving each element
May 31, 2018
43975e2
clientWidth instead of getClientBB()
May 31, 2018
649c595
docs docs docs!
May 31, 2018
d796f4a
revert constructor and clientWidth
May 31, 2018
a226afc
docs edits
May 31, 2018
d76a0c4
Update overflow-list.md
invliD May 31, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"popper.js": "^1.14.1",
"react-popper": "^0.8.2",
"react-transition-group": "^2.2.1",
"resize-observer-polyfill": "^1.5.0",
"tslib": "^1.9.0"
},
"peerDependencies": {
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/common/classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@ export const NON_IDEAL_STATE_VISUAL = `${NON_IDEAL_STATE}-visual`;

export const NUMERIC_INPUT = `${NS}-numeric-input`;

export const OVERFLOW_LIST = `${NS}-overflow-list`;
export const OVERFLOW_LIST_SPACER = `${NS}-overflow-list-spacer`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

${OVERFLOW_LIST}-spacer


export const OVERLAY = `${NS}-overlay`;
export const OVERLAY_BACKDROP = `${OVERLAY}-backdrop`;
export const OVERLAY_CONTENT = `${OVERLAY}-content`;
Expand Down
28 changes: 18 additions & 10 deletions packages/core/src/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ export function countDecimalPlaces(num: number) {
* @see https://developer.mozilla.org/en-US/docs/Web/Events/scroll
*/
export function throttleEvent(target: EventTarget, eventName: string, newEventName: string) {
const throttledFunc = _throttleHelper(undefined, undefined, (event: Event) => {
const throttledFunc = _throttleHelper((event: Event) => {
target.dispatchEvent(new CustomEvent(newEventName, event));
});
target.addEventListener(eventName, throttledFunc);
Expand All @@ -187,22 +187,32 @@ export function throttleReactEventCallback(
options: IThrottledReactEventOptions = {},
) {
const throttledFunc = _throttleHelper(
callback,
(event2: React.SyntheticEvent<any>) => {
if (options.preventDefault) {
event2.preventDefault();
}
},
// prevent React from reclaiming the event object before we reference it
(event2: React.SyntheticEvent<any>) => event2.persist(),
callback,
);
return throttledFunc;
}

function _throttleHelper(
onBeforeIsRunningCheck: (...args: any[]) => void,
onAfterIsRunningCheck: (...args: any[]) => void,
onAnimationFrameRequested: (...args: any[]) => void,
/**
* Throttle a method by wrapping it in a `requestAnimationFrame` call. Returns
* the throttled function.
*/
// tslint:disable-next-line:ban-types
export function throttle<T extends Function>(method: T): T {
return _throttleHelper(method);
}

// tslint:disable-next-line:ban-types
function _throttleHelper<T extends Function>(
onAnimationFrameRequested: T,
onBeforeIsRunningCheck?: T,
onAfterIsRunningCheck?: T,
) {
let isRunning = false;
const func = (...args: any[]) => {
Expand All @@ -222,11 +232,9 @@ function _throttleHelper(
}

requestAnimationFrame(() => {
if (isFunction(onAnimationFrameRequested)) {
onAnimationFrameRequested(...args);
}
onAnimationFrameRequested(...args);
isRunning = false;
});
};
return func;
return (func as any) as T;
}
1 change: 1 addition & 0 deletions packages/core/src/components/_index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
@import "menu/menu";
@import "navbar/navbar";
@import "non-ideal-state/non-ideal-state";
@import "overflow-list/overflow-list";
@import "overlay/overlay";
@import "popover/popover";
@import "portal/portal";
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/components/breadcrumbs/_breadcrumbs.scss
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ $breadcrumb-line-height: $pt-icon-size-large - 1px !default;
content: $pt-icon-chevron-right;
}

&:last-child::after {
&:last-of-type::after {
display: none;
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/components/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
@page menu
@page navbar
@page non-ideal-state
@page overflow-list
@page overlay
@page popover
@page portal
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export * from "./navbar/navbarDivider";
export * from "./navbar/navbarGroup";
export * from "./navbar/navbarHeading";
export * from "./non-ideal-state/nonIdealState";
export * from "./overflow-list/overflowList";
export * from "./overlay/overlay";
export * from "./table/table-html";
export * from "./text/text";
Expand Down
12 changes: 12 additions & 0 deletions packages/core/src/components/overflow-list/_overflow-list.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright 2018 Palantir Technologies, Inc. All rights reserved.
// Licensed under the terms of the LICENSE file distributed with this project.

.#{$ns}-overflow-list {
display: flex;
min-width: 0;
}

.#{$ns}-overflow-list-spacer {
flex-shrink: 1;
width: 1px;
}
5 changes: 5 additions & 0 deletions packages/core/src/components/overflow-list/overflow-list.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@# Overflow list

this is cool

@reactExample OverflowListExample
170 changes: 170 additions & 0 deletions packages/core/src/components/overflow-list/overflowList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/*
* Copyright 2018 Palantir Technologies, Inc. All rights reserved.
*
* Licensed under the terms of the LICENSE file distributed with this project.
*/

import classNames from "classnames";
import * as React from "react";
import ResizeObserver from "resize-observer-polyfill";

import { Boundary } from "../../common/boundary";
import * as Classes from "../../common/classes";
import { IProps } from "../../common/props";
import { throttle } from "../../common/utils";

export interface IOverflowListProps<T> extends IProps {
/**
* Which direction the items should collapse from: start or end of the children.
* @default Boundary.START
*/
collapseFrom?: Boundary;

/**
* All items to display in the list. Items that don’t fit in the container will be rendered in the overflow instead.
*/
items: T[];

/**
* If `true`, all parent elements of the container will also be observed. If changes to a parent’s size is detected, the overflow will
* be recalculated.
* Only enable this prop if the overflow should be recalculated when a parent element resizes in a way that does not also cause the
* `OverflowContainer` to resize.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OverflowContainer -> OverflowList everywhere

*/
observeParents?: boolean;

/**
* Callback invoked to render the overflow if necessary.
* @param overflowItems The items that didn’t fit in the container.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove @param

*/
overflowRenderer: (overflowItems: T[]) => React.ReactNode;

/**
* Callback invoked to render each visible item.
*/
visibleItemRenderer: (item: T, index: number) => React.ReactChild;
}

export interface IOverflowListState<T> {
overflow: T[];
visible: T[];
}

export class OverflowList<T> extends React.Component<IOverflowListProps<T>, IOverflowListState<T>> {
public static displayName = "Blueprint2.OverflowList";

public static defaultProps: Partial<IOverflowListProps<never>> = {
collapseFrom: Boundary.START,
};

public static ofType<T>() {
return OverflowList as new (props: IOverflowListProps<T>) => OverflowList<T>;
}

public state: IOverflowListState<T> = {
overflow: [],
visible: this.props.items,
};

private element: Element | null = null;
private observer = new ResizeObserver(
throttle((entries: ResizeObserverEntry[]) => {
this.resize(entries.map(entry => ({ element: entry.target, width: entry.contentRect.width })));
}),
);
private previousWidths = new Map<Element, number>();
private spacer: Element | null = null;

public componentDidMount() {
if (this.element != null) {
this.observer.observe(this.element);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add comment that observer invokes callback immediately to start

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also need to invoke this.resize() on mount for first frame

this.previousWidths.set(this.element, this.element.getBoundingClientRect().width);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are these necessary?

if (this.props.observeParents) {
for (let element: Element | null = this.element; element != null; element = element.parentElement) {
this.observer.observe(element);
this.previousWidths.set(element, element.getBoundingClientRect().width);
}
}
}
}

public componentWillReceiveProps(nextProps: IOverflowListProps<T>) {
this.setState({
overflow: [],
visible: nextProps.items,
});
}

public componentDidUpdate() {
const entries = Array.from(this.previousWidths.keys()).map(element => ({
element,
width: element.getBoundingClientRect().width,
}));
this.resize(entries);
}

public componentWillUnmount() {
Array.from(this.previousWidths.keys()).forEach(element => this.observer.unobserve(element));
}

public render() {
const { className, collapseFrom } = this.props;
const overflow = this.maybeRenderOverflow();
return (
<div className={classNames(Classes.OVERFLOW_LIST, className)} ref={ref => (this.element = ref)}>
{collapseFrom === Boundary.START ? overflow : null}
{this.renderItems()}
{collapseFrom === Boundary.END ? overflow : null}
<div className={Classes.OVERFLOW_LIST_SPACER} ref={ref => (this.spacer = ref)} />
</div>
);
}

private renderItems() {
return this.state.visible.map(this.props.visibleItemRenderer);
}

private maybeRenderOverflow() {
const { overflow } = this.state;
if (overflow.length === 0) {
return null;
}
return this.props.overflowRenderer(overflow);
}

private resize(entries: Array<{ element: Element; width: number }>) {
// if any parent is growing, assume we have more room than before
const growing = entries.some(entry => {
const previousWidth = this.previousWidths.get(entry.element) || 0;
return entry.width > previousWidth;
});
this.repartition(growing);
entries.forEach(entry => this.previousWidths.set(entry.element, entry.width));
}

private repartition(growing: boolean) {
if (this.spacer == null) {
return;
}
if (growing) {
this.setState({
overflow: [],
visible: this.props.items,
});
} else if (this.spacer.getBoundingClientRect().width < 1) {
this.setState(state => {
const collapseFromStart = this.props.collapseFrom === Boundary.START;
const visible = state.visible.slice();
const next = collapseFromStart ? visible.shift() : visible.pop();
if (next === undefined) {
return null;
}
const overflow = collapseFromStart ? [next, ...state.overflow] : [...state.overflow, next];
return {
overflow,
visible,
};
});
}
}
}
1 change: 1 addition & 0 deletions packages/core/test/isotest.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const customProps = {
Hotkey: { combo: "mod+s", global: true, label: "save" },
Icon: { iconName: "build" },
KeyCombo: { combo: "?" },
OverflowList: { items: [], overflowRenderer: () => null, visibleItemRenderer: () => null },
Overlay: { lazy: false, usePortal: false },
SVGTooltip: tooltipContent,
TagInput: { values: ["foo", "bar", "baz"] },
Expand Down
1 change: 1 addition & 0 deletions packages/docs-app/src/examples/core-examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export * from "./navbarExample";
export * from "./numericInputBasicExample";
export * from "./numericInputExtendedExample";
export * from "./nonIdealStateExample";
export * from "./overflowListExample";
export * from "./overlayExample";
export * from "./popoverExample";
export * from "./popoverInteractionKindExample";
Expand Down
Loading