Skip to content

Commit

Permalink
[UPP] 7.0 backports of #16535, #16210 (#16540)
Browse files Browse the repository at this point in the history
* [UPP] Add controlled callbacks to EditableItem (#16535)

* [UPP] Add onSuggestionsShown and onSuggestionsHidden callbacks (#16210)
  • Loading branch information
salaman authored Jan 19, 2021
1 parent ce56667 commit 8b70681
Show file tree
Hide file tree
Showing 8 changed files with 123 additions and 26 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"type": "patch",
"comment": "[UPP] Add onSuggestionsShown and onSuggestionsHidden callbacks (#16210)",
"packageName": "@fluentui/react-examples",
"email": "[email protected]",
"dependentChangeType": "patch",
"date": "2021-01-19T22:33:38.775Z"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"type": "patch",
"comment": "[UPP] Add controlled callbacks to EditableItem (#16535)",
"packageName": "@uifabric/experiments",
"email": "[email protected]",
"dependentChangeType": "patch",
"date": "2021-01-19T22:33:32.145Z"
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,30 @@ export const BaseFloatingSuggestions = <T extends {}>(props: IBaseFloatingSugges
pickerSuggestionsProps,
selectedFooterIndex,
selectedHeaderIndex,
onSuggestionsShown,
onSuggestionsHidden,
} = props;

const hidePicker = (ev: React.MouseEvent): void => {
onFloatingSuggestionsDismiss ? onFloatingSuggestionsDismiss(ev) : null;
};
// Picker shown/hidden callback logic
// Ref gate to prevent the onHidden callback from being called the first time
const suggestionsCallbackGate = React.useRef(false);
React.useEffect(() => {
if (suggestionsCallbackGate.current || isSuggestionsVisible) {
if (isSuggestionsVisible) {
onSuggestionsShown?.();
} else {
onSuggestionsHidden?.();
}
}
suggestionsCallbackGate.current = true;
}, [isSuggestionsVisible, onSuggestionsShown, onSuggestionsHidden]);

const hidePicker = React.useCallback(
(ev?: React.MouseEvent): void => {
onFloatingSuggestionsDismiss?.(ev);
},
[onFloatingSuggestionsDismiss],
);

return (
<div
Expand All @@ -51,7 +70,6 @@ export const BaseFloatingSuggestions = <T extends {}>(props: IBaseFloatingSugges
isBeakVisible={false}
gapSpace={5}
target={targetElement}
// eslint-disable-next-line react/jsx-no-bind
onDismiss={hidePicker}
onKeyDown={onKeyDown}
directionalHint={DirectionalHint.bottomLeftEdge}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,16 @@ export interface IBaseFloatingSuggestionsProps<T> {
* Should be used with the footerItemProps on the pickerSuggestionProps
*/
selectedFooterIndex?: number;

/**
* A callback for when the floating suggestions are shown
*/
onSuggestionsShown?: () => void;

/**
* A callback for when the floating suggestions are hidden (on dismiss or selection)
*/
onSuggestionsHidden?: () => void;
}

export interface IBaseFloatingPickerHeaderFooterProps {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as React from 'react';
import { ISelectedItemProps } from '../SelectedItemsList.types';
import { ItemCanDispatchTrigger, Item } from './ItemTrigger.types';
import { useBoolean } from '@uifabric/react-hooks';

export type EditingItemComponentProps<T> = {
item: T;
Expand All @@ -14,36 +13,84 @@ export type EditingItemComponentProps<T> = {
* Parameters to the EditingItem higher-order component
*/
export type EditableItemProps<T> = {
/**
* Component to render when item is in normal state
*/
itemComponent: ItemCanDispatchTrigger<T>;

/**
* Component to render when item is in editing state
*/
editingItemComponent: React.ComponentType<EditingItemComponentProps<T>>;

/**
* Returns editing state (boolean) of the item
*/
getIsEditing: (item: T, index: number) => boolean;

/**
* Callback when editing should be started. The controlling component should ensure
* the item is marked as being edited
*/
onEditingStarted: (item: T, index: number) => void;

/**
* Callback when editing is finished. The controlling component should ensure
* the item is marked as being not edited
*/
onEditingCompleted: (item: T, index: number) => void;

/**
* Callback when editing is cancelled/dismissed
*/
onEditingDismissed?: (item: T, index: number) => void;

/**
* Callback for a click on the normal state item component
*/
onClick?: (ev: React.MouseEvent<HTMLElement>, item: T, index: number) => void;
};

// `extends any` to trick the parser into parsing as a type decl instead of a jsx tag
export const EditableItem = <T extends any>(editableItemProps: EditableItemProps<T>): Item<T> => {
// `extends unknown` to trick the parser into parsing as a type decl instead of a jsx tag
export const EditableItem = <T extends unknown>(editableItemProps: EditableItemProps<T>): Item<T> => {
return React.memo((selectedItemProps: ISelectedItemProps<T>) => {
const { onItemChange, index } = selectedItemProps;
const [isEditing, { setTrue: setEditingTrue, setFalse: setEditingFalse }] = useBoolean(false);
const { getIsEditing, onEditingStarted, onEditingCompleted, onEditingDismissed, onClick } = editableItemProps;
const { onItemChange, item, index } = selectedItemProps;

const onItemEdited = React.useCallback(
const isEditing = getIsEditing(item, index);
const ItemComponent = editableItemProps.itemComponent;
const EditingItemComponent = editableItemProps.editingItemComponent;

const onTrigger = React.useCallback(() => onEditingStarted(item, index), [index, item, onEditingStarted]);

const onEditingComplete = React.useCallback(
(_oldItem: T, newItem: T) => {
onItemChange && onItemChange(newItem, index);
setEditingFalse();
onItemChange?.(newItem, index);
onEditingCompleted(item, index);
},
[onItemChange, index, setEditingFalse],
[index, item, onEditingCompleted, onItemChange],
);

const ItemComponent = editableItemProps.itemComponent;
const EditingItemComponent = editableItemProps.editingItemComponent;
const onDismiss = React.useCallback(() => {
onEditingDismissed?.(item, index);
}, [index, item, onEditingDismissed]);

const onItemClicked = React.useCallback(
(ev: React.MouseEvent<HTMLElement>) => {
onClick?.(ev, item, index);
},
[index, item, onClick],
);

return isEditing ? (
<EditingItemComponent
item={selectedItemProps.item}
onEditingComplete={onItemEdited}
onDismiss={setEditingFalse}
onEditingComplete={onEditingComplete}
onDismiss={onDismiss}
createGenericItem={selectedItemProps.createGenericItem}
/>
) : (
<ItemComponent {...selectedItemProps} onTrigger={setEditingTrue} />
<ItemComponent {...selectedItemProps} onTrigger={onTrigger} onClick={onItemClicked} />
);
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {

export const SelectedPeopleListWithEditExample = (): JSX.Element => {
const [currentSelectedItems, setCurrentSelectedItems] = React.useState<IPersonaProps[]>([people[40]]);
const [editingIndex, setEditingIndex] = React.useState(-1);

const _startsWith = (text: string, filterText: string): boolean => {
return text.toLowerCase().indexOf(filterText.toLowerCase()) === 0;
Expand All @@ -36,7 +37,7 @@ export const SelectedPeopleListWithEditExample = (): JSX.Element => {
/**
* Build a custom selected item capable of being edited when the item is right clicked
*/
const SelectedItem = EditableItem({
const SelectedItem = EditableItem<IPersonaProps>({
itemComponent: TriggerOnContextMenu(SelectedPersona),
editingItemComponent: DefaultEditingItem({
getEditingItemText: persona => persona.text || '',
Expand All @@ -45,6 +46,9 @@ export const SelectedPeopleListWithEditExample = (): JSX.Element => {
<FloatingPeopleSuggestions {...props} isSuggestionsVisible={true} />
),
}),
getIsEditing: (item, index) => index === editingIndex,
onEditingStarted: (item, index) => setEditingIndex(index),
onEditingCompleted: () => setEditingIndex(-1),
});

const _onAddItemButtonClicked = React.useCallback(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {

export const SelectedPeopleListWithEditInContextMenuExample = (): JSX.Element => {
const [currentSelectedItems, setCurrentSelectedItems] = React.useState<IPersonaProps[]>([people[40]]);
const [editingIndex, setEditingIndex] = React.useState(-1);

const _startsWith = (text: string, filterText: string): boolean => {
return text.toLowerCase().indexOf(filterText.toLowerCase()) === 0;
Expand Down Expand Up @@ -67,6 +68,9 @@ export const SelectedPeopleListWithEditInContextMenuExample = (): JSX.Element =>
],
itemComponent: TriggerOnContextMenu(SelectedPersona),
}),
getIsEditing: (item, index) => index === editingIndex,
onEditingStarted: (item, index) => setEditingIndex(index),
onEditingCompleted: () => setEditingIndex(-1),
});

const _onAddItemButtonClicked = React.useCallback(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export const UnifiedPeoplePickerWithEditExample = (): JSX.Element => {

const [peopleSelectedItems, setPeopleSelectedItems] = React.useState<IPersonaProps[]>([]);
const [inputText, setInputText] = React.useState<string>('');
const [editingIndex, setEditingIndex] = React.useState(-1);

const ref = React.useRef<any>();

Expand Down Expand Up @@ -128,13 +129,7 @@ export const UnifiedPeoplePickerWithEditExample = (): JSX.Element => {
return suggestionList;
};

const _isValid = React.useCallback((item: IPersonaProps): boolean => {
if (item.secondaryText) {
return true;
} else {
return false;
}
}, []);
const _isValid = React.useCallback((item: IPersonaProps): boolean => Boolean(item.secondaryText), []);

const SelectedItemInternal = (props: ISelectedItemProps<IPersonaProps>) => (
<SelectedPersona isValid={_isValid} {...props} />
Expand Down Expand Up @@ -169,6 +164,9 @@ export const UnifiedPeoplePickerWithEditExample = (): JSX.Element => {
],
itemComponent: TriggerOnContextMenu(SelectedItemInternal),
}),
getIsEditing: (item, index) => index === editingIndex,
onEditingStarted: (item, index) => setEditingIndex(index),
onEditingCompleted: () => setEditingIndex(-1),
});

const _copyToClipboardWrapper = (item: IPersona) => {
Expand Down

0 comments on commit 8b70681

Please sign in to comment.