- {{! template-lint-disable no-positive-tabindex no-pointer-down-event-binding }}
+ {{! template-lint-disable no-positive-tabindex }}
{{#let (uniqueId) as |id|}}
diff --git a/addon/components/select-box/trigger.gjs b/addon/components/select-box/trigger.gjs
deleted file mode 100644
index dd70d226..00000000
--- a/addon/components/select-box/trigger.gjs
+++ /dev/null
@@ -1,22 +0,0 @@
-import { on } from '@ember/modifier';
-import lifecycle from '@zestia/ember-select-box/modifiers/lifecycle';
-
-
- {{! template-lint-disable no-positive-tabindex require-aria-activedescendant-tabindex no-pointer-down-event-binding }}
-
- {{~yield~}}
-
-
diff --git a/addon/modifiers/lifecycle.js b/addon/modifiers/lifecycle.js
index 1e15e9f9..4dac9fa4 100644
--- a/addon/modifiers/lifecycle.js
+++ b/addon/modifiers/lifecycle.js
@@ -4,17 +4,17 @@ import { registerDestructor } from '@ember/destroyable';
export default class LifecycleModifier extends Modifier {
didSetup;
- constructor(appInstance, args) {
+ constructor(appInstance, { named: { onDestroy } }) {
super(...arguments);
- registerDestructor(this, args.named.onDestroy);
+ onDestroy && registerDestructor(this, onDestroy);
}
- modify(element, positional, named) {
+ modify(element, positional, { onInsert }) {
if (this.didSetup) {
return;
}
- named.onInsert(element);
+ onInsert?.(element);
this.didSetup = true;
}
}
diff --git a/app/components/dropdown.js b/app/components/dropdown.js
new file mode 100644
index 00000000..d64ab88e
--- /dev/null
+++ b/app/components/dropdown.js
@@ -0,0 +1 @@
+export { default } from '@zestia/ember-select-box/components/dropdown/index';
diff --git a/tests/dummy/app/components/example3.gjs b/tests/dummy/app/components/example3.gjs
index 549e664c..ddab0720 100644
--- a/tests/dummy/app/components/example3.gjs
+++ b/tests/dummy/app/components/example3.gjs
@@ -8,15 +8,19 @@ import SelectBox from '@zestia/ember-select-box/components/select-box';
...attributes
as |sb|
>
-
- {{yield sb.value to="trigger"}}
-
-
- {{#each @options as |value|}}
-
- {{yield value to="option"}}
-
- {{/each}}
-
+
+
+ {{yield sb.value to="trigger"}}
+
+
+
+ {{#each @options as |value|}}
+
+ {{yield value to="option"}}
+
+ {{/each}}
+
+
+
diff --git a/tests/dummy/app/components/example4.gjs b/tests/dummy/app/components/example4.gjs
index 21ef9c00..678b24ed 100644
--- a/tests/dummy/app/components/example4.gjs
+++ b/tests/dummy/app/components/example4.gjs
@@ -18,24 +18,28 @@ export default class extends Component {
...attributes
as |sb|
>
-
- {{yield sb.value to="trigger"}}
-
-
- {{#each @options as |value|}}
-
-
- {{yield value to="option"}}
-
- {{/each}}
-
+
+
+ {{yield sb.value to="trigger"}}
+
+
+
+ {{#each @options as |value|}}
+
+
+ {{yield value to="option"}}
+
+ {{/each}}
+
+
+
}
diff --git a/tests/dummy/app/components/example6.gjs b/tests/dummy/app/components/example6.gjs
index edfddce0..236ece66 100644
--- a/tests/dummy/app/components/example6.gjs
+++ b/tests/dummy/app/components/example6.gjs
@@ -1,29 +1,22 @@
import SelectBox from '@zestia/ember-select-box/components/select-box';
-import Component from '@glimmer/component';
-import { action } from '@ember/object';
+import { fn } from '@ember/helper';
-export default class extends Component {
- @action
- handleOpen(sb) {
- sb.search('');
- }
-
-
-
+
+
+
{{yield sb.value to="trigger"}}
- {{#if sb.isOpen}}
-
+ {{#if dd.isOpen}}
+
{{#each sb.options as |value|}}
@@ -36,8 +29,8 @@ export default class extends Component {
{{/each}}
-
+
{{/if}}
-
-
-}
+
+
+
diff --git a/tests/dummy/app/components/example7.gjs b/tests/dummy/app/components/example7.gjs
index 0f941531..fac00eab 100644
--- a/tests/dummy/app/components/example7.gjs
+++ b/tests/dummy/app/components/example7.gjs
@@ -3,15 +3,11 @@ import { on } from '@ember/modifier';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
+import { fn } from '@ember/helper';
export default class extends Component {
@tracked inputValue = '';
- @action
- handleOpen(sb) {
- sb.search(sb.query);
- }
-
@action
handleSelect(sb) {
this.inputValue = '';
@@ -24,42 +20,45 @@ export default class extends Component {
@value={{@value}}
@onChange={{@onChange}}
@onSelect={{this.handleSelect}}
- @onOpen={{this.handleOpen}}
@onSearch={{@onSearch}}
...attributes
as |sb|
>
-
-
+
+
+
-
- {{if sb.isOpen "↑" "↓"}}
-
-
+
+ {{if dd.isOpen "↑" "↓"}}
+
+
- {{#if sb.isOpen}}
-
- {{#if sb.isBusy}}
-
- {{yield to="busy"}}
-
- {{else if sb.options}}
- {{#each sb.options as |value|}}
-
- {{yield value to="option"}}
-
- {{/each}}
- {{else}}
-
- {{yield sb.query to="noOptions"}}
-
- {{/if}}
-
- {{/if}}
+ {{#if dd.isOpen}}
+
+
+ {{#if sb.isBusy}}
+
+ {{yield to="busy"}}
+
+ {{else if sb.options}}
+ {{#each sb.options as |value|}}
+
+ {{yield value to="option"}}
+
+ {{/each}}
+ {{else}}
+
+ {{yield sb.query to="noOptions"}}
+
+ {{/if}}
+
+
+ {{/if}}
+
}
diff --git a/tests/dummy/app/router.js b/tests/dummy/app/router.js
index d4735ab9..fc8eab4a 100644
--- a/tests/dummy/app/router.js
+++ b/tests/dummy/app/router.js
@@ -16,6 +16,6 @@ Router.map(function () {
this.route('example5');
this.route('example6');
this.route('example7');
+ this.route('dropdown');
this.route('performance');
- this.route('custom-trigger');
});
diff --git a/tests/dummy/app/styles/app.scss b/tests/dummy/app/styles/app.scss
index 28c77d49..5d9048d3 100644
--- a/tests/dummy/app/styles/app.scss
+++ b/tests/dummy/app/styles/app.scss
@@ -7,4 +7,4 @@
@use 'example5';
@use 'example6';
@use 'example7';
-@use 'custom-trigger';
+@use 'dropdown';
diff --git a/tests/dummy/app/styles/custom-trigger.scss b/tests/dummy/app/styles/custom-trigger.scss
deleted file mode 100644
index 79d26f82..00000000
--- a/tests/dummy/app/styles/custom-trigger.scss
+++ /dev/null
@@ -1,30 +0,0 @@
-@use 'example';
-
-.custom-trigger {
- &[data-open='false'] .select-box__options {
- @include example.screen-reader-only;
- }
-
- .select-box__trigger {
- display: contents;
- }
-
- .select-box__options {
- @include example.box;
- @include example.options;
-
- height: 240px;
- }
-
- .select-box__option {
- @include example.option;
- }
-
- .select-box__option[aria-current='true'] {
- @include example.option-current;
- }
-
- .select-box__option[aria-selected='true'] {
- @include example.option-selected;
- }
-}
diff --git a/tests/dummy/app/styles/dropdown.scss b/tests/dummy/app/styles/dropdown.scss
new file mode 100644
index 00000000..c947f66f
--- /dev/null
+++ b/tests/dummy/app/styles/dropdown.scss
@@ -0,0 +1,29 @@
+@use 'example';
+
+.dropdown.example {
+ .dropdown__trigger {
+ appearance: none;
+ all: unset;
+ cursor: pointer;
+ padding: var(--space-m);
+ outline: none;
+ user-select: none;
+ display: inline-block;
+ border: 2px solid var(--light-grey);
+ }
+
+ .dropdown__trigger:focus {
+ @include example.box-focused;
+ }
+
+ .dropdown__content {
+ border: 2px solid var(--light-grey);
+ margin-top: var(--space-m);
+ padding: var(--space-xl);
+ width: 300px;
+ }
+
+ .dropdown__content:focus-within {
+ @include example.box-focused;
+ }
+}
diff --git a/tests/dummy/app/styles/example.scss b/tests/dummy/app/styles/example.scss
index 6dccdbe5..10f69246 100644
--- a/tests/dummy/app/styles/example.scss
+++ b/tests/dummy/app/styles/example.scss
@@ -12,8 +12,8 @@
border: 2px solid var(--light-grey);
display: inline-flex;
flex-direction: column;
- margin: var(--space-sm);
- padding: var(--space-sm);
+ margin: var(--space-s);
+ padding: var(--space-s);
}
@mixin box-focused {
@@ -22,7 +22,7 @@
@mixin option {
cursor: pointer;
- padding: var(--space-lg);
+ padding: var(--space-l);
}
@mixin options {
@@ -45,14 +45,14 @@
}
@mixin group-label {
- padding: var(--space-sm);
- margin: var(--space-sm);
+ padding: var(--space-s);
+ margin: var(--space-s);
font-weight: bold;
}
@mixin trigger {
cursor: pointer;
- padding: var(--space-md);
+ padding: var(--space-m);
outline: none;
user-select: none;
border: 2px solid transparent;
@@ -68,8 +68,8 @@
box-sizing: border-box;
min-width: 200px;
border: 2px solid var(--light-grey);
- padding: var(--space-sm);
- margin: var(--space-sm);
+ padding: var(--space-s);
+ margin: var(--space-s);
outline: none;
display: block;
}
diff --git a/tests/dummy/app/styles/example3.scss b/tests/dummy/app/styles/example3.scss
index 72f5b16b..13decbdc 100644
--- a/tests/dummy/app/styles/example3.scss
+++ b/tests/dummy/app/styles/example3.scss
@@ -7,7 +7,7 @@
@include example.box-focused;
}
- &[data-open='false'] .select-box__options {
+ .select-box__dropdown[data-open='false'] .select-box__options {
@include example.screen-reader-only;
}
diff --git a/tests/dummy/app/styles/example4.scss b/tests/dummy/app/styles/example4.scss
index 8ef53f33..bf7171d3 100644
--- a/tests/dummy/app/styles/example4.scss
+++ b/tests/dummy/app/styles/example4.scss
@@ -7,7 +7,7 @@
@include example.box-focused;
}
- &[data-open='false'] .select-box__options {
+ .select-box__dropdown[data-open='false'] .select-box__options {
@include example.screen-reader-only;
}
diff --git a/tests/dummy/app/styles/example6.scss b/tests/dummy/app/styles/example6.scss
index c281a29a..716bbab5 100644
--- a/tests/dummy/app/styles/example6.scss
+++ b/tests/dummy/app/styles/example6.scss
@@ -7,6 +7,11 @@
@include example.box-focused;
}
+ .dropdown__content {
+ display: flex;
+ flex-direction: column;
+ }
+
.select-box__trigger {
@include example.trigger;
@@ -37,8 +42,3 @@
@include example.option-selected;
}
}
-
-.example6__dropdown {
- display: flex;
- flex-direction: column;
-}
diff --git a/tests/dummy/app/styles/variables.scss b/tests/dummy/app/styles/variables.scss
index c7c22a21..e69f8a9a 100644
--- a/tests/dummy/app/styles/variables.scss
+++ b/tests/dummy/app/styles/variables.scss
@@ -6,7 +6,8 @@
--red: red;
--blue: blue;
--white: white;
- --space-lg: 6px;
- --space-md: 4px;
- --space-sm: 2px;
+ --space-xl: 10px;
+ --space-l: 6px;
+ --space-m: 4px;
+ --space-s: 2px;
}
diff --git a/tests/dummy/app/templates/application.hbs b/tests/dummy/app/templates/application.hbs
index 8478ddb2..755034e9 100644
--- a/tests/dummy/app/templates/application.hbs
+++ b/tests/dummy/app/templates/application.hbs
@@ -30,6 +30,10 @@
Combobox with input outside
+ |
+
+ Dropdown
+
diff --git a/tests/dummy/app/templates/custom-trigger.hbs b/tests/dummy/app/templates/custom-trigger.hbs
deleted file mode 100644
index f022f634..00000000
--- a/tests/dummy/app/templates/custom-trigger.hbs
+++ /dev/null
@@ -1,26 +0,0 @@
-
- Selected:
- {{this.selected.name}}
-
-
-
-
-
- {{if sb.value sb.value.name "None"}}
-
-
- {{#if sb.isOpen}}
-
- {{#each this.data.pies as |value|}}
-
- {{value.name}}
-
- {{/each}}
-
- {{/if}}
-
\ No newline at end of file
diff --git a/tests/dummy/app/templates/dropdown.hbs b/tests/dummy/app/templates/dropdown.hbs
new file mode 100644
index 00000000..3ae6b6ef
--- /dev/null
+++ b/tests/dummy/app/templates/dropdown.hbs
@@ -0,0 +1,20 @@
+
+ This addon utilises a dropdown component to create comboboxes that can open
+ and close like a native single select.
+
+
+
+
+ Click here
+
+ {{#if dd.isOpen}}
+
+
+ Non interactive element
+
+
+ Interactive element
+
+
+ {{/if}}
+
\ No newline at end of file
diff --git a/tests/dummy/app/templates/performance.hbs b/tests/dummy/app/templates/performance.hbs
index 32af83eb..edb72320 100644
--- a/tests/dummy/app/templates/performance.hbs
+++ b/tests/dummy/app/templates/performance.hbs
@@ -9,16 +9,20 @@
@onChange={{this.handleSelect}}
as |sb|
>
-
- {{if sb.value sb.value.name "None"}}
-
- {{#if sb.isOpen}}
-
- {{#each this.data as |value|}}
-
- {{value.name}}
-
- {{/each}}
-
- {{/if}}
+
+
+ {{if sb.value sb.value.name "None"}}
+
+ {{#if dd.isOpen}}
+
+
+ {{#each this.data as |value|}}
+
+ {{value.name}}
+
+ {{/each}}
+
+
+ {{/if}}
+
\ No newline at end of file
diff --git a/tests/integration/components/dropdown/content/render-test.gjs b/tests/integration/components/dropdown/content/render-test.gjs
new file mode 100644
index 00000000..c2bd6fd2
--- /dev/null
+++ b/tests/integration/components/dropdown/content/render-test.gjs
@@ -0,0 +1,46 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'dummy/tests/helpers';
+import { render, find } from '@ember/test-helpers';
+import Dropdown from '@zestia/ember-select-box/components/dropdown';
+
+module('dropdown/content', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('it renders', async function (assert) {
+ assert.expect(1);
+
+ await render(
+
+
+
+ );
+
+ assert.dom('.dropdown__content').hasTagName('div');
+ });
+
+ test('it renders', async function (assert) {
+ assert.expect(1);
+
+ await render(
+
+
+ Hello World
+
+
+ );
+
+ assert.dom('.dropdown__content').hasText('Hello World');
+ });
+
+ test('splattributes', async function (assert) {
+ assert.expect(1);
+
+ await render(
+
+
+
+ );
+
+ assert.dom('.dropdown__content').hasClass('foo');
+ });
+});
diff --git a/tests/integration/components/dropdown/index/api-test.gjs b/tests/integration/components/dropdown/index/api-test.gjs
new file mode 100644
index 00000000..c3405f03
--- /dev/null
+++ b/tests/integration/components/dropdown/index/api-test.gjs
@@ -0,0 +1,90 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'dummy/tests/helpers';
+import { find, render, rerender, click } from '@ember/test-helpers';
+import Dropdown from '@zestia/ember-select-box/components/dropdown';
+
+module('dropdownx (api)', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('api', async function (assert) {
+ assert.expect(8);
+
+ let api;
+ let api2;
+
+ const handleReady = (dd) => (api = dd);
+ const capture = (dd) => (api2 = dd);
+
+ await render(
+
+
+
+ {{capture dd}}
+
+
+ );
+
+ assert.strictEqual(api, api2);
+
+ // Components
+ assert.strictEqual(typeof api.Trigger, 'object');
+ assert.strictEqual(typeof api.Content, 'object');
+
+ // Properties
+ assert.deepEqual(api.element, find('.dropdown'));
+ assert.strictEqual(api.isOpen, false);
+
+ // Actions
+ assert.strictEqual(typeof api.open, 'function');
+ assert.strictEqual(typeof api.close, 'function');
+ assert.strictEqual(typeof api.toggle, 'function');
+ });
+
+ test('isOpen', async function (assert) {
+ assert.expect(2);
+
+ let api;
+
+ const handleReady = (dd) => (api = dd);
+
+ await render(
+
+
+
+ );
+
+ assert.false(api.isOpen);
+
+ await click('.dropdown__trigger');
+
+ assert.true(api.isOpen);
+ });
+
+ test('toggle', async function (assert) {
+ assert.expect(3);
+
+ let api;
+
+ const handleReady = (dd) => (api = dd);
+
+ await render(
+
+
+
+ );
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'false');
+
+ api.toggle();
+
+ await rerender();
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'true');
+
+ api.toggle();
+
+ await rerender();
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'false');
+ });
+});
diff --git a/tests/integration/components/dropdown/index/click-trigger-test.gjs b/tests/integration/components/dropdown/index/click-trigger-test.gjs
new file mode 100644
index 00000000..3c3713b7
--- /dev/null
+++ b/tests/integration/components/dropdown/index/click-trigger-test.gjs
@@ -0,0 +1,52 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'dummy/tests/helpers';
+import { render, click } from '@ember/test-helpers';
+import Dropdown from '@zestia/ember-select-box/components/dropdown';
+
+module('dropdownx (clicking trigger)', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('clicking trigger', async function (assert) {
+ assert.expect(7);
+
+ // Whether or not the Content renders is up to the developer.
+ // This allows it to be hidden with CSS instead if preferred.
+
+ await render(
+
+
+
+
+ );
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'false');
+ assert.dom('.dropdown__trigger').hasAttribute('aria-expanded', 'false');
+ assert.dom('.dropdown__content').exists();
+
+ await click('.dropdown__trigger');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'true');
+ assert.dom('.dropdown__trigger').hasAttribute('aria-expanded', 'true');
+ assert.dom('.dropdown__content').exists();
+
+ await click('.dropdown__trigger');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'false');
+ });
+
+ test('right clicking trigger', async function (assert) {
+ assert.expect(2);
+
+ await render(
+
+
+
+ );
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'false');
+
+ await click('.dropdown__trigger', { button: 2 });
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'false');
+ });
+});
diff --git a/tests/integration/components/dropdown/index/closing-test.gjs b/tests/integration/components/dropdown/index/closing-test.gjs
new file mode 100644
index 00000000..e04fd0c0
--- /dev/null
+++ b/tests/integration/components/dropdown/index/closing-test.gjs
@@ -0,0 +1,278 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'dummy/tests/helpers';
+import {
+ render,
+ click,
+ rerender,
+ triggerEvent,
+ triggerKeyEvent
+} from '@ember/test-helpers';
+import Dropdown from '@zestia/ember-select-box/components/dropdown';
+import { on } from '@ember/modifier';
+import { tracked } from '@glimmer/tracking';
+import autoFocus from '@zestia/ember-auto-focus/modifiers/auto-focus';
+
+module('dropdownx (closing)', function (hooks) {
+ setupRenderingTest(hooks);
+
+ let handleClose;
+
+ hooks.beforeEach(function (assert) {
+ handleClose = (reason) => assert.step(`close ${reason.description}`);
+ });
+
+ test('closing with api', async function (assert) {
+ assert.expect(3);
+
+ let api;
+
+ const handleReady = (dd) => (api = dd);
+
+ await render(
+
+
+
+ );
+
+ await click('.dropdown__trigger');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'true');
+
+ api.close();
+
+ await rerender();
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'false');
+ assert.dom('.dropdown__trigger').isFocused();
+ });
+
+ test('pressing escape', async function (assert) {
+ assert.expect(3);
+
+ await render(
+
+
+
+ );
+
+ await click('.dropdown__trigger');
+
+ await triggerKeyEvent('.dropdown', 'keydown', 'Escape');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'false');
+ assert.verifySteps(['close ESCAPE']);
+ });
+
+ test('clicking outside', async function (assert) {
+ assert.expect(5);
+
+ await render(
+
+
+
+
+ );
+
+ await click('.dropdown__trigger');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'true');
+
+ await click('.outside');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'false');
+ assert.dom('.dropdown__trigger').isNotFocused();
+ assert.verifySteps(['close FOCUS_LEAVE']);
+ });
+
+ test('clicking dropdown container', async function (assert) {
+ assert.expect(5);
+
+ // This tests that the dropdown container is treated
+ // as empty space, and so click it is the same as
+ // clicking outside the dropdown content element.
+
+ await render(
+
+
+
+ );
+
+ await click('.dropdown__trigger');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'true');
+
+ await click('.dropdown');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'false');
+ assert.dom('.dropdown__trigger').isNotFocused();
+ assert.verifySteps(['close FOCUS_LEAVE']);
+ });
+
+ test('clicking a non interactive element inside the dropdown content', async function (assert) {
+ assert.expect(6);
+
+ // Selecting text won't cause the dropdown to close.
+
+ await render(
+
+
+
+
+
+
+
+
+ );
+
+ await click('.dropdown__trigger');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'true');
+
+ await click('.inside');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'true');
+ assert.dom('.dropdown__trigger').isNotFocused();
+ assert.verifySteps([]);
+
+ await click('.outside');
+
+ assert.verifySteps(['close CLICK_OUTSIDE']);
+ });
+
+ test('clicking an interactive element inside the dropdown container', async function (assert) {
+ assert.expect(4);
+
+ await render(
+
+
+
+
+ );
+
+ await click('.dropdown__trigger');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'true');
+
+ await click('.inside');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'true');
+ assert.dom('.inside').isFocused();
+ assert.verifySteps([]);
+ });
+
+ test('clicking an interactive element inside the dropdown content', async function (assert) {
+ assert.expect(5);
+
+ let event;
+
+ const handleMouseDown = (_event) => (event = _event);
+
+ await render(
+ {{! template-lint-disable no-pointer-down-event-binding }}
+
+
+
+
+
+
+ );
+
+ await click('.dropdown__trigger');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'true');
+
+ await click('.inside');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'true');
+ assert.dom('.dropdown__trigger').isNotFocused();
+ assert.false(event.defaultPrevented);
+ assert.verifySteps([]);
+ });
+
+ test('closing with api', async function (assert) {
+ assert.expect(5);
+
+ await render(
+
+
+
+
+
+
+ );
+
+ await click('.dropdown__trigger');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'true');
+
+ // Intentionally twice
+ await click('.close');
+ await click('.close');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'false');
+ assert.dom('.dropdown__trigger').isNotFocused();
+ assert.verifySteps(['close undefined']);
+ });
+
+ test('mousing down on the trigger but mousing up outside', async function (assert) {
+ assert.expect(3);
+
+ // aka click-abort
+
+ await render(
+
+
+
+
+ );
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'false');
+
+ await triggerEvent('.dropdown__trigger', 'mousedown');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'true');
+
+ await triggerEvent('.outside', 'mouseup');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'false');
+ });
+
+ test('closing does not steal focus', async function (assert) {
+ assert.expect(1);
+
+ const state = new (class {
+ @tracked showInput;
+ })();
+
+ const handleClick = () => {
+ state.showInput = true;
+ };
+
+ await render(
+
+
+
+
+
+
+
+ {{#if state.showInput}}
+
+ {{/if}}
+ );
+
+ await click('.dropdown__trigger');
+ await click('.inside');
+
+ assert.dom('.outside').isFocused();
+ });
+});
diff --git a/tests/integration/components/dropdown/index/focus-test.gjs b/tests/integration/components/dropdown/index/focus-test.gjs
new file mode 100644
index 00000000..2d1c1d69
--- /dev/null
+++ b/tests/integration/components/dropdown/index/focus-test.gjs
@@ -0,0 +1,160 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'dummy/tests/helpers';
+import { render, focus, click, blur } from '@ember/test-helpers';
+import Dropdown from '@zestia/ember-select-box/components/dropdown';
+import { on } from '@ember/modifier';
+
+module('dropdownx (focus)', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('focus leaving the dropdown trigger', async function (assert) {
+ assert.expect(2);
+
+ await render(
+
+
+
+ );
+
+ await click('.dropdown__trigger');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'true');
+
+ await blur('.dropdown__trigger');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'false');
+ });
+
+ test('focus leaving the dropdown trigger when manually opened', async function (assert) {
+ assert.expect(2);
+
+ await render(
+
+
+
+
+
+ );
+
+ await focus('.dropdown__trigger');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'true');
+
+ await focus('.outside');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'false');
+ });
+
+ test('focus leaving an interactive element inside the dropdown', async function (assert) {
+ assert.expect(2);
+
+ await render(
+
+
+
+
+
+
+ );
+
+ await click('.dropdown__trigger');
+ await focus('.inside');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'true');
+
+ await focus('.outside');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'false');
+ });
+
+ test('focus leaving an interactive element inside the content', async function (assert) {
+ assert.expect(2);
+
+ await render(
+
+
+
+
+
+
+
+
+ );
+
+ await click('.dropdown__trigger');
+ await focus('.inside');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'true');
+
+ await focus('.outside');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'false');
+ });
+
+ test('focus moving inside the dropdown', async function (assert) {
+ assert.expect(3);
+
+ await render(
+
+
+
+
+ );
+
+ await click('.dropdown__trigger');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'true');
+
+ await focus('.inside');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'true');
+ assert.dom('.inside').isFocused();
+ });
+
+ test('focus moving inside the content', async function (assert) {
+ assert.expect(3);
+
+ await render(
+
+
+
+
+
+
+ );
+
+ await click('.dropdown__trigger');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'true');
+
+ await focus('.inside');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'true');
+ assert.dom('.inside').isFocused();
+ });
+
+ test('keyboard-focusable-scrollers', async function (assert) {
+ assert.expect(2);
+
+ // we use do not employ any methods to prevent
+ // keyboard-focusable-scrollers from taking affect.
+
+ let event;
+
+ const handleMouseDown = (_event) => (event = _event);
+
+ await render(
+ {{! template-lint-disable no-pointer-down-event-binding }}
+
+
+
+
+ );
+
+ await click('.dropdown__trigger');
+ await click('.dropdown__content');
+
+ assert.dom('.dropdown__content').doesNotHaveAttribute('tabindex');
+ assert.false(event.defaultPrevented);
+ });
+});
diff --git a/tests/integration/components/dropdown/index/in-element-test.gjs b/tests/integration/components/dropdown/index/in-element-test.gjs
new file mode 100644
index 00000000..5c360204
--- /dev/null
+++ b/tests/integration/components/dropdown/index/in-element-test.gjs
@@ -0,0 +1,49 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'dummy/tests/helpers';
+import { click, render, find } from '@ember/test-helpers';
+import Dropdown from '@zestia/ember-select-box/components/dropdown';
+
+module('dropdownx (in-element)', function (hooks) {
+ setupRenderingTest(hooks);
+
+ const destination = () => find('.destination');
+
+ test('a common scenario of rendering a dropdown in an external element works', async function (assert) {
+ assert.expect(6);
+
+ await render(
+
+
+ Trigger
+
+ {{#if dd.isOpen}}
+ {{#in-element (destination) insertBefore=null}}
+
+ Hello World
+
+
+ {{/in-element}}
+ {{/if}}
+
+
+
+
+
+ );
+
+ await click('.dropdown__trigger');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'true');
+ assert.dom('.dropdown .dropdown__content').doesNotExist();
+ assert.dom('.destination .dropdown__content').exists();
+
+ await click('.test');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'true');
+
+ await click('.outside');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'false');
+ assert.dom('.outside').isFocused();
+ });
+});
diff --git a/tests/integration/components/dropdown/index/key-enter-test.gjs b/tests/integration/components/dropdown/index/key-enter-test.gjs
new file mode 100644
index 00000000..2316f089
--- /dev/null
+++ b/tests/integration/components/dropdown/index/key-enter-test.gjs
@@ -0,0 +1,26 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'dummy/tests/helpers';
+import { render, triggerKeyEvent } from '@ember/test-helpers';
+import Dropdown from '@zestia/ember-select-box/components/dropdown';
+
+module('dropdownx (enter)', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('enter on trigger', async function (assert) {
+ assert.expect(2);
+
+ await render(
+
+
+
+ );
+
+ await triggerKeyEvent('.dropdown__trigger', 'keydown', 'Enter');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'true');
+
+ await triggerKeyEvent('.dropdown__trigger', 'keydown', 'Enter');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'false');
+ });
+});
diff --git a/tests/integration/components/dropdown/index/key-escape-test.gjs b/tests/integration/components/dropdown/index/key-escape-test.gjs
new file mode 100644
index 00000000..a897bab3
--- /dev/null
+++ b/tests/integration/components/dropdown/index/key-escape-test.gjs
@@ -0,0 +1,128 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'dummy/tests/helpers';
+import { render, click, focus, triggerKeyEvent } from '@ember/test-helpers';
+import { on } from '@ember/modifier';
+import Dropdown from '@zestia/ember-select-box/components/dropdown';
+
+module('dropdownx (escape)', function (hooks) {
+ setupRenderingTest(hooks);
+
+ let handleClose;
+
+ hooks.beforeEach(function (assert) {
+ handleClose = () => assert.step('close');
+ });
+
+ test('escape on trigger', async function (assert) {
+ assert.expect(4);
+
+ await render(
+
+
+
+ );
+
+ await click('.dropdown__trigger');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'true');
+
+ await triggerKeyEvent('.dropdown__trigger', 'keydown', 'Escape');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'false');
+ assert.verifySteps(['close']);
+ });
+
+ test('escape on dropdown', async function (assert) {
+ assert.expect(4);
+
+ // This ensures our listeners are on the dropdown itself,
+ // and not just on the trigger.
+
+ await render(
+
+
+
+
+
+
+ );
+
+ await click('.dropdown__trigger');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'true');
+
+ await focus('.inside');
+ await triggerKeyEvent('.dropdown', 'keydown', 'Escape');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'false');
+ assert.verifySteps(['close']);
+ });
+
+ test('escape inside something else escapable (closed)', async function (assert) {
+ assert.expect(1);
+
+ let event;
+
+ const handleKeyDownParent = (_event) => (event = _event);
+
+ await render(
+ {{! template-lint-disable no-invalid-interactive }}
+
+
+
+
+
+
+
+ );
+
+ await triggerKeyEvent('.dropdown', 'keydown', 'Escape');
+
+ assert.true(
+ event instanceof Event,
+ 'event not stopped, escape allowed to bubble up'
+ );
+ });
+
+ test('escape inside something else escapable (open)', async function (assert) {
+ assert.expect(5);
+
+ let event;
+
+ const handleKeyDownParent = (_event) => (event = _event);
+
+ await render(
+ {{! template-lint-disable no-invalid-interactive }}
+
+
+
+
+
+
+
+
+
+ );
+
+ await click('.parent .dropdown__trigger');
+ await click('.child .dropdown__trigger');
+ await triggerKeyEvent('.child', 'keydown', 'Escape');
+
+ assert.dom('.parent').hasAttribute('data-open', 'true');
+ assert.dom('.child').hasAttribute('data-open', 'false');
+
+ assert.notOk(
+ event,
+ `event propagation is stopped, since escape has caused the dropdown
+ to close. we don't want escape to also close the parent element
+ that the select box is contained within`
+ );
+
+ assert.verifySteps(['close']);
+ });
+});
diff --git a/tests/integration/components/dropdown/index/key-space-test.gjs b/tests/integration/components/dropdown/index/key-space-test.gjs
new file mode 100644
index 00000000..0074ed7c
--- /dev/null
+++ b/tests/integration/components/dropdown/index/key-space-test.gjs
@@ -0,0 +1,26 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'dummy/tests/helpers';
+import { render, triggerKeyEvent } from '@ember/test-helpers';
+import Dropdown from '@zestia/ember-select-box/components/dropdown';
+
+module('dropdownx', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('space on trigger (space)', async function (assert) {
+ assert.expect(2);
+
+ await render(
+
+
+
+ );
+
+ await triggerKeyEvent('.dropdown__trigger', 'keydown', ' ');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'true');
+
+ await triggerKeyEvent('.dropdown__trigger', 'keydown', ' ');
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'false');
+ });
+});
diff --git a/tests/integration/components/dropdown/index/opening-test.gjs b/tests/integration/components/dropdown/index/opening-test.gjs
new file mode 100644
index 00000000..2e2d489a
--- /dev/null
+++ b/tests/integration/components/dropdown/index/opening-test.gjs
@@ -0,0 +1,72 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'dummy/tests/helpers';
+import { render, rerender } from '@ember/test-helpers';
+import { tracked } from '@glimmer/tracking';
+import Dropdown from '@zestia/ember-select-box/components/dropdown';
+
+module('dropdownx (opening)', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('open', async function (assert) {
+ assert.expect(2);
+
+ let api;
+
+ const handleReady = (dd) => (api = dd);
+
+ await render(
+
+
+
+ );
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'false');
+
+ api.open();
+
+ await rerender();
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'true');
+ });
+
+ test('can set initial open state', async function (assert) {
+ assert.expect(3);
+
+ await render(
+
+
+
+
+ );
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'true');
+ assert.dom('.dropdown__trigger').hasAttribute('aria-expanded', 'true');
+ assert.dom('.dropdown__trigger').isNotFocused();
+ });
+
+ test('cannot open dropdown manually with argument', async function (assert) {
+ assert.expect(2);
+
+ // We can easily support this using localCopy.
+ // But, changing `@open` will not pass through the expected
+ // code path, like it would with a user interaction, and so
+ // onOpen will not fire if we do that.
+
+ const state = new (class {
+ @tracked isOpen;
+ })();
+
+ await render(
+
+
+
+ );
+
+ state.isOpen = true;
+
+ await rerender();
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'false');
+ assert.dom('.dropdown__trigger').hasAttribute('aria-expanded', 'false');
+ });
+});
diff --git a/tests/integration/components/dropdown/index/render-test.gjs b/tests/integration/components/dropdown/index/render-test.gjs
new file mode 100644
index 00000000..9537f4a9
--- /dev/null
+++ b/tests/integration/components/dropdown/index/render-test.gjs
@@ -0,0 +1,40 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'dummy/tests/helpers';
+import { render, find } from '@ember/test-helpers';
+import Dropdown from '@zestia/ember-select-box/components/dropdown';
+
+module('dropdownx', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('it renders', async function (assert) {
+ assert.expect(1);
+
+ await render(
);
+
+ assert.dom('.dropdown').hasTagName('div');
+ });
+
+ test('open', async function (assert) {
+ assert.expect(1);
+
+ await render(
);
+
+ assert.dom('.dropdown').hasAttribute('data-open', 'false');
+ });
+
+ test('splattributes', async function (assert) {
+ assert.expect(1);
+
+ await render(
);
+
+ assert.dom('.dropdown').hasClass('foo');
+ });
+
+ test('whitespace', async function (assert) {
+ assert.expect(1);
+
+ await render(
);
+
+ assert.strictEqual(find('.dropdown').innerHTML, '');
+ });
+});
diff --git a/tests/integration/components/dropdown/trigger/render-test.gjs b/tests/integration/components/dropdown/trigger/render-test.gjs
new file mode 100644
index 00000000..443f0aca
--- /dev/null
+++ b/tests/integration/components/dropdown/trigger/render-test.gjs
@@ -0,0 +1,152 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'dummy/tests/helpers';
+import { render, find } from '@ember/test-helpers';
+import Dropdown from '@zestia/ember-select-box/components/dropdown';
+
+module('dropdown/trigger', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('it renders', async function (assert) {
+ assert.expect(1);
+
+ await render(
+
+
+
+ );
+
+ assert.dom('.dropdown__trigger').hasTagName('div');
+ });
+
+ test('splattributes', async function (assert) {
+ assert.expect(1);
+
+ await render(
+
+
+
+ );
+
+ assert.dom('.dropdown__trigger').hasClass('foo');
+ });
+
+ test('role', async function (assert) {
+ assert.expect(1);
+
+ await render(
+
+
+
+ );
+
+ assert.dom('.dropdown__trigger').doesNotHaveAttribute('role');
+ });
+
+ test('role (closure component)', async function (assert) {
+ assert.expect(1);
+
+ await render(
+
+ {{component dd.Trigger role="button"}}
+
+ );
+
+ assert.dom('.dropdown__trigger').hasAttribute('role', 'button');
+ });
+
+ test('tabindex', async function (assert) {
+ assert.expect(1);
+
+ // Requires tabindex so that Safari will populate relatedTarget
+ // and handle focus properly.
+
+ await render(
+
+
+
+ );
+
+ assert.dom('.dropdown__trigger').hasAttribute('tabindex', '0');
+ });
+
+ test('tabindex (closure component)', async function (assert) {
+ assert.expect(1);
+
+ await render(
+
+ {{component dd.Trigger tabindex="-1"}}
+
+ );
+
+ assert.dom('.dropdown__trigger').hasAttribute('tabindex', '-1');
+ });
+
+ test('class (closure component)', async function (assert) {
+ assert.expect(1);
+
+ await render(
+
+ {{component dd.Trigger class="foo"}}
+
+ );
+
+ assert.dom('.dropdown__trigger').hasClass('foo');
+ });
+
+ test('whitespace', async function (assert) {
+ assert.expect(1);
+
+ await render(
+
+
+
+ );
+
+ assert.strictEqual(find('.dropdown__trigger').innerHTML, '');
+ });
+
+ test('aria defaults', async function (assert) {
+ assert.expect(6);
+
+ await render(
+
+
+
+ );
+
+ assert
+ .dom('.dropdown__trigger')
+ .hasAttribute('aria-expanded', 'false')
+ .hasAttribute('aria-haspopup', 'true')
+ .doesNotHaveAttribute('aria-busy')
+ .doesNotHaveAttribute('aria-controls')
+ .doesNotHaveAttribute('aria-disabled')
+ .doesNotHaveAttribute('aria-activedescendant');
+ });
+
+ test('aria (closure component)', async function (assert) {
+ assert.expect(6);
+
+ await render(
+
+ {{component
+ dd.Trigger
+ aria-expanded="true"
+ aria-busy="true"
+ aria-controls="foo"
+ aria-disabled="true"
+ aria-activedescendant="bar"
+ }}
+
+ );
+
+ assert
+ .dom('.dropdown__trigger')
+ .hasAttribute('aria-haspopup', 'true')
+ .hasAttribute('aria-expanded', 'true')
+ .hasAttribute('aria-busy', 'true')
+ .hasAttribute('aria-controls', 'foo')
+ .hasAttribute('aria-disabled', 'true')
+ .hasAttribute('aria-activedescendant', 'bar');
+ });
+});
diff --git a/tests/integration/components/select-box/closing-test.gjs b/tests/integration/components/select-box/closing-test.gjs
deleted file mode 100644
index 66df39c2..00000000
--- a/tests/integration/components/select-box/closing-test.gjs
+++ /dev/null
@@ -1,301 +0,0 @@
-import { module, test } from 'qunit';
-import { setupRenderingTest } from 'dummy/tests/helpers';
-import {
- render,
- click,
- blur,
- focus,
- triggerEvent,
- triggerKeyEvent
-} from '@ember/test-helpers';
-import { tracked } from '@glimmer/tracking';
-import { on } from '@ember/modifier';
-import autoFocus from '@zestia/ember-auto-focus/modifiers/auto-focus';
-import SelectBox from '@zestia/ember-select-box/components/select-box';
-
-module('select-box (closing)', function (hooks) {
- setupRenderingTest(hooks);
-
- let api;
- let handleClose;
-
- const handleReady = (sb) => (api = sb);
-
- hooks.beforeEach(function (assert) {
- handleClose = () => assert.step('close');
- });
-
- test('closing with api', async function (assert) {
- assert.expect(6);
-
- await render(
-
-
-
-
-
- );
-
- assert.true(api.isOpen);
-
- await click('button');
- await click('button');
-
- assert.verifySteps(['close']);
-
- assert.dom('.select-box').hasAttribute('data-open', 'false');
- assert.dom('.select-box__trigger').hasAttribute('aria-expanded', 'false');
- assert.dom('.select-box__input').hasAttribute('aria-expanded', 'false');
- });
-
- test('closes when trigger loses focus', async function (assert) {
- assert.expect(3);
-
- await render(
-
-
-
-
- );
-
- await focus('.select-box__trigger');
- await blur('.select-box__trigger');
-
- assert.dom('.select-box').hasAttribute('data-open', 'false');
- assert.dom('.select-box__trigger').hasAttribute('aria-expanded', 'false');
- assert.dom('.select-box__input').hasAttribute('aria-expanded', 'false');
- });
-
- test('closes when input loses focus', async function (assert) {
- assert.expect(3);
-
- await render(
-
-
-
-
- );
-
- await focus('.select-box__input');
- await blur('.select-box__input');
-
- assert.dom('.select-box').hasAttribute('data-open', 'false');
- assert.dom('.select-box__trigger').hasAttribute('aria-expanded', 'false');
- assert.dom('.select-box__input').hasAttribute('aria-expanded', 'false');
- });
-
- test('closing listbox', async function (assert) {
- assert.expect(1);
-
- await render(
-
-
-
-
- );
-
- await click('button');
-
- assert.verifySteps([], 'listboxes cannot be closed');
- });
-
- test('closing does not steal focus', async function (assert) {
- assert.expect(2);
-
- const state = new (class {
- @tracked value;
- })();
-
- const handleChange = (value) => (state.value = value);
-
- await render(
-
-
-
-
-
-
-
- {{#if state.value}}
-
- {{/if}}
- );
-
- await click('.select-box__trigger');
- await click('.select-box__option');
-
- assert.dom('.outside').hasValue('foo').isFocused();
- });
-
- test('closing due to loss of focus', async function (assert) {
- assert.expect(3);
-
- await render(
-
-
-
-
-
-
- );
-
- await click('.select-box__trigger');
- await blur('.select-box__trigger');
-
- assert.dom('.select-box').hasAttribute('data-open', 'false');
- assert.verifySteps(['close']);
- });
-
- test('closing due to mousing up outside', async function (assert) {
- assert.expect(3);
-
- await render(
-
-
-
-
-
-
-
-
- );
-
- await triggerEvent('.select-box__trigger', 'mousedown');
- await triggerEvent('.outside', 'mouseup');
-
- assert.dom('.select-box').hasAttribute('data-open', 'false');
- assert.verifySteps(['close']);
- });
-
- test('mousing up outside must have moused down first', async function (assert) {
- assert.expect(2);
-
- await render(
-
-
-
-
-
- );
-
- await click('.outside');
-
- assert.dom('.select-box').hasAttribute('data-open', 'true');
- assert.verifySteps([]);
- });
-
- test('closing due to touching outside', async function (assert) {
- assert.expect(3);
-
- await render(
-
-
-
-
-
- );
-
- await triggerEvent('.select-box', 'mousedown');
- await triggerEvent('.outside', 'touchstart');
-
- assert.dom('.select-box').hasAttribute('data-open', 'false');
- assert.verifySteps(['close']);
- });
-
- test('closing due to pressing escape', async function (assert) {
- assert.expect(3);
-
- await render(
-
-
-
-
-
-
- );
-
- await click('.select-box__trigger');
- await triggerKeyEvent('.select-box__trigger', 'keydown', 'Escape');
-
- assert.dom('.select-box').hasAttribute('data-open', 'false');
- assert.verifySteps(['close']);
- });
-
- test('close only fires once', async function (assert) {
- assert.expect(2);
-
- await render(
-
-
-
-
-
- );
-
- await click('.select-box__trigger');
- await blur('.select-box__trigger');
- await triggerEvent('.outside', 'mouseup');
-
- assert.verifySteps(
- ['close'],
- `a select box may close on focus leave and mousing up outside,
- if focus is lost _because_ of the mousing up outside, then
- the onClose action only fires once, not twice`
- );
- });
-
- test('programmatically closing', async function (assert) {
- assert.expect(3);
-
- const handleSelect = () => false;
-
- await render(
-
-
-
-
- );
-
- await click('.select-box__trigger');
-
- assert.verifySteps(
- [],
- `the return value of onSelect controls whether or not the
- select box will close after making the selection`
- );
-
- await click('button');
-
- assert.verifySteps(['close']);
- });
-
- test('clicking to programmatically close', async function (assert) {
- assert.expect(1);
-
- await render(
-
-
- {{#if sb.isOpen}}
-
-
-
- {{/if}}
-
- );
-
- await click('.select-box__trigger');
- await click('button');
-
- assert.ok(true, 'does not cause infinite revalidation bug');
- });
-});
diff --git a/tests/integration/components/group/render-test.gjs b/tests/integration/components/select-box/group/render-test.gjs
similarity index 100%
rename from tests/integration/components/group/render-test.gjs
rename to tests/integration/components/select-box/group/render-test.gjs
diff --git a/tests/integration/components/select-box/api-test.gjs b/tests/integration/components/select-box/index/api-test.gjs
similarity index 65%
rename from tests/integration/components/select-box/api-test.gjs
rename to tests/integration/components/select-box/index/api-test.gjs
index 476332cd..058f5a5e 100644
--- a/tests/integration/components/select-box/api-test.gjs
+++ b/tests/integration/components/select-box/index/api-test.gjs
@@ -1,6 +1,13 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'dummy/tests/helpers';
-import { find, render, fillIn, settled, click } from '@ember/test-helpers';
+import {
+ find,
+ render,
+ rerender,
+ fillIn,
+ settled,
+ click
+} from '@ember/test-helpers';
import { tracked } from '@glimmer/tracking';
import { on } from '@ember/modifier';
import { fn } from '@ember/helper';
@@ -10,7 +17,7 @@ module('select-box (api)', function (hooks) {
setupRenderingTest(hooks);
test('api', async function (assert) {
- assert.expect(18);
+ assert.expect(16);
let api;
let api2;
@@ -20,10 +27,14 @@ module('select-box (api)', function (hooks) {
await render(
-
-
- {{capture sb}}
-
+
+
+
+
+ {{capture sb}}
+
+
+
);
@@ -34,26 +45,42 @@ module('select-box (api)', function (hooks) {
assert.strictEqual(typeof api.Input, 'object');
assert.strictEqual(typeof api.Option, 'object');
assert.strictEqual(typeof api.Options, 'object');
+ assert.strictEqual(typeof api.Dropdown, 'object');
assert.strictEqual(typeof api.Trigger, 'object');
// Properties
assert.deepEqual(api.element, find('.select-box'));
assert.strictEqual(api.isBusy, null);
- assert.false(api.isOpen);
assert.strictEqual(api.options, undefined);
assert.strictEqual(api.query, null);
assert.strictEqual(api.value, undefined);
+ assert.strictEqual(typeof api.dropdown, 'object');
// Actions
- assert.strictEqual(typeof api.close, 'function');
- assert.strictEqual(typeof api.open, 'function');
assert.strictEqual(typeof api.search, 'function');
- assert.strictEqual(typeof api.toggle, 'function');
assert.strictEqual(typeof api.update, 'function');
assert.strictEqual(typeof api.select, 'function');
});
- test('provides access to two useful elements', async function (assert) {
+ test('api without dropdown', async function (assert) {
+ assert.expect(2);
+
+ let api;
+
+ const handleReady = (sb) => (api = sb);
+
+ await render(
);
+
+ assert.strictEqual(typeof api.Dropdown, 'object');
+ assert.strictEqual(
+ api.Trigger,
+ undefined,
+ `can't render a select box trigger without first
+ having rendered a dropdown.`
+ );
+ });
+
+ test('provides access to the main element', async function (assert) {
assert.expect(1);
let api;
@@ -69,7 +96,7 @@ module('select-box (api)', function (hooks) {
assert.strictEqual(api.element, find('.select-box'));
});
- test('isBusy (searchable)', async function (assert) {
+ test('isBusy (aka searchable)', async function (assert) {
assert.expect(3);
let api;
@@ -81,9 +108,8 @@ module('select-box (api)', function (hooks) {
await render(
-
-
-
+
+
);
@@ -125,15 +151,17 @@ module('select-box (api)', function (hooks) {
await render(
-
+
+
+
);
- assert.false(api.isOpen);
+ assert.false(api.dropdown.isOpen);
await click('.select-box__trigger');
- assert.true(api.isOpen);
+ assert.true(api.dropdown.isOpen);
});
test('value', async function (assert) {
@@ -184,66 +212,6 @@ module('select-box (api)', function (hooks) {
assert.verifySteps(['foo']);
});
- test('open', async function (assert) {
- assert.expect(3);
-
- await render(
-
-
-
-
-
- );
-
- await click('button');
-
- assert.dom('.select-box').hasAttribute('data-open', 'true');
- assert.dom('.select-box__trigger').hasAttribute('aria-expanded', 'true');
- assert.dom('.select-box__input').hasAttribute('aria-expanded', 'true');
- });
-
- test('close', async function (assert) {
- assert.expect(3);
-
- await render(
-
-
-
-
-
- );
-
- await click('button');
-
- assert.dom('.select-box').hasAttribute('data-open', 'false');
- assert.dom('.select-box__trigger').hasAttribute('aria-expanded', 'false');
- assert.dom('.select-box__input').hasAttribute('aria-expanded', 'false');
- });
-
- test('toggle', async function (assert) {
- assert.expect(6);
-
- await render(
-
-
-
-
-
- );
-
- await click('button');
-
- assert.dom('.select-box').hasAttribute('data-open', 'true');
- assert.dom('.select-box__trigger').hasAttribute('aria-expanded', 'true');
- assert.dom('.select-box__input').hasAttribute('aria-expanded', 'true');
-
- await click('button');
-
- assert.dom('.select-box').hasAttribute('data-open', 'false');
- assert.dom('.select-box__trigger').hasAttribute('aria-expanded', 'false');
- assert.dom('.select-box__input').hasAttribute('aria-expanded', 'false');
- });
-
test('select', async function (assert) {
assert.expect(8);
@@ -303,30 +271,32 @@ module('select-box (api)', function (hooks) {
test('select (not focused)', async function (assert) {
assert.expect(4);
- const state = new (class {
- @tracked api;
- })();
+ let api;
- const handleReady = (sb) => (state.api = sb);
+ const handleReady = (sb) => (api = sb);
await render(
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
);
- await click('button');
+ api.select('2');
+
+ await rerender();
assert
.dom('.select-box__option:nth-child(2)')
- .hasAttribute('aria-selected', 'true', 'precondition');
+ .hasAttribute('aria-selected', 'true');
assert.dom('.select-box__input').isNotFocused();
assert.dom('.select-box__trigger').isNotFocused();
@@ -334,30 +304,36 @@ module('select-box (api)', function (hooks) {
});
test('select (closes)', async function (assert) {
- assert.expect(1);
+ assert.expect(2);
- const state = new (class {
- @tracked api;
- })();
+ let api;
- const handleReady = (sb) => (state.api = sb);
+ const handleReady = (sb) => (api = sb);
await render(
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
);
- await click('button');
+ await click('.dropdown__trigger');
+
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'true');
+
+ api.select('2');
+
+ await rerender();
assert
- .dom('.select-box')
+ .dom('.select-box__dropdown')
.hasAttribute(
'data-open',
'false',
@@ -368,25 +344,21 @@ module('select-box (api)', function (hooks) {
test('select (disabled option)', async function (assert) {
assert.expect(2);
- const state = new (class {
- @tracked api;
- })();
+ let api;
- const handleReady = (sb) => (state.api = sb);
+ const handleReady = (sb) => (api = sb);
await render(
-
-
-
-
);
- await click('button');
+ api.select('foo');
+
+ await rerender();
assert
.dom('.select-box__option:nth-child(1)')
@@ -426,7 +398,6 @@ module('select-box (api)', function (hooks) {
@onBuildSelection={{handleBuildSelection}}
as |sb|
>
-
@@ -434,8 +405,10 @@ module('select-box (api)', function (hooks) {
);
- await click('button');
- await click('button');
+ api.update('2');
+ api.update('2');
+
+ await rerender();
assert.strictEqual(state.value, '1', 'does not mutate value');
assert.strictEqual(api.value, '2', 'api is correct');
@@ -469,31 +442,4 @@ module('select-box (api)', function (hooks) {
await click('button');
});
-
- test('open boolean with a trigger defaults to closed', async function (assert) {
- assert.expect(2);
-
- await render(
-
-
-
-
- );
-
- assert.dom('.select-box').hasAttribute('data-open', 'false');
- assert.dom('.select-box__options').hasAttribute('data-open', 'false');
- });
-
- test('open boolean without a trigger is undefined', async function (assert) {
- assert.expect(2);
-
- await render(
-
-
-
- );
-
- assert.dom('.select-box').doesNotHaveAttribute('data-open');
- assert.dom('.select-box__options').doesNotHaveAttribute('data-open');
- });
});
diff --git a/tests/integration/components/select-box/change-options-test.gjs b/tests/integration/components/select-box/index/change-options-test.gjs
similarity index 98%
rename from tests/integration/components/select-box/change-options-test.gjs
rename to tests/integration/components/select-box/index/change-options-test.gjs
index 0411a1c0..3ef0ed53 100644
--- a/tests/integration/components/select-box/change-options-test.gjs
+++ b/tests/integration/components/select-box/index/change-options-test.gjs
@@ -99,18 +99,21 @@ module('select-box (changing options)', function (hooks) {
assert.expect(7);
const options = [
+ // a
'a1',
'a2',
'a3',
'a4',
'a5',
'a6',
+ // b
'b1',
'b2',
'b3',
'b4',
'b5',
'b6',
+ // c
'c1',
'c2',
'c3',
@@ -150,12 +153,12 @@ module('select-box (changing options)', function (hooks) {
assert.dom('.select-box__option').exists({ count: 18 });
assert.dom('.select-box__option[aria-current="true"]').hasText('b3');
- // 16px each times 6 preceeding options
+ // 16px each, times 6 preceding options
assert.strictEqual(find('.select-box__options').scrollTop, 96);
await fillIn('.select-box__input', 'b');
- // 16px each times 2 preceeding options
+ // 16px each, times 2 preceding options
assert.strictEqual(
find('.select-box__options').scrollTop,
32,
diff --git a/tests/integration/components/select-box/click-input-test.gjs b/tests/integration/components/select-box/index/click-input-test.gjs
similarity index 80%
rename from tests/integration/components/select-box/click-input-test.gjs
rename to tests/integration/components/select-box/index/click-input-test.gjs
index 47d36227..eebc0287 100644
--- a/tests/integration/components/select-box/click-input-test.gjs
+++ b/tests/integration/components/select-box/index/click-input-test.gjs
@@ -11,14 +11,16 @@ module('select-box (clicking input)', function (hooks) {
await render(
-
-
+
+
+
+
);
await click('.select-box__input');
- assert.dom('.select-box').hasAttribute('data-open', 'false');
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'false');
assert.dom('.select-box__input').hasAttribute('aria-expanded', 'false');
assert.dom('.select-box__trigger').hasAttribute('aria-expanded', 'false');
});
diff --git a/tests/integration/components/select-box/click-option-test.gjs b/tests/integration/components/select-box/index/click-option-test.gjs
similarity index 69%
rename from tests/integration/components/select-box/click-option-test.gjs
rename to tests/integration/components/select-box/index/click-option-test.gjs
index 1c551e60..7c2d3c32 100644
--- a/tests/integration/components/select-box/click-option-test.gjs
+++ b/tests/integration/components/select-box/index/click-option-test.gjs
@@ -1,6 +1,6 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'dummy/tests/helpers';
-import { render, click, triggerEvent } from '@ember/test-helpers';
+import { render, click } from '@ember/test-helpers';
import { on } from '@ember/modifier';
import { tracked } from '@glimmer/tracking';
import SelectBox from '@zestia/ember-select-box/components/select-box';
@@ -45,7 +45,7 @@ module('select-box (clicking option)', function (hooks) {
});
test('clicking on a child', async function (assert) {
- assert.expect(4);
+ assert.expect(5);
let event;
@@ -54,37 +54,45 @@ module('select-box (clicking option)', function (hooks) {
await render(
{{! template-lint-disable no-pointer-down-event-binding }}
-
-
-
- Link
-
-
+
+
+ {{sb.value}}
+
+
+
+
+ Link
+
+
+
+
);
await click('.select-box__trigger');
- assert.dom('.select-box').hasAttribute('data-open', 'true', 'precondition');
+ assert
+ .dom('.select-box__dropdown')
+ .hasAttribute('data-open', 'true', 'precondition');
- await triggerEvent('.select-box__option', 'mousedown');
await click('a');
assert.true(
event.defaultPrevented,
- 'focus will not leave the trigger or input'
+ `focus will not leave the trigger or input. but,
+ the link will still be visited. prevent default on the link click, if
+ you wish to have options that can be cmd clicked to open in a new tab`
);
- assert.dom('.select-box').hasAttribute(
+ assert.dom('.select-box__dropdown').hasAttribute(
'data-open',
'false',
- `
- the select box closes because an option was selected. even though the target
- was a child of the option. this allows for greater flexibility building UI's
- that require custom markup
- `
+ `the select box closes because an option was selected. even though the target
+ was a child of the option.`
);
+ assert.dom('.select-box__trigger').hasText('#');
+
assert.dom('.select-box__trigger').isFocused('does not lose focus');
});
@@ -93,21 +101,27 @@ module('select-box (clicking option)', function (hooks) {
await render(
-
-
-
-
+
+
+
+
+
+
+
+
);
await click('.select-box__trigger');
- assert.dom('.select-box').hasAttribute('data-open', 'true', 'precondition');
+ assert
+ .dom('.select-box__dropdown')
+ .hasAttribute('data-open', 'true', 'precondition');
await click('.select-box__option');
assert
- .dom('.select-box')
+ .dom('.select-box__dropdown')
.hasAttribute(
'data-open',
'false',
@@ -120,20 +134,26 @@ module('select-box (clicking option)', function (hooks) {
await render(
-
-
-
-
+
+
+
+
+
+
+
+
);
await click('.select-box__trigger');
- assert.dom('.select-box').hasAttribute('data-open', 'true', 'precondition');
+ assert
+ .dom('.select-box__dropdown')
+ .hasAttribute('data-open', 'true', 'precondition');
await click('.select-box__option');
- assert.dom('.select-box').hasAttribute(
+ assert.dom('.select-box__dropdown').hasAttribute(
'data-open',
'true',
`assume that more options are to be selected. do not assume its ok to close
@@ -177,12 +197,16 @@ module('select-box (clicking option)', function (hooks) {
await render(
{{#unless state.hideSelectBox}}
-
-
- {{#unless state.value}}
-
- {{/unless}}
-
+
+
+
+
+ {{#unless state.value}}
+
+ {{/unless}}
+
+
+
{{/unless}}
);
diff --git a/tests/integration/components/select-box/click-trigger-test.gjs b/tests/integration/components/select-box/index/click-trigger-test.gjs
similarity index 60%
rename from tests/integration/components/select-box/click-trigger-test.gjs
rename to tests/integration/components/select-box/index/click-trigger-test.gjs
index 63586579..5f92ebb3 100644
--- a/tests/integration/components/select-box/click-trigger-test.gjs
+++ b/tests/integration/components/select-box/index/click-trigger-test.gjs
@@ -1,6 +1,6 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'dummy/tests/helpers';
-import { render, triggerEvent } from '@ember/test-helpers';
+import { render, click, triggerEvent } from '@ember/test-helpers';
import { on } from '@ember/modifier';
import SelectBox from '@zestia/ember-select-box/components/select-box';
@@ -11,7 +11,7 @@ module('select-box (clicking trigger)', function (hooks) {
assert.expect(7);
// Mouse down makes the select box feel more responsive
- // than on click, which would open on mouseup.
+ // than on click, which would open on mouse up.
// It's also what would happen if you clicked on a
// native select box.
@@ -22,24 +22,32 @@ module('select-box (clicking trigger)', function (hooks) {
await render(
{{! template-lint-disable no-pointer-down-event-binding }}
-
+
+
+
);
- assert.dom('.select-box').hasAttribute('data-open', 'false');
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'false');
assert.dom('.select-box__trigger').hasAttribute('aria-expanded', 'false');
await triggerEvent('.select-box__trigger', 'mousedown');
- assert.dom('.select-box').hasAttribute('data-open', 'true');
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'true');
assert.dom('.select-box__trigger').hasAttribute('aria-expanded', 'true');
await triggerEvent('.select-box__trigger', 'mousedown');
- assert.dom('.select-box').hasAttribute('data-open', 'false');
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'false');
assert.dom('.select-box__trigger').hasAttribute('aria-expanded', 'false');
- assert.true(event.defaultPrevented);
+ assert.true(
+ event.defaultPrevented,
+ `whilst mousing down on the trigger would ordinarily focus it,
+ here we prevent that, so that we can manually control where
+ focus moves to next. e.g. either remaining on the trigger,
+ or advancing the a comobox's input`
+ );
});
test('right clicking trigger does not open select box', async function (assert) {
@@ -47,13 +55,15 @@ module('select-box (clicking trigger)', function (hooks) {
await render(
-
+
+
+
);
- await triggerEvent('.select-box__trigger', 'mousedown', { button: 2 });
+ await click('.select-box__trigger', { button: 2 });
- assert.dom('.select-box').hasAttribute('data-open', 'false');
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'false');
assert.dom('.select-box__trigger').hasAttribute('aria-expanded', 'false');
});
});
diff --git a/tests/integration/components/select-box/index/closing-test.gjs b/tests/integration/components/select-box/index/closing-test.gjs
new file mode 100644
index 00000000..fb212350
--- /dev/null
+++ b/tests/integration/components/select-box/index/closing-test.gjs
@@ -0,0 +1,313 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'dummy/tests/helpers';
+import {
+ render,
+ click,
+ blur,
+ focus,
+ triggerEvent,
+ triggerKeyEvent
+} from '@ember/test-helpers';
+import { tracked } from '@glimmer/tracking';
+import { on } from '@ember/modifier';
+import autoFocus from '@zestia/ember-auto-focus/modifiers/auto-focus';
+import SelectBox from '@zestia/ember-select-box/components/select-box';
+
+module('select-box (closing)', function (hooks) {
+ setupRenderingTest(hooks);
+
+ let handleClose;
+
+ hooks.beforeEach(function (assert) {
+ handleClose = () => assert.step('close');
+ });
+
+ test('closing with api', async function (assert) {
+ assert.expect(8);
+
+ await render(
+
+
+
+
+
+
+
+
+
+ );
+
+ await click('.dropdown__trigger');
+
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'true');
+ assert.dom('.select-box__trigger').hasAttribute('aria-expanded', 'true');
+ assert.dom('.select-box__input').hasAttribute('aria-expanded', 'true');
+
+ // Intentionally twice
+ await click('.close');
+ await click('.close');
+
+ assert.verifySteps(['close']);
+
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'false');
+ assert.dom('.select-box__trigger').hasAttribute('aria-expanded', 'false');
+ assert.dom('.select-box__input').hasAttribute('aria-expanded', 'false');
+ });
+
+ test('closes when trigger loses focus', async function (assert) {
+ assert.expect(6);
+
+ await render(
+
+
+
+
+
+
+ );
+
+ await click('.select-box__trigger');
+
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'true');
+ assert.dom('.select-box__trigger').hasAttribute('aria-expanded', 'true');
+ assert.dom('.select-box__input').hasAttribute('aria-expanded', 'true');
+
+ await focus('.select-box__trigger');
+ await blur('.select-box__trigger');
+
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'false');
+ assert.dom('.select-box__trigger').hasAttribute('aria-expanded', 'false');
+ assert.dom('.select-box__input').hasAttribute('aria-expanded', 'false');
+ });
+
+ test('closes when input loses focus', async function (assert) {
+ assert.expect(4);
+
+ await render(
+
+
+
+
+
+
+ );
+
+ await click('.select-box__trigger');
+
+ assert.dom('.select-box__input').isFocused();
+
+ await blur('.select-box__input');
+
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'false');
+ assert.dom('.select-box__trigger').hasAttribute('aria-expanded', 'false');
+ assert.dom('.select-box__input').hasAttribute('aria-expanded', 'false');
+ });
+
+ test('closing does not steal focus', async function (assert) {
+ assert.expect(2);
+
+ const state = new (class {
+ @tracked value;
+ })();
+
+ const handleChange = (value) => (state.value = value);
+
+ await render(
+
+
+
+
+
+
+
+
+
+
+
+ {{#if state.value}}
+
+ {{/if}}
+ );
+
+ await click('.select-box__trigger');
+ await click('.select-box__option');
+
+ assert.dom('.outside').hasValue('foo').isFocused();
+ });
+
+ test('closing due to mousing up outside', async function (assert) {
+ assert.expect(3);
+
+ // This is a 'click abort'. When the user mouses down on the
+ // select box, drags their cursor over an option, but then
+ // decides no, and drags their cursor outside the select box
+ // and releases.
+
+ await render(
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+
+ await triggerEvent('.select-box__trigger', 'mousedown');
+ await triggerEvent('.outside', 'mouseup');
+
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'false');
+ assert.verifySteps(['close']);
+ });
+
+ test('mousing up outside will close a manually opened dropdown', async function (assert) {
+ assert.expect(4);
+
+ await render(
+
+
+
+
+
+
+
+ );
+
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'true');
+
+ await click('.outside');
+
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'false');
+ assert.verifySteps(['close']);
+ });
+
+ test('closing due to pressing escape', async function (assert) {
+ assert.expect(3);
+
+ await render(
+
+
+
+
+
+
+
+
+
+
+ );
+
+ await click('.select-box__trigger');
+ await triggerKeyEvent('.select-box__trigger', 'keydown', 'Escape');
+
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'false');
+ assert.verifySteps(['close']);
+ });
+
+ test('programmatically closing', async function (assert) {
+ assert.expect(3);
+
+ const handleSelect = () => false;
+
+ await render(
+
+
+
+
+
+
+
+
+ );
+
+ await click('.select-box__trigger');
+
+ assert.verifySteps(
+ [],
+ `the return value of onSelect controls whether or not the
+ select box will close after making the selection`
+ );
+
+ await click('button');
+
+ assert.verifySteps(['close']);
+ });
+
+ test('clicking to programmatically close', async function (assert) {
+ assert.expect(3);
+
+ await render(
+
+
+
+ {{#if dd.isOpen}}
+
+
+
+
+
+
+ {{/if}}
+
+
+ );
+
+ await click('.select-box__trigger');
+
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'true');
+
+ await click('.close');
+
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'false');
+
+ assert.ok(true, 'does not cause infinite revalidation bug');
+ });
+
+ test('closing forgets previous active option', async function (assert) {
+ assert.expect(2);
+
+ await render(
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+
+ await click('.select-box__trigger');
+ await triggerEvent('.select-box__option:nth-child(2)', 'mouseenter');
+
+ assert
+ .dom('.select-box__option:nth-child(2)')
+ .hasAttribute('aria-current', 'true');
+
+ await click('.select-box__trigger');
+
+ assert.dom('.select-box__option[aria-current="true"]').doesNotExist();
+ });
+});
diff --git a/tests/integration/components/select-box/conditional-modifier-test.gjs b/tests/integration/components/select-box/index/conditional-modifier-test.gjs
similarity index 79%
rename from tests/integration/components/select-box/conditional-modifier-test.gjs
rename to tests/integration/components/select-box/index/conditional-modifier-test.gjs
index d7d88f40..3d5ec3cf 100644
--- a/tests/integration/components/select-box/conditional-modifier-test.gjs
+++ b/tests/integration/components/select-box/index/conditional-modifier-test.gjs
@@ -12,8 +12,7 @@ module('select-box', function (hooks) {
// https://github.com/ember-modifier/ember-modifier/issues/851
const position = modifier(
- (dropdown, [container]) => (dropdown.dataset.positioned = 'true'),
- { eager: false }
+ (dropdown, [container]) => (dropdown.dataset.positioned = 'true')
);
skip('it does not blow up', async function (assert) {
@@ -29,10 +28,14 @@ module('select-box', function (hooks) {
await render(
-
-
-
-
+
+
+
+
+
+
+
+
);
diff --git a/tests/integration/components/select-box/disabling-test.gjs b/tests/integration/components/select-box/index/disabling-test.gjs
similarity index 79%
rename from tests/integration/components/select-box/disabling-test.gjs
rename to tests/integration/components/select-box/index/disabling-test.gjs
index 87665a8c..76c93729 100644
--- a/tests/integration/components/select-box/disabling-test.gjs
+++ b/tests/integration/components/select-box/index/disabling-test.gjs
@@ -32,12 +32,16 @@ module('select-box (disabling)', function (hooks) {
await render(
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
);
@@ -54,7 +58,6 @@ module('select-box (disabling)', function (hooks) {
await render(
-
@@ -80,14 +83,18 @@ module('select-box (disabling)', function (hooks) {
await render(
-
- {{#if sb.isOpen}}
-
-
-
-
-
- {{/if}}
+
+
+ {{#if dd.isOpen}}
+
+
+
+
+
+
+
+ {{/if}}
+
);
@@ -95,7 +102,7 @@ module('select-box (disabling)', function (hooks) {
await click('.select-box__option:nth-child(2)');
assert
- .dom('.select-box')
+ .dom('.select-box__dropdown')
.hasAttribute(
'data-open',
'true',
diff --git a/tests/integration/components/select-box/focus-test.gjs b/tests/integration/components/select-box/index/focus-test.gjs
similarity index 72%
rename from tests/integration/components/select-box/focus-test.gjs
rename to tests/integration/components/select-box/index/focus-test.gjs
index 5c1504a8..0922976b 100644
--- a/tests/integration/components/select-box/focus-test.gjs
+++ b/tests/integration/components/select-box/index/focus-test.gjs
@@ -27,7 +27,7 @@ module('select-box (focus)', function (hooks) {
);
- await triggerEvent('.select-box__option', 'mousedown');
+ await click('.select-box__option');
assert
.dom('.select-box__options')
@@ -48,7 +48,7 @@ module('select-box (focus)', function (hooks) {
assert.dom('.select-box__input').isNotFocused('does not steal focus');
- await triggerEvent('.select-box__option', 'mousedown');
+ await click('.select-box__option');
assert
.dom('.select-box__input')
@@ -60,16 +60,20 @@ module('select-box (focus)', function (hooks) {
await render(
-
-
-
-
+
+
+
+
+
+
+
+
);
assert.dom('.select-box__trigger').isNotFocused('does not steal focus');
- await triggerEvent('.select-box__option', 'mousedown');
+ await click('.select-box__option');
assert
.dom('.select-box__trigger')
@@ -81,11 +85,15 @@ module('select-box (focus)', function (hooks) {
await render(
-
-
-
-
-
+
+
+
+
+
+
+
+
+
);
@@ -151,10 +159,14 @@ module('select-box (focus)', function (hooks) {
await render(
-
-
-
-
+
+
+
+
+
+
+
+
);
@@ -195,14 +207,16 @@ module('select-box (focus)', function (hooks) {
await render(
-
-
+
+
+
+
);
await focus('.select-box__trigger');
- assert.dom('.select-box').hasAttribute('data-open', 'false');
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'false');
assert.dom('.select-box__trigger').hasAttribute('aria-expanded', 'false');
assert.dom('.select-box__input').hasAttribute('aria-expanded', 'false');
});
@@ -212,13 +226,17 @@ module('select-box (focus)', function (hooks) {
await render(
-
- {{#if sb.isOpen}}
-
-
-
-
- {{/if}}
+
+
+ {{#if dd.isOpen}}
+
+
+
+
+
+
+ {{/if}}
+
);
@@ -232,18 +250,22 @@ module('select-box (focus)', function (hooks) {
assert.dom('.select-box__trigger').isFocused('focus restored to trigger');
});
- test('a focused input going due to focus leaving', async function (assert) {
+ test('a focused input going away', async function (assert) {
assert.expect(3);
await render(
-
- {{#if sb.isOpen}}
-
-
-
-
- {{/if}}
+
+
+ {{#if dd.isOpen}}
+
+
+
+
+
+
+ {{/if}}
+
@@ -267,13 +289,17 @@ module('select-box (focus)', function (hooks) {
await render(
-
- {{#if sb.isOpen}}
-
-
-
-
- {{/if}}
+
+
+ {{#if dd.isOpen}}
+
+
+
+
+
+
+ {{/if}}
+
);
@@ -289,44 +315,23 @@ module('select-box (focus)', function (hooks) {
assert.dom('.select-box__trigger').isFocused('focus restored to trigger');
});
- test('a focused input going away with no trigger', async function (assert) {
- assert.expect(1);
-
- await render(
-
- {{#if sb.isOpen}}
-
-
-
-
- {{/if}}
-
- );
-
- await focus('.select-box__input');
- await click('.select-box__option');
-
- assert.dom(document.body).isFocused(`
- no attempt is made to retain focus on the trigger,
- because there isn't one.
- this is an unlikely scenario since a trigger should be
- used to open the combobox in the first place
- `);
- });
-
test('focus leaving combobox (trigger)', async function (assert) {
assert.expect(4);
await render(
-
+
+
+
);
await click('.select-box__trigger');
- assert.dom('.select-box').hasAttribute('data-open', 'true', 'precondition');
+ assert
+ .dom('.select-box__dropdown')
+ .hasAttribute('data-open', 'true', 'precondition');
focus('.outside');
@@ -336,7 +341,7 @@ module('select-box (focus)', function (hooks) {
await settled();
- assert.dom('.select-box').hasAttribute('data-open', 'false');
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'false');
assert.dom('.outside').isFocused();
});
@@ -377,8 +382,10 @@ module('select-box (focus)', function (hooks) {
await render(
-
-
+
+
+
+
);
@@ -392,19 +399,16 @@ module('select-box (focus)', function (hooks) {
});
test('focus moving to somewhere else inside the combo box (none interactive element)', async function (assert) {
- assert.expect(3);
-
- let event;
-
- const handleMouseDown = (_event) => (event = _event);
+ assert.expect(2);
await render(
- {{! template-lint-disable no-pointer-down-event-binding }}
-
-
-
-
-
+
+
+
+
+
+
+
);
@@ -412,7 +416,7 @@ module('select-box (focus)', function (hooks) {
await click('.inside');
assert
- .dom('.select-box')
+ .dom('.select-box__dropdown')
.hasAttribute(
'data-open',
'true',
@@ -425,16 +429,6 @@ module('select-box (focus)', function (hooks) {
select box will not be receptive to user actions, which is the
same as how any other native component would behave
`);
-
- // Note
- // we use tabindex="-1" on the listbox to prevent
- // keyboard-focusable-scrollers from stealing focus, but
- // this has a down side of actually allowing the listbox
- // to be focusable on click, so we must prevent that.
- // https://issues.chromium.org/issues/376718258
- // https://bugzilla.mozilla.org/show_bug.cgi?id=1930662
-
- assert.true(event.defaultPrevented);
});
test('focus moving to somewhere else inside the combo box', async function (assert) {
@@ -447,27 +441,28 @@ module('select-box (focus)', function (hooks) {
await render(
{{! template-lint-disable no-pointer-down-event-binding }}
-
-
+
+
+
+
);
await click('.select-box__trigger');
await click('.inside');
- assert.dom('.select-box').hasAttribute(
+ assert.dom('.select-box__dropdown').hasAttribute(
'data-open',
'true',
- `focus is free to move about inside an open select box
- it will not be receptive to user actions, but the interactive
- elements inside it will, and that's ok. unusual, but at least
- allows flexibility for the developer if designing a highly
- custom select box`
+ `focus is free to move about inside an open select box dropdown
+ by using the keyboard so as to not hurt a11y. but when using a
+ mouse we must maintain focus on the primary interactive element,
+ otherwise the select box will not be receptive to user input`
);
- assert.dom('.inside').isFocused();
+ assert.dom('.select-box__trigger').isFocused();
- assert.false(event.defaultPrevented);
+ assert.true(event.defaultPrevented);
});
test('focus moving to somewhere else inside the combo box then leaving', async function (assert) {
@@ -475,8 +470,10 @@ module('select-box (focus)', function (hooks) {
await render(
-
-
+
+
+
+
);
@@ -484,11 +481,11 @@ module('select-box (focus)', function (hooks) {
await click('.select-box__trigger');
await click('.inside');
- assert.dom('.select-box').hasAttribute('data-open', 'true');
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'true');
await focus('.outside');
- assert.dom('.select-box').hasAttribute('data-open', 'false');
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'false');
});
test('focusable options', async function (assert) {
@@ -557,14 +554,18 @@ module('select-box (focus)', function (hooks) {
.select-box__trigger:focus-visible { outline: 2px solid red; }
-
- {{sb.value}}
-
-
- foo
- bar
- baz
-
+
+
+ {{sb.value}}
+
+
+
+ foo
+ bar
+ baz
+
+
+
);
@@ -578,4 +579,68 @@ module('select-box (focus)', function (hooks) {
to the element using the keyboard`
);
});
+
+ test('focus out listbox forgets active option', async function (assert) {
+ assert.expect(2);
+
+ // We only show the active option when the select box has focus,
+ // because it is receptive to user input and therefore can
+ // be selected. When not focused, there is no need for it.
+ // This is the equivalent of when a select box with a dropdown
+ // (a combobox) is closed.
+
+ await render(
+
+
+ a
+ b
+ c
+
+
+ );
+
+ await focus('.select-box__options');
+ await triggerEvent('.select-box__option:nth-child(2)', 'mouseenter');
+ await triggerEvent('.select-box__options', 'mouseleave');
+
+ assert
+ .dom('.select-box__option:nth-child(2)')
+ .hasAttribute('aria-current', 'true');
+
+ await blur('.select-box__options');
+
+ assert.dom('.select-box__option[aria-current="true"]').doesNotExist();
+ });
+
+ test('keyboard-focusable-scrollers fix', async function (assert) {
+ assert.expect(1);
+
+ // we use tabindex="-1" on the listbox to prevent
+ // keyboard-focusable-scrollers from stealing focus, but
+ // this has a down side of actually allowing the listbox
+ // to be focusable on click, so we must prevent that.
+ // https://issues.chromium.org/issues/376718258
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1930662
+
+ let event;
+
+ const handleMouseDown = (_event) => (event = _event);
+
+ await render(
+ {{! template-lint-disable no-pointer-down-event-binding }}
+
+
+
+
+
+
+
+
+ );
+
+ await click('.select-box__trigger');
+ await click('.select-box__options');
+
+ assert.true(event.defaultPrevented);
+ });
});
diff --git a/tests/integration/components/select-box/in-element-test.gjs b/tests/integration/components/select-box/index/in-element-test.gjs
similarity index 55%
rename from tests/integration/components/select-box/in-element-test.gjs
rename to tests/integration/components/select-box/index/in-element-test.gjs
index 429ef5ee..ff49cb12 100644
--- a/tests/integration/components/select-box/in-element-test.gjs
+++ b/tests/integration/components/select-box/index/in-element-test.gjs
@@ -13,17 +13,21 @@ module('select-box (in-element)', function (hooks) {
await render(
-
- {{sb.value}}
-
- {{#if sb.isOpen}}
- {{#in-element (destination) insertBefore=null}}
-
-
-
-
- {{/in-element}}
- {{/if}}
+
+
+ {{sb.value}}
+
+ {{#if dd.isOpen}}
+ {{#in-element (destination) insertBefore=null}}
+
+
+
+
+
+
+ {{/in-element}}
+ {{/if}}
+
@@ -31,18 +35,15 @@ module('select-box (in-element)', function (hooks) {
);
- await triggerEvent('.select-box__trigger', 'mousedown');
+ await click('.select-box__trigger');
- assert.dom('.select-box').hasAttribute('data-open', 'true');
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'true');
assert.dom('.select-box .select-box__options').doesNotExist();
assert.dom('.destination .select-box__options').exists();
- // Implied that the mouse will leave the select box,
- // because we used in-element so the options are _outside_.
- await triggerEvent('.select-box', 'mouseleave');
- await triggerEvent('.select-box__option:nth-child(2)', 'mouseup');
+ await click('.select-box__option:nth-child(2)');
- assert.dom('.select-box').hasAttribute('data-open', 'false');
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'false');
assert.dom('.select-box__trigger').hasText('bar');
await click('.outside');
diff --git a/tests/integration/components/select-box/jump-to-option-test.gjs b/tests/integration/components/select-box/index/jump-to-option-test.gjs
similarity index 86%
rename from tests/integration/components/select-box/jump-to-option-test.gjs
rename to tests/integration/components/select-box/index/jump-to-option-test.gjs
index f6990bb5..60d4525c 100644
--- a/tests/integration/components/select-box/jump-to-option-test.gjs
+++ b/tests/integration/components/select-box/index/jump-to-option-test.gjs
@@ -47,12 +47,16 @@ module('select-box (jump to option)', function (hooks) {
await render(
-
-
- a
- b1
- b2
-
+
+
+
+
+ a
+ b1
+ b2
+
+
+
);
@@ -71,12 +75,16 @@ module('select-box (jump to option)', function (hooks) {
await render(
-
-
- a
- a1
- B
-
+
+
+
+
+ a
+ a1
+ B
+
+
+
);
@@ -95,11 +103,15 @@ module('select-box (jump to option)', function (hooks) {
await render(
-
-
- a
- b
-
+
+
+
+
+ a
+ b
+
+
+
);
@@ -118,11 +130,15 @@ module('select-box (jump to option)', function (hooks) {
await render(
-
-
- a
- b
-
+
+
+
+
+ a
+ b
+
+
+
);
@@ -253,12 +269,16 @@ module('select-box (jump to option)', function (hooks) {
await render(
-
-
- a
- b
- c
-
+
+
+
+
+ a
+ b
+ c
+
+
+
);
@@ -367,12 +387,16 @@ module('select-box (jump to option)', function (hooks) {
await render(
-
-
- a1
- a2
- a3
-
+
+
+
+
+ a1
+ a2
+ a3
+
+
+
);
@@ -392,12 +416,16 @@ module('select-box (jump to option)', function (hooks) {
await render(
-
-
- a1
- a2
- a3
-
+
+
+
+
+ a1
+ a2
+ a3
+
+
+
);
diff --git a/tests/integration/components/select-box/key-arrow-down-test.gjs b/tests/integration/components/select-box/index/key-arrow-down-test.gjs
similarity index 76%
rename from tests/integration/components/select-box/key-arrow-down-test.gjs
rename to tests/integration/components/select-box/index/key-arrow-down-test.gjs
index c3a8f12c..2e61cd2f 100644
--- a/tests/integration/components/select-box/key-arrow-down-test.gjs
+++ b/tests/integration/components/select-box/index/key-arrow-down-test.gjs
@@ -60,12 +60,16 @@ module('select-box (down arrow key)', function (hooks) {
await render(
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
);
@@ -181,7 +185,7 @@ module('select-box (down arrow key)', function (hooks) {
await focus('.select-box__options');
await triggerKeyEvent('.select-box__options', 'keydown', 'ArrowDown');
- assert.true(event.defaultPrevented);
+ assert.true(event.defaultPrevented, 'prevents window scrolling');
});
test('down scrolls the active option into view', async function (assert) {
@@ -190,19 +194,23 @@ module('select-box (down arrow key)', function (hooks) {
await render(
{{! template-lint-disable no-forbidden-elements }}
-
- {{#if sb.isOpen}}
-
- a
- b
- c
-
- {{/if}}
+
+
+ {{#if dd.isOpen}}
+
+
+ a
+ b
+ c
+
+
+ {{/if}}
+
);
@@ -213,30 +221,11 @@ module('select-box (down arrow key)', function (hooks) {
assert.strictEqual(startTop, 16);
assert.strictEqual(expectedTop, 32);
- assert.strictEqual(find('.select-box__options').scrollTop, startTop);
+ assert.strictEqual(find('.dropdown__content').scrollTop, startTop);
await triggerKeyEvent('.select-box__trigger', 'keydown', 'ArrowDown');
- assert.strictEqual(find('.select-box__options').scrollTop, expectedTop);
- });
-
- test('down on options will not open listbox', async function (assert) {
- assert.expect(1);
-
- await render(
-
-
-
-
-
-
-
- );
-
- await focus('.select-box__options');
- await triggerKeyEvent('.select-box__options', 'keydown', 'ArrowDown');
-
- assert.dom('.select-box').doesNotHaveAttribute('data-open');
+ assert.strictEqual(find('.dropdown__content').scrollTop, expectedTop);
});
test('down on trigger will open combobox', async function (assert) {
@@ -244,12 +233,16 @@ module('select-box (down arrow key)', function (hooks) {
await render(
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
);
@@ -257,29 +250,31 @@ module('select-box (down arrow key)', function (hooks) {
await triggerKeyEvent('.select-box__trigger', 'keydown', 'ArrowDown');
assert
- .dom('.select-box')
- .hasAttribute('data-open', 'true', 'opens the select box');
+ .dom('.select-box__dropdown')
+ .hasAttribute('data-open', 'true', 'opens the select box dropdown');
assert
.dom('.select-box__trigger')
.hasAttribute('aria-expanded', 'true', 'opens the combobox box');
- assert
- .dom('.select-box__option:nth-child(1)')
- .hasAttribute('aria-current', 'true', 'activates the first option');
+ assert.dom('.select-box__option[aria-current="true"]').doesNotExist();
});
test('down on trigger will open combobox (with selected value)', async function (assert) {
assert.expect(1);
await render(
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
);
@@ -291,32 +286,33 @@ module('select-box (down arrow key)', function (hooks) {
.hasAttribute('aria-current', 'true', 'the selected option is active');
});
- test('down on input will not open combobox (behaviour undefined)', async function (assert) {
- assert.expect(3);
+ test('down on input will open combobox', async function (assert) {
+ assert.expect(2);
+
+ // We can assume a Trigger is for opening/closing.
+ // But if there's a dropdown and no trigger, then its
+ // probably a custom select box, and we make no assumptions
+ // The developer should probably just render both an input
+ // and a trigger.
await render(
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
);
await focus('.select-box__input');
await triggerKeyEvent('.select-box__input', 'keydown', 'ArrowDown');
- assert.dom('.select-box').doesNotHaveAttribute('data-open', 'false');
-
- assert
- .dom('.select-box__input')
- .doesNotHaveAttribute('aria-expanded', 'false');
-
- assert
- .dom('.select-box__option:nth-child(1)')
- .hasAttribute('aria-current', 'true', 'still navigates options');
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'false');
+ assert.dom('.select-box__input').hasAttribute('aria-expanded', 'false');
});
test('up on options of a listbox after making a selection', async function (assert) {
@@ -387,14 +383,20 @@ module('select-box (down arrow key)', function (hooks) {
test('does not scroll the active option into view when closed', async function (assert) {
assert.expect(1);
+ // check this
+
await render(
-
-
- a
- b
- c
-
+
+
+
+
+ a
+ b
+ c
+
+
+
);
diff --git a/tests/integration/components/select-box/key-arrow-up-test.gjs b/tests/integration/components/select-box/index/key-arrow-up-test.gjs
similarity index 78%
rename from tests/integration/components/select-box/key-arrow-up-test.gjs
rename to tests/integration/components/select-box/index/key-arrow-up-test.gjs
index 84815d07..dd3806a1 100644
--- a/tests/integration/components/select-box/key-arrow-up-test.gjs
+++ b/tests/integration/components/select-box/index/key-arrow-up-test.gjs
@@ -65,12 +65,16 @@ module('select-box (up arrow key)', function (hooks) {
await render(
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
);
@@ -178,7 +182,7 @@ module('select-box (up arrow key)', function (hooks) {
await focus('.select-box__options');
await triggerKeyEvent('.select-box__options', 'keydown', 'ArrowUp');
- assert.true(event.defaultPrevented);
+ assert.true(event.defaultPrevented, 'prevents window scrolling');
});
test('up scrolls the active option into view', async function (assert) {
@@ -187,19 +191,23 @@ module('select-box (up arrow key)', function (hooks) {
await render(
{{! template-lint-disable no-forbidden-elements }}
-
- {{#if sb.isOpen}}
-
- a
- b
- c
-
- {{/if}}
+
+
+ {{#if dd.isOpen}}
+
+
+ a
+ b
+ c
+
+
+ {{/if}}
+
);
@@ -210,30 +218,11 @@ module('select-box (up arrow key)', function (hooks) {
assert.strictEqual(startTop, 16);
assert.strictEqual(expectedTop, 0);
- assert.strictEqual(find('.select-box__options').scrollTop, startTop);
+ assert.strictEqual(find('.dropdown__content').scrollTop, startTop);
await triggerKeyEvent('.select-box__trigger', 'keydown', 'ArrowUp');
- assert.strictEqual(find('.select-box__options').scrollTop, expectedTop);
- });
-
- test('up on options will not open listbox', async function (assert) {
- assert.expect(1);
-
- await render(
-
-
-
-
-
-
-
- );
-
- await focus('.select-box__options');
- await triggerKeyEvent('.select-box__options', 'keydown', 'ArrowUp');
-
- assert.dom('.select-box').doesNotHaveAttribute('data-open');
+ assert.strictEqual(find('.dropdown__content').scrollTop, expectedTop);
});
test('up on trigger will open combobox', async function (assert) {
@@ -241,12 +230,16 @@ module('select-box (up arrow key)', function (hooks) {
await render(
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
);
@@ -254,7 +247,7 @@ module('select-box (up arrow key)', function (hooks) {
await triggerKeyEvent('.select-box__trigger', 'keydown', 'ArrowUp');
assert
- .dom('.select-box')
+ .dom('.select-box__dropdown')
.hasAttribute('data-open', 'true', 'opens the select box');
assert
@@ -271,12 +264,16 @@ module('select-box (up arrow key)', function (hooks) {
await render(
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
);
@@ -291,25 +288,30 @@ module('select-box (up arrow key)', function (hooks) {
test('up on input will not open combobox (behaviour undefined)', async function (assert) {
assert.expect(2);
+ // We can assume a Trigger is for opening/closing.
+ // But if there's a dropdown and no trigger, then its
+ // probably a custom select box, and we make no assumptions
+ // The developer should probably just render both an input
+ // and a trigger.
+
await render(
-
-
-
-
-
-
+
+
+
+
+
+
+
+
);
await focus('.select-box__input');
await triggerKeyEvent('.select-box__input', 'keydown', 'ArrowUp');
- assert.dom('.select-box').doesNotHaveAttribute('data-open', 'false');
-
- assert
- .dom('.select-box__input')
- .doesNotHaveAttribute('aria-expanded', 'false');
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'false');
+ assert.dom('.select-box__input').hasAttribute('aria-expanded', 'false');
});
test('down on options of a listbox after making a selection', async function (assert) {
@@ -380,14 +382,20 @@ module('select-box (up arrow key)', function (hooks) {
test('does not scroll the active option into view when closed', async function (assert) {
assert.expect(1);
+ // check this
+
await render(
-
-
- a
- b
- c
-
+
+
+
+
+ a
+ b
+ c
+
+
+
);
diff --git a/tests/integration/components/select-box/key-enter-test.gjs b/tests/integration/components/select-box/index/key-enter-test.gjs
similarity index 73%
rename from tests/integration/components/select-box/key-enter-test.gjs
rename to tests/integration/components/select-box/index/key-enter-test.gjs
index 99d4e2f3..56e1b3da 100644
--- a/tests/integration/components/select-box/key-enter-test.gjs
+++ b/tests/integration/components/select-box/index/key-enter-test.gjs
@@ -27,12 +27,16 @@ module('select-box (enter)', function (hooks) {
await render(
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
);
@@ -42,7 +46,7 @@ module('select-box (enter)', function (hooks) {
assert.verifySteps([], 'change event is not fired');
assert.true(event.defaultPrevented);
- assert.dom('.select-box').hasAttribute('data-open', 'true');
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'true');
assert.dom('.select-box__trigger').hasAttribute('aria-expanded', 'true');
assert
@@ -56,7 +60,7 @@ module('select-box (enter)', function (hooks) {
assert.true(event.defaultPrevented);
assert
- .dom('.select-box')
+ .dom('.select-box__dropdown')
.hasAttribute(
'data-open',
'true',
@@ -71,21 +75,25 @@ module('select-box (enter)', function (hooks) {
await render(
-
-
-
-
+
+
+
+
+
+
+
+
);
await focus('.select-box__trigger');
await triggerKeyEvent('.select-box__trigger', 'keydown', 'Enter');
- assert.dom('.select-box').hasAttribute('data-open', 'true');
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'true');
await triggerKeyEvent('.select-box__trigger', 'keydown', 'Enter');
- assert.dom('.select-box').hasAttribute('data-open', 'true');
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'true');
});
test('enter in input of combobox', async function (assert) {
@@ -93,12 +101,16 @@ module('select-box (enter)', function (hooks) {
await render(
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
);
@@ -114,8 +126,8 @@ module('select-box (enter)', function (hooks) {
assert.verifySteps([]);
assert.true(event.defaultPrevented);
- assert.dom('.select-box').doesNotHaveAttribute('data-open');
- assert.dom('.select-box__input').doesNotHaveAttribute('aria-expanded');
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'false');
+ assert.dom('.select-box__input').hasAttribute('aria-expanded', 'false');
assert.dom('.select-box__option[aria-current="true"]').doesNotExist();
assert.dom('.select-box__option[aria-selected="true"]').doesNotExist();
@@ -123,8 +135,8 @@ module('select-box (enter)', function (hooks) {
assert.verifySteps([]);
assert.true(event.defaultPrevented);
- assert.dom('.select-box').doesNotHaveAttribute('data-open');
- assert.dom('.select-box__input').doesNotHaveAttribute('aria-expanded');
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'false');
+ assert.dom('.select-box__input').hasAttribute('aria-expanded', 'false');
assert.dom('.select-box__option[aria-current="true"]').doesNotExist();
assert.dom('.select-box__option[aria-selected="true"]').doesNotExist();
});
@@ -134,13 +146,17 @@ module('select-box (enter)', function (hooks) {
await render(
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
);
@@ -152,7 +168,7 @@ module('select-box (enter)', function (hooks) {
assert.verifySteps([]);
assert.true(event.defaultPrevented);
- assert.dom('.select-box').hasAttribute('data-open', 'true');
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'true');
assert.dom('.select-box__input').hasAttribute('aria-expanded', 'true');
assert
@@ -164,7 +180,7 @@ module('select-box (enter)', function (hooks) {
assert.verifySteps([]);
assert.true(event.defaultPrevented);
- assert.dom('.select-box').hasAttribute('data-open', 'false');
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'false');
assert.dom('.select-box__input').hasAttribute('aria-expanded', 'false');
assert
@@ -178,12 +194,16 @@ module('select-box (enter)', function (hooks) {
await render(
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
);
@@ -199,8 +219,8 @@ module('select-box (enter)', function (hooks) {
because instead, it will select the active option`
);
- assert.dom('.select-box').doesNotHaveAttribute('data-open');
- assert.dom('.select-box__input').doesNotHaveAttribute('aria-expanded');
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'false');
+ assert.dom('.select-box__input').hasAttribute('aria-expanded', 'false');
});
test('enter in listbox', async function (assert) {
@@ -277,10 +297,14 @@ module('select-box (enter)', function (hooks) {
await render(
-
-
-
-
+
+
+
+
+
+
+
+
);
diff --git a/tests/integration/components/select-box/key-escape-test.gjs b/tests/integration/components/select-box/index/key-escape-test.gjs
similarity index 58%
rename from tests/integration/components/select-box/key-escape-test.gjs
rename to tests/integration/components/select-box/index/key-escape-test.gjs
index 47f97e6c..6d160617 100644
--- a/tests/integration/components/select-box/key-escape-test.gjs
+++ b/tests/integration/components/select-box/index/key-escape-test.gjs
@@ -1,6 +1,6 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'dummy/tests/helpers';
-import { render, click, triggerKeyEvent } from '@ember/test-helpers';
+import { render, click, focus, triggerKeyEvent } from '@ember/test-helpers';
import { on } from '@ember/modifier';
import SelectBox from '@zestia/ember-select-box/components/select-box';
@@ -17,11 +17,15 @@ module('select-box (escape)', function (hooks) {
assert.expect(12);
await render(
-
-
-
- foo
-
+
+
+
+
+
+ foo
+
+
+
);
@@ -34,12 +38,12 @@ module('select-box (escape)', function (hooks) {
.hasAttribute('aria-current', 'false')
.hasAttribute('aria-selected', 'false');
- assert.dom('.select-box').hasAttribute('data-open', 'true');
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'true');
assert.dom('.select-box__trigger').hasAttribute('aria-expanded', 'true');
await triggerKeyEvent('.select-box__trigger', 'keydown', 'Escape');
- assert.dom('.select-box').hasAttribute('data-open', 'false');
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'false');
assert.dom('.select-box__trigger').hasAttribute('aria-expanded', 'false');
assert.dom('.select-box__trigger').isFocused();
@@ -55,11 +59,15 @@ module('select-box (escape)', function (hooks) {
assert.expect(12);
await render(
-
-
-
- foo
-
+
+
+
+
+
+ foo
+
+
+
);
@@ -72,12 +80,12 @@ module('select-box (escape)', function (hooks) {
.hasAttribute('aria-current', 'false')
.hasAttribute('aria-selected', 'false');
- assert.dom('.select-box').hasAttribute('data-open', 'true');
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'true');
assert.dom('.select-box__input').hasAttribute('aria-expanded', 'true');
await triggerKeyEvent('.select-box__input', 'keydown', 'Escape');
- assert.dom('.select-box').hasAttribute('data-open', 'false');
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'false');
assert.dom('.select-box__input').hasAttribute('aria-expanded', 'false');
assert.dom('.select-box__input').isFocused();
@@ -89,7 +97,34 @@ module('select-box (escape)', function (hooks) {
assert.verifySteps(['close']);
});
- test('escape inside something else escapable (e.g. a dropdown) - escape parent', async function (assert) {
+ test('escape on an interactive element', async function (assert) {
+ assert.expect(4);
+
+ // This ensures our listeners are on the dropdown itself,
+ // and not just on the trigger.
+
+ await render(
+
+
+
+
+
+
+ );
+
+ await click('.select-box__trigger');
+
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'true');
+
+ await focus('.inside');
+ await triggerKeyEvent('.select-box__dropdown', 'keydown', 'Escape');
+
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'false');
+
+ assert.verifySteps(['close']);
+ });
+
+ test('escape inside something else escapable (closed)', async function (assert) {
assert.expect(1);
let event;
@@ -99,14 +134,15 @@ module('select-box (escape)', function (hooks) {
await render(
{{! template-lint-disable no-invalid-interactive }}
-
-
-
+
+
+
+
);
- await triggerKeyEvent('.select-box__trigger', 'keydown', 'Escape');
+ await triggerKeyEvent('.select-box__dropdown', 'keydown', 'Escape');
assert.true(
event instanceof Event,
@@ -114,7 +150,7 @@ module('select-box (escape)', function (hooks) {
);
});
- test('escape inside something else escapable (e.g. a dropdown) - escape child', async function (assert) {
+ test('escape inside something else escapable (open)', async function (assert) {
assert.expect(3);
let event;
@@ -124,20 +160,21 @@ module('select-box (escape)', function (hooks) {
await render(
{{! template-lint-disable no-invalid-interactive }}
-
-
-
+
+
+
+
);
await click('.select-box__trigger');
- await triggerKeyEvent('.select-box__trigger', 'keydown', 'Escape');
+ await triggerKeyEvent('.select-box__dropdown', 'keydown', 'Escape');
assert.notOk(
event,
`event propagation is stopped, since escape has caused the select box
- to close. we don't want escape to also close the parent element (dropdown)
+ to close. we don't want escape to also close the parent element
that the select box is contained within`
);
diff --git a/tests/integration/components/select-box/key-options-test.gjs b/tests/integration/components/select-box/index/key-options-test.gjs
similarity index 86%
rename from tests/integration/components/select-box/key-options-test.gjs
rename to tests/integration/components/select-box/index/key-options-test.gjs
index 0247c1b2..d46b08e3 100644
--- a/tests/integration/components/select-box/key-options-test.gjs
+++ b/tests/integration/components/select-box/index/key-options-test.gjs
@@ -51,12 +51,16 @@ module('select-box (keydown on options)', function (hooks) {
await render(
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
);
diff --git a/tests/integration/components/select-box/key-space-test.gjs b/tests/integration/components/select-box/index/key-space-test.gjs
similarity index 70%
rename from tests/integration/components/select-box/key-space-test.gjs
rename to tests/integration/components/select-box/index/key-space-test.gjs
index 5fae871b..f19ac2d1 100644
--- a/tests/integration/components/select-box/key-space-test.gjs
+++ b/tests/integration/components/select-box/index/key-space-test.gjs
@@ -26,12 +26,16 @@ module('select-box (space)', function (hooks) {
await render(
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
);
@@ -41,7 +45,7 @@ module('select-box (space)', function (hooks) {
assert.verifySteps([], 'change event is not fired');
assert.true(event.defaultPrevented);
- assert.dom('.select-box').hasAttribute('data-open', 'true');
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'true');
assert.dom('.select-box__trigger').hasAttribute('aria-expanded', 'true');
assert
@@ -54,14 +58,10 @@ module('select-box (space)', function (hooks) {
assert.verifySteps([], 'change event is not fired');
assert.true(event.defaultPrevented);
- assert
- .dom('.select-box')
- .hasAttribute(
- 'data-open',
- 'true',
- 'does not close (no option was active)'
- );
+ // Does not close (no option was active)
+ // This is counter to a normal dropdown, which toggles.
+ assert.dom('.select-box__dropdown').hasAttribute('data-open', 'true');
assert.dom('.select-box__trigger').hasAttribute('aria-expanded', 'true');
});
@@ -70,12 +70,16 @@ module('select-box (space)', function (hooks) {
await render(
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
);
@@ -85,11 +89,13 @@ module('select-box (space)', function (hooks) {
assert.verifySteps([], 'change event is not fired');
assert.false(event.defaultPrevented, 'can type spaces still');
- assert.dom('.select-box').doesNotHaveAttribute(
- 'data-open',
- `does not open, because the space character is typed into the input
- also there is no trigger present to imply its openable`
- );
+ assert
+ .dom('.select-box__dropdown')
+ .hasAttribute(
+ 'data-open',
+ 'false',
+ 'does not open, because the space character is typed into the input'
+ );
assert
.dom('.select-box__option:nth-child(1)')
@@ -134,17 +140,21 @@ module('select-box (space)', function (hooks) {
});
test('space on trigger of combobox whilst typing', async function (assert) {
- assert.expect(10);
+ assert.expect(11);
await render(
{{! template-lint-disable no-whitespace-for-layout }}
-
-
- a
- a1
- a 2
-
+
+
+
+
+ a
+ a1
+ a 2
+
+
+
);
@@ -158,6 +168,8 @@ module('select-box (space)', function (hooks) {
assert.false(event.defaultPrevented);
+ assert.verifySteps(['A'], 'change event fires');
+
await triggerKeyEvent('.select-box__trigger', 'keydown', ' ');
assert.true(
@@ -169,14 +181,15 @@ module('select-box (space)', function (hooks) {
assert.false(event.defaultPrevented);
- assert.verifySteps(['A', 'A 2'], 'change event fires');
+ assert.verifySteps(['A 2'], 'change event fires');
- assert.dom('.select-box').hasAttribute(
+ assert.dom('.select-box__dropdown').hasAttribute(
'data-open',
'false',
`space usually toggles a select box open/closed. but in this case
- we are in the middle of jumping to an option. (this is how native
- select boxes behave).`
+ we are in the middle of jumping to an option and so that character
+ should form part of the 'search' for the option
+ (this is how native select boxes behave).`
);
assert
diff --git a/tests/integration/components/select-box/mouse-option-test.gjs b/tests/integration/components/select-box/index/mouse-option-test.gjs
similarity index 84%
rename from tests/integration/components/select-box/mouse-option-test.gjs
rename to tests/integration/components/select-box/index/mouse-option-test.gjs
index 6243a9e1..2ad9c5ec 100644
--- a/tests/integration/components/select-box/mouse-option-test.gjs
+++ b/tests/integration/components/select-box/index/mouse-option-test.gjs
@@ -99,36 +99,11 @@ module('select-box (mouseenter option)', function (hooks) {
.doesNotHaveAttribute('aria-current');
});
- test("mousing into an option activates it even if the select box doesn't have focus", async function (assert) {
- assert.expect(4);
-
- const handleActivateOption = () => assert.step('activate');
-
- await render(
-
-
-
-
-
-
- );
-
- await triggerEvent('.select-box__option:nth-child(2)', 'mouseenter');
-
- assert
- .dom('.select-box__option:nth-child(1)')
- .hasAttribute('aria-current', 'false');
-
- assert
- .dom('.select-box__option:nth-child(2)')
- .hasAttribute('aria-current', 'true');
-
- assert.verifySteps(['activate']);
- });
-
test('mousing over an option does not scroll to it', async function (assert) {
assert.expect(1);
+ // This would create annoying shift as you move the mouse.
+
await render(
{{! template-lint-disable no-forbidden-elements }}
+
+
+
+
+ {{#if dd.isOpen}}
+
+
+ One
+ Two
+ Three
+
+
+ {{/if}}
+
+
+ );
+
+ await click('.select-box__trigger');
+
+ const expectedTop = find('.select-box__option:nth-child(3)').offsetTop;
+
+ assert.strictEqual(expectedTop, 32);
+ assert.strictEqual(find('.dropdown__content').scrollTop, expectedTop);
+ });
+
+ todo('dropdown trigger not available', async function (assert) {
+ assert.expect(1);
+
+ await render(
+
+
+
+
+
+ );
+
+ assert.dom('.dropdown__trigger').doesNotExist(`
+ For valid aria, the select box implementation of the dropdown
+ trigger should be used (sb.Trigger), not (dd.Trigger)
+ Sadly, there's not that easy to prevent accidental use of this.
+ `);
+ });
+});
diff --git a/tests/integration/components/select-box/render-test.gjs b/tests/integration/components/select-box/index/render-test.gjs
similarity index 58%
rename from tests/integration/components/select-box/render-test.gjs
rename to tests/integration/components/select-box/index/render-test.gjs
index 32c6b695..1b56b63b 100644
--- a/tests/integration/components/select-box/render-test.gjs
+++ b/tests/integration/components/select-box/index/render-test.gjs
@@ -1,6 +1,6 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'dummy/tests/helpers';
-import { render } from '@ember/test-helpers';
+import { render, find } from '@ember/test-helpers';
import SelectBox from '@zestia/ember-select-box/components/select-box';
module('select-box', function (hooks) {
@@ -30,27 +30,11 @@ module('select-box', function (hooks) {
assert.dom('.select-box').hasClass('foo');
});
- test('expanded (listbox)', async function (assert) {
+ test('whitespace', async function (assert) {
assert.expect(1);
- await render(
-
-
-
- );
-
- assert.dom('.select-box').doesNotHaveAttribute('data-open');
- });
-
- test('expanded (combobox)', async function (assert) {
- assert.expect(1);
-
- await render(
-
-
-
- );
+ await render( );
- assert.dom('.select-box').hasAttribute('data-open', 'false');
+ assert.strictEqual(find('.select-box').innerHTML, '');
});
});
diff --git a/tests/integration/components/select-box/searching-test.gjs b/tests/integration/components/select-box/index/searching-test.gjs
similarity index 92%
rename from tests/integration/components/select-box/searching-test.gjs
rename to tests/integration/components/select-box/index/searching-test.gjs
index 21bef217..32b85232 100644
--- a/tests/integration/components/select-box/searching-test.gjs
+++ b/tests/integration/components/select-box/index/searching-test.gjs
@@ -19,8 +19,10 @@ module('select-box (searching)', function (hooks) {
await render(
-
-
+
+
+
+
);
@@ -104,7 +106,6 @@ module('select-box (searching)', function (hooks) {
const handleSearch = async (query, sb) => {
await deferred.promise;
- sb.open();
};
await render(
@@ -331,18 +332,22 @@ module('select-box (searching)', function (hooks) {
const handleSearch = (q) => deferred.promise;
- const handleClickInput = (sb) => sb.search('f').then(sb.open);
+ const handleClickInput = (sb) => sb.search('f').then(sb.dropdown.open);
await render(
-
- {{#if sb.isOpen}}
-
- {{#each sb.options}}
-
- {{/each}}
-
- {{/if}}
+
+
+ {{#if dd.isOpen}}
+
+
+ {{#each sb.options}}
+
+ {{/each}}
+
+
+ {{/if}}
+
);
@@ -364,11 +369,15 @@ module('select-box (searching)', function (hooks) {
-
- {{#each sb.options as |value|}}
-
- {{/each}}
-
+
+
+
+ {{#each sb.options as |value|}}
+
+ {{/each}}
+
+
+
);
diff --git a/tests/integration/components/select-box/single-test.gjs b/tests/integration/components/select-box/index/single-test.gjs
similarity index 84%
rename from tests/integration/components/select-box/single-test.gjs
rename to tests/integration/components/select-box/index/single-test.gjs
index ec780e8a..0f9e7860 100644
--- a/tests/integration/components/select-box/single-test.gjs
+++ b/tests/integration/components/select-box/index/single-test.gjs
@@ -97,8 +97,8 @@ module('select-box (single)', function (hooks) {
find('.select-box__options').scrollTop,
0,
`does not trigger scroll into view on the active option
- on initial render, we don't want to accidently cause
- pages to jump around`
+ on initial render, we don't want to accidentally cause
+ the page to jump around`
);
assert
@@ -113,7 +113,7 @@ module('select-box (single)', function (hooks) {
assert.strictEqual(
find('.select-box__options').scrollTop,
0,
- `changing the value programatically does not scroll the active
+ `changing the value programmatically does not scroll the active
option into view. scrolling into view should only happen as
a result of the user interacting with the select box`
);
@@ -141,14 +141,18 @@ module('select-box (single)', function (hooks) {
await render(
-
- {{sb.value}}
-
-
- 1
- 2
- 3
-
+
+
+ {{sb.value}}
+
+
+
+ 1
+ 2
+ 3
+
+
+
);
@@ -177,8 +181,8 @@ module('select-box (single)', function (hooks) {
.hasAttribute('aria-selected', 'true');
});
- test('onActivate doesnt fire initially', async function (assert) {
- assert.expect(1);
+ test("onActivate doesn't fire initially", async function (assert) {
+ assert.expect(2);
const handleActivate = () => assert.step('activate');
@@ -190,6 +194,8 @@ module('select-box (single)', function (hooks) {
);
+ assert.dom('.select-box__option').hasAttribute('aria-current', 'true');
+
assert.verifySteps([]);
});
@@ -243,15 +249,17 @@ module('select-box (single)', function (hooks) {
await render(
-
- {{state.value}},
- {{sb.value}}
-
-
- One
- Two
- Three
-
+
+
+ {{state.value}},
+ {{sb.value}}
+
+
+ One
+ Two
+ Three
+
+
);
@@ -289,15 +297,17 @@ module('select-box (single)', function (hooks) {
@onBuildSelection={{buildSelection}}
as |sb|
>
-
- {{state.value}},
- {{sb.value}}
-
-
- One
- Two
- Three
-
+
+
+ {{state.value}},
+ {{sb.value}}
+
+
+ One
+ Two
+ Three
+
+
);
diff --git a/tests/integration/components/select-box/value-test.gjs b/tests/integration/components/select-box/index/value-test.gjs
similarity index 97%
rename from tests/integration/components/select-box/value-test.gjs
rename to tests/integration/components/select-box/index/value-test.gjs
index df178028..cd355d89 100644
--- a/tests/integration/components/select-box/value-test.gjs
+++ b/tests/integration/components/select-box/index/value-test.gjs
@@ -44,10 +44,14 @@ module('select-box (value)', function (hooks) {
await render(
-
- {{sb.value}}
-
-
+
+
+ {{sb.value}}
+
+
+
+
+
);
diff --git a/tests/integration/components/input/render-test.gjs b/tests/integration/components/select-box/input/render-test.gjs
similarity index 76%
rename from tests/integration/components/input/render-test.gjs
rename to tests/integration/components/select-box/input/render-test.gjs
index c409a642..c2ecc5c3 100644
--- a/tests/integration/components/input/render-test.gjs
+++ b/tests/integration/components/select-box/input/render-test.gjs
@@ -1,6 +1,6 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'dummy/tests/helpers';
-import { render, setupOnerror, resetOnerror } from '@ember/test-helpers';
+import { render } from '@ember/test-helpers';
import SelectBox from '@zestia/ember-select-box/components/select-box';
module('select-box/input', function (hooks) {
@@ -47,8 +47,10 @@ module('select-box/input', function (hooks) {
await render(
-
-
+
+
+
+
);
@@ -75,24 +77,4 @@ module('select-box/input', function (hooks) {
.doesNotHaveAttribute('type', 'text')
.doesNotHaveAttribute('type', 'search');
});
-
- test('multiple inputs', async function (assert) {
- assert.expect(1);
-
- setupOnerror((error) => {
- assert.strictEqual(
- error.message,
- 'Assertion Failed: can only have 1 input'
- );
- });
-
- await render(
-
-
-
-
- );
-
- resetOnerror();
- });
});
diff --git a/tests/integration/components/select-box/opening-test.gjs b/tests/integration/components/select-box/opening-test.gjs
deleted file mode 100644
index 8d25e268..00000000
--- a/tests/integration/components/select-box/opening-test.gjs
+++ /dev/null
@@ -1,429 +0,0 @@
-import { module, test } from 'qunit';
-import { setupRenderingTest } from 'dummy/tests/helpers';
-import {
- render,
- rerender,
- click,
- focus,
- find,
- triggerEvent,
- triggerKeyEvent
-} from '@ember/test-helpers';
-import { on } from '@ember/modifier';
-import { array } from '@ember/helper';
-import { tracked } from '@glimmer/tracking';
-import SelectBox from '@zestia/ember-select-box/components/select-box';
-
-module('select-box (opening)', function (hooks) {
- setupRenderingTest(hooks);
-
- test('opening with api', async function (assert) {
- assert.expect(7);
-
- let api;
-
- const handleReady = (sb) => (api = sb);
- const handleOpen = () => assert.step('open');
-
- await render(
-
-
-
-
-
- );
-
- assert.false(api.isOpen);
-
- await click('button');
- await click('button');
-
- assert.true(api.isOpen);
-
- assert.verifySteps(['open']);
-
- assert.dom('.select-box').hasAttribute('data-open', 'true');
- assert.dom('.select-box__trigger').hasAttribute('aria-expanded', 'true');
- assert.dom('.select-box__input').hasAttribute('aria-expanded', 'true');
- });
-
- test('can set initial open state', async function (assert) {
- assert.expect(6);
-
- await render(
-
-
-
-
-
-
-
- );
-
- assert.dom('.select-box').hasAttribute('data-open', 'true');
- assert.dom('.select-box__trigger').hasAttribute('aria-expanded', 'true');
- assert.dom('.select-box__input').hasAttribute('aria-expanded', 'true');
- assert.dom('.select-box__trigger').isNotFocused();
- assert.dom('.select-box__input').isNotFocused();
- assert.dom('.select-box__option').hasAttribute('aria-current', 'true');
- });
-
- test('initial open state of combobox without a trigger (custom behaviour!)', async function (assert) {
- assert.expect(5);
-
- // The addon can't know if the combobox is one where the options are already visible,
- // or one where they will be shown by interacting with the input element. If the developer
- // had used a Trigger element, we make the assumption that it will control the expanded state.
-
- await render(
-
-
-
-
- );
-
- assert
- .dom('.select-box')
- .doesNotHaveAttribute(
- 'data-open',
- 'technically invalid, ought to have expanded attr'
- );
-
- // ...therefore, because the developer has opted out of having a Trigger button,
- // they must manually set its initial expanded state and configure a way to
- // control that expanded state, in order to be valid aria. Example:
-
- await render(
-
-
-
-
- );
-
- assert.dom('.select-box').hasAttribute('data-open', 'false');
- assert.dom('.select-box__input').hasAttribute('aria-expanded', 'false');
-
- await focus('.select-box__input');
-
- assert.dom('.select-box').hasAttribute('data-open', 'true');
- assert.dom('.select-box__input').hasAttribute('aria-expanded', 'true');
- });
-
- test('cannot manually open listbox', async function (assert) {
- assert.expect(2);
-
- const handleOpen = () => assert.step('open');
-
- await render(
-
-
-
- );
-
- assert.dom('.select-box').doesNotHaveAttribute('data-open');
- assert.verifySteps([]);
- });
-
- test('cannot open combobox manually with argument', async function (assert) {
- assert.expect(3);
-
- const state = new (class {
- @tracked isOpen;
- })();
-
- await render(
-
-
-
-
- );
-
- state.isOpen = true;
-
- await rerender();
-
- assert.dom('.select-box').hasAttribute('data-open', 'false');
- assert.dom('.select-box__trigger').hasAttribute('aria-expanded', 'false');
- assert.dom('.select-box__input').hasAttribute('aria-expanded', 'false');
- });
-
- test('activates first option (undefined === undefined)', async function (assert) {
- assert.expect(3);
-
- await render(
-
-
-
-
-
-
-
-
- );
-
- await click('.select-box__trigger');
-
- assert
- .dom('.select-box__option:nth-child(1)')
- .hasAttribute('aria-current', 'true');
-
- assert
- .dom('.select-box__option:nth-child(2)')
- .hasAttribute('aria-current', 'false');
-
- assert
- .dom('.select-box__option:nth-child(3)')
- .hasAttribute('aria-current', 'false');
- });
-
- test('activates option for value (single)', async function (assert) {
- assert.expect(1);
-
- await render(
-
-
-
-
-
-
-
-
- );
-
- await click('.select-box__trigger');
-
- assert
- .dom('.select-box__option:nth-child(2)')
- .hasAttribute('aria-current', 'true');
- });
-
- test('activating option (multiple)', async function (assert) {
- assert.expect(1);
-
- await render(
-
-
-
-
-
-
-
-
-
- );
-
- await click('.select-box__trigger');
-
- assert
- .dom('.select-box__option[aria-current="true"]')
- .doesNotExist(
- 'does not attempt to activate any of the options for the given value'
- );
- });
-
- test('activates option for value after they are rendered', async function (assert) {
- assert.expect(1);
-
- await render(
-
-
- {{#if sb.isOpen}}
-
-
-
-
-
- {{/if}}
-
- );
-
- await click('.select-box__trigger');
-
- assert
- .dom('.select-box__option:nth-child(2)')
- .hasAttribute('aria-current', 'true');
- });
-
- test('opening via the trigger does not lose focus', async function (assert) {
- assert.expect(1);
-
- await render(
-
-
-
- );
-
- await click('.select-box__trigger');
-
- assert.dom('.select-box__trigger').isFocused();
- });
-
- test('opening via the trigger advances focus to the input (mouse)', async function (assert) {
- assert.expect(1);
-
- await render(
-
-
-
-
- );
-
- await click('.select-box__trigger');
-
- assert.dom('.select-box__input').isFocused();
- });
-
- test('opening via the trigger advances focus to the input (keyboard)', async function (assert) {
- assert.expect(1);
-
- await render(
-
-
-
-
- );
-
- await focus('.select-box__trigger');
- await triggerKeyEvent('.select-box__trigger', 'keydown', 'Enter');
-
- assert.dom('.select-box__input').isFocused();
- });
-
- test('opening via the api advances focus to the input', async function (assert) {
- assert.expect(1);
-
- await render(
-
-
-
-
- );
-
- await triggerEvent('.select-box__trigger', 'mouseenter');
-
- assert.dom('.select-box__input').isFocused();
- });
-
- test('opening listbox', async function (assert) {
- assert.expect(1);
-
- const handleOpen = () => assert.step('open');
-
- await render(
-
-
-
-
- );
-
- await click('button');
-
- assert.verifySteps([], 'listboxes cannot be opened');
- });
-
- test('opening combo box with arg (trigger)', async function (assert) {
- assert.expect(1);
-
- await render(
-
-
-
- );
-
- assert.dom('.select-box__trigger').isNotFocused('does not steal focus');
- });
-
- test('opening combo box with arg (input)', async function (assert) {
- assert.expect(1);
-
- await render(
-
-
-
- );
-
- assert.dom('.select-box__input').isNotFocused('does not steal focus');
- });
-
- test('opening combobox with input only', async function (assert) {
- assert.expect(4);
-
- await render(
-
-
-
-
-
-
-
- );
-
- assert.dom('.select-box[aria-current="true"]').doesNotExist();
-
- await click('.select-box__input');
-
- assert
- .dom('.select-box__option:nth-child(1)')
- .hasAttribute('aria-current', 'true');
-
- assert.dom('.select-box').hasAttribute('data-open', 'true');
- assert.dom('.select-box__input').hasAttribute('aria-expanded', 'true');
- });
-
- test('opening with a selected value scrolls it into view', async function (assert) {
- assert.expect(2);
-
- await render(
- {{! template-lint-disable no-forbidden-elements }}
-
-
-
-
- {{#if sb.isOpen}}
-
- One
- Two
- Three
-
- {{/if}}
-
- );
-
- await click('.select-box__trigger');
-
- const expectedTop = find('.select-box__option:nth-child(3)').offsetTop;
-
- assert.strictEqual(expectedTop, 32);
- assert.strictEqual(find('.select-box__options').scrollTop, expectedTop);
- });
-
- test('opening forgets previous active option', async function (assert) {
- assert.expect(2);
-
- await render(
-
-
-
-
-
-
-
-
- );
-
- await click('.select-box__trigger');
-
- await triggerEvent('.select-box__option:nth-child(2)', 'mouseenter');
-
- assert
- .dom('.select-box__option:nth-child(2)')
- .hasAttribute('aria-current', 'true');
-
- await click('.select-box__trigger');
-
- assert.dom('.select-box__option[aria-current="true"]').doesNotExist();
- });
-});
diff --git a/tests/integration/components/option/api-test.gjs b/tests/integration/components/select-box/option/api-test.gjs
similarity index 100%
rename from tests/integration/components/option/api-test.gjs
rename to tests/integration/components/select-box/option/api-test.gjs
diff --git a/tests/integration/components/option/render-test.gjs b/tests/integration/components/select-box/option/render-test.gjs
similarity index 100%
rename from tests/integration/components/option/render-test.gjs
rename to tests/integration/components/select-box/option/render-test.gjs
diff --git a/tests/integration/components/options/render-test.gjs b/tests/integration/components/select-box/options/render-test.gjs
similarity index 75%
rename from tests/integration/components/options/render-test.gjs
rename to tests/integration/components/select-box/options/render-test.gjs
index 366f7a2a..7549d669 100644
--- a/tests/integration/components/options/render-test.gjs
+++ b/tests/integration/components/select-box/options/render-test.gjs
@@ -1,6 +1,6 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'dummy/tests/helpers';
-import { find, render, setupOnerror, resetOnerror } from '@ember/test-helpers';
+import { find, render } from '@ember/test-helpers';
import SelectBox from '@zestia/ember-select-box/components/select-box';
module('select-box/options', function (hooks) {
@@ -75,9 +75,7 @@ module('select-box/options', function (hooks) {
'-1',
`the main interactive element is the input (combobox)
focus should not move to the listbox, which is
- aria controlled by the input.
- this prevents keyboard-focusable-scrollers from stealing focus,
- since options often overflows`
+ aria controlled virtually by the input.`
);
});
@@ -86,8 +84,12 @@ module('select-box/options', function (hooks) {
await render(
-
-
+
+
+
+
+
+
);
@@ -95,8 +97,8 @@ module('select-box/options', function (hooks) {
'tabindex',
'-1',
`the main interactive element is the trigger (combobox)
- focus should not move to the listbox, which is
- aria controlled by the trigger`
+ focus should not move to the listbox (options element), which is
+ aria controlled virtually by the trigger`
);
});
@@ -155,39 +157,4 @@ module('select-box/options', function (hooks) {
.dom('.select-box__options')
.hasAttribute('aria-multiselectable', 'true');
});
-
- test('multiple listboxes', async function (assert) {
- assert.expect(1);
-
- setupOnerror((error) => {
- assert.strictEqual(
- error.message,
- 'Assertion Failed: can only have 1 listbox'
- );
- });
-
- await render(
-
-
-
-
- );
-
- resetOnerror();
- });
-
- test('no listbox', async function (assert) {
- assert.expect(1);
-
- setupOnerror((error) => {
- assert.strictEqual(
- error.message,
- 'Assertion Failed: must have an interactive element'
- );
- });
-
- await render( );
-
- resetOnerror();
- });
});
diff --git a/tests/integration/components/trigger/render-test.gjs b/tests/integration/components/trigger/render-test.gjs
deleted file mode 100644
index 3d656903..00000000
--- a/tests/integration/components/trigger/render-test.gjs
+++ /dev/null
@@ -1,171 +0,0 @@
-import { module, test } from 'qunit';
-import { setupRenderingTest } from 'dummy/tests/helpers';
-import {
- render,
- find,
- setupOnerror,
- resetOnerror,
- triggerEvent
-} from '@ember/test-helpers';
-import SelectBox from '@zestia/ember-select-box/components/select-box';
-
-module('select-box/trigger', function (hooks) {
- setupRenderingTest(hooks);
-
- test('it renders', async function (assert) {
- assert.expect(1);
-
- await render(
-
-
-
- );
-
- assert.dom('.select-box__trigger').hasTagName('div');
- });
-
- test('splattributes', async function (assert) {
- assert.expect(1);
-
- await render(
-
-
-
- );
-
- assert.dom('.select-box__trigger').hasClass('foo');
- });
-
- test('aria defaults', async function (assert) {
- assert.expect(6);
-
- await render(
-
-
-
- );
-
- assert.dom('.select-box__trigger').doesNotHaveAttribute('aria-busy');
- assert.dom('.select-box__trigger').doesNotHaveAttribute('aria-controls');
- assert.dom('.select-box__trigger').doesNotHaveAttribute('aria-disabled');
- assert.dom('.select-box__trigger').hasAttribute('aria-expanded', 'false');
- assert
- .dom('.select-box__trigger')
- .doesNotHaveAttribute('aria-activedescendant');
-
- assert
- .dom('.select-box__trigger')
- .doesNotHaveAttribute(
- 'aria-haspopup',
- 'listbox',
- 'spec says this is implicit due to role of combobox'
- );
- });
-
- test('aria controls', async function (assert) {
- assert.expect(2);
-
- await render(
-
-
-
-
- );
-
- assert.ok(
- find('.select-box__trigger')
- .getAttribute('aria-controls')
- .match(/[\w\d]+/)
- );
-
- assert
- .dom('.select-box__options')
- .hasAttribute(
- 'id',
- find('.select-box__trigger').getAttribute('aria-controls')
- );
- });
-
- test('role', async function (assert) {
- assert.expect(1);
-
- await render(
-
-
-
- );
-
- assert.dom('.select-box__trigger').hasAttribute('role', 'combobox');
- });
-
- test('role (with input)', async function (assert) {
- assert.expect(1);
-
- await render(
-
-
-
-
- );
-
- assert
- .dom('.select-box__trigger')
- .hasAttribute(
- 'role',
- 'button',
- 'the trigger is not the primary interactive element'
- );
- });
-
- test('whitespace', async function (assert) {
- assert.expect(1);
-
- await render(
-
-
-
- );
-
- assert.strictEqual(find('.select-box__trigger').innerHTML, '');
- });
-
- test('multiple triggers', async function (assert) {
- assert.expect(1);
-
- setupOnerror((error) => {
- assert.strictEqual(
- error.message,
- 'Assertion Failed: can only have 1 trigger'
- );
- });
-
- await render(
-
-
-
-
- );
-
- resetOnerror();
- });
-
- test('active descendant (with input)', async function (assert) {
- assert.expect(1);
-
- await render(
-
-
-
-
-
-
-
- );
-
- await triggerEvent('.select-box__option', 'mouseenter');
-
- assert
- .dom('.select-box__trigger')
- .doesNotHaveAttribute('aria-activedescendant');
- });
-});