diff --git a/common/changes/@uifabric/experiments/amyngu-extendedPickerFixes_2018-02-12-22-44.json b/common/changes/@uifabric/experiments/amyngu-extendedPickerFixes_2018-02-12-22-44.json new file mode 100644 index 0000000000000..5e73f2205e2cf --- /dev/null +++ b/common/changes/@uifabric/experiments/amyngu-extendedPickerFixes_2018-02-12-22-44.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@uifabric/experiments", + "comment": "BaseExtendedPicker: Create component to wrap the rendered item, so users get contextual menu if certain props are present, get rid of loading state, fix autofocus on input after suggestion selection", + "type": "minor" + } + ], + "packageName": "@uifabric/experiments", + "email": "amyngu@microsoft.com" +} \ No newline at end of file diff --git a/packages/experiments/src/components/ExtendedPicker/BaseExtendedPicker.tsx b/packages/experiments/src/components/ExtendedPicker/BaseExtendedPicker.tsx index fda64e3fdbe45..3430f31ece89e 100644 --- a/packages/experiments/src/components/ExtendedPicker/BaseExtendedPicker.tsx +++ b/packages/experiments/src/components/ExtendedPicker/BaseExtendedPicker.tsx @@ -248,10 +248,11 @@ export class BaseExtendedPicker> extend this.input.clear(); this.floatingPicker.hidePicker(); + this.focus(); } @autobind protected _onSelectedItemsChanged(): void { - this.input.focus(); + this.focus(); } } \ No newline at end of file diff --git a/packages/experiments/src/components/ExtendedPicker/examples/ExtendedPeoplePicker.Basic.Example.tsx b/packages/experiments/src/components/ExtendedPicker/examples/ExtendedPeoplePicker.Basic.Example.tsx index 36cdd40ed5eb9..cd13f19e5e7a2 100644 --- a/packages/experiments/src/components/ExtendedPicker/examples/ExtendedPeoplePicker.Basic.Example.tsx +++ b/packages/experiments/src/components/ExtendedPicker/examples/ExtendedPeoplePicker.Basic.Example.tsx @@ -164,15 +164,15 @@ export class ExtendedPeoplePickerTypesExample extends BaseComponent<{}, IPeopleP } @autobind - private _onFilterChanged(filterText: string, currentPersonas: IPersonaProps[], limitResults?: number): IPersonaProps[] { + private _onFilterChanged(filterText: string, currentPersonas: IPersonaProps[], limitResults?: number): Promise | null { if (filterText) { let filteredPersonas: IPersonaProps[] = this._filterPersonasByText(filterText); filteredPersonas = this._removeDuplicates(filteredPersonas, currentPersonas); filteredPersonas = limitResults ? filteredPersonas.splice(0, limitResults) : filteredPersonas; - return filteredPersonas; + return this._convertResultsToPromise(filteredPersonas); } else { - return []; + return this._convertResultsToPromise([]); } } @@ -180,7 +180,7 @@ export class ExtendedPeoplePickerTypesExample extends BaseComponent<{}, IPeopleP private _returnMostRecentlyUsed(currentPersonas: IPersonaProps[]): IPersonaProps[] | Promise { let { mostRecentlyUsed } = this.state; mostRecentlyUsed = this._removeDuplicates(mostRecentlyUsed, this._picker.items); - return mostRecentlyUsed; + return this._convertResultsToPromise(mostRecentlyUsed); } private _onCopyItems(items: IExtendedPersonaProps[]): string { @@ -225,6 +225,11 @@ export class ExtendedPeoplePickerTypesExample extends BaseComponent<{}, IPeopleP return persona.primaryText as string; } + private _convertResultsToPromise(results: IPersonaProps[]): Promise { + // tslint:disable-next-line:no-any + return new Promise((resolve: any, reject: any) => setTimeout(() => resolve(results), 150)); + } + @autobind private _validateInput(input: string): boolean { if (input.indexOf('@') !== -1) { diff --git a/packages/experiments/src/components/FloatingPicker/BaseFloatingPicker.tsx b/packages/experiments/src/components/FloatingPicker/BaseFloatingPicker.tsx index d682d8dc5dd57..b30e4fd5bd6e5 100644 --- a/packages/experiments/src/components/FloatingPicker/BaseFloatingPicker.tsx +++ b/packages/experiments/src/components/FloatingPicker/BaseFloatingPicker.tsx @@ -88,7 +88,6 @@ export class BaseFloatingPicker> extend } else { this.updateValue(queryString); } - } } @@ -103,10 +102,13 @@ export class BaseFloatingPicker> extend suggestionsVisible: true, }); - if (this.state.queryString === '') { - this.updateSuggestionWithZeroState(); - } else { - this.updateValue(this.state.queryString); + if (this.suggestionStore.suggestions.length === 0 + || this.props.inputElement && this.props.inputElement.textContent !== this.state.queryString) { + if (this.state.queryString === '') { + this.updateSuggestionWithZeroState(); + } else { + this.updateValue(this.state.queryString); + } } } @@ -255,18 +257,9 @@ export class BaseFloatingPicker> extend this.suggestionStore.updateSuggestions(suggestionsArray, 0); } } else if (suggestionsPromiseLike && suggestionsPromiseLike.then) { - if (!this.loadingTimer) { - this.loadingTimer = this._async.setTimeout( - () => - this.setState({ - suggestionsLoading: true - }), - 500 - ); - } - - // Clear suggestions - this.suggestionStore.updateSuggestions([]); + this.setState({ + suggestionsLoading: true + }); if (updatedValue !== undefined) { this.setState({ @@ -289,6 +282,7 @@ export class BaseFloatingPicker> extend } else { this.suggestionStore.updateSuggestions(newSuggestions); this.setState({ + suggestionsVisible: newSuggestions.length > 0, suggestionsLoading: false }); } @@ -483,9 +477,11 @@ export class BaseFloatingPicker> extend } private _onResolveSuggestions(updatedValue: string): void { - let suggestions: T[] | PromiseLike = this.props.onResolveSuggestions(updatedValue, this.props.selectedItems); + let suggestions: T[] | PromiseLike | null = this.props.onResolveSuggestions(updatedValue, this.props.selectedItems); - this.updateSuggestionsList(suggestions, updatedValue); + if (suggestions !== null) { + this.updateSuggestionsList(suggestions, updatedValue); + } } @autobind diff --git a/packages/experiments/src/components/FloatingPicker/BaseFloatingPicker.types.ts b/packages/experiments/src/components/FloatingPicker/BaseFloatingPicker.types.ts index 7261231864356..e12e929569c8d 100644 --- a/packages/experiments/src/components/FloatingPicker/BaseFloatingPicker.types.ts +++ b/packages/experiments/src/components/FloatingPicker/BaseFloatingPicker.types.ts @@ -46,7 +46,7 @@ export interface IBaseFloatingPickerProps extends React.Props { * Returns the already selected items so the resolver can filter them out. * If used in conjunction with resolveDelay this will ony kick off after the delay throttle. */ - onResolveSuggestions: (filter: string, selectedItems?: T[]) => T[] | PromiseLike; + onResolveSuggestions: (filter: string, selectedItems?: T[]) => T[] | PromiseLike | null; /** * A callback for when the input has been changed diff --git a/packages/experiments/src/components/SelectedItemsList/SelectedPeopleList/Items/EditingItem.scss b/packages/experiments/src/components/SelectedItemsList/SelectedPeopleList/Items/EditingItem.scss index b2332b1861a07..c2b4d2dbd7240 100644 --- a/packages/experiments/src/components/SelectedItemsList/SelectedPeopleList/Items/EditingItem.scss +++ b/packages/experiments/src/components/SelectedItemsList/SelectedPeopleList/Items/EditingItem.scss @@ -1,8 +1,13 @@ .editingInput { border: 0px; outline: none; + width: 100%; &::-ms-clear { display: none; } +} + +.editingContainer { + margin: 4px; } \ No newline at end of file diff --git a/packages/experiments/src/components/SelectedItemsList/SelectedPeopleList/Items/EditingItem.tsx b/packages/experiments/src/components/SelectedItemsList/SelectedPeopleList/Items/EditingItem.tsx index 6678b842fa3bd..34b7a3230df6a 100644 --- a/packages/experiments/src/components/SelectedItemsList/SelectedPeopleList/Items/EditingItem.tsx +++ b/packages/experiments/src/components/SelectedItemsList/SelectedPeopleList/Items/EditingItem.tsx @@ -1,7 +1,7 @@ /* tslint:disable */ import * as React from 'react'; /* tslint:enable */ -import { BaseComponent, KeyCodes, autobind, getId, getNativeProps, inputProperties } from '../../../../Utilities'; +import { BaseComponent, KeyCodes, autobind, getId, getNativeProps, inputProperties, css } from '../../../../Utilities'; import { FloatingPeoplePicker, IBaseFloatingPickerProps } from '../../../../FloatingPicker'; import { ISelectedPeopleItemProps } from '../SelectedPeopleList'; import { IExtendedPersonaProps } from '../SelectedPeopleList'; @@ -49,7 +49,7 @@ export class EditingItem extends BaseComponent +
- { this.state.contextualMenuVisible ? ( - ) - : null - }
); } @@ -97,15 +85,4 @@ export class ExtendedSelectedItem extends BaseComponent): void { - ev.preventDefault(); - this.setState({ contextualMenuVisible: true }); - } - - @autobind - private _onCloseContextualMenu(ev: Event): void { - this.setState({ contextualMenuVisible: false }); - } } \ No newline at end of file diff --git a/packages/experiments/src/components/SelectedItemsList/SelectedPeopleList/Items/SelectedItemWithContextMenu.tsx b/packages/experiments/src/components/SelectedItemsList/SelectedPeopleList/Items/SelectedItemWithContextMenu.tsx new file mode 100644 index 0000000000000..7ace81e2f45ab --- /dev/null +++ b/packages/experiments/src/components/SelectedItemsList/SelectedPeopleList/Items/SelectedItemWithContextMenu.tsx @@ -0,0 +1,63 @@ +/* tslint:disable */ +import * as React from 'react'; +/* tslint:enable */ +import { BaseComponent, autobind } from '../../../../Utilities'; +import { IExtendedPersonaProps } from '../SelectedPeopleList'; +import { ContextualMenu, DirectionalHint } from 'office-ui-fabric-react/lib/ContextualMenu'; +import { IContextualMenuItem } from 'office-ui-fabric-react/lib/ContextualMenu'; +import { IBaseProps } from 'office-ui-fabric-react/lib/Utilities'; + +export interface IPeoplePickerItemState { + contextualMenuVisible: boolean; +} + +export interface ISelectedItemWithContextMenuProps extends IBaseProps { + renderedItem: JSX.Element; + beginEditing?: (item: IExtendedPersonaProps) => void; + menuItems: IContextualMenuItem[]; + item: IExtendedPersonaProps; +} + +export class SelectedItemWithContextMenu extends BaseComponent { + protected itemElement: HTMLElement; + + constructor(props: ISelectedItemWithContextMenuProps) { + super(props); + this.state = { contextualMenuVisible: false }; + } + + public render(): JSX.Element { + return ( +
+ { this.props.renderedItem } + { this.state.contextualMenuVisible ? ( + ) + : null + } +
); + } + + @autobind + private _onClick(ev: React.MouseEvent): void { + ev.preventDefault(); + if (this.props.beginEditing && !this.props.item.isValid) { + this.props.beginEditing(this.props.item); + } else { + this.setState({ contextualMenuVisible: true }); + } + } + + @autobind + private _onCloseContextualMenu(ev: Event): void { + this.setState({ contextualMenuVisible: false }); + } +} \ No newline at end of file diff --git a/packages/experiments/src/components/SelectedItemsList/SelectedPeopleList/SelectedPeopleList.tsx b/packages/experiments/src/components/SelectedItemsList/SelectedPeopleList/SelectedPeopleList.tsx index 28a951de54aa6..3c854dbad16b3 100644 --- a/packages/experiments/src/components/SelectedItemsList/SelectedPeopleList/SelectedPeopleList.tsx +++ b/packages/experiments/src/components/SelectedItemsList/SelectedPeopleList/SelectedPeopleList.tsx @@ -5,6 +5,7 @@ import { BaseSelectedItemsList } from '../BaseSelectedItemsList'; import { IBaseSelectedItemsListProps, ISelectedItemProps } from '../BaseSelectedItemsList.types'; import { IPersonaProps } from 'office-ui-fabric-react/lib/Persona'; import { ExtendedSelectedItem } from './Items/ExtendedSelectedItem'; +import { SelectedItemWithContextMenu } from './Items/SelectedItemWithContextMenu'; import { autobind, IRenderFunction } from '../../../Utilities'; import { IContextualMenuItem } from 'office-ui-fabric-react/lib/ContextualMenu'; import { IBaseFloatingPickerProps } from '../../../FloatingPicker'; @@ -20,7 +21,6 @@ export interface IExtendedPersonaProps extends IPersonaProps { export interface ISelectedPeopleItemProps extends ISelectedItemProps { onExpandItem?: () => void; - menuItems: IContextualMenuItem[]; renderPersonaCoin?: IRenderFunction; renderPrimaryText?: IRenderFunction; } @@ -93,10 +93,28 @@ export class SelectedPeopleList extends BasePeopleSelectedItemsList { ); } else { let onRenderItem = this.props.onRenderItem as (props: ISelectedPeopleItemProps) => JSX.Element; - return onRenderItem({ ...props }); + let renderedItem = onRenderItem(props); + return ( + props.menuItems.length > 0 ? + ( + + ) + : renderedItem + ); } } + @autobind + private _beginEditing(item: IExtendedPersonaProps): void { + item.isEditing = true; + this.forceUpdate(); + } + @autobind // tslint:disable-next-line:no-any private _completeEditing(oldItem: any, newItem: any): void { @@ -111,35 +129,38 @@ export class SelectedPeopleList extends BasePeopleSelectedItemsList { if (this.props.editMenuItemText && this.props.getEditingItemText) { menuItems.push({ key: 'Edit', - name: this.props.editMenuItemText ? this.props.editMenuItemText : 'Edit', + name: this.props.editMenuItemText, onClick: (ev: React.MouseEvent, menuItem: IContextualMenuItem) => { - (menuItem.data as IExtendedPersonaProps).isEditing = true; - this.forceUpdate(); + this._beginEditing(menuItem.data); }, data: item, }); } - menuItems.push( - { - key: 'Remove', - name: this.props.removeMenuItemText ? this.props.removeMenuItemText : 'Remove', - onClick: (ev: React.MouseEvent, menuItem: IContextualMenuItem) => { - this.removeItem(menuItem.data as ISelectedItemProps); - }, - data: item, - }, - { + if (this.props.removeMenuItemText) { + menuItems.push( + { + key: 'Remove', + name: this.props.removeMenuItemText, + onClick: (ev: React.MouseEvent, menuItem: IContextualMenuItem) => { + this.removeItem(menuItem.data as ISelectedItemProps); + }, + data: item, + }); + } + + if (this.props.copyMenuItemText) { + menuItems.push({ key: 'Copy', - name: this.props.copyMenuItemText ? this.props.copyMenuItemText : 'Copy', + name: this.props.copyMenuItemText, onClick: (ev: React.MouseEvent, menuItem: IContextualMenuItem) => { if (this.props.onCopyItems) { (this.copyItems as (items: IExtendedPersonaProps[]) => void)([menuItem.data] as IExtendedPersonaProps[]); } }, data: item, - }, - ); + }); + } return menuItems; }