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

[Breadcrumbs] Add new component #3106

Merged
merged 22 commits into from
Nov 2, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/src/components/breadcrumbs/_breadcrumbs.scss
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ Styleguide breadcrumbs
background: $light-gray1;
cursor: pointer;
padding: 1px ($pt-grid-size / 2);
vertical-align: text-bottom;

&::before {
display: block;
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/components/breadcrumbs/breadcrumb.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ export const Breadcrumb: React.SFC<IBreadcrumbProps> = breadcrumbProps => {
},
breadcrumbProps.className,
);
if (breadcrumbProps.href == null && breadcrumbProps.onClick == null) {
return (
<span className={classes}>
{breadcrumbProps.text}
{breadcrumbProps.children}
</span>
);
}
return (
<a
className={classes}
Expand Down
49 changes: 44 additions & 5 deletions packages/core/src/components/breadcrumbs/breadcrumbs.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,51 @@
@# Breadcrumbs

Breadcrumbs identify the current resource in an application.
Breadcrumbs identify the path to the current resource in an application.

@css breadcrumbs
@reactExample BreadcrumbsExample

@## Props

The component renders an `a.@ns-breadcrumb`. You are responsible for constructing
the `ul.@ns-breadcrumbs` list. [`CollapsibleList`](#core/components/collapsible-list)
works nicely with this component because its props are a subset of `IMenuItemProps`.
@### Breadcrumbs

The `Breadcrumbs` component requires an `items` array of
[breadcrumb props](#core/components/breadcrumbs.breadcrumb) and renders them in
an [`OverflowList`](#core/components/overflow-list) to automatically collapse
breadcrumbs that do not fit in the available space.

```tsx
const { Breadcrumbs, IBreadcrumbProps, Icon } = "@blueprintjs/core";

const BREADCRUMBS: IBreadcrumbProps[] = [
{ href: "/users", icon: "folder-close", text: "Users" },
{ href: "/users/janet", icon: "folder-close", text: "Janet" },
{ icon: "document", text: "image.jpg" },
];

export class BreadcrumbsExample extends React.Component {
public render() {
return (
<Breadcrumbs
currentBreadcrumbRenderer={this.renderCurrentBreadcrumb}
items={BREADCRUMBS}
/>
);
}
private renderCurrentBreadcrumb = ({ text, ...restProps }: IBreadcrumbProps) => {
// customize rendering of last breadcrumb
return <Breadcrumb {...restProps}>{text} <Icon icon="star" /></Breadcrumb>;
};
}
```

@interface IBreadcrumbsProps

@### Breadcrumb

The `Breadcrumb` component renders an `a.@ns-breadcrumb` if given an `href` or
`onClick` and a `span.@ns-breadcrumb` otherwise. Typically you will supply an
array of `IBreadcrumbProps` to the `<Breadcrumbs items>` prop and only render
this component directly when defining a custom `breadcrumbRenderer`.

@interface IBreadcrumbProps

Expand All @@ -27,3 +64,5 @@ user to that resource.
containing breadcrumbs that are collapsed due to layout constraints.
* When adding another element (such as a [tooltip](#core/components/tooltip) or
[popover](#core/components/popover)) to a breadcrumb, wrap it around the contents of the `li`.

@css breadcrumbs
130 changes: 130 additions & 0 deletions packages/core/src/components/breadcrumbs/breadcrumbs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
* 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 { Boundary } from "../../common/boundary";
import * as Classes from "../../common/classes";
import { Position } from "../../common/position";
import { IProps } from "../../common/props";
import { Menu } from "../menu/menu";
import { MenuItem } from "../menu/menuItem";
import { IOverflowListProps, OverflowList } from "../overflow-list/overflowList";
import { IPopoverProps, Popover } from "../popover/popover";
import { Breadcrumb, IBreadcrumbProps } from "./breadcrumb";

export interface IBreadcrumbsProps extends IProps {
/**
* Callback invoked to render visible breadcrumbs. Best practice is to
* render a `<Breadcrumb>` element. If `currentBreadcrumbRenderer` is also
* supplied, that callback will be used for the current breadcrumb instead.
* @default Breadcrumb
*/
breadcrumbRenderer?: (props: IBreadcrumbProps) => JSX.Element;

/**
* Which direction the breadcrumbs should collapse from: start or end.
* @default Boundary.START
*/
collapseFrom?: Boundary;

/**
* Callback invoked to render the current breadcrumb, which is the last
* element in the `items` array.
*
* If this prop is omitted, `breadcrumbRenderer` will be invoked for the
* current breadcrumb instead.
*/
currentBreadcrumbRenderer?: (props: IBreadcrumbProps) => JSX.Element;

/**
* All breadcrumbs to display. Breadcrumbs that do not fit in the container
* will be rendered in an overflow menu instead.
*/
items: IBreadcrumbProps[];

/**
* The minimum number of visible breadcrumbs that should never collapse into
* the overflow menu, regardless of DOM dimensions.
* @default 0
*/
minVisibleItems?: number;

/**
* Props to spread to `OverflowList`. Note that `items`,
* `overflowRenderer`, and `visibleItemRenderer` cannot be changed.
*/
overflowListProps?: Partial<IOverflowListProps<IBreadcrumbProps>>;

/**
* Props to spread to the `Popover` showing the overflow menu.
*/
popoverProps?: IPopoverProps;
}

export class Breadcrumbs extends React.PureComponent<IBreadcrumbsProps> {
public static defaultProps: Partial<IBreadcrumbsProps> = {
collapseFrom: Boundary.START,
};

public render() {
const { className, collapseFrom, items, minVisibleItems, overflowListProps = {} } = this.props;
return (
<OverflowList
collapseFrom={collapseFrom}
minVisibleItems={minVisibleItems}
tagName="ul"
{...overflowListProps}
className={classNames(Classes.BREADCRUMBS, overflowListProps.className, className)}
items={items}
overflowRenderer={this.renderOverflow}
visibleItemRenderer={this.renderBreadcrumbWrapper}
/>
);
}

private renderOverflow = (items: IBreadcrumbProps[]) => {
const { collapseFrom } = this.props;
const position = collapseFrom === Boundary.END ? Position.BOTTOM_RIGHT : Position.BOTTOM_LEFT;
let orderedItems = items;
if (collapseFrom === Boundary.START) {
// If we're collapsing from the start, the menu should be read from the bottom to the
// top, continuing with the breadcrumbs to the right. Since this means the first
// breadcrumb in the props must be the last in the menu, we need to reverse the overlow
// order.
orderedItems = items.slice().reverse();
invliD marked this conversation as resolved.
Show resolved Hide resolved
}
return (
<li>
<Popover position={position} {...this.props.popoverProps}>
<span className={Classes.BREADCRUMBS_COLLAPSED} />
<Menu>{orderedItems.map(this.renderOverflowBreadcrumb)}</Menu>
</Popover>
</li>
);
};

private renderOverflowBreadcrumb = (props: IBreadcrumbProps, index: number) => {
const isClickable = props.href != null || props.onClick != null;
return <MenuItem disabled={!isClickable} {...props} text={props.text} key={index} />;
};

private renderBreadcrumbWrapper = (props: IBreadcrumbProps, index: number) => {
const isCurrent = this.props.items[this.props.items.length - 1] === props;
return <li key={index}>{this.renderBreadcrumb(props, isCurrent)}</li>;
};

private renderBreadcrumb(props: IBreadcrumbProps, isCurrent: boolean) {
if (isCurrent && this.props.currentBreadcrumbRenderer != null) {
return this.props.currentBreadcrumbRenderer(props);
} else if (this.props.breadcrumbRenderer != null) {
return this.props.breadcrumbRenderer(props);
} else {
return <Breadcrumb {...props} current={isCurrent} />;
}
}
}
1 change: 1 addition & 0 deletions packages/core/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const ContextMenu = contextMenu;

export * from "./alert/alert";
export * from "./breadcrumbs/breadcrumb";
export * from "./breadcrumbs/breadcrumbs";
export * from "./button/buttons";
export * from "./button/buttonGroup";
export * from "./callout/callout";
Expand Down
23 changes: 18 additions & 5 deletions packages/core/src/components/overflow-list/overflowList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ export interface IOverflowListProps<T> extends IProps {
items: T[];

/**
* The number of visible items will never be lower than the number passed to
* this prop.
* The minimum number of visible items that should never collapse into the
* overflow menu, regardless of DOM dimensions.
* @default 0
*/
minVisibleItems?: number;
Expand Down Expand Up @@ -75,6 +75,12 @@ export interface IOverflowListProps<T> extends IProps {
/** CSS properties to apply to the root element. */
style?: React.CSSProperties;

/**
* HTML tag name for the container element.
* @default "div"
*/
tagName?: keyof JSX.IntrinsicElements;

/**
* Callback invoked to render each visible item.
* Remember to set a `key` on the rendered element!
Expand Down Expand Up @@ -164,16 +170,23 @@ export class OverflowList<T> extends React.PureComponent<IOverflowListProps<T>,
}

public render() {
const { className, collapseFrom, observeParents, style, visibleItemRenderer } = this.props;
const {
className,
collapseFrom,
observeParents,
style,
tagName: TagName = "div",
visibleItemRenderer,
} = this.props;
const overflow = this.maybeRenderOverflow();
return (
<ResizeSensor onResize={this.resize} observeParents={observeParents}>
<div className={classNames(Classes.OVERFLOW_LIST, className)} style={style}>
<TagName className={classNames(Classes.OVERFLOW_LIST, className)} style={style}>
{collapseFrom === Boundary.START ? overflow : null}
{this.state.visible.map(visibleItemRenderer)}
{collapseFrom === Boundary.END ? overflow : null}
<div className={Classes.OVERFLOW_LIST_SPACER} ref={ref => (this.spacer = ref)} />
</div>
</TagName>
</ResizeSensor>
);
}
Expand Down
10 changes: 10 additions & 0 deletions packages/core/test/breadcrumbs/breadcrumbTests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,14 @@ describe("Breadcrumb", () => {
shallow(<Breadcrumb disabled={true} onClick={onClick} text="Hello" />).simulate("click");
assert.isTrue(onClick.notCalled, "onClick called");
});

it("renders an a tag if it's clickable", () => {
assert.lengthOf(shallow(<Breadcrumb href="test" />).find("a"), 1);
assert.lengthOf(shallow(<Breadcrumb href="test" />).find("span"), 0);
});

it("renders a span tag if it's not clickable", () => {
assert.lengthOf(shallow(<Breadcrumb />).find("a"), 0);
assert.lengthOf(shallow(<Breadcrumb />).find("span"), 1);
});
});
Loading