-
Notifications
You must be signed in to change notification settings - Fork 2.9k
New Component: ComboBox #1920
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
New Component: ComboBox #1920
Conversation
|
@jspurlin, It will cover your contributions to all Microsoft-managed open source projects. |
jair-cazarin
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you check the code coverage of the UTs?
I believe we should move away from inheritance, if there is some code that need to be shared then we should extract and use through composition.
It also feels like ComboBox can be broken down in smaller stateless components for better maintainability and readability.
Regarding the promise, is that a common practice in Fabric? It feels the component should just receive new props when something is done async in the app.
| @@ -1,398 +1,405 @@ | |||
| import * as React from 'react'; | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what's up with all the space changes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't make any space changes... not sure what that was
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the only thing I did add is the combobox piece
| import { IDropdownOption, IDropdownProps } from '../../Dropdown'; | ||
| import { IIconProps } from '../../Icon'; | ||
|
|
||
| export interface IComboBox { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why do you need an empty interface?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, it's not needed was just following the convention in the fabric codebase
| buttonIconProps?: IIconProps; | ||
| } | ||
|
|
||
| export interface IComboBoxOption extends IDropdownOption { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This inheritance is fragile IMO, and some of the fields don't make sense to be inherited, DropdownMenuItemType for example.
Why not extract an ISelectableOption interface that both Dropdown and Combobox can use and have the both components decouple?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, I'll rework this. It does make sense to have the contents of the DropdownMenuItemType although a better name would make it easier to think about
| * Callback issued when the options should be resolved, if they have been updated or | ||
| * if they need to be passed in the first time | ||
| */ | ||
| onResolveOptions?: () => IComboBoxOption[] | PromiseLike<IComboBoxOption[]>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what's the difference between this function returning IComboBoxOption[] and setting options directly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One is handling the full state outside of the component (e.g. setting the options directly) opposed to the uncontrolled case where it may take some time to get the options back but we want the state to be stored inside of the component. The promise allows for the update without blocking
|
|
||
| } | ||
|
|
||
| export interface IComboBoxProps extends IDropdownProps { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we use composition instead of inheritance?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally a combo box is just a dropdown + input text so that should be reflected in the props as well, IMO
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm more conflicted now that the DropDown component is not even used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comboBox doesn't use a dropdown since dropdowns allow you to style the visible collapsed where comboBoxes enforce that the rendering is inside of a <input> element which should not allow for an arbitrary render functionality
| private _inputElement: HTMLInputElement; | ||
| private _autoFillEnabled: boolean = true; | ||
| private _value: string; | ||
| protected _inputElement: HTMLInputElement; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The correct way to do this in React is through composition rather than exposing internals in a base class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll look into changing this to use composition
| /** | ||
| * Font-family associated with this option. | ||
| */ | ||
| fontFamily?: string; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is interesting, isn't this specific to our font type combo box? Isn't there already an onRenderItem or similar that would allow the consumer to specify how to render each item?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True, but I felt like this aspect is scoped enough to expose it as well without making the creator create an onRenderOption
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question for you @jair-cazarin : Do you want to handle this with onRenderOption for our use case? If so I can remove the IComboBoxOption
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it feels very specific to that particular ComboBox IMO
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so I'd vote to remove it
| * @param index - the index to check | ||
| * @returns {boolean} - true if the index is valid for the given options, false otherwise | ||
| */ | ||
| @autobind |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does this also need to be autobind?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nope, removed
| * allows freeform entry | ||
| * @param updatedValue - the input's newly changed value | ||
| */ | ||
| @autobind |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
doesn't need autobind
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you make sure only the methods that need to be bound are decorated with autobind? there is a perf overhead otherwise.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
| selectedIndex: newSelectedIndex | ||
| }); | ||
|
|
||
| if (onChanged) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should this be called after setState is done?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same response as above
|
Can you add a few images to the description so that we know what you're building here? @jspurlin |
|
I would assume it's just a dropdown that you can type in? It looks like we have this in SharePoint already. @yiminwu might be able to help. |
|
@betrue-final-final We already have a dropdown... @jspurlin So what is this component? Why would a customer use this vs dropdown? |
dzearing
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
block until we get some clarification on what dropdown isn't providing.
jspurlin
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replying to the existing comments
|
got it. screenshots would be handy |
|
Added some more details to the description which lays out why this component ended up not either being a dropdown or being composed of a dropdown (as well as a gif showing off the functionality in the different states). My intention was to add those details earlier, but I was getting pulled other directions. Take a look and I'd like to get your input after the update. Note that the gif show performing similar actions across the different permutations of autoComplete and allowFreeform. |
…ecoupled the shared props/memebers from comboBox and Dropdown, and extended BaseAutoFill which allowed DynamicAutoFill to be removed
…nderOption instead. The fontFamily aspect was too specific for the generic comboBox
| "@microsoft/web-library-build": { | ||
| "version": "3.0.2", | ||
| "resolved": "https://registry.npmjs.org/@microsoft/web-library-build/-/web-library-build-3.0.2.tgz", | ||
| "integrity": "sha1-DldYaol/ddjdDAKdKVtBapg4f5w=", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you actually need to change the ShrinkWrap file?
| buttonIconProps | ||
| } = this.props; | ||
| let { isOpen, selectedIndex, focused, suggestedDisplayValue, currentOptions } = this.state; | ||
| let selectedOption = currentOptions[selectedIndex]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could selectedIndex be -1 when rendering?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, that's a valid state and means that none of the options are selected
| return ( | ||
| <div ref='root' className={ css('ms-ComboBox-container') }> | ||
| { label && ( | ||
| <Label className={ css('ms-ComboBox-label') } id={ id + '-label' } ref={ this._resolveRef('_comboBoxLabel') } required={ required } htmlFor={ id }>{ label }</Label> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why do you need to capture the reference of the Label?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we don't, fixed
| * @param {IBaseAutoFillProps} nextProps - the props that got passed in to the auto fill's componentWillReceiveProps | ||
| * @returns {string} - the updated value to set, if needed | ||
| */ | ||
| onComponentWillReceiveProps?: (nextProps: IBaseAutoFillProps) => string; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typically lifecycle methods like this are used to update the component state in case of new props so exposing a callback with this name is quite weird. Can you actually rename these handlers to convey what they actually do?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and perhaps instead of exposing the entire set of props only the ones the ones you care about? It should be ideally only the ones that are in the local state (other props were set by the parent anyhow).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, we already have the props and we don't need to pass them back... removed the parameter and renamed the callbacks
| private _inputElement: HTMLInputElement; | ||
| private _autoFillEnabled: boolean = true; | ||
| private _value: string; | ||
| protected _inputElement: HTMLInputElement; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
these don't have to be protected anymore right?
| 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 !== DropdownMenuItemType.Header && option.itemType !== DropdownMenuItemType.Divider).filter((option) => option.text.toLocaleLowerCase().indexOf(updatedValue) === 0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do you need the two filters or can it be done with one?
| } 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 !== DropdownMenuItemType.Header && option.itemType !== DropdownMenuItemType.Divider).filter((option) => option.text.toLocaleLowerCase() === updatedValue); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
perhas extract to a function so that it can be shared with the above?
| 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 !== DropdownMenuItemType.Header && option.itemType !== DropdownMenuItemType.Divider).filter((option) => option.text.toLocaleLowerCase().indexOf(updatedValue) === 0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
another case where we can reuse the same function
| } | ||
|
|
||
| // If we get here, either autoComplete is on or we did not find a match with autoComplete on. | ||
| // Remeber we are not allowing freeform, so at this point, if we have a pending valid value index |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: Remember
|
|
||
| let newIndex = index + searchDirection; | ||
|
|
||
| newIndex = Math.max(0, Math.min(currentOptions.length - 1, newIndex)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this get in an infinite loop?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, if you reach one of the ends of the array and are still at an option which shouldn't get focus, we won't make another call. Also, if the caller didn't tell us which way to move we won't try
| @@ -1,428 +0,0 @@ | |||
| import * as React from 'react'; | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mm this is weird, bad merge?
* Enable no implicit any in the utilities package (microsoft#1970) * Fix no implicit anys in utilities package. * Rush change * Reverse all shrinkwrap changes except the typings one. * Fix bundle minification to exclude debug warnings. (microsoft#1973) * Updating shrinkwrap, rush, and making minify build have production flag to remove debug code. * Updates. * removing lockfile. * dropping to 7. Moving back to npm run build. * Downgrading rush. * Applying package updates. * Website: Update dev.office.com header (microsoft#1966) * Use Fabric Core 7.1.0 for the website * Adjust position of caret's in header so that they spin around central axis * Update to latest icon font from dev.office.com for header and move outside out :global{} to fix build issue * Remove a u- prefix that was missed earlier * Update dev.office.com header with the latest navigation links * Add change file * Make sure the quote rule is enabled for tsline (microsoft#1961) * With responsive mode error (microsoft#1956) * withResponsiveMode: Adding error handling around the case where window.innerWidth throws an exception * adding change log file * Create withResponsiveMode.tsx
* First step at stepper implementation. * Add first implementation of stepper. * Add functionality to stepper * Refine the Stepper class and add tests * let's make sure to put focus back on the text field when submitting via enter * Added documentation to Stepper. * Add flexibility to current stepper implementation. * Modified example implementations. * Add aria-valuemax. * Change Stepper to SpinButton. * Add example with unit. * Implement color scheme in the ContextualMenu control to enable alternative theming. * Improvements to SpinButton. * Fix increment function calls. * Add new width optional parameter. * Add label direction. * Fix border. * Add Position enum. * `defaultValue` is now the deciding prop for using the default implementation or not. * onBlur is now onValidate. * Fix tests. * Fix warnings. * Add implementation for labelGap. * Put some polish on the styling, added some icon support, and added some more example spinButtons * Implement the bar and unit tests and component page * Add the ability for the spinButton buttons to look pressed when spinning via keyboard * Revert "Implement color scheme in the ContextualMenu control to enable alternative theming." This reverts commit 4f830cd. * Don't render an empty icon for an icon-less header menu item. * Revert "Implement Document Title Bar" * update some CSS for high contrast in ff and use css utility instead of concatenating string classnames * Fix quotation issue * Fix Spin Button properties table. * Fix Spin Button example code * Use iconProps instead of string * Extracted `spinning` out of state * Add autobind instead of manually binding private functions to this * Change `+` syntax for more explicit `Number()` * Remove unnecessary cast * Fix typos * `incrementButtonIcon` and `decrementButtonIcon` are now IIconProps * Add KeyboardSpinDirection enum * Fix test description * Fix SpinButton tests * Remove unused onChange callback from SpinButton * Revert onChange * Remove old Stepper.ts file * Use module css instead of global * Fix missing word in comment * Callback functions now allow for void return (state to be updated outside) * Use `_async` instead of window * Fix minor rendering issue with browser zoom * Rename `_spinning` to `_spinningByMouse` for clarity * Fix tests * Fix extra space before label * Remove width outside of SpinButton component and fix styling * Add more tests to SpinButton * Fix SpinButton documentation * Fix typo * Fix AppDefinition for SpinButton and Spinner * Add missing documentation to SpinButton title prop * Various SpinButton fixes * Fix SpinButton path for properties * Fix SpinButton styling issues * Remove labelGap property from SpinButton
…c-react into jspurlin/ComboBox # Conflicts: # packages/office-ui-fabric-react/src/components/Dropdown/examples/Dropdown.Basic.Example.tsx
| let newOptions: ISelectableOption[] | PromiseLike<ISelectableOption[]> = | ||
| this.props.onResolveOptions({ ...this.state.currentOptions }); | ||
|
|
||
| // the options are either goingto be an array or a promise |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you remove this? I don't think it really adds anything to the code, it's pretty clear from the above it can be either type.
| } | ||
|
|
||
| // Render Callout container and pass in list | ||
| @autobind |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think any of these render functions need @autoBind
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes they do need autobind since they reference "this" which won't be the expected this. I had narrowed down the autobind usages already every one that's there now needs to be there and would throw an error otherwise
| it('Can change items in uncontrolled case', () => { | ||
| let comboBoxRoot; | ||
|
|
||
| try { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we remove the try/finally's? I don't think they are needed...if the test throws...it should fail and the finally is not doing any clean up or similar.
| * in to the auto fill's componentWillReceiveProps | ||
| * @returns {string} - the updated value to set, if needed | ||
| */ | ||
| updateValueInWillReceiceProps?: () => string; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: there is a typo in here. Is it common to add the lifecycle function where these prop functions are used in Fabric? It looks to me as if the name itself is leaking already implementation details of the component.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it still feels weird that we are extending BaseAutoFill this way, would composition achieve the same in a more react idiomatic way? Looking at the code, doesn't seem to be that straightforward.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed the typo. The name is being explicit about where the callback is being plugged in to. This functionality was created so that I could reuse the class instead of having to have almost duplicate functionality except for the changes in these lifecycle functions
| * 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is used above like: ISelectableDroppableTextProps, so the componentReference is really of type IComboBoxProps, is that the intention?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated so that the componentRef is really IComboBox instead
| /** | ||
| * Collection of options for this ISelectableDroppableText | ||
| */ | ||
| options?: any; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what's the reason this became any?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To allow for the options to be generic enough so that different implementers can utilize this as they see fit
|
|
||
| @autobind | ||
| private _onChanged(option: ISelectableOption, index: number, value: string) { | ||
| if (option != null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it would be great to be consistent between the usages of != and !==
|
So glad that this component is getting build. What's ETA on this, so that Microsoft Flow team can start utilizing it. |
|
This was merged several weeks ago. It is already available in the latest fabric releases |
|
Forgive my ignorance, but we use this site: https://dev.office.com/fabric#/components to understand what components are available for consumption (as well as to understand best practices, etc...). Is there any sort of gate to ensure that as new components (such as ComboBox) are added they get put on that above page so that consumers can be aware of them? Thanks! |

Pull request checklist
$ npm run changeDescription of changes
Add a new combobox component.
A few notes to how this differs from a dropdown:
<input>element for displaying the collapsed value to align more closely with the correct accessibility representation of a comboboxThe gif shows:
Another side note: if allowFreeform is false, clicking on the combobox will expand the combobox as opposed to when it is true, it places an IP. Also allowFreeform allows the user to enter new options and they will get added to the options, in the uncontrolled case, or will be passed back as the value in the onChanged callback
Focus areas to test
The input and selection of options with both mouse and keyboard (via text input and arrow keys) as well as getting the option on initial render or dynamically on focus