diff --git a/core/src/App.svelte b/core/src/App.svelte
index 2ebd6f60c5..87bb0beddb 100644
--- a/core/src/App.svelte
+++ b/core/src/App.svelte
@@ -105,7 +105,7 @@
export let isSearchFieldVisible;
export let inputElem;
- export let luigiCustomSearchRenderer__slot;
+ export let customSearchItemRendererSlot;
export let displaySearchResult;
export let searchResult;
export let storedUserSettings;
@@ -556,7 +556,7 @@
searchProvider.onSearchResultItemSelected(item);
}
};
- searchProvider.customSearchResultRenderer(arr, luigiCustomSearchRenderer__slot, searchApiObj);
+ searchProvider.customSearchResultRenderer(arr, customSearchItemRendererSlot, searchApiObj);
} else {
displaySearchResult = true;
searchResult = arr;
@@ -571,9 +571,9 @@
if (checkSearchProvider(searchProvider)) {
displaySearchResult = false;
searchResult = [];
- if (luigiCustomSearchRenderer__slot) {
- while (luigiCustomSearchRenderer__slot.lastElementChild) {
- luigiCustomSearchRenderer__slot.removeChild(luigiCustomSearchRenderer__slot.lastElementChild);
+ if (customSearchItemRendererSlot) {
+ while (customSearchItemRendererSlot.lastElementChild) {
+ customSearchItemRendererSlot.removeChild(customSearchItemRendererSlot.lastElementChild);
}
}
}
@@ -1880,7 +1880,7 @@
bind:displaySearchResult
bind:searchResult
bind:inputElem
- bind:luigiCustomSearchRenderer__slot
+ bind:customSearchItemRendererSlot
{burgerTooltip}
/>
{/if}
@@ -2003,7 +2003,7 @@
bind:displaySearchResult
bind:searchResult
bind:inputElem
- bind:luigiCustomSearchRenderer__slot
+ bind:customSearchItemRendererSlot
{burgerTooltip}
/>
{/if}
diff --git a/core/src/navigation/GlobalSearch.svelte b/core/src/navigation/GlobalSearch.svelte
index efecd5ff8e..4b8dd001bf 100644
--- a/core/src/navigation/GlobalSearch.svelte
+++ b/core/src/navigation/GlobalSearch.svelte
@@ -1,190 +1,48 @@
@@ -223,23 +81,23 @@
{/if}
- {#if !isCustomSearchRenderer}
+ {#if !globalSearchHelper.isCustomSearchRenderer}
{:else}
- {@html renderCustomSearchItem(result, luigiCustomSearchItemRenderer__slotContainer, index)}
+ {@html globalSearchHelper.renderCustomSearchItem(result, customSearchItemRendererSlotContainer, index)}
{/if}
{/each}
@@ -258,7 +116,7 @@
{:else}
-
+
{/if}
diff --git a/core/src/navigation/GlobalSearchCentered.svelte b/core/src/navigation/GlobalSearchCentered.svelte
index 9802080f84..2002ff2673 100644
--- a/core/src/navigation/GlobalSearchCentered.svelte
+++ b/core/src/navigation/GlobalSearchCentered.svelte
@@ -1,111 +1,56 @@
@@ -279,14 +145,14 @@
{/if}
- {#if !isCustomSearchRenderer}
+ {#if !globalSearchHelper.isCustomSearchRenderer}
{:else}
- {@html renderCustomSearchItem(result, luigiCustomSearchItemRenderer__slotContainer, index)}
+ {@html renderCustomSearchItem(result, customSearchItemRendererSlotContainer, index)}
{/if}
{/each}
@@ -314,7 +180,7 @@
{:else}
-
+
{/if}
diff --git a/core/src/navigation/TopNav.svelte b/core/src/navigation/TopNav.svelte
index aa16d723b0..88884217c4 100644
--- a/core/src/navigation/TopNav.svelte
+++ b/core/src/navigation/TopNav.svelte
@@ -48,7 +48,7 @@
export let isGlobalSearchCentered;
export let isSearchFieldVisible;
export let inputElem;
- export let luigiCustomSearchRenderer__slot;
+ export let customSearchItemRendererSlot;
export let displaySearchResult;
export let searchResult;
export let burgerTooltip;
@@ -208,7 +208,7 @@
dispatch('toggleSearch', {
isSearchFieldVisible,
inputElem,
- luigiCustomSearchRenderer__slot
+ customSearchItemRendererSlot
});
}
@@ -304,7 +304,7 @@
bind:searchResult
bind:displaySearchResult
bind:inputElem
- bind:luigiCustomSearchRenderer__slot
+ bind:customSearchItemRendererSlot
on:closeSearchResult
/>
@@ -319,7 +319,7 @@
bind:searchResult
bind:displaySearchResult
bind:inputElem
- bind:luigiCustomSearchRenderer__slot
+ bind:customSearchItemRendererSlot
on:closeSearchResult
{globalSearchConfig}
/>
diff --git a/core/src/utilities/helpers/global-search-helpers.js b/core/src/utilities/helpers/global-search-helpers.js
index f8e9dc79d0..6db11f2555 100644
--- a/core/src/utilities/helpers/global-search-helpers.js
+++ b/core/src/utilities/helpers/global-search-helpers.js
@@ -1,6 +1,31 @@
/* istanbul ignore file */
-class GlobalSearchHelperClass {
- constructor() {}
+import { GenericHelpers } from './';
+import { KEYCODE_ARROW_UP, KEYCODE_ARROW_DOWN, KEYCODE_ENTER, KEYCODE_ESC } from './../keycode.js';
+import { Routing } from './../../services/routing';
+import { LuigiI18N } from './../../core-api';
+export class GlobalSearchHelperClass {
+ dispatch;
+ search;
+ isCustomSearchRenderer;
+ isCustomSearchResultItemRenderer;
+ customSearchItemRendererSlotContainer;
+
+ constructor(search, dispatcher) {
+ this.search = search;
+ this.dispatch = dispatcher;
+ }
+
+ getCustomRenderer() {
+ if (!this.search.searchProvider) return;
+ this.isCustomSearchRenderer = GenericHelpers.isFunction(this.search.searchProvider.customSearchResultRenderer);
+ this.isCustomSearchResultItemRenderer = GenericHelpers.isFunction(
+ this.search.searchProvider.customSearchResultItemRenderer
+ );
+ }
+
+ updateCustomSearchItemRendererSlotContainer(updatedSlotContainer) {
+ this.customSearchItemRendererSlotContainer = updatedSlotContainer;
+ }
handleVisibilityGlobalSearch() {
if (!document.querySelector('.lui-global-search')) return;
@@ -8,6 +33,163 @@ class GlobalSearchHelperClass {
const condition = globalSearchCtn.offsetWidth <= 384;
globalSearchCtn.classList.toggle('lui-global-search-toggle', condition);
}
-}
-export const GlobalSearchHelper = new GlobalSearchHelperClass();
+ setSearchPlaceholder(inputElement) {
+ const placeHolder = this.getSearchPlaceholder();
+ if (placeHolder) {
+ inputElement.placeholder = placeHolder;
+ }
+ }
+
+ getSearchPlaceholder() {
+ const searchProvider = this.search.searchProvider;
+ if (!searchProvider || !searchProvider.inputPlaceholder) {
+ return undefined;
+ }
+ const currentLocale = LuigiI18N.getCurrentLocale();
+ if (GenericHelpers.isFunction(searchProvider.inputPlaceholder)) {
+ return searchProvider.inputPlaceholder();
+ }
+ if (typeof searchProvider.inputPlaceholder === 'string') {
+ const translated = LuigiI18N.getTranslation(searchProvider.inputPlaceholder);
+ if (!!translated && translated.trim().length > 0) {
+ return translated;
+ }
+ return searchProvider.inputPlaceholder;
+ }
+ if (typeof searchProvider.inputPlaceholder === 'object') {
+ return searchProvider.inputPlaceholder[currentLocale];
+ }
+ }
+
+ renderCustomSearchItem(item, slotContainer, index) {
+ setTimeout(() => {
+ search.searchProvider.customSearchResultItemRenderer(item, slotContainer.children[index], searchApiObj);
+ });
+ return '';
+ }
+
+ onKeyUp({ keyCode }, displaySearchResult) {
+ if (this.search && this.search.searchProvider) {
+ if (GenericHelpers.isFunction(this.search.searchProvider.onEnter) && keyCode === KEYCODE_ENTER) {
+ this.search.searchProvider.onEnter();
+ } else if (GenericHelpers.isFunction(this.search.searchProvider.onEscape) && keyCode === KEYCODE_ESC) {
+ this.search.searchProvider.onEscape();
+ } else if (keyCode === KEYCODE_ARROW_DOWN) {
+ if (displaySearchResult) {
+ document.querySelector('.luigi-search-result-item__0').childNodes[0].setAttribute('aria-selected', 'true');
+ document.querySelector('.luigi-search-result-item__0').focus();
+ }
+ } else if (GenericHelpers.isFunction(this.search.searchProvider.onInput)) {
+ this.search.searchProvider.onInput();
+ }
+ } else {
+ console.warn('GlobalSearch is not available.');
+ }
+ }
+
+ calcSearchResultItemSelected(direction) {
+ let renderedSearchResultItems = this.customSearchItemRendererSlotContainer.children;
+ if (renderedSearchResultItems) {
+ for (let index = 0; index < renderedSearchResultItems.length; index++) {
+ let { childNodes, nextSibling, previousSibling } = renderedSearchResultItems[index];
+ let nodeSibling;
+ if (childNodes[0].getAttribute('aria-selected') === 'true') {
+ if (direction === KEYCODE_ARROW_DOWN) {
+ nodeSibling = nextSibling !== null ? nextSibling : renderedSearchResultItems[0];
+ }
+ if (direction === KEYCODE_ARROW_UP) {
+ nodeSibling =
+ previousSibling !== null
+ ? previousSibling
+ : renderedSearchResultItems[renderedSearchResultItems.length - 1];
+ }
+ childNodes[0].setAttribute('aria-selected', 'false');
+ nodeSibling.childNodes[0].setAttribute('aria-selected', 'true');
+ nodeSibling.focus();
+ break;
+ }
+ }
+ }
+ }
+
+ clearAriaSelected() {
+ let renderedSearchResultItems = this.customSearchItemRendererSlotContainer.children;
+ if (renderedSearchResultItems) {
+ for (let index = 0; index < renderedSearchResultItems.length; index++) {
+ let element = renderedSearchResultItems[index];
+ if (element.childNodes[0].getAttribute('aria-selected') === 'true') {
+ element.childNodes[0].setAttribute('aria-selected', 'false');
+ }
+ }
+ }
+ }
+
+ closeSearchResult() {
+ this.dispatch('closeSearchResult');
+ }
+
+ onSearchResultItemSelected(searchResultItem) {
+ if (this.search && GenericHelpers.isFunction(this.search.searchProvider.onSearchResultItemSelected)) {
+ this.search.searchProvider.onSearchResultItemSelected(searchResultItem);
+ } else if (GenericHelpers.isFunction(this.search.searchProvider.onEscape) && event.keyCode === KEYCODE_ESC) {
+ this.search.searchProvider.onEscape();
+ }
+ }
+
+ handleKeydown(result, { keyCode }, inputElement, customSearchItemRendererSlotContainer) {
+ this.updateCustomSearchItemRendererSlotContainer(customSearchItemRendererSlotContainer);
+ if (keyCode === KEYCODE_ENTER) {
+ this.search.searchProvider.onSearchResultItemSelected(result, this.search);
+ }
+ if (keyCode === KEYCODE_ARROW_UP || keyCode === KEYCODE_ARROW_DOWN) {
+ this.calcSearchResultItemSelected(keyCode);
+ } else if (GenericHelpers.isFunction(this.search.searchProvider.onEscape) && keyCode === KEYCODE_ESC) {
+ this.clearAriaSelected(this.customSearchItemRendererSlotContainer);
+ setTimeout(() => {
+ this.setFocusOnGlobalSearchFieldDesktop(inputElement);
+ });
+ this.search.searchProvider.onEscape();
+ }
+ }
+
+ setFocusOnGlobalSearchFieldDesktop(inputElement) {
+ if (inputElement) {
+ inputElement.focus();
+ }
+ }
+
+ onActionClick(searchResultItem) {
+ let node = searchResultItem.pathObject;
+ if (node.externalLink) {
+ Routing.navigateToLink(node);
+ } else {
+ this.dispatch('handleSearchNavigation', { node });
+ }
+ }
+
+ toggleSearch(isSearchFieldVisible, displaySearchResult, inputElem, customSearchItemRendererSlot) {
+ if (!isSearchFieldVisible)
+ setTimeout(() => {
+ this.setFocusOnGlobalSearchFieldDesktop();
+ });
+ else {
+ displaySearchResult = false;
+ }
+
+ this.dispatch('toggleSearch', {
+ isSearchFieldVisible,
+ inputElem,
+ customSearchItemRendererSlot
+ });
+
+ if (
+ this.search &&
+ this.search.searchProvider &&
+ GenericHelpers.isFunction(this.search.searchProvider.toggleSearch)
+ ) {
+ const fieldVisible = isSearchFieldVisible === undefined ? true : !isSearchFieldVisible;
+ this.search.searchProvider.toggleSearch(inputElem, fieldVisible);
+ }
+ }
+}
diff --git a/core/test/utilities/helpers/global-search-helpers.spec.js b/core/test/utilities/helpers/global-search-helpers.spec.js
new file mode 100644
index 0000000000..9fb10b25c7
--- /dev/null
+++ b/core/test/utilities/helpers/global-search-helpers.spec.js
@@ -0,0 +1,307 @@
+import { GlobalSearchHelperClass } from '../../../src/utilities/helpers';
+import { LuigiI18N } from '../../../src/core-api';
+import { Routing } from '../../../src/services';
+import { KEYCODE_ENTER, KEYCODE_ESC, KEYCODE_ARROW_DOWN, KEYCODE_ARROW_UP } from '../../../src/utilities/keycode';
+const sinon = require('sinon');
+const chai = require('chai');
+const assert = chai.assert;
+
+describe('Global-search-helpers', () => {
+ let globalSearchHelpers;
+ let search;
+ let dispatch;
+ let inputElement;
+
+ beforeEach(() => {
+ inputElement = document.createElement('input');
+ search = {};
+ dispatch = sinon.spy();
+ globalSearchHelpers = new GlobalSearchHelperClass(search, dispatch);
+ getCurrentLocaleStub = sinon.stub(LuigiI18N, 'getCurrentLocale').returns('en');
+ getTranslationStub = sinon.stub(LuigiI18N, 'getTranslation').returns('Digit here text to search....');
+ });
+
+ afterEach(() => {
+ sinon.restore();
+ });
+
+ describe('getCustomRenderer', () => {
+ it('should return if no search provider', () => {
+ globalSearchHelpers.getCustomRenderer();
+ assert.notExists(globalSearchHelpers.customSearchResultRenderer);
+ assert.notExists(globalSearchHelpers.customSearchResultItemRenderer);
+ });
+ });
+
+ describe('setSearchPlaceholder', () => {
+ function setupSearchProvider(inputPlaceholder) {
+ search = {
+ searchProvider: {
+ inputPlaceholder
+ }
+ };
+ globalSearchHelpers = new GlobalSearchHelperClass(search, dispatch);
+ globalSearchHelpers.setSearchPlaceholder(inputElement);
+ }
+
+ it('should set the search input placeholder when it is a string', () => {
+ setupSearchProvider('Digit here text to search....');
+ assert.strictEqual(inputElement.placeholder, 'Digit here text to search....');
+ });
+
+ it('should set the search input placeholder correctly when it is a function', () => {
+ setupSearchProvider(() => 'Function placeholder text');
+ assert.strictEqual(inputElement.placeholder, 'Function placeholder text');
+ });
+
+ it('should set the search input placeholder correctly with translation', () => {
+ setupSearchProvider({
+ en: 'English placeholder text',
+ fr: 'Texte de remplacement français'
+ });
+ assert.strictEqual(inputElement.placeholder, 'English placeholder text');
+ });
+
+ it('should not set placeholder if searchProvider was not defined', () => {
+ assert.strictEqual(inputElement.placeholder, '');
+ });
+ });
+
+ describe('onKeyUp', () => {
+ it('should call onEnter when ENTER key is pressed', () => {
+ const onEnterSpy = sinon.spy();
+ search = {
+ searchProvider: {
+ onEnter: onEnterSpy
+ }
+ };
+ globalSearchHelpers = new GlobalSearchHelperClass(search, dispatch);
+ const event = { keyCode: KEYCODE_ENTER };
+ globalSearchHelpers.onKeyUp(event);
+ assert.isTrue(onEnterSpy.calledOnce, 'onEnter should be called once');
+ });
+
+ it('should call onEscape when ESC key is pressed', () => {
+ const onEscapeSpy = sinon.spy();
+ search = {
+ searchProvider: {
+ onEscape: onEscapeSpy
+ }
+ };
+ globalSearchHelpers = new GlobalSearchHelperClass(search, dispatch);
+ const event = { keyCode: KEYCODE_ESC };
+ globalSearchHelpers.onKeyUp(event);
+ assert.isTrue(onEscapeSpy.calledOnce, 'onEscape should be called once');
+ });
+
+ it('should call onInput for other keys when onInput is defined', () => {
+ const onInputSpy = sinon.spy();
+ search = {
+ searchProvider: {
+ onInput: onInputSpy
+ }
+ };
+ globalSearchHelpers = new GlobalSearchHelperClass(search, dispatch);
+ const event = { keyCode: 65 }; // ASCII code for 'A'
+ globalSearchHelpers.onKeyUp(event);
+ assert.isTrue(onInputSpy.calledOnce, 'onInput should be called once');
+ });
+ });
+
+ describe('onActionClick', () => {
+ it('should navigate to external link if node.externalLink is defined', () => {
+ const navigateToLinkStub = sinon.stub(Routing, 'navigateToLink');
+ const searchResultItem = {
+ pathObject: {
+ externalLink: 'http://example.com'
+ }
+ };
+
+ globalSearchHelpers.onActionClick(searchResultItem);
+
+ assert.isTrue(navigateToLinkStub.calledOnce);
+ assert.isTrue(navigateToLinkStub.calledWith(searchResultItem.pathObject));
+
+ navigateToLinkStub.restore();
+ });
+
+ it('should dispatch handleSearchNavigation if node.externalLink is not defined', () => {
+ const searchResultItem = {
+ pathObject: {
+ link: '/some/path'
+ }
+ };
+
+ globalSearchHelpers.onActionClick(searchResultItem);
+
+ assert.isTrue(dispatch.calledOnce);
+ assert.isTrue(dispatch.calledWith('handleSearchNavigation', { node: searchResultItem.pathObject }));
+ });
+ });
+
+ describe('handleKeydown', () => {
+ let search;
+ let inputElement;
+ let customSearchItemRendererSlotContainer;
+ let globalSearchHelpers;
+ let clock;
+
+ beforeEach(() => {
+ search = {
+ searchProvider: {
+ onSearchResultItemSelected: sinon.spy(),
+ onEscape: sinon.spy()
+ }
+ };
+ inputElement = document.createElement('input');
+ customSearchItemRendererSlotContainer = document.createElement('div');
+ globalSearchHelpers = new GlobalSearchHelperClass(search);
+ clock = sinon.useFakeTimers();
+ });
+
+ afterEach(() => {
+ clock.restore();
+ });
+
+ function assertMethodCalledWithArgs(method, args) {
+ const methodStub = sinon.stub(globalSearchHelpers, method);
+
+ globalSearchHelpers.handleKeydown(args.result, args.event, inputElement, customSearchItemRendererSlotContainer);
+
+ assert.isTrue(methodStub.calledOnce, `${method} should be called once`);
+ assert.isTrue(methodStub.calledWith(...args.expectedArgs), `${method} should be called with correct arguments`);
+
+ methodStub.restore();
+ }
+
+ it('should call onSearchResultItemSelected when ENTER key is pressed', () => {
+ const result = {};
+ const event = { keyCode: KEYCODE_ENTER };
+
+ globalSearchHelpers.handleKeydown(result, event, inputElement, customSearchItemRendererSlotContainer);
+
+ assert.isTrue(search.searchProvider.onSearchResultItemSelected.calledOnce);
+ assert.isTrue(search.searchProvider.onSearchResultItemSelected.calledWith(result, search));
+ });
+
+ it('should call calcSearchResultItemSelected when ARROW_UP key is pressed', () => {
+ const event = { keyCode: KEYCODE_ARROW_UP };
+ assertMethodCalledWithArgs('calcSearchResultItemSelected', {
+ result: null,
+ event,
+ expectedArgs: [KEYCODE_ARROW_UP]
+ });
+ });
+
+ it('should call calcSearchResultItemSelected when ARROW_DOWN key is pressed', () => {
+ const event = { keyCode: KEYCODE_ARROW_DOWN };
+ assertMethodCalledWithArgs('calcSearchResultItemSelected', {
+ result: null,
+ event,
+ expectedArgs: [KEYCODE_ARROW_DOWN]
+ });
+ });
+
+ it('should call onEscape and clearAriaSelected when ESC key is pressed', () => {
+ const event = { keyCode: KEYCODE_ESC };
+
+ const clearAriaSelectedStub = sinon.stub(globalSearchHelpers, 'clearAriaSelected');
+ const setFocusOnGlobalSearchFieldDesktopStub = sinon.stub(
+ globalSearchHelpers,
+ 'setFocusOnGlobalSearchFieldDesktop'
+ );
+
+ globalSearchHelpers.handleKeydown(null, event, inputElement, customSearchItemRendererSlotContainer);
+ clock.tick(0);
+
+ assert.isTrue(search.searchProvider.onEscape.calledOnce);
+ assert.isTrue(clearAriaSelectedStub.calledOnce);
+ assert.isTrue(setFocusOnGlobalSearchFieldDesktopStub.calledOnce);
+
+ clearAriaSelectedStub.restore();
+ setFocusOnGlobalSearchFieldDesktopStub.restore();
+ });
+ });
+
+ describe('toggleSearch', () => {
+ let globalSearchHelpers;
+ let search;
+ let dispatchSpy;
+ let clock;
+
+ beforeEach(() => {
+ search = {
+ searchProvider: {
+ toggleSearch: sinon.spy()
+ }
+ };
+ dispatchSpy = sinon.spy();
+ globalSearchHelpers = new GlobalSearchHelperClass(search, dispatchSpy);
+ clock = sinon.useFakeTimers();
+ });
+
+ afterEach(() => {
+ clock.restore();
+ });
+
+ it('should set focus on the search field if it is not visible', () => {
+ const isSearchFieldVisible = false;
+ const displaySearchResult = true;
+ const inputElem = document.createElement('input');
+ const customSearchItemRendererSlot = document.createElement('div');
+
+ const setFocusSpy = sinon.spy(globalSearchHelpers, 'setFocusOnGlobalSearchFieldDesktop');
+
+ globalSearchHelpers.toggleSearch(
+ isSearchFieldVisible,
+ displaySearchResult,
+ inputElem,
+ customSearchItemRendererSlot
+ );
+
+ clock.tick(0);
+ assert.isTrue(setFocusSpy.calledOnce);
+ setFocusSpy.restore();
+ });
+
+ it('should dispatch toggleSearch with correct parameters', () => {
+ const isSearchFieldVisible = true;
+ const displaySearchResult = true;
+ const inputElem = document.createElement('input');
+ const customSearchItemRendererSlot = document.createElement('div');
+
+ globalSearchHelpers.toggleSearch(
+ isSearchFieldVisible,
+ displaySearchResult,
+ inputElem,
+ customSearchItemRendererSlot
+ );
+
+ assert.isTrue(dispatchSpy.calledOnce);
+ assert.isTrue(
+ dispatchSpy.calledWith('toggleSearch', {
+ isSearchFieldVisible,
+ inputElem,
+ customSearchItemRendererSlot
+ })
+ );
+ });
+
+ it('should call searchProvider.toggleSearch with correct parameters', () => {
+ const isSearchFieldVisible = true;
+ const displaySearchResult = true;
+ const inputElem = document.createElement('input');
+ const customSearchItemRendererSlot = document.createElement('div');
+
+ globalSearchHelpers.toggleSearch(
+ isSearchFieldVisible,
+ displaySearchResult,
+ inputElem,
+ customSearchItemRendererSlot
+ );
+
+ assert.isTrue(search.searchProvider.toggleSearch.calledOnce);
+ assert.isTrue(search.searchProvider.toggleSearch.calledWith(inputElem, !isSearchFieldVisible));
+ });
+ });
+});