diff --git a/packages/collections/src/components/listbox/item.gts b/packages/collections/src/components/listbox/item.gts index 9cc4b4f3..b4f7fb78 100644 --- a/packages/collections/src/components/listbox/item.gts +++ b/packages/collections/src/components/listbox/item.gts @@ -140,6 +140,7 @@ class ListboxItem extends Component { data-active="{{this.listItem.isActive}}" data-selected="{{this.listItem.isSelected}}" data-test-id="listbox-item" + data-component="listbox-item" data-key={{this.key}} disabled={{this.listItem.isDisabled}} class={{this.classNames.base}} diff --git a/packages/collections/src/components/listbox/listbox.gts b/packages/collections/src/components/listbox/listbox.gts index f827d636..983fe787 100644 --- a/packages/collections/src/components/listbox/listbox.gts +++ b/packages/collections/src/components/listbox/listbox.gts @@ -134,6 +134,7 @@ class Listbox extends Component { {{on "keydown" this.handleKeyDown}} {{on "keyup" this.handleKeyUp}} data-test-id="listbox" + data-component="listbox" class={{this.classNames}} ...attributes > diff --git a/packages/forms/src/components/native-select.gts b/packages/forms/src/components/native-select.gts index 5e5519f9..88650f76 100644 --- a/packages/forms/src/components/native-select.gts +++ b/packages/forms/src/components/native-select.gts @@ -104,6 +104,7 @@ class NativeSelect extends Component { {{on "change" this.handleOnChange}} multiple={{this.isMultiple}} data-test-id="native-select" + data-component="native-select" class={{this.classNames}} ...attributes > diff --git a/packages/forms/src/components/select.gts b/packages/forms/src/components/select.gts index 6336d360..6bc09a1c 100644 --- a/packages/forms/src/components/select.gts +++ b/packages/forms/src/components/select.gts @@ -68,7 +68,7 @@ interface SelectArgs interface SelectSignature { Args: SelectArgs; - Element: HTMLUListElement | HTMLSelectElement; + Element: HTMLDivElement; Blocks: ListboxSignature['Blocks']; } @@ -134,116 +134,118 @@ class Select extends Component { get classNames() { const { select } = useStyles(); - const { base, icon, listbox } = select(); + const { base, icon, trigger, listbox } = select(); return { base: base({ class: this.args.class }), + trigger: trigger(), icon: icon(), listbox: listbox() }; } } diff --git a/packages/theme/src/components/forms/form-field.ts b/packages/theme/src/components/forms/form-field.ts index 84a0f118..937c8f32 100644 --- a/packages/theme/src/components/forms/form-field.ts +++ b/packages/theme/src/components/forms/form-field.ts @@ -140,7 +140,8 @@ const radio = tv({ const select = tv({ slots: { - base: [ + base: [], + trigger: [ input(), 'flex items-center justify-between', 'disabled:cursor-not-allowed disabled:opacity-50' diff --git a/test-app/tests/integration/components/forms/native-select-test.ts b/test-app/tests/integration/components/forms/native-select-test.ts index 69e0b178..7730d1e9 100644 --- a/test-app/tests/integration/components/forms/native-select-test.ts +++ b/test-app/tests/integration/components/forms/native-select-test.ts @@ -1,6 +1,6 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; -import { click, render, triggerEvent } from '@ember/test-helpers'; +import { render, triggerEvent } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; function selectNativeOptionByKey( @@ -74,8 +74,6 @@ module( selectedKeys = keys; }); - assert.ok; - await render( hbs` { + return changeOption('selectNativeOptionByKey', selectSelector, key, false); +} + +function changeOption( + functionName: string, + selectSelector: string, + key: string, + toggle: boolean +): Promise { + const select = document.querySelector(selectSelector); + if (!select) { + throw new Error( + `You called "${functionName}('${selectSelector}', '${key}')" but no select was found using selector "${selectSelector}"` + ); + } + const option = select.querySelector(`[data-key="${key}"]`) as + | HTMLOptionElement + | undefined; + if (!option) { + throw new Error( + `You called "${functionName}('${selectSelector}', '${key}')" but no option with key "${key}" was found` + ); + } + if (option.selected && toggle) { + option.selected = false; + } else { + option.selected = true; + } + return triggerEvent(select, 'change'); +} + +module('Integration | Component | Select | @frontile/forms', function (hooks) { + setupRenderingTest(hooks); + + const isSelected = ( + assert: { ok: (val: boolean, mes: string) => void }, + queryString: string + ): void => { + const select = document.querySelector('[data-component="native-select"]'); + if (!select) { + throw new Error( + 'did not find native-select to check if options is selected' + ); + } + const option = select.querySelector(queryString); + const isSelected = option && (option as HTMLOptionElement).selected; + assert.ok(!!isSelected, `Expected ${queryString} to be selected`); + }; + + const isNotSelected = ( + assert: { ok: (val: boolean, mes: string) => void }, + queryString: string + ): void => { + const select = document.querySelector('[data-component="native-select"]'); + if (!select) { + throw new Error( + 'did not find native-select to check if options is selected' + ); + } + const option = select.querySelector(queryString); + const isSelected = option && (option as HTMLOptionElement).selected; + assert.ok(!isSelected, `Expected ${queryString} to not be selected`); + }; + + test('it render static items in NativeSelect and Listbox', async function (assert) { + let selectedKeys: string[] = []; + this.set('selectedKeys', []); + this.set('onSelectionChange', (keys: string[]) => { + selectedKeys = keys; + this.set('selectedKeys', keys); + }); + + await render( + hbs` +
+ ` + ); + + assert.dom('[data-component="native-select"]').exists(); + assert.dom('[data-component="native-select"] [data-key="item-1"]').exists(); + assert.dom('[data-component="native-select"] [data-key="item-2"]').exists(); + assert.dom('[data-component="native-select"] [data-key="item-3"]').exists(); + assert.dom('[data-component="native-select"] [data-key="item-4"]').exists(); + assert.dom('[data-component="native-select"] [data-key="item-5"]').exists(); + + assert + .dom('[data-component="native-select"] [data-key="item-3"]') + .hasAttribute('disabled'); + assert + .dom('[data-component="native-select"] [data-key="item-4"]') + .hasAttribute('disabled'); + + assert + .dom('[data-component="native-select"] [data-key="item-1"]') + .containsText('Item 1'); + assert + .dom('[data-component="native-select"] [data-key="item-2"]') + .containsText('Item 2'); + assert + .dom('[data-component="native-select"] [data-key="item-3"]') + .containsText('Item 3'); + assert + .dom('[data-component="native-select"] [data-key="item-4"]') + .containsText('Item 4'); + assert + .dom('[data-component="native-select"] [data-key="item-5"]') + .containsText('Item 5'); + + isNotSelected( + assert, + '[data-component="native-select"] [data-key="item-2"]' + ); + + await selectNativeOptionByKey('[data-component="native-select"]', 'item-2'); + + assert.deepEqual(selectedKeys, ['item-2']); + isSelected(assert, '[data-key="item-2"]'); + + // Check Listbox + await click('[data-component="select-trigger"]'); + + assert.dom('[data-component="listbox"]').exists(); + assert.dom('[data-component="listbox"] [data-key="item-1"]').exists(); + assert.dom('[data-component="listbox"] [data-key="item-2"]').exists(); + assert.dom('[data-component="listbox"] [data-key="item-3"]').exists(); + assert.dom('[data-component="listbox"] [data-key="item-4"]').exists(); + assert.dom('[data-component="listbox"] [data-key="item-5"]').exists(); + + assert + .dom('[data-component="listbox"] [data-key="item-3"]') + .hasAttribute('disabled'); + assert + .dom('[data-component="listbox"] [data-key="item-4"]') + .hasAttribute('disabled'); + + assert + .dom('[data-component="listbox"] [data-key="item-1"]') + .containsText('Item 1'); + assert + .dom('[data-component="listbox"] [data-key="item-2"]') + .containsText('Item 2'); + assert + .dom('[data-component="listbox"] [data-key="item-3"]') + .containsText('Item 3'); + assert + .dom('[data-component="listbox"] [data-key="item-4"]') + .containsText('Item 4'); + assert + .dom('[data-component="listbox"] [data-key="item-5"]') + .containsText('Item 5'); + + assert + .dom('[data-component="listbox"] [data-key="item-2"]') + .hasAttribute('data-selected', 'true'); + }); + + test('it render dynamic items without yield of item selectionMode = single / multiple, closes on item click', async function (assert) { + this.set('selectionMode', 'single'); + this.set('animals', ['cheetah', 'crocodile', 'elephant']); + let selectedKeys: string[] = []; + + this.set('selectedKeys', []); + this.set('onSelectionChange', (keys: string[]) => { + this.set('selectedKeys', keys); + selectedKeys = keys; + }); + + await render( + hbs` +
+ + <:item as |o|> + + {{o.item.value}} + + + ` + ); + + assert.dom('[data-test-id="native-select"]').exists(); + + assert.dom('[data-key="cheetah-key"]').exists(); + assert.dom('[data-key="crocodile-key"]').exists(); + assert.dom('[data-key="elephant-key"]').exists(); + + assert.dom('[data-key="cheetah-key"]').containsText('cheetah-value'); + assert.dom('[data-key="crocodile-key"]').containsText('crocodile-value'); + assert.dom('[data-key="elephant-key"]').containsText('elephant-value'); + + // Check Listbox + await click('[data-component="select-trigger"]'); + + assert.dom('[data-component="listbox"]').exists(); + assert.dom('[data-component="listbox"] [data-key="cheetah-key"]').exists(); + assert + .dom('[data-component="listbox"] [data-key="crocodile-key"]') + .exists(); + assert.dom('[data-component="listbox"] [data-key="elephant-key"]').exists(); + + assert + .dom('[data-component="listbox"] [data-key="cheetah-key"]') + .containsText('cheetah-value'); + assert + .dom('[data-component="listbox"] [data-key="crocodile-key"]') + .containsText('crocodile-value'); + assert + .dom('[data-component="listbox"] [data-key="elephant-key"]') + .containsText('elephant-value'); + }); + + test('keyboard navigation work (roving focus))', async function (assert) { + let selectedKeys: string[] = []; + this.set('selectedKeys', []); + this.set('onSelectionChange', (keys: string[]) => { + selectedKeys = keys; + this.set('selectedKeys', keys); + }); + + await render( + hbs` +
+ ` + ); + + // Check Listbox + await click('[data-component="select-trigger"]'); + + assert.dom('[data-component="listbox"]').exists(); + assert.dom('[data-component="listbox"] [data-key="item-1"]').exists(); + assert.dom('[data-component="listbox"] [data-key="item-2"]').exists(); + assert.dom('[data-component="listbox"] [data-key="item-3"]').exists(); + assert.dom('[data-component="listbox"] [data-key="item-4"]').exists(); + assert.dom('[data-component="listbox"] [data-key="item-5"]').exists(); + + await triggerKeyEvent('[data-component="listbox"]', 'keyup', 'ArrowDown'); + assert.dom('[data-key="item-1"]').hasAttribute('data-active', 'true'); + + await triggerKeyEvent('[data-component="listbox"]', 'keyup', 'ArrowDown'); + assert.dom('[data-key="item-1"]').hasAttribute('data-active', 'false'); + assert.dom('[data-key="item-2"]').hasAttribute('data-active', 'true'); + + await triggerKeyEvent('[data-component="listbox"]', 'keyup', 'ArrowUp'); + assert.dom('[data-key="item-1"]').hasAttribute('data-active', 'true'); + assert.dom('[data-key="item-2"]').hasAttribute('data-active', 'false'); + + await triggerKeyEvent('[data-component="listbox"]', 'keyup', 'ArrowDown'); + await triggerKeyEvent('[data-component="listbox"]', 'keypress', 'Enter'); + assert.dom('[data-component="listbox"]').doesNotExist(); + + assert.equal(selectedKeys.length, 1); + assert.equal(selectedKeys[0], 'item-2'); + }); +});