From 675bcd73f2ddb0741988b4278741c7e0de26c5ef Mon Sep 17 00:00:00 2001 From: Tom Robinson <tlrobinson@gmail.com> Date: Wed, 28 Feb 2018 17:44:46 +0700 Subject: [PATCH] Various FieldValuesWidget polish --- .../metabase/components/FieldValuesWidget.jsx | 98 ++++++++++------ .../src/metabase/components/TokenField.jsx | 84 ++++++++------ .../widgets/ParameterFieldWidget.jsx | 24 +++- .../components/GuiQueryEditor.jsx | 2 +- .../components/filters/FilterPopover.jsx | 10 +- .../components/filters/FilterWidget.jsx | 2 +- .../test/components/TokenField.unit.spec.js | 106 +++++++++++------- .../components.unit.spec.js.snap | 8 ++ frontend/test/pulse/pulse.unit.spec.js | 4 +- 9 files changed, 219 insertions(+), 119 deletions(-) diff --git a/frontend/src/metabase/components/FieldValuesWidget.jsx b/frontend/src/metabase/components/FieldValuesWidget.jsx index a84182116b6..5bfe6afcf59 100644 --- a/frontend/src/metabase/components/FieldValuesWidget.jsx +++ b/frontend/src/metabase/components/FieldValuesWidget.jsx @@ -20,6 +20,7 @@ import { stripId } from "metabase/lib/formatting"; import type Field from "metabase-lib/lib/metadata/Field"; import type { FieldId } from "metabase/meta/types/Field"; import type { Value } from "metabase/meta/types/Dataset"; +import type { LayoutRendererProps } from "metabase/components/TokenField"; const MAX_SEARCH_RESULTS = 100; @@ -42,10 +43,10 @@ type Props = { placeholder?: string, maxWidth?: number, minWidth?: number, + alwaysShowOptions?: boolean, }; type State = { - focused: boolean, loadingState: "INIT" | "LOADING" | "LOADED", options: [Value, ?string][], lastValue: string, @@ -61,7 +62,6 @@ export class FieldValuesWidget extends Component { constructor(props: Props) { super(props); this.state = { - focused: false, options: [], loadingState: "INIT", lastValue: "", @@ -189,6 +189,36 @@ export class FieldValuesWidget extends Component { } }, 500); + renderOptions({ + optionsList, + isFocused, + isAllSelected, + }: LayoutRendererProps) { + const { alwaysShowOptions, field, searchField } = this.props; + const { loadingState } = this.state; + if (alwaysShowOptions || isFocused) { + if (optionsList) { + return optionsList; + } else if (this.hasList()) { + if (isAllSelected) { + return <EveryOptionState />; + } + } else if (this.isSearchable()) { + if (loadingState === "INIT") { + return alwaysShowOptions && <SearchState />; + } else if (loadingState === "LOADING") { + return <LoadingState />; + } else if (loadingState === "LOADED") { + if (isAllSelected) { + return alwaysShowOptions && <SearchState />; + } else { + return <NoMatchState field={searchField || field} />; + } + } + } + } + } + render() { const { value, @@ -270,25 +300,10 @@ export class FieldValuesWidget extends Component { autoLoad={false} /> )} - layoutRenderer={({ valuesList, optionsList, isFiltered }) => ( + layoutRenderer={props => ( <div> - {valuesList} - {this.props.alwaysShowOptions || this.state.focused - ? optionsList || - (this.hasList() && !isFiltered ? ( - <OptionsMessage - message={t`Including every option in your filter probably won’t do much…`} - /> - ) : this.isSearchable() && loadingState === "LOADED" ? ( - <OptionsMessage - message={jt`No matching ${( - <strong> - {(searchField || field).display_name} - </strong> - )} found.`} - /> - ) : null) - : null} + {props.valuesList} + {this.renderOptions(props)} </div> )} filterOption={(option, filterString) => @@ -319,29 +334,38 @@ export class FieldValuesWidget extends Component { } return v; }} - onFocus={() => this.setState({ focused: true })} - onBlur={() => this.setState({ focused: false })} /> - {this.isSearchable() && loadingState === "INIT" ? ( - <div - className="flex layout-centered align-center" - style={{ minHeight: 100 }} - > - <Icon name="search" size={35} className="text-grey-1" /> - </div> - ) : this.isSearchable() && loadingState === "LOADING" ? ( - <div - className="flex layout-centered align-center" - style={{ minHeight: 100 }} - > - <LoadingSpinner size={32} /> - </div> - ) : null} </div> ); } } +const LoadingState = () => ( + <div className="flex layout-centered align-center" style={{ minHeight: 100 }}> + <LoadingSpinner size={32} /> + </div> +); + +const SearchState = () => ( + <div className="flex layout-centered align-center" style={{ minHeight: 100 }}> + <Icon name="search" size={35} className="text-grey-1" /> + </div> +); + +const NoMatchState = ({ field }) => ( + <OptionsMessage + message={jt`No matching ${( + <strong> {field.display_name} </strong> + )} found.`} + /> +); + +const EveryOptionState = () => ( + <OptionsMessage + message={t`Including every option in your filter probably won’t do much…`} + /> +); + const OptionsMessage = ({ message }) => ( <div className="flex layout-centered p4">{message}</div> ); diff --git a/frontend/src/metabase/components/TokenField.jsx b/frontend/src/metabase/components/TokenField.jsx index 816e7597c6a..d3559b67d2f 100644 --- a/frontend/src/metabase/components/TokenField.jsx +++ b/frontend/src/metabase/components/TokenField.jsx @@ -23,13 +23,21 @@ import { import { isObscured } from "metabase/lib/dom"; const inputBoxClasses = cxs({ - maxHeight: "130px", + maxHeight: 130, overflow: "scroll", }); type Value = any; type Option = any; +export type LayoutRendererProps = { + valuesList: React$Element<any>, + optionsList: ?React$Element<any>, + isFocused: boolean, + isAllSelected: boolean, + onClose: () => void, +}; + type Props = { value: Value[], onChange: (value: Value[]) => void, @@ -61,12 +69,7 @@ type Props = { valueRenderer: (value: Value) => React$Element<any>, optionRenderer: (option: Option) => React$Element<any>, - layoutRenderer: ({ - valuesList: React$Element<any>, - optionsList: ?React$Element<any>, - focused: boolean, - onClose: () => void, - }) => React$Element<any>, + layoutRenderer: (props: LayoutRendererProps) => React$Element<any>, }; type State = { @@ -74,7 +77,8 @@ type State = { searchValue: string, filteredOptions: Option[], selectedOptionValue: ?Value, - focused: boolean, + isFocused: boolean, + isAllSelected: boolean, listIsHovered: boolean, }; @@ -93,7 +97,8 @@ export default class TokenField extends Component { searchValue: "", filteredOptions: [], selectedOptionValue: null, - focused: props.autoFocus || false, + isFocused: props.autoFocus || false, + isAllSelected: false, listIsHovered: false, }; } @@ -204,18 +209,30 @@ export default class TokenField extends Component { String(this._label(option) || "").indexOf(searchValue) >= 0; } - let filteredOptions = options.filter( - option => - // filter out options who have already been selected, unless: + let selectedCount = 0; + let filteredOptions = options.filter(option => { + const isSelected = selectedValues.has( + JSON.stringify(this._value(option)), + ); + const isLastFreeform = + this._isLastFreeformValue(this._value(option)) && + this._isLastFreeformValue(searchValue); + const isMatching = filterOption(option, searchValue); + if (isSelected) { + selectedCount++; + } + // filter out options who have already been selected, unless: + return ( // remove selected is disabled (!removeSelected || // or it's not in the selectedValues - !selectedValues.has(JSON.stringify(this._value(option))) || + !isSelected || // or it's the current "freeform" value, which updates as we type - (this._isLastFreeformValue(this._value(option)) && - this._isLastFreeformValue(searchValue))) && - filterOption(option, searchValue), - ); + isLastFreeform) && + // and it's matching + isMatching + ); + }); if ( selectedOptionValue == null || @@ -235,6 +252,7 @@ export default class TokenField extends Component { this.setState({ filteredOptions, selectedOptionValue, + isAllSelected: options.length > 0 && selectedCount === options.length, }); }; @@ -324,7 +342,7 @@ export default class TokenField extends Component { if (this.props.onFocus) { this.props.onFocus(); } - this.setState({ focused: true, searchValue: this.state.inputValue }, () => + this.setState({ isFocused: true, searchValue: this.state.inputValue }, () => this._updateFilteredValues(this.props), ); }; @@ -333,9 +351,7 @@ export default class TokenField extends Component { if (this.props.onBlur) { this.props.onBlur(); } - setTimeout(() => { - this.setState({ focused: false }); - }, 100); + this.setState({ isFocused: false }); }; onInputPaste = (e: SyntheticClipboardEvent) => { @@ -364,7 +380,7 @@ export default class TokenField extends Component { }; onClose = () => { - this.setState({ focused: false }); + this.setState({ isFocused: false }); }; addSelectedOption(e: SyntheticKeyboardEvent) { @@ -481,11 +497,12 @@ export default class TokenField extends Component { inputValue, searchValue, filteredOptions, - focused, + isFocused, + isAllSelected, selectedOptionValue, } = this.state; - if (!multi && focused) { + if (!multi && isFocused) { inputValue = inputValue || value[0]; value = []; } @@ -497,7 +514,7 @@ export default class TokenField extends Component { parseFreeformValue && value[value.length - 1] === parseFreeformValue(inputValue) ) { - if (focused) { + if (isFocused) { // if focused, don't render the last value value = value.slice(0, -1); } else { @@ -507,7 +524,7 @@ export default class TokenField extends Component { } // if not focused we won't get key events to accept the selected value, so don't render as selected - if (!focused) { + if (!isFocused) { selectedOptionValue = null; } @@ -522,7 +539,7 @@ export default class TokenField extends Component { "m1 p0 pb1 bordered rounded flex flex-wrap bg-white scroll-x scroll-y", inputBoxClasses, { - [`border-grey-2`]: this.state.focused, + [`border-grey-2`]: this.state.isFocused, }, )} style={this.props.style} @@ -544,6 +561,7 @@ export default class TokenField extends Component { this.removeValue(v); e.preventDefault(); }} + onMouseDown={e => e.preventDefault()} > <Icon name="close" className="" size={12} /> </a> @@ -558,7 +576,7 @@ export default class TokenField extends Component { size={10} placeholder={placeholder} value={inputValue} - autoFocus={focused} + autoFocus={isFocused} onKeyDown={this.onInputKeyDown} onChange={this.onInputChange} onFocus={this.onInputFocus} @@ -603,6 +621,7 @@ export default class TokenField extends Component { this.clearInputValue(filteredOptions.length === 1); e.preventDefault(); }} + onMouseDown={e => e.preventDefault()} > {optionRenderer(option)} </div> @@ -614,7 +633,8 @@ export default class TokenField extends Component { return layoutRenderer({ valuesList, optionsList, - focused, + isFocused, + isAllSelected, isFiltered: !!searchValue, onClose: this.onClose, }); @@ -626,14 +646,14 @@ const dedup = array => Array.from(new Set(array)); const DefaultTokenFieldLayout = ({ valuesList, optionsList, - focused, + isFocused, onClose, }) => ( <OnClickOutsideWrapper handleDismissal={onClose}> <div> {valuesList} <Popover - isOpen={focused && !!optionsList} + isOpen={isFocused && !!optionsList} hasArrow={false} tetherOptions={{ attachment: "top left", @@ -650,6 +670,6 @@ const DefaultTokenFieldLayout = ({ DefaultTokenFieldLayout.propTypes = { valuesList: PropTypes.element.isRequired, optionsList: PropTypes.element, - focused: PropTypes.bool, + isFocused: PropTypes.bool, onClose: PropTypes.func, }; diff --git a/frontend/src/metabase/parameters/components/widgets/ParameterFieldWidget.jsx b/frontend/src/metabase/parameters/components/widgets/ParameterFieldWidget.jsx index fbc50d0f224..7edef827224 100644 --- a/frontend/src/metabase/parameters/components/widgets/ParameterFieldWidget.jsx +++ b/frontend/src/metabase/parameters/components/widgets/ParameterFieldWidget.jsx @@ -1,6 +1,7 @@ /* @flow */ import React, { Component } from "react"; +import ReactDOM from "react-dom"; import { t } from "c-3po"; @@ -24,18 +25,24 @@ type Props = { type State = { value: any[], isFocused: boolean, + widgetWidth: ?number, }; +const BORDER_WIDTH = 2; + // TODO: rename this something else since we're using it for more than searching and more than text export default class ParameterFieldWidget extends Component<*, Props, State> { props: Props; state: State; + _unfocusedElement: React$Component<any, any, any>; + constructor(props: Props) { super(props); this.state = { isFocused: false, value: props.value, + widgetWidth: null, }; } @@ -58,6 +65,16 @@ export default class ParameterFieldWidget extends Component<*, Props, State> { } } + componentDidUpdate() { + let element = ReactDOM.findDOMNode(this._unfocusedElement); + if (!this.state.isFocused && element) { + const parameterWidgetElement = element.parentNode.parentNode.parentNode; + if (parameterWidgetElement.clientWidth !== this.state.widgetWidth) { + this.setState({ widgetWidth: parameterWidgetElement.clientWidth }); + } + } + } + render() { let { setValue, isEditing, field, parentFocusChanged } = this.props; let { value, isFocused } = this.state; @@ -82,6 +99,7 @@ export default class ParameterFieldWidget extends Component<*, Props, State> { if (!isFocused) { return ( <div + ref={_ => (this._unfocusedElement = _)} className="flex-full cursor-pointer" onClick={() => focusChanged(true)} > @@ -115,8 +133,10 @@ export default class ParameterFieldWidget extends Component<*, Props, State> { autoFocus color="brand" style={{ - borderWidth: 2, - minWidth: 182, + borderWidth: BORDER_WIDTH, + minWidth: this.state.widgetWidth + ? this.state.widgetWidth + BORDER_WIDTH * 2 + : null, }} maxWidth={400} /> diff --git a/frontend/src/metabase/query_builder/components/GuiQueryEditor.jsx b/frontend/src/metabase/query_builder/components/GuiQueryEditor.jsx index 9e81615d30c..3134d5503f9 100644 --- a/frontend/src/metabase/query_builder/components/GuiQueryEditor.jsx +++ b/frontend/src/metabase/query_builder/components/GuiQueryEditor.jsx @@ -171,7 +171,7 @@ export default class GuiQueryEditor extends Component { triggerElement={addFilterButton} triggerClasses="flex align-center" getTarget={() => this.refs.addFilterTarget} - horizontalAttachments={["left"]} + horizontalAttachments={["left", "center"]} autoWidth > <FilterPopover diff --git a/frontend/src/metabase/query_builder/components/filters/FilterPopover.jsx b/frontend/src/metabase/query_builder/components/filters/FilterPopover.jsx index be62084eb66..df9ea37b2be 100644 --- a/frontend/src/metabase/query_builder/components/filters/FilterPopover.jsx +++ b/frontend/src/metabase/query_builder/components/filters/FilterPopover.jsx @@ -202,7 +202,7 @@ export default class FilterPopover extends Component { renderPicker(filter: FieldFilter, field: Field) { let operator: ?Operator = field.operators_lookup[filter[0]]; - return ( + let fieldWidgets = operator && operator.fields.map((operatorField, index) => { if (!operator) { @@ -279,8 +279,12 @@ export default class FilterPopover extends Component { {operator.multi ? t`true` : t`false`} </span> ); - }) - ); + }); + if (fieldWidgets && fieldWidgets.filter(f => f).length > 0) { + return fieldWidgets; + } else { + return <div className="mb1" />; + } } onCommit = () => { diff --git a/frontend/src/metabase/query_builder/components/filters/FilterWidget.jsx b/frontend/src/metabase/query_builder/components/filters/FilterWidget.jsx index e547c0dae0f..00601621387 100644 --- a/frontend/src/metabase/query_builder/components/filters/FilterWidget.jsx +++ b/frontend/src/metabase/query_builder/components/filters/FilterWidget.jsx @@ -159,7 +159,7 @@ export default class FilterWidget extends Component { className="FilterPopover" isInitiallyOpen={this.props.filter[1] === null} onClose={this.close} - horizontalAttachments={["left"]} + horizontalAttachments={["left", "center"]} autoWidth > <FilterPopover diff --git a/frontend/test/components/TokenField.unit.spec.js b/frontend/test/components/TokenField.unit.spec.js index f50194da904..e13be0899b6 100644 --- a/frontend/test/components/TokenField.unit.spec.js +++ b/frontend/test/components/TokenField.unit.spec.js @@ -62,21 +62,26 @@ class TokenFieldWithStateAndDefaults extends React.Component { } describe("TokenField", () => { - let component, input; + let component; + const input = () => component.find("input"); const value = () => component.state().value; const options = () => component.find(MockOption).map(o => o.text()); const values = () => component.find(MockValue).map(v => v.text()); - const blur = () => input.simulate("blur"); - const focus = () => input.simulate("focus"); - const type = str => input.simulate("change", { target: { value: str } }); + const blur = () => input().simulate("blur"); + const focus = () => input().simulate("focus"); + const type = str => input().simulate("change", { target: { value: str } }); const focusAndType = str => focus() && type(str); - const keyDown = keyCode => input.simulate("keydown", { keyCode }); + const keyDown = keyCode => input().simulate("keydown", { keyCode }); const clickOption = (n = 0) => component .find(MockOption) .at(n) .simulate("click"); + afterEach(() => { + component = null; + }); + it("should render with no options or values", () => { component = mount(<TokenFieldWithStateAndDefaults />); expect(values()).toEqual([]); @@ -112,8 +117,6 @@ describe("TokenField", () => { options={["bar", "baz"]} />, ); - input = component.find("input"); - focusAndType("nope"); expect(options()).toEqual([]); type("bar"); @@ -128,7 +131,6 @@ describe("TokenField", () => { parseFreeformValue={value => value} />, ); - input = component.find("input"); focusAndType("yep"); expect(value()).toEqual([]); keyDown(KEYCODE_ENTER); @@ -140,10 +142,7 @@ describe("TokenField", () => { <TokenFieldWithStateAndDefaults value={[]} options={["bar", "baz"]} />, ); expect(value()).toEqual([]); - component - .find(MockOption) - .first() - .simulate("click"); + clickOption(0); expect(value()).toEqual(["bar"]); }); @@ -152,10 +151,7 @@ describe("TokenField", () => { <TokenFieldWithStateAndDefaults value={[]} options={["bar", "baz"]} />, ); expect(options()).toEqual(["bar", "baz"]); - component - .find(MockOption) - .first() - .simulate("click"); + clickOption(0); await delay(100); expect(options()).toEqual(["baz"]); }); @@ -164,15 +160,11 @@ describe("TokenField", () => { component = mount( <TokenFieldWithStateAndDefaults value={[]} options={["foo", "bar"]} />, ); - input = component.find("input"); focus(); expect(value()).toEqual([]); type("ba"); - component - .find(MockOption) - .first() - .simulate("click"); + clickOption(0); expect(value()).toEqual(["bar"]); }); @@ -187,7 +179,6 @@ describe("TokenField", () => { updateOnInputChange />, ); - input = component.find("input"); }); it("should add freeform value immediately if updateOnInputChange is provided", () => { @@ -200,13 +191,9 @@ describe("TokenField", () => { focusAndType("Do"); expect(value()).toEqual(["Do"]); - // click the first option - component - .find(MockOption) - .first() - .simulate("click"); + clickOption(0); expect(value()).toEqual(["Doohickey"]); - expect(input.props().value).toEqual(""); + expect(input().props().value).toEqual(""); }); it("should only add one option when filtered and enter is pressed", async () => { @@ -217,7 +204,7 @@ describe("TokenField", () => { // press enter keyDown(KEYCODE_ENTER); expect(value()).toEqual(["Doohickey"]); - expect(input.props().value).toEqual(""); + expect(input().props().value).toEqual(""); }); it("shouldn't hide option matching input freeform value", () => { @@ -250,7 +237,7 @@ describe("TokenField", () => { expect(options()).toEqual(["Gadget", "Gizmo"]); keyDown(KEYCODE_ENTER); expect(options()).toEqual(["Gizmo"]); - expect(component.find("input").props().value).toEqual(""); + expect(input().props().value).toEqual(""); }); it("should reset the search when focusing", () => { @@ -316,7 +303,6 @@ describe("TokenField", () => { onChange={spy} />, ); - input = component.find("input"); // limit our options by typing focusAndType("G"); @@ -324,7 +310,7 @@ describe("TokenField", () => { // the initially selected option should be the first option expect(component.state().selectedOptionValue).toBe(DEFAULT_OPTIONS[1]); - input.simulate("keydown", { + input().simulate("keydown", { keyCode: KEYCODE_DOWN, preventDefault: jest.fn(), }); @@ -332,7 +318,7 @@ describe("TokenField", () => { // the next possible option should be selected now expect(component.state().selectedOptionValue).toBe(DEFAULT_OPTIONS[2]); - input.simulate("keydown", { + input().simulate("keydown", { keyCode: key, preventDefalut: jest.fn(), }); @@ -355,9 +341,8 @@ describe("TokenField", () => { multi />, ); - input = component.find("input"); focusAndType("asdf"); - input.simulate("keydown", { + input().simulate("keydown", { keyCode: KEYCODE_TAB, preventDefault: preventDefault, }); @@ -373,8 +358,7 @@ describe("TokenField", () => { multi />, ); - input = component.find("input"); - input.simulate("paste", { + input().simulate("paste", { clipboardData: { getData: () => "1,2,3", }, @@ -396,9 +380,8 @@ describe("TokenField", () => { updateOnInputChange />, ); - input = component.find("input"); focusAndType("asdf"); - input.simulate("keydown", { + input().simulate("keydown", { keyCode: KEYCODE_TAB, preventDefault: preventDefault, }); @@ -413,8 +396,7 @@ describe("TokenField", () => { updateOnInputChange />, ); - input = component.find("input"); - input.simulate("paste", { + input().simulate("paste", { clipboardData: { getData: () => "1,2,3", }, @@ -425,4 +407,46 @@ describe("TokenField", () => { expect(preventDefault).toHaveBeenCalled(); }); }); + + describe("custom layoutRenderer", () => { + let layoutRenderer; + beforeEach(() => { + layoutRenderer = jest + .fn() + .mockImplementation(({ valuesList, optionsList }) => ( + <div> + {valuesList} + {optionsList} + </div> + )); + component = mount( + <TokenFieldWithStateAndDefaults + options={["hello"]} + layoutRenderer={layoutRenderer} + />, + ); + }); + it("should be called with isFiltered=true when filtered", () => { + let call = layoutRenderer.mock.calls.pop(); + expect(call[0].isFiltered).toEqual(false); + expect(call[0].isAllSelected).toEqual(false); + focus(); + type("blah"); + call = layoutRenderer.mock.calls.pop(); + expect(call[0].optionList).toEqual(undefined); + expect(call[0].isFiltered).toEqual(true); + expect(call[0].isAllSelected).toEqual(false); + }); + it("should be called with isAllSelected=true when all options are selected", () => { + let call = layoutRenderer.mock.calls.pop(); + expect(call[0].isFiltered).toEqual(false); + expect(call[0].isAllSelected).toEqual(false); + focus(); + keyDown(KEYCODE_ENTER); + call = layoutRenderer.mock.calls.pop(); + expect(call[0].optionList).toEqual(undefined); + expect(call[0].isFiltered).toEqual(false); + expect(call[0].isAllSelected).toEqual(true); + }); + }); }); diff --git a/frontend/test/internal/__snapshots__/components.unit.spec.js.snap b/frontend/test/internal/__snapshots__/components.unit.spec.js.snap index 3c6895034a8..819163c5566 100644 --- a/frontend/test/internal/__snapshots__/components.unit.spec.js.snap +++ b/frontend/test/internal/__snapshots__/components.unit.spec.js.snap @@ -794,6 +794,7 @@ exports[`TokenField should render "" correctly 1`] = ` <div className="py1 pl1 pr2 block rounded text-bold text-brand-hover inline-block full cursor-pointer bg-grey-0-hover" onClick={[Function]} + onMouseDown={[Function]} > <span> Doohickey @@ -804,6 +805,7 @@ exports[`TokenField should render "" correctly 1`] = ` <div className="py1 pl1 pr2 block rounded text-bold text-brand-hover inline-block full cursor-pointer bg-grey-0-hover" onClick={[Function]} + onMouseDown={[Function]} > <span> Gadget @@ -814,6 +816,7 @@ exports[`TokenField should render "" correctly 1`] = ` <div className="py1 pl1 pr2 block rounded text-bold text-brand-hover inline-block full cursor-pointer bg-grey-0-hover" onClick={[Function]} + onMouseDown={[Function]} > <span> Gizmo @@ -824,6 +827,7 @@ exports[`TokenField should render "" correctly 1`] = ` <div className="py1 pl1 pr2 block rounded text-bold text-brand-hover inline-block full cursor-pointer bg-grey-0-hover" onClick={[Function]} + onMouseDown={[Function]} > <span> Widget @@ -872,6 +876,7 @@ exports[`TokenField should render "updateOnInputChange" correctly 1`] = ` <div className="py1 pl1 pr2 block rounded text-bold text-brand-hover inline-block full cursor-pointer bg-grey-0-hover" onClick={[Function]} + onMouseDown={[Function]} > <span> Doohickey @@ -882,6 +887,7 @@ exports[`TokenField should render "updateOnInputChange" correctly 1`] = ` <div className="py1 pl1 pr2 block rounded text-bold text-brand-hover inline-block full cursor-pointer bg-grey-0-hover" onClick={[Function]} + onMouseDown={[Function]} > <span> Gadget @@ -892,6 +898,7 @@ exports[`TokenField should render "updateOnInputChange" correctly 1`] = ` <div className="py1 pl1 pr2 block rounded text-bold text-brand-hover inline-block full cursor-pointer bg-grey-0-hover" onClick={[Function]} + onMouseDown={[Function]} > <span> Gizmo @@ -902,6 +909,7 @@ exports[`TokenField should render "updateOnInputChange" correctly 1`] = ` <div className="py1 pl1 pr2 block rounded text-bold text-brand-hover inline-block full cursor-pointer bg-grey-0-hover" onClick={[Function]} + onMouseDown={[Function]} > <span> Widget diff --git a/frontend/test/pulse/pulse.unit.spec.js b/frontend/test/pulse/pulse.unit.spec.js index 477ecbf6de4..54b730cdb9f 100644 --- a/frontend/test/pulse/pulse.unit.spec.js +++ b/frontend/test/pulse/pulse.unit.spec.js @@ -38,7 +38,7 @@ describe("recipient picker", () => { wrapper .find(TokenField) .dive() - .state().focused, + .state().isFocused, ).toBe(true); }); it("should not be focused if there are existing recipients", () => { @@ -55,7 +55,7 @@ describe("recipient picker", () => { wrapper .find(TokenField) .dive() - .state().focused, + .state().isFocused, ).toBe(false); }); }); -- GitLab