diff --git a/tests/dummy/app/templates/listbox/listbox-basic.hbs b/tests/dummy/app/templates/listbox/listbox-basic.hbs
new file mode 100644
index 00000000..2510e0ff
--- /dev/null
+++ b/tests/dummy/app/templates/listbox/listbox-basic.hbs
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tests/integration/components/listbox-test.js b/tests/integration/components/listbox-test.js
new file mode 100644
index 00000000..02ea3e93
--- /dev/null
+++ b/tests/integration/components/listbox-test.js
@@ -0,0 +1,3320 @@
+import { module, test, todo, skip } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import {
+ click,
+ render,
+ triggerKeyEvent,
+ triggerEvent,
+ focus,
+} from '@ember/test-helpers';
+import { hbs } from 'ember-cli-htmlbars';
+import {
+ assertActiveElement,
+ assertListboxButton,
+ assertListboxLabel,
+ assertListboxOption,
+ assertListbox,
+ assertListboxButtonLinkedWithListboxLabel,
+ assertListboxButtonLinkedWithListbox,
+ assertListboxLabelLinkedWithListbox,
+ assertActiveListboxOption,
+ assertNoActiveListboxOption,
+ assertNoSelectedListboxOption,
+ ListboxState,
+ getListboxButton,
+ getListboxButtons,
+ getListboxLabel,
+ getListboxOptions,
+ getListbox,
+ getListboxes,
+} from '../../accessibility-assertions';
+
+async function typeWord(word) {
+ word.split('').forEach((char) => {
+ triggerEvent(document.activeElement, 'keypress', {
+ key: char,
+ });
+ });
+ await new Promise((r) => setTimeout(r, 600));
+}
+
+module('Integration | Component | ', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('should be possible to render a Listbox without crashing', async function () {
+ await render(hbs`
+
+ Trigger
+
+ option
+
+
+ `);
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'my-custom-property' },
+ });
+ assertListbox({
+ state: ListboxState.InvisibleUnmounted,
+ });
+ });
+
+ test('should be possible to render a Listbox using a "isOpen" property', async function () {
+ await render(hbs`
+
+ Trigger
+ {{#if listbox.isOpen}}
+
+ option
+
+ {{/if}}
+
+ `);
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'my-custom-property' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ await click(getListboxButton());
+
+ assertListboxButton({
+ state: ListboxState.Visible,
+ attributes: { 'data-test': 'my-custom-property' },
+ });
+ assertListbox({ state: ListboxState.Visible });
+ });
+
+ test('should be possible to disable a Listbox', async function () {
+ await render(hbs`
+
+ Trigger
+
+ option
+
+
+ `);
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'my-custom-property' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+ });
+
+ module('', () => {
+ test('should be possible to render a using yielded props', async () => {
+ await render(hbs`
+
+ {{listbox.isOpen}} {{listbox.disabled}}
+ Trigger
+
+ option
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'my-custom-property' },
+ });
+ assertListboxLabel({
+ attributes: { 'data-test': 'headlessui-listbox-label-1' },
+ textContent: 'false false',
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ await click(getListboxButton());
+
+ assertListboxLabel({
+ attributes: { 'data-test': 'headlessui-listbox-label-1' },
+ textContent: 'true false',
+ });
+ assertListbox({ state: ListboxState.Visible });
+ assertListboxLabelLinkedWithListbox();
+ assertListboxButtonLinkedWithListboxLabel();
+ });
+
+ test('should be possible to render a Listbox.Label using a yielded props and tag name', async () => {
+ await render(hbs`
+
+ Label
+ Trigger
+
+ Option A
+ Option B
+ Option C
+
+
+ `);
+
+ assertListboxLabel({
+ attributes: { 'data-test': 'headlessui-listbox-label-1' },
+ textContent: 'Label',
+ tag: 'p',
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ await click(getListboxButton());
+ assertListboxLabel({
+ attributes: { 'data-test': 'headlessui-listbox-label-1' },
+ textContent: 'Label',
+ tag: 'p',
+ });
+ assertListbox({ state: ListboxState.Visible });
+ });
+ });
+
+ module('', () => {
+ test('should be possible to render a using yielded props', async () => {
+ await render(hbs`
+
+ Label
+ {{listbox.isOpen}} {{listbox.disabled}}
+
+ Option A
+ Option B
+ Option C
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ textContent: 'false false',
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ await click(getListboxButton());
+
+ assertListboxButton({
+ state: ListboxState.Visible,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ textContent: 'true false',
+ });
+ assertListbox({ state: ListboxState.Visible });
+ });
+
+ test('should be possible to render a using yielded props and tag name', async () => {
+ await render(hbs`
+
+ Label
+ {{listbox.isOpen}} {{listbox.disabled}}
+
+ Option A
+ Option B
+ Option C
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ textContent: 'false false',
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ await click(getListboxButton());
+
+ assertListboxButton({
+ state: ListboxState.Visible,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ textContent: 'true false',
+ });
+ assertListbox({ state: ListboxState.Visible });
+ });
+
+ test('should be possible to render a Listbox.Button and a Listbox.Label and see them linked together', async () => {
+ await render(hbs`
+
+ Label
+ Trigger
+
+ Option A
+ Option B
+ Option C
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+ assertListboxButtonLinkedWithListboxLabel();
+ });
+ });
+
+ module('', () => {
+ test('should be possible to render a using yielded props', async () => {
+ await render(hbs`
+
+ Trigger
+
+ {{listbox.isOpen}}
+ {{listbox.isOpen}}
+ {{listbox.isOpen}}
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ await click(getListboxButton());
+
+ assertListboxButton({
+ state: ListboxState.Visible,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({
+ state: ListboxState.Visible,
+ textContent: 'true true true',
+ });
+ await assertActiveElement(getListbox());
+ });
+
+ test('should be possible to always render the Listbox.Options if we provide it a `static` prop', async (assert) => {
+ await render(hbs`
+
+ Trigger
+
+ Option A
+ Option B
+ Option C
+
+
+ `);
+
+ // Let's verify that the Listbox is already there
+ assert.dom(getListbox()).exists();
+ });
+
+ todo(
+ 'should be possible to use a different render strategy for the Listbox.Options',
+ async () => {}
+ );
+ });
+
+ module('', () => {
+ test('should be possible to render a using yielded props', async () => {
+ await render(hbs`
+
+ Trigger
+
+ {{option.active}} {{option.selected}} {{option.disabled}}
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ await click(getListboxButton());
+
+ assertListboxButton({
+ state: ListboxState.Visible,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({
+ state: ListboxState.Visible,
+ textContent: 'false false false',
+ });
+ });
+ });
+
+ module('Listbox Rendering composition', () => {
+ todo(
+ 'should be possible to conditionally render classNames (aka className can be a function?!)',
+ async function () {}
+ );
+
+ test('should be possible to swap the Listbox option with a button for example', async () => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Open Listbox
+ await click(getListboxButton());
+
+ // Verify options are buttons now
+ getListboxOptions().forEach((option) =>
+ assertListboxOption({ tag: 'button' }, option)
+ );
+ });
+ });
+
+ module('Listbox composition', () => {
+ todo(
+ 'test should be possible to wrap the Listbox.Options with a Transition component',
+ async function () {}
+ );
+ });
+
+ module('Listbox keyboard actions', () => {
+ test('`Enter` key', async (assert) => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Focus the button
+ getListboxButton()?.focus();
+
+ // Open listbox
+ await triggerKeyEvent(document.activeElement, 'keypress', 'Enter');
+
+ // Verify it is visible
+ assertListboxButton({ state: ListboxState.Visible });
+ assertListbox({
+ state: ListboxState.Visible,
+ });
+ await assertActiveElement(getListbox());
+ assertListboxButtonLinkedWithListbox();
+
+ // Verify we have listbox options
+ let options = getListboxOptions();
+ assert.equal(options.length, 3);
+
+ getListboxOptions().forEach((option) =>
+ assertListboxOption({ selected: false }, option)
+ );
+ // Verify that the first listbox option is active
+ assertActiveListboxOption(options[0]);
+ assertNoSelectedListboxOption();
+ });
+
+ test('should not be possible to open the listbox with Enter when the button is disabled', async () => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Focus the button
+ getListboxButton()?.focus();
+
+ // Try to open the listbox
+ await triggerKeyEvent(document.activeElement, 'keypress', 'Enter');
+
+ // Verify it is still closed
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+ });
+
+ test('should be possible to open the listbox with Enter, and focus the selected option', async (assert) => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Focus the button
+ getListboxButton()?.focus();
+
+ // Open listbox
+ await triggerKeyEvent(document.activeElement, 'keypress', 'Enter');
+
+ // Verify it is visible
+ assertListboxButton({ state: ListboxState.Visible });
+ assertListbox({
+ state: ListboxState.Visible,
+ attributes: { 'data-test': 'headlessui-listbox-options-1' },
+ });
+ await assertActiveElement(getListbox());
+ assertListboxButtonLinkedWithListbox();
+
+ // Verify we have listbox options
+ let options = getListboxOptions();
+ assert.equal(options.length, 3);
+ options.forEach((option, i) =>
+ assertListboxOption({ selected: i === 1 }, option)
+ );
+
+ // Verify that the second listbox option is active (because it is already selected)
+ assertActiveListboxOption(options[1]);
+ });
+
+ todo(
+ 'should be possible to open the listbox with Enter, and focus the selected option (when using the `hidden` render strategy)',
+ async () => {}
+ );
+
+ test('should be possible to open the listbox with Enter, and focus the selected option (with a list of objects)', async function (assert) {
+ this.set('myOptions', [
+ { id: 'a', name: 'Option A' },
+ { id: 'b', name: 'Option B' },
+ { id: 'c', name: 'Option C' },
+ ]);
+ this.set('selectedOption', this.myOptions[1]);
+ await render(hbs`
+
+ Trigger
+
+ {{#each this.myOptions as |myOption|}}
+
+ {{myOption.name}}
+
+ {{/each}}
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Focus the button
+ getListboxButton()?.focus();
+
+ // Open listbox
+ await triggerKeyEvent(document.activeElement, 'keypress', 'Enter');
+
+ // Verify it is visible
+ assertListboxButton({ state: ListboxState.Visible });
+ assertListbox({
+ state: ListboxState.Visible,
+ attributes: { 'data-test': 'headlessui-listbox-options-1' },
+ });
+ await assertActiveElement(getListbox());
+ assertListboxButtonLinkedWithListbox();
+
+ // Verify we have listbox options
+ let options = getListboxOptions();
+ assert.equal(options.length, 3);
+ options.forEach((option, i) =>
+ assertListboxOption({ selected: i === 1 }, option)
+ );
+
+ // Verify that the second listbox option is active (because it is already selected)
+ assertActiveListboxOption(options[1]);
+ });
+
+ test('should have no active listbox option when there are no listbox options at all', async () => {
+ await render(hbs`
+
+ Trigger
+
+
+ `);
+
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Focus the button
+ getListboxButton()?.focus();
+
+ // Open listbox
+ await triggerKeyEvent(document.activeElement, 'keypress', 'Enter');
+ assertListbox({ state: ListboxState.Visible });
+ await assertActiveElement(getListbox());
+
+ assertNoActiveListboxOption();
+ });
+
+ test('should focus the first non disabled listbox option when opening with Enter', async () => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Focus the button
+ getListboxButton()?.focus();
+
+ // Open listbox
+ await triggerKeyEvent(document.activeElement, 'keypress', 'Enter');
+
+ let options = getListboxOptions();
+
+ // Verify that the first non-disabled listbox option is active
+ assertActiveListboxOption(options[1]);
+ });
+
+ test('should focus the first non disabled listbox option when opening with Enter (jump over multiple disabled ones)', async () => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Focus the button
+ getListboxButton()?.focus();
+
+ // Open listbox
+ await triggerKeyEvent(document.activeElement, 'keypress', 'Enter');
+
+ let options = getListboxOptions();
+
+ // Verify that the first non-disabled listbox option is active
+ assertActiveListboxOption(options[2]);
+ });
+
+ test('should have no active listbox option upon Enter key press, when there are no non-disabled listbox options', async () => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Focus the button
+ getListboxButton().focus();
+
+ // Open listbox
+ await triggerKeyEvent(document.activeElement, 'keypress', 'Enter');
+
+ assertNoActiveListboxOption();
+ });
+
+ test('should be possible to close the listbox with Enter when there is no active listboxoption', async () => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Open listbox
+ await click(getListboxButton());
+
+ // Verify it is visible
+ assertListboxButton({ state: ListboxState.Visible });
+
+ // Close listbox
+ await triggerKeyEvent(document.activeElement, 'keypress', 'Enter');
+
+ // Verify it is closed
+ assertListboxButton({ state: ListboxState.InvisibleUnmounted });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Verify the button is focused again
+ await assertActiveElement(getListboxButton());
+ });
+
+ test('should be possible to close the listbox with Enter and choose the active listbox option', async function (assert) {
+ let callValue = '',
+ callCount = 0;
+
+ this.set('onChange', (value) => {
+ this.set('selectedOption', value);
+ callValue = value;
+ callCount++;
+ });
+
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Open listbox
+ await click(getListboxButton());
+
+ // Verify it is visible
+ assertListboxButton({ state: ListboxState.Visible });
+
+ // Activate the first listbox option
+ let options = getListboxOptions();
+ await triggerEvent(options[0], 'mouseover');
+
+ // Choose option, and close listbox
+ await triggerKeyEvent(document.activeElement, 'keypress', 'Enter');
+
+ // Verify it is closed
+ assertListboxButton({ state: ListboxState.InvisibleUnmounted });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Verify we got the change event
+ assert.equal(callCount, 1, 'handleChange called once exactly');
+ assert.equal(callValue, 'a', 'handleChange called with "a"');
+
+ // Verify the button is focused again
+ await assertActiveElement(getListboxButton());
+
+ // Open listbox again
+ await click(getListboxButton());
+
+ // Verify the active option is the previously selected one
+ assertActiveListboxOption(getListboxOptions()[0]);
+ });
+ });
+
+ module('Listbox `Space` key', () => {
+ test('should be possible to open the listbox with Space', async (assert) => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Focus the button
+ getListboxButton()?.focus();
+
+ // Open listbox
+ await triggerKeyEvent(document.activeElement, 'keypress', 'Space');
+
+ // Verify it is visible
+ assertListboxButton({ state: ListboxState.Visible });
+ assertListbox({
+ state: ListboxState.Visible,
+ attributes: { 'data-test': 'headlessui-listbox-options-1' },
+ });
+ await assertActiveElement(getListbox());
+ assertListboxButtonLinkedWithListbox();
+
+ // Verify we have listbox options
+ let options = getListboxOptions();
+ assert.equal(options.length, 3);
+ options.forEach((option) => assertListboxOption({}, option));
+ assertActiveListboxOption(options[0]);
+ });
+
+ test('should not be possible to open the listbox with Space when the button is disabled', async () => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Focus the button
+ getListboxButton()?.focus();
+
+ // Try to open the listbox
+ await triggerKeyEvent(document.activeElement, 'keypress', 'Space');
+ // Verify it is still closed
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+ });
+
+ test('should be possible to open the listbox with Space, and focus the selected option', async (assert) => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Focus the button
+ getListboxButton()?.focus();
+
+ // Open listbox
+ await triggerKeyEvent(document.activeElement, 'keypress', 'Space');
+
+ // Verify it is visible
+ assertListboxButton({ state: ListboxState.Visible });
+ assertListbox({
+ state: ListboxState.Visible,
+ attributes: { 'data-test': 'headlessui-listbox-options-1' },
+ });
+ await assertActiveElement(getListbox());
+ assertListboxButtonLinkedWithListbox();
+
+ // Verify we have listbox options
+ let options = getListboxOptions();
+ assert.equal(options.length, 3);
+ options.forEach((option, i) =>
+ assertListboxOption({ selected: i === 1 }, option)
+ );
+
+ // Verify that the second listbox option is active (because it is already selected)
+ assertActiveListboxOption(options[1]);
+ });
+
+ test('should have no active listbox option when there are no listbox options at all', async () => {
+ await render(hbs`
+
+ Trigger
+
+
+ `);
+
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Focus the button
+ getListboxButton()?.focus();
+
+ // Open listbox
+ await triggerKeyEvent(document.activeElement, 'keypress', 'Space');
+ assertListbox({ state: ListboxState.Visible });
+ await assertActiveElement(getListbox());
+
+ assertNoActiveListboxOption();
+ });
+
+ test('should focus the first non disabled listbox option when opening with Space', async () => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Focus the button
+ getListboxButton()?.focus();
+
+ // Open listbox
+ await triggerKeyEvent(document.activeElement, 'keypress', 'Space');
+
+ let options = getListboxOptions();
+
+ // Verify that the first non-disabled listbox option is active
+ assertActiveListboxOption(options[1]);
+ });
+
+ test('should focus the first non disabled listbox option when opening with Space (jump over multiple disabled ones)', async () => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Focus the button
+ getListboxButton()?.focus();
+
+ // Open listbox
+ await triggerKeyEvent(document.activeElement, 'keypress', 'Space');
+
+ let options = getListboxOptions();
+
+ // Verify that the first non-disabled listbox option is active
+ assertActiveListboxOption(options[2]);
+ });
+
+ test('should have no active listbox option upon Space key press, when there are no non-disabled listbox options', async () => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Focus the button
+ getListboxButton()?.focus();
+
+ // Open listbox
+ await triggerKeyEvent(document.activeElement, 'keypress', 'Space');
+
+ assertNoActiveListboxOption();
+ });
+
+ test('should be possible to close the listbox with Space and choose the active listbox option', async function (assert) {
+ let callValue = '',
+ callCount = 0;
+
+ this.set('onChange', (value) => {
+ this.set('selectedOption', value);
+ callValue = value;
+ callCount++;
+ });
+
+ await render(hbs`
+
+ Trigger
+
+ Option A
+ Option B
+ Option C
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Open listbox
+ await click(getListboxButton());
+
+ // Verify it is visible
+ assertListboxButton({ state: ListboxState.Visible });
+
+ // Activate the first listbox option
+ let options = getListboxOptions();
+ await triggerEvent(options[0], 'mouseover');
+
+ // Choose option, and close listbox
+ await triggerKeyEvent(document.activeElement, 'keypress', 'Space');
+
+ // Verify it is closed
+ assertListboxButton({ state: ListboxState.InvisibleUnmounted });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Verify we got the change event
+ assert.equal(callCount, 1, 'handleChange called once exactly');
+ assert.equal(callValue, 'a', 'handleChange called with "a"');
+
+ // Verify the button is focused again
+ await assertActiveElement(getListboxButton());
+
+ // Open listbox again
+ await click(getListboxButton());
+
+ // Verify the active option is the previously selected one
+ assertActiveListboxOption(getListboxOptions()[0]);
+ });
+ });
+
+ module('Listbox `Escape` key', () => {
+ test('should be possible to close an open listbox with Escape', async () => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+
+ `);
+
+ // Focus the button
+ getListboxButton().focus();
+
+ // Open listbox
+ await triggerKeyEvent(document.activeElement, 'keypress', 'Space');
+
+ // Verify it is visible
+ assertListboxButton({ state: ListboxState.Visible });
+ assertListbox({
+ state: ListboxState.Visible,
+ attributes: { 'data-test': 'headlessui-listbox-options-1' },
+ });
+ await assertActiveElement(getListbox());
+ assertListboxButtonLinkedWithListbox();
+
+ // Close listbox
+ await triggerKeyEvent(document.activeElement, 'keyup', 'Escape');
+
+ // Verify it is closed
+ assertListboxButton({ state: ListboxState.InvisibleUnmounted });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Verify the button is focused again
+ await assertActiveElement(getListboxButton());
+ });
+ });
+
+ module('Listbox `Tab` key', () => {
+ test('should focus trap when we use Tab', async (assert) => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Focus the button
+ getListboxButton()?.focus();
+
+ // Open listbox
+ await triggerKeyEvent(document.activeElement, 'keypress', 'Enter');
+
+ // Verify it is visible
+ assertListboxButton({ state: ListboxState.Visible });
+ assertListbox({
+ state: ListboxState.Visible,
+ attributes: { 'data-test': 'headlessui-listbox-options-1' },
+ });
+ await assertActiveElement(getListbox());
+ assertListboxButtonLinkedWithListbox();
+
+ // Verify we have listbox options
+ let options = getListboxOptions();
+ assert.equal(options.length, 3);
+ options.forEach((option) => assertListboxOption({}, option));
+ assertActiveListboxOption(options[0]);
+
+ // Try to tab
+ await triggerKeyEvent(document.activeElement, 'keypress', 'Tab');
+
+ // Verify it is still open
+ assertListboxButton({ state: ListboxState.Visible });
+ assertListbox({ state: ListboxState.Visible });
+ await assertActiveElement(getListbox());
+ });
+
+ test('should focus trap when we use Shift+Tab', async (assert) => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Focus the button
+ getListboxButton()?.focus();
+
+ // Open listbox
+ await triggerKeyEvent(document.activeElement, 'keypress', 'Enter');
+
+ // Verify it is visible
+ assertListboxButton({ state: ListboxState.Visible });
+ assertListbox({
+ state: ListboxState.Visible,
+ attributes: { 'data-test': 'headlessui-listbox-options-1' },
+ });
+ await assertActiveElement(getListbox());
+ assertListboxButtonLinkedWithListbox();
+
+ // Verify we have listbox options
+ let options = getListboxOptions();
+ assert.equal(options.length, 3);
+ options.forEach((option) => assertListboxOption({}, option));
+ assertActiveListboxOption(options[0]);
+
+ // Try to Shift+Tab
+ await triggerKeyEvent(document.activeElement, 'keypress', 'Tab', {
+ shiftKey: true,
+ });
+
+ // Verify it is still open
+ assertListboxButton({ state: ListboxState.Visible });
+ assertListbox({ state: ListboxState.Visible });
+ await assertActiveElement(getListbox());
+ });
+ });
+
+ module('Listbox `ArrowDown` key', () => {
+ test('should be possible to open the listbox with ArrowDown', async (assert) => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Focus the button
+ getListboxButton()?.focus();
+
+ // Open listbox
+ await triggerKeyEvent(document.activeElement, 'keyup', 'ArrowDown');
+
+ // Verify it is visible
+ assertListboxButton({ state: ListboxState.Visible });
+ assertListbox({
+ state: ListboxState.Visible,
+ attributes: { 'data-test': 'headlessui-listbox-options-1' },
+ });
+ await assertActiveElement(getListbox());
+ assertListboxButtonLinkedWithListbox();
+
+ // Verify we have listbox options
+ let options = getListboxOptions();
+ assert.equal(options.length, 3);
+ options.forEach((option) => assertListboxOption({}, option));
+
+ // Verify that the first listbox option is active
+ assertActiveListboxOption(options[0]);
+ });
+
+ test('should be possible to use ArrowDown to navigate the listbox options', async (assert) => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Focus the button
+ getListboxButton()?.focus();
+
+ // Open listbox
+ await triggerKeyEvent(document.activeElement, 'keypress', 'Enter');
+
+ // Verify we have listbox options
+ let options = getListboxOptions();
+ assert.equal(options.length, 3);
+ options.forEach((option) => assertListboxOption({}, option));
+ assertActiveListboxOption(options[0]);
+
+ // We should be able to go down once
+ await triggerKeyEvent(document.activeElement, 'keyup', 'ArrowDown');
+ assertActiveListboxOption(options[1]);
+
+ // We should be able to go down again
+ await triggerKeyEvent(document.activeElement, 'keyup', 'ArrowDown');
+ assertActiveListboxOption(options[2]);
+
+ // We should NOT be able to go down again (because last option). Current implementation won't go around.
+ await triggerKeyEvent(document.activeElement, 'keyup', 'ArrowDown');
+ assertActiveListboxOption(options[2]);
+ });
+
+ test('should be possible to use ArrowDown to navigate the listbox options and skip the first disabled one', async (assert) => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Focus the button
+ getListboxButton()?.focus();
+
+ // Open listbox
+ await triggerKeyEvent(document.activeElement, 'keypress', 'Enter');
+
+ // Verify we have listbox options
+ let options = getListboxOptions();
+ assert.equal(options.length, 3);
+ options.forEach((option) => assertListboxOption({}, option));
+ assertActiveListboxOption(options[1]);
+
+ // We should be able to go down once
+ await triggerKeyEvent(document.activeElement, 'keyup', 'ArrowDown');
+ assertActiveListboxOption(options[2]);
+ });
+
+ test('should be possible to use ArrowDown to navigate the listbox options and jump to the first non-disabled one', async (assert) => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Focus the button
+ getListboxButton()?.focus();
+
+ // Open listbox
+ await triggerKeyEvent(document.activeElement, 'keypress', 'Enter');
+
+ // Verify we have listbox options
+ let options = getListboxOptions();
+ assert.equal(options.length, 3);
+ options.forEach((option) => assertListboxOption({}, option));
+ assertActiveListboxOption(options[2]);
+ });
+ });
+
+ module('Listbox `ArrowRight` key', () => {
+ test('should be possible to use ArrowRight to navigate the listbox options', async (assert) => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Focus the button
+ getListboxButton()?.focus();
+
+ // Open listbox
+ await triggerKeyEvent(document.activeElement, 'keypress', 'Enter');
+
+ // Verify we have listbox options
+ let options = getListboxOptions();
+ assert.equal(options.length, 3);
+ options.forEach((option) => assertListboxOption({}, option));
+ assertActiveListboxOption(options[0]);
+
+ // We should be able to go right once
+ await triggerKeyEvent(document.activeElement, 'keyup', 'ArrowRight');
+ assertActiveListboxOption(options[1]);
+
+ // We should be able to go right again
+ await triggerKeyEvent(document.activeElement, 'keyup', 'ArrowRight');
+ assertActiveListboxOption(options[2]);
+
+ // We should NOT be able to go right again (because last option). Current implementation won't go around.
+ await triggerKeyEvent(document.activeElement, 'keyup', 'ArrowRight');
+ assertActiveListboxOption(options[2]);
+ });
+ });
+
+ module('Listbox `ArrowUp` key', () => {
+ test('should be possible to open the listbox with ArrowUp and the last option should be active', async (assert) => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Focus the button
+ getListboxButton()?.focus();
+
+ // Open listbox
+ await triggerKeyEvent(document.activeElement, 'keyup', 'ArrowUp');
+
+ // Verify it is visible
+ assertListboxButton({ state: ListboxState.Visible });
+ assertListbox({
+ state: ListboxState.Visible,
+ attributes: { 'data-test': 'headlessui-listbox-options-1' },
+ });
+ await assertActiveElement(getListbox());
+ assertListboxButtonLinkedWithListbox();
+
+ // Verify we have listbox options
+ let options = getListboxOptions();
+ assert.equal(options.length, 3);
+ options.forEach((option) => assertListboxOption({}, option));
+
+ // ! ALERT: The LAST option should now be active
+ assertActiveListboxOption(options[2]);
+ });
+
+ test('should not be possible to open the listbox with ArrowUp and the last option should be active when the button is disabled', async () => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Focus the button
+ getListboxButton()?.focus();
+
+ // Try to open the listbox
+ await triggerKeyEvent(document.activeElement, 'keyup', 'ArrowUp');
+
+ // Verify it is still closed
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+ });
+
+ test('should be possible to open the listbox with ArrowUp, and focus the selected option', async (assert) => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Focus the button
+ getListboxButton()?.focus();
+
+ // Open listbox
+ await triggerKeyEvent(document.activeElement, 'keyup', 'ArrowUp');
+
+ // Verify it is visible
+ assertListboxButton({ state: ListboxState.Visible });
+ assertListbox({
+ state: ListboxState.Visible,
+ attributes: { 'data-test': 'headlessui-listbox-options-1' },
+ });
+ await assertActiveElement(getListbox());
+ assertListboxButtonLinkedWithListbox();
+
+ // Verify we have listbox options
+ let options = getListboxOptions();
+ assert.equal(options.length, 3);
+ options.forEach((option, i) =>
+ assertListboxOption({ selected: i === 1 }, option)
+ );
+
+ // Verify that the second listbox option is active (because it is already selected)
+ assertActiveListboxOption(options[1]);
+ });
+
+ test('should have no active listbox option when there are no listbox options at all', async () => {
+ await render(hbs`
+
+ Trigger
+
+
+ `);
+
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Focus the button
+ getListboxButton()?.focus();
+
+ // Open listbox
+ await triggerKeyEvent(document.activeElement, 'keyup', 'ArrowUp');
+ assertListbox({ state: ListboxState.Visible });
+ await assertActiveElement(getListbox());
+
+ assertNoActiveListboxOption();
+ });
+
+ test('should be possible to use ArrowUp to navigate the listbox options and jump to the first non-disabled one', async (assert) => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Focus the button
+ getListboxButton()?.focus();
+
+ // Open listbox
+ await triggerKeyEvent(document.activeElement, 'keyup', 'ArrowUp');
+
+ // Verify we have listbox options
+ let options = getListboxOptions();
+ assert.equal(options.length, 3);
+ options.forEach((option) => assertListboxOption({}, option));
+ assertActiveListboxOption(options[0]);
+ });
+
+ test('should not be possible to navigate up or down if there is only a single non-disabled option', async (assert) => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Focus the button
+ getListboxButton()?.focus();
+
+ // Open listbox
+ await triggerKeyEvent(document.activeElement, 'keypress', 'Enter');
+
+ // Verify we have listbox options
+ let options = getListboxOptions();
+ assert.equal(options.length, 3);
+ options.forEach((option) => assertListboxOption({}, option));
+ assertActiveListboxOption(options[2]);
+
+ // We should not be able to go up (because those are disabled)
+ await triggerKeyEvent(document.activeElement, 'keyup', 'ArrowUp');
+ assertActiveListboxOption(options[2]);
+
+ // We should not be able to go down (because this is the last option)
+ await triggerKeyEvent(document.activeElement, 'keyup', 'ArrowDown');
+ assertActiveListboxOption(options[2]);
+ });
+
+ test('should be possible to use ArrowUp to navigate the listbox options', async (assert) => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Focus the button
+ getListboxButton()?.focus();
+
+ // Open listbox
+ await triggerKeyEvent(document.activeElement, 'keyup', 'ArrowUp');
+
+ // Verify it is visible
+ assertListboxButton({ state: ListboxState.Visible });
+ assertListbox({
+ state: ListboxState.Visible,
+ attributes: { 'data-test': 'headlessui-listbox-options-1' },
+ });
+ await assertActiveElement(getListbox());
+ assertListboxButtonLinkedWithListbox();
+
+ // Verify we have listbox options
+ let options = getListboxOptions();
+ assert.equal(options.length, 3);
+ options.forEach((option) => assertListboxOption({}, option));
+ assertActiveListboxOption(options[2]);
+
+ // We should be able to go down once
+ await triggerKeyEvent(document.activeElement, 'keyup', 'ArrowUp');
+ assertActiveListboxOption(options[1]);
+
+ // We should be able to go down again
+ await triggerKeyEvent(document.activeElement, 'keyup', 'ArrowUp');
+ assertActiveListboxOption(options[0]);
+
+ // We should NOT be able to go up again (because first option). Current implementation won't go around.
+ await triggerKeyEvent(document.activeElement, 'keyup', 'ArrowUp');
+ assertActiveListboxOption(options[0]);
+ });
+ });
+
+ module('Listbox `ArrowLeft` key', () => {
+ test('should be possible to use ArrowLeft to navigate the listbox options', async (assert) => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Focus the button
+ getListboxButton()?.focus();
+
+ // Open listbox
+ await triggerKeyEvent(document.activeElement, 'keyup', 'ArrowUp');
+
+ // Verify it is visible
+ assertListboxButton({ state: ListboxState.Visible });
+ assertListbox({
+ state: ListboxState.Visible,
+ attributes: { 'data-test': 'headlessui-listbox-options-1' },
+ orientation: 'horizontal',
+ });
+ await assertActiveElement(getListbox());
+ assertListboxButtonLinkedWithListbox();
+
+ // Verify we have listbox options
+ let options = getListboxOptions();
+ assert.equal(options.length, 3);
+ options.forEach((option) => assertListboxOption({}, option));
+ assertActiveListboxOption(options[2]);
+
+ // We should be able to go left once
+ await triggerKeyEvent(document.activeElement, 'keyup', 'ArrowLeft');
+ assertActiveListboxOption(options[1]);
+
+ // We should be able to go left again
+ await triggerKeyEvent(document.activeElement, 'keyup', 'ArrowLeft');
+ assertActiveListboxOption(options[0]);
+
+ // We should NOT be able to go left again (because first option). Current implementation won't go around.
+ await triggerKeyEvent(document.activeElement, 'keyup', 'ArrowLeft');
+ assertActiveListboxOption(options[0]);
+ });
+ });
+
+ module('Listbox `End` key', () => {
+ test('should be possible to use the End key to go to the last listbox option', async () => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+
+ `);
+
+ // Focus the button
+ getListboxButton()?.focus();
+
+ // Open listbox
+ await triggerKeyEvent(document.activeElement, 'keypress', 'Enter');
+
+ let options = getListboxOptions();
+
+ // We should be on the first option
+ assertActiveListboxOption(options[0]);
+
+ // We should be able to go to the last option
+ await triggerKeyEvent(document.activeElement, 'keyup', 'End');
+ assertActiveListboxOption(options[2]);
+ });
+
+ test('should be possible to use the End key to go to the last non disabled listbox option', async () => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+ Option D
+
+
+
+ `);
+
+ // Focus the button
+ getListboxButton()?.focus();
+
+ // Open listbox
+ await triggerKeyEvent(document.activeElement, 'keypress', 'Enter');
+
+ let options = getListboxOptions();
+
+ // We should be on the first option
+ assertActiveListboxOption(options[0]);
+
+ // We should be able to go to the last non-disabled option
+ await triggerKeyEvent(document.activeElement, 'keyup', 'End');
+ assertActiveListboxOption(options[1]);
+ });
+
+ test('should be possible to use the End key to go to the first listbox option if that is the only non-disabled listbox option', async () => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+ Option D
+
+
+
+ `);
+
+ // Open listbox
+ await click(getListboxButton());
+
+ // We opened via click, we don't have an active option
+ assertNoActiveListboxOption();
+
+ // We should not be able to go to the end
+ await triggerKeyEvent(document.activeElement, 'keyup', 'End');
+
+ let options = getListboxOptions();
+ assertActiveListboxOption(options[0]);
+ });
+
+ test('should have no active listbox option upon End key press, when there are no non-disabled listbox options', async () => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+ Option D
+
+
+
+ `);
+
+ // Open listbox
+ await click(getListboxButton());
+
+ // We opened via click, we don't have an active option
+ assertNoActiveListboxOption();
+
+ // We should not be able to go to the end
+ await triggerKeyEvent(document.activeElement, 'keyup', 'End');
+
+ assertNoActiveListboxOption();
+ });
+ });
+
+ module('Listbox `PageDown` key', () => {
+ test('should be possible to use the PageDown key to go to the last listbox option', async () => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+
+ `);
+
+ // Focus the button
+ getListboxButton()?.focus();
+
+ // Open listbox
+ await triggerKeyEvent(document.activeElement, 'keypress', 'Enter');
+
+ let options = getListboxOptions();
+
+ // We should be on the first option
+ assertActiveListboxOption(options[0]);
+
+ // We should be able to go to the last option
+ await triggerKeyEvent(document.activeElement, 'keyup', 'PageDown');
+ assertActiveListboxOption(options[2]);
+ });
+
+ test('should be possible to use the PageDown key to go to the last non disabled listbox option', async () => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+ Option D
+
+
+
+ `);
+
+ // Focus the button
+ getListboxButton()?.focus();
+
+ // Open listbox
+ await triggerKeyEvent(document.activeElement, 'keypress', 'Enter');
+
+ let options = getListboxOptions();
+
+ // We should be on the first option
+ assertActiveListboxOption(options[0]);
+
+ // We should be able to go to the last non-disabled option
+ await triggerKeyEvent(document.activeElement, 'keyup', 'PageDown');
+ assertActiveListboxOption(options[1]);
+ });
+
+ test('should be possible to use the PageDown key to go to the first listbox option if that is the only non-disabled listbox option', async () => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+ Option D
+
+
+
+ `);
+
+ // Open listbox
+ await click(getListboxButton());
+
+ // We opened via click, we don't have an active option
+ assertNoActiveListboxOption();
+
+ // We should not be able to go to the end
+ await triggerKeyEvent(document.activeElement, 'keyup', 'PageDown');
+
+ let options = getListboxOptions();
+ assertActiveListboxOption(options[0]);
+ });
+
+ test('should have no active listbox option upon PageDown key press, when there are no non-disabled listbox options', async () => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+ Option D
+
+
+
+ `);
+
+ // Open listbox
+ await click(getListboxButton());
+
+ // We opened via click, we don't have an active option
+ assertNoActiveListboxOption();
+
+ // We should not be able to go to the end
+ await triggerKeyEvent(document.activeElement, 'keyup', 'PageDown');
+
+ assertNoActiveListboxOption();
+ });
+ });
+
+ module('Listbox `Home` key', () => {
+ test('should be possible to use the Home key to go to the first listbox option', async () => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+
+ `);
+
+ // Focus the button
+ getListboxButton()?.focus();
+
+ // Open listbox
+ await triggerKeyEvent(document.activeElement, 'keyup', 'ArrowUp');
+
+ let options = getListboxOptions();
+
+ // We should be on the last option
+ assertActiveListboxOption(options[2]);
+
+ // We should be able to go to the first option
+ await triggerKeyEvent(document.activeElement, 'keyup', 'Home');
+ assertActiveListboxOption(options[0]);
+ });
+
+ test('should be possible to use the Home key to go to the first non disabled listbox option', async () => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+ Option D
+
+
+
+ `);
+
+ // Open listbox
+ await click(getListboxButton());
+
+ // We opened via click, we don't have an active option
+ assertNoActiveListboxOption();
+
+ // We should not be able to go to the end
+ await triggerKeyEvent(document.activeElement, 'keyup', 'Home');
+
+ let options = getListboxOptions();
+
+ // We should be on the first non-disabled option
+ assertActiveListboxOption(options[2]);
+ });
+
+ test('should be possible to use the Home key to go to the last listbox option if that is the only non-disabled listbox option', async () => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+ Option D
+
+
+
+ `);
+
+ // Open listbox
+ await click(getListboxButton());
+
+ // We opened via click, we don't have an active option
+ assertNoActiveListboxOption();
+
+ // We should not be able to go to the end
+ await triggerKeyEvent(document.activeElement, 'keyup', 'Home');
+
+ let options = getListboxOptions();
+ assertActiveListboxOption(options[3]);
+ });
+
+ test('should have no active listbox option upon Home key press, when there are no non-disabled listbox options', async () => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+ Option D
+
+
+
+ `);
+
+ // Open listbox
+ await click(getListboxButton());
+
+ // We opened via click, we don't have an active option
+ assertNoActiveListboxOption();
+
+ // We should not be able to go to the end
+ await triggerKeyEvent(document.activeElement, 'keyup', 'Home');
+
+ assertNoActiveListboxOption();
+ });
+ });
+
+ module('Listbox `PageUp` key', () => {
+ test('should be possible to use the PageUp key to go to the first listbox option', async () => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+
+ `);
+
+ // Focus the button
+ getListboxButton()?.focus();
+
+ // Open listbox
+ await triggerKeyEvent(document.activeElement, 'keyup', 'ArrowUp');
+
+ let options = getListboxOptions();
+
+ // We should be on the last option
+ assertActiveListboxOption(options[2]);
+
+ // We should be able to go to the first option
+ await triggerKeyEvent(document.activeElement, 'keyup', 'PageUp');
+ assertActiveListboxOption(options[0]);
+ });
+
+ test('should be possible to use the PageUp key to go to the first non disabled listbox option', async () => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+ Option D
+
+
+
+ `);
+
+ // Open listbox
+ await click(getListboxButton());
+
+ // We opened via click, we don't have an active option
+ assertNoActiveListboxOption();
+
+ // We should not be able to go to the end
+ await triggerKeyEvent(document.activeElement, 'keyup', 'PageUp');
+
+ let options = getListboxOptions();
+
+ // We should be on the first non-disabled option
+ assertActiveListboxOption(options[2]);
+ });
+
+ test('should be possible to use the PageUp key to go to the last listbox option if that is the only non-disabled listbox option', async () => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+ Option D
+
+
+
+ `);
+
+ // Open listbox
+ await click(getListboxButton());
+
+ // We opened via click, we don't have an active option
+ assertNoActiveListboxOption();
+
+ // We should not be able to go to the end
+ await triggerKeyEvent(document.activeElement, 'keyup', 'PageUp');
+
+ let options = getListboxOptions();
+ assertActiveListboxOption(options[3]);
+ });
+
+ test('should have no active listbox option upon PageUp key press, when there are no non-disabled listbox options', async () => {
+ await render(hbs`
+
+ Trigger
+
+
+ Option A
+
+
+ Option B
+
+
+ Option C
+
+
+ Option D
+
+
+
+ `);
+
+ // Open listbox
+ await click(getListboxButton());
+
+ // We opened via click, we don't have an active option
+ assertNoActiveListboxOption();
+
+ // We should not be able to go to the end
+ await triggerKeyEvent(document.activeElement, 'keyup', 'PageUp');
+
+ assertNoActiveListboxOption();
+ });
+ });
+
+ module('Listbox `Any` key aka search', () => {
+ test('should be possible to type a full word that has a perfect match', async () => {
+ await render(hbs`
+
+ Trigger
+
+ alice
+ bob
+ charlie
+
+
+ `);
+
+ // Open listbox
+ await click(getListboxButton());
+
+ let options = getListboxOptions();
+
+ // We should be able to go to the second option
+ await typeWord('bob');
+ assertActiveListboxOption(options[1]);
+
+ // We should be able to go to the first option
+ await typeWord('alice');
+ assertActiveListboxOption(options[0]);
+
+ // We should be able to go to the last option
+ await typeWord('charlie');
+ assertActiveListboxOption(options[2]);
+ });
+
+ test('should be possible to type a partial of a word', async () => {
+ await render(hbs`
+
+ Trigger
+
+ alice
+ bob
+ charlie
+
+
+ `);
+
+ // Focus the button
+ getListboxButton()?.focus();
+
+ // Open listbox
+ await triggerKeyEvent(document.activeElement, 'keyup', 'ArrowUp');
+
+ let options = getListboxOptions();
+
+ // We should be on the last option
+ assertActiveListboxOption(options[2]);
+
+ // We should be able to go to the second option
+ await typeWord('bo');
+ assertActiveListboxOption(options[1]);
+
+ // We should be able to go to the first option
+ await typeWord('ali');
+ assertActiveListboxOption(options[0]);
+
+ // We should be able to go to the last option
+ await typeWord('char');
+ assertActiveListboxOption(options[2]);
+ });
+
+ test('should be possible to type words with spaces', async () => {
+ await render(hbs`
+
+ Trigger
+
+ value a
+ value b
+ value c
+
+
+ `);
+
+ // Focus the button
+ getListboxButton()?.focus();
+
+ // Open listbox
+ await triggerKeyEvent(document.activeElement, 'keyup', 'ArrowUp');
+
+ let options = getListboxOptions();
+
+ // We should be on the last option
+ assertActiveListboxOption(options[2]);
+
+ // We should be able to go to the second option
+ await typeWord('value b');
+ assertActiveListboxOption(options[1]);
+
+ // We should be able to go to the first option
+ await typeWord('value a');
+ assertActiveListboxOption(options[0]);
+
+ // We should be able to go to the last option
+ await typeWord('value c');
+ assertActiveListboxOption(options[2]);
+ });
+
+ test('should not be possible to search for a disabled option', async () => {
+ await render(hbs`
+
+ Trigger
+
+ alice
+ bob
+ charlie
+
+
+ `);
+
+ // Focus the button
+ getListboxButton()?.focus();
+
+ // Open listbox
+ await triggerKeyEvent(document.activeElement, 'keyup', 'ArrowUp');
+
+ let options = getListboxOptions();
+
+ // We should be on the last option
+ assertActiveListboxOption(options[2]);
+
+ // We should not be able to go to the disabled option
+ await typeWord('bo');
+
+ // We should still be on the last option
+ assertActiveListboxOption(options[2]);
+ });
+
+ test('should be possible to search for a word (case insensitive)', async () => {
+ await render(hbs`
+
+ Trigger
+
+ alice
+ bob
+ charlie
+
+
+ `);
+
+ // Focus the button
+ getListboxButton()?.focus();
+
+ // Open listbox
+ await triggerKeyEvent(document.activeElement, 'keyup', 'ArrowUp');
+
+ let options = getListboxOptions();
+
+ // We should be on the last option
+ assertActiveListboxOption(options[2]);
+
+ // Search for bob in a different casing
+ await typeWord('BO');
+
+ // We should be on `bob`
+ assertActiveListboxOption(options[1]);
+ });
+ });
+
+ module('listbox mouse interactions', () => {
+ test('should focus the Listbox.Button when we click the Listbox.Label', async () => {
+ await render(hbs`
+
+ Label
+ Trigger
+
+ alice
+ bob
+ charlie
+
+
+ `);
+
+ // Ensure the button is not focused yet
+ await assertActiveElement(document.body);
+
+ // Focus the label
+ await click(getListboxLabel());
+
+ // Ensure that the actual button is focused instead
+ await assertActiveElement(getListboxButton());
+ });
+
+ test('should not focus the Listbox.Button when we right click the Listbox.Label', async () => {
+ await render(hbs`
+
+ Label
+ Trigger
+
+ alice
+ bob
+ charlie
+
+
+ `);
+
+ // Ensure the button is not focused yet
+ await assertActiveElement(document.body);
+
+ // Focus the label
+ await click(getListboxLabel(), { button: 2 });
+
+ // Ensure that the body is still active
+ await assertActiveElement(document.body);
+ });
+
+ test('should be possible to open the listbox on click', async (assert) => {
+ await render(hbs`
+
+ Trigger
+
+ alice
+ bob
+ charlie
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Open listbox
+ await click(getListboxButton());
+
+ // Verify it is visible
+ assertListboxButton({ state: ListboxState.Visible });
+ assertListbox({
+ state: ListboxState.Visible,
+ attributes: { 'data-test': 'headlessui-listbox-options-1' },
+ });
+ await assertActiveElement(getListbox());
+ assertListboxButtonLinkedWithListbox();
+
+ // Verify we have listbox options
+ let options = getListboxOptions();
+ assert.equal(options.length, 3);
+ options.forEach((option) => assertListboxOption({}, option));
+ });
+
+ test('should not be possible to open the listbox on right click', async () => {
+ await render(hbs`
+
+ Trigger
+
+ alice
+ bob
+ charlie
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Try to open the listbox
+ await click(getListboxButton(), { button: 2 });
+
+ // Verify it is still closed
+ assertListboxButton({ state: ListboxState.InvisibleUnmounted });
+ });
+
+ test('should not be possible to open the listbox on click when the button is disabled', async () => {
+ await render(hbs`
+
+ Trigger
+
+ alice
+ bob
+ charlie
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Try to open the listbox
+ try {
+ await click(getListboxButton());
+ } catch (e) {
+ //
+ }
+
+ // Verify it is still closed
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+ });
+
+ test('should be possible to open the listbox on click, and focus the selected option', async (assert) => {
+ await render(hbs`
+
+ Trigger
+
+ a
+ b
+ c
+
+
+ `);
+
+ assertListboxButton({
+ state: ListboxState.InvisibleUnmounted,
+ attributes: { 'data-test': 'headlessui-listbox-button-1' },
+ });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Open listbox
+ await click(getListboxButton());
+
+ // Verify it is visible
+ assertListboxButton({ state: ListboxState.Visible });
+ assertListbox({
+ state: ListboxState.Visible,
+ attributes: { 'data-test': 'headlessui-listbox-options-1' },
+ });
+ await assertActiveElement(getListbox());
+ assertListboxButtonLinkedWithListbox();
+
+ // Verify we have listbox options
+ let options = getListboxOptions();
+ assert.equal(options.length, 3);
+ options.forEach((option, i) =>
+ assertListboxOption({ selected: i === 1 }, option)
+ );
+
+ // Verify that the second listbox option is active (because it is already selected)
+ assertActiveListboxOption(options[1]);
+ });
+
+ test('should be possible to close a listbox on click', async () => {
+ await render(hbs`
+
+ Trigger
+
+ a
+ b
+ c
+
+
+ `);
+
+ // Open listbox
+ await click(getListboxButton());
+
+ // Verify it is visible
+ assertListboxButton({ state: ListboxState.Visible });
+
+ // Click to close
+ await click(getListboxButton());
+
+ // Verify it is closed
+ assertListboxButton({ state: ListboxState.InvisibleUnmounted });
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+ });
+
+ test('should be a no-op when we click outside of a closed listbox', async () => {
+ await render(hbs`
+
+ Trigger
+
+ a
+ b
+ c
+
+
+ `);
+
+ // Verify that the window is closed
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Click something that is not related to the listbox
+ await click(document.body);
+
+ // Should still be closed
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+ });
+
+ test('should be possible to click outside of the listbox which should close the listbox', async () => {
+ await render(hbs`
+
+ Trigger
+
+ a
+ b
+ c
+
+
+ `);
+
+ // Open listbox
+ await click(getListboxButton());
+ assertListbox({ state: ListboxState.Visible });
+ await assertActiveElement(getListbox());
+
+ // Click something that is not related to the listbox
+ await click(document.body);
+
+ // Should be closed now
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Verify the button is focused again
+ await assertActiveElement(getListboxButton());
+ });
+
+ test('should be possible to click outside of the listbox on another listbox button which should close the current listbox and open the new listbox', async (assert) => {
+ await render(hbs`
+
+
+ Trigger
+
+ a
+ b
+ c
+
+
+
+ Trigger
+
+ a
+ b
+ c
+
+
+
+ `);
+
+ let [button1, button2] = getListboxButtons();
+
+ // Click the first listbox button
+ await click(button1);
+ assert.equal(getListboxes().length, 1); // Only 1 listbox should be visible
+
+ // Ensure the open listbox is linked to the first button
+ assertListboxButtonLinkedWithListbox(button1, getListbox());
+
+ // Click the second listbox button
+ await click(button2);
+
+ assert.equal(getListboxes().length, 1); // Only 1 listbox should be visible
+
+ // Ensure the open listbox is linked to the second button
+ assertListboxButtonLinkedWithListbox(button2, getListbox());
+ });
+
+ test('should be possible to click outside of the listbox which should close the listbox (even if we press the listbox button)', async () => {
+ await render(hbs`
+
+ Trigger
+
+ a
+ b
+ c
+
+
+ `);
+
+ // Open listbox
+ await click(getListboxButton());
+ assertListbox({ state: ListboxState.Visible });
+ await assertActiveElement(getListbox());
+
+ // Click the listbox button again
+ await click(getListboxButton());
+
+ // Should be closed now
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Verify the button is focused again
+ await assertActiveElement(getListboxButton());
+ });
+
+ skip('should be possible to click outside of the listbox, on an element which is within a focusable element, which closes the listbox', async function (assert) {
+ let callCount = 0;
+
+ this.set('handleFocus', () => {
+ callCount++;
+ });
+
+ await render(hbs`
+
+
+ Trigger
+
+ a
+ b
+ c
+
+
+
+
+ `);
+
+ // Click the listbox button
+ await click(getListboxButton());
+
+ // Ensure the listbox is open
+ assertListbox({ state: ListboxState.Visible });
+
+ // Click the span inside the button
+ await click(document.querySelector('#btn span'));
+
+ // Ensure the listbox is closed
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+
+ // Ensure the outside button is focused
+ await assertActiveElement(document.getElementById('btn'));
+
+ // Ensure that the focus button only got focus once (first click)
+ assert.equal(callCount, 1, 'handleFocus called once exactly');
+ });
+
+ test('should be possible to hover an option and make it active', async () => {
+ await render(hbs`
+
+
+ Trigger
+
+ a
+ b
+ c
+
+
+
+
+ `);
+
+ // Open listbox
+ await click(getListboxButton());
+
+ let options = getListboxOptions();
+ // We should be able to go to the second option
+ await triggerEvent(options[1], 'mouseover');
+ assertActiveListboxOption(options[1]);
+
+ // We should be able to go to the first option
+ await triggerEvent(options[0], 'mouseover');
+ assertActiveListboxOption(options[0]);
+
+ // We should be able to go to the last option
+ await triggerEvent(options[2], 'mouseover');
+ assertActiveListboxOption(options[2]);
+ });
+
+ test('should make a listbox option active when you move the mouse over it', async () => {
+ await render(hbs`
+
+
+ Trigger
+
+ a
+ b
+ c
+
+
+
+
+ `);
+
+ // Open listbox
+ await click(getListboxButton());
+
+ let options = getListboxOptions();
+ // We should be able to go to the second option
+ await triggerEvent(options[1], 'mouseover');
+ assertActiveListboxOption(options[1]);
+ });
+
+ test('should be a no-op when we move the mouse and the listbox option is already active', async () => {
+ await render(hbs`
+
+
+ Trigger
+
+ a
+ b
+ c
+
+
+
+
+ `);
+
+ // Open listbox
+ await click(getListboxButton());
+
+ let options = getListboxOptions();
+
+ // We should be able to go to the second option
+ await triggerEvent(options[1], 'mouseover');
+ assertActiveListboxOption(options[1]);
+
+ await triggerEvent(options[1], 'mouseover');
+
+ // Nothing should be changed
+ assertActiveListboxOption(options[1]);
+ });
+
+ test('should be a no-op when we move the mouse and the listbox option is disabled', async () => {
+ await render(hbs`
+
+
+ Trigger
+
+ a
+ b
+ c
+
+
+
+
+ `);
+
+ // Open listbox
+ await click(getListboxButton());
+
+ let options = getListboxOptions();
+
+ await triggerEvent(options[1], 'mouseover');
+ assertNoActiveListboxOption();
+ });
+
+ test('should not be possible to hover an option that is disabled', async () => {
+ await render(hbs`
+
+
+ Trigger
+
+ a
+ b
+ c
+
+
+
+
+ `);
+
+ // Open listbox
+ await click(getListboxButton());
+
+ let options = getListboxOptions();
+
+ // Try to hover over option 1, which is disabled
+ await triggerEvent(options[1], 'mouseover');
+
+ // We should not have an active option now
+ assertNoActiveListboxOption();
+ });
+
+ test('should be possible to mouse leave an option and make it inactive', async () => {
+ await render(hbs`
+
+
+ Trigger
+
+ a
+ b
+ c
+
+
+
+
+ `);
+
+ // Open listbox
+ await click(getListboxButton());
+
+ let options = getListboxOptions();
+
+ // We should be able to go to the second option
+ await triggerEvent(options[1], 'mouseover');
+ assertActiveListboxOption(options[1]);
+
+ await triggerEvent(options[1], 'mouseout');
+ assertNoActiveListboxOption();
+
+ // We should be able to go to the first option
+ await triggerEvent(options[0], 'mouseover');
+ assertActiveListboxOption(options[0]);
+
+ await triggerEvent(options[0], 'mouseout');
+ assertNoActiveListboxOption();
+
+ // We should be able to go to the last option
+ await triggerEvent(options[2], 'mouseover');
+ assertActiveListboxOption(options[2]);
+
+ await triggerEvent(options[2], 'mouseout');
+ assertNoActiveListboxOption();
+ });
+
+ test('should be possible to mouse leave a disabled option and be a no-op', async () => {
+ await render(hbs`
+
+
+ Trigger
+
+ a
+ b
+ c
+
+
+
+
+ `);
+
+ // Open listbox
+ await click(getListboxButton());
+
+ let options = getListboxOptions();
+
+ // Try to hover over option 1, which is disabled
+ await triggerEvent(options[1], 'mouseover');
+ assertNoActiveListboxOption();
+
+ await triggerEvent(options[1], 'mouseout');
+ assertNoActiveListboxOption();
+ });
+
+ test('should be possible to click a listbox option, which closes the listbox', async function (assert) {
+ let callValue = '',
+ callCount = 0;
+
+ this.set('onChange', (value) => {
+ this.set('selectedOption', value);
+ callValue = value;
+ callCount++;
+ });
+
+ await render(hbs`
+
+ Trigger
+
+ alice
+ bob
+ charlie
+
+
+ `);
+
+ // Open listbox
+ await click(getListboxButton());
+ assertListbox({ state: ListboxState.Visible });
+ await assertActiveElement(getListbox());
+
+ let options = getListboxOptions();
+
+ // We should be able to click the first option
+ await click(options[1]);
+ assertListbox({ state: ListboxState.InvisibleUnmounted });
+ assert.equal(callCount, 1, 'handleChange called once exactly');
+ assert.equal(callValue, 'bob', 'handleChange called with "bob"');
+
+ // Verify the button is focused again
+ await assertActiveElement(getListboxButton());
+
+ // Open listbox again
+ await click(getListboxButton());
+
+ // Verify the active option is the previously selected one
+ assertActiveListboxOption(getListboxOptions()[1]);
+ });
+
+ test('should be possible to click a disabled listbox option, which is a no-op', async function (assert) {
+ let callCount = 0;
+
+ this.set('onChange', (value) => {
+ this.set('selectedOption', value);
+ callCount++;
+ });
+
+ await render(hbs`
+
+ Trigger
+
+ alice
+ bob
+ charlie
+
+
+ `);
+
+ // Open listbox
+ await click(getListboxButton());
+ assertListbox({ state: ListboxState.Visible });
+ await assertActiveElement(getListbox());
+
+ let options = getListboxOptions();
+
+ // We should be able to click the first option
+ await click(options[1]);
+ assertListbox({ state: ListboxState.Visible });
+ await assertActiveElement(getListbox());
+ assert.equal(callCount, 0, 'handleChange not called');
+
+ // Close the listbox
+ await click(getListboxButton());
+
+ // Open listbox again
+ await click(getListboxButton());
+
+ // Verify the active option is non existing
+ assertNoActiveListboxOption();
+ });
+
+ test('should be possible focus a listbox option, so that it becomes active', async () => {
+ await render(hbs`
+
+ Trigger
+
+ alice
+ bob
+ charlie
+
+
+ `);
+
+ // Open listbox
+ await click(getListboxButton());
+ assertListbox({ state: ListboxState.Visible });
+ await assertActiveElement(getListbox());
+
+ let options = getListboxOptions();
+
+ // Verify that nothing is active yet
+ assertNoActiveListboxOption();
+
+ // We should be able to focus the first option
+ await focus(options[1]);
+ assertActiveListboxOption(options[1]);
+ });
+
+ test('should not be possible to focus a listbox option which is disabled', async () => {
+ await render(hbs`
+
+ Trigger
+
+ alice
+ bob
+ charlie
+
+
+ `);
+
+ // Open listbox
+ await click(getListboxButton());
+ assertListbox({ state: ListboxState.Visible });
+ await assertActiveElement(getListbox());
+
+ let options = getListboxOptions();
+
+ // We should not be able to focus the first option
+ await focus(options[1]);
+ assertNoActiveListboxOption();
+ });
+ });
+});