diff --git a/ember-basic-dropdown/src/template-registry.ts b/ember-basic-dropdown/src/template-registry.ts index 90a4ca1a..8d9f52a9 100644 --- a/ember-basic-dropdown/src/template-registry.ts +++ b/ember-basic-dropdown/src/template-registry.ts @@ -4,10 +4,14 @@ import type BasicDropdownComponent from './components/basic-dropdown'; import type BasicDropdownWormholeComponent from './components/basic-dropdown-wormhole'; +import type BasicDropdownContentTrigger from './components/basic-dropdown-trigger'; +import type BasicDropdownContentComponent from './components/basic-dropdown-content'; import type DropdownTriggerModifier from './modifiers/basic-dropdown-trigger'; export default interface Registry { BasicDropdown: typeof BasicDropdownComponent; BasicDropdownWormhole: typeof BasicDropdownWormholeComponent; + BasicDropdownTrigger: typeof BasicDropdownContentTrigger; + BasicDropdownContent: typeof BasicDropdownContentComponent; 'basic-dropdown-trigger': typeof DropdownTriggerModifier; } diff --git a/test-app/app/components/my-custom-content.ts b/test-app/app/components/my-custom-content.ts index e6de28d4..e675f7ab 100644 --- a/test-app/app/components/my-custom-content.ts +++ b/test-app/app/components/my-custom-content.ts @@ -1,7 +1,4 @@ import templateOnly from '@ember/component/template-only'; +import type { BasicDropdownContentSignature } from 'ember-basic-dropdown/components/basic-dropdown-content'; -export interface MyCustomContentSignature { - Element: Element; -} - -export default templateOnly(); +export default templateOnly(); diff --git a/test-app/app/components/my-custom-trigger.ts b/test-app/app/components/my-custom-trigger.ts index e9db0981..363649c3 100644 --- a/test-app/app/components/my-custom-trigger.ts +++ b/test-app/app/components/my-custom-trigger.ts @@ -1,7 +1,4 @@ import templateOnly from '@ember/component/template-only'; +import type { BasicDropdownTriggerSignature } from 'ember-basic-dropdown/components/basic-dropdown-trigger'; -export interface MyCustomTriggerSignature { - Element: Element; -} - -export default templateOnly(); +export default templateOnly(); diff --git a/test-app/app/components/shadow.ts b/test-app/app/components/shadow.ts index dbc401df..83858742 100644 --- a/test-app/app/components/shadow.ts +++ b/test-app/app/components/shadow.ts @@ -25,3 +25,9 @@ export default class ShadowComponent extends Component<{ }, ); } + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + Shadow: typeof ShadowComponent; + } +} diff --git a/test-app/app/config/environment.d.ts b/test-app/app/config/environment.d.ts index a1d67fdc..5e4f4b27 100644 --- a/test-app/app/config/environment.d.ts +++ b/test-app/app/config/environment.d.ts @@ -9,6 +9,7 @@ declare const config: { locationType: 'history' | 'hash' | 'none'; rootURL: string; APP: Record; + 'ember-basic-dropdown': Record; }; export default config; diff --git a/test-app/app/controllers/application.js b/test-app/app/controllers/application.js deleted file mode 100644 index 41543925..00000000 --- a/test-app/app/controllers/application.js +++ /dev/null @@ -1,29 +0,0 @@ -import Controller from '@ember/controller'; -import { getOwner } from '@ember/owner'; - -const isFastBoot = typeof FastBoot !== 'undefined'; - -export default class extends Controller { - shadowDom = false; - - constructor() { - super(...arguments); - - const config = getOwner(this).resolveRegistration('config:environment'); - - this.shadowDom = config.APP.shadowDom ?? false; - - if (!this.shadowDom || isFastBoot) { - return; - } - - customElements.define( - 'shadow-root', - class extends HTMLElement { - connectedCallback() { - this.attachShadow({ mode: 'open' }); - } - }, - ); - } -} diff --git a/test-app/app/controllers/application.ts b/test-app/app/controllers/application.ts new file mode 100644 index 00000000..244a257d --- /dev/null +++ b/test-app/app/controllers/application.ts @@ -0,0 +1,35 @@ +import type ApplicationInstance from '@ember/application/instance'; +import Controller from '@ember/controller'; +import type Owner from '@ember/owner'; +import type environment from 'test-app/config/environment'; +import { getOwner } from '@ember/owner'; + +// @ts-expect-error Cannot find name 'FastBoot'. +const isFastBoot = typeof FastBoot !== 'undefined'; + +export default class extends Controller { + shadowDom = false; + + constructor(owner: Owner) { + super(owner); + + const config = (getOwner(this) as ApplicationInstance).resolveRegistration( + 'config:environment', + ) as typeof environment; + + this.shadowDom = (config.APP['shadowDom'] as boolean) ?? false; + + if (!this.shadowDom || isFastBoot) { + return; + } + + customElements.define( + 'shadow-root', + class extends HTMLElement { + connectedCallback() { + this.attachShadow({ mode: 'open' }); + } + }, + ); + } +} diff --git a/test-app/app/instance-initializers/shadow-root.js b/test-app/app/instance-initializers/shadow-root.ts similarity index 58% rename from test-app/app/instance-initializers/shadow-root.js rename to test-app/app/instance-initializers/shadow-root.ts index 66b68c52..205580fe 100644 --- a/test-app/app/instance-initializers/shadow-root.js +++ b/test-app/app/instance-initializers/shadow-root.ts @@ -1,17 +1,20 @@ +import type ApplicationInstance from '@ember/application/instance'; import config from 'test-app/config/environment'; // @ts-expect-error Public property 'isFastBoot' of exported class const isFastBoot = typeof FastBoot !== 'undefined'; -export function initialize(appInstance) { - if (config.environment !== 'test' || isFastBoot || !config.APP.shadowDom) { +export function initialize(appInstance: ApplicationInstance) { + if (config.environment !== 'test' || isFastBoot || !config.APP['shadowDom']) { return; } - let appRootElement = appInstance.rootElement; + let appRootElement = appInstance.rootElement as HTMLElement | null; if (typeof appRootElement === 'string') { - appRootElement = document.querySelector(appRootElement); + appRootElement = document.querySelector( + appRootElement, + ) as HTMLElement | null; } const targetElement = appRootElement || document.getElementsByTagName('body')[0]; @@ -23,11 +26,11 @@ export function initialize(appInstance) { const wormhole = document.createElement('div'); wormhole.id = 'ember-basic-dropdown-wormhole'; - hostElement.shadowRoot.appendChild(wormhole); - hostElement.shadowRoot.appendChild(rootElement); - targetElement.appendChild(hostElement); + hostElement.shadowRoot?.appendChild(wormhole); + hostElement.shadowRoot?.appendChild(rootElement); + targetElement?.appendChild(hostElement); - config.APP.rootElement = '#ember-basic-dropdown-wormhole'; + config.APP['rootElement'] = '#ember-basic-dropdown-wormhole'; appInstance.set('rootElement', rootElement); } diff --git a/test-app/tests/integration/components/basic-dropdown-test.js b/test-app/tests/integration/components/basic-dropdown-test.ts similarity index 54% rename from test-app/tests/integration/components/basic-dropdown-test.js rename to test-app/tests/integration/components/basic-dropdown-test.ts index 154daf74..95ea1f7f 100644 --- a/test-app/tests/integration/components/basic-dropdown-test.js +++ b/test-app/tests/integration/components/basic-dropdown-test.ts @@ -1,4 +1,3 @@ -import { registerDeprecationHandler } from '@ember/debug'; import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { hbs } from 'ember-cli-htmlbars'; @@ -10,26 +9,44 @@ import { waitUntil, find, settled, + type TestContext, } from '@ember/test-helpers'; - -let deprecations = []; - -registerDeprecationHandler((message, options, next) => { - deprecations.push(message); - next(message, options); -}); +import type { Dropdown, HorizontalPosition } from 'ember-basic-dropdown/types'; +import type { CalculatePosition } from 'ember-basic-dropdown/utils/calculate-position'; +import type { ComponentLike } from '@glint/template'; +import type { BasicDropdownTriggerSignature } from 'ember-basic-dropdown/components/basic-dropdown-trigger'; +import MyCustomTrigger from 'test-app/components/my-custom-trigger'; +import MyCustomContent from 'test-app/components/my-custom-content'; +import type { BasicDropdownContentSignature } from 'ember-basic-dropdown/components/basic-dropdown-content'; + +interface ExtendedTestContext extends TestContext { + element: HTMLElement; + disabled?: boolean; + isOpen?: boolean; + remoteController?: Dropdown | null; + triggerComponent?: ComponentLike; + contentComponent?: ComponentLike; + toggleDisabled: () => void; + onFocusOut: () => void; + registerAPI?: (dropdown: Dropdown | null) => void; + onOpen?: (dropdown: Dropdown, e?: Event) => boolean | void; + onClose?: (dropdown: Dropdown, e?: Event) => boolean | void; + calculatePosition?: CalculatePosition; +} + +function getRootNode(element: Element): HTMLElement { + return element.getRootNode() as HTMLElement; +} module('Integration | Component | basic-dropdown', function (hooks) { - hooks.beforeEach(() => (deprecations = [])); - setupRenderingTest(hooks); - test('Its `toggle` action opens and closes the dropdown', async function (assert) { + test('Its `toggle` action opens and closes the dropdown', async function (assert) { assert.expect(3); await render(hbs` - + {{#if dropdown.isOpen}} {{/if}} @@ -37,23 +54,27 @@ module('Integration | Component | basic-dropdown', function (hooks) { `); assert - .dom('#dropdown-is-opened', this.element.getRootNode()) + .dom('#dropdown-is-opened', getRootNode(this.element)) .doesNotExist('The dropdown is closed'); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); assert - .dom('#dropdown-is-opened', this.element.getRootNode()) + .dom('#dropdown-is-opened', getRootNode(this.element)) .exists('The dropdown is opened'); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); assert - .dom('#dropdown-is-opened', this.element.getRootNode()) + .dom('#dropdown-is-opened', getRootNode(this.element)) .doesNotExist('The dropdown is closed again'); }); - test("The click event with the right button doesn't open it", async function (assert) { + test("The click event with the right button doesn't open it", async function (assert) { assert.expect(2); await render(hbs` @@ -66,20 +87,20 @@ module('Integration | Component | basic-dropdown', function (hooks) { `); assert - .dom('#dropdown-is-opened', this.element.getRootNode()) + .dom('#dropdown-is-opened', getRootNode(this.element)) .doesNotExist('The dropdown is closed'); await triggerEvent('.ember-basic-dropdown-trigger', 'click', { button: 2 }); assert - .dom('#dropdown-is-opened', this.element.getRootNode()) + .dom('#dropdown-is-opened', getRootNode(this.element)) .doesNotExist('The dropdown is closed'); }); - test('Its `open` action opens the dropdown', async function (assert) { + test('Its `open` action opens the dropdown', async function (assert) { assert.expect(3); await render(hbs` - + {{#if dropdown.isOpen}} {{/if}} @@ -87,28 +108,32 @@ module('Integration | Component | basic-dropdown', function (hooks) { `); assert - .dom('#dropdown-is-opened', this.element.getRootNode()) + .dom('#dropdown-is-opened', getRootNode(this.element)) .doesNotExist('The dropdown is closed'); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); assert - .dom('#dropdown-is-opened', this.element.getRootNode()) + .dom('#dropdown-is-opened', getRootNode(this.element)) .exists('The dropdown is opened'); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); assert - .dom('#dropdown-is-opened', this.element.getRootNode()) + .dom('#dropdown-is-opened', getRootNode(this.element)) .exists('The dropdown is still opened'); }); - test('Its `close` action closes the dropdown', async function (assert) { + test('Its `close` action closes the dropdown', async function (assert) { assert.expect(3); await render(hbs` - + {{#if dropdown.isOpen}} {{/if}} @@ -116,26 +141,30 @@ module('Integration | Component | basic-dropdown', function (hooks) { `); assert - .dom('#dropdown-is-opened', this.element.getRootNode()) + .dom('#dropdown-is-opened', getRootNode(this.element)) .exists('The dropdown is opened'); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); assert - .dom('#dropdown-is-opened', this.element.getRootNode()) + .dom('#dropdown-is-opened', getRootNode(this.element)) .doesNotExist('The dropdown is closed'); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); assert - .dom('#dropdown-is-opened', this.element.getRootNode()) + .dom('#dropdown-is-opened', getRootNode(this.element)) .doesNotExist('The dropdown is still closed'); }); - test('It can receive an onOpen action that is fired just before the component opens', async function (assert) { + test('It can receive an onOpen action that is fired just before the component opens', async function (assert) { assert.expect(4); - this.willOpen = function (dropdown, e) { + this.onOpen = function (dropdown: Dropdown, e?: Event) { assert.false( dropdown.isOpen, 'The received dropdown has a `isOpen` property that is still false', @@ -147,9 +176,9 @@ module('Integration | Component | basic-dropdown', function (hooks) { assert.ok(!!e, 'Receives an argument as second argument'); assert.ok(true, 'onOpen action was invoked'); }; - await render(hbs` - - + await render(hbs` + + {{#if dropdown.isOpen}} {{/if}} @@ -157,20 +186,22 @@ module('Integration | Component | basic-dropdown', function (hooks) { `); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); }); - test('returning false from the `onOpen` action prevents the dropdown from opening', async function (assert) { + test('returning false from the `onOpen` action prevents the dropdown from opening', async function (assert) { assert.expect(2); - this.willOpen = function () { + this.onOpen = function () { assert.ok(true, 'willOpen has been called'); return false; }; - await render(hbs` - - + await render(hbs` + + {{#if dropdown.isOpen}} {{/if}} @@ -178,17 +209,19 @@ module('Integration | Component | basic-dropdown', function (hooks) { `); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); assert - .dom('#dropdown-is-opened', this.element.getRootNode()) + .dom('#dropdown-is-opened', getRootNode(this.element)) .doesNotExist('The dropdown is still closed'); }); - test('It can receive an onClose action that is fired when the component closes', async function (assert) { + test('It can receive an onClose action that is fired when the component closes', async function (assert) { assert.expect(7); - this.willClose = function (dropdown, e) { + this.onClose = function (dropdown, e) { assert.true( dropdown.isOpen, 'The received dropdown has a `isOpen` property and its value is `true`', @@ -200,9 +233,9 @@ module('Integration | Component | basic-dropdown', function (hooks) { assert.ok(!!e, 'Receives an argument as second argument'); assert.ok(true, 'onClose action was invoked'); }; - await render(hbs` - - + await render(hbs` + + {{#if dropdown.isOpen}} {{/if}} @@ -210,30 +243,36 @@ module('Integration | Component | basic-dropdown', function (hooks) { `); assert - .dom('#dropdown-is-opened', this.element.getRootNode()) + .dom('#dropdown-is-opened', getRootNode(this.element)) .doesNotExist('The dropdown is closed'); - await click('.ember-basic-dropdown-trigger', this.element.getRootNode()); + await click( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + ); assert - .dom('#dropdown-is-opened', this.element.getRootNode()) + .dom('#dropdown-is-opened', getRootNode(this.element)) .exists('The dropdown is opened'); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); assert - .dom('#dropdown-is-opened', this.element.getRootNode()) + .dom('#dropdown-is-opened', getRootNode(this.element)) .doesNotExist('The dropdown is now opened'); }); - test('returning false from the `onClose` action prevents the dropdown from closing', async function (assert) { + test('returning false from the `onClose` action prevents the dropdown from closing', async function (assert) { assert.expect(4); - this.willClose = function () { + this.onClose = function () { assert.ok(true, 'willClose has been invoked'); return false; }; - await render(hbs` - - + await render(hbs` + + {{#if dropdown.isOpen}} {{/if}} @@ -241,23 +280,27 @@ module('Integration | Component | basic-dropdown', function (hooks) { `); assert - .dom('#dropdown-is-opened', this.element.getRootNode()) + .dom('#dropdown-is-opened', getRootNode(this.element)) .doesNotExist('The dropdown is closed'); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); assert - .dom('#dropdown-is-opened', this.element.getRootNode()) + .dom('#dropdown-is-opened', getRootNode(this.element)) .exists('The dropdown is opened'); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); assert - .dom('#dropdown-is-opened', this.element.getRootNode()) + .dom('#dropdown-is-opened', getRootNode(this.element)) .exists('The dropdown is still opened'); }); - test('It can be rendered already opened when the `initiallyOpened=true`', async function (assert) { + test('It can be rendered already opened when the `initiallyOpened=true`', async function (assert) { assert.expect(1); await render(hbs` @@ -269,68 +312,78 @@ module('Integration | Component | basic-dropdown', function (hooks) { `); assert - .dom('#dropdown-is-opened', this.element.getRootNode()) + .dom('#dropdown-is-opened', getRootNode(this.element)) .exists('The dropdown is opened'); }); - test('Calling the `open` method while the dropdown is already opened does not call `onOpen` action', async function (assert) { + test('Calling the `open` method while the dropdown is already opened does not call `onOpen` action', async function (assert) { assert.expect(1); let onOpenCalls = 0; this.onOpen = () => { onOpenCalls++; }; - await render(hbs` + await render(hbs` - + {{#if dropdown.isOpen}} {{/if}} `); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); assert.strictEqual(onOpenCalls, 1, 'onOpen has been called only once'); }); - test('Calling the `close` method while the dropdown is already opened does not call `onOpen` action', async function (assert) { + test('Calling the `close` method while the dropdown is already opened does not call `onOpen` action', async function (assert) { assert.expect(1); let onCloseCalls = 0; - this.onFocus = (dropdown) => { - dropdown.actions.close(); - }; + this.onClose = () => { onCloseCalls++; }; - await render(hbs` + await render(hbs` - + {{#if dropdown.isOpen}} {{/if}} `); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); assert.strictEqual(onCloseCalls, 0, 'onClose was never called'); }); - test('It adds the proper class to trigger and content when it receives `@horizontalPosition="right"`', async function (assert) { + test('It adds the proper class to trigger and content when it receives `@horizontalPosition="right"`', async function (assert) { assert.expect(2); await render(hbs` @@ -341,24 +394,26 @@ module('Integration | Component | basic-dropdown', function (hooks) { `); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); assert - .dom('.ember-basic-dropdown-trigger', this.element.getRootNode()) + .dom('.ember-basic-dropdown-trigger', getRootNode(this.element)) .hasClass( 'ember-basic-dropdown-trigger--right', 'The proper class has been added', ); assert - .dom('.ember-basic-dropdown-content', this.element.getRootNode()) + .dom('.ember-basic-dropdown-content', getRootNode(this.element)) .hasClass( 'ember-basic-dropdown-content--right', 'The proper class has been added', ); }); - test('It adds the proper class to trigger and content when it receives `horizontalPosition="center"`', async function (assert) { + test('It adds the proper class to trigger and content when it receives `horizontalPosition="center"`', async function (assert) { assert.expect(2); await render(hbs` @@ -369,23 +424,25 @@ module('Integration | Component | basic-dropdown', function (hooks) { `); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); assert - .dom('.ember-basic-dropdown-trigger', this.element.getRootNode()) + .dom('.ember-basic-dropdown-trigger', getRootNode(this.element)) .hasClass( 'ember-basic-dropdown-trigger--center', 'The proper class has been added', ); assert - .dom('.ember-basic-dropdown-content', this.element.getRootNode()) + .dom('.ember-basic-dropdown-content', getRootNode(this.element)) .hasClass( 'ember-basic-dropdown-content--center', 'The proper class has been added', ); }); - test('It prefers right over left when it receives "auto-right"', async function (assert) { + test('It prefers right over left when it receives "auto-right"', async function (assert) { assert.expect(2); await render(hbs` @@ -396,23 +453,25 @@ module('Integration | Component | basic-dropdown', function (hooks) { `); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); assert - .dom('.ember-basic-dropdown-trigger', this.element.getRootNode()) + .dom('.ember-basic-dropdown-trigger', getRootNode(this.element)) .hasClass( 'ember-basic-dropdown-trigger--right', 'The proper class has been added', ); assert - .dom('.ember-basic-dropdown-content', this.element.getRootNode()) + .dom('.ember-basic-dropdown-content', getRootNode(this.element)) .hasClass( 'ember-basic-dropdown-content--right', 'The proper class has been added', ); }); - test('It adds the proper class to trigger and content when it receives `verticalPosition="above"`', async function (assert) { + test('It adds the proper class to trigger and content when it receives `verticalPosition="above"`', async function (assert) { assert.expect(2); await render(hbs` @@ -423,23 +482,25 @@ module('Integration | Component | basic-dropdown', function (hooks) { `); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); assert - .dom('.ember-basic-dropdown-trigger', this.element.getRootNode()) + .dom('.ember-basic-dropdown-trigger', getRootNode(this.element)) .hasClass( 'ember-basic-dropdown-trigger--above', 'The proper class has been added', ); assert - .dom('.ember-basic-dropdown-content', this.element.getRootNode()) + .dom('.ember-basic-dropdown-content', getRootNode(this.element)) .hasClass( 'ember-basic-dropdown-content--above', 'The proper class has been added', ); }); - test('It passes the `renderInPlace` property to the yielded content component', async function (assert) { + test('It passes the `renderInPlace` property to the yielded content component', async function (assert) { assert.expect(1); await render(hbs` @@ -450,14 +511,16 @@ module('Integration | Component | basic-dropdown', function (hooks) { `); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); assert - .dom('.ember-basic-dropdown-content', this.element.getRootNode()) + .dom('.ember-basic-dropdown-content', getRootNode(this.element)) .exists('The dropdown is rendered in place'); }); - test('It adds a special class to both trigger and content when `@renderInPlace={{true}}`', async function (assert) { + test('It adds a special class to both trigger and content when `@renderInPlace={{true}}`', async function (assert) { assert.expect(2); await render(hbs` @@ -468,23 +531,25 @@ module('Integration | Component | basic-dropdown', function (hooks) { `); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); assert - .dom('.ember-basic-dropdown-trigger', this.element.getRootNode()) + .dom('.ember-basic-dropdown-trigger', getRootNode(this.element)) .hasClass( 'ember-basic-dropdown-trigger--in-place', 'The trigger has a special `--in-place` class', ); assert - .dom('.ember-basic-dropdown-content', this.element.getRootNode()) + .dom('.ember-basic-dropdown-content', getRootNode(this.element)) .hasClass( 'ember-basic-dropdown-content--in-place', 'The content has a special `--in-place` class', ); }); - test('When rendered in-place, the content still contains the --above/below classes', async function (assert) { + test('When rendered in-place, the content still contains the --above/below classes', async function (assert) { assert.expect(2); await render(hbs` @@ -495,10 +560,12 @@ module('Integration | Component | basic-dropdown', function (hooks) { `); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); assert - .dom('.ember-basic-dropdown-content', this.element.getRootNode()) + .dom('.ember-basic-dropdown-content', getRootNode(this.element)) .hasClass( 'ember-basic-dropdown-content--below', 'The content has a class indicating that it was placed below the trigger', @@ -512,17 +579,19 @@ module('Integration | Component | basic-dropdown', function (hooks) { `); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); assert - .dom('.ember-basic-dropdown-content', this.element.getRootNode()) + .dom('.ember-basic-dropdown-content', getRootNode(this.element)) .hasClass( 'ember-basic-dropdown-content--above', 'The content has a class indicating that it was placed above the trigger', ); }); - test('It adds a wrapper element when `@renderInPlace={{true}}`', async function (assert) { + test('It adds a wrapper element when `@renderInPlace={{true}}`', async function (assert) { assert.expect(2); await render(hbs` @@ -533,18 +602,20 @@ module('Integration | Component | basic-dropdown', function (hooks) { `); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); - assert.dom('.ember-basic-dropdown', this.element.getRootNode()).exists(); + assert.dom('.ember-basic-dropdown', getRootNode(this.element)).exists(); assert - .dom('.ember-basic-dropdown-trigger', this.element.getRootNode()) + .dom('.ember-basic-dropdown-trigger', getRootNode(this.element)) .hasClass( 'ember-basic-dropdown-trigger--in-place', 'The trigger has a special `--in-place` class', ); }); - test('When rendered in-place, it prefers right over left with position "auto-right"', async function (assert) { + test('When rendered in-place, it prefers right over left with position "auto-right"', async function (assert) { assert.expect(2); await render(hbs` @@ -555,23 +626,25 @@ module('Integration | Component | basic-dropdown', function (hooks) { `); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); assert - .dom('.ember-basic-dropdown-trigger', this.element.getRootNode()) + .dom('.ember-basic-dropdown-trigger', getRootNode(this.element)) .hasClass( 'ember-basic-dropdown-trigger--right', 'The proper class has been added', ); assert - .dom('.ember-basic-dropdown-content', this.element.getRootNode()) + .dom('.ember-basic-dropdown-content', getRootNode(this.element)) .hasClass( 'ember-basic-dropdown-content--right', 'The proper class has been added', ); }); - test('When rendered in-place, it applies right class for position "right"', async function (assert) { + test('When rendered in-place, it applies right class for position "right"', async function (assert) { assert.expect(2); await render(hbs` @@ -582,23 +655,25 @@ module('Integration | Component | basic-dropdown', function (hooks) { `); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); assert - .dom('.ember-basic-dropdown-trigger', this.element.getRootNode()) + .dom('.ember-basic-dropdown-trigger', getRootNode(this.element)) .hasClass( 'ember-basic-dropdown-trigger--right', 'The proper class has been added', ); assert - .dom('.ember-basic-dropdown-content', this.element.getRootNode()) + .dom('.ember-basic-dropdown-content', getRootNode(this.element)) .hasClass( 'ember-basic-dropdown-content--right', 'The proper class has been added', ); }); - test('[ISSUE #127] Having more than one dropdown with `@renderInPlace={{true}}` raises an exception', async function (assert) { + test('[ISSUE #127] Having more than one dropdown with `@renderInPlace={{true}}` raises an exception', async function (assert) { assert.expect(1); await render(hbs` @@ -609,10 +684,10 @@ module('Integration | Component | basic-dropdown', function (hooks) { assert.ok(true, 'The test has run without errors'); }); - test('It passes the `disabled` property as part of the public API, and updates is if it changes', async function (assert) { + test('It passes the `disabled` property as part of the public API, and updates is if it changes', async function (assert) { assert.expect(2); this.disabled = true; - await render(hbs` + await render(hbs` {{#if dropdown.disabled}}
Disabled!
@@ -623,17 +698,16 @@ module('Integration | Component | basic-dropdown', function (hooks) { `); assert - .dom('#disabled-dropdown-marker', this.element.getRootNode()) + .dom('#disabled-dropdown-marker', getRootNode(this.element)) .exists('The public API of the component is marked as disabled'); this.set('disabled', false); assert - .dom('#enabled-dropdown-marker', this.element.getRootNode()) + .dom('#enabled-dropdown-marker', getRootNode(this.element)) .exists('The public API of the component is marked as enabled'); }); - test('It passes the `uniqueId` property as part of the public API', async function (assert) { + test('It passes the `uniqueId` property as part of the public API', async function (assert) { assert.expect(1); - this.disabled = true; await render(hbs` @@ -642,41 +716,43 @@ module('Integration | Component | basic-dropdown', function (hooks) { `); assert - .dom('#dropdown-unique-id-container', this.element.getRootNode()) + .dom('#dropdown-unique-id-container', getRootNode(this.element)) .hasText(/ember\d+/, 'It yields the uniqueId'); }); - test("If the dropdown gets disabled while it's open, it closes automatically", async function (assert) { + test("If the dropdown gets disabled while it's open, it closes automatically", async function (assert) { assert.expect(2); - this.isDisabled = false; - await render(hbs` - + this.disabled = false; + await render(hbs` + Click me `); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); assert - .dom('#dropdown-is-opened', this.element.getRootNode()) + .dom('#dropdown-is-opened', getRootNode(this.element)) .exists('The select is open'); - this.set('isDisabled', true); + this.set('disabled', true); assert - .dom('#dropdown-is-opened', this.element.getRootNode()) + .dom('#dropdown-is-opened', getRootNode(this.element)) .doesNotExist('The select is now closed'); }); - test("If the component's `disabled` property changes, the `registerAPI` action is called", async function (assert) { + test("If the component's `disabled` property changes, the `registerAPI` action is called", async function (assert) { assert.expect(3); - this.isDisabled = false; - this.toggleDisabled = () => this.toggleProperty('isDisabled'); + this.disabled = false; + this.toggleDisabled = () => this.set('disabled', this.disabled); this.registerAPI = (api) => this.set('remoteController', api); - await render(hbs` - + await render(hbs` + Click me @@ -686,22 +762,24 @@ module('Integration | Component | basic-dropdown', function (hooks) { `); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); assert - .dom('#is-disabled', this.element.getRootNode()) + .dom('#is-disabled', getRootNode(this.element)) .doesNotExist('The select is enabled'); - this.set('isDisabled', true); + this.set('disabled', true); assert - .dom('#is-disabled', this.element.getRootNode()) + .dom('#is-disabled', getRootNode(this.element)) .exists('The select is disabled'); - this.set('isDisabled', false); + this.set('disabled', false); assert - .dom('#is-disabled', this.element.getRootNode()) + .dom('#is-disabled', getRootNode(this.element)) .doesNotExist('The select is enabled again'); }); - test('It can receive `@destination="id-of-elmnt"` to customize where `#-in-element` is going to render the content', async function (assert) { + test('It can receive `@destination="id-of-elmnt"` to customize where `#-in-element` is going to render the content', async function (assert) { assert.expect(1); await render(hbs` @@ -712,14 +790,18 @@ module('Integration | Component | basic-dropdown', function (hooks) {
`); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); assert .dom( - this.element - .getRootNode() - .querySelector('.ember-basic-dropdown-content').parentNode, + ( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-content', + ) as HTMLElement + ).parentNode as HTMLElement, ) .hasAttribute( 'id', @@ -729,7 +811,7 @@ module('Integration | Component | basic-dropdown', function (hooks) { }); // A11y - test('By default, the `aria-controls` attribute of the trigger contains the id of the content', async function (assert) { + test('By default, the `aria-controls` attribute of the trigger contains the id of the content', async function (assert) { assert.expect(1); await render(hbs` @@ -739,13 +821,15 @@ module('Integration | Component | basic-dropdown', function (hooks) {
`); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); - let content = this.element - .getRootNode() - .querySelector('.ember-basic-dropdown-content'); + const content = getRootNode(this.element).querySelector( + '.ember-basic-dropdown-content', + ) as HTMLElement; assert - .dom('.ember-basic-dropdown-trigger', this.element.getRootNode()) + .dom('.ember-basic-dropdown-trigger', getRootNode(this.element)) .hasAttribute( 'aria-controls', content.id, @@ -753,7 +837,7 @@ module('Integration | Component | basic-dropdown', function (hooks) { ); }); - test('When opened, the `aria-owns` attribute of the trigger parent contains the id of the content', async function (assert) { + test('When opened, the `aria-owns` attribute of the trigger parent contains the id of the content', async function (assert) { assert.expect(2); await render(hbs` @@ -761,23 +845,25 @@ module('Integration | Component | basic-dropdown', function (hooks) { `); - let trigger = this.element - .getRootNode() - .querySelector('.ember-basic-dropdown-trigger'); + const trigger = getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement; assert - .dom(trigger.parentNode) + .dom(trigger.parentNode as HTMLElement) .doesNotHaveAttribute( 'aria-owns', 'Closed dropdown parent does not have aria-owns', ); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); - let content = this.element - .getRootNode() - .querySelector('.ember-basic-dropdown-content'); + const content = getRootNode(this.element).querySelector( + '.ember-basic-dropdown-content', + ) as HTMLElement; assert - .dom(trigger.parentNode) + .dom(trigger.parentNode as HTMLElement) .hasAttribute( 'aria-owns', content.id, @@ -786,37 +872,41 @@ module('Integration | Component | basic-dropdown', function (hooks) { }); // Repositioning - test('The `reposition` public action returns an object with the changes', async function (assert) { + test('The `reposition` public action returns an object with the changes', async function (assert) { assert.expect(4); - let remoteController; - this.saveAPI = (api) => (remoteController = api); + let remoteController: Dropdown | null = null; + this.registerAPI = (api) => (remoteController = api); - await render(hbs` - + await render(hbs` + Click me `); - let returnValue; + await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); - returnValue = remoteController.actions.reposition(); + const returnValue = ( + remoteController as unknown as Dropdown + )?.actions.reposition(); assert.ok(Object.prototype.hasOwnProperty.call(returnValue, 'hPosition')); assert.ok(Object.prototype.hasOwnProperty.call(returnValue, 'vPosition')); assert.ok(Object.prototype.hasOwnProperty.call(returnValue, 'top')); assert.ok(Object.prototype.hasOwnProperty.call(returnValue, 'left')); }); - test('The user can pass a custom `calculatePosition` function to customize how the component is placed on the screen', async function (assert) { + test('The user can pass a custom `calculatePosition` function to customize how the component is placed on the screen', async function (assert) { assert.expect(4); this.calculatePosition = function ( - triggerElement, - dropdownElement, - destinationElement, + _triggerElement, + _dropdownElement, + _destinationElement, { dropdown }, ) { assert.ok(dropdown, 'dropdown should be passed to the component'); @@ -830,7 +920,7 @@ module('Integration | Component | basic-dropdown', function (hooks) { }, }; }; - await render(hbs` + await render(hbs` Click me @@ -839,19 +929,21 @@ module('Integration | Component | basic-dropdown', function (hooks) { `); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); assert - .dom('.ember-basic-dropdown-content', this.element.getRootNode()) + .dom('.ember-basic-dropdown-content', getRootNode(this.element)) .hasClass('ember-basic-dropdown-content--above', 'The dropdown is above'); assert - .dom('.ember-basic-dropdown-content', this.element.getRootNode()) + .dom('.ember-basic-dropdown-content', getRootNode(this.element)) .hasClass( 'ember-basic-dropdown-content--right', 'The dropdown is in the right', ); assert - .dom('.ember-basic-dropdown-content', this.element.getRootNode()) + .dom('.ember-basic-dropdown-content', getRootNode(this.element)) .hasAttribute( 'style', 'top: 111px; width: 100px; height: 110px;', @@ -859,12 +951,12 @@ module('Integration | Component | basic-dropdown', function (hooks) { ); }); - test('The user can use the `renderInPlace` flag option to modify how the position is calculated in the `calculatePosition` function', async function (assert) { + test('The user can use the `renderInPlace` flag option to modify how the position is calculated in the `calculatePosition` function', async function (assert) { assert.expect(4); this.calculatePosition = function ( - triggerElement, - dropdownElement, - destinationElement, + _triggerElement, + _dropdownElement, + _destinationElement, { dropdown, renderInPlace }, ) { assert.ok(dropdown, 'dropdown should be passed to the component'); @@ -880,7 +972,7 @@ module('Integration | Component | basic-dropdown', function (hooks) { } else { return { horizontalPosition: 'left', - verticalPosition: 'bottom', + verticalPosition: 'below', style: { top: 333, right: 444, @@ -888,7 +980,7 @@ module('Integration | Component | basic-dropdown', function (hooks) { }; } }; - await render(hbs` + await render(hbs` Click me @@ -897,19 +989,21 @@ module('Integration | Component | basic-dropdown', function (hooks) { `); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); assert - .dom('.ember-basic-dropdown-content', this.element.getRootNode()) + .dom('.ember-basic-dropdown-content', getRootNode(this.element)) .hasClass('ember-basic-dropdown-content--above', 'The dropdown is above'); assert - .dom('.ember-basic-dropdown-content', this.element.getRootNode()) + .dom('.ember-basic-dropdown-content', getRootNode(this.element)) .hasClass( 'ember-basic-dropdown-content--right', 'The dropdown is in the right', ); assert - .dom('.ember-basic-dropdown-content', this.element.getRootNode()) + .dom('.ember-basic-dropdown-content', getRootNode(this.element)) .hasStyle( { top: '111px', right: '222px' }, 'The style attribute is the expected one', @@ -917,47 +1011,55 @@ module('Integration | Component | basic-dropdown', function (hooks) { }); // Customization of inner components - test('It allows to customize the trigger passing `@triggerComponent="my-custom-trigger"`', async function (assert) { + test('It allows to customize the trigger passing `@triggerComponent="my-custom-trigger"`', async function (assert) { assert.expect(1); - await render(hbs` - + this.triggerComponent = MyCustomTrigger; + + await render(hbs` + Press me

Content of the dropdown

`); assert - .dom('#my-custom-trigger', this.element.getRootNode()) + .dom('#my-custom-trigger', getRootNode(this.element)) .exists('The custom component has been rendered'); }); - test('It allows to customize the content passing `contentComponent="my-custom-content"`', async function (assert) { + test('It allows to customize the content passing `contentComponent="my-custom-content"`', async function (assert) { assert.expect(1); - await render(hbs` - + this.contentComponent = MyCustomContent; + + await render(hbs` + Press me

Content of the dropdown

`); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); assert - .dom('#my-custom-content', this.element.getRootNode()) + .dom('#my-custom-content', getRootNode(this.element)) .exists('The custom component has been rendered'); }); // State replacement - test('The registerAPI is called with every mutation of the publicAPI object', async function (assert) { + test('The registerAPI is called with every mutation of the publicAPI object', async function (assert) { assert.expect(7); - let apis = []; + const apis: Dropdown[] = []; this.disabled = false; this.registerAPI = function (api) { - apis.push(api); + if (api) { + apis.push(api); + } }; - await render(hbs` + await render(hbs` Open me

Content of the dropdown

@@ -965,26 +1067,33 @@ module('Integration | Component | basic-dropdown', function (hooks) { `); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); assert.strictEqual( apis.length, 3, 'There have been 3 changes in the state of the public API', ); - assert.false(apis[0].isOpen, 'The component was closed'); - assert.true(apis[1].isOpen, 'Then it opened'); - assert.false(apis[2].isOpen, 'Then it closed again'); + assert.false(apis[0] ? apis[0].isOpen : true, 'The component was closed'); + assert.true(apis[1] ? apis[1].isOpen : false, 'Then it opened'); + assert.false(apis[2] ? apis[2].isOpen : true, 'Then it closed again'); this.set('disabled', true); assert.strictEqual(apis.length, 4, 'There have been 4 changes now'); - assert.false(apis[2].disabled, 'the component was enabled'); - assert.true(apis[3].disabled, 'and it became disabled'); + assert.false( + apis[2] ? apis[2].disabled : true, + 'the component was enabled', + ); + assert.true(apis[3] ? apis[3].disabled : false, 'and it became disabled'); }); - test('removing the dropdown in response to onClose does not error', async function (assert) { + test('removing the dropdown in response to onClose does not error', async function (assert) { assert.expect(2); this.isOpen = true; @@ -993,7 +1102,7 @@ module('Integration | Component | basic-dropdown', function (hooks) { this.set('isOpen', false); }; - await render(hbs` + await render(hbs` {{#if this.isOpen}} Open me @@ -1003,20 +1112,24 @@ module('Integration | Component | basic-dropdown', function (hooks) { `); assert - .dom('.ember-basic-dropdown-trigger', this.element.getRootNode()) + .dom('.ember-basic-dropdown-trigger', getRootNode(this.element)) .exists('the dropdown is rendered'); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); assert - .dom('.ember-basic-dropdown-trigger', this.element.getRootNode()) + .dom('.ember-basic-dropdown-trigger', getRootNode(this.element)) .doesNotExist('the dropdown has been removed'); }); - test('Dropdowns can be infinitely nested, clicking in children will not close parents, clicking in parents closes children', async function (assert) { + test('Dropdowns can be infinitely nested, clicking in children will not close parents, clicking in parents closes children', async function (assert) { assert.expect(12); await render(hbs` @@ -1051,77 +1164,85 @@ module('Integration | Component | basic-dropdown', function (hooks) { //open the nested dropdown await click( - this.element - .getRootNode() - .querySelector('.ember-basic-dropdown-trigger.parent'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger.parent', + ) as HTMLElement, ); assert - .dom('.body-parent', this.element.getRootNode()) + .dom('.body-parent', getRootNode(this.element)) .exists('the parent dropdown is rendered'); await click( - this.element - .getRootNode() - .querySelector('.ember-basic-dropdown-trigger.child'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger.child', + ) as HTMLElement, ); assert - .dom('.body-child', this.element.getRootNode()) + .dom('.body-child', getRootNode(this.element)) .exists('the child dropdown is rendered'); await click( - this.element - .getRootNode() - .querySelector('.ember-basic-dropdown-trigger.grandchild'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger.grandchild', + ) as HTMLElement, ); assert - .dom('.body-grandchild', this.element.getRootNode()) + .dom('.body-grandchild', getRootNode(this.element)) .exists('the grandchild dropdown is rendered'); // click in the grandchild dropdown - await click(this.element.getRootNode().querySelector('.body-grandchild')); + await click( + getRootNode(this.element).querySelector( + '.body-grandchild', + ) as HTMLElement, + ); assert - .dom('.body-grandchild', this.element.getRootNode()) + .dom('.body-grandchild', getRootNode(this.element)) .exists('can click in grandchild dropdown and still be open'); assert - .dom('.body-child', this.element.getRootNode()) + .dom('.body-child', getRootNode(this.element)) .exists('can click in grandchild dropdown and still be open'); assert - .dom('.body-parent', this.element.getRootNode()) + .dom('.body-parent', getRootNode(this.element)) .exists('can click in grandchild dropdown and still be open'); // click in the child dropdown - await click(this.element.getRootNode().querySelector('.body-child')); + await click( + getRootNode(this.element).querySelector('.body-child') as HTMLElement, + ); assert - .dom('.body-grandchild', this.element.getRootNode()) + .dom('.body-grandchild', getRootNode(this.element)) .doesNotExist( 'grandchild dropdown should not exist becuase we clicked in child', ); assert - .dom('.body-child', this.element.getRootNode()) + .dom('.body-child', getRootNode(this.element)) .exists('can click in child dropdown and still be open'); assert - .dom('.body-parent', this.element.getRootNode()) + .dom('.body-parent', getRootNode(this.element)) .exists('can click in child dropdown and still be open'); // click in the parent dropdown - await click(this.element.getRootNode().querySelector('.body-parent')); + await click( + getRootNode(this.element).querySelector('.body-parent') as HTMLElement, + ); assert - .dom('.body-grandchild', this.element.getRootNode()) + .dom('.body-grandchild', getRootNode(this.element)) .doesNotExist( 'grandchild dropdown should not exist becuase we clicked in parent', ); assert - .dom('.body-child', this.element.getRootNode()) + .dom('.body-child', getRootNode(this.element)) .doesNotExist( 'child dropdown should not exist becuase we clicked in parent', ); assert - .dom('.body-parent', this.element.getRootNode()) + .dom('.body-parent', getRootNode(this.element)) .exists('can click in parent dropdown and still be open'); }); // Misc bugfixes - test('[BUGFIX] Dropdowns rendered in place do not register events twice', async function (assert) { + test('[BUGFIX] Dropdowns rendered in place do not register events twice', async function (assert) { assert.expect(2); let called = false; this.onFocusOut = function () { @@ -1131,7 +1252,7 @@ module('Integration | Component | basic-dropdown', function (hooks) { this.onOpen = function () { assert.ok(true); }; - await render(hbs` + await render(hbs` Open me @@ -1139,54 +1260,68 @@ module('Integration | Component | basic-dropdown', function (hooks) { `); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + ); + await focus( + getRootNode(this.element).querySelector('#inner-input') as HTMLElement, + ); + await focus( + getRootNode(this.element).querySelector('#outer-input') as HTMLElement, ); - await focus(this.element.getRootNode().querySelector('#inner-input')); - await focus(this.element.getRootNode().querySelector('#outer-input')); }); - test('[BUGFIX] It protects the inline styles from undefined values returned in the `calculatePosition` callback', async function (assert) { + test('[BUGFIX] It protects the inline styles from undefined values returned in the `calculatePosition` callback', async function (assert) { assert.expect(1); this.calculatePosition = function () { return { + horizontalPosition: 'auto', + verticalPosition: 'auto', style: {}, }; }; - await render(hbs` + await render(hbs` Open me Some content `); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); assert - .dom('.ember-basic-dropdown-content', this.element.getRootNode()) + .dom('.ember-basic-dropdown-content', getRootNode(this.element)) .doesNotHaveAttribute('style'); }); - test('It includes the inline styles returned from the `calculatePosition` callback', async function (assert) { + test('It includes the inline styles returned from the `calculatePosition` callback', async function (assert) { assert.expect(1); this.calculatePosition = function () { return { + horizontalPosition: 'auto', + verticalPosition: 'auto', style: { 'max-height': '500px', 'overflow-y': 'auto', }, }; }; - await render(hbs` + await render(hbs` Open me Some content `); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); assert - .dom('.ember-basic-dropdown-content', this.element.getRootNode()) + .dom('.ember-basic-dropdown-content', getRootNode(this.element)) .hasAttribute('style', /max-height: 500px; overflow-y: auto/); }); @@ -1195,7 +1330,7 @@ module('Integration | Component | basic-dropdown', function (hooks) { * Just in case animationEnabled on TEST ENV, this test would cover this change */ - test.skip('[BUGFIX] Dropdowns rendered in place have correct animation flow', async function (assert) { + test.skip('[BUGFIX] Dropdowns rendered in place have correct animation flow', async function (assert) { assert.expect(4); const basicDropdownContentClass = 'ember-basic-dropdown-content'; @@ -1222,7 +1357,9 @@ module('Integration | Component | basic-dropdown', function (hooks) { `); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); assert @@ -1233,13 +1370,15 @@ module('Integration | Component | basic-dropdown', function (hooks) { ); await waitUntil(() => - find('.ember-basic-dropdown-content').classList.contains( + find('.ember-basic-dropdown-content')?.classList.contains( transitionedInClass, ), ); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); assert @@ -1250,7 +1389,9 @@ module('Integration | Component | basic-dropdown', function (hooks) { ); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); assert @@ -1261,13 +1402,15 @@ module('Integration | Component | basic-dropdown', function (hooks) { ); await waitUntil(() => - find('.ember-basic-dropdown-content').classList.contains( + find('.ember-basic-dropdown-content')?.classList.contains( transitionedInClass, ), ); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); assert @@ -1279,22 +1422,22 @@ module('Integration | Component | basic-dropdown', function (hooks) { }); // Styles properly get reset - test('Styles properly get reset if the positioning changes while open', async function (assert) { + test('Styles properly get reset if the positioning changes while open', async function (assert) { assert.expect(4); - let publicApi; + let publicApi: Dropdown | null = null; this.registerAPI = (api) => (publicApi = api); let timesCalled = 0; this.calculatePosition = function () { - const style = { + const style: Record = { top: 111, }; - let horizontalPosition; + let horizontalPosition: HorizontalPosition; if (timesCalled % 2 === 1) { - style.right = 100; + style['right'] = 100; horizontalPosition = 'right'; } else { - style.left = 100; + style['left'] = 100; horizontalPosition = 'left'; } @@ -1307,7 +1450,7 @@ module('Integration | Component | basic-dropdown', function (hooks) { }; }; - await render(hbs` + await render(hbs` Click me @@ -1317,36 +1460,38 @@ module('Integration | Component | basic-dropdown', function (hooks) { `); await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, ); assert - .dom('.ember-basic-dropdown-content', this.element.getRootNode()) + .dom('.ember-basic-dropdown-content', getRootNode(this.element)) .hasStyle( { top: '111px', left: '100px' }, 'The style attribute is the expected one', ); assert - .dom('.ember-basic-dropdown-content', this.element.getRootNode()) + .dom('.ember-basic-dropdown-content', getRootNode(this.element)) .doesNotHaveStyle( { right: '100px' }, 'The style attribute is the expected one', ); - publicApi.actions.reposition(); + (publicApi as unknown as Dropdown).actions.reposition(); await settled(); assert - .dom('.ember-basic-dropdown-content', this.element.getRootNode()) + .dom('.ember-basic-dropdown-content', getRootNode(this.element)) .hasStyle( { top: '111px', right: '100px' }, 'The style attribute is the expected one', ); assert - .dom('.ember-basic-dropdown-content', this.element.getRootNode()) + .dom('.ember-basic-dropdown-content', getRootNode(this.element)) .doesNotHaveStyle( { left: '100px' }, 'The style attribute is the expected one', @@ -1354,12 +1499,12 @@ module('Integration | Component | basic-dropdown', function (hooks) { }); // Shadow dom test - test('Shadow dom: Its `toggle` action opens and closes the dropdown', async function (assert) { + test('Shadow dom: Its `toggle` action opens and closes the dropdown', async function (assert) { const wormhole = document.createElement('div'); wormhole.id = 'ember-basic-dropdown-wormhole'; - document.getElementById('ember-testing').appendChild(wormhole); + document.getElementById('ember-testing')?.appendChild(wormhole); - await render(hbs` + await render(hbs` Click me @@ -1371,14 +1516,16 @@ module('Integration | Component | basic-dropdown', function (hooks) { `); assert - .dom('#dropdown-is-opened', this.element.getRootNode()) + .dom('#dropdown-is-opened', getRootNode(this.element)) .doesNotExist('The dropdown is closed'); - const triggerElement = find('[data-shadow]')?.shadowRoot.querySelector( + const triggerElement = find('[data-shadow]')?.shadowRoot?.querySelector( '.ember-basic-dropdown-trigger', ); - await click(triggerElement); + if (triggerElement) { + await click(triggerElement); + } assert .dom('.ember-basic-dropdown-content') @@ -1386,7 +1533,9 @@ module('Integration | Component | basic-dropdown', function (hooks) { assert.dom('#dropdown-is-opened').exists('The dropdown is opened'); - await click(triggerElement); + if (triggerElement) { + await click(triggerElement); + } assert .dom('#dropdown-is-opened') @@ -1395,7 +1544,7 @@ module('Integration | Component | basic-dropdown', function (hooks) { wormhole.remove(); }); - test('Shadow dom: Its `toggle` action opens and closes the dropdown with renderInPlace', async function (assert) { + test('Shadow dom: Its `toggle` action opens and closes the dropdown with renderInPlace', async function (assert) { await render(hbs` @@ -1408,53 +1557,63 @@ module('Integration | Component | basic-dropdown', function (hooks) { `); assert - .dom('#dropdown-is-opened', this.element.getRootNode()) + .dom('#dropdown-is-opened', getRootNode(this.element)) .doesNotExist('The dropdown is closed'); - const triggerElement = find('[data-shadow]')?.shadowRoot.querySelector( + const triggerElement = find('[data-shadow]')?.shadowRoot?.querySelector( '.ember-basic-dropdown-trigger', ); - await click(triggerElement); + if (triggerElement) { + await click(triggerElement); + } assert - .dom('.ember-basic-dropdown-content', find('[data-shadow]').shadowRoot) + .dom('.ember-basic-dropdown-content', find('[data-shadow]')?.shadowRoot) .exists('The dropdown is rendered'); assert - .dom('#dropdown-is-opened', find('[data-shadow]').shadowRoot) + .dom('#dropdown-is-opened', find('[data-shadow]')?.shadowRoot) .exists('The dropdown is opened'); await click( - find('[data-shadow]').shadowRoot.getElementById('dropdown-is-opened'), + find('[data-shadow]')?.shadowRoot?.getElementById( + 'dropdown-is-opened', + ) as HTMLElement, ); assert - .dom('#dropdown-is-opened', find('[data-shadow]').shadowRoot) + .dom('#dropdown-is-opened', find('[data-shadow]')?.shadowRoot) .exists('The dropdown stays opened when clicking content'); - await click(triggerElement); + if (triggerElement) { + await click(triggerElement); + } assert - .dom('#dropdown-is-opened', find('[data-shadow]').shadowRoot) + .dom('#dropdown-is-opened', find('[data-shadow]')?.shadowRoot) .doesNotExist('The dropdown is closed again'); - await click(triggerElement); + if (triggerElement) { + await click(triggerElement); + } assert - .dom('#dropdown-is-opened', find('[data-shadow]').shadowRoot) + .dom('#dropdown-is-opened', find('[data-shadow]')?.shadowRoot) .exists('The dropdown is opened 2d time'); await click( - find('[data-shadow]').shadowRoot.getElementById('dropdown-is-opened'), + find('[data-shadow]')?.shadowRoot?.getElementById( + 'dropdown-is-opened', + ) as HTMLElement, ); assert - .dom('#dropdown-is-opened', find('[data-shadow]').shadowRoot) + .dom('#dropdown-is-opened', find('[data-shadow]')?.shadowRoot) .exists('The dropdown stays opened when clicking content after 2d open'); }); - test('Shadow dom: Its `toggle` action opens and closes the dropdown when wormhole is inside shadow dom', async function (assert) { + test('Shadow dom: Its `toggle` action opens and closes the dropdown when wormhole is inside shadow dom', async function (assert) { await render(hbs` @@ -1469,29 +1628,33 @@ module('Integration | Component | basic-dropdown', function (hooks) { `); assert - .dom('#dropdown-is-opened', this.element.getRootNode()) + .dom('#dropdown-is-opened', getRootNode(this.element)) .doesNotExist('The dropdown is closed'); const shadowRoot = find('[data-shadow]')?.shadowRoot; - const triggerElement = shadowRoot.querySelector( + const triggerElement = shadowRoot?.querySelector( '.ember-basic-dropdown-trigger', ); - await click(triggerElement); + if (triggerElement) { + await click(triggerElement); + } assert - .dom(shadowRoot.querySelector('.ember-basic-dropdown-content')) + .dom(shadowRoot?.querySelector('.ember-basic-dropdown-content')) .exists('The dropdown is rendered'); assert - .dom(shadowRoot.querySelector('#dropdown-is-opened')) + .dom(shadowRoot?.querySelector('#dropdown-is-opened')) .exists('The dropdown is opened'); - await click(triggerElement); + if (triggerElement) { + await click(triggerElement); + } assert - .dom(shadowRoot.querySelector('#dropdown-is-opened')) + .dom(shadowRoot?.querySelector('#dropdown-is-opened')) .doesNotExist('The dropdown is closed again'); }); }); diff --git a/test-app/tests/integration/components/basic-dropdown-wormhole-test.js b/test-app/tests/integration/components/basic-dropdown-wormhole-test.ts similarity index 59% rename from test-app/tests/integration/components/basic-dropdown-wormhole-test.js rename to test-app/tests/integration/components/basic-dropdown-wormhole-test.ts index 67084e51..42ec1fb3 100644 --- a/test-app/tests/integration/components/basic-dropdown-wormhole-test.js +++ b/test-app/tests/integration/components/basic-dropdown-wormhole-test.ts @@ -3,32 +3,42 @@ import { setupRenderingTest } from 'ember-qunit'; import { hbs } from 'ember-cli-htmlbars'; import { render } from '@ember/test-helpers'; import config from 'test-app/config/environment'; +import type { TestContext } from '@ember/test-helpers'; + +interface ExtendedTestContext extends TestContext { + element: HTMLElement; + originalConfig: Record; +} + +function getRootNode(element: Element): HTMLElement { + return element.getRootNode() as HTMLElement; +} module('Integration | Component | basic-dropdown-wormhole', function (hooks) { setupRenderingTest(hooks); - hooks.beforeEach(async function () { + hooks.beforeEach(function (this: ExtendedTestContext) { // Duplicate config to avoid mutating global config this.originalConfig = JSON.parse( JSON.stringify(config['ember-basic-dropdown'] || {}), - ); + ) as Record; }); - hooks.afterEach(async function () { + hooks.afterEach(function (this: ExtendedTestContext) { config['ember-basic-dropdown'] = this.originalConfig; }); - test('Is present', async function (assert) { + test('Is present', async function (assert) { await render(hbs` `); assert - .dom('#ember-basic-dropdown-wormhole', this.element.getRootNode()) + .dom('#ember-basic-dropdown-wormhole', getRootNode(this.element)) .exists('wormhole is present'); }); - test('Uses custom destination from config if present', async function (assert) { + test('Uses custom destination from config if present', async function (assert) { config['ember-basic-dropdown'] = { destination: 'custom-wormhole-destination', }; @@ -38,25 +48,25 @@ module('Integration | Component | basic-dropdown-wormhole', function (hooks) { assert .dom( '.ember-application #custom-wormhole-destination', - this.element.getRootNode(), + getRootNode(this.element), ) .exists('custom destination is used'); assert .dom( '.ember-application #ember-basic-dropdown-wormhole', - this.element.getRootNode(), + getRootNode(this.element), ) .doesNotExist('default destination is not used'); }); - test('Has class my-custom-class', async function (assert) { + test('Has class my-custom-class', async function (assert) { await render(hbs` `); assert - .dom('.my-custom-class', this.element.getRootNode()) + .dom('.my-custom-class', getRootNode(this.element)) .exists('my-custom-class was set'); }); }); diff --git a/test-app/tests/integration/components/content-test.js b/test-app/tests/integration/components/content-test.ts similarity index 51% rename from test-app/tests/integration/components/content-test.js rename to test-app/tests/integration/components/content-test.ts index 25bfce69..9cb2a696 100644 --- a/test-app/tests/integration/components/content-test.js +++ b/test-app/tests/integration/components/content-test.ts @@ -1,103 +1,195 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { hbs } from 'ember-cli-htmlbars'; -import { render, click, triggerEvent, settled } from '@ember/test-helpers'; +import { + render, + click, + triggerEvent, + settled, + type TestContext, +} from '@ember/test-helpers'; +import type { Dropdown } from 'ember-basic-dropdown/types'; + +interface ExtendedTestContext extends TestContext { + element: HTMLElement; + dropdown: Dropdown; + dropdown1: Dropdown; + dropdown2: Dropdown; + divVisible?: boolean; + onFocusIn: (dropdown?: Dropdown, event?: FocusEvent) => void; + onFocusOut: (dropdown?: Dropdown, event?: FocusEvent) => void; + onMouseEnter: (dropdown?: Dropdown, event?: MouseEvent) => void; + onMouseLeave: (dropdown?: Dropdown, event?: MouseEvent) => void; + shouldReposition?: ( + mutations: MutationRecord[], + dropdown?: Dropdown, + ) => boolean; +} + +function getRootNode(element: Element): HTMLElement { + return element.getRootNode() as HTMLElement; +} module('Integration | Component | basic-dropdown-content', function (hooks) { setupRenderingTest(hooks); // Basic rendering - test('If the dropdown is open renders the given block in a div with class `ember-basic-dropdown-content`', async function (assert) { + test('If the dropdown is open renders the given block in a div with class `ember-basic-dropdown-content`', async function (assert) { assert.expect(2); this.dropdown = { uniqueId: 'e123', isOpen: true, - actions: { reposition() {} }, + disabled: false, + actions: { + toggle: () => {}, + close: () => {}, + open: () => {}, + reposition: () => { + return undefined; + }, + registerTriggerElement: () => {}, + registerDropdownElement: () => {}, + getTriggerElement: () => { + return null; + }, + }, }; - await render(hbs` + await render(hbs`
Lorem ipsum `); assert - .dom('.ember-basic-dropdown-content', this.element.getRootNode()) + .dom('.ember-basic-dropdown-content', getRootNode(this.element)) .hasText('Lorem ipsum', 'It contains the given block'); assert .dom( '#destination-el > .ember-basic-dropdown-content', - this.element.getRootNode(), + getRootNode(this.element), ) .exists('It is rendered in the #ember-testing div'); }); - test('If a `@defaultClass` argument is provided to the content, its value is added to the list of classes', async function (assert) { + test('If a `@defaultClass` argument is provided to the content, its value is added to the list of classes', async function (assert) { assert.expect(2); this.dropdown = { uniqueId: 'e123', isOpen: true, - actions: { reposition() {} }, + disabled: false, + actions: { + toggle: () => {}, + close: () => {}, + open: () => {}, + reposition: () => { + return undefined; + }, + registerTriggerElement: () => {}, + registerDropdownElement: () => {}, + getTriggerElement: () => { + return null; + }, + }, }; - await render(hbs` + await render(hbs`
Lorem ipsum `); assert - .dom('.ember-basic-dropdown-content', this.element.getRootNode()) + .dom('.ember-basic-dropdown-content', getRootNode(this.element)) .hasText('Lorem ipsum', 'It contains the given block'); assert - .dom('.ember-basic-dropdown-content', this.element.getRootNode()) + .dom('.ember-basic-dropdown-content', getRootNode(this.element)) .hasClass('extra-class'); }); - test('If the dropdown is closed, nothing is rendered', async function (assert) { + test('If the dropdown is closed, nothing is rendered', async function (assert) { assert.expect(1); - this.dropdown = { uniqueId: 'e123', isOpen: false }; - await render(hbs` + this.dropdown = { + uniqueId: 'e123', + isOpen: false, + disabled: false, + actions: { + toggle: () => {}, + close: () => {}, + open: () => {}, + reposition: () => { + return undefined; + }, + registerTriggerElement: () => {}, + registerDropdownElement: () => {}, + getTriggerElement: () => { + return null; + }, + }, + }; + await render(hbs`
Lorem ipsum `); assert - .dom('.ember-basic-dropdown-content', this.element.getRootNode()) + .dom('.ember-basic-dropdown-content', getRootNode(this.element)) .doesNotExist('Nothing is rendered'); }); - test('If it receives `@renderInPlace={{true}}`, it is rendered right here instead of elsewhere', async function (assert) { + test('If it receives `@renderInPlace={{true}}`, it is rendered right here instead of elsewhere', async function (assert) { assert.expect(2); this.dropdown = { uniqueId: 'e123', isOpen: true, + disabled: false, actions: { - reposition() { - return {}; + toggle: () => {}, + close: () => {}, + open: () => {}, + reposition: () => { + return undefined; + }, + registerTriggerElement: () => {}, + registerDropdownElement: () => {}, + getTriggerElement: () => { + return null; }, }, }; - await render(hbs` + await render(hbs` Lorem ipsum `); assert - .dom('.ember-basic-dropdown-content', this.element.getRootNode()) + .dom('.ember-basic-dropdown-content', getRootNode(this.element)) .exists('It is rendered in the spot'); assert .dom( '#destination-el .ember-basic-dropdown-content', - this.element.getRootNode(), + getRootNode(this.element), ) .doesNotExist("It isn't rendered in the #ember-testing div"); }); - test('It derives the ID of the content from the `uniqueId` property of of the dropdown', async function (assert) { + test('It derives the ID of the content from the `uniqueId` property of of the dropdown', async function (assert) { assert.expect(1); this.dropdown = { uniqueId: 'e123', isOpen: true, - actions: { reposition() {} }, + disabled: false, + actions: { + toggle: () => {}, + close: () => {}, + open: () => {}, + reposition: () => { + return undefined; + }, + registerTriggerElement: () => {}, + registerDropdownElement: () => {}, + getTriggerElement: () => { + return null; + }, + }, }; - await render(hbs` + await render(hbs`
Lorem ipsum `); assert - .dom('.ember-basic-dropdown-content', this.element.getRootNode()) + .dom('.ember-basic-dropdown-content', getRootNode(this.element)) .hasAttribute( 'id', 'ember-basic-dropdown-content-e123', @@ -105,48 +197,88 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { ); }); - test('If it receives `class="foo123"`, the rendered content will have that class along with the default one', async function (assert) { + test('If it receives `class="foo123"`, the rendered content will have that class along with the default one', async function (assert) { assert.expect(1); this.dropdown = { uniqueId: 'e123', isOpen: true, - actions: { reposition() {} }, + disabled: false, + actions: { + toggle: () => {}, + close: () => {}, + open: () => {}, + reposition: () => { + return undefined; + }, + registerTriggerElement: () => {}, + registerDropdownElement: () => {}, + getTriggerElement: () => { + return null; + }, + }, }; - await render(hbs` + await render(hbs`
Lorem ipsum `); assert - .dom('.ember-basic-dropdown-content', this.element.getRootNode()) + .dom('.ember-basic-dropdown-content', getRootNode(this.element)) .hasClass('foo123', 'The dropdown contains that class'); }); - test('If it receives `dir="rtl"`, the rendered content will have the attribute set', async function (assert) { + test('If it receives `dir="rtl"`, the rendered content will have the attribute set', async function (assert) { assert.expect(1); - this.dropdown = { isOpen: true, actions: { reposition() {} } }; - await render(hbs` + this.dropdown = { + uniqueId: '', + isOpen: true, + disabled: false, + actions: { + toggle: () => {}, + close: () => {}, + open: () => {}, + reposition: () => { + return undefined; + }, + registerTriggerElement: () => {}, + registerDropdownElement: () => {}, + getTriggerElement: () => { + return null; + }, + }, + }; + await render(hbs`
Lorem ipsum `); assert - .dom('.ember-basic-dropdown-content', this.element.getRootNode()) + .dom('.ember-basic-dropdown-content', getRootNode(this.element)) .hasAttribute('dir', 'rtl', 'The dropdown has `dir="rtl"`'); }); // Clicking while the component is opened - test('Clicking anywhere in the app outside the component will invoke the close action on the dropdown', async function (assert) { + test('Clicking anywhere in the app outside the component will invoke the close action on the dropdown', async function (assert) { assert.expect(1); this.dropdown = { uniqueId: 'e123', isOpen: true, + disabled: false, actions: { + toggle: () => {}, close() { assert.ok(true, 'The close action gets called'); }, - reposition() {}, + open: () => {}, + reposition: () => { + return undefined; + }, + registerTriggerElement: () => {}, + registerDropdownElement: () => {}, + getTriggerElement: () => { + return null; + }, }, }; - await render(hbs` + await render(hbs`
Lorem ipsum @@ -155,19 +287,29 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { await click('#other-div'); }); - test('Specifying the rootEventType as click will not close a component if it is opened', async function (assert) { + test('Specifying the rootEventType as click will not close a component if it is opened', async function (assert) { assert.expect(0); this.dropdown = { uniqueId: 'e123', isOpen: true, + disabled: false, actions: { + toggle: () => {}, close() { assert.ok(true, 'The close action should not be called'); }, - reposition() {}, + open: () => {}, + reposition: () => { + return undefined; + }, + registerTriggerElement: () => {}, + registerDropdownElement: () => {}, + getTriggerElement: () => { + return null; + }, }, }; - await render(hbs` + await render(hbs`
Lorem ipsum @@ -176,19 +318,29 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { await triggerEvent('#other-div', 'mousedown'); }); - test('Specifying the rootEventType as mousedown will close a component if it is opened', async function (assert) { + test('Specifying the rootEventType as mousedown will close a component if it is opened', async function (assert) { assert.expect(1); this.dropdown = { uniqueId: 'e123', isOpen: true, + disabled: false, actions: { + toggle: () => {}, close() { assert.ok(true, 'The close action gets called'); }, - reposition() {}, + open: () => {}, + reposition: () => { + return undefined; + }, + registerTriggerElement: () => {}, + registerDropdownElement: () => {}, + getTriggerElement: () => { + return null; + }, }, }; - await render(hbs` + await render(hbs`
Lorem ipsum @@ -197,53 +349,79 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { await triggerEvent('#other-div', 'mousedown'); }); - test("Clicking anywhere inside the dropdown content doesn't invoke the close action", async function (assert) { + test("Clicking anywhere inside the dropdown content doesn't invoke the close action", async function (assert) { assert.expect(0); this.dropdown = { uniqueId: 'e123', isOpen: true, + disabled: false, actions: { + toggle: () => {}, close() { assert.ok(false, 'The close action should not be called'); }, - reposition() {}, + open: () => {}, + reposition: () => { + return undefined; + }, + registerTriggerElement: () => {}, + registerDropdownElement: () => {}, + getTriggerElement: () => { + return null; + }, }, }; - await render(hbs` + await render(hbs`
Lorem ipsum
`); await click('#inside-div'); }); - test("Clicking in inside the a dropdown content nested inside another dropdown content doesn't invoke the close action on neither of them if the second is rendered in place", async function (assert) { + test("Clicking in inside the a dropdown content nested inside another dropdown content doesn't invoke the close action on neither of them if the second is rendered in place", async function (assert) { assert.expect(0); this.dropdown1 = { uniqueId: 'ember1', isOpen: true, + disabled: false, actions: { + toggle: () => {}, close() { assert.ok(false, 'The close action should not be called'); }, - reposition() { - return {}; + open: () => {}, + reposition: () => { + return undefined; + }, + registerTriggerElement: () => {}, + registerDropdownElement: () => {}, + getTriggerElement: () => { + return null; }, }, }; this.dropdown2 = { uniqueId: 'ember2', isOpen: true, + disabled: false, actions: { + toggle: () => {}, close() { assert.ok(false, 'The close action should not be called either'); }, - reposition() { - return {}; + open: () => {}, + reposition: () => { + return undefined; + }, + registerTriggerElement: () => {}, + registerDropdownElement: () => {}, + getTriggerElement: () => { + return null; }, }, }; - await render(hbs` + await render(hbs`
@@ -258,19 +436,29 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { }); // Touch gestures while the component is opened - test('Tapping anywhere in the app outside the component will invoke the close action on the dropdown', async function (assert) { + test('Tapping anywhere in the app outside the component will invoke the close action on the dropdown', async function (assert) { assert.expect(1); this.dropdown = { uniqueId: 'e123', isOpen: true, + disabled: false, actions: { + toggle: () => {}, close() { assert.ok(true, 'The close action gets called'); }, - reposition() {}, + open: () => {}, + reposition: () => { + return undefined; + }, + registerTriggerElement: () => {}, + registerDropdownElement: () => {}, + getTriggerElement: () => { + return null; + }, }, }; - await render(hbs` + await render(hbs`
Lorem ipsum @@ -280,19 +468,29 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { await triggerEvent('#other-div', 'touchend'); }); - test('Scrolling (touchstart + touchmove + touchend) anywhere in the app outside the component will invoke the close action on the dropdown', async function (assert) { + test('Scrolling (touchstart + touchmove + touchend) anywhere in the app outside the component will invoke the close action on the dropdown', async function (assert) { assert.expect(0); this.dropdown = { uniqueId: 'e123', isOpen: true, + disabled: false, actions: { + toggle: () => {}, close() { assert.ok(false, 'The close action does not called'); }, - reposition() {}, + open: () => {}, + reposition: () => { + return undefined; + }, + registerTriggerElement: () => {}, + registerDropdownElement: () => {}, + getTriggerElement: () => { + return null; + }, }, }; - await render(hbs` + await render(hbs`
Lorem ipsum @@ -307,19 +505,29 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { }); }); - test('Using stylus on touch device will handle scroll/tap to fire close action properly', async function (assert) { + test('Using stylus on touch device will handle scroll/tap to fire close action properly', async function (assert) { assert.expect(1); this.dropdown = { uniqueId: 'e123', isOpen: true, + disabled: false, actions: { + toggle: () => {}, close() { assert.ok(true, 'The close action is called'); }, - reposition() {}, + open: () => {}, + reposition: () => { + return undefined; + }, + registerTriggerElement: () => {}, + registerDropdownElement: () => {}, + getTriggerElement: () => { + return null; + }, }, }; - await render(hbs` + await render(hbs`
Lorem ipsum @@ -345,19 +553,32 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { }); // Arbitrary events - test('The user can attach arbitrary events to the content', async function (assert) { + test('The user can attach arbitrary events to the content', async function (assert) { assert.expect(3); this.dropdown = { uniqueId: 'e123', isOpen: true, - actions: { reposition() {} }, + disabled: false, + actions: { + toggle: () => {}, + close: () => {}, + open: () => {}, + reposition: () => { + return undefined; + }, + registerTriggerElement: () => {}, + registerDropdownElement: () => {}, + getTriggerElement: () => { + return null; + }, + }, }; this.onMouseEnter = (api, e) => { assert.ok(true, 'The action is invoked'); assert.deepEqual(api, this.dropdown, 'The first argument is the API'); assert.ok(e instanceof window.Event, 'the second argument is an event'); }; - await render(hbs` + await render(hbs`
Content @@ -367,51 +588,83 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { }); // Repositining - test('The component is repositioned immediatly when opened', async function (assert) { + test('The component is repositioned immediatly when opened', async function (assert) { assert.expect(1); this.dropdown = { uniqueId: 'e123', isOpen: true, + disabled: false, actions: { + toggle: () => {}, + close: () => {}, + open: () => {}, reposition() { assert.ok(true, 'Reposition is invoked exactly once'); + + return undefined; + }, + registerTriggerElement: () => {}, + registerDropdownElement: () => {}, + getTriggerElement: () => { + return null; }, }, }; - await render(hbs` + await render(hbs`
Lorem ipsum `); }); - test('The component is not repositioned if it is closed', async function (assert) { + test('The component is not repositioned if it is closed', async function (assert) { assert.expect(0); this.dropdown = { uniqueId: 'e123', isOpen: false, + disabled: false, actions: { + toggle: () => {}, + close: () => {}, + open: () => {}, reposition() { assert.ok(false, 'Reposition is invoked exactly once'); + return undefined; + }, + registerTriggerElement: () => {}, + registerDropdownElement: () => {}, + getTriggerElement: () => { + return null; }, }, }; - await render(hbs` + await render(hbs`
Lorem ipsum `); }); - test('The component cancels events when preventScroll is true', async function (assert) { + test('The component cancels events when preventScroll is true', async function (assert) { assert.expect(4); this.dropdown = { uniqueId: 'e123', isOpen: true, + disabled: false, actions: { - reposition() {}, + toggle: () => {}, + close: () => {}, + open: () => {}, + reposition: () => { + return undefined; + }, + registerTriggerElement: () => {}, + registerDropdownElement: () => {}, + getTriggerElement: () => { + return null; + }, }, }; - await render(hbs` + await render(hbs`
content scroll test
@@ -423,10 +676,10 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { `); - let innerScrollable = this.element - .getRootNode() - .querySelector('#inner-div'); - let innerScrollableEvent = new WheelEvent('wheel', { + const innerScrollable = getRootNode(this.element).querySelector( + '#inner-div', + ) as HTMLElement; + const innerScrollableEvent = new WheelEvent('wheel', { deltaY: 4, cancelable: true, bubbles: true, @@ -438,7 +691,7 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { ); innerScrollable.scrollTop = 4; - let innerScrollableCanceledEvent = new WheelEvent('wheel', { + const innerScrollableCanceledEvent = new WheelEvent('wheel', { deltaY: -10, cancelable: true, bubbles: true, @@ -454,10 +707,10 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { 'The innerScrollable was scrolled anyway.', ); - let outerScrollable = this.element - .getRootNode() - .querySelector('#outer-div'); - let outerScrollableEvent = new WheelEvent('wheel', { + const outerScrollable = getRootNode(this.element).querySelector( + '#outer-div', + ) as HTMLElement; + const outerScrollableEvent = new WheelEvent('wheel', { deltaY: 4, cancelable: true, bubbles: true, @@ -469,19 +722,29 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { ); }); - test('The component is repositioned if the window scrolls', async function (assert) { + test('The component is repositioned if the window scrolls', async function (assert) { assert.expect(1); let repositions = 0; this.dropdown = { uniqueId: 'e123', isOpen: true, + disabled: false, actions: { + toggle: () => {}, + close: () => {}, + open: () => {}, reposition() { repositions++; + return undefined; + }, + registerTriggerElement: () => {}, + registerDropdownElement: () => {}, + getTriggerElement: () => { + return null; }, }, }; - await render(hbs` + await render(hbs`
Lorem ipsum `); @@ -494,19 +757,29 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { ); }); - test('The component is repositioned if the window is resized', async function (assert) { + test('The component is repositioned if the window is resized', async function (assert) { assert.expect(1); let repositions = 0; this.dropdown = { uniqueId: 'e123', isOpen: true, + disabled: false, actions: { + toggle: () => {}, + close: () => {}, + open: () => {}, reposition() { repositions++; + return undefined; + }, + registerTriggerElement: () => {}, + registerDropdownElement: () => {}, + getTriggerElement: () => { + return null; }, }, }; - await render(hbs` + await render(hbs`
Lorem ipsum `); @@ -519,19 +792,29 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { ); }); - test('The component is repositioned if the orientation changes', async function (assert) { + test('The component is repositioned if the orientation changes', async function (assert) { assert.expect(1); let repositions = 0; this.dropdown = { uniqueId: 'e123', isOpen: true, + disabled: false, actions: { + toggle: () => {}, + close: () => {}, + open: () => {}, reposition() { repositions++; + return undefined; + }, + registerTriggerElement: () => {}, + registerDropdownElement: () => {}, + getTriggerElement: () => { + return null; }, }, }; - await render(hbs` + await render(hbs`
Lorem ipsum `); @@ -544,47 +827,67 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { ); }); - test('The component is repositioned when the content of the dropdown changes', async function (assert) { + test('The component is repositioned when the content of the dropdown changes', async function (assert) { assert.expect(1); let repositions = 0; this.dropdown = { uniqueId: 'e123', isOpen: true, + disabled: false, actions: { + toggle: () => {}, + close: () => {}, + open: () => {}, reposition() { repositions++; + return undefined; + }, + registerTriggerElement: () => {}, + registerDropdownElement: () => {}, + getTriggerElement: () => { + return null; }, }, }; - await render(hbs` + await render(hbs`
`); - let target = this.element - .getRootNode() - .querySelector('#content-target-div'); - let span = document.createElement('SPAN'); + const target = getRootNode(this.element).querySelector( + '#content-target-div', + ) as HTMLElement; + const span = document.createElement('SPAN'); target.appendChild(span); await settled(); assert.strictEqual(repositions, 2, 'It was repositioned twice'); }); - test('The component is repositioned when the content of the dropdown is changed through ember', async function (assert) { + test('The component is repositioned when the content of the dropdown is changed through ember', async function (assert) { assert.expect(1); let repositions = 0; this.dropdown = { uniqueId: 'e123', isOpen: true, + disabled: false, actions: { + toggle: () => {}, + close: () => {}, + open: () => {}, reposition() { repositions++; + return undefined; + }, + registerTriggerElement: () => {}, + registerDropdownElement: () => {}, + getTriggerElement: () => { + return null; }, }, }; this.divVisible = false; - await render(hbs` + await render(hbs`
{{#if this.divVisible}} @@ -597,55 +900,78 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { assert.strictEqual(repositions, 2, 'It was repositioned twice'); }); - test('@shouldReposition can be used to control which mutations should trigger a reposition', async function (assert) { + test('@shouldReposition can be used to control which mutations should trigger a reposition', async function (assert) { assert.expect(2); - let done = assert.async(); + const done = assert.async(); this.dropdown = { uniqueId: 'e123', isOpen: true, + disabled: false, actions: { + toggle: () => {}, + close: () => {}, + open: () => {}, reposition() { assert.ok(true, 'It was repositioned once'); done(); + return undefined; + }, + registerTriggerElement: () => {}, + registerDropdownElement: () => {}, + getTriggerElement: () => { + return null; }, }, }; this.shouldReposition = (mutations) => { assert.ok(true, '@shouldReposition was called'); + if (!mutations[0]) { + return false; + } return Array.prototype.slice .call(mutations[0].addedNodes) - .some((node) => { + .some((node: Node) => { return node.nodeName !== 'SPAN'; }); }; - await render(hbs` + await render(hbs`
`); - let target = this.element - .getRootNode() - .querySelector('#content-target-div'); - let span = document.createElement('SPAN'); - target.appendChild(span); + const target = getRootNode(this.element).querySelector( + '#content-target-div', + ); + const span = document.createElement('SPAN'); + target?.appendChild(span); }); - test('A renderInPlace component is repositioned if the window scrolls', async function (assert) { + test('A renderInPlace component is repositioned if the window scrolls', async function (assert) { assert.expect(1); let repositions = 0; this.dropdown = { uniqueId: 'e123', isOpen: true, + disabled: false, actions: { + toggle: () => {}, + close: () => {}, + open: () => {}, reposition() { repositions++; + return undefined; + }, + registerTriggerElement: () => {}, + registerDropdownElement: () => {}, + getTriggerElement: () => { + return null; }, }, }; - await render(hbs` + await render(hbs`
Lorem ipsum `); @@ -659,25 +985,38 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { }); // Overlay - test('If it receives an `overlay=true` option, there is an overlay covering all the screen', async function (assert) { + test('If it receives an `overlay=true` option, there is an overlay covering all the screen', async function (assert) { assert.expect(2); this.dropdown = { uniqueId: 'e123', isOpen: true, - actions: { reposition() {} }, + disabled: false, + actions: { + toggle: () => {}, + close: () => {}, + open: () => {}, + reposition: () => { + return undefined; + }, + registerTriggerElement: () => {}, + registerDropdownElement: () => {}, + getTriggerElement: () => { + return null; + }, + }, }; - await render(hbs` + await render(hbs`
`); assert - .dom('.ember-basic-dropdown-overlay', this.element.getRootNode()) + .dom('.ember-basic-dropdown-overlay', getRootNode(this.element)) .exists('There is one overlay'); this.set('dropdown.isOpen', false); assert - .dom('.ember-basic-dropdown-overlay', this.element.getRootNode()) + .dom('.ember-basic-dropdown-overlay', getRootNode(this.element)) .doesNotExist('There is no overlay when closed'); }); @@ -690,15 +1029,28 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { this.set('dropdown', { uniqueId: 'e123', isOpen: true, + disabled: false, actions: { - reposition() { - return {}; + toggle: () => {}, + close: () => {}, + open: () => {}, + reposition: () => { + return undefined; + }, + registerTriggerElement: () => {}, + registerDropdownElement: () => {}, + getTriggerElement: () => { + return null; }, }, }); }); - function assertCommonEventHandlerArgs(assert, args) { + function assertCommonEventHandlerArgs( + this: ExtendedTestContext, + assert: Assert, + args: [Dropdown, Event | undefined], + ) { const [dropdown, e] = args; assert.ok( @@ -712,17 +1064,17 @@ module('Integration | Component | basic-dropdown-content', function (hooks) { assert.ok(args.length === 2, 'It receives only 2 arguments'); } - test('It properly handles the onFocusIn action', async function (assert) { + test('It properly handles the onFocusIn action', async function (assert) { assert.expect(4); - const onFocusIn = function () { + const onFocusIn = (dropdown: Dropdown, e?: FocusEvent) => { assert.ok(true, 'The `onFocusIn()` action has been fired'); - assertCommonEventHandlerArgs.call(this, assert, arguments); + assertCommonEventHandlerArgs.call(this, assert, [dropdown, e]); }; - this.set('onFocusIn', onFocusIn.bind(this)); + this.set('onFocusIn', onFocusIn); - await render(hbs` + await render(hbs`
('It properly handles the onFocusOut action', async function (assert) { assert.expect(4); - const onFocusOut = function () { + const onFocusOut = (dropdown: Dropdown, e?: FocusEvent) => { assert.ok(true, 'The `onFocusOut()` action has been fired'); - assertCommonEventHandlerArgs.call(this, assert, arguments); + assertCommonEventHandlerArgs.call(this, assert, [dropdown, e]); }; - this.set('onFocusOut', onFocusOut.bind(this)); + this.set('onFocusOut', onFocusOut); - await render(hbs` + await render(hbs`
('It properly handles the onMouseEnter action', async function (assert) { assert.expect(4); - const onMouseEnter = function () { + const onMouseEnter = (dropdown: Dropdown, e?: MouseEvent) => { assert.ok(true, 'The `onMouseEnter()` action has been fired'); - assertCommonEventHandlerArgs.call(this, assert, arguments); + assertCommonEventHandlerArgs.call(this, assert, [dropdown, e]); }; - this.set('onMouseEnter', onMouseEnter.bind(this)); + this.set('onMouseEnter', onMouseEnter); - await render(hbs` + await render(hbs`
('It properly handles the onMouseLeave action', async function (assert) { assert.expect(4); - const onMouseLeave = function () { + const onMouseLeave = (dropdown: Dropdown, e?: MouseEvent) => { assert.ok(true, 'The `onMouseLeave()` action has been fired'); - assertCommonEventHandlerArgs.call(this, assert, arguments); + assertCommonEventHandlerArgs.call(this, assert, [dropdown, e]); }; - this.set('onMouseLeave', onMouseLeave.bind(this)); + this.set('onMouseLeave', onMouseLeave); - await render(hbs` + await render(hbs`
- Click me -
- `); - - assert - .dom( - '#direct-parent > .ember-basic-dropdown-trigger', - this.element.getRootNode(), - ) - .exists('The trigger is not wrapped'); - assert - .dom('.ember-basic-dropdown-trigger', this.element.getRootNode()) - .hasText('Click me', 'The trigger contains the given block'); - }); - - test('If a `@defaultClass` argument is provided to the trigger, its value is added to the list of classes', async function (assert) { - assert.expect(1); - this.dropdown = { uniqueId: 123 }; - await render(hbs` -
- Click me -
- `); - - assert - .dom('.ember-basic-dropdown-trigger', this.element.getRootNode()) - .hasClass('extra-class'); - }); - - // Attributes and a11y - test("If it doesn't receive any tabindex, defaults to 0", async function (assert) { - assert.expect(1); - this.dropdown = { uniqueId: 123 }; - await render(hbs` - Click me - `); - - assert - .dom('.ember-basic-dropdown-trigger', this.element.getRootNode()) - .hasAttribute('tabindex', '0', 'Has a tabindex of 0'); - }); - - test('If it receives a tabindex={{false}}, it removes the tabindex', async function (assert) { - assert.expect(1); - this.dropdown = { uniqueId: 123 }; - await render(hbs` - Click me - `); - - assert - .dom('.ember-basic-dropdown-trigger', this.element.getRootNode()) - .doesNotHaveAttribute('tabindex'); - }); - - test('If it receives `tabindex="3"`, the tabindex of the element is 3', async function (assert) { - assert.expect(1); - this.dropdown = { uniqueId: 123 }; - await render(hbs` - Click me - `); - - assert - .dom('.ember-basic-dropdown-trigger', this.element.getRootNode()) - .hasAttribute('tabindex', '3', 'Has a tabindex of 3'); - }); - - test('If it receives `title="something"`, if has that title attribute', async function (assert) { - assert.expect(1); - this.dropdown = { uniqueId: 123 }; - await render(hbs` - Click me - `); - - assert - .dom('.ember-basic-dropdown-trigger', this.element.getRootNode()) - .hasAttribute('title', 'foobar', 'Has the given title'); - }); - - test('If it receives `id="some-id"`, if has that id', async function (assert) { - assert.expect(1); - this.dropdown = { uniqueId: 123 }; - await render(hbs` - Click me - `); - - assert - .dom('.ember-basic-dropdown-trigger', this.element.getRootNode()) - .hasAttribute('id', 'my-own-id', 'Has the given id'); - }); - - test("If the dropdown is disabled, the trigger doesn't have tabindex attribute", async function (assert) { - assert.expect(1); - this.dropdown = { uniqueId: 123, disabled: true }; - await render(hbs` - Click me - `); - - assert - .dom('.ember-basic-dropdown-trigger', this.element.getRootNode()) - .doesNotHaveAttribute('tabindex', "The component doesn't have tabindex"); - }); - - test('If it belongs to a disabled dropdown, it gets an `aria-disabled=true` attribute for a11y', async function (assert) { - assert.expect(2); - this.dropdown = { uniqueId: 123, disabled: true }; - await render(hbs` - Click me - `); - - assert - .dom('.ember-basic-dropdown-trigger', this.element.getRootNode()) - .hasAttribute('aria-disabled', 'true', 'It is marked as disabled'); - this.set('dropdown', { ...this.dropdown, disabled: false }); - assert - .dom('.ember-basic-dropdown-trigger', this.element.getRootNode()) - .hasAttribute('aria-disabled', 'false', 'It is NOT marked as disabled'); - }); - - test('If the received dropdown is open, it has an `aria-expanded="true"` attribute, otherwise `"false"`', async function (assert) { - assert.expect(2); - this.dropdown = { uniqueId: 123, isOpen: false }; - await render(hbs` - Click me - `); - assert - .dom('.ember-basic-dropdown-trigger', this.element.getRootNode()) - .hasAttribute('aria-expanded', 'false', 'the aria-expanded is false'); - this.set('dropdown', { ...this.dropdown, isOpen: true }); - assert - .dom('.ember-basic-dropdown-trigger', this.element.getRootNode()) - .hasAttribute('aria-expanded', 'true', 'the aria-expanded is true'); - }); - - test('If it has an `aria-controls="foo123"` attribute pointing to the id of the content', async function (assert) { - assert.expect(1); - this.dropdown = { uniqueId: 123 }; - await render(hbs` - Click me - `); - assert - .dom('.ember-basic-dropdown-trigger', this.element.getRootNode()) - .hasAttribute('aria-controls', 'ember-basic-dropdown-content-123'); - }); - - test('If it receives `@htmlTag`, the trigger uses that tag name', async function (assert) { - assert.expect(2); - this.dropdown = { uniqueId: 123 }; - await render(hbs` - Click me - `); - assert.strictEqual( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger') - .tagName, - 'BUTTON', - ); - assert - .dom('.ember-basic-dropdown-trigger', this.element.getRootNode()) - .hasAttribute('type', 'button'); - }); - - test('If it receives `role="presentation"` it gets that attribute', async function (assert) { - assert.expect(1); - this.dropdown = { uniqueId: 123 }; - await render(hbs` - Click me - `); - assert - .dom('.ember-basic-dropdown-trigger', this.element.getRootNode()) - .hasAttribute('role', 'presentation'); - }); - - test('If it receives `aria-owns="custom-owns"` it gets that attribute', async function (assert) { - assert.expect(1); - this.dropdown = { uniqueId: 123 }; - await render(hbs` - Click me - `); - assert - .dom('.ember-basic-dropdown-trigger', this.element.getRootNode()) - .hasAttribute('aria-owns', 'custom-owns'); - }); - - test('If it receives `aria-controls="custom-controls"` it gets that attribute', async function (assert) { - assert.expect(1); - this.dropdown = { uniqueId: 123 }; - await render(hbs` - Click me - `); - assert - .dom('.ember-basic-dropdown-trigger', this.element.getRootNode()) - .hasAttribute('aria-controls', 'custom-controls'); - }); - - test('If it does not receive an specific `role`, the default is `button`', async function (assert) { - assert.expect(1); - this.dropdown = { uniqueId: 123 }; - this.role = undefined; - await render(hbs` - Click me - `); - assert - .dom('.ember-basic-dropdown-trigger', this.element.getRootNode()) - .hasAttribute('role', 'button'); - }); - - // Custom actions - test('the user can bind arbitrary events to the trigger', async function (assert) { - assert.expect(2); - this.dropdown = { uniqueId: 123 }; - this.onMouseEnter = (dropdown, e) => { - assert.deepEqual( - dropdown, - this.dropdown, - 'receives the dropdown as 1st argument', - ); - assert.ok( - e instanceof window.Event, - 'It receives the event as second argument', - ); - }; - await render(hbs` - Click me - `); - await triggerEvent( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), - 'mouseenter', - ); - }); - - // Default behaviour - test('click events invoke the `toggle` action on the dropdown by default', async function (assert) { - assert.expect(3); - this.dropdown = { - uniqueId: 123, - actions: { - toggle(e) { - assert.ok(true, 'The `toggle()` action has been fired'); - assert.ok( - e instanceof window.Event, - 'It receives the event as first argument', - ); - assert.strictEqual( - arguments.length, - 1, - 'It receives only one argument', - ); - }, - }, - }; - await render(hbs` - Click me - `); - await triggerEvent( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), - 'click', - ); - }); - - test('mousedown events DO NOT invoke the `toggle` action on the dropdown by default', async function (assert) { - assert.expect(0); - this.dropdown = { - uniqueId: 123, - actions: { - toggle() { - assert.ok(false); - }, - }, - }; - await render(hbs` - Click me - `); - await triggerEvent( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), - 'mousedown', - ); - }); - - test('click events DO NOT invoke the `toggle` action on the dropdown if `@eventType="mousedown"`', async function (assert) { - assert.expect(0); - this.dropdown = { - uniqueId: 123, - actions: { - toggle() { - assert.ok(false); - }, - }, - }; - await render(hbs` - Click me - `); - await triggerEvent( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), - 'click', - ); - }); - - test('mousedown events invoke the `toggle` action on the dropdown if `eventType="mousedown"', async function (assert) { - assert.expect(3); - this.dropdown = { - uniqueId: 123, - actions: { - toggle(e) { - assert.ok(true, 'The `toggle()` action has been fired'); - assert.ok( - e instanceof window.Event, - 'It receives the event as first argument', - ); - assert.strictEqual( - arguments.length, - 1, - 'It receives only one argument', - ); - }, - }, - }; - await render(hbs` - Click me - `); - await triggerEvent( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), - 'mousedown', - ); - }); - - test('when `stopPropagation` is true the `click` event does not bubble', async function (assert) { - assert.expect(3); - this.handlerInParent = () => - assert.ok(false, 'This should never be called'); - - this.dropdown = { - uniqueId: 123, - actions: { - toggle(e) { - assert.ok(true, 'The `toggle()` action has been fired'); - assert.ok( - e instanceof window.Event, - 'It receives the event as first argument', - ); - assert.strictEqual( - arguments.length, - 1, - 'It receives only one argument', - ); - }, - }, - }; - await render(hbs` -
- Click me -
- `); - await triggerEvent( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), - 'click', - ); - }); - - test('when `stopPropagation` is true and eventType is true, the `click` event does not bubble', async function (assert) { - assert.expect(3); - this.handlerInParent = () => - assert.ok(false, 'This should never be called'); - - this.dropdown = { - uniqueId: 123, - actions: { - toggle(e) { - assert.ok(true, 'The `toggle()` action has been fired'); - assert.ok( - e instanceof window.Event, - 'It receives the event as first argument', - ); - assert.strictEqual( - arguments.length, - 1, - 'It receives only one argument', - ); - }, - }, - }; - await render(hbs` -
- Click me -
- `); - await triggerEvent( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), - 'click', - ); - }); - - test('Pressing ENTER fires the `toggle` action on the dropdown', async function (assert) { - assert.expect(3); - this.dropdown = { - uniqueId: 123, - actions: { - toggle(e) { - assert.ok(true, 'The `toggle()` action has been fired'); - assert.ok( - e instanceof window.Event, - 'It receives the event as first argument', - ); - assert.strictEqual( - arguments.length, - 1, - 'It receives only one argument', - ); - }, - }, - }; - await render(hbs` - Click me - `); - - await triggerKeyEvent( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), - 'keydown', - 13, - ); - }); - - test('Pressing SPACE fires the `toggle` action on the dropdown and preventsDefault to avoid scrolling', async function (assert) { - assert.expect(4); - this.dropdown = { - uniqueId: 123, - actions: { - toggle(e) { - assert.ok(true, 'The `toggle()` action has been fired'); - assert.ok( - e instanceof window.Event, - 'It receives the event as first argument', - ); - assert.strictEqual( - arguments.length, - 1, - 'It receives only one argument', - ); - assert.ok(e.defaultPrevented, 'The event is defaultPrevented'); - }, - }, - }; - await render(hbs` - Click me - `); - - await triggerKeyEvent( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), - 'keydown', - 32, - ); - }); - - test('Pressing ESC fires the `close` action on the dropdown', async function (assert) { - assert.expect(3); - this.dropdown = { - uniqueId: 123, - actions: { - close(e) { - assert.ok(true, 'The `close()` action has been fired'); - assert.ok( - e instanceof window.Event, - 'It receives the event as first argument', - ); - assert.strictEqual( - arguments.length, - 1, - 'It receives only one argument', - ); - }, - }, - }; - await render(hbs` - Click me - `); - - await triggerKeyEvent( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), - 'keydown', - 27, - ); - }); - - test('Pressing ENTER/SPACE/ESC does nothing if there is a `{{on "keydown"}}` event that calls stopImmediatePropagation', async function (assert) { - assert.expect(0); - this.onKeyDown = (e) => e.stopImmediatePropagation(); - this.dropdown = { - uniqueId: 123, - actions: { - close() { - assert.ok(false, 'This action is not called'); - }, - toggle() { - assert.ok(false, 'This action is not called'); - }, - }, - }; - await render(hbs` - Click me - `); - - await triggerKeyEvent( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), - 'keydown', - 13, - ); - await triggerKeyEvent( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), - 'keydown', - 32, - ); - await triggerKeyEvent( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), - 'keydown', - 27, - ); - }); - - test('Tapping invokes the toggle action on the dropdown', async function (assert) { - assert.expect(4); - this.dropdown = { - actions: { - uniqueId: 123, - toggle(e) { - assert.ok(true, 'The `toggle()` action has been fired'); - assert.strictEqual( - e.type, - 'touchend', - 'The event that toggles the dropdown is the touchend', - ); - assert.ok( - e instanceof window.Event, - 'It receives the event as first argument', - ); - assert.strictEqual( - arguments.length, - 1, - 'It receives only one argument', - ); - }, - }, - }; - await render(hbs` - Click me - `); - await tap( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), - ); - }); - - test("Firing a mousemove between a touchstart and a touchend (touch scroll) doesn't fire the toggle action", async function (assert) { - assert.expect(0); - this.dropdown = { - uniqueId: 123, - actions: { - toggle() { - assert.ok(false, 'This action in not called'); - }, - }, - }; - await render(hbs` - Click me - `); - - await triggerEvent( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), - 'touchstart', - ); - await triggerEvent( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), - 'touchmove', - { - changedTouches: [{ touchType: 'direct', pageX: 0, pageY: 0 }], - }, - ); - await triggerEvent( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), - 'touchend', - { - changedTouches: [{ touchType: 'direct', pageX: 0, pageY: 10 }], - }, - ); - }); - - test('Using stylus on touch device will handle scroll/tap to fire toggle action properly', async function (assert) { - assert.expect(1); - this.dropdown = { - uniqueId: 123, - actions: { - toggle() { - assert.ok(true, 'The toggle action is called'); - }, - reposition() {}, - }, - }; - await render(hbs` - Click me - `); - - // scroll - await triggerEvent( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), - 'touchstart', - ); - await triggerEvent( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), - 'touchmove', - { - changedTouches: [{ touchType: 'stylus', pageX: 0, pageY: 0 }], - }, - ); - await triggerEvent( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), - 'touchend', - { - changedTouches: [{ touchType: 'stylus', pageX: 0, pageY: 10 }], - }, - ); - - // tap - await triggerEvent( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), - 'touchstart', - ); - await triggerEvent( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), - 'touchmove', - { - changedTouches: [{ touchType: 'stylus', pageX: 0, pageY: 0 }], - }, - ); - await triggerEvent( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), - 'touchend', - { - changedTouches: [{ touchType: 'stylus', pageX: 4, pageY: 0 }], - }, - ); - }); - - test("If its dropdown is disabled it won't respond to mouse, touch or keyboard event", async function (assert) { - assert.expect(0); - this.dropdown = { - uniqueId: 123, - disabled: true, - actions: { - toggle() { - assert.ok(false, 'This action in not called'); - }, - open() { - assert.ok(false, 'This action in not called'); - }, - close() { - assert.ok(false, 'This action in not called'); - }, - }, - }; - await render(hbs` - Click me - `); - await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), - ); - await tap('.ember-basic-dropdown-trigger'); - await triggerKeyEvent( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), - 'keydown', - 13, - ); - await triggerKeyEvent( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), - 'keydown', - 32, - ); - await triggerKeyEvent( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), - 'keydown', - 27, - ); - }); - - // Decorating and overriding default event handlers - test('A user-supplied {{on "mousedown"}} callback will execute before the default toggle behavior', async function (assert) { - assert.expect(3); - let userActionRanfirst = false; - - this.dropdown = { - uniqueId: 123, - actions: { - toggle: () => { - assert.ok( - userActionRanfirst, - 'User-supplied `{{on "mousedown"}}` ran before default `toggle`', - ); - }, - }, - }; - - this.onMouseDown = (e) => { - assert.ok(true, 'The `userSuppliedAction()` action has been fired'); - assert.ok(e instanceof window.Event, 'It receives the event'); - userActionRanfirst = true; - }; - - await render(hbs` - {{!-- template-lint-disable no-pointer-down-event-binding --}} - Click me - `); - - await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), - ); - }); - - test('A user-supplied {{on "click"}} callback that calls `stopImmediatePropagation`, will prevent the default behavior', async function (assert) { - assert.expect(1); - - this.dropdown = { - uniqueId: 123, - actions: { - toggle: () => { - assert.ok(false, 'Default `toggle` action should not run'); - }, - }, - }; - - this.onClick = (e) => { - e.stopImmediatePropagation(); - assert.ok(true, 'The `userSuppliedAction()` action has been fired'); - }; - - await render(hbs` - Click me - `); - - await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), - ); - }); - - test('A user-supplied {{on "mousedown"}} callback that calls `stopImmediatePropagation` will prevent the default behavior', async function (assert) { - assert.expect(1); - - this.dropdown = { - uniqueId: 123, - actions: { - toggle: () => { - assert.ok(false, 'Default `toggle` action should not run'); - }, - }, - }; - - this.onMouseDown = (e) => { - e.stopImmediatePropagation(); - assert.ok(true, 'The user-supplied action has been fired'); - }; - - await render(hbs` - {{!-- template-lint-disable no-pointer-down-event-binding --}} - Click me - `); - - await click( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), - ); - }); - - test('A user-supplied {{on "touchend"}} callback will execute before the default toggle behavior', async function (assert) { - assert.expect(3); - let userActionRanfirst = false; - - this.dropdown = { - uniqueId: 123, - actions: { - toggle: () => { - assert.ok( - userActionRanfirst, - 'User-supplied `{{on "touchend"}}` ran before default `toggle`', - ); - }, - }, - }; - - this.onTouchEnd = (e) => { - assert.ok(true, 'The user-supplied touchend callback has been fired'); - assert.ok(e instanceof window.Event, 'It receives the event'); - userActionRanfirst = true; - }; - - await render(hbs` - - Click me - - `); - await tap( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), - ); - }); - - test('A user-supplied {{on "touchend"}} callback calling e.stopImmediatePropagation will prevent the default behavior', async function (assert) { - assert.expect(2); - - this.dropdown = { - uniqueId: 123, - actions: { - toggle: (e) => { - assert.notEqual( - e.type, - 'touchend', - 'Default `toggle` action should not run', - ); - }, - }, - }; - - this.onTouchEnd = (e) => { - e.stopImmediatePropagation(); - assert.ok(true, 'The user-supplied touchend callback has been fired'); - }; - - await render(hbs` - - Click me - - `); - await tap( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), - ); - }); - - test('A user-supplied `{{on "keydown"}}` action will execute before the default toggle behavior', async function (assert) { - assert.expect(3); - let userActionRanfirst = false; - - this.dropdown = { - uniqueId: 123, - actions: { - toggle: () => { - assert.ok( - userActionRanfirst, - 'User-supplied `{{on "keydown}}` ran before default `toggle`', - ); - }, - }, - }; - - this.onKeyDown = (e) => { - assert.ok(true, 'The `userSuppliedAction()` action has been fired'); - assert.ok(e instanceof window.Event, 'It receives the event'); - userActionRanfirst = true; - }; - - await render(hbs` - Click me - `); - await triggerKeyEvent( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), - 'keydown', - 13, - ); // Enter - }); - - test('A user-supplied `{{on "keydown"}}` action calling `stopImmediatePropagation` will prevent the default behavior', async function (assert) { - assert.expect(1); - - this.dropdown = { - uniqueId: 123, - actions: { - toggle: () => { - assert.ok(false, 'Default `toggle` action should not run'); - }, - }, - }; - - this.onKeyDown = (e) => { - e.stopImmediatePropagation(); - assert.ok(true, 'The `userSuppliedAction()` action has been fired'); - }; - - await render(hbs` - Click me - `); - await triggerKeyEvent( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), - 'keydown', - 13, - ); // Enter - }); - - test('Tapping an SVG inside of the trigger invokes the toggle action on the dropdown', async function (assert) { - assert.expect(3); - this.dropdown = { - actions: { - uniqueId: 123, - toggle(e) { - assert.ok(true, 'The `toggle()` action has been fired'); - assert.ok( - e instanceof window.Event, - 'It receives the event as first argument', - ); - assert.strictEqual( - arguments.length, - 1, - 'It receives only one argument', - ); - }, - }, - }; - await render(hbs` - Click me - `); - await tap( - this.element.getRootNode().querySelector('.ember-basic-dropdown-trigger'), - ); - }); - - /** - * Tests related to https://github.com/cibernox/ember-basic-dropdown/issues/498 - * Can be removed when the template `V1` compatability event handlers are removed. - */ - module('trigger event handlers', function (hooks) { - hooks.beforeEach(function () { - this.set('dropdown', { uniqueId: 'e123', actions: { toggle: () => {} } }); - }); - - function assertCommonEventHandlerArgs(assert, args) { - const [dropdown, e] = args; - - assert.ok( - dropdown.uniqueId === this.dropdown.uniqueId, - 'It receives the dropdown argument as the first argument', - ); - assert.ok( - e instanceof window.Event, - 'It receives the event as second argument', - ); - assert.ok(args.length === 2, 'It receives only 2 arguments'); - } - - test('It properly handles the onBlur action', async function (assert) { - assert.expect(4); - - const onBlur = function () { - assert.ok(true, 'The `onBlur()` action has been fired'); - assertCommonEventHandlerArgs.call(this, assert, arguments); - }; - - this.set('onBlur', onBlur.bind(this)); - - await render(hbs` - hello - `); - await triggerEvent( - this.element - .getRootNode() - .querySelector('.ember-basic-dropdown-trigger'), - 'blur', - ); // For some reason, `blur` test-helper fails here - }); - - test('It properly handles the onClick action', async function (assert) { - assert.expect(4); - - const onClick = function () { - assert.ok(true, 'The `onClick()` action has been fired'); - assertCommonEventHandlerArgs.call(this, assert, arguments); - }; - - this.set('onClick', onClick.bind(this)); - - await render(hbs` - hello - `); - await click( - this.element - .getRootNode() - .querySelector('.ember-basic-dropdown-trigger'), - ); - }); - - test('It properly handles the onFocus action', async function (assert) { - assert.expect(4); - - const onFocus = function () { - assert.ok(true, 'The `onFocus()` action has been fired'); - assertCommonEventHandlerArgs.call(this, assert, arguments); - }; - - this.set('onFocus', onFocus.bind(this)); - - await render(hbs` - hello - `); - await focus( - this.element - .getRootNode() - .querySelector('.ember-basic-dropdown-trigger'), - ); - }); - - test('It properly handles the onFocusIn action', async function (assert) { - assert.expect(4); - - const onFocusIn = function () { - assert.ok(true, 'The `onFocusIn()` action has been fired'); - assertCommonEventHandlerArgs.call(this, assert, arguments); - }; - - this.set('onFocusIn', onFocusIn.bind(this)); - - await render(hbs` - hello - `); - await triggerEvent( - this.element - .getRootNode() - .querySelector('.ember-basic-dropdown-trigger'), - 'focusin', - ); - }); - - test('It properly handles the onFocusOut action', async function (assert) { - assert.expect(4); - - const onFocusOut = function () { - assert.ok(true, 'The `onFocusOut()` action has been fired'); - assertCommonEventHandlerArgs.call(this, assert, arguments); - }; - - this.set('onFocusOut', onFocusOut.bind(this)); - - await render(hbs` - hello - `); - await triggerEvent( - this.element - .getRootNode() - .querySelector('.ember-basic-dropdown-trigger'), - 'focusout', - ); - }); - - test('It properly handles the onKeyDown action', async function (assert) { - assert.expect(4); - - const onKeyDown = function () { - assert.ok(true, 'The `onKeyDown()` action has been fired'); - assertCommonEventHandlerArgs.call(this, assert, arguments); - }; - - this.set('onKeyDown', onKeyDown.bind(this)); - - await render(hbs` - hello - `); - await triggerEvent( - this.element - .getRootNode() - .querySelector('.ember-basic-dropdown-trigger'), - 'keydown', - ); - }); - - test('It properly handles the onMouseDown action', async function (assert) { - assert.expect(4); - - const onMouseDown = function () { - assert.ok(true, 'The `onMouseDown()` action has been fired'); - assertCommonEventHandlerArgs.call(this, assert, arguments); - }; - - this.set('onMouseDown', onMouseDown.bind(this)); - - await render(hbs` - hello - `); - await triggerEvent( - this.element - .getRootNode() - .querySelector('.ember-basic-dropdown-trigger'), - 'mousedown', - ); - }); - - test('It properly handles the onMouseEnter action', async function (assert) { - assert.expect(4); - - const onMouseEnter = function () { - assert.ok(true, 'The `onMouseEnter()` action has been fired'); - assertCommonEventHandlerArgs.call(this, assert, arguments); - }; - - this.set('onMouseEnter', onMouseEnter.bind(this)); - - await render(hbs` - hello - `); - await triggerEvent('.ember-basic-dropdown-trigger', 'mouseenter'); - }); - - test('It properly handles the onMouseLeave action', async function (assert) { - assert.expect(4); - - const onMouseLeave = function () { - assert.ok(true, 'The `onMouseLeave()` action has been fired'); - assertCommonEventHandlerArgs.call(this, assert, arguments); - }; - - this.set('onMouseLeave', onMouseLeave.bind(this)); - - await render(hbs` - hello - `); - await triggerEvent( - this.element - .getRootNode() - .querySelector('.ember-basic-dropdown-trigger'), - 'mouseleave', - ); - }); - - test('It properly handles the onTouchEnd action', async function (assert) { - assert.expect(4); - - const onTouchEnd = function () { - assert.ok(true, 'The `onTouchEnd()` action has been fired'); - assertCommonEventHandlerArgs.call(this, assert, arguments); - }; - - this.set('onTouchEnd', onTouchEnd.bind(this)); - - await render(hbs` - hello - `); - await triggerEvent( - this.element - .getRootNode() - .querySelector('.ember-basic-dropdown-trigger'), - 'touchend', - ); - }); - }); -}); diff --git a/test-app/tests/integration/components/trigger-test.ts b/test-app/tests/integration/components/trigger-test.ts new file mode 100644 index 00000000..d70cd1bc --- /dev/null +++ b/test-app/tests/integration/components/trigger-test.ts @@ -0,0 +1,1394 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { hbs } from 'ember-cli-htmlbars'; +import { + render, + triggerEvent, + triggerKeyEvent, + tap, + click, + focus, + type TestContext, +} from '@ember/test-helpers'; +import type { Dropdown } from 'ember-basic-dropdown/types'; + +interface ExtendedTestContext extends TestContext { + element: HTMLElement; + dropdown: Dropdown; + // divVisible?: boolean; + // onFocusIn: (dropdown?: Dropdown, event?: FocusEvent) => void; + // onFocusOut: (dropdown?: Dropdown, event?: FocusEvent) => void; + onMouseEnter: (dropdown?: Dropdown, event?: MouseEvent) => void; + onBlur: (dropdown?: Dropdown, event?: FocusEvent) => void; + onClick: (dropdown?: Dropdown, event?: MouseEvent) => void; + onFocus: (dropdown?: Dropdown, event?: FocusEvent) => void; + onFocusIn: (dropdown?: Dropdown, event?: FocusEvent) => void; + onFocusOut: (dropdown?: Dropdown, event?: FocusEvent) => void; + onKeyDown: (dropdown?: Dropdown, event?: KeyboardEvent) => void; + onMouseDown: (dropdown?: Dropdown, event?: MouseEvent) => void; + onMouseLeave: (dropdown?: Dropdown, event?: MouseEvent) => void; + onTouchEnd: (dropdown?: Dropdown, event?: TouchEvent) => void; + touchEnd: (e: Event) => void; + keyDown: (e: Event) => void; + mouseDown: (e: Event) => void; + click: (e: Event) => void; + handlerInParent: (e: Event) => void; + // onMouseLeave: (dropdown?: Dropdown, event?: MouseEvent) => void; + // shouldReposition?: ( + // mutations: MutationRecord[], + // dropdown?: Dropdown, + // ) => boolean; +} + +function getRootNode(element: Element): HTMLElement { + return element.getRootNode() as HTMLElement; +} + +const dropdownBase: Dropdown = { + uniqueId: '', + isOpen: false, + disabled: false, + actions: { + toggle: () => {}, + close: () => {}, + open: () => {}, + reposition: () => { + return undefined; + }, + registerTriggerElement: () => {}, + registerDropdownElement: () => {}, + getTriggerElement: () => { + return null; + }, + }, +}; + +module('Integration | Component | basic-dropdown-trigger', function (hooks) { + setupRenderingTest(hooks); + + test('It renders the given block in a div with class `ember-basic-dropdown-trigger`, with no wrapper around', async function (assert) { + assert.expect(2); + this.dropdown = { + ...dropdownBase, + uniqueId: '123', + }; + await render(hbs` +
+ Click me +
+ `); + + assert + .dom( + '#direct-parent > .ember-basic-dropdown-trigger', + getRootNode(this.element), + ) + .exists('The trigger is not wrapped'); + assert + .dom('.ember-basic-dropdown-trigger', getRootNode(this.element)) + .hasText('Click me', 'The trigger contains the given block'); + }); + + test('If a `@defaultClass` argument is provided to the trigger, its value is added to the list of classes', async function (assert) { + assert.expect(1); + this.dropdown = { + ...dropdownBase, + uniqueId: '123', + }; + await render(hbs` +
+ Click me +
+ `); + + assert + .dom('.ember-basic-dropdown-trigger', getRootNode(this.element)) + .hasClass('extra-class'); + }); + + // Attributes and a11y + test("If it doesn't receive any tabindex, defaults to 0", async function (assert) { + assert.expect(1); + this.dropdown = { + ...dropdownBase, + uniqueId: '123', + }; + await render(hbs` + Click me + `); + + assert + .dom('.ember-basic-dropdown-trigger', getRootNode(this.element)) + .hasAttribute('tabindex', '0', 'Has a tabindex of 0'); + }); + + test('If it receives a tabindex={{false}}, it removes the tabindex', async function (assert) { + assert.expect(1); + this.dropdown = { + ...dropdownBase, + uniqueId: '123', + }; + await render(hbs` + Click me + `); + + assert + .dom('.ember-basic-dropdown-trigger', getRootNode(this.element)) + .doesNotHaveAttribute('tabindex'); + }); + + test('If it receives `tabindex="3"`, the tabindex of the element is 3', async function (assert) { + assert.expect(1); + this.dropdown = { + ...dropdownBase, + uniqueId: '123', + }; + await render(hbs` + Click me + `); + + assert + .dom('.ember-basic-dropdown-trigger', getRootNode(this.element)) + .hasAttribute('tabindex', '3', 'Has a tabindex of 3'); + }); + + test('If it receives `title="something"`, if has that title attribute', async function (assert) { + assert.expect(1); + this.dropdown = { + ...dropdownBase, + uniqueId: '123', + }; + await render(hbs` + Click me + `); + + assert + .dom('.ember-basic-dropdown-trigger', getRootNode(this.element)) + .hasAttribute('title', 'foobar', 'Has the given title'); + }); + + test('If it receives `id="some-id"`, if has that id', async function (assert) { + assert.expect(1); + this.dropdown = { + ...dropdownBase, + uniqueId: '123', + }; + await render(hbs` + Click me + `); + + assert + .dom('.ember-basic-dropdown-trigger', getRootNode(this.element)) + .hasAttribute('id', 'my-own-id', 'Has the given id'); + }); + + test("If the dropdown is disabled, the trigger doesn't have tabindex attribute", async function (assert) { + assert.expect(1); + this.dropdown = { + ...dropdownBase, + uniqueId: '123', + disabled: true, + }; + await render(hbs` + Click me + `); + + assert + .dom('.ember-basic-dropdown-trigger', getRootNode(this.element)) + .doesNotHaveAttribute('tabindex', "The component doesn't have tabindex"); + }); + + test('If it belongs to a disabled dropdown, it gets an `aria-disabled=true` attribute for a11y', async function (assert) { + assert.expect(2); + this.dropdown = { + ...dropdownBase, + uniqueId: '123', + disabled: true, + }; + await render(hbs` + Click me + `); + + assert + .dom('.ember-basic-dropdown-trigger', getRootNode(this.element)) + .hasAttribute('aria-disabled', 'true', 'It is marked as disabled'); + this.set('dropdown', { ...this.dropdown, disabled: false }); + assert + .dom('.ember-basic-dropdown-trigger', getRootNode(this.element)) + .hasAttribute('aria-disabled', 'false', 'It is NOT marked as disabled'); + }); + + test('If the received dropdown is open, it has an `aria-expanded="true"` attribute, otherwise `"false"`', async function (assert) { + assert.expect(2); + this.dropdown = { + ...dropdownBase, + uniqueId: '123', + isOpen: false, + }; + await render(hbs` + Click me + `); + assert + .dom('.ember-basic-dropdown-trigger', getRootNode(this.element)) + .hasAttribute('aria-expanded', 'false', 'the aria-expanded is false'); + this.set('dropdown', { ...this.dropdown, isOpen: true }); + assert + .dom('.ember-basic-dropdown-trigger', getRootNode(this.element)) + .hasAttribute('aria-expanded', 'true', 'the aria-expanded is true'); + }); + + test('If it has an `aria-controls="foo123"` attribute pointing to the id of the content', async function (assert) { + assert.expect(1); + this.dropdown = { + ...dropdownBase, + uniqueId: '123', + }; + await render(hbs` + Click me + `); + assert + .dom('.ember-basic-dropdown-trigger', getRootNode(this.element)) + .hasAttribute('aria-controls', 'ember-basic-dropdown-content-123'); + }); + + test('If it receives `@htmlTag`, the trigger uses that tag name', async function (assert) { + assert.expect(2); + this.dropdown = { + ...dropdownBase, + uniqueId: '123', + }; + await render(hbs` + Click me + `); + assert.strictEqual( + ( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement + ).tagName, + 'BUTTON', + ); + assert + .dom('.ember-basic-dropdown-trigger', getRootNode(this.element)) + .hasAttribute('type', 'button'); + }); + + test('If it receives `role="presentation"` it gets that attribute', async function (assert) { + assert.expect(1); + this.dropdown = { + ...dropdownBase, + uniqueId: '123', + }; + await render(hbs` + Click me + `); + assert + .dom('.ember-basic-dropdown-trigger', getRootNode(this.element)) + .hasAttribute('role', 'presentation'); + }); + + test('If it receives `aria-owns="custom-owns"` it gets that attribute', async function (assert) { + assert.expect(1); + this.dropdown = { + ...dropdownBase, + uniqueId: '123', + }; + await render(hbs` + Click me + `); + assert + .dom('.ember-basic-dropdown-trigger', getRootNode(this.element)) + .hasAttribute('aria-owns', 'custom-owns'); + }); + + test('If it receives `aria-controls="custom-controls"` it gets that attribute', async function (assert) { + assert.expect(1); + this.dropdown = { + ...dropdownBase, + uniqueId: '123', + }; + await render(hbs` + Click me + `); + assert + .dom('.ember-basic-dropdown-trigger', getRootNode(this.element)) + .hasAttribute('aria-controls', 'custom-controls'); + }); + + test('If it does not receive an specific `role`, the default is `button`', async function (assert) { + assert.expect(1); + this.dropdown = { + ...dropdownBase, + uniqueId: '123', + }; + await render(hbs` + Click me + `); + assert + .dom('.ember-basic-dropdown-trigger', getRootNode(this.element)) + .hasAttribute('role', 'button'); + }); + + // Custom actions + test('the user can bind arbitrary events to the trigger', async function (assert) { + assert.expect(2); + this.dropdown = { + ...dropdownBase, + uniqueId: '123', + }; + this.onMouseEnter = (dropdown, e) => { + assert.deepEqual( + dropdown, + this.dropdown, + 'receives the dropdown as 1st argument', + ); + assert.ok( + e instanceof window.Event, + 'It receives the event as second argument', + ); + }; + await render(hbs` + Click me + `); + await triggerEvent( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + 'mouseenter', + ); + }); + + // Default behaviour + test('click events invoke the `toggle` action on the dropdown by default', async function (assert) { + assert.expect(3); + this.dropdown = { + ...dropdownBase, + uniqueId: '123', + actions: { + ...dropdownBase.actions, + toggle(e) { + assert.ok(true, 'The `toggle()` action has been fired'); + assert.ok( + e instanceof window.Event, + 'It receives the event as first argument', + ); + assert.strictEqual( + arguments.length, + 1, + 'It receives only one argument', + ); + }, + }, + }; + await render(hbs` + Click me + `); + await triggerEvent( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + 'click', + ); + }); + + test('mousedown events DO NOT invoke the `toggle` action on the dropdown by default', async function (assert) { + assert.expect(0); + this.dropdown = { + ...dropdownBase, + uniqueId: '123', + actions: { + ...dropdownBase.actions, + toggle() { + assert.ok(false); + }, + }, + }; + await render(hbs` + Click me + `); + await triggerEvent( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + 'mousedown', + ); + }); + + test('click events DO NOT invoke the `toggle` action on the dropdown if `@eventType="mousedown"`', async function (assert) { + assert.expect(0); + this.dropdown = { + ...dropdownBase, + uniqueId: '123', + actions: { + ...dropdownBase.actions, + toggle() { + assert.ok(false); + }, + }, + }; + await render(hbs` + Click me + `); + await triggerEvent( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + 'click', + ); + }); + + test('mousedown events invoke the `toggle` action on the dropdown if `eventType="mousedown"', async function (assert) { + assert.expect(3); + this.dropdown = { + ...dropdownBase, + uniqueId: '123', + actions: { + ...dropdownBase.actions, + toggle(e) { + assert.ok(true, 'The `toggle()` action has been fired'); + assert.ok( + e instanceof window.Event, + 'It receives the event as first argument', + ); + assert.strictEqual( + arguments.length, + 1, + 'It receives only one argument', + ); + }, + }, + }; + await render(hbs` + Click me + `); + await triggerEvent( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + 'mousedown', + ); + }); + + test('when `stopPropagation` is true the `click` event does not bubble', async function (assert) { + assert.expect(3); + this.handlerInParent = () => + assert.ok(false, 'This should never be called'); + + this.dropdown = { + ...dropdownBase, + uniqueId: '123', + actions: { + ...dropdownBase.actions, + toggle(e) { + assert.ok(true, 'The `toggle()` action has been fired'); + assert.ok( + e instanceof window.Event, + 'It receives the event as first argument', + ); + assert.strictEqual( + arguments.length, + 1, + 'It receives only one argument', + ); + }, + }, + }; + await render(hbs` +
+ Click me +
+ `); + await triggerEvent( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + 'click', + ); + }); + + test('when `stopPropagation` is true and eventType is true, the `click` event does not bubble', async function (assert) { + assert.expect(3); + this.handlerInParent = () => + assert.ok(false, 'This should never be called'); + + this.dropdown = { + ...dropdownBase, + uniqueId: '123', + actions: { + ...dropdownBase.actions, + toggle(e) { + assert.ok(true, 'The `toggle()` action has been fired'); + assert.ok( + e instanceof window.Event, + 'It receives the event as first argument', + ); + assert.strictEqual( + arguments.length, + 1, + 'It receives only one argument', + ); + }, + }, + }; + await render(hbs` +
+ Click me +
+ `); + await triggerEvent( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + 'click', + ); + }); + + test('Pressing ENTER fires the `toggle` action on the dropdown', async function (assert) { + assert.expect(3); + this.dropdown = { + ...dropdownBase, + uniqueId: '123', + actions: { + ...dropdownBase.actions, + toggle(e) { + assert.ok(true, 'The `toggle()` action has been fired'); + assert.ok( + e instanceof window.Event, + 'It receives the event as first argument', + ); + assert.strictEqual( + arguments.length, + 1, + 'It receives only one argument', + ); + }, + }, + }; + await render(hbs` + Click me + `); + + await triggerKeyEvent( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + 'keydown', + 13, + ); + }); + + test('Pressing SPACE fires the `toggle` action on the dropdown and preventsDefault to avoid scrolling', async function (assert) { + assert.expect(4); + this.dropdown = { + ...dropdownBase, + uniqueId: '123', + actions: { + ...dropdownBase.actions, + toggle(e) { + assert.ok(true, 'The `toggle()` action has been fired'); + assert.ok( + e instanceof window.Event, + 'It receives the event as first argument', + ); + assert.strictEqual( + arguments.length, + 1, + 'It receives only one argument', + ); + assert.ok(e?.defaultPrevented, 'The event is defaultPrevented'); + }, + }, + }; + await render(hbs` + Click me + `); + + await triggerKeyEvent( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + 'keydown', + 32, + ); + }); + + test('Pressing ESC fires the `close` action on the dropdown', async function (assert) { + assert.expect(3); + this.dropdown = { + ...dropdownBase, + uniqueId: '123', + actions: { + ...dropdownBase.actions, + close(e) { + assert.ok(true, 'The `close()` action has been fired'); + assert.ok( + e instanceof window.Event, + 'It receives the event as first argument', + ); + assert.strictEqual( + arguments.length, + 1, + 'It receives only one argument', + ); + }, + }, + }; + await render(hbs` + Click me + `); + + await triggerKeyEvent( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + 'keydown', + 27, + ); + }); + + test('Pressing ENTER/SPACE/ESC does nothing if there is a `{{on "keydown"}}` event that calls stopImmediatePropagation', async function (assert) { + assert.expect(0); + this.keyDown = (e) => e.stopImmediatePropagation(); + this.dropdown = { + ...dropdownBase, + uniqueId: '123', + actions: { + ...dropdownBase.actions, + close() { + assert.ok(false, 'This action is not called'); + }, + toggle() { + assert.ok(false, 'This action is not called'); + }, + }, + }; + await render(hbs` + Click me + `); + + await triggerKeyEvent( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + 'keydown', + 13, + ); + await triggerKeyEvent( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + 'keydown', + 32, + ); + await triggerKeyEvent( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + 'keydown', + 27, + ); + }); + + test('Tapping invokes the toggle action on the dropdown', async function (assert) { + assert.expect(4); + this.dropdown = { + ...dropdownBase, + uniqueId: '123', + actions: { + ...dropdownBase.actions, + toggle(e) { + assert.ok(true, 'The `toggle()` action has been fired'); + assert.strictEqual( + e?.type, + 'touchend', + 'The event that toggles the dropdown is the touchend', + ); + assert.ok( + e instanceof window.Event, + 'It receives the event as first argument', + ); + assert.strictEqual( + arguments.length, + 1, + 'It receives only one argument', + ); + }, + }, + }; + await render(hbs` + Click me + `); + await tap( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + ); + }); + + test("Firing a mousemove between a touchstart and a touchend (touch scroll) doesn't fire the toggle action", async function (assert) { + assert.expect(0); + this.dropdown = { + ...dropdownBase, + uniqueId: '123', + actions: { + ...dropdownBase.actions, + toggle() { + assert.ok(false, 'This action in not called'); + }, + }, + }; + await render(hbs` + Click me + `); + + await triggerEvent( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + 'touchstart', + ); + await triggerEvent( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + 'touchmove', + { + changedTouches: [{ touchType: 'direct', pageX: 0, pageY: 0 }], + }, + ); + await triggerEvent( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + 'touchend', + { + changedTouches: [{ touchType: 'direct', pageX: 0, pageY: 10 }], + }, + ); + }); + + test('Using stylus on touch device will handle scroll/tap to fire toggle action properly', async function (assert) { + assert.expect(1); + this.dropdown = { + ...dropdownBase, + uniqueId: '123', + actions: { + ...dropdownBase.actions, + toggle() { + assert.ok(true, 'The toggle action is called'); + }, + reposition() { + return undefined; + }, + }, + }; + await render(hbs` + Click me + `); + + // scroll + await triggerEvent( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + 'touchstart', + ); + await triggerEvent( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + 'touchmove', + { + changedTouches: [{ touchType: 'stylus', pageX: 0, pageY: 0 }], + }, + ); + await triggerEvent( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + 'touchend', + { + changedTouches: [{ touchType: 'stylus', pageX: 0, pageY: 10 }], + }, + ); + + // tap + await triggerEvent( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + 'touchstart', + ); + await triggerEvent( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + 'touchmove', + { + changedTouches: [{ touchType: 'stylus', pageX: 0, pageY: 0 }], + }, + ); + await triggerEvent( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + 'touchend', + { + changedTouches: [{ touchType: 'stylus', pageX: 4, pageY: 0 }], + }, + ); + }); + + test("If its dropdown is disabled it won't respond to mouse, touch or keyboard event", async function (assert) { + assert.expect(0); + this.dropdown = { + ...dropdownBase, + uniqueId: '123', + disabled: true, + actions: { + ...dropdownBase.actions, + toggle() { + assert.ok(false, 'This action in not called'); + }, + open() { + assert.ok(false, 'This action in not called'); + }, + close() { + assert.ok(false, 'This action in not called'); + }, + }, + }; + await render(hbs` + Click me + `); + await click( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + ); + await tap('.ember-basic-dropdown-trigger'); + await triggerKeyEvent( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + 'keydown', + 13, + ); + await triggerKeyEvent( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + 'keydown', + 32, + ); + await triggerKeyEvent( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + 'keydown', + 27, + ); + }); + + // Decorating and overriding default event handlers + test('A user-supplied {{on "mousedown"}} callback will execute before the default toggle behavior', async function (assert) { + assert.expect(3); + let userActionRunFirst = false; + + this.dropdown = { + ...dropdownBase, + uniqueId: '123', + actions: { + ...dropdownBase.actions, + toggle: () => { + assert.ok( + userActionRunFirst, + 'User-supplied `{{on "mousedown"}}` ran before default `toggle`', + ); + }, + }, + }; + + this.mouseDown = (e: Event) => { + assert.ok(true, 'The `userSuppliedAction()` action has been fired'); + assert.ok(e instanceof window.Event, 'It receives the event'); + userActionRunFirst = true; + }; + + await render(hbs` + {{!-- template-lint-disable no-pointer-down-event-binding --}} + Click me + `); + + await click( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + ); + }); + + test('A user-supplied {{on "click"}} callback that calls `stopImmediatePropagation`, will prevent the default behavior', async function (assert) { + assert.expect(1); + + this.dropdown = { + ...dropdownBase, + uniqueId: '123', + actions: { + ...dropdownBase.actions, + toggle: () => { + assert.ok(false, 'Default `toggle` action should not run'); + }, + }, + }; + + this.click = (e: Event) => { + e.stopImmediatePropagation(); + assert.ok(true, 'The `userSuppliedAction()` action has been fired'); + }; + + await render(hbs` + Click me + `); + + await click( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + ); + }); + + test('A user-supplied {{on "mousedown"}} callback that calls `stopImmediatePropagation` will prevent the default behavior', async function (assert) { + assert.expect(1); + + this.dropdown = { + ...dropdownBase, + uniqueId: '123', + actions: { + ...dropdownBase.actions, + toggle: () => { + assert.ok(false, 'Default `toggle` action should not run'); + }, + }, + }; + + this.mouseDown = (e: Event) => { + e.stopImmediatePropagation(); + assert.ok(true, 'The user-supplied action has been fired'); + }; + + await render(hbs` + {{!-- template-lint-disable no-pointer-down-event-binding --}} + Click me + `); + + await click( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + ); + }); + + test('A user-supplied {{on "touchend"}} callback will execute before the default toggle behavior', async function (assert) { + assert.expect(3); + let userActionRunFirst = false; + + this.dropdown = { + ...dropdownBase, + uniqueId: '123', + actions: { + ...dropdownBase.actions, + toggle: () => { + assert.ok( + userActionRunFirst, + 'User-supplied `{{on "touchend"}}` ran before default `toggle`', + ); + }, + }, + }; + + this.touchEnd = (e: Event) => { + assert.ok(true, 'The user-supplied touchend callback has been fired'); + assert.ok(e instanceof window.Event, 'It receives the event'); + userActionRunFirst = true; + }; + + await render(hbs` + + Click me + + `); + await tap( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + ); + }); + + test('A user-supplied {{on "touchend"}} callback calling e.stopImmediatePropagation will prevent the default behavior', async function (assert) { + assert.expect(2); + + this.dropdown = { + ...dropdownBase, + uniqueId: '123', + actions: { + ...dropdownBase.actions, + toggle: (e) => { + assert.notEqual( + e?.type, + 'touchend', + 'Default `toggle` action should not run', + ); + }, + }, + }; + + this.touchEnd = (e: Event) => { + e.stopImmediatePropagation(); + assert.ok(true, 'The user-supplied touchend callback has been fired'); + }; + + await render(hbs` + + Click me + + `); + await tap( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + ); + }); + + test('A user-supplied `{{on "keydown"}}` action will execute before the default toggle behavior', async function (assert) { + assert.expect(3); + let userActionRunFirst = false; + + this.dropdown = { + ...dropdownBase, + uniqueId: '123', + actions: { + ...dropdownBase.actions, + toggle: () => { + assert.ok( + userActionRunFirst, + 'User-supplied `{{on "keydown}}` ran before default `toggle`', + ); + }, + }, + }; + + this.keyDown = (e: Event) => { + assert.ok(true, 'The `userSuppliedAction()` action has been fired'); + assert.ok(e instanceof window.Event, 'It receives the event'); + userActionRunFirst = true; + }; + + await render(hbs` + Click me + `); + await triggerKeyEvent( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + 'keydown', + 13, + ); // Enter + }); + + test('A user-supplied `{{on "keydown"}}` action calling `stopImmediatePropagation` will prevent the default behavior', async function (assert) { + assert.expect(1); + + this.dropdown = { + ...dropdownBase, + uniqueId: '123', + actions: { + ...dropdownBase.actions, + toggle: () => { + assert.ok(false, 'Default `toggle` action should not run'); + }, + }, + }; + + this.keyDown = (e: Event) => { + e.stopImmediatePropagation(); + assert.ok(true, 'The `userSuppliedAction()` action has been fired'); + }; + + await render(hbs` + Click me + `); + await triggerKeyEvent( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + 'keydown', + 13, + ); // Enter + }); + + test('Tapping an SVG inside of the trigger invokes the toggle action on the dropdown', async function (assert) { + assert.expect(3); + this.dropdown = { + ...dropdownBase, + uniqueId: '123', + actions: { + ...dropdownBase.actions, + toggle(e) { + assert.ok(true, 'The `toggle()` action has been fired'); + assert.ok( + e instanceof window.Event, + 'It receives the event as first argument', + ); + assert.strictEqual( + arguments.length, + 1, + 'It receives only one argument', + ); + }, + }, + }; + await render(hbs` + Click me + `); + await tap( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + ); + }); + + /** + * Tests related to https://github.com/cibernox/ember-basic-dropdown/issues/498 + * Can be removed when the template `V1` compatability event handlers are removed. + */ + module('trigger event handlers', function (hooks) { + hooks.beforeEach(function () { + this.set('dropdown', { + ...dropdownBase, + uniqueId: 'e123', + actions: { ...dropdownBase.actions, toggle: () => {} }, + }); + }); + + function assertCommonEventHandlerArgs( + this: ExtendedTestContext, + assert: Assert, + args: [Dropdown, Event | undefined], + ) { + const [dropdown, e] = args; + + assert.ok( + dropdown.uniqueId === this.dropdown.uniqueId, + 'It receives the dropdown argument as the first argument', + ); + assert.ok( + e instanceof window.Event, + 'It receives the event as second argument', + ); + assert.ok(args.length === 2, 'It receives only 2 arguments'); + } + + test('It properly handles the onBlur action', async function (assert) { + assert.expect(4); + + const onBlur = (dropdown: Dropdown, e?: FocusEvent) => { + assert.ok(true, 'The `onBlur()` action has been fired'); + assertCommonEventHandlerArgs.call(this, assert, [dropdown, e]); + }; + + this.set('onBlur', onBlur); + + await render(hbs` + hello + `); + await triggerEvent( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + 'blur', + ); // For some reason, `blur` test-helper fails here + }); + + test('It properly handles the onClick action', async function (assert) { + assert.expect(4); + + const onClick = (dropdown: Dropdown, e?: MouseEvent) => { + assert.ok(true, 'The `onClick()` action has been fired'); + assertCommonEventHandlerArgs.call(this, assert, [dropdown, e]); + }; + + this.set('onClick', onClick); + + await render(hbs` + hello + `); + await click( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + ); + }); + + test('It properly handles the onFocus action', async function (assert) { + assert.expect(4); + + const onFocus = (dropdown: Dropdown, e?: FocusEvent) => { + assert.ok(true, 'The `onFocus()` action has been fired'); + assertCommonEventHandlerArgs.call(this, assert, [dropdown, e]); + }; + + this.set('onFocus', onFocus); + + await render(hbs` + hello + `); + await focus( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + ); + }); + + test('It properly handles the onFocusIn action', async function (assert) { + assert.expect(4); + + const onFocusIn = (dropdown: Dropdown, e?: FocusEvent) => { + assert.ok(true, 'The `onFocusIn()` action has been fired'); + assertCommonEventHandlerArgs.call(this, assert, [dropdown, e]); + }; + + this.set('onFocusIn', onFocusIn); + + await render(hbs` + hello + `); + await triggerEvent( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + 'focusin', + ); + }); + + test('It properly handles the onFocusOut action', async function (assert) { + assert.expect(4); + + const onFocusOut = (dropdown: Dropdown, e?: FocusEvent) => { + assert.ok(true, 'The `onFocusOut()` action has been fired'); + assertCommonEventHandlerArgs.call(this, assert, [dropdown, e]); + }; + + this.set('onFocusOut', onFocusOut); + + await render(hbs` + hello + `); + await triggerEvent( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + 'focusout', + ); + }); + + test('It properly handles the onKeyDown action', async function (assert) { + assert.expect(4); + + const onKeyDown = (dropdown: Dropdown, e?: KeyboardEvent) => { + assert.ok(true, 'The `onKeyDown()` action has been fired'); + assertCommonEventHandlerArgs.call(this, assert, [dropdown, e]); + }; + + this.set('onKeyDown', onKeyDown); + + await render(hbs` + hello + `); + await triggerEvent( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + 'keydown', + ); + }); + + test('It properly handles the onMouseDown action', async function (assert) { + assert.expect(4); + + const onMouseDown = (dropdown: Dropdown, e?: MouseEvent) => { + assert.ok(true, 'The `onMouseDown()` action has been fired'); + assertCommonEventHandlerArgs.call(this, assert, [dropdown, e]); + }; + + this.set('onMouseDown', onMouseDown); + + await render(hbs` + hello + `); + await triggerEvent( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + 'mousedown', + ); + }); + + test('It properly handles the onMouseEnter action', async function (assert) { + assert.expect(4); + + const onMouseEnter = (dropdown: Dropdown, e?: MouseEvent) => { + assert.ok(true, 'The `onMouseEnter()` action has been fired'); + assertCommonEventHandlerArgs.call(this, assert, [dropdown, e]); + }; + + this.set('onMouseEnter', onMouseEnter); + + await render(hbs` + hello + `); + await triggerEvent('.ember-basic-dropdown-trigger', 'mouseenter'); + }); + + test('It properly handles the onMouseLeave action', async function (assert) { + assert.expect(4); + + const onMouseLeave = (dropdown: Dropdown, e?: MouseEvent) => { + assert.ok(true, 'The `onMouseLeave()` action has been fired'); + assertCommonEventHandlerArgs.call(this, assert, [dropdown, e]); + }; + + this.set('onMouseLeave', onMouseLeave); + + await render(hbs` + hello + `); + await triggerEvent( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + 'mouseleave', + ); + }); + + test('It properly handles the onTouchEnd action', async function (assert) { + assert.expect(4); + + const onTouchEnd = (dropdown: Dropdown, e?: MouseEvent) => { + assert.ok(true, 'The `onTouchEnd()` action has been fired'); + assertCommonEventHandlerArgs.call(this, assert, [dropdown, e]); + }; + + this.set('onTouchEnd', onTouchEnd); + + await render(hbs` + hello + `); + await triggerEvent( + getRootNode(this.element).querySelector( + '.ember-basic-dropdown-trigger', + ) as HTMLElement, + 'touchend', + ); + }); + }); +}); diff --git a/test-app/tests/unit/utils/calculate-position-test.js b/test-app/tests/unit/utils/calculate-position-test.ts similarity index 100% rename from test-app/tests/unit/utils/calculate-position-test.js rename to test-app/tests/unit/utils/calculate-position-test.ts diff --git a/test-app/tests/unit/utils/scroll-helpers-test.js b/test-app/tests/unit/utils/scroll-helpers-test.ts similarity index 96% rename from test-app/tests/unit/utils/scroll-helpers-test.js rename to test-app/tests/unit/utils/scroll-helpers-test.ts index 484685e7..b51bd7b6 100644 --- a/test-app/tests/unit/utils/scroll-helpers-test.js +++ b/test-app/tests/unit/utils/scroll-helpers-test.ts @@ -13,7 +13,7 @@ import { module, test } from 'qunit'; module('Unit | Utility | scroll helpers', function () { test('getScrollLineHeight', function (assert) { // Depends on device and settings. - let result = getScrollLineHeight(); + const result = getScrollLineHeight(); // Also blows up on 0, which is an invalid value. assert.ok(result, 'did not throw errors'); @@ -90,7 +90,7 @@ module('Unit | Utility | scroll helpers', function () { }); test('getScrollDeltas DOM_DELTA_LINE', function (assert) { - const scrollLineHeight = getScrollLineHeight(); + const scrollLineHeight = getScrollLineHeight() || 0; const originalDeltaX = 25; const originalDeltaY = 15; const { deltaX, deltaY } = getScrollDeltas({ @@ -103,7 +103,7 @@ module('Unit | Utility | scroll helpers', function () { }); test('getScrollDeltas DOM_DELTA_PAGE', function (assert) { - const scrollLineHeight = getScrollLineHeight(); + const scrollLineHeight = getScrollLineHeight() || 0; const originalDeltaX = 25; const originalDeltaY = 15; const { deltaX, deltaY } = getScrollDeltas({