-
Notifications
You must be signed in to change notification settings - Fork 2.2k
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
[Select] Create-able Select components #3306
Conversation
FLUPs:
|
@giladgray can you give some feedback on the API before I start with tests and documentation? |
* This is invoked when user interaction causes a new item to be created, either by pressing the `enter` key or | ||
* by clicking on the "Create Item" option. It transforms a query string into an item type. | ||
*/ | ||
createItemFromQuery?: (query: string) => T; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
API mostly looks good but can we be a little more explicit and say createNewItemFromQuery
/ createNewItemRenderer
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ack
*/ | ||
createItemRenderer?: ( | ||
query: string, | ||
active: boolean, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
when is this active
param useful?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is for when the "create" item should be highlighted; for example, if you interact with the dropdown with the up/down key, when your selection is on the create item, active
would be true
, and the item should be rendered accordingly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what else could you select in this case? there would be no other results to select, right? so it should be automatically "active"? I think the param is probably required for full customization of rendering, I'm not rejecting it, but maybe you could post a gif of that interaction?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And to the point of "full customization of rendering" - the consumers provide code for rendering the "create" item just like they provide code for rendering other items, so to some extent this rendering is always custom
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be isActive
, or is active
the norm (like disabled
)?
@adidahiya cleaned up tests and docs, this is ready for another look/merging |
Merge branch 'develop' into sl/1710-create-able-multiselectPreviews: documentation | landing | table |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@shuyangli Great stuff - I want this desperately! 🎉 Left some thoughts.
- Also, can we add an "Allow creating new items" toggle to the Suggest example?
@@ -56,6 +59,8 @@ export class MultiSelectExample extends React.PureComponent<IExampleProps, IMult | |||
// explicit undefined (not null) for default behavior (show full list) | |||
undefined | |||
); | |||
const maybeCreateNewItemFromQuery = this.state.allowCreate ? createFilm : undefined; | |||
const maybeCreateNewItemRenderer = this.state.allowCreate ? this.renderCreateFilmOption : null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit; Unpack this on L50 since you're accessing it twice.
const { allowCreate, films, hasInitialContent, tagMinimal, popoverMinimal, ...flags } = this.state;
@@ -72,6 +77,8 @@ export class MultiSelectExample extends React.PureComponent<IExampleProps, IMult | |||
tagRenderer={this.renderTag} | |||
tagInputProps={{ tagProps: getTagProps, onRemove: this.handleTagRemove, rightElement: clearButton }} | |||
selectedItems={this.state.films} | |||
createNewItemFromQuery={maybeCreateNewItemFromQuery} | |||
createNewItemRenderer={maybeCreateNewItemRenderer} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Alphabetize props.
@@ -55,12 +58,16 @@ export class SelectExample extends React.PureComponent<IExampleProps, ISelectExa | |||
) : ( | |||
undefined | |||
); | |||
const maybeCreateNewItemFromQuery = this.state.allowCreate ? createFilm : undefined; | |||
const maybeCreateNewItemRenderer = this.state.allowCreate ? this.renderCreateFilmOption : null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same.
} | ||
|
||
export function getActiveItem<T>(activeItem: IQueryListActiveItem<T> | null | undefined): T | null { | ||
if (activeItem && activeItem.type === QueryListActiveItemType.ITEM) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prefer explicit boolean expressions for clarity and to generally avoid possibly unintended ignoring of falsey values (like ""
, []
). activeItem != null
.
Also, I'd just use a ternary here.
return activeItem != null && activeItem.type === QueryListActiveItemType.ITEM
? activeItem.item
: null;
@@ -28,7 +28,7 @@ export interface IListItemsProps<T> extends IProps { | |||
* uncontrolled (managed by the component's state). Use `onActiveItemChange` | |||
* to listen for updates. | |||
*/ | |||
activeItem?: T | null; | |||
activeItem?: IQueryListActiveItem<T> | null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a breaking change, no? Also, I don't love this API. Is there a way for us to just accept T | null
, or T | IQueryListCreateItem<T> | null
, or something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, this is a break and we can't merge as-is. I don't see a way to make this non-breaking if we try to adjust the existing prop types (we can make the addition to activeItem
non-breaking but not onActiveItemChange
)... so we'll have to add new optional props for this behavior. we could consider refactoring them to make the API more streamlined in the next major version, but for now I propose:
isCreateNewItemActive: boolean;
onCreateNewItemActiveChange: () => null;
users will have to reconcile for themselves what happens between onActiveItemChange
and onCreateNewItemActiveChange
private getActiveElement() { | ||
if (this.itemsParentRef != null) { | ||
return this.itemsParentRef.children.item(this.getActiveIndex()) as HTMLElement; | ||
if (this.state.activeItem && this.state.activeItem.type === QueryListActiveItemType.CREATE) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Unpack
- Explicit boolean expression
private getNextActiveItem(direction: number, startIndex = this.getActiveIndex()): T | null { | ||
private getNextActiveItem(direction: number, startIndex = this.getActiveIndex()): IQueryListActiveItem<T> | null { | ||
if (this.isCreateItemRendered()) { | ||
const reachedCreate = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Improve this boolean variable name. Maybe willSelectCreateItem
?
@@ -341,6 +398,10 @@ export class QueryList<T> extends React.Component<IQueryListProps<T>, IQueryList | |||
} | |||
Utils.safeInvoke(this.props.onActiveItemChange, activeItem); | |||
} | |||
|
|||
private isCreateItemRendered(): boolean { | |||
return !!(this.props.createNewItemFromQuery && this.props.createNewItemRenderer && this.state.query !== ""); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Use explicit boolean expressions for existence checks.
- Implement
validateProps
to assert (in dev mode at least) that if one of these is provided, both must be provided. Otherwise the create item will just mysteriously never appear.- Add unit tests verifying the above.
text={`Create "${query}"`} | ||
active={active} | ||
onClick={handleClick} | ||
shouldDismissPopover={false} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might as well alphabetize these IMO.
itemPredicate={Films.itemPredicate} | ||
itemRenderer={Films.itemRenderer} | ||
noResults={<MenuItem disabled={true} text="No results." />} | ||
createNewItemFromQuery={createFilm} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same.
@@ -28,7 +28,7 @@ export interface IListItemsProps<T> extends IProps { | |||
* uncontrolled (managed by the component's state). Use `onActiveItemChange` | |||
* to listen for updates. | |||
*/ | |||
activeItem?: T | null; | |||
activeItem?: IQueryListActiveItem<T> | null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, this is a break and we can't merge as-is. I don't see a way to make this non-breaking if we try to adjust the existing prop types (we can make the addition to activeItem
non-breaking but not onActiveItemChange
)... so we'll have to add new optional props for this behavior. we could consider refactoring them to make the API more streamlined in the next major version, but for now I propose:
isCreateNewItemActive: boolean;
onCreateNewItemActiveChange: () => null;
users will have to reconcile for themselves what happens between onActiveItemChange
and onCreateNewItemActiveChange
@adidahiya @shuyangli See my second attempt in this separate PR: #3381. It avoids breaking the API, and it also avoids bloating the props interface with any other fields. |
Closing in favor of #3381 for backcompat-preserving API |
Fixes #1710
Changes proposed in this pull request:
IListItemsProps
, now takes two additional props:createNewItemFromQuery?: (query: string) => T
which converts a query to a new item; if this prop is present, the select component is considered "create-able";createNewItemRenderer?: (query: string, handleClick: React.MouseEventHandler<HTMLElement>) => JSX.Element | undefined
which could be used to render a customMenuItem
at the end of the list, that represents "create a new element from query".createNewItemRenderer
, and the user can activate it to create a new item with the current query.