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 & Select components #1159

Merged
merged 30 commits into from
Jun 8, 2017
Merged

[Labs] InputList & Select components #1159

merged 30 commits into from
Jun 8, 2017

Conversation

giladgray
Copy link
Contributor

@giladgray giladgray commented May 30, 2017

Changes proposed in this pull request:

  • 📦 new @blueprintjs/labs package for unstable and in-development components
  • InputList component composes input to filter list of items.
  • Select uses InputList to build basic dropdown chooser functionality.

Reviewers should focus on:

  • API design

Screenshot

image

@blueprint-bot
Copy link

scaffold tests, disable coverage check in labs package

Preview: documentation
Coverage: core | datetime

@@ -3,6 +3,15 @@
*/
"use strict";

const COVERAGE_PERCENT = 80;
const COVERAGE_PERCENT_MAX = 90;
const COVERAGE_CHECK = {
Copy link
Contributor

Choose a reason for hiding this comment

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

this is only used once, I don't think you need to make a const


public getInterface = (name: string) => {
return this.props[name];
// remove generics from end of name
const actualName = /^(\w+)<?/.exec(name)[1];
Copy link
Contributor

Choose a reason for hiding this comment

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

hmm this feels like it should probably be fixed at the ts-quick-docs layer? interfaces have names and type params, which we could preserve in a data object and avoid this regex

Copy link
Contributor Author

Choose a reason for hiding this comment

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

agreed but needed quick fix

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@@ -3,6 +3,15 @@
*/
"use strict";

const COVERAGE_PERCENT = 80;
const COVERAGE_PERCENT_MAX = 90;
Copy link
Contributor

Choose a reason for hiding this comment

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

"max" doesn't sound right semantically; what does istanbul call these numbers?

* and https://github.com/palantir/blueprint/blob/master/PATENTS
*/

// import * as classNames from "classnames";
Copy link
Contributor

Choose a reason for hiding this comment

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

commented code


import { IProps, Keys, Utils } from "@blueprintjs/core";

export interface ICoreInputListProps<D> extends IProps {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this type param should be T... D probably isn't semantically clear enough to warrant a deviation from the naming standard (same for all the other interfaces)

* Customize filtering of entire items array. Return subset of items that pass filter.
* (Some filter algorithms operate on the entire set, rather than individual items.)
*/
itemsFilterer?: (items: D[], query: string) => D[];
Copy link
Contributor

Choose a reason for hiding this comment

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

w/ above suggestion: itemListPredicate

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

public static ofType<T>() { return InputList as new () => InputList<T>; }
Copy link
Contributor

Choose a reason for hiding this comment

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

if it has a return keyword might as well make it multiline...

*/
private shouldCheckActiveItemInViewport: boolean;

constructor(props?: IInputListProps<D>, context?: any) {
Copy link
Contributor

Choose a reason for hiding this comment

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

props isn't optional

inputProps?: IInputGroupProps & HTMLInputProps;

/** React props to spread to `Popover`. Note that `content` cannot be changed. */
popoverProps?: Partial<IPopoverProps>;
Copy link
Contributor

Choose a reason for hiding this comment

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

need Partial<IPopoverProps> & object; currently this type is too permissive

leftIconName="search"
onChange={onQueryChange}
placeholder="Filter..."
rightElement={<InputClearButton onClick={this.clearQuery} visible={query.length > 0} />}
Copy link
Contributor

Choose a reason for hiding this comment

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

meh, this doesn't seem better than inlining the ternary... {query.length > 0 ? <Button .../> : undefined}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it fits on one line and doesn't require a const. analogous to renderItems member below; is that pattern preferable?

Copy link
Contributor

Choose a reason for hiding this comment

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

yeah, I'd prefer to encapsulate it inside this class as a private method. or just an inline expression in this method. const InputClearButton is only used once, so it doesn't make much sense as a separate component

Current components:

- `InputList` is a low-level component for composing an `InputGroup` to filter an array of `items`.
- `Select` is a high-level component for choosing items for a list. It can be used instead of the
Copy link
Contributor

Choose a reason for hiding this comment

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

for choosing items from a list?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes

{ title: "Inglourious Basterds", year: 2009 },
{ title: "Snatch", year: 2000 },
{ title: "3 Idiots", year: 2009 },
{ title: "Monty Python and the Holy Grail", year: 1975 },
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it make sense to select then F5 in VS Code to alphabetize these? Would placate my inner demon.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

no they're actually ordered, top 100 movies from imdb. i should note in code.

Copy link
Contributor Author

@giladgray giladgray May 30, 2017

Choose a reason for hiding this comment

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

renamed const accordingly: TOP_100_FILMS

Copy link
Contributor

Choose a reason for hiding this comment

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

Sounds good.

Copy link
Contributor

Choose a reason for hiding this comment

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

(Resolved)

@blueprint-bot
Copy link

refactor clear button SFC

Preview: documentation | table
Coverage: core | datetime

@cmslewis
Copy link
Contributor

cmslewis commented May 31, 2017

  • FYI there's some z-index weirdness on the docs page:

image

Looks like the navbar border is above the popover, but the popover is above the navigator search box.

@cmslewis
Copy link
Contributor

cmslewis commented May 31, 2017

Couple UX questions:

  • I'd expect to be able to type "10." and pull up the result prefixed with "10," but that doesn't happen at present.
  • Pressing enter to select an item in Select, doesn't close the popover. But I see an explicit this.setState({ isOpen: false }) in the code.

2017-05-31 14 25 24

Gilad Gray added 3 commits May 31, 2017 15:45
ICoreInputListProps => IListItemsProps cuz it’s all about a list of
items.
Remove `onQueryChange` because it’s a renderer implementation detail.
Rename renderer props to `handle*` to indicate they are handler
callbacks, not events.
Gilad Gray added 5 commits June 1, 2017 19:24
Pull activeItem state up to Select; InputList requires controlled props
for it now (and translates item to index).
…repaint

Super awesome for scroll adjustments + no flicker!
@giladgray
Copy link
Contributor Author

resolved all issues by @cmslewis above.

the z-index weirdness was due to top margin on the input group causing the top 5px of popover to have transparent background, so navbar border would show through it 😱 . i had to wiggle some styles to get the padding to work correctly.

@blueprint-bot
Copy link

major docs updates, fix navbar/border bleed issue

Preview: documentation | table
Coverage: core | datetime

@adidahiya
Copy link
Contributor

Looking great so far!

One minor UX request: can we get ESC to close the dropdown when it's open & focused?

* Selection handler that should be invoked when a new item has been chosen,
* perhaps because the user clicked it.
*/
handleItemSelect: (item: T, event: React.SyntheticEvent<HTMLElement>) => void;
Copy link
Contributor

Choose a reason for hiding this comment

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

generally, handlers in props interfaces should be named onItemSelect. is there a reason why this is exempt?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this is not an event listener prop for you to supply but an actual handler function, passed to the renderer callback prop:

// very pseudocode ahead
<InputList renderer=({ handleItemSelect }) => <div onClick={handleItemSelect} />) />

Copy link
Contributor Author

Choose a reason for hiding this comment

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

see the docs note on this interface:

An object with the following properties will be passed to an InputList renderer.

Copy link
Contributor

Choose a reason for hiding this comment

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

cool ok


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

private DInputList = InputList.ofType<T>();
Copy link
Contributor

Choose a reason for hiding this comment

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

missed a rename? StaticInputList maybe? ConcreteInputList?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

oh TInputList. this is neither static nor concrete.

@llorca
Copy link
Contributor

llorca commented Jun 6, 2017

One minor UX request: can we get ESC to close the dropdown when it's open & focused?

Can we make that a Popover feature as opposed to restricting it in Select? Useful in so many places

@giladgray
Copy link
Contributor Author

esc key is already supported, and pressing it closes the popover. what is missing? also, popoverProps={{ canEscapeKeyClose: false }} is already a thing.

@cmslewis
Copy link
Contributor

cmslewis commented Jun 6, 2017

  • Text-overflow issue in this particular Select example:
    image

  • Animation on the minimal style feels like it could use some scaleing or something. Feels mildly incomplete right now.
    2017-06-06 15 32 13

  • If filterable={false}, the top padding is excessive because of the additional 5px top padding on pt-menu; I'd expect it to be 5px all around.
    image

  • Popover doesn't move with the input field when the page is resized.
    2017-06-06 15 27 58

@giladgray
Copy link
Contributor Author

@cmslewis:

  1. text wrapping solved by [Menu] Flexify menu items #1136
  2. not relevant to this PR; minimal animation intentionally has no scale.
  3. known, fixed locally
  4. fuck.

@giladgray
Copy link
Contributor Author

  1. above is also an issue on PopoverExample so def not a regression here. that's part of the larger issue with popovers not staying where they should be.

@llorca
Copy link
Contributor

llorca commented Jun 6, 2017

+1 on the filterable={false} padding problem. The other things aren't related to Select nor InputList.

I also saw a layout issue:
image

{ title: "Inglourious Basterds", year: 2009 },
{ title: "Snatch", year: 2000 },
{ title: "3 Idiots", year: 2009 },
{ title: "Monty Python and the Holy Grail", year: 1975 },
Copy link
Contributor

Choose a reason for hiding this comment

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

(Resolved)

import { Select } from "../src";
import { Film, TOP_100_FILMS } from "./data";

const MovieSelect = Select.ofType<Film>();
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: FilmSelect for consistency of terminology?

* and https://github.com/palantir/blueprint/blob/master/PATENTS
*/


Copy link
Contributor

Choose a reason for hiding this comment

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

nit (lintable?): delete double newline?

if (nextProps.items !== this.props.items
|| nextProps.itemListPredicate !== this.props.itemListPredicate
|| nextProps.itemPredicate !== this.props.itemPredicate
|| nextProps.query !== this.props.query
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's use Utils.shallowCompareKeys eventually, or now if you already have it on this branch.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

that function is only in table right? don't want to make that refactor now...

Copy link
Contributor

Choose a reason for hiding this comment

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

Good point. 😛


function getFilteredItems<T>({ items, itemPredicate, itemListPredicate, query }: IInputListProps<T>) {
if (Utils.isFunction(itemListPredicate)) {
// TODO: this can reorder the items, which can cause weirdness while filtering. do some design.
Copy link
Contributor

Choose a reason for hiding this comment

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

TODO in this PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

these are questions for the future

Copy link
Contributor Author

Choose a reason for hiding this comment

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

turns out this one is fine--the reordering is useful and the component handles it well.

/**
* Customize filtering of entire items array. Return subset of items that pass filter.
* (Some filter algorithms operate on the entire set, rather than individual items.)
*/
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we clarify how these two props (itemPredicate and itemListPredicate) work together if they're both defined? In that case, is an error thrown, is one of these props ignored, etc. (I see from the code that itemListPredicate takes precedence.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

there is a section in the docs

Choose a reason for hiding this comment

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

I think that it would be nice if we can provide a warning that says something along the lines of "you've provided both itemListPredicate and itemPredicate, itemPredicate will be ignored" or something.


public render() {
// omit props specific to this component, spread the rest.
// TODO: should InputList just support arbitrary props? could be useful for re-rendering
Copy link
Contributor

Choose a reason for hiding this comment

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

TODO in this PR? if not, can we file an issue and add a link here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

what do you think about the question? should it just support [key: string]: any; so you can spread arbitrary things?

private renderItems({ activeItem, filteredItems, handleItemSelect }: IInputListRendererProps<T>) {
const { itemRenderer, noResults } = this.props;
return filteredItems.length > 0
? filteredItems.map((item) => itemRenderer(item, item === activeItem, (e) => handleItemSelect(item, e)))
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it up to the consumer to add a key to each item? Should we mention that in the docs?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

i can add a note in prop docs. but also React will tell you.

@adidahiya
Copy link
Contributor

feedback here seems pretty minor; let's get a rough cut of this out and fixup small things later

Copy link

@suchanlee suchanlee left a comment

Choose a reason for hiding this comment

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

looks awesome!

- `InputList` is a low-level component for composing an `InputGroup` to filter an array of `items`.
- `Select` is a high-level component for choosing items from a list. It can be used instead of the
HTML `<select>` element.
- `Suggest` is a high-level component for an `InputGroup` that suggests completions based on its

Choose a reason for hiding this comment

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

Is this not part of this PR?

Copy link
Contributor

Choose a reason for hiding this comment

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

Not yet, we're building that next

onItemSelect={this.handleValueChange}
popoverProps={{ popoverClassName: minimal ? Classes.MINIMAL : "" }}
>
<Button text={film ? film.title : "(No selection)"} rightIconName="double-caret-vertical" />

Choose a reason for hiding this comment

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

nit: recommend 1 prop per line if there are > 1 props, for better diffing

Copy link
Contributor Author

Choose a reason for hiding this comment

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

i do it based on line length typically but i hear that

"style": "dist/blueprint-labs.css",
"unpkg": "dist/labs.bundle.js",
"dependencies": {
"@blueprintjs/core": "^1.18.0",

Choose a reason for hiding this comment

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

(not knowing how blueprint dependencies are normally packaged) Would it be better to keep blueprintjs/core as peerDependency so that there aren't version conflicts with applications that already consume blueprint, but at a different version?

Copy link
Contributor

@adidahiya adidahiya Jun 7, 2017

Choose a reason for hiding this comment

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

nope, peer dependencies are annoying in general and should be avoided. there's no harm to having multiple versions of blueprint in your node_modules tree, and everyone should be setting semver ranges appropriately so that they rarely get into that situation anyway. since we ship a single global CSS file, it's pretty hard to run into conflicts within a single app (your bundler will only pick up node_modules/@blueprinjs/core/dist/blueprint.css)

/**
* Customize filtering of entire items array. Return subset of items that pass filter.
* (Some filter algorithms operate on the entire set, rather than individual items.)
*/

Choose a reason for hiding this comment

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

I think that it would be nice if we can provide a warning that says something along the lines of "you've provided both itemListPredicate and itemPredicate, itemPredicate will be ignored" or something.

* The active item is the current keyboard-focused element.
* Listen to `onActiveItemChange` for updates from interactions.
*/
activeItem: T;

Choose a reason for hiding this comment

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

should this be T | undefined, since if filtered items = [], there would be no active item? same for onActiveItemChange below


private getActiveIndex() {
// NOTE: this operation is O(n) so it should be avoided in render(). safe for events though.
return this.state.filteredItems.indexOf(this.props.activeItem);

Choose a reason for hiding this comment

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

nit: this function gets called multiple times every time the user presses up/down and may lead to a laggy experience if the filteredItems size is large and the user holds up/down. May be able to make this faster with something like an ordered map but probably not worth the trouble if there are plans to virtualize this. Actually, if the number of items is large, the rendering perf may dwarf whatever perf issue this may result in...

Copy link
Contributor

Choose a reason for hiding this comment

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

worth consideration: a simple @debounce

itemRenderer: (item: T, isActive: boolean, onClick: React.MouseEventHandler<HTMLElement>) => JSX.Element;

/** React child to render when filtering items returns zero results. */
noResults?: string | JSX.Element;

Choose a reason for hiding this comment

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

thoughts on changing this to nonIdealState? I think that this could be used for displaying a spinner if the user decides to use server-side search/pagination on input change

Copy link
Contributor

Choose a reason for hiding this comment

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

Let's keep it as is for now, some apps have complex empty states that I need to review again to make sure NonIdealState is always appropriate. (I suppose the current JSX.Element already allows you to provide your own NonIdealState 👍)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

using an actual NonIdealState in the menu might be too much UI for a small space? i prefer this name, as the semantics are clear but what you put there is up to you.

Choose a reason for hiding this comment

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

an actual NonIdealState is definitely too big for this, but can totally see it being used for a "non ideal state". I think the name noResults might be a bit limiting but up to you

Copy link
Contributor Author

Choose a reason for hiding this comment

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

but this prop is specifically rendered when there are no results: empty items array or query returns empty array (same thing really).

Choose a reason for hiding this comment

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

hmm i suppose you're right

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh I misunderstood, I thought you were talking about the typing. Up to you guys on the prop naming, we can always change it later


export interface ISelectState<T> {
activeItem?: T;
isOpen?: boolean;

Choose a reason for hiding this comment

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

isOpen and query are never undefined. Should they be optional? or is it the blueprint's style to make all state fields optional?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

blueprint style cuz we use old react typings (pre-Pick)

// 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;
const { handleKeyDown, handleKeyUp, query } = listProps;

Choose a reason for hiding this comment

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

nit: unnecessary newline?

}
}

public scrollActiveItemIntoView() {

Choose a reason for hiding this comment

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

seems like a good candidate for taking out into a reusable function/component

Copy link
Contributor Author

Choose a reason for hiding this comment

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

i mean it is in a reusable component... InputList doesn't do much else beyond keyboard selection

@suchanlee
Copy link

Any thoughts about supporting debouncing the list render for when the query changes? re-rendering a lot of items, for every query change can be expensive and can lead to a laggy experience

* this item is active (selected by keyboard arrows) and an `onClick` event handler that
* should be attached to the returned element.
*/
itemRenderer: (item: T, isActive: boolean, onClick: React.MouseEventHandler<HTMLElement>) => JSX.Element;

Choose a reason for hiding this comment

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

thoughts on including the item index as well? Can support use cases like rendering N items

Copy link
Contributor Author

Choose a reason for hiding this comment

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

might as well move to object argument like renderer: (props) => JSX.Element

Copy link

@suchanlee suchanlee Jun 7, 2017

Choose a reason for hiding this comment

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

IMO that would work better if you include the index

Copy link
Contributor

@cmslewis cmslewis left a comment

Choose a reason for hiding this comment

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

Looks good for a first pass! We can keep working on things of course. <3 Labs.

@blueprint-bot
Copy link

fix tests

Preview: documentation | table
Coverage: core | datetime

@giladgray giladgray merged commit ce6e6f9 into master Jun 8, 2017
@giladgray giladgray deleted the gg/labs branch June 8, 2017 00:32
This was referenced Jun 8, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants