From 5a97beaa11a41d83f64ee0116584301763e40eed Mon Sep 17 00:00:00 2001 From: Markus Sanin Date: Wed, 11 Dec 2024 07:45:46 +0100 Subject: [PATCH] Add `@searchFieldPosition` argument (#1864) * Add @searchFieldPosition argument * Add option `searchFieldPosition=trigger` for single select * Make position value strict * Add `searchFieldPosition` in docs * Fix lint * Handle blur directly in input component instead global * Fix lint * Fix input clear * Fix searchFieldPosition typing --- docs/app/components/snippets/the-search-2.hbs | 1 + docs/app/components/snippets/the-search-8.hbs | 9 ++ docs/app/components/snippets/the-search-8.js | 5 ++ docs/app/components/snippets/the-search-9.hbs | 9 ++ docs/app/components/snippets/the-search-9.js | 5 ++ .../public-pages/docs/the-search.js | 4 + .../public-pages/docs/api-reference.hbs | 5 ++ .../public-pages/docs/the-search.hbs | 16 ++++ ember-power-select/less/base.less | 21 +++++ ember-power-select/package.json | 1 + ember-power-select/scss/base.scss | 21 +++++ .../src/components/power-select-multiple.hbs | 3 +- .../power-select-multiple/trigger.hbs | 2 +- .../power-select-multiple/trigger.ts | 3 +- .../src/components/power-select.hbs | 2 + .../src/components/power-select.ts | 29 +++++++ .../power-select/before-options.hbs | 2 +- .../components/power-select/before-options.ts | 3 +- .../src/components/power-select/input.hbs | 27 ++++++ .../src/components/power-select/input.ts | 87 +++++++++++++++++++ .../src/components/power-select/trigger.hbs | 83 ++++++++++++++---- .../src/components/power-select/trigger.ts | 18 +++- .../power-select/general-behaviour-test.js | 21 +++++ .../components/power-select/multiple-test.js | 25 ++++++ 24 files changed, 380 insertions(+), 22 deletions(-) create mode 100644 docs/app/components/snippets/the-search-8.hbs create mode 100644 docs/app/components/snippets/the-search-8.js create mode 100644 docs/app/components/snippets/the-search-9.hbs create mode 100644 docs/app/components/snippets/the-search-9.js create mode 100644 ember-power-select/src/components/power-select/input.hbs create mode 100644 ember-power-select/src/components/power-select/input.ts diff --git a/docs/app/components/snippets/the-search-2.hbs b/docs/app/components/snippets/the-search-2.hbs index 8b08e9f59..696d7e19e 100644 --- a/docs/app/components/snippets/the-search-2.hbs +++ b/docs/app/components/snippets/the-search-2.hbs @@ -2,6 +2,7 @@

+ {{name}} + \ No newline at end of file diff --git a/docs/app/components/snippets/the-search-8.js b/docs/app/components/snippets/the-search-8.js new file mode 100644 index 000000000..876872c93 --- /dev/null +++ b/docs/app/components/snippets/the-search-8.js @@ -0,0 +1,5 @@ +import Component from '@glimmer/component'; + +export default class extends Component { + diacritics = ['María', 'Søren Larsen', 'João', 'Saša Jurić', 'Íñigo']; +} diff --git a/docs/app/components/snippets/the-search-9.hbs b/docs/app/components/snippets/the-search-9.hbs new file mode 100644 index 000000000..ebc063c25 --- /dev/null +++ b/docs/app/components/snippets/the-search-9.hbs @@ -0,0 +1,9 @@ + + {{name}} + \ No newline at end of file diff --git a/docs/app/components/snippets/the-search-9.js b/docs/app/components/snippets/the-search-9.js new file mode 100644 index 000000000..876872c93 --- /dev/null +++ b/docs/app/components/snippets/the-search-9.js @@ -0,0 +1,5 @@ +import Component from '@glimmer/component'; + +export default class extends Component { + diacritics = ['María', 'Søren Larsen', 'João', 'Saša Jurić', 'Íñigo']; +} diff --git a/docs/app/controllers/public-pages/docs/the-search.js b/docs/app/controllers/public-pages/docs/the-search.js index 9a0c4c7f0..6d468bdf4 100644 --- a/docs/app/controllers/public-pages/docs/the-search.js +++ b/docs/app/controllers/public-pages/docs/the-search.js @@ -6,6 +6,8 @@ import TheSearch4 from '../../../components/snippets/the-search-4'; import TheSearch5 from '../../../components/snippets/the-search-5'; import TheSearch6 from '../../../components/snippets/the-search-6'; import TheSearch7 from '../../../components/snippets/the-search-7'; +import TheSearch8 from '../../../components/snippets/the-search-8'; +import TheSearch9 from '../../../components/snippets/the-search-9'; export default class TheSearch extends Controller { theSearch1 = TheSearch1; @@ -15,6 +17,8 @@ export default class TheSearch extends Controller { theSearch5 = TheSearch5; theSearch6 = TheSearch6; theSearch7 = TheSearch7; + theSearch8 = TheSearch8; + theSearch9 = TheSearch9; names = [ 'Stefan', 'Miguel', diff --git a/docs/app/templates/public-pages/docs/api-reference.hbs b/docs/app/templates/public-pages/docs/api-reference.hbs index 759682215..1ed0f456d 100644 --- a/docs/app/templates/public-pages/docs/api-reference.hbs +++ b/docs/app/templates/public-pages/docs/api-reference.hbs @@ -301,6 +301,11 @@ string When the options are objects and no custom matches function is provided, this option tells the component what property of the options should the default matches use to filter + + searchFieldPosition + string + Allows to change the position of search field. Possible values: 'before-options' or 'trigger'. Default is single is before-options, default for multiple is trigger + searchMessage string diff --git a/docs/app/templates/public-pages/docs/the-search.hbs b/docs/app/templates/public-pages/docs/the-search.hbs index 3af50982a..d156a0344 100644 --- a/docs/app/templates/public-pages/docs/the-search.hbs +++ b/docs/app/templates/public-pages/docs/the-search.hbs @@ -25,6 +25,22 @@ {{component (ensure-safe-component this.theSearch2)}} +

Search field position

+ +

+ The default search field position for single select is inside the dropdown and only visible when the dropdown is open (@searchFieldPosition="before-options").
+ In multiple selection you will find the search field inside trigger box (@searchFieldPosition="trigger").
+ By passing @searchFieldPosition you can change this logic for single and multiple selection. +

+ + + {{component (ensure-safe-component this.theSearch8)}} + + + + {{component (ensure-safe-component this.theSearch9)}} + +

Customize the search field

diff --git a/ember-power-select/less/base.less b/ember-power-select/less/base.less index b5d2d5dd7..e8b72d31e 100644 --- a/ember-power-select/less/base.less +++ b/ember-power-select/less/base.less @@ -24,6 +24,14 @@ display:table; clear:both; } + + .ember-power-select-input { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + } } .ember-power-select-trigger:focus, .ember-power-select-trigger--active { @@ -152,6 +160,19 @@ } } } +.ember-power-select-search-input-field { + width: 100%; + height: 100%; + padding: 0 8px; + font-family: inherit; + font-size: inherit; + border: none; + display: block; + line-height: inherit; + -webkit-appearance: none; + outline: none; + background-color: transparent; +} // Dropdown .ember-power-select-dropdown { diff --git a/ember-power-select/package.json b/ember-power-select/package.json index 2ed08446f..5f66469f8 100644 --- a/ember-power-select/package.json +++ b/ember-power-select/package.json @@ -164,6 +164,7 @@ "./components/power-select-multiple/trigger.js": "./dist/_app_/components/power-select-multiple/trigger.js", "./components/power-select.js": "./dist/_app_/components/power-select.js", "./components/power-select/before-options.js": "./dist/_app_/components/power-select/before-options.js", + "./components/power-select/input.js": "./dist/_app_/components/power-select/input.js", "./components/power-select/label.js": "./dist/_app_/components/power-select/label.js", "./components/power-select/no-matches-message.js": "./dist/_app_/components/power-select/no-matches-message.js", "./components/power-select/options.js": "./dist/_app_/components/power-select/options.js", diff --git a/ember-power-select/scss/base.scss b/ember-power-select/scss/base.scss index 2a88a1f0d..d5cf704e9 100644 --- a/ember-power-select/scss/base.scss +++ b/ember-power-select/scss/base.scss @@ -28,6 +28,14 @@ display:table; clear:both; } + + .ember-power-select-input { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + } } .ember-power-select-trigger:focus, .ember-power-select-trigger--active { @@ -155,6 +163,19 @@ } } } +.ember-power-select-search-input-field { + width: 100%; + height: 100%; + padding: 0 8px; + font-family: inherit; + font-size: inherit; + border: none; + display: block; + line-height: inherit; + -webkit-appearance: none; + outline: none; + background-color: transparent; +} // Dropdown .ember-power-select-dropdown { diff --git a/ember-power-select/src/components/power-select-multiple.hbs b/ember-power-select/src/components/power-select-multiple.hbs index a7504ec04..196b8b978 100644 --- a/ember-power-select/src/components/power-select-multiple.hbs +++ b/ember-power-select/src/components/power-select-multiple.hbs @@ -11,7 +11,7 @@ @labelComponent={{ensure-safe-component @labelComponent}} @afterOptionsComponent={{ensure-safe-component @afterOptionsComponent}} @allowClear={{@allowClear}} - @beforeOptionsComponent={{if @beforeOptionsComponent (ensure-safe-component @beforeOptionsComponent) null}} + @beforeOptionsComponent={{if @beforeOptionsComponent (ensure-safe-component @beforeOptionsComponent)}} @buildSelection={{or @buildSelection this.defaultBuildSelection}} @calculatePosition={{@calculatePosition}} @closeOnSelect={{@closeOnSelect}} @@ -50,6 +50,7 @@ @search={{@search}} @searchEnabled={{@searchEnabled}} @searchField={{@searchField}} + @searchFieldPosition={{or @searchFieldPosition 'trigger'}} @searchMessage={{@searchMessage}} @searchMessageComponent={{@searchMessageComponent}} @searchPlaceholder={{@searchPlaceholder}} diff --git a/ember-power-select/src/components/power-select-multiple/trigger.hbs b/ember-power-select/src/components/power-select-multiple/trigger.hbs index 33030452f..5f3dfe48a 100644 --- a/ember-power-select/src/components/power-select-multiple/trigger.hbs +++ b/ember-power-select/src/components/power-select-multiple/trigger.hbs @@ -52,7 +52,7 @@ {{/if}} {{/each}} - {{#if @searchEnabled}} + {{#if (and @searchEnabled (eq @searchFieldPosition 'trigger'))}}

  • {{#let (component diff --git a/ember-power-select/src/components/power-select-multiple/trigger.ts b/ember-power-select/src/components/power-select-multiple/trigger.ts index 181b0cc5f..5656da2d9 100644 --- a/ember-power-select/src/components/power-select-multiple/trigger.ts +++ b/ember-power-select/src/components/power-select-multiple/trigger.ts @@ -2,7 +2,7 @@ import Component from '@glimmer/component'; import { action } from '@ember/object'; import { get } from '@ember/object'; import { scheduleTask } from 'ember-lifeline'; -import type { Select } from '../power-select'; +import type { Select, TSearchFieldPosition } from '../power-select'; import type { ComponentLike } from '@glint/template'; import { modifier } from 'ember-modifier'; import { deprecate } from '@ember/debug'; @@ -14,6 +14,7 @@ interface PowerSelectMultipleTriggerSignature { searchEnabled: boolean; placeholder?: string; searchField: string; + searchFieldPosition?: TSearchFieldPosition; listboxId?: string; tabindex?: string; ariaLabel?: string; diff --git a/ember-power-select/src/components/power-select.hbs b/ember-power-select/src/components/power-select.hbs index 42e4baab8..e94033552 100644 --- a/ember-power-select/src/components/power-select.hbs +++ b/ember-power-select/src/components/power-select.hbs @@ -85,6 +85,7 @@ @select={{publicAPI}} @searchEnabled={{@searchEnabled}} @searchField={{@searchField}} + @searchFieldPosition={{this.searchFieldPosition}} @onFocus={{this.handleFocus}} @onBlur={{this.handleBlur}} @extra={{@extra}} @@ -133,6 +134,7 @@ @ariaActiveDescendant={{if this.highlightedIndex (concat publicAPI.uniqueId "-" this.highlightedIndex)}} @selectedItemComponent={{ensure-safe-component @selectedItemComponent}} @searchPlaceholder={{@searchPlaceholder}} + @searchFieldPosition={{this.searchFieldPosition}} @ariaLabel={{@ariaLabel}} @ariaLabelledBy={{this.ariaLabelledBy}} @ariaDescribedBy={{@ariaDescribedBy}} diff --git a/ember-power-select/src/components/power-select.ts b/ember-power-select/src/components/power-select.ts index bda794020..3083c5298 100644 --- a/ember-power-select/src/components/power-select.ts +++ b/ember-power-select/src/components/power-select.ts @@ -91,6 +91,7 @@ export interface PowerSelectArgs { animationEnabled?: boolean; tabindex?: number | string; searchPlaceholder?: string; + searchFieldPosition?: TSearchFieldPosition; verticalPosition?: string; horizontalPosition?: string; triggerId?: string; @@ -134,6 +135,7 @@ export interface PowerSelectArgs { } export type TLabelClickAction = 'focus' | 'open'; +export type TSearchFieldPosition = 'before-options' | 'trigger'; export interface PowerSelectSignature { Element: HTMLElement; @@ -368,6 +370,12 @@ export default class PowerSelectComponent extends Component {{!-- template-lint-disable require-input-label --}} false | void; diff --git a/ember-power-select/src/components/power-select/input.hbs b/ember-power-select/src/components/power-select/input.hbs new file mode 100644 index 000000000..8373cb8ab --- /dev/null +++ b/ember-power-select/src/components/power-select/input.hbs @@ -0,0 +1,27 @@ +
    + {{!-- template-lint-disable require-input-label --}} + +
    \ No newline at end of file diff --git a/ember-power-select/src/components/power-select/input.ts b/ember-power-select/src/components/power-select/input.ts new file mode 100644 index 000000000..b2923bb40 --- /dev/null +++ b/ember-power-select/src/components/power-select/input.ts @@ -0,0 +1,87 @@ +import Component from '@glimmer/component'; +import { runTask } from 'ember-lifeline'; +import { action } from '@ember/object'; +import { modifier } from 'ember-modifier'; +import type { Select, TSearchFieldPosition } from '../power-select'; + +interface PowerSelectInputSignature { + Element: HTMLElement; + Args: { + select: Select; + ariaLabel?: string; + ariaLabelledBy?: string; + ariaDescribedBy?: string; + role?: string; + searchPlaceholder?: string; + searchFieldPosition?: TSearchFieldPosition; + ariaActiveDescendant?: string; + listboxId?: string; + onKeydown: (e: KeyboardEvent) => false | void; + onBlur: (e: FocusEvent) => void; + onFocus: (e: FocusEvent) => void; + onInput: (e: InputEvent) => boolean; + autofocus?: boolean; + }; +} + +export default class PowerSelectInput extends Component { + didSetup: boolean = false; + + @action + handleKeydown(e: KeyboardEvent): false | void { + if (this.args.onKeydown(e) === false) { + return false; + } + if (e.keyCode === 13) { + this.args.select.actions.close(e); + } + } + + @action + handleInput(event: Event): false | void { + const e = event as InputEvent; + if (this.args.onInput(e) === false) { + return false; + } + } + + @action + handleBlur(event: Event) { + if (this.args.searchFieldPosition === 'trigger') { + this.args.select.actions?.search(''); + } + + this.args.onBlur(event as FocusEvent); + } + + setupInput = modifier( + (el: HTMLElement) => { + if (this.didSetup) { + return; + } + + this.didSetup = true; + + this._focusInput(el); + + return () => { + this.args.select.actions?.search(''); + }; + }, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + { eager: false }, + ); + + private _focusInput(el: HTMLElement) { + runTask( + this, + () => { + if (this.args.autofocus !== false) { + el.focus(); + } + }, + 0, + ); + } +} diff --git a/ember-power-select/src/components/power-select/trigger.hbs b/ember-power-select/src/components/power-select/trigger.hbs index adf1143cd..f583271bd 100644 --- a/ember-power-select/src/components/power-select/trigger.hbs +++ b/ember-power-select/src/components/power-select/trigger.hbs @@ -1,24 +1,77 @@ {{#if (ember-power-select-is-selected-present @select.selected)}} - {{#if @selectedItemComponent}} - {{#let (component (ensure-safe-component @selectedItemComponent)) as |SelectedItemComponent|}} - - {{/let}} - {{else}} - {{yield @select.selected @select}} + {{#if (or (not-eq @searchFieldPosition 'trigger') (not @select.searchText))}} + {{#if @selectedItemComponent}} + {{#let (component (ensure-safe-component @selectedItemComponent)) as |SelectedItemComponent|}} + + {{/let}} + {{else}} + {{yield @select.selected @select}} + {{/if}} + {{/if}} + {{#if (and @searchEnabled (eq @searchFieldPosition 'trigger'))}} + {{/if}} {{#if (and @allowClear (not @select.disabled))}} {{!-- template-lint-disable no-pointer-down-event-binding --}} × {{/if}} {{else}} - {{#let (component (ensure-safe-component @placeholderComponent)) as |PlaceholderComponent|}} - - {{/let}} + {{#if (and @searchEnabled (eq @searchFieldPosition 'trigger'))}} + {{#let + (component + "power-select/input" + select=@select + ariaActiveDescendant=@ariaActiveDescendant + ariaLabelledBy=@ariaLabelledBy + ariaDescribedBy=@ariaDescribedBy + role=@role + ariaLabel=@ariaLabel + listboxId=@listboxId + searchPlaceholder=@placeholder + onFocus=@onFocus + onBlur=@onBlur + onKeydown=@onKeydown + onInput=@onInput + searchFieldPosition=@searchFieldPosition + autofocus=false + ) + as |InputComponent| + }} + {{#let (component (ensure-safe-component @placeholderComponent)) as |PlaceholderComponent|}} + + {{/let}} + {{/let}} + {{else}} + {{#let (component (ensure-safe-component @placeholderComponent)) as |PlaceholderComponent|}} + + {{/let}} + {{/if}} {{/if}} diff --git a/ember-power-select/src/components/power-select/trigger.ts b/ember-power-select/src/components/power-select/trigger.ts index 0934361a1..006fb42d8 100644 --- a/ember-power-select/src/components/power-select/trigger.ts +++ b/ember-power-select/src/components/power-select/trigger.ts @@ -1,6 +1,6 @@ import Component from '@glimmer/component'; import { action } from '@ember/object'; -import type { Select } from '../power-select'; +import type { Select, TSearchFieldPosition } from '../power-select'; import type { ComponentLike } from '@glint/template'; interface PowerSelectTriggerSignature { @@ -8,10 +8,24 @@ interface PowerSelectTriggerSignature { Args: { select: Select; allowClear: boolean; - extra: any; + searchEnabled: boolean; placeholder?: string; + searchField: string; + searchFieldPosition?: TSearchFieldPosition; + listboxId?: string; + tabindex?: string; + ariaLabel?: string; + ariaLabelledBy?: string; + ariaDescribedBy?: string; + role?: string; + ariaActiveDescendant: string; + extra?: any; placeholderComponent?: string | ComponentLike; selectedItemComponent?: string | ComponentLike; + onInput?: (e: InputEvent) => boolean; + onKeydown?: (e: KeyboardEvent) => boolean; + onFocus?: (e: FocusEvent) => void; + onBlur?: (e: FocusEvent) => void; }; Blocks: { default: [selected: any, select: Select]; diff --git a/test-app/tests/integration/components/power-select/general-behaviour-test.js b/test-app/tests/integration/components/power-select/general-behaviour-test.js index cc9b0d957..fbf27133f 100644 --- a/test-app/tests/integration/components/power-select/general-behaviour-test.js +++ b/test-app/tests/integration/components/power-select/general-behaviour-test.js @@ -126,6 +126,27 @@ module( .exists('The search box is rendered'); }); + test('The search box position is inside the trigger by passing `@searchFieldPosition="trigger"`', async function (assert) { + assert.expect(3); + + this.numbers = numbers; + await render(hbs` + + {{option}} + + `); + + assert + .dom('.ember-power-select-trigger input[type="search"]') + .exists('The search box is rendered in trigger'); + + await clickTrigger(); + assert.dom('.ember-power-select-dropdown').exists('Dropdown is rendered'); + assert + .dom('.ember-power-select-search') + .doesNotExist('The search box does not exists in dropdown'); + }); + test("The search box shouldn't gain focus if autofocus is disabled", async function (assert) { assert.expect(1); this.numbers = numbers; diff --git a/test-app/tests/integration/components/power-select/multiple-test.js b/test-app/tests/integration/components/power-select/multiple-test.js index 1ccbef067..cdb56515a 100644 --- a/test-app/tests/integration/components/power-select/multiple-test.js +++ b/test-app/tests/integration/components/power-select/multiple-test.js @@ -55,6 +55,19 @@ module( assert.dom('.ember-power-select-dropdown input').doesNotExist(); }); + test('Multiple selects have a search box in the dropdown when the search is enabled and search position is `after-options`', async function (assert) { + assert.expect(2); + this.numbers = numbers; + await render(hbs` + + {{option}} + + `); + await clickTrigger(); + assert.dom('.ember-power-select-trigger input').doesNotExist(); + assert.dom('.ember-power-select-dropdown input').exists(); + }); + test('The searchbox of multiple selects has type="search" and a form attribute to prevent submitting the wrapper form when pressing enter', async function (assert) { assert.expect(2); @@ -86,6 +99,18 @@ module( assert.dom('.ember-power-select-trigger-multiple-input').isFocused(); }); + test('When the select opens and search position is `after-options`, the search input (if any) in the dropdown gets the focus', async function (assert) { + assert.expect(1); + this.numbers = numbers; + await render(hbs` + + {{option}} + + `); + await clickTrigger(); + assert.dom('.ember-power-select-search-input').isFocused(); + }); + test("Click on an element selects it and closes the dropdown and focuses the trigger's input", async function (assert) { assert.expect(4);