UI: Add focus styles to QueryField component (#45933)

* Add focus styles to QueryField component
pull/46105/head
Connor Lindsey 3 years ago committed by GitHub
parent 5c05a3deb9
commit 9067715d1d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .betterer.results
  2. 56
      packages/grafana-ui/src/components/QueryField/QueryField.test.tsx
  3. 37
      packages/grafana-ui/src/components/QueryField/QueryField.tsx

@ -44,7 +44,7 @@ exports[`no enzyme tests`] = {
"packages/grafana-ui/src/components/Logs/LogRows.test.tsx:2288254498": [ "packages/grafana-ui/src/components/Logs/LogRows.test.tsx:2288254498": [
[3, 17, 13, "RegExp match", "2409514259"] [3, 17, 13, "RegExp match", "2409514259"]
], ],
"packages/grafana-ui/src/components/QueryField/QueryField.test.tsx:1906163280": [ "packages/grafana-ui/src/components/QueryField/QueryField.test.tsx:1297745712": [
[1, 19, 13, "RegExp match", "2409514259"] [1, 19, 13, "RegExp match", "2409514259"]
], ],
"packages/grafana-ui/src/components/Slider/Slider.test.tsx:2110443485": [ "packages/grafana-ui/src/components/Slider/Slider.test.tsx:2110443485": [

@ -1,30 +1,43 @@
import React from 'react'; import React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import { QueryField } from './QueryField'; import { UnThemedQueryField } from './QueryField';
import { Editor } from 'slate'; import { Editor } from 'slate';
import { createTheme } from '@grafana/data';
describe('<QueryField />', () => { describe('<QueryField />', () => {
it('should render with null initial value', () => { it('should render with null initial value', () => {
const wrapper = shallow(<QueryField query={null} onTypeahead={jest.fn()} portalOrigin="mock-origin" />); const wrapper = shallow(
<UnThemedQueryField theme={createTheme()} query={null} onTypeahead={jest.fn()} portalOrigin="mock-origin" />
);
expect(wrapper.find('div').exists()).toBeTruthy(); expect(wrapper.find('div').exists()).toBeTruthy();
}); });
it('should render with empty initial value', () => { it('should render with empty initial value', () => {
const wrapper = shallow(<QueryField query="" onTypeahead={jest.fn()} portalOrigin="mock-origin" />); const wrapper = shallow(
<UnThemedQueryField theme={createTheme()} query="" onTypeahead={jest.fn()} portalOrigin="mock-origin" />
);
expect(wrapper.find('div').exists()).toBeTruthy(); expect(wrapper.find('div').exists()).toBeTruthy();
}); });
it('should render with initial value', () => { it('should render with initial value', () => {
const wrapper = shallow(<QueryField query="my query" onTypeahead={jest.fn()} portalOrigin="mock-origin" />); const wrapper = shallow(
<UnThemedQueryField theme={createTheme()} query="my query" onTypeahead={jest.fn()} portalOrigin="mock-origin" />
);
expect(wrapper.find('div').exists()).toBeTruthy(); expect(wrapper.find('div').exists()).toBeTruthy();
}); });
it('should execute query on blur', () => { it('should execute query on blur', () => {
const onRun = jest.fn(); const onRun = jest.fn();
const wrapper = shallow( const wrapper = shallow(
<QueryField query="my query" onTypeahead={jest.fn()} onRunQuery={onRun} portalOrigin="mock-origin" /> <UnThemedQueryField
theme={createTheme()}
query="my query"
onTypeahead={jest.fn()}
onRunQuery={onRun}
portalOrigin="mock-origin"
/>
); );
const field = wrapper.instance() as QueryField; const field = wrapper.instance() as UnThemedQueryField;
expect(onRun.mock.calls.length).toBe(0); expect(onRun.mock.calls.length).toBe(0);
field.handleBlur(new Event('bogus'), new Editor({}), () => {}); field.handleBlur(new Event('bogus'), new Editor({}), () => {});
expect(onRun.mock.calls.length).toBe(1); expect(onRun.mock.calls.length).toBe(1);
@ -33,9 +46,15 @@ describe('<QueryField />', () => {
it('should run onChange with clean text', () => { it('should run onChange with clean text', () => {
const onChange = jest.fn(); const onChange = jest.fn();
const wrapper = shallow( const wrapper = shallow(
<QueryField query={`my\r clean query `} onTypeahead={jest.fn()} onChange={onChange} portalOrigin="mock-origin" /> <UnThemedQueryField
theme={createTheme()}
query={`my\r clean query `}
onTypeahead={jest.fn()}
onChange={onChange}
portalOrigin="mock-origin"
/>
); );
const field = wrapper.instance() as QueryField; const field = wrapper.instance() as UnThemedQueryField;
field.runOnChange(); field.runOnChange();
expect(onChange.mock.calls.length).toBe(1); expect(onChange.mock.calls.length).toBe(1);
expect(onChange.mock.calls[0][0]).toBe('my clean query '); expect(onChange.mock.calls[0][0]).toBe('my clean query ');
@ -45,7 +64,8 @@ describe('<QueryField />', () => {
const onBlur = jest.fn(); const onBlur = jest.fn();
const onRun = jest.fn(); const onRun = jest.fn();
const wrapper = shallow( const wrapper = shallow(
<QueryField <UnThemedQueryField
theme={createTheme()}
query="my query" query="my query"
onTypeahead={jest.fn()} onTypeahead={jest.fn()}
onBlur={onBlur} onBlur={onBlur}
@ -53,7 +73,7 @@ describe('<QueryField />', () => {
portalOrigin="mock-origin" portalOrigin="mock-origin"
/> />
); );
const field = wrapper.instance() as QueryField; const field = wrapper.instance() as UnThemedQueryField;
expect(onBlur.mock.calls.length).toBe(0); expect(onBlur.mock.calls.length).toBe(0);
expect(onRun.mock.calls.length).toBe(0); expect(onRun.mock.calls.length).toBe(0);
field.handleBlur(new Event('bogus'), new Editor({}), () => {}); field.handleBlur(new Event('bogus'), new Editor({}), () => {});
@ -62,14 +82,18 @@ describe('<QueryField />', () => {
}); });
describe('syntaxLoaded', () => { describe('syntaxLoaded', () => {
it('should re-render the editor after syntax has fully loaded', () => { it('should re-render the editor after syntax has fully loaded', () => {
const wrapper: any = shallow(<QueryField query="my query" portalOrigin="mock-origin" />); const wrapper: any = shallow(
<UnThemedQueryField theme={createTheme()} query="my query" portalOrigin="mock-origin" />
);
const spyOnChange = jest.spyOn(wrapper.instance(), 'onChange').mockImplementation(jest.fn()); const spyOnChange = jest.spyOn(wrapper.instance(), 'onChange').mockImplementation(jest.fn());
wrapper.instance().editor = { insertText: () => ({ deleteBackward: () => ({ value: 'fooo' }) }) }; wrapper.instance().editor = { insertText: () => ({ deleteBackward: () => ({ value: 'fooo' }) }) };
wrapper.setProps({ syntaxLoaded: true }); wrapper.setProps({ syntaxLoaded: true });
expect(spyOnChange).toHaveBeenCalledWith('fooo', true); expect(spyOnChange).toHaveBeenCalledWith('fooo', true);
}); });
it('should not re-render the editor if syntax is already loaded', () => { it('should not re-render the editor if syntax is already loaded', () => {
const wrapper: any = shallow(<QueryField query="my query" portalOrigin="mock-origin" />); const wrapper: any = shallow(
<UnThemedQueryField theme={createTheme()} query="my query" portalOrigin="mock-origin" />
);
const spyOnChange = jest.spyOn(wrapper.instance(), 'onChange').mockImplementation(jest.fn()); const spyOnChange = jest.spyOn(wrapper.instance(), 'onChange').mockImplementation(jest.fn());
wrapper.setProps({ syntaxLoaded: true }); wrapper.setProps({ syntaxLoaded: true });
wrapper.instance().editor = {}; wrapper.instance().editor = {};
@ -77,14 +101,18 @@ describe('<QueryField />', () => {
expect(spyOnChange).not.toBeCalled(); expect(spyOnChange).not.toBeCalled();
}); });
it('should not re-render the editor if editor itself is not defined', () => { it('should not re-render the editor if editor itself is not defined', () => {
const wrapper: any = shallow(<QueryField query="my query" portalOrigin="mock-origin" />); const wrapper: any = shallow(
<UnThemedQueryField theme={createTheme()} query="my query" portalOrigin="mock-origin" />
);
const spyOnChange = jest.spyOn(wrapper.instance(), 'onChange').mockImplementation(jest.fn()); const spyOnChange = jest.spyOn(wrapper.instance(), 'onChange').mockImplementation(jest.fn());
wrapper.setProps({ syntaxLoaded: true }); wrapper.setProps({ syntaxLoaded: true });
expect(wrapper.instance().editor).toBeFalsy(); expect(wrapper.instance().editor).toBeFalsy();
expect(spyOnChange).not.toBeCalled(); expect(spyOnChange).not.toBeCalled();
}); });
it('should not re-render the editor twice once syntax is fully loaded', () => { it('should not re-render the editor twice once syntax is fully loaded', () => {
const wrapper: any = shallow(<QueryField query="my query" portalOrigin="mock-origin" />); const wrapper: any = shallow(
<UnThemedQueryField theme={createTheme()} query="my query" portalOrigin="mock-origin" />
);
const spyOnChange = jest.spyOn(wrapper.instance(), 'onChange').mockImplementation(jest.fn()); const spyOnChange = jest.spyOn(wrapper.instance(), 'onChange').mockImplementation(jest.fn());
wrapper.instance().editor = { insertText: () => ({ deleteBackward: () => ({ value: 'fooo' }) }) }; wrapper.instance().editor = { insertText: () => ({ deleteBackward: () => ({ value: 'fooo' }) }) };
wrapper.setProps({ syntaxLoaded: true }); wrapper.setProps({ syntaxLoaded: true });

@ -16,10 +16,22 @@ import {
SuggestionsPlugin, SuggestionsPlugin,
} from '../../slate-plugins'; } from '../../slate-plugins';
import { makeValue, SCHEMA, CompletionItemGroup, TypeaheadOutput, TypeaheadInput, SuggestionsState } from '../..'; import {
makeValue,
SCHEMA,
CompletionItemGroup,
TypeaheadOutput,
TypeaheadInput,
SuggestionsState,
Themeable2,
} from '../..';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { css, cx } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { withTheme2 } from '../../themes';
import { getFocusStyles } from '../../themes/mixins';
export interface QueryFieldProps { export interface QueryFieldProps extends Themeable2 {
additionalPlugins?: Plugin[]; additionalPlugins?: Plugin[];
cleanText?: (text: string) => string; cleanText?: (text: string) => string;
disabled?: boolean; disabled?: boolean;
@ -38,6 +50,7 @@ export interface QueryFieldProps {
portalOrigin: string; portalOrigin: string;
syntax?: string; syntax?: string;
syntaxLoaded?: boolean; syntaxLoaded?: boolean;
theme: GrafanaTheme2;
} }
export interface QueryFieldState { export interface QueryFieldState {
@ -54,7 +67,7 @@ export interface QueryFieldState {
* This component can only process strings. Internally it uses Slate Value. * This component can only process strings. Internally it uses Slate Value.
* Implement props.onTypeahead to use suggestions, see PromQueryField.tsx as an example. * Implement props.onTypeahead to use suggestions, see PromQueryField.tsx as an example.
*/ */
export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldState> { export class UnThemedQueryField extends React.PureComponent<QueryFieldProps, QueryFieldState> {
plugins: Plugin[]; plugins: Plugin[];
runOnChangeDebounced: Function; runOnChangeDebounced: Function;
lastExecutedValue: Value | null = null; lastExecutedValue: Value | null = null;
@ -197,13 +210,14 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
} }
render() { render() {
const { disabled } = this.props; const { disabled, theme } = this.props;
const wrapperClassName = classnames('slate-query-field__wrapper', { const wrapperClassName = classnames('slate-query-field__wrapper', {
'slate-query-field__wrapper--disabled': disabled, 'slate-query-field__wrapper--disabled': disabled,
}); });
const styles = getStyles(theme);
return ( return (
<div className={wrapperClassName}> <div className={cx(wrapperClassName, styles.wrapper)}>
<div className="slate-query-field" aria-label={selectors.components.QueryField.container}> <div className="slate-query-field" aria-label={selectors.components.QueryField.container}>
<Editor <Editor
ref={(editor) => (this.editor = editor!)} ref={(editor) => (this.editor = editor!)}
@ -227,4 +241,15 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
} }
} }
export default QueryField; export const QueryField = withTheme2(UnThemedQueryField);
const getStyles = (theme: GrafanaTheme2) => {
const focusStyles = getFocusStyles(theme);
return {
wrapper: css`
&:focus-within {
${focusStyles}
}
`,
};
};

Loading…
Cancel
Save