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

[Labs] InputList β‡’ QueryList #1243

Merged
merged 1 commit into from
Jun 15, 2017
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
2 changes: 1 addition & 1 deletion packages/labs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This package contains in-progress and unstable React components as we develop th

Current components:

- `InputList` is a low-level component for composing an `InputGroup` to filter an array of `items`.
- `QueryList` is a low-level component for filtering an array of `items` with keyboard selection interactions.
- `Select` is a high-level component for choosing items from a list. It can be used instead of the
HTML `<select>` element.

Expand Down
29 changes: 1 addition & 28 deletions packages/labs/src/blueprint-labs.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,4 @@
* and https://github.com/palantir/blueprint/blob/master/PATENTS
*/

@import "~@blueprintjs/core/src/common/variables";

$select-popover-max-height: $pt-grid-size * 20 !default;
$select-popover-max-width: $pt-grid-size * 40 !default;

.pt-select-popover {
.pt-popover-content {
// use padding on container rather than margin on input group
// because top margin leaves some empty space with no background color.
padding: $pt-grid-size / 2;
}

.pt-input-group {
margin-bottom: 0;
}

.pt-menu {
max-width: $select-popover-max-width;
max-height: $select-popover-max-height;
overflow: auto;
padding: 0;

&:not(:first-child) {
// adjust padding to account for that on .pt-popover-content above
padding-top: $pt-grid-size / 2;
}
}
}
@import "select/select";
4 changes: 2 additions & 2 deletions packages/labs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
* and https://github.com/palantir/blueprint/blob/master/PATENTS
*/

export * from "./inputList";
export * from "./select";
export * from "./query-list/queryList";
export * from "./select/select";
18 changes: 9 additions & 9 deletions packages/labs/src/labs.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
@# Labs

<div class="pt-callout pt-intent-warning pt-icon-info-sign">
<h5>Under construuction</h5>
<h5>Under construction</h5>
The **[@blueprintjs/labs](https://www.npmjs.com/package/@blueprintjs/labs)** NPM package contains **unstable React components under active development by team members**. It is an incubator and staging area for components as we refine the API design; as such, this package will never reach 1.0.0, and every minor version should be considered breaking.
</div>

Expand Down Expand Up @@ -39,7 +39,7 @@ In TypeScript, `Select<T>` is a *generic component* so you must define a local t

@### Querying

Supply a predicate to automatically query items based on the `InputGroup` value. Use `itemPredicate` to filter each item individually; this is great for lightweight searches. Use `itemListPredicate` to query the entire array in one go, and even reorder it, such as with [fuzz-aldrin-plus](https://github.com/jeancroy/fuzz-aldrin-plus). The array of filtered items is cached internally by `InputList` state and only recomputed when `query` or `items`-related props change.
Supply a predicate to automatically query items based on the `InputGroup` value. Use `itemPredicate` to filter each item individually; this is great for lightweight searches. Use `itemListPredicate` to query the entire array in one go, and even reorder it, such as with [fuzz-aldrin-plus](https://github.com/jeancroy/fuzz-aldrin-plus). The array of filtered items is cached internally by `QueryList` state and only recomputed when `query` or `items`-related props change.

If the query returns no results or `items` is empty, then `noResults` will be rendered in place of the usual list.

Expand Down Expand Up @@ -91,20 +91,20 @@ const renderMenuItem = ({ handleClick, item: film, isActive }: ISelectItemRender

@interface ISelectItemRendererProps

@## InputList
@## QueryList

`InputList<T>` is a higher-order component that provides interactions between a filter input and a list of items. Specifically, it implements the two predicate props and provides keyboard selection. It does not render anything on its own, instead deferring to a `renderer` prop to perform the actual composition of components.
`QueryList<T>` is a higher-order component that provides interactions between a query string and a list of items. Specifically, it implements the two predicate props describe above and provides keyboard selection. It does not render anything on its own, instead deferring to a `renderer` prop to perform the actual composition of components.

`InputList<T>` is a generic component where `<T>` represents the type of one item in the array of `items`. The static method `InputList.ofType<T>()` is available to simplify the TypeScript usage.
`QueryList<T>` is a generic component where `<T>` represents the type of one item in the array of `items`. The static method `QueryList.ofType<T>()` is available to simplify the TypeScript usage.

If the `Select` interactions are not sufficient for your use case, you can use `InputList` directly to render your own components while leveraging basic interactions for keyboard selection and filtering. The `Select` source code is a great place to start when implementing a custom `InputList` `renderer`.
If the `Select` interactions are not sufficient for your use case, you can use `QueryList` directly to render your own components while leveraging basic interactions for keyboard selection and filtering. The `Select` source code is a great place to start when implementing a custom `QueryList` `renderer`.

@interface IInputListProps
@interface IQueryListProps

@### Renderer API

An object with the following properties will be passed to an `InputList` `renderer`. Required properties will always be defined; optional ones will only be defined if they are passed as props to the `InputList`.
An object with the following properties will be passed to an `QueryList` `renderer`. Required properties will always be defined; optional ones will only be defined if they are passed as props to the `QueryList`.

This interface is generic, accepting a type parameter `<T>` for an item in the list.

@interface IInputListRendererProps
@interface IQueryListRendererProps
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export interface IListItemsProps<T> extends IProps {
onItemSelect: (item: T | undefined, event?: React.SyntheticEvent<HTMLElement>) => void;
}

export interface IInputListProps<T> extends IListItemsProps<T> {
export interface IQueryListProps<T> extends IListItemsProps<T> {
/**
* The active item is the current keyboard-focused element.
* Listen to `onActiveItemChange` for updates from interactions.
Expand All @@ -53,24 +53,24 @@ export interface IInputListProps<T> extends IListItemsProps<T> {
onActiveItemChange: (activeItem: T | undefined) => void;

/**
* Callback invoked when user presses a key, after processing `InputList`'s own key events
* (up/down to navigate active item). This callback is passed to `compose` and (along with
* Callback invoked when user presses a key, after processing `QueryList`'s own key events
* (up/down to navigate active item). This callback is passed to `renderer` and (along with
* `onKeyUp`) can be attached to arbitrary content elements to support keyboard selection.
*/
onKeyDown?: React.KeyboardEventHandler<HTMLElement>;

/**
* Callback invoked when user releases a key, after processing `InputList`'s own key events
* (enter to select active item). This callback is passed to `compose` and (along with
* Callback invoked when user releases a key, after processing `QueryList`'s own key events
* (enter to select active item). This callback is passed to `renderer` and (along with
* `onKeyDown`) can be attached to arbitrary content elements to support keyboard selection.
*/
onKeyUp?: React.KeyboardEventHandler<HTMLElement>;

/**
* Customize rendering of the input list.
* Customize rendering of the component.
* Receives an object with props that should be applied to elements as necessary.
*/
renderer: (listProps: IInputListRendererProps<T>) => JSX.Element;
renderer: (listProps: IQueryListRendererProps<T>) => JSX.Element;

/**
* Query string passed to `itemListPredicate` or `itemPredicate` to filter items.
Expand All @@ -80,7 +80,7 @@ export interface IInputListProps<T> extends IListItemsProps<T> {
query: string;
}

export interface IInputListRendererProps<T> extends IProps {
export interface IQueryListRendererProps<T> extends IProps {
/** The item focused by the keyboard (arrow keys). This item should stand out visually from the rest. */
activeItem: T | undefined;

Expand Down Expand Up @@ -116,26 +116,26 @@ export interface IInputListRendererProps<T> extends IProps {

/**
* A ref handler that should be applied to the HTML element that contains the rendererd items.
* This is required for the `InputList` to scroll the active item into view automatically.
* This is required for the `QueryList` to scroll the active item into view automatically.
*/
itemsParentRef: (ref: HTMLElement) => void;

/**
* Controlled text value of the filter input. Attach an `onChange` handler to the relevant
* Controlled query string. Attach an `onChange` handler to the relevant
* element to control this prop from your application's state.
*/
query: string;
}

export interface IInputListState<T> {
export interface IQueryListState<T> {
filteredItems?: T[];
}

export class InputList<T> extends React.Component<IInputListProps<T>, IInputListState<T>> {
public static displayName = "Blueprint.InputList";
export class QueryList<T> extends React.Component<IQueryListProps<T>, IQueryListState<T>> {
public static displayName = "Blueprint.QueryList";

public static ofType<T>() {
return InputList as new (props: IInputListProps<T>) => InputList<T>;
return QueryList as new (props: IQueryListProps<T>) => QueryList<T>;
}

private itemsParentRef: HTMLElement;
Expand Down Expand Up @@ -166,7 +166,7 @@ export class InputList<T> extends React.Component<IInputListProps<T>, IInputList
this.setState({ filteredItems: getFilteredItems(this.props) });
}

public componentWillReceiveProps(nextProps: IInputListProps<T>) {
public componentWillReceiveProps(nextProps: IQueryListProps<T>) {
if (nextProps.items !== this.props.items
|| nextProps.itemListPredicate !== this.props.itemListPredicate
|| nextProps.itemPredicate !== this.props.itemPredicate
Expand Down Expand Up @@ -284,7 +284,7 @@ function pxToNumber(value: string) {
return parseInt(value.slice(0, -2), 10);
}

function getFilteredItems<T>({ items, itemPredicate, itemListPredicate, query }: IInputListProps<T>) {
function getFilteredItems<T>({ items, itemPredicate, itemListPredicate, query }: IQueryListProps<T>) {
if (Utils.isFunction(itemListPredicate)) {
// note that implementations can reorder the items here
return itemListPredicate(query, items);
Expand Down
35 changes: 35 additions & 0 deletions packages/labs/src/select/_select.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2017 Palantir Technologies, Inc. All rights reserved.
* Licensed under the BSD-3 License as modified (the β€œLicense”); you may obtain a copy
* of the license at https://github.com/palantir/blueprint/blob/master/LICENSE
* and https://github.com/palantir/blueprint/blob/master/PATENTS
*/

@import "~@blueprintjs/core/src/common/variables";

$select-popover-max-height: $pt-grid-size * 20 !default;
$select-popover-max-width: $pt-grid-size * 40 !default;

.pt-select-popover {
.pt-popover-content {
// use padding on container rather than margin on input group
// because top margin leaves some empty space with no background color.
padding: $pt-grid-size / 2;
}

.pt-input-group {
margin-bottom: 0;
}

.pt-menu {
max-width: $select-popover-max-width;
max-height: $select-popover-max-height;
overflow: auto;
padding: 0;

&:not(:first-child) {
// adjust padding to account for that on .pt-popover-content above
padding-top: $pt-grid-size / 2;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
Position,
Utils,
} from "@blueprintjs/core";
import { IInputListRendererProps, IListItemsProps, InputList } from "./inputList";
import { IListItemsProps, IQueryListRendererProps, QueryList } from "../query-list/queryList";

export interface ISelectProps<T> extends IListItemsProps<T> {
/**
Expand Down Expand Up @@ -97,38 +97,34 @@ export class Select<T> extends AbstractComponent<ISelectProps<T>, ISelectState<T

public state: ISelectState<T> = { isOpen: false, query: "" };

private TypedInputList = InputList.ofType<T>();
private inputList: InputList<T>;
private TypedQueryList = QueryList.ofType<T>();
private list: QueryList<T>;
private refHandlers = {
inputList: (ref: InputList<T>) => {
this.inputList = ref;
(window as any).inputList = ref;
},
queryList: (ref: QueryList<T>) => this.list = ref,
};
private previousFocusedElement: HTMLElement;

public render() {
// omit props specific to this component, spread the rest.
// TODO: should InputList just support arbitrary props? could be useful for re-rendering
const { filterable, itemRenderer, inputProps, noResults, popoverProps, ...props } = this.props;
return <this.TypedInputList
return <this.TypedQueryList
{...props}
activeItem={this.state.activeItem}
onActiveItemChange={this.handleActiveItemChange}
onItemSelect={this.handleItemSelect}
query={this.state.query}
ref={this.refHandlers.inputList}
renderer={this.renderInputList}
ref={this.refHandlers.queryList}
renderer={this.renderQueryList}
/>;
}

public componentDidUpdate(_prevProps: ISelectProps<T>, prevState: ISelectState<T>) {
if (this.state.isOpen && !prevState.isOpen && this.inputList != null) {
this.inputList.scrollActiveItemIntoView();
if (this.state.isOpen && !prevState.isOpen && this.list != null) {
this.list.scrollActiveItemIntoView();
}
}

private renderInputList = (listProps: IInputListRendererProps<T>) => {
private renderQueryList = (listProps: IQueryListRendererProps<T>) => {
// not using defaultProps cuz they're hard to type with generics (can't use <T> on static members)
const { filterable = true, inputProps = {}, popoverProps = {} } = this.props;

Expand Down Expand Up @@ -176,7 +172,7 @@ export class Select<T> extends AbstractComponent<ISelectProps<T>, ISelectState<T
);
}

private renderItems({ activeItem, filteredItems, handleItemSelect }: IInputListRendererProps<T>) {
private renderItems({ activeItem, filteredItems, handleItemSelect }: IQueryListRendererProps<T>) {
const { itemRenderer, noResults } = this.props;
if (filteredItems.length === 0) {
return noResults;
Expand Down Expand Up @@ -229,8 +225,8 @@ export class Select<T> extends AbstractComponent<ISelectProps<T>, ISelectState<T

private handlePopoverDidOpen = () => {
// scroll active item into view after popover transition completes and all dimensions are stable.
if (this.inputList != null) {
this.inputList.scrollActiveItemIntoView();
if (this.list != null) {
this.list.scrollActiveItemIntoView();
}

const { popoverProps = {} } = this.props;
Expand Down
2 changes: 1 addition & 1 deletion packages/labs/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
* Copyright 2017-present Palantir Technologies, Inc. All rights reserved.
*/

import "./inputListTests";
import "./queryListTests";
import "./selectTests";
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import { shallow } from "enzyme";
import * as React from "react";

import { Film, TOP_100_FILMS } from "../examples/data";
import { IInputListRendererProps, InputList } from "../src/index";
import { IQueryListRendererProps, QueryList } from "../src/index";

describe("<InputList>", () => {
const FilmInputList = InputList.ofType<Film>();
describe("<QueryList>", () => {
const FilmQueryList = QueryList.ofType<Film>();
let props: {
activeItem: Film,
items: Film[],
Expand All @@ -35,26 +35,26 @@ describe("<InputList>", () => {
describe("filtering", () => {
it("itemPredicate filters each item by query", () => {
const predicate = sinon.spy((query: string, film: Film) => film.year === +query);
shallow(<FilmInputList {...props} itemPredicate={predicate} query="1980" />);
shallow(<FilmQueryList {...props} itemPredicate={predicate} query="1980" />);

assert.equal(predicate.callCount, props.items.length, "called once per item");
const { filteredItems } = props.renderer.args[0][0] as IInputListRendererProps<Film>;
const { filteredItems } = props.renderer.args[0][0] as IQueryListRendererProps<Film>;
assert.lengthOf(filteredItems, 2, "returns only films from 1980");
});

it("itemListPredicate filters entire list by query", () => {
const predicate = sinon.spy((query: string, films: Film[]) => films.filter((f) => f.year === +query));
shallow(<FilmInputList {...props} itemListPredicate={predicate} query="1980" />);
shallow(<FilmQueryList {...props} itemListPredicate={predicate} query="1980" />);

assert.equal(predicate.callCount, 1, "called once for entire list");
const { filteredItems } = props.renderer.args[0][0] as IInputListRendererProps<Film>;
const { filteredItems } = props.renderer.args[0][0] as IQueryListRendererProps<Film>;
assert.lengthOf(filteredItems, 2, "returns only films from 1980");
});

it("prefers itemListPredicate if both are defined", () => {
const predicate = sinon.spy(() => true);
const listPredicate = sinon.spy(() => true);
shallow(<FilmInputList
shallow(<FilmQueryList
{...props}
itemPredicate={predicate}
itemListPredicate={listPredicate}
Expand Down