diff --git a/packages/atomic-angular/projects/atomic-angular/src/lib/stencil-generated/components.ts b/packages/atomic-angular/projects/atomic-angular/src/lib/stencil-generated/components.ts index 2222535dce4..9156001e581 100644 --- a/packages/atomic-angular/projects/atomic-angular/src/lib/stencil-generated/components.ts +++ b/packages/atomic-angular/projects/atomic-angular/src/lib/stencil-generated/components.ts @@ -1490,13 +1490,13 @@ Example: @ProxyCmp({ defineCustomElementFn: undefined, - inputs: ['clearFilters', 'disableSearch', 'enableQuerySyntax', 'minimumQueryLength', 'numberOfQueries', 'redirectionUrl', 'suggestionTimeout'] + inputs: ['clearFilters', 'disableSearch', 'enableQuerySyntax', 'minimumQueryLength', 'numberOfQueries', 'redirectionUrl', 'suggestionTimeout', 'textarea'] }) @Component({ selector: 'atomic-search-box', changeDetection: ChangeDetectionStrategy.OnPush, template: '', - inputs: ['clearFilters', 'disableSearch', 'enableQuerySyntax', 'minimumQueryLength', 'numberOfQueries', 'redirectionUrl', 'suggestionTimeout'] + inputs: ['clearFilters', 'disableSearch', 'enableQuerySyntax', 'minimumQueryLength', 'numberOfQueries', 'redirectionUrl', 'suggestionTimeout', 'textarea'] }) export class AtomicSearchBox { protected el: HTMLElement; diff --git a/packages/atomic/cypress/e2e/search-box/search-box-actions.ts b/packages/atomic/cypress/e2e/search-box/search-box-actions.ts index 16ad941d58e..c4ed1a73298 100644 --- a/packages/atomic/cypress/e2e/search-box/search-box-actions.ts +++ b/packages/atomic/cypress/e2e/search-box/search-box-actions.ts @@ -61,3 +61,12 @@ export function typeSearchInput(query: string, verifyInput = '') { SearchBoxSelectors.inputBox().type(`${query}{enter}`, {force: true}); SearchBoxAssertions.assertHasText(verifyInput || query); } + +export function typeSearchTextArea(query: string, verifyInput = '') { + SearchBoxSelectors.textArea().click(); + SearchBoxSelectors.textArea().type(`${query}{enter}`, {force: true}); + SearchBoxAssertions.assertHasText( + verifyInput || query, + SearchBoxSelectors.textArea + ); +} diff --git a/packages/atomic/cypress/e2e/search-box/search-box-assertions.ts b/packages/atomic/cypress/e2e/search-box/search-box-assertions.ts index 655f884d5b4..51a4de8dd69 100644 --- a/packages/atomic/cypress/e2e/search-box/search-box-assertions.ts +++ b/packages/atomic/cypress/e2e/search-box/search-box-assertions.ts @@ -1,14 +1,19 @@ import {SearchBoxSelectors} from './search-box-selectors'; -export function assertFocusSearchBox() { +export function assertFocusSearchBox( + searchBoxSelector = SearchBoxSelectors.inputBox +) { it('should focus on the search box', () => { - SearchBoxSelectors.inputBox().should('be.focused'); + searchBoxSelector().should('be.focused'); }); } -export function assertHasText(text: string) { +export function assertHasText( + text: string, + searchBoxSelector = SearchBoxSelectors.inputBox +) { it(`should contain "${text}"`, () => { - SearchBoxSelectors.inputBox().should('have.value', text); + searchBoxSelector().should('have.value', text); }); } diff --git a/packages/atomic/cypress/e2e/search-box/search-box-selectors.ts b/packages/atomic/cypress/e2e/search-box/search-box-selectors.ts index 07694a37f7f..550157b9c9b 100644 --- a/packages/atomic/cypress/e2e/search-box/search-box-selectors.ts +++ b/packages/atomic/cypress/e2e/search-box/search-box-selectors.ts @@ -6,6 +6,7 @@ export const SearchBoxSelectors = { host: () => cy.get(searchBoxComponent), shadow: () => cy.get(searchBoxComponent).shadow(), inputBox: () => SearchBoxSelectors.shadow().find('[part="input"]'), + textArea: () => SearchBoxSelectors.shadow().find('[part="textarea"]'), submitButton: () => SearchBoxSelectors.shadow().find('[part="submit-button"]'), querySuggestionsWrapper: () => diff --git a/packages/atomic/cypress/e2e/search-box/text-area-search-box.cypress.ts b/packages/atomic/cypress/e2e/search-box/text-area-search-box.cypress.ts new file mode 100644 index 00000000000..865cbcb028d --- /dev/null +++ b/packages/atomic/cypress/e2e/search-box/text-area-search-box.cypress.ts @@ -0,0 +1,159 @@ +import { + SafeStorage, + StorageItems, +} from '../../../src/utils/local-storage-utils'; +import {RouteAlias} from '../../fixtures/fixture-common'; +import {TestFixture} from '../../fixtures/test-fixture'; +import * as CommonAssertions from '../common-assertions'; +import {selectIdleCheckboxValueAt} from '../facets/facet-common-actions'; +import * as FacetCommonAssertions from '../facets/facet-common-assertions'; +import {addFacet, field} from '../facets/facet/facet-actions'; +import {FacetSelectors} from '../facets/facet/facet-selectors'; +import {addQuerySummary} from '../query-summary-actions'; +import * as QuerySummaryAssertions from '../query-summary-assertions'; +import { + addSearchBox, + AddSearchBoxOptions, + typeSearchTextArea, +} from './search-box-actions'; +import * as SearchBoxAssertions from './search-box-assertions'; +import {searchBoxComponent, SearchBoxSelectors} from './search-box-selectors'; + +const setSuggestions = (count: number) => () => { + cy.intercept( + {method: 'POST', path: '**/rest/search/v2/querySuggest?*'}, + (request) => { + request.reply((response) => { + const newResponse = response.body; + newResponse.completions = Array.from({length: count}, (_, i) => ({ + expression: `query-suggestion-${i}`, + executableConfidence: 0, + highlighted: `Suggestion ${i}`, + score: 0, + })); + response.send(200, newResponse); + }); + } + ).as(TestFixture.interceptAliases.QuerySuggestions.substring(1)); +}; + +const setRecentQueries = (count: number) => () => { + new SafeStorage().setJSON( + StorageItems.RECENT_QUERIES, + Array.from({length: count}, (_, i) => `Recent query ${i}`) + ); +}; + +const addTextAreaSearchBox = (options: AddSearchBoxOptions = {}) => { + const textAreaProp = {textarea: 'true'}; + const props = options?.props + ? {...options.props, ...textAreaProp} + : textAreaProp; + return addSearchBox(options ? {...options, props} : {props}); +}; + +describe('TextArea Search Box Test Suites', () => { + describe('with no suggestions nor recentQueries', () => { + beforeEach(() => { + new TestFixture() + .with(setSuggestions(0)) + .with(setRecentQueries(0)) + .with(addTextAreaSearchBox()) + .init(); + }); + + it('should be accessible', () => { + SearchBoxSelectors.textArea().click(); + CommonAssertions.assertAriaLiveMessage( + SearchBoxSelectors.searchBoxAriaLive, + ' no ' + ); + CommonAssertions.assertAccessibility(searchBoxComponent); + }); + }); + + describe('with default textarea search box', () => { + beforeEach(() => { + new TestFixture() + .with(addTextAreaSearchBox()) + .with(addQuerySummary()) + .withoutFirstAutomaticSearch() + .init(); + SearchBoxSelectors.textArea().click(); + SearchBoxSelectors.textArea().type('test{enter}', {force: true}); + cy.wait(RouteAlias.UA); + }); + + it('search button is enabled to start with', () => { + SearchBoxSelectors.textArea().should('be.empty'); + SearchBoxSelectors.submitButton().should('be.enabled'); + }); + + CommonAssertions.assertConsoleError(false); + }); + + describe('with disableSearch set to true', () => { + beforeEach(() => { + new TestFixture() + .with( + addTextAreaSearchBox({ + props: { + 'disable-search': 'true', + 'minimum-query-length': 1, // disable-search should override this setting + }, + }) + ) + .with(addQuerySummary()) + .withoutFirstAutomaticSearch() + .init(); + }); + + it('should be accessible', () => { + CommonAssertions.assertAccessibility(searchBoxComponent); + }); + + it('there are no search suggestions or errors on query input', () => { + typeSearchTextArea('test'); + SearchBoxSelectors.submitButton().should('be.disabled'); + SearchBoxAssertions.assertNoSuggestionGenerated(); + QuerySummaryAssertions.assertHasPlaceholder(); + CommonAssertions.assertConsoleError(false); + }); + }); + + describe('with a facet & clear-filters set to false', () => { + beforeEach(() => { + new TestFixture() + .with(addTextAreaSearchBox({props: {'clear-filters': 'false'}})) + .with(addFacet({field})) + .init(); + selectIdleCheckboxValueAt(FacetSelectors, 0); + cy.wait(TestFixture.interceptAliases.Search); + SearchBoxSelectors.submitButton().click({force: true}); + cy.wait(TestFixture.interceptAliases.Search); + }); + + FacetCommonAssertions.assertNumberOfSelectedCheckboxValues( + FacetSelectors, + 1 + ); + }); + + describe('with a facet & clear-filters set to true', () => { + beforeEach(() => { + new TestFixture() + .with(addTextAreaSearchBox({props: {'clear-filters': 'true'}})) + .with(addFacet({field})) + .init(); + selectIdleCheckboxValueAt(FacetSelectors, 0); + cy.wait(TestFixture.interceptAliases.Search); + SearchBoxSelectors.submitButton().click({force: true}); + cy.wait(TestFixture.interceptAliases.Search); + }); + + FacetCommonAssertions.assertNumberOfSelectedCheckboxValues( + FacetSelectors, + 0 + ); + }); +}); diff --git a/packages/atomic/src/components.d.ts b/packages/atomic/src/components.d.ts index e2d818c5407..3adf865ad95 100644 --- a/packages/atomic/src/components.d.ts +++ b/packages/atomic/src/components.d.ts @@ -1655,6 +1655,10 @@ export namespace Components { * The timeout for suggestion queries, in milliseconds. If a suggestion query times out, the suggestions from that particular query won't be shown. */ "suggestionTimeout": number; + /** + * Whether to render the search box using a [textarea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) element. The resulting component will expand to support multi-line queries. When customizing the dimensions of the textarea element using the `"textarea"` CSS part, it is important to also apply the styling to its ::after pseudo-element as well as the `"textarea-spacer"` part. The buttons within the search box are likely to need adjusting as well. Example: ```css ``` + */ + "textarea": boolean; } interface AtomicSearchBoxInstantResults { /** @@ -4583,6 +4587,10 @@ declare namespace LocalJSX { * The timeout for suggestion queries, in milliseconds. If a suggestion query times out, the suggestions from that particular query won't be shown. */ "suggestionTimeout"?: number; + /** + * Whether to render the search box using a [textarea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) element. The resulting component will expand to support multi-line queries. When customizing the dimensions of the textarea element using the `"textarea"` CSS part, it is important to also apply the styling to its ::after pseudo-element as well as the `"textarea-spacer"` part. The buttons within the search box are likely to need adjusting as well. Example: ```css ``` + */ + "textarea"?: boolean; } interface AtomicSearchBoxInstantResults { /** diff --git a/packages/atomic/src/components/common/search-box/clear-button.tsx b/packages/atomic/src/components/common/search-box/clear-button.tsx index 41634804f6a..e761c8487ef 100644 --- a/packages/atomic/src/components/common/search-box/clear-button.tsx +++ b/packages/atomic/src/components/common/search-box/clear-button.tsx @@ -5,7 +5,7 @@ import {AnyBindings} from '../interface/bindings'; interface Props extends Partial { bindings: AnyBindings; - inputRef: HTMLInputElement | null; + inputRef: HTMLInputElement | HTMLTextAreaElement | null; } export const ClearButton: FunctionalComponent = ({ @@ -17,7 +17,7 @@ export const ClearButton: FunctionalComponent = ({