diff --git a/common/changes/office-ui-fabric-react/stanleyy-fillin_2018-02-07-00-36.json b/common/changes/office-ui-fabric-react/stanleyy-fillin_2018-02-07-00-36.json new file mode 100644 index 00000000000000..3930100a40a969 --- /dev/null +++ b/common/changes/office-ui-fabric-react/stanleyy-fillin_2018-02-07-00-36.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "office-ui-fabric-react", + "comment": "Add multiSelect capability for ComboBox", + "type": "minor" + } + ], + "packageName": "office-ui-fabric-react", + "email": "stanleyy@microsoft.com" +} \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/ComboBox/ComboBox.styles.ts b/packages/office-ui-fabric-react/src/components/ComboBox/ComboBox.styles.ts index 1d357dc9ecd080..1361c542ad94fa 100644 --- a/packages/office-ui-fabric-react/src/components/ComboBox/ComboBox.styles.ts +++ b/packages/office-ui-fabric-react/src/components/ComboBox/ComboBox.styles.ts @@ -53,7 +53,8 @@ const getListOptionHighContrastStyles = memoizeFunction((theme: ITheme): IRawSty export const getOptionStyles = memoizeFunction(( theme: ITheme, customStylesForAllOptions?: Partial, - customOptionStylesForCurrentOption?: Partial + customOptionStylesForCurrentOption?: Partial, + isPending?: boolean ): Partial => { const { semanticColors, palette } = theme; @@ -67,7 +68,7 @@ export const getOptionStyles = memoizeFunction(( const optionStyles: IComboBoxOptionStyles = { root: [ { - backgroundColor: 'transparent', + backgroundColor: isPending ? ComboBoxOptionBackgroundHovered : 'transparent', boxSizing: 'border-box', cursor: 'pointer', display: 'block', diff --git a/packages/office-ui-fabric-react/src/components/ComboBox/ComboBox.tsx b/packages/office-ui-fabric-react/src/components/ComboBox/ComboBox.tsx index f3dd413faee6e8..61e21df8e95b95 100644 --- a/packages/office-ui-fabric-react/src/components/ComboBox/ComboBox.tsx +++ b/packages/office-ui-fabric-react/src/components/ComboBox/ComboBox.tsx @@ -3,6 +3,7 @@ import { IComboBoxOption, IComboBoxProps } from './ComboBox.types'; import { DirectionalHint } from '../../common/DirectionalHint'; import { Callout } from '../../Callout'; import { Label } from '../../Label'; +import { Checkbox } from '../../Checkbox'; import { CommandButton, IconButton @@ -36,8 +37,8 @@ export interface IComboBoxState { // The open state isOpen?: boolean; - // The currently selected index (-1 if no index is selected) - selectedIndex: number; + // The currently selected indices + selectedIndices?: number[]; // The focused state of the comboBox focused?: boolean; @@ -143,19 +144,20 @@ export class ComboBox extends BaseComponent { 'defaultSelectedKey': 'selectedKey', 'value': 'defaultSelectedKey', 'selectedKey': 'value', - 'dropdownWidth': 'useComboBoxAsMenuWidth' + 'dropdownWidth': 'useComboBoxAsMenuWidth', }); this._id = props.id || getId('ComboBox'); - const selectedKey = props.defaultSelectedKey !== undefined ? props.defaultSelectedKey : props.selectedKey; + const selectedKeys: (string | number)[] = this._getSelectedKeys(props.defaultSelectedKey, props.selectedKey); + this._isScrollIdle = true; - const index: number = this._getSelectedIndex(props.options, selectedKey); + const initialSelectedIndices: number[] = this._getSelectedIndices(props.options, selectedKeys); this.state = { isOpen: false, - selectedIndex: index, + selectedIndices: initialSelectedIndices, focused: false, suggestedDisplayValue: '', currentOptions: this.props.options, @@ -176,11 +178,11 @@ export class ComboBox extends BaseComponent { if (newProps.selectedKey !== this.props.selectedKey || newProps.value !== this.props.value || newProps.options !== this.props.options) { - - const index: number = this._getSelectedIndex(newProps.options, newProps.selectedKey); + const selectedKeys: string[] | number[] = this._getSelectedKeys(undefined, newProps.selectedKey); + const indices: number[] = this._getSelectedIndices(newProps.options, selectedKeys); this.setState({ - selectedIndex: index, + selectedIndices: indices, currentOptions: newProps.options }); } @@ -196,7 +198,7 @@ export class ComboBox extends BaseComponent { const { isOpen, focused, - selectedIndex, + selectedIndices, currentPendingValueValidIndex } = this.state; @@ -230,7 +232,7 @@ export class ComboBox extends BaseComponent { // we need to set selection if (this._focusInputAfterClose && (prevState.isOpen && !isOpen || (focused && - ((!isOpen && prevState.selectedIndex !== selectedIndex) || + ((!isOpen && !this.props.multiSelect && prevState.selectedIndices && selectedIndices && prevState.selectedIndices[0] !== selectedIndices[0]) || !allowFreeform || value !== prevProps.value) ))) { this._select(); @@ -275,7 +277,7 @@ export class ComboBox extends BaseComponent { theme, title } = this.props; - const { isOpen, focused, suggestedDisplayValue } = this.state; + const { isOpen, selectedIndices, focused, suggestedDisplayValue } = this.state; this._currentVisibleValue = this._getVisibleValue(); const divProps = getNativeProps(this.props, divProperties); @@ -344,8 +346,8 @@ export class ComboBox extends BaseComponent { title={ title } /> { /> - { isOpen && ( + {isOpen && ( (onRenderContainer as any)({ ...this.props, onRenderList, @@ -366,13 +368,13 @@ export class ComboBox extends BaseComponent { options: this.state.currentOptions.map((item, index) => ({ ...item, index: index })) }, this._onRenderContainer) - ) } + )} { errorMessage &&
- { errorMessage } + {errorMessage}
} @@ -450,12 +452,13 @@ export class ComboBox extends BaseComponent { autoComplete } = this.props; const { - selectedIndex, + selectedIndices, currentPendingValueValidIndex, currentOptions, currentPendingValue, suggestedDisplayValue, - isOpen + isOpen, + focused } = this.state; const currentPendingIndexValid = this._indexWithinBounds(currentOptions, currentPendingValueValidIndex); @@ -466,41 +469,63 @@ export class ComboBox extends BaseComponent { return value; } - let index = selectedIndex; + // Values to display in the BaseAutoFill area + const displayValues = []; - if (allowFreeform) { - // If we are allowing freeform and autocomplete is also true - // and we've got a pending value that matches an option, remember - // the matched option's index - if (autoComplete === 'on' && currentPendingIndexValid) { - index = currentPendingValueValidIndex; + if (this.props.multiSelect) { + // MUlti-select + if (focused) { + let index = -1; + if (autoComplete === 'on' && currentPendingIndexValid) { + index = currentPendingValueValidIndex; + } + displayValues.push(currentPendingValue !== '' ? currentPendingValue : (this._indexWithinBounds(currentOptions, index) ? currentOptions[index].text : '')); + } else { + for (let idx = 0; selectedIndices && (idx < selectedIndices.length); idx ++) { + const index: number = selectedIndices[idx]; + displayValues.push(this._indexWithinBounds(currentOptions, index) ? currentOptions[index].text : suggestedDisplayValue); + } } - - // Since we are allowing freeform, if there is currently a nonempty pending value, use that - // otherwise use the index determined above (falling back to '' if we did not get a valid index) - return currentPendingValue !== '' ? currentPendingValue : (this._indexWithinBounds(currentOptions, index) ? currentOptions[index].text : ''); - } else { - - // If we are not allowing freeform and have a - // valid index that matches the pending value, - // we know we will need some version of the pending value - if (currentPendingIndexValid) { - - // If autoComplete is on, return the - // raw pending value, otherwise remember + // Single-select + let index: number = this._getFirstSelectedIndex(); + if (allowFreeform) { + // If we are allowing freeform and autocomplete is also true + // and we've got a pending value that matches an option, remember // the matched option's index - if (autoComplete === 'on') { - return currentPendingValue; + if (autoComplete === 'on' && currentPendingIndexValid) { + index = currentPendingValueValidIndex; } - index = currentPendingValueValidIndex; + // Since we are allowing freeform, if there is currently a nonempty pending value, use that + // otherwise use the index determined above (falling back to '' if we did not get a valid index) + displayValues.push(currentPendingValue !== '' ? currentPendingValue : (this._indexWithinBounds(currentOptions, index) ? currentOptions[index].text : '')); + } else { + // If we are not allowing freeform and have a + // valid index that matches the pending value, + // we know we will need some version of the pending value + if (currentPendingIndexValid && autoComplete === 'on') { + // If autoComplete is on, return the + // raw pending value, otherwise remember + // the matched option's index + index = currentPendingValueValidIndex; + displayValues.push(currentPendingValue); + } else { + displayValues.push(this._indexWithinBounds(currentOptions, index) ? currentOptions[index].text : suggestedDisplayValue); + } } + } - // If we have a valid index then return the text value of that option, - // otherwise return the suggestedDisplayValue - return this._indexWithinBounds(currentOptions, index) ? currentOptions[index].text : suggestedDisplayValue; + // If we have a valid index then return the text value of that option, + // otherwise return the suggestedDisplayValue + let displayString = ''; + for (let idx = 0; idx < displayValues.length; idx++) { + if (idx > 0) { + displayString += ', '; + } + displayString += displayValues[idx]; } + return displayString; } /** @@ -591,7 +616,7 @@ export class ComboBox extends BaseComponent { currentPendingValue, currentPendingValueValidIndex, currentOptions, - selectedIndex + selectedIndices } = this.state; if (this.props.autoComplete === 'on') { @@ -637,7 +662,7 @@ export class ComboBox extends BaseComponent { // If we get here, either autoComplete is on or we did not find a match with autoComplete on. // Remember we are not allowing freeform, so at this point, if we have a pending valid value index // use that; otherwise use the selectedIndex - const index = currentPendingValueValidIndex >= 0 ? currentPendingValueValidIndex : selectedIndex; + const index = currentPendingValueValidIndex >= 0 ? currentPendingValueValidIndex : this._getFirstSelectedIndex(); // Since we are not allowing freeform, we need to // set both the pending and suggested values/index @@ -646,6 +671,10 @@ export class ComboBox extends BaseComponent { this._setPendingInfoFromIndex(index); } + private _getFirstSelectedIndex(): number { + return (this.state.selectedIndices && this.state.selectedIndices.length > 0) ? this.state.selectedIndices[0] : -1; + } + /** * Walk along the options starting at the index, stepping by the delta (positive or negative) * looking for the next valid selectable index (e.g. skipping headings and dividers) @@ -694,7 +723,12 @@ export class ComboBox extends BaseComponent { */ private _setSelectedIndex(index: number, searchDirection: SearchDirection = SearchDirection.none) { const { onChanged, onPendingValueChanged } = this.props; - const { selectedIndex, currentOptions } = this.state; + const { currentOptions } = this.state; + let { selectedIndices } = this.state; + + if (!selectedIndices) { + selectedIndices = []; + } // Find the next selectable index, if searchDirection is none // we will get our starting index back @@ -706,12 +740,25 @@ export class ComboBox extends BaseComponent { // Are we at a new index? If so, update the state, otherwise // there is nothing to do - if (index !== selectedIndex) { + if (this.props.multiSelect || selectedIndices.length < 1 || (selectedIndices.length === 1 && selectedIndices[0] !== index)) { const option: IComboBoxOption = currentOptions[index]; + if (!option) { + return; + } + if (this.props.multiSelect) { + option.selected = !option.selected; + if (option.selected && selectedIndices.indexOf(index) < 0) { + selectedIndices.push(index); + } else if (!option.selected && selectedIndices.indexOf(index) >= 0) { + selectedIndices = selectedIndices.filter((value: number) => value !== index); + } + } else { + selectedIndices[0] = index; + } // Set the selected option this.setState({ - selectedIndex: index + selectedIndices: selectedIndices }); // If ComboBox value is changed, revert preview first @@ -797,7 +844,9 @@ export class ComboBox extends BaseComponent { if (this.state.focused) { this.setState({ focused: false }); - this._submitPendingValue(); + if (!this.props.multiSelect) { + this._submitPendingValue(); + } } } @@ -816,6 +865,7 @@ export class ComboBox extends BaseComponent { currentOptions, currentPendingValueValidIndexOnHover } = this.state; + let { selectedIndices } = this.state; // If we allow freeform and we have a pending value, we // need to handle that @@ -847,10 +897,15 @@ export class ComboBox extends BaseComponent { // If we are not controlled, create a new option const newOption: IComboBoxOption = { key: currentPendingValue, text: currentPendingValue }; const newOptions: IComboBoxOption[] = [...currentOptions, newOption]; - + if (selectedIndices) { + if (!this.props.multiSelect) { + selectedIndices = []; + } + selectedIndices.push(newOptions.length - 1); + } this.setState({ currentOptions: newOptions, - selectedIndex: newOptions.length - 1 + selectedIndices: selectedIndices }); } } else if (currentPendingValueValidIndex >= 0) { @@ -878,11 +933,11 @@ export class ComboBox extends BaseComponent { return ( {
{ (onRenderList as any)({ ...props }, this._onRenderList) }
- { onRenderLowerContent(this.props, this._onRenderLowerContent) } + {onRenderLowerContent(this.props, this._onRenderLowerContent)}
); } @@ -912,12 +967,12 @@ export class ComboBox extends BaseComponent { const id = this._id; return (
- { options.map((item) => (onRenderItem as any)(item, this._onRenderItem)) } + {options.map((item) => (onRenderItem as any)(item, this._onRenderItem))}
); } @@ -947,8 +1002,8 @@ export class ComboBox extends BaseComponent { return (
); } @@ -968,29 +1023,48 @@ export class ComboBox extends BaseComponent { const { onRenderOption = this._onRenderOptionContent } = this.props; const id = this._id; const isSelected: boolean = this._isOptionSelected(item.index); - const rootClassNames = getComboBoxOptionClassNames(this._getCurrentOptionStyles(item)).root; + const optionStyles = this._getCurrentOptionStyles(item); return ( - { - { onRenderOption(item, this._onRenderOptionContent) } - - } - + !this.props.multiSelect ? ( + { + { onRenderOption(item, this._onRenderOptionContent) } + + } + + ) : ( + + {onRenderOption(item, this._onRenderOptionContent)} + + ) ); } @@ -1017,7 +1091,15 @@ export class ComboBox extends BaseComponent { return false; } - return this._getPendingSelectedIndex(true /* includePendingValue */) === index; + if (!this.props.multiSelect && this._getPendingSelectedIndex(true /* includePendingValue */) === index) { + return true; + } + + let idxOfSelectedIndex = -1; + if ((index !== undefined) && this.state.selectedIndices) { + idxOfSelectedIndex = this.state.selectedIndices.indexOf(index); + } + return (idxOfSelectedIndex >= 0); } /** @@ -1030,7 +1112,7 @@ export class ComboBox extends BaseComponent { currentPendingValueValidIndexOnHover, currentPendingValueValidIndex, currentPendingValue, - selectedIndex + selectedIndices } = this.state; return ( @@ -1038,7 +1120,7 @@ export class ComboBox extends BaseComponent { currentPendingValueValidIndexOnHover : (currentPendingValueValidIndex >= 0 || (includeCurrentPendingValue && currentPendingValue !== '')) ? currentPendingValueValidIndex : - selectedIndex + this.props.multiSelect ? 0 : this._getFirstSelectedIndex() ); } @@ -1069,12 +1151,12 @@ export class ComboBox extends BaseComponent { const { currentPendingValueValidIndex, currentPendingValue, - selectedIndex + selectedIndices } = this.state; if (onScrollToItem) { // Use the custom scroll handler - onScrollToItem((currentPendingValueValidIndex >= 0 || currentPendingValue !== '') ? currentPendingValueValidIndex : selectedIndex); + onScrollToItem((currentPendingValueValidIndex >= 0 || currentPendingValue !== '') ? currentPendingValueValidIndex : this._getFirstSelectedIndex()); } else if (this._selectedElement.value && this._selectedElement.value.offsetParent) { // We are using refs, scroll the ref into view if (scrollSelectedToTop) { @@ -1105,7 +1187,7 @@ export class ComboBox extends BaseComponent { private _onRenderOptionContent = (item: IComboBoxOption): JSX.Element => { const optionClassNames = getComboBoxOptionClassNames(this._getCurrentOptionStyles(item)); - return { item.text }; + return {item.text}; } /** @@ -1116,9 +1198,12 @@ export class ComboBox extends BaseComponent { private _onItemClick(index: number | undefined): () => void { return (): void => { this._setSelectedIndex(index as number); - this.setState({ - isOpen: false - }); + if (!this.props.multiSelect) { + // only close the callout when it's in single-select mode + this.setState({ + isOpen: false + }); + } }; } @@ -1142,15 +1227,22 @@ export class ComboBox extends BaseComponent { /** * Get the index of the option that is marked as selected * @param options - the comboBox options - * @param selectedKey - the known selected key to find + * @param selectedKeys - the known selected key to find * @returns { number } - the index of the selected option, -1 if not found */ - private _getSelectedIndex(options: IComboBoxOption[] | undefined, selectedKey: string | number | undefined): number { - if (options === undefined || selectedKey === undefined) { - return -1; + private _getSelectedIndices(options: IComboBoxOption[] | undefined, selectedKeys: (string | number | undefined)[]): number[] { + const selectedIndices: any[] = []; + if (options === undefined || selectedKeys === undefined) { + return selectedIndices; } - return findIndex(options, (option => (option.selected || option.key === selectedKey))); + for (const selectedKey of selectedKeys) { + const index = findIndex(options, (option => (option.selected || option.key === selectedKey))); + if (index > -1) { + selectedIndices.push(index); + } + } + return selectedIndices; } /** @@ -1161,7 +1253,7 @@ export class ComboBox extends BaseComponent { */ private _resetSelectedIndex() { const { - selectedIndex, + selectedIndices, currentOptions } = this.state; if (this._comboBox.value) { @@ -1169,6 +1261,7 @@ export class ComboBox extends BaseComponent { } this._clearPendingInfo(); + const selectedIndex: number = this._getFirstSelectedIndex(); if (selectedIndex > 0 && selectedIndex < currentOptions.length) { this.setState({ suggestedDisplayValue: currentOptions[selectedIndex].text @@ -1227,7 +1320,10 @@ export class ComboBox extends BaseComponent { * @param searchDirection - the direction to search */ private _setPendingInfoFromIndexAndDirection(index: number, searchDirection: SearchDirection) { - const { currentOptions } = this.state; + const { + isOpen, + currentOptions + } = this.state; index = this._getNextSelectableIndex(index, searchDirection); if (this._indexWithinBounds(currentOptions, index)) { @@ -1293,7 +1389,8 @@ export class ComboBox extends BaseComponent { const { isOpen, currentOptions, - currentPendingValueValidIndexOnHover + currentPendingValueValidIndexOnHover, + selectedIndices } = this.state; if (disabled) { @@ -1305,34 +1402,36 @@ export class ComboBox extends BaseComponent { switch (ev.which) { case KeyCodes.enter: - // On enter submit the pending value this._submitPendingValue(); - - // if we are open or - // if we are not allowing freeform or - // our we have no pending value - // and no valid pending index - // flip the open state - if ((isOpen || - ((!allowFreeform || - this.state.currentPendingValue === undefined || - this.state.currentPendingValue === null || - this.state.currentPendingValue.length <= 0) && - this.state.currentPendingValueValidIndex < 0))) { + if (this.props.multiSelect && isOpen) { this.setState({ - isOpen: !isOpen + currentPendingValueValidIndex: index }); - } - - // Allow TAB to propigate - if (ev.which as number === KeyCodes.tab) { - return; + } else { + // On enter submit the pending value + if ((isOpen || + ((!allowFreeform || + this.state.currentPendingValue === undefined || + this.state.currentPendingValue === null || + this.state.currentPendingValue.length <= 0) && + this.state.currentPendingValueValidIndex < 0))) { + // if we are open or + // if we are not allowing freeform or + // our we have no pending value + // and no valid pending index + // flip the open state + this.setState({ + isOpen: !isOpen + }); + } } break; case KeyCodes.tab: // On enter submit the pending value - this._submitPendingValue(); + if (!this.props.multiSelect) { + this._submitPendingValue(); + } // If we are not allowing freeform // or the comboBox is open, flip the open state @@ -1574,7 +1673,7 @@ export class ComboBox extends BaseComponent { const { comboBoxOptionStyles: customStylesForAllOptions } = this.props; const { styles: customStylesForCurrentOption } = item; - return getOptionStyles(this.props.theme!, customStylesForAllOptions, customStylesForCurrentOption); + return getOptionStyles(this.props.theme!, customStylesForAllOptions, customStylesForCurrentOption, this._isPendingOption(item)); } /** @@ -1582,7 +1681,7 @@ export class ComboBox extends BaseComponent { * @returns the id of the current focused combo item, otherwise the id of the currently selected element, null otherwise */ private _getAriaActiveDescentValue(): string | null { - let descendantText = (this.state.isOpen && (this.state.selectedIndex as number) >= 0 ? (this._id + '-list' + this.state.selectedIndex) : null); + let descendantText = (this.state.isOpen && this.state.selectedIndices && this.state.selectedIndices.length >= 0 ? (this._id + '-list' + this.state.selectedIndices[0]) : null); if (this.state.isOpen && this.state.focused && this.state.currentPendingValueValidIndex !== -1) { descendantText = (this._id + '-list' + this.state.currentPendingValueValidIndex); } @@ -1598,4 +1697,36 @@ export class ComboBox extends BaseComponent { const autoComplete = !this.props.disabled && this.props.autoComplete === 'on'; return autoComplete ? (this.props.allowFreeform ? 'inline' : 'both') : 'none'; } + + private _isPendingOption(item: IComboBoxOption): boolean { + return item && item.index === this.state.currentPendingValueValidIndex; + } + + private _getSelectedKeys( + defaultSelectedKey: string | number | string[] | number[] | undefined, + selectedKey: string | number | string[] | number[] | undefined + ): string[] | number[] { + + let retKeys: string[] | number[] = []; + + if (defaultSelectedKey) { + if (defaultSelectedKey instanceof Array) { + retKeys = defaultSelectedKey; + } else if (typeof defaultSelectedKey === 'string') { + retKeys = [defaultSelectedKey as string]; + } else if (typeof defaultSelectedKey === 'number') { + retKeys = [defaultSelectedKey as number]; + } + } else if (selectedKey) { + if (selectedKey instanceof Array) { + retKeys = selectedKey; + } else if (typeof selectedKey === 'string') { + retKeys = [selectedKey as string]; + } else if (typeof selectedKey === 'number') { + retKeys = [selectedKey as number]; + } + } + + return retKeys; + } } diff --git a/packages/office-ui-fabric-react/src/components/ComboBox/ComboBox.types.ts b/packages/office-ui-fabric-react/src/components/ComboBox/ComboBox.types.ts index 4a8d53820f8b48..f42806a614f706 100644 --- a/packages/office-ui-fabric-react/src/components/ComboBox/ComboBox.types.ts +++ b/packages/office-ui-fabric-react/src/components/ComboBox/ComboBox.types.ts @@ -154,6 +154,11 @@ export interface IComboBoxProps extends ISelectableDroppableTextProps */ useComboBoxAsMenuWidth?: boolean; + /** + * Optional mode indicates if multi-choice selections is allowed. Default to false + */ + multiSelect?: boolean; + /** * Sets the 'aria-hidden' attribute on the ComboBox's button element instructing screen readers how to handle the element. This element is hidden by default because all functionality is handled by the input element and the arrow button is only meant to be decorative. * @default true diff --git a/packages/office-ui-fabric-react/src/components/ComboBox/examples/ComboBox.Basic.Example.tsx b/packages/office-ui-fabric-react/src/components/ComboBox/examples/ComboBox.Basic.Example.tsx index 118e160b76f934..ffe90d3376b969 100644 --- a/packages/office-ui-fabric-react/src/components/ComboBox/examples/ComboBox.Basic.Example.tsx +++ b/packages/office-ui-fabric-react/src/components/ComboBox/examples/ComboBox.Basic.Example.tsx @@ -12,10 +12,34 @@ import { SelectableOptionMenuItemType } from 'office-ui-fabric-react/lib/utiliti import { IComboBox } from '../ComboBox.types'; import { PrimaryButton } from '../../../Button'; +const INITIAL_OPTIONS = +[ + { key: 'Header', text: 'Theme Fonts', itemType: SelectableOptionMenuItemType.Header }, + { key: 'A', text: 'Arial Black', fontFamily: '"Arial Black", "Arial Black_MSFontService", sans-serif' }, + { key: 'B', text: 'Time New Roman', fontFamily: '"Times New Roman", "Times New Roman_MSFontService", serif' }, + { key: 'C', text: 'Comic Sans MS', fontFamily: '"Comic Sans MS", "Comic Sans MS_MSFontService", fantasy' }, + { key: 'C1', text: 'Calibri', fontFamily: 'Calibri, Calibri_MSFontService, sans-serif' }, + { key: 'divider_2', text: '-', itemType: SelectableOptionMenuItemType.Divider }, + { key: 'Header1', text: 'Other Options', itemType: SelectableOptionMenuItemType.Header }, + { key: 'D', text: 'Option d' }, + { key: 'E', text: 'Option e' }, + { key: 'F', text: 'Option f' }, + { key: 'G', text: 'Option g' }, + { key: 'H', text: 'Option h' }, + { key: 'I', text: 'Option i' }, + { key: 'J', text: 'Option j' } +]; + export class ComboBoxBasicExample extends React.Component<{}, { + // For controled single select options: IComboBoxOption[]; selectedOptionKey?: string | number; value?: string; + + // For controled multi select + optionsMulti: IComboBoxOption[]; + selectedOptionKeys?: string[]; + valueMulti?: string; }> { private _testOptions = [{ key: 'Header', text: 'Theme Fonts', itemType: SelectableOptionMenuItemType.Header }, @@ -47,6 +71,7 @@ export class ComboBoxBasicExample extends React.Component<{}, { super(props); this.state = { options: [], + optionsMulti: [], selectedOptionKey: undefined, value: 'Calibri' }; @@ -61,6 +86,7 @@ export class ComboBoxBasicExample extends React.Component<{}, { public render() { const { options, selectedOptionKey, value } = this.state; + const { optionsMulti, selectedOptionKeys, valueMulti } = this.state; return (
@@ -88,6 +114,23 @@ export class ComboBoxBasicExample extends React.Component<{}, { onClick={ this._basicComboBoxOnClick } /> + console.log('onFocus called') } + onBlur={ () => console.log('onBlur called') } + onMenuOpen={ () => console.log('ComboBox menu opened') } + // tslint:enable:jsx-no-lambda + /> + } + + console.log('onFocus called') } + onBlur={ () => console.log('onBlur called') } + onMenuOpen={ () => console.log('ComboBox menu opened') } + // tslint:enable:jsx-no-lambda + />
); @@ -261,30 +323,28 @@ export class ComboBoxBasicExample extends React.Component<{}, { return this.state.options; } - const newOptions = - [ - { key: 'Header', text: 'Theme Fonts', itemType: SelectableOptionMenuItemType.Header }, - { key: 'A', text: 'Arial Black', fontFamily: '"Arial Black", "Arial Black_MSFontService", sans-serif' }, - { key: 'B', text: 'Time New Roman', fontFamily: '"Times New Roman", "Times New Roman_MSFontService", serif' }, - { key: 'C', text: 'Comic Sans MS', fontFamily: '"Comic Sans MS", "Comic Sans MS_MSFontService", fantasy' }, - { key: 'C1', text: 'Calibri', fontFamily: 'Calibri, Calibri_MSFontService, sans-serif' }, - { key: 'divider_2', text: '-', itemType: SelectableOptionMenuItemType.Divider }, - { key: 'Header1', text: 'Other Options', itemType: SelectableOptionMenuItemType.Header }, - { key: 'D', text: 'Option d' }, - { key: 'E', text: 'Option e' }, - { key: 'F', text: 'Option f' }, - { key: 'G', text: 'Option g' }, - { key: 'H', text: 'Option h' }, - { key: 'I', text: 'Option i' }, - { key: 'J', text: 'Option j' } - ]; this.setState({ - options: newOptions, + options: INITIAL_OPTIONS, selectedOptionKey: 'C1', value: undefined }); - return newOptions; + return INITIAL_OPTIONS; + } + + private _getOptionsMulti(currentOptions: IComboBoxOption[]): IComboBoxOption[] { + + if (this.state.options.length > 0) { + return this.state.optionsMulti; + } + + this.setState({ + optionsMulti: INITIAL_OPTIONS, + selectedOptionKeys: [ 'C1' ], + value: undefined + }); + + return INITIAL_OPTIONS; } private _onChanged = (option: IComboBoxOption, index: number, value: string): void => { @@ -309,6 +369,37 @@ export class ComboBoxBasicExample extends React.Component<{}, { } } + private _onChangedMulti = (option: IComboBoxOption, index: number, value: string) => { + if (option !== undefined) { + // User selected/de-selected an existing option + this.setState({ + selectedOptionKeys: this._updateSelectedOptionKeys(this.state.selectedOptionKeys || [], option), + valueMulti: undefined + }); + } else if (value !== undefined) { + // User typed a freeform option + const newOption: IComboBoxOption = { key: value, text: value }; + const updatedSelectedKeys: string[] = this.state.selectedOptionKeys ? [...this.state.selectedOptionKeys, newOption.key as string] : [newOption.key as string]; + this.setState({ + optionsMulti: [...this.state.optionsMulti, newOption], + selectedOptionKeys: updatedSelectedKeys, + valueMulti: undefined + }); + } + } + + private _updateSelectedOptionKeys = (selectedKeys: string[], option: IComboBoxOption): string[] => { + if (selectedKeys && option) { + const index = selectedKeys.indexOf(option.key as string); + if (option.selected && index < 0) { + selectedKeys.push(option.key as string); + } else { + selectedKeys.splice(index, 1); + } + } + return selectedKeys; + } + private _basicComboBoxOnClick = (): void => { this._basicCombobox.focus(true); } diff --git a/packages/office-ui-fabric-react/src/components/Dropdown/Dropdown.tsx b/packages/office-ui-fabric-react/src/components/Dropdown/Dropdown.tsx index 44733cd04ad06f..e099ca490076d3 100644 --- a/packages/office-ui-fabric-react/src/components/Dropdown/Dropdown.tsx +++ b/packages/office-ui-fabric-react/src/components/Dropdown/Dropdown.tsx @@ -628,7 +628,10 @@ export class Dropdown extends BaseComponent extends React.HTMLAttributes