Skip to content

Commit

Permalink
[select] feat(MultiSelect2): 'onClear' prop renders clear button (#5361)
Browse files Browse the repository at this point in the history
  • Loading branch information
adidahiya authored Jun 7, 2022
1 parent b6ea71e commit f07320d
Show file tree
Hide file tree
Showing 2 changed files with 113 additions and 43 deletions.
101 changes: 71 additions & 30 deletions packages/docs-app/src/examples/select-examples/multiSelectExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@

import * as React from "react";

import { Button, H5, Intent, MenuItem, Switch, TagProps } from "@blueprintjs/core";
import { Code, H5, Intent, MenuItem, Switch, TagProps } from "@blueprintjs/core";
import { Example, IExampleProps } from "@blueprintjs/docs-theme";
import { Popover2 } from "@blueprintjs/popover2";
import { Popover2, Tooltip2 } from "@blueprintjs/popover2";
import { ItemRenderer, MultiSelect2 } from "@blueprintjs/select";

import {
Expand Down Expand Up @@ -50,6 +50,7 @@ export interface IMultiSelectExampleState {
openOnKeyDown: boolean;
popoverMinimal: boolean;
resetOnSelect: boolean;
showClearButton: boolean;
tagMinimal: boolean;
}

Expand All @@ -67,6 +68,7 @@ export class MultiSelectExample extends React.PureComponent<IExampleProps, IMult
openOnKeyDown: false,
popoverMinimal: true,
resetOnSelect: true,
showClearButton: true,
tagMinimal: false,
};

Expand All @@ -90,6 +92,8 @@ export class MultiSelectExample extends React.PureComponent<IExampleProps, IMult

private handleResetChange = this.handleSwitchChange("resetOnSelect");

private handleShowClearButtonChange = this.handleSwitchChange("showClearButton");

private handleTagMinimalChange = this.handleSwitchChange("tagMinimal");

public render() {
Expand All @@ -107,11 +111,6 @@ export class MultiSelectExample extends React.PureComponent<IExampleProps, IMult
const maybeCreateNewItemFromQuery = allowCreate ? createFilm : undefined;
const maybeCreateNewItemRenderer = allowCreate ? renderCreateFilmOption : null;

const clearButton =
films.length > 0 ? (
<Button disabled={flags.disabled} icon="cross" minimal={true} onClick={this.handleClear} />
) : undefined;

return (
<Example options={this.renderOptions()} {...this.props}>
<FilmMultiSelect
Expand All @@ -126,14 +125,14 @@ export class MultiSelectExample extends React.PureComponent<IExampleProps, IMult
// adding newly created items to the list, so pass our own
items={this.state.items}
noResults={<MenuItem disabled={true} text="No results." />}
onClear={this.state.showClearButton ? this.handleClear : undefined}
onItemSelect={this.handleFilmSelect}
onItemsPaste={this.handleFilmsPaste}
popoverProps={{ matchTargetWidth, minimal: popoverMinimal }}
popoverRef={this.popoverRef}
tagRenderer={this.renderTag}
tagInputProps={{
onRemove: this.handleTagRemove,
rightElement: clearButton,
tagProps: getTagProps,
}}
selectedItems={this.state.films}
Expand Down Expand Up @@ -161,14 +160,42 @@ export class MultiSelectExample extends React.PureComponent<IExampleProps, IMult
checked={this.state.hasInitialContent}
onChange={this.handleInitialContentChange}
/>
<Switch
label="Allow creating new films"
checked={this.state.allowCreate}
onChange={this.handleAllowCreateChange}
/>
<Tooltip2
content={
<>
<Code>createNewItemFromQuery</Code> and <Code>createNewItemRenderer</Code> are{" "}
{this.state.allowCreate ? "defined" : "undefined"}
</>
}
placement="left"
>
<Switch
label="Allow creating new films"
checked={this.state.allowCreate}
onChange={this.handleAllowCreateChange}
/>
</Tooltip2>
<Tooltip2
content={
<>
<Code>onClear</Code> is {this.state.showClearButton ? "defined" : "undefined"}
</>
}
placement="left"
>
<Switch
label="Show clear button"
checked={this.state.showClearButton}
onChange={this.handleShowClearButtonChange}
/>
</Tooltip2>
<H5>Appearance props</H5>
<Switch label="Disabled" checked={this.state.disabled} onChange={this.handleDisabledChange} />
<Switch label="Fill container width" checked={this.state.fill} onChange={this.handleFillChange} />
<Tooltip2 content={<Code>disabled=&#123;{this.state.disabled.toString()}&#125;</Code>} placement="left">
<Switch label="Disabled" checked={this.state.disabled} onChange={this.handleDisabledChange} />
</Tooltip2>
<Tooltip2 content={<Code>fill=&#123;{this.state.fill.toString()}&#125;</Code>} placement="left">
<Switch label="Fill container width" checked={this.state.fill} onChange={this.handleFillChange} />
</Tooltip2>
<H5>Tag props</H5>
<Switch
label="Minimal tag style"
Expand All @@ -181,16 +208,35 @@ export class MultiSelectExample extends React.PureComponent<IExampleProps, IMult
onChange={this.handleIntentChange}
/>
<H5>Popover props</H5>
<Switch
label="Match target width"
checked={this.state.matchTargetWidth}
onChange={this.handleMatchTargetWidthChange}
/>
<Switch
label="Minimal popover style"
checked={this.state.popoverMinimal}
onChange={this.handlePopoverMinimalChange}
/>
<Tooltip2
content={
<Code>
popoverProps=&#123;&#123; matchTargetWidth: {this.state.matchTargetWidth.toString()}{" "}
&#125;&#125;
</Code>
}
placement="left"
>
<Switch
label="Match target width"
checked={this.state.matchTargetWidth}
onChange={this.handleMatchTargetWidthChange}
/>
</Tooltip2>
<Tooltip2
content={
<Code>
popoverProps=&#123;&#123; minimal: {this.state.popoverMinimal.toString()} &#125;&#125;
</Code>
}
placement="left"
>
<Switch
label="Minimal popover style"
checked={this.state.popoverMinimal}
onChange={this.handlePopoverMinimalChange}
/>
</Tooltip2>
</>
);
}
Expand Down Expand Up @@ -296,10 +342,5 @@ export class MultiSelectExample extends React.PureComponent<IExampleProps, IMult

private handleClear = () => {
this.setState({ films: [] });
// N.B. if MultiSelect2 had a "clear" button API provided out of the box, we wouldn't have to
// reach in to grab the Popover2 ref to reposition it... until then, we should do this to match
// the behavior which happens during TagInput's onRemove callback.
// see https://popper.js.org/docs/v2/modifiers/event-listeners/#when-the-reference-element-moves-or-changes-size
this.popoverRef.current?.reposition();
};
}
55 changes: 42 additions & 13 deletions packages/select/src/components/multi-select/multiSelect2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import * as React from "react";

import {
AbstractPureComponent2,
Button,
Classes as CoreClasses,
DISPLAYNAME_PREFIX,
Keys,
Expand Down Expand Up @@ -51,6 +52,12 @@ export interface MultiSelect2Props<T> extends IListItemsProps<T>, SelectPopoverP
*/
fill?: boolean;

/**
* If provided, this component will render a "clear" button inside its TagInput.
* Clicking that button will invoke this callback to clear all items from the current selection.
*/
onClear?: () => void;

/**
* Callback invoked when an item is removed from the selection by
* removing its tag in the TagInput. This is generally more useful than
Expand Down Expand Up @@ -93,10 +100,13 @@ export interface MultiSelect2Props<T> extends IListItemsProps<T>, SelectPopoverP
/**
* Props to spread to `TagInput`.
* If you wish to control the value of the input, use `query` and `onQueryChange` instead.
* Note that you are responsible for disabling any elements you may render in `tagInputProps.rightElement`
* when the overall `MultiSelect2` is disabled.
*
* Notes for `tagInputProps.rightElement`:
* - you are responsible for disabling any elements you may render here when the overall
* `MultiSelect2` is disabled.
* - if the `onClear` prop is defined, this element will override/replace the default rightElement,
* which is a "clear" button that removes all items from the current selection.
*/
// eslint-disable-next-line @typescript-eslint/ban-types
tagInputProps?: Partial<TagInputProps>;

/** Custom renderer to transform an item into tag content. */
Expand Down Expand Up @@ -148,6 +158,12 @@ export class MultiSelect2<T> extends AbstractPureComponent2<MultiSelect2Props<T>
this.refHandlers.input = refHandler(this, "input", this.props.tagInputProps?.inputRef);
setRef(this.props.tagInputProps?.inputRef, this.input);
}
if (
(prevProps.onClear === undefined && this.props.onClear !== undefined) ||
(prevProps.onClear !== undefined && this.props.onClear === undefined)
) {
this.forceUpdate();
}
}

public render() {
Expand Down Expand Up @@ -214,12 +230,13 @@ export class MultiSelect2<T> extends AbstractPureComponent2<MultiSelect2Props<T>
const {
disabled,
fill,
tagInputProps = {},
selectedItems = [],
onClear,
placeholder,
popoverTargetProps = {},
selectedItems = [],
tagInputProps = {},
} = this.props;
const { handlePaste, handleKeyDown, handleKeyUp } = listProps;
const { handleKeyDown, handleKeyUp } = listProps;

if (disabled) {
tagInputProps.disabled = true;
Expand All @@ -234,11 +251,11 @@ export class MultiSelect2<T> extends AbstractPureComponent2<MultiSelect2Props<T>
className: classNames(tagInputProps.inputProps?.className, Classes.MULTISELECT_TAG_INPUT_INPUT),
};

const handleTagInputAdd = (values: any[], method: TagInputAddMethod) => {
if (method === "paste") {
handlePaste(values);
}
};
const maybeClearButton =
onClear !== undefined && selectedItems.length > 0 ? (
<Button disabled={disabled} icon="cross" minimal={true} onClick={this.handleClearButtonClick} />
) : undefined;

return (
<div
aria-controls={this.listboxId}
Expand All @@ -259,13 +276,13 @@ export class MultiSelect2<T> extends AbstractPureComponent2<MultiSelect2Props<T>
>
<TagInput
placeholder={placeholder}
rightElement={maybeClearButton}
{...tagInputProps}
className={classNames(Classes.MULTISELECT, tagInputProps.className)}
inputRef={this.refHandlers.input}
inputProps={inputProps}
inputValue={listProps.query}
/* eslint-disable-next-line react/jsx-no-bind */
onAdd={handleTagInputAdd}
onAdd={this.getTagInputAddHandler(listProps)}
onInputChange={listProps.handleQueryChange}
onRemove={this.handleTagRemove}
values={selectedItems.map(this.props.tagRenderer)}
Expand Down Expand Up @@ -319,6 +336,13 @@ export class MultiSelect2<T> extends AbstractPureComponent2<MultiSelect2Props<T>
this.refHandlers.popover.current?.reposition(); // reposition when size of input changes
};

private getTagInputAddHandler =
(listProps: IQueryListRendererProps<T>) => (values: any[], method: TagInputAddMethod) => {
if (method === "paste") {
listProps.handlePaste(values);
}
};

private getTagInputKeyDownHandler = (handleQueryListKeyDown: React.KeyboardEventHandler<HTMLElement>) => {
return (e: React.KeyboardEvent<HTMLElement>) => {
// HACKHACK: https://github.com/palantir/blueprint/issues/4165
Expand Down Expand Up @@ -355,4 +379,9 @@ export class MultiSelect2<T> extends AbstractPureComponent2<MultiSelect2Props<T>
}
};
};

private handleClearButtonClick = () => {
this.props.onClear?.();
this.refHandlers.popover.current?.reposition(); // reposition when size of input changes
};
}

1 comment on commit f07320d

@blueprint-bot
Copy link

Choose a reason for hiding this comment

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

[select] feat(MultiSelect2): 'onClear' prop renders clear button (#5361)

Previews: documentation | landing | table | demo

Please sign in to comment.