diff --git a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.html b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.html index 1179263c1858..b5c36852ff1b 100644 --- a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.html +++ b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.html @@ -43,6 +43,7 @@ ng-if="listingController.getSelectedItemsCount() > 0" tooltip="Delete selected dashboards" tooltip-append-to-body="true" + data-test-subj="deleteSelectedDashboards" > @@ -100,6 +101,7 @@
@@ -155,6 +157,7 @@ diff --git a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js index ec90a9f0cec5..c9b7094b7613 100644 --- a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js +++ b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js @@ -3,6 +3,7 @@ import 'ui/pager_control'; import 'ui/pager'; import { DashboardConstants, createDashboardEditUrl } from '../dashboard_constants'; import { SortableProperties } from 'ui_framework/services'; +import { ConfirmationButtonTypes } from 'ui/modals'; export function DashboardListingController($injector, $scope) { const $filter = $injector.get('$filter'); @@ -125,7 +126,8 @@ export function DashboardListingController($injector, $scope) { 'Are you sure you want to delete the selected dashboards? This action is irreversible!', { confirmButtonText: 'Delete', - onConfirm: doDelete + onConfirm: doDelete, + defaultFocusedButton: ConfirmationButtonTypes.CANCEL }); }; diff --git a/src/ui/public/modals/__tests__/confirm_modal.js b/src/ui/public/modals/__tests__/confirm_modal.js index 163e3f3598b4..28683a4407ac 100644 --- a/src/ui/public/modals/__tests__/confirm_modal.js +++ b/src/ui/public/modals/__tests__/confirm_modal.js @@ -114,39 +114,5 @@ describe('ui/modals/confirm_modal', function () { expect(confirmCallback.called).to.be(true); expect(cancelCallback.called).to.be(false); }); - - it('onKeyDown detects ESC and calls onCancel', function () { - const confirmModalOptions = { - confirmButtonText: 'bye', - onConfirm: confirmCallback, - onCancel: cancelCallback, - title: 'hi' - }; - - confirmModal('hi', confirmModalOptions); - - const e = angular.element.Event('keydown'); // eslint-disable-line new-cap - e.keyCode = 27; - angular.element(document.body).trigger(e); - - expect(cancelCallback.called).to.be(true); - }); - - it('onKeyDown ignores keys other than ESC', function () { - const confirmModalOptions = { - confirmButtonText: 'bye', - onConfirm: confirmCallback, - onCancel: cancelCallback, - title: 'hi' - }; - - confirmModal('hi', confirmModalOptions); - - const e = angular.element.Event('keydown'); // eslint-disable-line new-cap - e.keyCode = 50; - angular.element(document.body).trigger(e); - - expect(cancelCallback.called).to.be(false); - }); }); }); diff --git a/src/ui/public/modals/confirm_modal.html b/src/ui/public/modals/confirm_modal.html index 9b4044189ab7..05b3cc1c5969 100644 --- a/src/ui/public/modals/confirm_modal.html +++ b/src/ui/public/modals/confirm_modal.html @@ -6,4 +6,5 @@ cancel-button-text="cancelButtonText" message="message" title="title" + default-focused-button="defaultFocusedButton" > diff --git a/src/ui/public/modals/confirm_modal.js b/src/ui/public/modals/confirm_modal.js index 6e9048803b76..3e64631e2933 100644 --- a/src/ui/public/modals/confirm_modal.js +++ b/src/ui/public/modals/confirm_modal.js @@ -6,9 +6,11 @@ import { ModalOverlay } from './modal_overlay'; const module = uiModules.get('kibana'); +import { CONFIRM_BUTTON, CANCEL_BUTTON } from 'ui_framework/components/modal/confirm_modal'; + export const ConfirmationButtonTypes = { - CONFIRM: 'Confirm', - CANCEL: 'Cancel' + CONFIRM: CONFIRM_BUTTON, + CANCEL: CANCEL_BUTTON }; /** @@ -48,6 +50,7 @@ module.factory('confirmModal', function ($rootScope, $compile) { const confirmScope = $rootScope.$new(); confirmScope.message = message; + confirmScope.defaultFocusedButton = options.defaultFocusedButton; confirmScope.confirmButtonText = options.confirmButtonText; confirmScope.cancelButtonText = options.cancelButtonText; confirmScope.title = options.title; @@ -67,21 +70,6 @@ module.factory('confirmModal', function ($rootScope, $compile) { function showModal(confirmScope) { const modalInstance = $compile(template)(confirmScope); modalPopover = new ModalOverlay(modalInstance); - angular.element(document.body).on('keydown', (event) => { - if (event.keyCode === 27) { - confirmScope.onCancel(); - } - }); - - switch (options.defaultFocusedButton) { - case ConfirmationButtonTypes.CONFIRM: - modalInstance.find('[data-test-subj=confirmModalConfirmButton]').focus(); - break; - case ConfirmationButtonTypes.CANCEL: - modalInstance.find('[data-test-subj=confirmModalCancelButton]').focus(); - break; - default: - } } if (modalPopover) { diff --git a/src/ui/public/modals/index.js b/src/ui/public/modals/index.js index cba0dd8c9ea0..8052ced3b514 100644 --- a/src/ui/public/modals/index.js +++ b/src/ui/public/modals/index.js @@ -1,2 +1,4 @@ import './confirm_modal'; import './confirm_modal_promise'; + +export { ConfirmationButtonTypes } from './confirm_modal'; diff --git a/test/functional/apps/dashboard/_dashboard_listing.js b/test/functional/apps/dashboard/_dashboard_listing.js new file mode 100644 index 000000000000..0bdfc80ce5d0 --- /dev/null +++ b/test/functional/apps/dashboard/_dashboard_listing.js @@ -0,0 +1,69 @@ +import expect from 'expect.js'; + +export default function ({ getPageObjects }) { + const PageObjects = getPageObjects(['dashboard', 'header', 'common']); + + describe('dashboard listing page', function describeIndexTests() { + const dashboardName = 'Dashboard Listing Test'; + + before(async function () { + await PageObjects.dashboard.initTests(); + }); + + describe('create prompt', async () => { + it('appears when there are no dashboards', async function () { + const promptExists = await PageObjects.dashboard.getCreateDashboardPromptExists(); + expect(promptExists).to.be(true); + }); + + it('creates a new dashboard', async function () { + await PageObjects.dashboard.clickCreateDashboardPrompt(); + await PageObjects.dashboard.saveDashboard(dashboardName); + await PageObjects.header.clickToastOK(); + + await PageObjects.dashboard.gotoDashboardLandingPage(); + const countOfDashboards = await PageObjects.dashboard.getDashboardCountWithName(dashboardName); + expect(countOfDashboards).to.equal(1); + }); + + it('is not shown when there is a dashboard', async function () { + const promptExists = await PageObjects.dashboard.getCreateDashboardPromptExists(); + expect(promptExists).to.be(false); + }); + + it('is not shown when there are no dashboards shown during a search', async function () { + const countOfDashboards = await PageObjects.dashboard.getDashboardCountWithName('gobeldeguck'); + expect(countOfDashboards).to.equal(0); + + const promptExists = await PageObjects.dashboard.getCreateDashboardPromptExists(); + expect(promptExists).to.be(false); + }); + }); + + describe('delete', async function () { + it('default confirm action is cancel', async function () { + await PageObjects.dashboard.searchForDashboardWithName(''); + await PageObjects.dashboard.clickListItemCheckbox(); + await PageObjects.dashboard.clickDeleteSelectedDashboards(); + + await PageObjects.common.pressEnterKey(); + + const isConfirmOpen = await PageObjects.common.isConfirmModalOpen(); + expect(isConfirmOpen).to.be(false); + + const countOfDashboards = await PageObjects.dashboard.getDashboardCountWithName(dashboardName); + expect(countOfDashboards).to.equal(1); + }); + + it('succeeds on confirmation press', async function () { + await PageObjects.dashboard.clickListItemCheckbox(); + await PageObjects.dashboard.clickDeleteSelectedDashboards(); + + await PageObjects.common.clickConfirmOnModal(); + + const countOfDashboards = await PageObjects.dashboard.getDashboardCountWithName(dashboardName); + expect(countOfDashboards).to.equal(0); + }); + }); + }); +} diff --git a/test/functional/apps/dashboard/index.js b/test/functional/apps/dashboard/index.js index e254d0a26f5a..4ddc32663d2f 100644 --- a/test/functional/apps/dashboard/index.js +++ b/test/functional/apps/dashboard/index.js @@ -8,5 +8,6 @@ export default function ({ getService, loadTestFile }) { loadTestFile(require.resolve('./_dashboard')); loadTestFile(require.resolve('./_dashboard_save')); loadTestFile(require.resolve('./_dashboard_time')); + loadTestFile(require.resolve('./_dashboard_listing')); }); } diff --git a/test/functional/page_objects/common_page.js b/test/functional/page_objects/common_page.js index ae2f921c4eb9..edf6726a8907 100644 --- a/test/functional/page_objects/common_page.js +++ b/test/functional/page_objects/common_page.js @@ -229,6 +229,10 @@ export function CommonPageProvider({ getService, getPageObjects }) { await this.ensureModalOverlayHidden(); } + async pressEnterKey() { + await remote.pressKeys('\uE007'); + } + async clickCancelOnModal() { log.debug('Clicking modal cancel'); await testSubjects.click('confirmModalCancelButton'); diff --git a/test/functional/page_objects/dashboard_page.js b/test/functional/page_objects/dashboard_page.js index 9daa2aca701a..490739628fcf 100644 --- a/test/functional/page_objects/dashboard_page.js +++ b/test/functional/page_objects/dashboard_page.js @@ -93,6 +93,22 @@ export function DashboardPageProvider({ getService, getPageObjects }) { return testSubjects.click('newDashboardLink'); } + async clickCreateDashboardPrompt() { + await retry.try(() => testSubjects.click('createDashboardPromptButton')); + } + + async getCreateDashboardPromptExists() { + return await testSubjects.exists('createDashboardPromptButton'); + } + + async clickListItemCheckbox() { + await testSubjects.click('dashboardListItemCheckbox'); + } + + async clickDeleteSelectedDashboards() { + await testSubjects.click('deleteSelectedDashboards'); + } + clickAddVisualization() { return testSubjects.click('dashboardAddPanelButton'); } @@ -135,9 +151,9 @@ export function DashboardPageProvider({ getService, getPageObjects }) { filterVizNames(vizName) { return retry.try(() => getRemote() - .findByCssSelector('input[placeholder="Visualizations Filter..."]') - .click() - .pressKeys(vizName)); + .findByCssSelector('input[placeholder="Visualizations Filter..."]') + .click() + .pressKeys(vizName)); } clickVizNameLink(vizName) { @@ -242,6 +258,7 @@ export function DashboardPageProvider({ getService, getPageObjects }) { await retry.try(async () => { const searchFilter = await testSubjects.find('searchFilter'); + await searchFilter.clearValue(); await searchFilter.click(); // Note: this replacement of - to space is to preserve original logic but I'm not sure why or if it's needed. await searchFilter.type(dashName.replace('-',' ')); diff --git a/ui_framework/components/modal/confirm_modal.js b/ui_framework/components/modal/confirm_modal.js index ad715fc52b94..806db8f10d99 100644 --- a/ui_framework/components/modal/confirm_modal.js +++ b/ui_framework/components/modal/confirm_modal.js @@ -8,6 +8,15 @@ import { KuiModalHeaderTitle } from './modal_header_title'; import { KuiModalBody } from './modal_body'; import { KuiModalBodyText } from './modal_body_text'; import { KuiButton } from '../index'; +import { ESC_KEY_CODE } from '../../services'; + +export const CONFIRM_BUTTON = 'confirm'; +export const CANCEL_BUTTON = 'cancel'; + +const CONFIRM_MODAL_BUTTONS = [ + CONFIRM_BUTTON, + CANCEL_BUTTON, +]; export function KuiConfirmModal({ message, @@ -17,7 +26,17 @@ export function KuiConfirmModal({ cancelButtonText, confirmButtonText, className, - ...rest }) { + defaultFocusedButton, + ...rest, + }) { + + const onKeyDown = (event) => { + // Treat the 'esc' key as a cancel indicator. + if (event.keyCode === ESC_KEY_CODE) { + onCancel(); + } + }; + const ariaLabel = rest['aria-label']; const dataTestSubj = rest['data-test-subj']; return ( @@ -26,6 +45,7 @@ export function KuiConfirmModal({ data-tests-subj={ dataTestSubj } aria-label={ ariaLabel } className={ className } + onKeyDown={ onKeyDown } > { title ? @@ -45,6 +65,7 @@ export function KuiConfirmModal({ @@ -52,6 +73,7 @@ export function KuiConfirmModal({ @@ -72,4 +94,5 @@ KuiConfirmModal.propTypes = { dataTestSubj: PropTypes.string, ariaLabel: PropTypes.string, className: PropTypes.string, + defaultFocusedButton: PropTypes.oneOf(CONFIRM_MODAL_BUTTONS) }; diff --git a/ui_framework/components/modal/confirm_modal.test.js b/ui_framework/components/modal/confirm_modal.test.js index fe3314bf66a9..216074d7ab2b 100644 --- a/ui_framework/components/modal/confirm_modal.test.js +++ b/ui_framework/components/modal/confirm_modal.test.js @@ -5,7 +5,7 @@ import { mount, render } from 'enzyme'; import { requiredProps } from '../../test/required_props'; import { - KuiConfirmModal, + CANCEL_BUTTON, CONFIRM_BUTTON, KuiConfirmModal, } from './confirm_modal'; let onConfirm; @@ -44,18 +44,81 @@ test('onConfirm', () => { sinon.assert.notCalled(onCancel); }); -test('onCancel', () => { - const component = mount(); - component.find('[data-test-subj="confirmModalCancelButton"]').simulate('click'); - sinon.assert.notCalled(onConfirm); - sinon.assert.calledOnce(onCancel); +describe('onCancel', () => { + test('triggerd by click', () => { + const component = mount(); + component.find('[data-test-subj="confirmModalCancelButton"]').simulate('click'); + sinon.assert.notCalled(onConfirm); + sinon.assert.calledOnce(onCancel); + }); + + test('triggered by esc key', () => { + const component = mount(); + component.simulate('keydown', { keyCode: 27 }); + sinon.assert.notCalled(onConfirm); + sinon.assert.calledOnce(onCancel); + }); +}); + +describe('defaultFocusedButton', () => { + test('is cancel', () => { + const component = mount(); + const button = component.find('[data-test-subj="confirmModalCancelButton"]').getDOMNode(); + expect(document.activeElement).toEqual(button); + }); + + test('is confirm', () => { + const component = mount(); + const button = component.find('[data-test-subj="confirmModalConfirmButton"]').getDOMNode(); + expect(document.activeElement).toEqual(button); + }); + + test('when not given focuses on the confirm button', () => { + const component = mount(); + const button = component.find('[data-test-subj="confirmModalConfirmButton"]').getDOMNode(); + expect(document.activeElement).toEqual(button); + }); }); diff --git a/ui_framework/services/index.js b/ui_framework/services/index.js index 287383f7e057..06f62c9687d7 100644 --- a/ui_framework/services/index.js +++ b/ui_framework/services/index.js @@ -1 +1,2 @@ export { SortableProperties } from './sort'; +export { ESC_KEY_CODE } from './key_codes'; diff --git a/ui_framework/services/key_codes.js b/ui_framework/services/key_codes.js new file mode 100644 index 000000000000..0dfedd711f7e --- /dev/null +++ b/ui_framework/services/key_codes.js @@ -0,0 +1 @@ +export const ESC_KEY_CODE = 27;