diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4b13ae8a11c..9795a422f6a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,8 +2,14 @@
- Added the ability to access certain `EuiDataGrid` internal methods via the `ref` prop ([#5499](https://github.com/elastic/eui/pull/5499))
+#### END FEATURE BRANCH
+
+- Added virtulized rendering option to `EuiSelectableList` with `isVirtualized` ([#5521](https://github.com/elastic/eui/pull/5521))
+- Added expanded option properties to `EuiSelectableOption` with `data` ([#5521](https://github.com/elastic/eui/pull/5521))
+
**Breaking changes**
+- Changed `EuiSearchBar` to preserve phrases with leading and trailing spaces, instead of dropping surrounding whitespace ([#5514](https://github.com/elastic/eui/pull/5514))
- Removed `data-test-subj="dataGridWrapper"` from `EuiDataGrid` in favor of `data-test-subj="euiDataGridBody"` ([#5506](https://github.com/elastic/eui/pull/5506))
## [`44.0.0`](https://github.com/elastic/eui/tree/v44.0.0)
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
index 796ad8435e3..bf637d62d16 100644
--- a/cypress/support/commands.js
+++ b/cypress/support/commands.js
@@ -1,4 +1,5 @@
import { mount as cypressMount } from '@cypress/react';
+import { React, Fragment } from 'react';
import { EuiProvider } from '../../src';
// Provide global cy.mount() shortcut that includes required providers
@@ -6,3 +7,27 @@ import { EuiProvider } from '../../src';
Cypress.Commands.add('mount', (children) => {
return cypressMount({children});
});
+
+// This ensures the correct testing window has focus when using Cypress Real Events.
+// @see https://github.com/dmtrKovalenko/cypress-real-events/issues/196
+Cypress.Commands.add('realMount', (children) => {
+ cy.mount(
+
+
+ {children}
+
+ ).then(() => {
+ cy.get('[data-test-subj="cypress-real-event-target"]').realClick({
+ position: 'topLeft',
+ });
+ });
+});
+
+Cypress.Commands.add('repeatRealPress', (keyToPress, count = 2) => {
+ for (let i = 0; i < count; i++) {
+ cy.realPress(keyToPress);
+ }
+});
diff --git a/cypress/support/index.d.ts b/cypress/support/index.d.ts
index f42a67f5b73..0d3952f90e5 100644
--- a/cypress/support/index.d.ts
+++ b/cypress/support/index.d.ts
@@ -3,5 +3,13 @@ declare namespace Cypress {
interface Chainable {
mount(children: React.ReactNode): Cypress.Chainable;
+ realMount(children: React.ReactNode): void;
+ /**
+ * Repeat the Real Events `realPress()` method 2 or more times
+ *
+ * @param keyToPress Any valid key or array of keys https://docs.cypress.io/api/commands/type#Arguments
+ * @param count Number of times to invoke `realPress()`. Defaults to 2.
+ */
+ repeatRealPress(keyToPress: string | string[], count?: number): void;
}
}
diff --git a/cypress/support/index.js b/cypress/support/index.js
index 0a7a68b89c1..da19e9c4fef 100644
--- a/cypress/support/index.js
+++ b/cypress/support/index.js
@@ -17,6 +17,7 @@ import '@cypress/code-coverage/support';
import './commands.js';
require(THEME_IMPORT); // defined by DefinePlugin in the cypress webpack config
require('cypress-plugin-tab'); // adds the `.tab()` command to cypress chains, see https://docs.cypress.io/api/commands/type#Typing-tab-key-does-not-work
+require('cypress-real-events/support'); // uses the Chrome Devtools Protocol to replace simulated events, see https://github.com/dmtrKovalenko/cypress-real-events#why
// @see https://github.com/quasarframework/quasar/issues/2233#issuecomment-492975745
Cypress.on('uncaught:exception', (err) => {
diff --git a/package.json b/package.json
index 09eaa2cc698..e854986c777 100644
--- a/package.json
+++ b/package.json
@@ -158,6 +158,7 @@
"cssnano": "^4.1.10",
"cypress": "^9.1.1",
"cypress-plugin-tab": "^1.0.5",
+ "cypress-real-events": "^1.6.0",
"deasync": "^0.1.20",
"dedent": "^0.7.0",
"dts-generator": "^3.0.0",
diff --git a/src-docs/src/views/selectable/selectable_custom_render.js b/src-docs/src/views/selectable/selectable_custom_render.js
index 75e4f2050e8..ef4c3dcea12 100644
--- a/src-docs/src/views/selectable/selectable_custom_render.js
+++ b/src-docs/src/views/selectable/selectable_custom_render.js
@@ -12,6 +12,7 @@ import { createDataStore } from '../tables/data_store';
export default () => {
const [useCustomContent, setUseCustomContent] = useState(false);
+ const [isVirtualized, setIsVirtualized] = useState(true);
const countries = createDataStore().countries.map((country) => {
return {
@@ -20,6 +21,9 @@ export default () => {
prepend: country.flag,
append: {country.code},
showIcons: false,
+ data: {
+ secondaryContent: 'I am secondary content, I am!',
+ },
};
});
@@ -38,15 +42,19 @@ export default () => {
setUseCustomContent(e.currentTarget.checked);
};
+ const onVirtualized = (e) => {
+ setIsVirtualized(e.currentTarget.checked);
+ };
+
const renderCountryOption = (option, searchValue) => {
return (
<>
{option.label}
-
-
+ {/* */}
+
- I am secondary content, I am!
+ {option.secondaryContent}
@@ -54,33 +62,42 @@ export default () => {
);
};
+ let listProps = {
+ isVirtualized,
+ };
+
let customProps;
if (useCustomContent) {
customProps = {
height: 240,
renderOption: renderCountryOption,
- listProps: {
- rowHeight: 50,
- showIcons: false,
- },
+ };
+ listProps = {
+ rowHeight: 50,
+ isVirtualized,
};
}
return (
<>
+ {' '}
+
-
-
{(list, search) => (
diff --git a/src-docs/src/views/selectable/selectable_example.js b/src-docs/src/views/selectable/selectable_example.js
index 9f36c676b1f..cfc07fc6491 100644
--- a/src-docs/src/views/selectable/selectable_example.js
+++ b/src-docs/src/views/selectable/selectable_example.js
@@ -347,6 +347,20 @@ export const SelectableExample = {
similar to a title. Add one of these by setting the{' '}
option.isGroupLabel to true.{' '}
+
Row height and virtualization
+
+ When virtualization is on,{' '}
+ every row must be the same height in order for the
+ list to know how to scroll to the selected or highlighted option. It
+ applies the listProps.rowHeight (in pixels)
+ directly to each option hiding any overflow.
+
+
+ If listProps.isVirtualized is set to{' '}
+ false, each row will fit its contents and removes
+ all scrolling. Therefore, we recommend having a large enough
+ container to accomodate all optons.
+
Custom content
While it is best to stick to the{' '}
@@ -357,15 +371,17 @@ export const SelectableExample = {
searchValue to use for highlighting.
- In order for the list to know how to scroll to the selected or
- highlighted option, it must also know the height of the rows. It
- applies this pixel height directly to options. If your custom
- content is taller than the default of 32px tall,
- you will need to recalculate this height and apply it via{' '}
- listProps.rowHeight.
+ To provide data that can be used by the{' '}
+ renderOption function that does not match the
+ standard option API, use option.data which will
+ make custom data available in the option{' '}
+ parameter. See the secondaryContent configuration
+ in the following example.
- Every row must be the same height.
+ Also, if your custom content is taller than the default{' '}
+ listProps.rowHeight of 32px{' '}
+ tall, you will need to pass in a custom value to this prop.
),
diff --git a/src/components/accordion/accordion.spec.tsx b/src/components/accordion/accordion.spec.tsx
new file mode 100644
index 00000000000..0352cba3d11
--- /dev/null
+++ b/src/components/accordion/accordion.spec.tsx
@@ -0,0 +1,120 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React from 'react';
+import { EuiAccordion, EuiAccordionProps } from './index';
+import { EuiPanel } from '../../components/panel';
+import { htmlIdGenerator } from '../../services';
+
+const baseProps: EuiAccordionProps = {
+ buttonContent: 'Click me to toggle',
+ id: htmlIdGenerator()(),
+ initialIsOpen: false,
+};
+
+const noArrow = { arrowDisplay: 'none' };
+const noArrowProps: EuiAccordionProps = Object.assign(baseProps, noArrow);
+
+describe('EuiAccordion', () => {
+ describe('Keyboard and screen reader accessibility', () => {
+ it('renders with required props', () => {
+ cy.realMount(
+
+
+ Any content inside of EuiAccordion will appear
+ here.
+
+
+ );
+ cy.realPress('Tab');
+ cy.focused().contains('Click me to toggle');
+ });
+
+ it('opens and closes on ENTER keypress', () => {
+ cy.realMount(
+
+
+ Any content inside of EuiAccordion will appear
+ here.
+
+
+ );
+ cy.realPress('Tab');
+ cy.focused().contains('Click me to toggle').realPress('Enter');
+ cy.realPress(['Shift', 'Tab']);
+ cy.focused().invoke('attr', 'aria-expanded').should('equal', 'true');
+ cy.realPress('Enter');
+ cy.focused().invoke('attr', 'aria-expanded').should('equal', 'false');
+ });
+
+ it('opens and closes on SPACE keypress', () => {
+ cy.realMount(
+
+
+ Any content inside of EuiAccordion will appear
+ here.
+
+
+ );
+ cy.realPress('Tab');
+ cy.focused().contains('Click me to toggle').realPress('Space');
+ cy.realPress(['Shift', 'Tab']);
+ cy.focused().invoke('attr', 'aria-expanded').should('equal', 'true');
+ cy.realPress('Space');
+ cy.focused().invoke('attr', 'aria-expanded').should('equal', 'false');
+ });
+ });
+
+ describe('Props and keyboard navigation', () => {
+ it('should not have an arrow', () => {
+ cy.realMount(
+
+
+ Any content inside of EuiAccordion will appear
+ here.
+
+
+ );
+ cy.get('.euiAccordion__iconButton').should('not.exist');
+ });
+
+ it('manages focus when panel is opened', () => {
+ cy.realMount(
+
+
+ Any content inside of EuiAccordion will appear
+ here. We will include a link to confirm focus.
+
+
+ );
+ cy.realPress('Tab');
+ cy.focused().contains('Click me to toggle').realPress('Enter');
+ cy.focused().invoke('attr', 'tabindex').should('equal', '-1');
+ cy.focused().contains('Any content inside of EuiAccordion');
+ cy.realPress('Tab');
+ cy.focused().contains('a link');
+ });
+
+ it('manages focus when forceState is open', () => {
+ cy.realMount(
+
+
+ Any content inside of EuiAccordion will appear
+ here. We will include a link to confirm focus.
+
+
+ );
+ cy.realPress('Tab');
+ cy.focused().contains('Click me to toggle');
+ cy.focused().invoke('attr', 'aria-expanded').should('equal', 'true');
+ cy.focused().invoke('attr', 'tabindex').should('not.exist');
+ cy.realPress('Tab');
+ cy.focused().contains('a link');
+ });
+ });
+});
diff --git a/src/components/context_menu/context_menu_panel.spec.tsx b/src/components/context_menu/context_menu_panel.spec.tsx
index 45bd94e5de1..3950b471483 100644
--- a/src/components/context_menu/context_menu_panel.spec.tsx
+++ b/src/components/context_menu/context_menu_panel.spec.tsx
@@ -19,7 +19,6 @@ describe('EuiContextMenuPanel', () => {
);
-
cy.focused().should('have.attr', 'data-test-subj', 'button');
});
@@ -29,7 +28,6 @@ describe('EuiContextMenuPanel', () => {
);
-
cy.focused().should('not.exist');
});
});
@@ -47,13 +45,9 @@ describe('EuiContextMenuPanel', () => {
,
];
- // Intermittent flake workaround: without this, the first downarrow key does not always focus into the menu items as expected
- const FLAKE_WAIT = 500;
-
describe('up/down keys', () => {
beforeEach(() => {
cy.mount();
- cy.wait(FLAKE_WAIT);
});
it('focuses the panel by default', () => {
@@ -61,35 +55,32 @@ describe('EuiContextMenuPanel', () => {
});
it('down arrow key focuses the first menu item', () => {
- cy.get('body').type('{downarrow}');
-
+ cy.focused().should('have.attr', 'class', 'euiContextMenuPanel');
+ cy.realPress('{downarrow}');
cy.focused().should('have.attr', 'data-test-subj', 'itemA');
});
it('subsequently, down arrow key focuses the next menu item', () => {
- cy.get('body').type('{downarrow}');
- cy.get('body').type('{downarrow}');
-
+ cy.focused().should('have.attr', 'class', 'euiContextMenuPanel');
+ cy.repeatRealPress('{downarrow}');
cy.focused().should('have.attr', 'data-test-subj', 'itemB');
});
it('up arrow key wraps to last menu item', () => {
- cy.get('body').type('{uparrow}');
-
+ cy.focused().should('have.attr', 'class', 'euiContextMenuPanel');
+ cy.realPress('{uparrow}');
cy.focused().should('have.attr', 'data-test-subj', 'itemC');
});
it('down arrow key wraps to first menu item', () => {
- cy.get('body').type('{uparrow}');
- cy.get('body').type('{downarrow}');
-
+ cy.focused().should('have.attr', 'class', 'euiContextMenuPanel');
+ cy.repeatRealPress('{downarrow}', 4);
cy.focused().should('have.attr', 'data-test-subj', 'itemA');
});
it('subsequently, up arrow key focuses the previous menu item', () => {
- cy.get('body').type('{uparrow}');
- cy.get('body').type('{uparrow}');
-
+ cy.focused().should('have.attr', 'class', 'euiContextMenuPanel');
+ cy.repeatRealPress('{uparrow}');
cy.focused().should('have.attr', 'data-test-subj', 'itemB');
});
});
@@ -103,14 +94,10 @@ describe('EuiContextMenuPanel', () => {
showNextPanel={showNextPanelHandler}
/>
);
- cy.wait(FLAKE_WAIT);
-
- cy.get('body')
- .type('{downarrow}')
- .type('{rightarrow}')
- .then(() => {
- expect(showNextPanelHandler).to.be.called;
- });
+ cy.realPress('{downarrow}');
+ cy.realPress('{rightarrow}').then(() => {
+ expect(showNextPanelHandler).to.be.called;
+ });
});
it('left arrow key shows previous panel', () => {
@@ -121,14 +108,10 @@ describe('EuiContextMenuPanel', () => {
showPreviousPanel={showPreviousPanelHandler}
/>
);
- cy.wait(FLAKE_WAIT);
-
- cy.get('body')
- .type('{downarrow}')
- .type('{leftarrow}')
- .then(() => {
- expect(showPreviousPanelHandler).to.be.called;
- });
+ cy.realPress('{downarrow}');
+ cy.realPress('{leftarrow}').then(() => {
+ expect(showPreviousPanelHandler).to.be.called;
+ });
});
});
});
diff --git a/src/components/search_bar/query/default_syntax.test.ts b/src/components/search_bar/query/default_syntax.test.ts
index df51fdc8f26..0114d50582f 100644
--- a/src/components/search_bar/query/default_syntax.test.ts
+++ b/src/components/search_bar/query/default_syntax.test.ts
@@ -562,8 +562,8 @@ describe('defaultSyntax', () => {
expect(printedQuery).toBe(query);
});
- test('relaxed phrases with spaces', () => {
- const query = 'f:" this is a relaxed phrase \t"';
+ test('phrases with spaces', () => {
+ const query = 'f:" this is a phrase that preserves spaces\t"';
const ast = defaultSyntax.parse(query);
expect(ast).toBeDefined();
@@ -574,10 +574,10 @@ describe('defaultSyntax', () => {
expect(AST.Field.isInstance(clause)).toBe(true);
expect(AST.Match.isMustClause(clause)).toBe(true);
expect(clause.field).toBe('f');
- expect(clause.value).toBe('this is a relaxed phrase');
+ expect(clause.value).toBe(' this is a phrase that preserves spaces ');
const printedQuery = defaultSyntax.print(ast);
- expect(printedQuery).toBe('f:"this is a relaxed phrase"');
+ expect(printedQuery).toBe('f:" this is a phrase that preserves spaces "');
});
test('phrases with extra characters', () => {
diff --git a/src/components/search_bar/query/default_syntax.ts b/src/components/search_bar/query/default_syntax.ts
index 0e054276891..d538b529575 100644
--- a/src/components/search_bar/query/default_syntax.ts
+++ b/src/components/search_bar/query/default_syntax.ts
@@ -146,9 +146,9 @@ containsValue
/ word
phrase
- = '"' space? phrase:(
- (phraseWord+)? (space phraseWord+)* { return unescapePhraseValue(text()); }
- ) space? '"' { return Exp.string(phrase, location()); }
+ = '"' phrase:(
+ space? (phraseWord+)? (space phraseWord+)* space? { return unescapePhraseValue(text()); }
+ ) '"' { return Exp.string(phrase, location()); }
phraseWord
// not a backslash, quote, or space
diff --git a/src/components/selectable/__snapshots__/selectable.test.tsx.snap b/src/components/selectable/__snapshots__/selectable.test.tsx.snap
index 35ce39017b3..dfb568309cf 100644
--- a/src/components/selectable/__snapshots__/selectable.test.tsx.snap
+++ b/src/components/selectable/__snapshots__/selectable.test.tsx.snap
@@ -1,5 +1,107 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`EuiSelectable custom options with data 1`] = `
+
+
+
+
+
+
+
+
+
+
+ VI: Titan
+
+
+
+
+
+
+
+
+
+ II: Enceladus
+
+
+
+
+
+
+
+
+
+ XVII: Pandora is one of Saturn's moons, named for a Titaness of Greek mythology
+
+
+
+