[QueryBar] a11y improvements, focus glitches fixes, unskip tests (#94148) (#94806)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Anton Dosov 2021-03-18 12:26:44 +01:00 committed by GitHub
parent 6af6593118
commit 2df4d5ca96
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 148 additions and 37 deletions

View file

@ -26,18 +26,18 @@
padding-top: $euiSizeS + 3px;
box-shadow: 0 0 0 1px $euiFormBorderColor;
&:not(:focus):not(:invalid) {
&:not(.kbnQueryBar__textarea--autoHeight):not(:invalid) {
@include euiYScrollWithShadows;
}
&:not(:focus) {
&:not(.kbnQueryBar__textarea--autoHeight) {
white-space: nowrap;
overflow-y: hidden;
overflow-x: hidden;
}
// When focused, let it scroll
&:focus {
&.kbnQueryBar__textarea--autoHeight {
overflow-x: auto;
overflow-y: auto;
white-space: normal;

View file

@ -86,6 +86,8 @@ export function QueryLanguageSwitcher({
isOpen={isPopoverOpen}
closePopover={() => setIsPopoverOpen(false)}
repositionOnScroll
ownFocus={true}
initialFocus={'[role="switch"]'}
>
<EuiPopoverTitle>
<FormattedMessage

View file

@ -21,13 +21,14 @@ import { render } from '@testing-library/react';
import { EuiTextArea, EuiIcon } from '@elastic/eui';
import { QueryLanguageSwitcher } from './language_switcher';
import { QueryStringInput } from './';
import type QueryStringInputUI from './query_string_input';
import QueryStringInputUI from './query_string_input';
import { coreMock } from '../../../../../core/public/mocks';
import { dataPluginMock } from '../../mocks';
import { stubIndexPatternWithFields } from '../../stubs';
import { KibanaContextProvider } from 'src/plugins/kibana_react/public';
import { KibanaContextProvider, withKibana } from 'src/plugins/kibana_react/public';
jest.useFakeTimers();
const startMock = coreMock.createStart();
@ -62,12 +63,9 @@ const createMockStorage = () => ({
clear: jest.fn(),
});
function wrapQueryStringInputInContext(testProps: any, storage?: any) {
const defaultOptions = {
screenTitle: 'Another Screen',
intl: null as any,
};
const QueryStringInput = withKibana(QueryStringInputUI);
function wrapQueryStringInputInContext(testProps: any, storage?: any) {
const services = {
...startMock,
data: dataPluginMock.createStartContract(),
@ -75,6 +73,11 @@ function wrapQueryStringInputInContext(testProps: any, storage?: any) {
storage: storage || createMockStorage(),
};
const defaultOptions = {
screenTitle: 'Another Screen',
intl: null as any,
};
return (
<I18nProvider>
<KibanaContextProvider services={services}>
@ -84,15 +87,12 @@ function wrapQueryStringInputInContext(testProps: any, storage?: any) {
);
}
// FAILING: https://github.com/elastic/kibana/issues/85715
// FAILING: https://github.com/elastic/kibana/issues/89603
// FAILING: https://github.com/elastic/kibana/issues/89641
describe.skip('QueryStringInput', () => {
describe('QueryStringInput', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it.skip('Should render the given query', async () => {
it('Should render the given query', async () => {
const { getByText } = render(
wrapQueryStringInputInContext({
query: kqlQuery,
@ -228,7 +228,7 @@ describe.skip('QueryStringInput', () => {
expect(mockCallback).toHaveBeenCalledWith();
});
it('Should fire onChangeQueryInputFocus callback on input blur', () => {
it('Should fire onChangeQueryInputFocus after a delay', () => {
const mockCallback = jest.fn();
const component = mount(
@ -243,10 +243,93 @@ describe.skip('QueryStringInput', () => {
const inputWrapper = component.find(EuiTextArea).find('textarea');
inputWrapper.simulate('blur');
jest.advanceTimersByTime(10);
expect(mockCallback).toHaveBeenCalledTimes(0);
jest.advanceTimersByTime(100);
expect(mockCallback).toHaveBeenCalledTimes(1);
expect(mockCallback).toHaveBeenCalledWith(false);
});
it('Should not fire onChangeQueryInputFocus if input is focused back', () => {
const mockCallback = jest.fn();
const component = mount(
wrapQueryStringInputInContext({
query: kqlQuery,
onChangeQueryInputFocus: mockCallback,
indexPatterns: [stubIndexPatternWithFields],
disableAutoFocus: true,
})
);
const inputWrapper = component.find(EuiTextArea).find('textarea');
inputWrapper.simulate('blur');
jest.advanceTimersByTime(5);
expect(mockCallback).toHaveBeenCalledTimes(0);
inputWrapper.simulate('focus');
expect(mockCallback).toHaveBeenCalledTimes(1);
expect(mockCallback).toHaveBeenCalledWith(true);
jest.advanceTimersByTime(100);
expect(mockCallback).toHaveBeenCalledTimes(1);
});
it('Should call onSubmit after a delay when submitOnBlur is on and blurs input', () => {
const mockCallback = jest.fn();
const component = mount(
wrapQueryStringInputInContext({
query: kqlQuery,
onSubmit: mockCallback,
indexPatterns: [stubIndexPatternWithFields],
disableAutoFocus: true,
submitOnBlur: true,
})
);
const inputWrapper = component.find(EuiTextArea).find('textarea');
inputWrapper.simulate('blur');
jest.advanceTimersByTime(10);
expect(mockCallback).toHaveBeenCalledTimes(0);
jest.advanceTimersByTime(100);
expect(mockCallback).toHaveBeenCalledTimes(1);
expect(mockCallback).toHaveBeenCalledWith(kqlQuery);
});
it("Shouldn't call onSubmit on blur by default", () => {
const mockCallback = jest.fn();
const component = mount(
wrapQueryStringInputInContext({
query: kqlQuery,
onSubmit: mockCallback,
indexPatterns: [stubIndexPatternWithFields],
disableAutoFocus: true,
})
);
const inputWrapper = component.find(EuiTextArea).find('textarea');
inputWrapper.simulate('blur');
jest.advanceTimersByTime(10);
expect(mockCallback).toHaveBeenCalledTimes(0);
jest.advanceTimersByTime(100);
expect(mockCallback).toHaveBeenCalledTimes(0);
});
it('Should use PersistedLog for recent search suggestions', async () => {
const component = mount(
wrapQueryStringInputInContext({

View file

@ -123,6 +123,12 @@ export default class QueryStringInputUI extends Component<Props, State> {
private componentIsUnmounting = false;
private queryBarInputDivRefInstance: RefObject<HTMLDivElement> = createRef();
/**
* If any element within the container is currently focused
* @private
*/
private isFocusWithin = false;
private getQueryString = () => {
return toUser(this.props.query.query);
};
@ -492,29 +498,36 @@ export default class QueryStringInputUI extends Component<Props, State> {
private onOutsideClick = () => {
if (this.state.isSuggestionsVisible) {
this.setState({ isSuggestionsVisible: false, index: null });
}
this.handleBlurHeight();
if (this.props.onChangeQueryInputFocus) {
this.props.onChangeQueryInputFocus(false);
this.scheduleOnInputBlur();
}
};
private onInputBlur = () => {
this.handleBlurHeight();
if (this.props.onChangeQueryInputFocus) {
this.props.onChangeQueryInputFocus(false);
}
if (isFunction(this.props.onBlur)) {
this.props.onBlur();
}
if (this.props.submitOnBlur) {
// Input blur triggers when the user selects something from autocomplete, so wait a bit to ensure that
// the entire QueryStringInput component has actually blurred (e.g. from user clicking or tabbing away)
setTimeout(() => {
if (document.activeElement !== this.inputRef) {
private blurTimeoutHandle: number | undefined;
/**
* Notify parent about input's blur after a delay only
* if the focus didn't get back inside the input container
* and if suggestions were closed
* https://github.com/elastic/kibana/issues/92040
*/
private scheduleOnInputBlur = () => {
clearTimeout(this.blurTimeoutHandle);
this.blurTimeoutHandle = window.setTimeout(() => {
if (!this.isFocusWithin && !this.state.isSuggestionsVisible && !this.componentIsUnmounting) {
this.handleBlurHeight();
if (this.props.onChangeQueryInputFocus) {
this.props.onChangeQueryInputFocus(false);
}
if (this.props.submitOnBlur) {
this.onSubmit(this.props.query);
}
}, 200);
}
}, 50);
};
private onInputBlur = () => {
if (isFunction(this.props.onBlur)) {
this.props.onBlur();
}
};
@ -604,6 +617,7 @@ export default class QueryStringInputUI extends Component<Props, State> {
handleAutoHeight = () => {
if (this.inputRef !== null && document.activeElement === this.inputRef) {
this.inputRef.classList.add('kbnQueryBar__textarea--autoHeight');
this.inputRef.style.setProperty('height', `${this.inputRef.scrollHeight}px`, 'important');
}
this.handleListUpdate();
@ -612,6 +626,7 @@ export default class QueryStringInputUI extends Component<Props, State> {
handleRemoveHeight = () => {
if (this.inputRef !== null) {
this.inputRef.style.removeProperty('height');
this.inputRef.classList.remove('kbnQueryBar__textarea--autoHeight');
}
};
@ -648,7 +663,16 @@ export default class QueryStringInputUI extends Component<Props, State> {
);
return (
<div className={containerClassName}>
<div
className={containerClassName}
onFocus={(e) => {
this.isFocusWithin = true;
}}
onBlur={(e) => {
this.isFocusWithin = false;
this.scheduleOnInputBlur();
}}
>
{this.props.prepend}
<EuiOutsideClickDetector onOutsideClick={this.onOutsideClick}>
<div

View file

@ -196,6 +196,7 @@ export function SavedQueryManagementComponent({
panelPaddingSize="none"
buffer={-8}
repositionOnScroll
ownFocus={true}
>
<div
className="kbnSavedQueryManagement__popover"

View file

@ -45,7 +45,8 @@ export function QueryBarProvider({ getService, getPageObjects }: FtrProviderCont
public async clearQuery(): Promise<void> {
await this.setQuery('');
await PageObjects.common.pressTabKey();
await PageObjects.common.pressTabKey(); // move outside of input into language switcher
await PageObjects.common.pressTabKey(); // move outside of language switcher so time picker appears
}
public async submitQuery(): Promise<void> {