+ { label && (
+
+ ) }
+
0 ? styles.wrapperForError : null),
+ styles.root, className, {
+ 'is-open': isOpen,
+ ['is-disabled ' + styles.rootIsDisabled]: disabled,
+ 'is-required ': required,
+ [styles.focused]: focused,
+ [styles.readOnly]: !allowFreeform
+ }
+ )
+ } >
+ = 0 ? (id + '-list' + selectedIndex) : null) }
+ aria-disabled={ disabled }
+ aria-owns={ (id + '-list') }
+ spellCheck={ false }
+ defaultVisibleValue={ this._currentVisibleValue }
+ suggestedDisplayValue={ suggestedDisplayValue }
+ updateValueInWillReceiveProps={ this._onUpdateValueInAutoFillWillReceiveProps }
+ shouldSelectFullInputValueInComponentDidUpdate={ this._onShouldSelectFullInputValueInAutoFillComponentDidUpdate } />
+
+
+
+ { isOpen && (
+ onRenderContainer({ ...this.props }, this._onRenderContainer)
+ ) }
+ {
+ errorMessage &&
+
+ { errorMessage }
+
+ }
+
+ );
+ }
+
+ /**
+ * Set focus on the input
+ */
+ @autobind
+ public focus() {
+ if (this._comboBox) {
+ this._comboBox.focus();
+ }
+ }
+
+ /**
+ * componentWillReceiveProps handler for the auto fill component
+ * Checks/updates the iput value to set, if needed
+ * @param {IBaseAutoFillProps} defaultVisibleValue - the defaultVisibleValue that got passed
+ * in to the auto fill's componentWillReceiveProps
+ * @returns {string} - the updated value to set, if needed
+ */
+ @autobind
+ private _onUpdateValueInAutoFillWillReceiveProps(): string {
+ if (this._comboBox === null || this._comboBox === undefined) {
+ return null;
+ }
+
+ if (this._currentVisibleValue && this._currentVisibleValue !== '' && this._comboBox.value !== this._currentVisibleValue) {
+ return this._currentVisibleValue;
+ }
+
+ return this._comboBox.value;
+ }
+
+ /**
+ * componentDidUpdate handler for the auto fill component
+ *
+ * @param { string } defaultVisibleValue - the current defaultVisibleValue in the auto fill's componentDidUpdate
+ * @param { string } suggestedDisplayValue - the current suggestedDisplayValue in the auto fill's componentDidUpdate
+ * @returns {boolean} - should the full value of the input be selected?
+ * True if the defaultVisibleValue equals the suggestedDisplayValue, false otherwise
+ */
+ @autobind
+ private _onShouldSelectFullInputValueInAutoFillComponentDidUpdate(): boolean {
+ return this._currentVisibleValue === this.state.suggestedDisplayValue;
+ }
+
+ /**
+ * Get the correct value to pass to the input
+ * to show to the user based off of the current props and state
+ * @returns {string} the value to pass to the input
+ */
+ @autobind
+ private _getVisibleValue(): string {
+ let {
+ value,
+ allowFreeform,
+ autoComplete
+ } = this.props;
+ let {
+ selectedIndex,
+ currentPendingValueValidIndex,
+ currentOptions,
+ currentPendingValue,
+ suggestedDisplayValue
+ } = this.state;
+
+ // If the user passed is a value prop, use that
+ if (value) {
+ return value;
+ }
+
+ let index = selectedIndex;
+
+ 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 && this._indexWithinBounds(currentOptions, 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)
+ 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 (this._indexWithinBounds(currentOptions, currentPendingValueValidIndex)) {
+
+ // If autoComplete is on, return the
+ // raw pending value, otherwise remember
+ // the matched option's index
+ if (autoComplete) {
+ return currentPendingValue;
+ }
+
+ index = currentPendingValueValidIndex;
+ }
+
+ // 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;
+ }
+ }
+
+ /**
+ * Is the index within the bounds of the array?
+ * @param options - options to check if the index is valid for
+ * @param index - the index to check
+ * @returns {boolean} - true if the index is valid for the given options, false otherwise
+ */
+ private _indexWithinBounds(options: ISelectableOption[], index: number): boolean {
+ return index >= 0 && index < options.length;
+ }
+
+ /**
+ * Handler for typing changes on the input
+ * @param updatedValue - the newly changed value
+ */
+ @autobind
+ private _onInputChange(updatedValue: string) {
+ if (this.props.disabled) {
+ this._handleInputWhenDisabled(null /* event */);
+ return;
+ }
+
+ this.props.allowFreeform ?
+ this._processInputChangeWithFreeform(updatedValue) :
+ this._processInputChangeWithoutFreeform(updatedValue);
+ }
+
+ /**
+ * Process the new input's new value when the comboBox
+ * allows freeform entry
+ * @param updatedValue - the input's newly changed value
+ */
+ private _processInputChangeWithFreeform(updatedValue: string) {
+ let {
+ currentOptions
+ } = this.state;
+
+ // if the new value is empty, nothing needs to be done
+ if (updatedValue === '') {
+ return;
+ }
+
+ // Remember the original value and then,
+ // make the value lowercase for comparison
+ let originalUpdatedValue: string = updatedValue;
+ updatedValue = updatedValue.toLocaleLowerCase();
+
+ let newSuggestedDisplayValue = '';
+ let newCurrentPendingValueValidIndex = -1;
+
+ // If autoComplete is on, attempt to find a match from the available options
+ if (this.props.autoComplete) {
+
+ // If autoComplete is on, attempt to find a match where the text of an option starts with the updated value
+ let items = currentOptions.map((item, index) => { return { ...item, index }; }).filter((option) => option.itemType !== SelectableOptionMenuItemType.Header && option.itemType !== SelectableOptionMenuItemType.Divider).filter((option) => option.text.toLocaleLowerCase().indexOf(updatedValue) === 0);
+ if (items.length > 0) {
+ // If the user typed out the complete option text, we don't need any suggested display text anymore
+ newSuggestedDisplayValue = items[0].text.toLocaleLowerCase() !== updatedValue ? items[0].text : '';
+
+ // remember the index of the match we found
+ newCurrentPendingValueValidIndex = items[0].index;
+ }
+ } else {
+
+ // If autoComplete is off, attempt to find a match only when the value is exactly equal to the text of an option
+ let items = currentOptions.map((item, index) => { return { ...item, index }; }).filter((option) => option.itemType !== SelectableOptionMenuItemType.Header && option.itemType !== SelectableOptionMenuItemType.Divider).filter((option) => option.text.toLocaleLowerCase() === updatedValue);
+
+ // if we fould a match remember the index
+ if (items.length === 1) {
+ newCurrentPendingValueValidIndex = items[0].index;
+ }
+ }
+
+ // Set the updated state
+ this._setPendingInfo(originalUpdatedValue, newCurrentPendingValueValidIndex, newSuggestedDisplayValue);
+ }
+
+ /**
+ * Process the new input's new value when the comboBox
+ * does not allow freeform entry
+ * @param updatedValue - the input's newly changed value
+ */
+ private _processInputChangeWithoutFreeform(updatedValue: string) {
+ let {
+ currentPendingValue,
+ currentPendingValueValidIndex,
+ currentOptions,
+ selectedIndex
+ } = this.state;
+
+ if (this.props.autoComplete) {
+
+ // If autoComplete is on while allow freeform is off,
+ // we will remember the keypresses and build up a string to attempt to match
+ // as long as characters are typed within a the timeout span of each other,
+ // otherwise we will clear the string and start building a new one on the next keypress.
+ // Also, only do this processing if we have a non-empty value
+ if (updatedValue !== '') {
+
+ // If we have a pending autocomplete clearing task,
+ // we know that the user is typing with keypresses happening
+ // within the timeout of each other so remove the clearing task
+ // and continue building the pending value with the udpated value
+ if (this._lastReadOnlyAutoCompleteChangeTimeoutId > 0) {
+ this._async.clearTimeout(this._lastReadOnlyAutoCompleteChangeTimeoutId);
+ this._lastReadOnlyAutoCompleteChangeTimeoutId = -1;
+ updatedValue = currentPendingValue + updatedValue;
+ }
+
+ let originalUpdatedValue: string = updatedValue;
+ updatedValue = updatedValue.toLocaleLowerCase();
+
+ // If autoComplete is on, attempt to find a match where the text of an option starts with the updated value
+ let items = currentOptions.map((item, index) => { return { ...item, index }; }).filter((option) => option.itemType !== SelectableOptionMenuItemType.Header && option.itemType !== SelectableOptionMenuItemType.Divider).filter((option) => option.text.toLocaleLowerCase().indexOf(updatedValue) === 0);
+
+ // If we found a match, udpdate the state
+ if (items.length > 0) {
+ this._setPendingInfo(originalUpdatedValue, items[0].index, items[0].text);
+ }
+
+ // Schedule a timeout to clear the pending value after the timeout span
+ this._lastReadOnlyAutoCompleteChangeTimeoutId =
+ this._async.setTimeout(
+ () => { this._lastReadOnlyAutoCompleteChangeTimeoutId = -1; },
+ this._readOnlyPendingAutoCompleteTimeout
+ );
+ return;
+ }
+ }
+
+ // 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
+ let index = currentPendingValueValidIndex >= 0 ? currentPendingValueValidIndex : selectedIndex;
+
+ // Since we are not allowing freeform, we need to
+ // set both the pending and suggested values/index
+ // to allow us to select all content in the input to
+ // give the illusion that we are readonly (e.g. freeform off)
+ this._setPendingInfoFromIndex(index);
+ }
+
+ /**
+ * 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)
+ * @param index - the index to get the next selectable index from
+ * @param delta - optional delta to step by when finding the next index, defaults to 0
+ * @returns {number} - the next valid selectable index. If the new index is outside of the bounds,
+ * it will snap to the edge of the options array. If delta == 0 and the given index is not selectable
+ */
+ private _getNextSelectableIndex(index: number, searchDirection: SearchDirection): number {
+ let { currentOptions } = this.state;
+
+ let newIndex = index + searchDirection;
+
+ newIndex = Math.max(0, Math.min(currentOptions.length - 1, newIndex));
+
+ let option: ISelectableOption = currentOptions[newIndex];
+
+ // attempt to skip headers and dividers
+ if ((option.itemType === SelectableOptionMenuItemType.Header ||
+ option.itemType === SelectableOptionMenuItemType.Divider)) {
+
+ // Should we continue looking for an index to select?
+ if (searchDirection !== SearchDirection.none &&
+ ((newIndex !== 0 && searchDirection < SearchDirection.none) ||
+ (newIndex !== currentOptions.length - 1 && searchDirection > SearchDirection.none))) {
+ newIndex = this._getNextSelectableIndex(newIndex, searchDirection);
+ } else {
+ // If we cannot perform a useful search just return the index we were given
+ return index;
+ }
+ }
+
+ // We have the next valid selectable index, return it
+ return newIndex;
+ }
+
+ /**
+ * Set the selected index. Note, this is
+ * the "real" selected index, not the pending selected index
+ * @param index - the index to set (or the index to set from if a search direction is provided)
+ * @param searchDirection - the direction to search along the options from the given index
+ */
+ private _setSelectedIndex(index: number, searchDirection: SearchDirection = SearchDirection.none) {
+ let { onChanged } = this.props;
+ let { selectedIndex, currentOptions } = this.state;
+
+ // Find the next selectable index, if searchDirection is none
+ // we will get our starting index back
+ index = this._getNextSelectableIndex(index, searchDirection);
+
+ // Are we at a new index? If so, update the state, otherwise
+ // there is nothing to do
+ if (index !== selectedIndex) {
+ let option: ISelectableOption = currentOptions[index];
+
+ // Set the selected option
+ this.setState({
+ selectedIndex: index
+ });
+
+ // Did the creator give us an onChanged callback?
+ if (onChanged) {
+ onChanged(option, index);
+ }
+
+ // if we have a new selected index,
+ // clear all of the pending info
+ this._clearPendingInfo();
+ }
+ }
+
+ /**
+ * Focus (and select) the content of the input
+ * and set the focused state
+ */
+ @autobind
+ private _select() {
+ this._comboBox.inputElement.select();
+
+ if (!this.state.focused) {
+ this.setState({ focused: true });
+ }
+ }
+
+ /**
+ * Callback issued when the options should be resolved, if they have been updated or
+ * if they need to be passed in the first time. This only does work if an onResolveOptions
+ * callback was passed in
+ */
+ @autobind
+ private _onResolveOptions() {
+ if (this.props.onResolveOptions) {
+
+ // get the options
+ let newOptions = this.props.onResolveOptions({ ...this.state.currentOptions });
+
+ // Check to see if the returned value is an array, if it is update the state
+ // If the returned value is not an array then check to see if it's a promise or PromiseLike. If it is then resolve it asynchronously.
+ if (Array.isArray(newOptions)) {
+ this.setState({
+ currentOptions: newOptions
+ });
+ } else if (newOptions && newOptions.then) {
+
+ // Ensure that the promise will only use the callback if it was the most recent one
+ // and update the state when the promise returns
+ let promise: PromiseLike = this._currentPromise = newOptions;
+ promise.then((newOptionsFromPromise: ISelectableOption[]) => {
+ if (promise === this._currentPromise) {
+ this.setState({
+ currentOptions: newOptionsFromPromise
+ });
+ }
+ });
+ }
+ }
+ }
+
+ /**
+ * OnBlur handler. Set the focused state to false
+ * and submit any pending value
+ */
+ @autobind
+ private _onBlur() {
+ if (this.state.focused) {
+ this.setState({ focused: false });
+ this._submitPendingValue();
+ }
+ }
+
+ /**
+ * Submit a pending value if there is one
+ */
+ private _submitPendingValue() {
+ let {
+ onChanged,
+ allowFreeform
+ } = this.props;
+ let {
+ currentPendingValue,
+ currentPendingValueValidIndex,
+ currentOptions
+ } = this.state;
+
+ // If we allow freeform and we have a pending value, we
+ // need to handle that
+ if (allowFreeform && currentPendingValue !== '') {
+
+ // Check to see if the user typed an exact match
+ if (currentPendingValueValidIndex >= 0) {
+ let pendingOptionText: string = currentOptions[currentPendingValueValidIndex].text.toLocaleLowerCase();
+
+ // By exact match, that means: our pending value is the same as the the pending option text OR
+ // the peding option starts with the pending value and we have an "autoComplete" selection
+ // where the total lenght is equal to pending option length; update the state
+ if (currentPendingValue.toLocaleLowerCase() === pendingOptionText ||
+ (pendingOptionText.indexOf(currentPendingValue.toLocaleLowerCase()) === 0 &&
+ this._comboBox.isValueSelected &&
+ currentPendingValue.length + (this._comboBox.selectionEnd - this._comboBox.selectionStart) === pendingOptionText.length)) {
+ this._setSelectedIndex(currentPendingValueValidIndex);
+ this._clearPendingInfo();
+ return;
+ }
+ }
+
+ // Create a new option
+ let newOption: ISelectableOption = { key: currentPendingValue, text: currentPendingValue };
+ let newOptions: ISelectableOption[] = [...currentOptions, newOption];
+ let newSelectedIndex: number = this._getSelectedIndex(newOptions, currentPendingValue);
+
+ this.setState({
+ currentOptions: newOptions,
+ selectedIndex: newSelectedIndex
+ });
+
+ if (onChanged) {
+ onChanged(null, null, currentPendingValue);
+ }
+ } else if (currentPendingValueValidIndex >= 0) {
+ // Since we are not allowing freeform, we must have a matching
+ // to be able to update state
+ this._setSelectedIndex(currentPendingValueValidIndex);
+ }
+
+ // Finally, clear the pending info
+ this._clearPendingInfo();
+ }
+
+ // Render Callout container and pass in list
+ @autobind
+ private _onRenderContainer(props: IComboBoxProps): JSX.Element {
+ let {
+ onRenderList = this._onRenderList,
+ calloutProps
+ } = props;
+
+ return (
+
+
+ { onRenderList({ ...props }, this._onRenderList) }
+
+
+ );
+ }
+
+ // Render List of items
+ @autobind
+ private _onRenderList(props: IComboBoxProps): JSX.Element {
+ let {
+ onRenderItem = this._onRenderItem
+ } = this.props;
+
+ let id = this._id;
+ let { selectedIndex } = this.state;
+
+ return (
+
+ { this.state.currentOptions.map((item, index) => onRenderItem({ ...item, index }, this._onRenderItem)) }
+
+ );
+ }
+
+ // Render items
+ @autobind
+ private _onRenderItem(item: ISelectableOption): JSX.Element {
+ switch (item.itemType) {
+ case SelectableOptionMenuItemType.Divider:
+ return this._renderSeparator(item);
+ case SelectableOptionMenuItemType.Header:
+ return this._renderHeader(item);
+ default:
+ return this._renderOption(item);
+ }
+ }
+
+ // Render separator
+ private _renderSeparator(item: ISelectableOption): JSX.Element {
+ let { index, key } = item;
+ if (index > 0) {
+ return ;
+ }
+ return null;
+ }
+
+ private _renderHeader(item: ISelectableOption): JSX.Element {
+ let { onRenderOption = this._onRenderOption } = this.props;
+ return (
+
+ { onRenderOption(item, this._onRenderOption) }
+
);
+ }
+
+ // Render menu item
+ @autobind
+ private _renderOption(item: ISelectableOption): JSX.Element {
+ let { onRenderOption = this._onRenderOption } = this.props;
+ let id = this._id;
+ let isSelected: boolean = this._isOptionSelected(item.index);
+ return (
+ this._onItemClick(item.index) }
+ role='option'
+ aria-selected={ isSelected ? 'true' : 'false' }
+ ariaLabel={ item.text }
+ > {
+ { onRenderOption(item, this._onRenderOption) }
+
+ }
+
+ );
+ }
+
+ /**
+ * Use the current valid pending index if it exists OR
+ * we do not have a valid index and we currently have a pending input value,
+ * otherwise use the selected index
+ * */
+ private _isOptionSelected(index: number): boolean {
+ let {
+ currentPendingValueValidIndex,
+ currentPendingValue,
+ selectedIndex
+ } = this.state;
+ return ((currentPendingValueValidIndex >= 0 || currentPendingValue !== '') ?
+ currentPendingValueValidIndex === index : selectedIndex === index);
+ }
+
+ /**
+ * Scroll the selected element into view
+ */
+ private _scrollIntoView() {
+ if (this._selectedElement) {
+ let alignToTop = true;
+ if (this._comboBoxMenu.offsetParent) {
+ let scrollableParentRect = this._comboBoxMenu.offsetParent.getBoundingClientRect();
+ let selectedElementRect = this._selectedElement.offsetParent.getBoundingClientRect();
+
+ if (scrollableParentRect.top + scrollableParentRect.height <= selectedElementRect.top) {
+ alignToTop = false;
+ }
+ }
+
+ this._selectedElement.offsetParent.scrollIntoView(alignToTop);
+ }
+ }
+
+ // Render content of item
+ @autobind
+ private _onRenderOption(item: ISelectableOption): JSX.Element {
+ return { item.text };
+ }
+
+ /**
+ * Click handler for the menu items
+ * to select the item and also close the menu
+ * @param index - the index of the item that was clicked
+ */
+ private _onItemClick(index) {
+ this._setSelectedIndex(index);
+ this.setState({
+ isOpen: false
+ });
+ }
+
+ /**
+ * Handles dismissing (cancelling) the menu
+ */
+ @autobind
+ private _onDismiss() {
+
+ // reset the selected index
+ // to the last valud state
+ this._resetSelectedIndex();
+
+ // close the menu and focus the input
+ this.setState({ isOpen: false });
+ this._comboBox.focus();
+ }
+
+ /**
+ * Get the index of the option that is marked as selected
+ * @param options - the comboBox options
+ * @param selectedKey - the known selected key to find
+ * @returns {number} - the index of the selected option, -1 if not found
+ */
+ private _getSelectedIndex(options: ISelectableOption[], selectedKey: string | number): number {
+ return findIndex(options, (option => (option.isSelected || option.selected || (selectedKey != null) && option.key === selectedKey)));
+ }
+
+ /**
+ * Reset the selected index by clearing the
+ * input (of any pending text), clearing the pending state,
+ * and setting the suggested display value to the last
+ * selected state text
+ */
+ private _resetSelectedIndex() {
+ let {
+ selectedIndex,
+ currentOptions
+ } = this.state;
+ this._comboBox.clear();
+ this._clearPendingInfo();
+
+ if (selectedIndex > 0 && selectedIndex < currentOptions.length) {
+ this.setState({
+ suggestedDisplayValue: currentOptions[selectedIndex].text
+ });
+ }
+ }
+
+ /**
+ * Clears the pending info state
+ */
+ private _clearPendingInfo() {
+ this._setPendingInfo('' /* suggestedDisplayValue */, -1 /* currentPendingValueValidIndex */, '' /* currentPendingValue */);
+ }
+
+ /**
+ * Set the pending info
+ * @param currentPendingValue - new pending value to set
+ * @param currentPendingValueValidIndex - new pending value index to set
+ * @param suggestedDisplayValue - new suggest display value to set
+ */
+ private _setPendingInfo(currentPendingValue: string, currentPendingValueValidIndex: number, suggestedDisplayValue) {
+ this.setState({
+ currentPendingValue: currentPendingValue,
+ currentPendingValueValidIndex: currentPendingValueValidIndex,
+ suggestedDisplayValue: suggestedDisplayValue
+ });
+ }
+
+ /**
+ * Set the pending info from the given index
+ * @param index - the index to set the pending info from
+ */
+ private _setPendingInfoFromIndex(index: number) {
+ let {
+ currentOptions
+ } = this.state;
+
+ if (index > 0 && index < currentOptions.length) {
+ let option = currentOptions[index];
+ this._setPendingInfo(option.text, index, option.text);
+ } else {
+ this._clearPendingInfo();
+ }
+
+ }
+
+ /**
+ * Sets either the pending info or the
+ * selected index depending of if the comboBox is open
+ * @param index - the index to search from
+ * @param searchDirection - the direction to search
+ */
+ private _setInfoForIndexAndDirection(index: number, searchDirection: SearchDirection) {
+ let {
+ isOpen,
+ selectedIndex
+ } = this.state;
+
+ if (isOpen) {
+ index = this._getNextSelectableIndex(index, searchDirection);
+ this._setPendingInfoFromIndex(index);
+ } else {
+ this._setSelectedIndex(selectedIndex, searchDirection);
+ }
+ }
+
+ /**
+ * Handle keydown on the input
+ * @param ev - The keyboard event that was fired
+ */
+ @autobind
+ private _onInputKeyDown(ev: React.KeyboardEvent) {
+ let {
+ disabled,
+ allowFreeform,
+ autoComplete
+ } = this.props;
+ let {
+ isOpen,
+ currentPendingValueValidIndex,
+ selectedIndex,
+ currentOptions
+ } = this.state;
+
+ if (disabled) {
+ this._handleInputWhenDisabled(ev);
+ return;
+ }
+
+ let index = currentPendingValueValidIndex >= 0 ? currentPendingValueValidIndex : selectedIndex;
+
+ 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))) {
+ this.setState({
+ isOpen: !isOpen
+ });
+ }
+
+ // Allow TAB to propigate
+ if (ev.which === KeyCodes.tab) {
+ return;
+ }
+ break;
+
+ case KeyCodes.tab:
+ // On enter submit the pending value
+ this._submitPendingValue();
+
+ // If we are not allowing freeform
+ // or the comboBox is open, flip the open state
+ if (isOpen) {
+ this.setState({
+ isOpen: !isOpen
+ });
+ }
+
+ // Allow TAB to propigate
+ return;
+
+ case KeyCodes.escape:
+ // reset the selected index
+ this._resetSelectedIndex();
+
+ // Close the menu if opened
+ if (isOpen) {
+ this.setState({
+ isOpen: false
+ });
+ }
+ break;
+
+ case KeyCodes.up:
+ // Go to the previous option
+ this._setInfoForIndexAndDirection(index, SearchDirection.backward);
+ break;
+
+ case KeyCodes.down:
+ // Expand the comboBox on ALT + DownArrow
+ if (ev.altKey || ev.metaKey) {
+ this.setState({ isOpen: true });
+ } else {
+ // Got to the next option
+ this._setInfoForIndexAndDirection(index, SearchDirection.forward);
+ }
+ break;
+
+ case KeyCodes.home:
+ case KeyCodes.end:
+ if (allowFreeform) {
+ return;
+ }
+
+ // Set the initial values to respond to HOME
+ // which goes to the first selectable option
+ index = -1;
+ let directionToSearch = SearchDirection.forward;
+
+ // If end, update the values to respond to END
+ // which goes to the last selectable option
+ if (ev.which === KeyCodes.end) {
+ index = currentOptions.length;
+ directionToSearch = SearchDirection.backward;
+ }
+
+ this._setInfoForIndexAndDirection(index, directionToSearch);
+ break;
+
+ case KeyCodes.space:
+ // event handled in _onComboBoxKeyUp
+ if (!allowFreeform && !autoComplete) {
+ break;
+ }
+
+ default:
+
+ // are we processing a function key? if so bail out
+ if (ev.which >= 112 /* F1 */ && ev.which <= 123 /* F12 */) {
+ return;
+ }
+
+ // If we get here and we got either and ALT key
+ // or meta key and we are current open, let's close the menu
+ if ((ev.altKey || ev.metaKey) && isOpen) {
+ this.setState({ isOpen: !isOpen });
+ }
+
+ // If we are not allowing freeform and
+ // allowing autoComplete, handle the input here
+ // since we have marked the input as readonly
+ if (!allowFreeform && autoComplete) {
+ this._onInputChange(String.fromCharCode(ev.which));
+ break;
+ }
+
+ // allow the key to propigate by default
+ return;
+ }
+
+ ev.stopPropagation();
+ ev.preventDefault();
+ }
+
+ /**
+ * Handle keyup on the input
+ * @param ev - the keyboard event that was fired
+ */
+ @autobind
+ private _onInputKeyUp(ev: React.KeyboardEvent) {
+ let {
+ disabled,
+ allowFreeform,
+ autoComplete
+ } = this.props;
+
+ if (disabled) {
+ this._handleInputWhenDisabled(ev);
+ return;
+ }
+
+ switch (ev.which) {
+ case KeyCodes.space:
+ // If we are not allowing freeform and are not autoComplete
+ // make space expand/collapse the comboBox
+ // and allow the event to propagate
+ if (!allowFreeform && !autoComplete) {
+ this.setState({
+ isOpen: !this.state.isOpen
+ });
+ return;
+ }
+ break;
+
+ default:
+ return;
+ }
+
+ ev.stopPropagation();
+ ev.preventDefault();
+ }
+
+ /**
+ * Handle dismissing the menu and
+ * eating the required key event when disabled
+ * @param ev - the keyboard event that was fired
+ */
+ private _handleInputWhenDisabled(ev: React.KeyboardEvent) {
+ // If we are disabled, close the menu (if needed)
+ // and eat all keystokes other than TAB or ESC
+ if (this.props.disabled) {
+ if (this.state.isOpen) {
+ this.setState({ isOpen: false });
+ }
+
+ // When disabled stop propagation and prevent default
+ // of the event unless we have a tab, escape, or function key
+ if (ev !== null &&
+ ev.which !== KeyCodes.tab &&
+ ev.which !== KeyCodes.escape &&
+ (ev.which < 112 /* F1 */ || ev.which > 123 /* F12 */)) {
+ ev.stopPropagation();
+ ev.preventDefault();
+ }
+ }
+ }
+
+ /**
+ * Click handler for the button of the comboBox
+ * and the input when not allowing freeform. This
+ * toggles the expand/collapse state of the comboBox (if enbled)
+ */
+ @autobind
+ private _onComboBoxClick() {
+ let { disabled } = this.props;
+ let { isOpen } = this.state;
+
+ if (!disabled) {
+ this.setState({
+ isOpen: !isOpen
+ });
+ }
+ }
+}
diff --git a/packages/office-ui-fabric-react/src/components/ComboBox/ComboBox.visualtest.ts b/packages/office-ui-fabric-react/src/components/ComboBox/ComboBox.visualtest.ts
new file mode 100644
index 00000000000000..300231665fe215
--- /dev/null
+++ b/packages/office-ui-fabric-react/src/components/ComboBox/ComboBox.visualtest.ts
@@ -0,0 +1,29 @@
+import { Casper } from '../../visualtest/PhantomCssInterface';
+import { baseUrl } from '../../common/VisualTest';
+import { defaultScreenshot, testRunner, mouseClickScreenshot, mouseMoveScreenshot } from '../../visualtest/RunVisualTest';
+import { IRunVisualTest } from '../../visualtest/IRunVisualTest';
+declare var casper: Casper;
+let componentIds: IRunVisualTest[] = [];
+
+componentIds.push(
+ {
+ selector: '.' + 'ms-ComboBox-Input',
+ fileName: 'comboBox_input',
+ imageSelector: '.' + 'ms-ComboBox-Container',
+ commands: [defaultScreenshot, mouseClickScreenshot, mouseMoveScreenshot]
+ },
+ {
+ selector: '.' + 'ms-ComboBox-Button',
+ fileName: 'comboBox_button',
+ imageSelector: '.' + 'ms-comboBox-Container',
+ commands: [defaultScreenshot, mouseClickScreenshot, mouseMoveScreenshot]
+ },
+);
+
+casper.
+ start(baseUrl + 'comboBox').
+ then(() => {
+ testRunner(componentIds);
+ });
+
+casper.run(() => { casper.test.done(); });
\ No newline at end of file
diff --git a/packages/office-ui-fabric-react/src/components/ComboBox/ComboBoxPage.tsx b/packages/office-ui-fabric-react/src/components/ComboBox/ComboBoxPage.tsx
new file mode 100644
index 00000000000000..2179418e30cd43
--- /dev/null
+++ b/packages/office-ui-fabric-react/src/components/ComboBox/ComboBoxPage.tsx
@@ -0,0 +1,59 @@
+import * as React from 'react';
+import {
+ ExampleCard,
+ IComponentDemoPageProps,
+ ComponentPage,
+ PropertiesTableSet
+} from '@uifabric/example-app-base';
+import { ComboBoxBasicExample } from './examples/ComboBox.Basic.Example';
+
+const ComboBoxBasicExampleCode = require('!raw-loader!office-ui-fabric-react/src/components/ComboBox/examples/ComboBox.Basic.Example.tsx') as string;
+
+export class ComboBoxPage extends React.Component {
+ public render() {
+ return (
+
+
+
+ }
+ propertiesTables={
+ ('!raw-loader!office-ui-fabric-react/src/components/ComboBox/ComboBox.Props.ts')
+ ] }
+ />
+ }
+ overview={
+
+
+ A ComboBox is a list in which the selected item is always visible, and the others are visible on demand by clicking a drop-down button or by typing in the input (unless allowFreeform and autoComplete are both false). They are used to simplify the design and make a choice within the UI. When closed, only the selected item is visible. When users click the drop-down button, all the options become visible. To change the value, users open the list and click another value or use the arrow keys (up and down) to select a new value. When collapsed if autoComplete and/or allowFreeform are true, the user can select a new value by typing.
+
+
+ }
+ bestPractices={
+
+ }
+ dos={
+
+
+ - Use a ComboBox when there are multiple choices that can be collapsed under one title. Or if the list of items is long or when space is constrained.
+ - ComboBoxs contain shortened statements or words.
+ - Use a ComboBox when the selected option is more important than the alternatives (in contrast to radio buttons where all the choices are visible putting more emphasis on the other options).
+
+
+ }
+ donts={
+
+ }
+ related={
+ Fabric JS
+ }
+ isHeaderVisible={ this.props.isHeaderVisible }>
+
+ );
+ }
+}
diff --git a/packages/office-ui-fabric-react/src/components/ComboBox/ComboBoxPage.visualtest.tsx b/packages/office-ui-fabric-react/src/components/ComboBox/ComboBoxPage.visualtest.tsx
new file mode 100644
index 00000000000000..b7e35f4416d958
--- /dev/null
+++ b/packages/office-ui-fabric-react/src/components/ComboBox/ComboBoxPage.visualtest.tsx
@@ -0,0 +1,29 @@
+import { ComboBox } from './index';
+/* tslint:disable:no-unused-variable */
+import * as React from 'react';
+/* tslint:enable:no-unused-variable */
+export default class ComboBoxVPage extends React.Component {
+ public render() {
+ return
+
+
;
+ }
+}
\ No newline at end of file
diff --git a/packages/office-ui-fabric-react/src/components/ComboBox/examples/ComboBox.Basic.Example.scss b/packages/office-ui-fabric-react/src/components/ComboBox/examples/ComboBox.Basic.Example.scss
new file mode 100644
index 00000000000000..f71fcb3e8911cd
--- /dev/null
+++ b/packages/office-ui-fabric-react/src/components/ComboBox/examples/ComboBox.Basic.Example.scss
@@ -0,0 +1,15 @@
+:global {
+ .ms-ComboBoxBasicExample {
+ max-width: 300px;
+ }
+ .ms-ComboBox-optionText {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ min-width: 0px;
+ max-width: 100%;
+ word-wrap: break-word;
+ overflow-wrap: break-word;
+ margin: 1px;
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 00000000000000..dc221e04c6a93c
--- /dev/null
+++ b/packages/office-ui-fabric-react/src/components/ComboBox/examples/ComboBox.Basic.Example.tsx
@@ -0,0 +1,234 @@
+import * as React from 'react';
+import { ComboBox } from 'office-ui-fabric-react/lib/ComboBox';
+import './ComboBox.Basic.Example.scss';
+import {
+ assign,
+ autobind
+} from 'office-ui-fabric-react/lib/Utilities';
+import { ISelectableOption, SelectableOptionMenuItemType } from 'office-ui-fabric-react/lib/utilities/selectableOption/SelectableOption.Props';
+
+export class ComboBoxBasicExample extends React.Component {
+ private _testOptions =
+ [{ key: 'Header', text: 'Theme Fonts', itemType: SelectableOptionMenuItemType.Header },
+ { key: 'A', text: 'Arial Black' },
+ { key: 'B', text: 'Time New Roman' },
+ { key: 'C', text: 'Comic Sans MS' },
+ { 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' },
+ ];
+
+ private _fontMapping: { [key: string]: string } = {
+ ['Arial Black']: '"Arial Black", "Arial Black_MSFontService", sans-serif',
+ ['Time New Roman']: '"Times New Roman", "Times New Roman_MSFontService", serif',
+ ['Comic Sans MS']: '"Comic Sans MS", "Comic Sans MS_MSFontService", fantasy',
+ ['Calibri']: 'Calibri, Calibri_MSFontService, sans-serif'
+ };
+
+ constructor() {
+ super();
+ this.state = {
+ options: [],
+ selectedOptionKey: null,
+ value: 'Calibri'
+ };
+ }
+
+ public render() {
+ let { options, selectedOptionKey, value } = this.state;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ { value ?
+
+ :
+
+ }
+
+
+
+ );
+ }
+
+ // Render content of item
+ @autobind
+ private _onRenderFontOption(item: ISelectableOption): JSX.Element {
+
+ if (item.itemType === SelectableOptionMenuItemType.Header ||
+ item.itemType === SelectableOptionMenuItemType.Divider) {
+ return { item.text };
+ }
+
+ let fontFamily = this._fontMapping[item.text];
+
+ if (fontFamily === null || fontFamily === undefined) {
+ let newFontFamily: string = item.text;
+ if (newFontFamily.indexOf(' ') > -1) {
+ newFontFamily = '"' + newFontFamily + '"';
+ }
+
+ // add a default fallback font
+ newFontFamily += ',"Segoe UI",Tahoma,Sans-Serif';
+
+ this._fontMapping = assign({}, this._fontMapping, { [fontFamily]: newFontFamily });
+ fontFamily = newFontFamily;
+ }
+
+ return { item.text };
+ }
+
+ @autobind
+ private _getOptions(currentOptions: ISelectableOption[]): ISelectableOption[] {
+
+ if (this.state.options.length > 0) {
+ return this.state.options;
+ }
+
+ let 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,
+ selectedOptionKey: 'C1',
+ value: null
+ });
+
+ return newOptions;
+ }
+
+ @autobind
+ private _onChanged(option: ISelectableOption, index: number, value: string) {
+ if (option !== null) {
+ this.setState({
+ selectedOptionKey: option.key,
+ value: null
+ });
+ } else if (index !== null && index >= 0 && index < this.state.options.length) {
+ this.setState({
+ selectedOptionKey: this.state.options[index].key,
+ value: null
+ });
+ } else if (value !== null) {
+ let newOption: ISelectableOption = { key: value, text: value };
+
+ this.setState({
+ options: [...this.state.options, newOption],
+ selectedOptionKey: newOption.key,
+ value: null
+ });
+ }
+ }
+}
diff --git a/packages/office-ui-fabric-react/src/components/ComboBox/index.ts b/packages/office-ui-fabric-react/src/components/ComboBox/index.ts
new file mode 100644
index 00000000000000..fddc1b1ca5ba75
--- /dev/null
+++ b/packages/office-ui-fabric-react/src/components/ComboBox/index.ts
@@ -0,0 +1,2 @@
+export * from './ComboBox';
+export * from './ComboBox.Props';
diff --git a/packages/office-ui-fabric-react/src/components/Dropdown/Dropdown.Props.ts b/packages/office-ui-fabric-react/src/components/Dropdown/Dropdown.Props.ts
index a28fafc3d552f6..2f62833c2eec0c 100644
--- a/packages/office-ui-fabric-react/src/components/Dropdown/Dropdown.Props.ts
+++ b/packages/office-ui-fabric-react/src/components/Dropdown/Dropdown.Props.ts
@@ -1,66 +1,20 @@
-import * as React from 'react';
import { IRenderFunction } from '../../Utilities';
import { Dropdown } from './Dropdown';
-import { ICalloutProps } from '../../Callout';
+import { ISelectableOption } from '../../utilities/selectableOption/SelectableOption.Props';
+import { ISelectableDroppableTextProps } from '../../utilities/selectableOption/SelectableDroppableText.Props';
-export enum DropdownMenuItemType {
- Normal = 0,
- Divider = 1,
- Header = 2
-}
+export { SelectableOptionMenuItemType as DropdownMenuItemType } from '../../utilities/selectableOption/SelectableOption.Props';
export interface IDropdown {
}
-export interface IDropdownProps extends React.Props {
- /**
- * Optional callback to access the IDropdown interface. Use this instead of ref for accessing
- * the public methods and properties of the component.
- */
- componentRef?: (component: IDropdown) => void;
-
- /**
- * Descriptive label for the Dropdown
- */
- label?: string;
-
+export interface IDropdownProps extends ISelectableDroppableTextProps {
/**
* Input placeholder text. Displayed until option is selected.
*/
placeHolder?: string;
- /**
- * Aria Label for the Dropdown for screen reader users.
- */
- ariaLabel?: string;
-
- /**
- * Id of the drop down
- */
- id?: string;
-
- /**
- * If provided, additional class name to provide on the root element.
- */
- className?: string;
-
- /**
- * The key that will be initially used to set a selected item.
- */
- defaultSelectedKey?: string | number;
-
- /**
- * The key of the selected item. If you provide this, you must maintain selection
- * state by observing onChange events and passing a new value in when changed.
- */
- selectedKey?: string | number;
-
- /**
- * Collection of options for this Dropdown
- */
- options?: IDropdownOption[];
-
/**
* Callback issues when the selected option changes
*/
@@ -76,82 +30,14 @@ export interface IDropdownProps extends React.Props {
*/
onRenderTitle?: IRenderFunction;
- /**
- * Optional custom renderer for the dropdown container
- */
- onRenderContainer?: IRenderFunction;
-
- /**
- * Optional custom renderer for the dropdown list
- */
- onRenderList?: IRenderFunction;
-
- /**
- * Optional custom renderer for the dropdown options
- */
- onRenderItem?: IRenderFunction;
-
- /**
- * Optional custom renderer for the dropdown option content
- */
- onRenderOption?: IRenderFunction;
-
- /**
- * Whether or not the Dropdown is disabled.
- */
- disabled?: boolean;
-
- /**
- * Whether or not the Dropdown is required.
- */
- required?: boolean;
-
- /**
- * Custom properties for Dropdown's Callout used to render options.
- */
- calloutProps?: ICalloutProps;
-
/**
* Deprecated at v0.52.0, use 'disabled' instead.
* @deprecated
*/
isDisabled?: boolean;
-
- /**
- * Descriptive label for the Dropdown Error Message
- */
- errorMessage?: string;
}
-export interface IDropdownOption {
- /**
- * Arbitrary string associated with this option.
- */
- key: string | number;
-
- /**
- * Text to render for this option
- */
- text: string;
-
- /**
- * Text to render for this option
- */
- itemType?: DropdownMenuItemType;
-
- /**
- * Index for this option
- */
- index?: number;
-
- /**
- * The aria label for the dropdown option. If not present, the `text` will be used.
- */
- ariaLabel?: string;
-
- /** If option is selected. */
- selected?: boolean;
-
+export interface IDropdownOption extends ISelectableOption {
/**
* Data available to custom onRender functions.
*/
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 e818ad5583f0a4..96f673c8760462 100644
--- a/packages/office-ui-fabric-react/src/components/Dropdown/Dropdown.tsx
+++ b/packages/office-ui-fabric-react/src/components/Dropdown/Dropdown.tsx
@@ -17,6 +17,7 @@ import {
findIndex,
getId
} from '../../Utilities';
+import { SelectableOptionMenuItemType } from '../../utilities/selectableOption/SelectableOption.Props';
import * as stylesImport from './Dropdown.scss';
const styles: any = stylesImport;
@@ -330,9 +331,9 @@ export class Dropdown extends BaseComponent string;
+
+ /**
+ * Handler for checking if the full value of the input should
+ * be seleced in componentDidUpdate
+ *
+ * @returns {boolean} - should the full value of the input be selected?
+ */
+ shouldSelectFullInputValueInComponentDidUpdate?: () => boolean;
+
}
\ No newline at end of file
diff --git a/packages/office-ui-fabric-react/src/components/pickers/AutoFill/BaseAutoFill.tsx b/packages/office-ui-fabric-react/src/components/pickers/AutoFill/BaseAutoFill.tsx
index 0ee0b1865f67dc..a05af2ce803e2a 100644
--- a/packages/office-ui-fabric-react/src/components/pickers/AutoFill/BaseAutoFill.tsx
+++ b/packages/office-ui-fabric-react/src/components/pickers/AutoFill/BaseAutoFill.tsx
@@ -29,7 +29,7 @@ export class BaseAutoFill extends BaseComponent 0) {
- this._inputElement.setSelectionRange(differenceIndex, suggestedDisplayValue.length, SELECTION_BACKWARD);
+
+ if (shouldSelectFullRange) {
+ this._inputElement.setSelectionRange(0, suggestedDisplayValue.length, SELECTION_BACKWARD);
+ } else {
+ while (differenceIndex < value.length && value[differenceIndex].toLocaleLowerCase() === suggestedDisplayValue[differenceIndex].toLocaleLowerCase()) {
+ differenceIndex++;
+ }
+ if (differenceIndex > 0) {
+ this._inputElement.setSelectionRange(differenceIndex, suggestedDisplayValue.length, SELECTION_BACKWARD);
+ }
}
}
}
@@ -99,7 +121,7 @@ export class BaseAutoFill extends BaseComponent;
}
@@ -121,7 +143,11 @@ export class BaseAutoFill extends BaseComponent) {
+ private _onKeyDown(ev: React.KeyboardEvent) {
+ if (this.props.onKeyDown) {
+ this.props.onKeyDown(ev);
+ }
+
switch (ev.which) {
case KeyCodes.backspace:
this._autoFillEnabled = false;
diff --git a/packages/office-ui-fabric-react/src/demo/AppDefinition.tsx b/packages/office-ui-fabric-react/src/demo/AppDefinition.tsx
index 6a02d00c8537d4..f7deda39eb3fe0 100644
--- a/packages/office-ui-fabric-react/src/demo/AppDefinition.tsx
+++ b/packages/office-ui-fabric-react/src/demo/AppDefinition.tsx
@@ -52,6 +52,12 @@ export const AppDefinition: IAppDefinition = {
name: 'ChoiceGroup',
url: '#/examples/choicegroup'
},
+ {
+ component: require('../components/ComboBox/ComboBoxPage').ComboBoxPage,
+ key: 'ComboBox',
+ name: 'ComboBox',
+ url: '#/examples/ComboBox'
+ },
{
component: require('../components/CommandBar/CommandBarPage').CommandBarPage,
key: 'CommandBar',
diff --git a/packages/office-ui-fabric-react/src/index.ts b/packages/office-ui-fabric-react/src/index.ts
index 952bd1f6b4afaa..7bc84c48df80ff 100644
--- a/packages/office-ui-fabric-react/src/index.ts
+++ b/packages/office-ui-fabric-react/src/index.ts
@@ -10,6 +10,7 @@ export * from './Callout';
export * from './Checkbox';
export * from './ChoiceGroup';
export * from './ColorPicker';
+export * from './ComboBox';
export * from './CommandBar';
export * from './ContextualMenu';
export * from './DatePicker';
diff --git a/packages/office-ui-fabric-react/src/utilities/selectableOption/SelectableDroppableText.Props.ts b/packages/office-ui-fabric-react/src/utilities/selectableOption/SelectableDroppableText.Props.ts
new file mode 100644
index 00000000000000..633a04cd68d90d
--- /dev/null
+++ b/packages/office-ui-fabric-react/src/utilities/selectableOption/SelectableDroppableText.Props.ts
@@ -0,0 +1,93 @@
+import * as React from 'react';
+import { IRenderFunction } from '../../Utilities';
+import { ICalloutProps } from '../../Callout';
+import { ISelectableOption } from '../../utilities/selectableOption/SelectableOption.Props';
+
+export interface ISelectableDroppableTextProps extends React.Props {
+ /**
+ * Optional callback to access the ISelectableDroppableText interface. Use this instead of ref for accessing
+ * the public methods and properties of the component.
+ */
+ componentRef?: (component: T) => void;
+
+ /**
+ * Descriptive label for the ISelectableDroppableText
+ */
+ label?: string;
+
+ /**
+ * Aria Label for the ISelectableDroppableText for screen reader users.
+ */
+ ariaLabel?: string;
+
+ /**
+ * Id of the ISelectableDroppableText
+ */
+ id?: string;
+
+ /**
+ * If provided, additional class name to provide on the root element.
+ */
+ className?: string;
+
+ /**
+ * The key that will be initially used to set a selected item.
+ */
+ defaultSelectedKey?: string | number;
+
+ /**
+ * The key of the selected item. If you provide this, you must maintain selection
+ * state by observing onChange events and passing a new value in when changed.
+ */
+ selectedKey?: string | number;
+
+ /**
+ * Collection of options for this ISelectableDroppableText
+ */
+ options?: any;
+
+ /**
+ * Callback issues when the selected option changes
+ */
+ onChanged?: (option: ISelectableOption, index?: number) => void;
+
+ /**
+ * Optional custom renderer for the ISelectableDroppableText container
+ */
+ onRenderContainer?: IRenderFunction>;
+
+ /**
+ * Optional custom renderer for the ISelectableDroppableText list
+ */
+ onRenderList?: IRenderFunction>;
+
+ /**
+ * Optional custom renderer for the ISelectableDroppableText options
+ */
+ onRenderItem?: IRenderFunction;
+
+ /**
+ * Optional custom renderer for the ISelectableDroppableText option content
+ */
+ onRenderOption?: IRenderFunction;
+
+ /**
+ * Whether or not the ISelectableDroppableText is disabled.
+ */
+ disabled?: boolean;
+
+ /**
+ * Whether or not the ISelectableDroppableText is required.
+ */
+ required?: boolean;
+
+ /**
+ * Custom properties for ISelectableDroppableText's Callout used to render options.
+ */
+ calloutProps?: ICalloutProps;
+
+ /**
+ * Descriptive label for the ISelectableDroppableText Error Message
+ */
+ errorMessage?: string;
+}
\ No newline at end of file
diff --git a/packages/office-ui-fabric-react/src/utilities/selectableOption/SelectableOption.Props.ts b/packages/office-ui-fabric-react/src/utilities/selectableOption/SelectableOption.Props.ts
new file mode 100644
index 00000000000000..c58caa9c512cec
--- /dev/null
+++ b/packages/office-ui-fabric-react/src/utilities/selectableOption/SelectableOption.Props.ts
@@ -0,0 +1,35 @@
+export interface ISelectableOption {
+ /**
+ * Arbitrary string associated with this option.
+ */
+ key: string | number;
+
+ /**
+ * Text to render for this option
+ */
+ text: string;
+
+ /**
+ * Text to render for this option
+ */
+ itemType?: SelectableOptionMenuItemType;
+
+ /**
+ * Index for this option
+ */
+ index?: number;
+
+ /**
+ * The aria label for the dropdown option. If not present, the `text` will be used.
+ */
+ ariaLabel?: string;
+
+ /** If option is selected. */
+ selected?: boolean;
+}
+
+export enum SelectableOptionMenuItemType {
+ Normal = 0,
+ Divider = 1,
+ Header = 2
+}
\ No newline at end of file
diff --git a/packages/office-ui-fabric-react/src/utilities/selectableOption/index.ts b/packages/office-ui-fabric-react/src/utilities/selectableOption/index.ts
new file mode 100644
index 00000000000000..4c2fe6688b9930
--- /dev/null
+++ b/packages/office-ui-fabric-react/src/utilities/selectableOption/index.ts
@@ -0,0 +1,2 @@
+export * from './SelectableOption.Props';
+export * from './SelectableDroppableText.Props';
\ No newline at end of file