Skip to content

Commit

Permalink
add dropdown primitive ctd
Browse files Browse the repository at this point in the history
  • Loading branch information
amk221 committed Dec 11, 2024
1 parent bbb302d commit fedeb4c
Show file tree
Hide file tree
Showing 18 changed files with 288 additions and 207 deletions.
6 changes: 4 additions & 2 deletions addon/components/select-box/index.gjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import SelectBoxOption from '@zestia/ember-select-box/components/select-box/opti
import SelectBoxOptions from '@zestia/ember-select-box/components/select-box/options';
const { assign } = Object;

const SELECTED = Symbol('SELECTED');

export default class SelectBox extends Component {
@tracked _activeOption;
@tracked _options = tracked([]);
Expand Down Expand Up @@ -337,7 +339,7 @@ export default class SelectBox extends Component {
handleCloseDropdown(reason) {
this._forgetActiveOption();

if (reason?.description !== 'FOCUS_LEAVE') {
if (reason === SELECTED) {
this._ensureFocus();
}
}
Expand Down Expand Up @@ -431,7 +433,7 @@ export default class SelectBox extends Component {
const close = this.args.onSelect?.(this.api) ?? this.canAutoClose;

if (close) {
this.dropdown.close();
this.dropdown.close(SELECTED);
}
}

Expand Down
2 changes: 1 addition & 1 deletion tests/integration/components/dropdown/index/api-test.gjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { setupRenderingTest } from 'dummy/tests/helpers';
import { find, render, rerender, click } from '@ember/test-helpers';
import Dropdown from '@zestia/ember-select-box/components/dropdown';

module('dropdownx (api)', function (hooks) {
module('dropdown (api)', function (hooks) {
setupRenderingTest(hooks);

test('api', async function (assert) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { setupRenderingTest } from 'dummy/tests/helpers';
import { render, click } from '@ember/test-helpers';
import Dropdown from '@zestia/ember-select-box/components/dropdown';

module('dropdownx (clicking trigger)', function (hooks) {
module('dropdown (clicking trigger)', function (hooks) {
setupRenderingTest(hooks);

test('clicking trigger', async function (assert) {
Expand Down
186 changes: 94 additions & 92 deletions tests/integration/components/dropdown/index/closing-test.gjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,46 +4,21 @@ import {
render,
click,
rerender,
focus,
blur,
triggerEvent,
triggerKeyEvent
} from '@ember/test-helpers';
import Dropdown from '@zestia/ember-select-box/components/dropdown';
import { on } from '@ember/modifier';
import { tracked } from '@glimmer/tracking';
import autoFocus from '@zestia/ember-auto-focus/modifiers/auto-focus';

module('dropdownx (closing)', function (hooks) {
module('dropdown (closing)', function (hooks) {
setupRenderingTest(hooks);

let handleClose;

hooks.beforeEach(function (assert) {
handleClose = (reason) => assert.step(`close ${reason.description}`);
});

test('closing with api', async function (assert) {
assert.expect(3);

let api;

const handleReady = (dd) => (api = dd);

await render(<template>
<Dropdown @onReady={{handleReady}} as |dd|>
<dd.Trigger />
</Dropdown>
</template>);

await click('.dropdown__trigger');

assert.dom('.dropdown').hasAttribute('data-open', 'true');

api.close();

await rerender();

assert.dom('.dropdown').hasAttribute('data-open', 'false');
assert.dom('.dropdown__trigger').isFocused();
handleClose = (reason) => assert.step(`close ${reason?.description}`);
});
test('pressing escape', async function (assert) {
Expand Down Expand Up @@ -81,7 +56,7 @@ module('dropdownx (closing)', function (hooks) {
assert.dom('.dropdown').hasAttribute('data-open', 'false');
assert.dom('.dropdown__trigger').isNotFocused();
assert.verifySteps(['close FOCUS_LEAVE']);
assert.verifySteps(['close FOCUS_LEAVE'], 'focusout fires before mouseup');
});
test('clicking dropdown container', async function (assert) {
Expand Down Expand Up @@ -109,7 +84,7 @@ module('dropdownx (closing)', function (hooks) {
});
test('clicking a non interactive element inside the dropdown content', async function (assert) {
assert.expect(6);
assert.expect(7);
// Selecting text won't cause the dropdown to close.
Expand All @@ -136,64 +111,37 @@ module('dropdownx (closing)', function (hooks) {
await click('.outside');
assert.dom('.dropdown').hasAttribute('data-open', 'false');
assert.verifySteps(['close CLICK_OUTSIDE']);
});
test('clicking an interactive element inside the dropdown container', async function (assert) {
assert.expect(4);

await render(<template>
<Dropdown @onClose={{handleClose}} as |dd|>
<dd.Trigger />
<button type="button" class="inside" />
</Dropdown>
</template>);

await click('.dropdown__trigger');

assert.dom('.dropdown').hasAttribute('data-open', 'true');

await click('.inside');

assert.dom('.dropdown').hasAttribute('data-open', 'true');
assert.dom('.inside').isFocused();
assert.verifySteps([]);
});

test('clicking an interactive element inside the dropdown content', async function (assert) {
test('closing with exposed api', async function (assert) {
assert.expect(5);
let event;
let api;
const handleMouseDown = (_event) => (event = _event);
const handleReady = (dd) => (api = dd);
await render(<template>
{{! template-lint-disable no-pointer-down-event-binding }}
<Dropdown
@onClose={{handleClose}}
{{on "mousedown" handleMouseDown}}
as |dd|
>
<Dropdown @onReady={{handleReady}} @onClose={{handleClose}} as |dd|>
<dd.Trigger />
<dd.Content>
<button type="button" class="inside" />
</dd.Content>
</Dropdown>
</template>);
await click('.dropdown__trigger');
assert.dom('.dropdown').hasAttribute('data-open', 'true');
await click('.inside');
api.close();
assert.dom('.dropdown').hasAttribute('data-open', 'true');
assert.dom('.dropdown__trigger').isNotFocused();
assert.false(event.defaultPrevented);
assert.verifySteps([]);
await rerender();
assert.dom('.dropdown').hasAttribute('data-open', 'false');
assert.dom('.dropdown__trigger').isFocused();
assert.verifySteps(['close undefined']);
});
test('closing with api', async function (assert) {
test('closing with yielded api', async function (assert) {
assert.expect(5);
await render(<template>
Expand All @@ -219,12 +167,12 @@ module('dropdownx (closing)', function (hooks) {
});
test('mousing down on the trigger but mousing up outside', async function (assert) {
assert.expect(3);
assert.expect(5);
// aka click-abort
await render(<template>
<Dropdown as |dd|>
<Dropdown @onClose={{handleClose}} as |dd|>
<dd.Trigger />
</Dropdown>
<div class="outside"></div>
Expand All @@ -239,40 +187,94 @@ module('dropdownx (closing)', function (hooks) {
await triggerEvent('.outside', 'mouseup');
assert.dom('.dropdown').hasAttribute('data-open', 'false');
assert.verifySteps(['close CLICK_OUTSIDE']);
});
test('closing does not steal focus', async function (assert) {
assert.expect(1);
test('focus leaving the dropdown trigger', async function (assert) {
assert.expect(4);
await render(<template>
<Dropdown @onClose={{handleClose}} as |dd|>
<dd.Trigger />
</Dropdown>
</template>);
await click('.dropdown__trigger');
assert.dom('.dropdown').hasAttribute('data-open', 'true');
const state = new (class {
@tracked showInput;
})();
await blur('.dropdown__trigger');
const handleClick = () => {
state.showInput = true;
};
assert.dom('.dropdown').hasAttribute('data-open', 'false');
assert.verifySteps(['close FOCUS_LEAVE']);
});
test('focus leaving the dropdown trigger when manually opened', async function (assert) {
assert.expect(4);
await render(<template>
<Dropdown as |dd|>
<Dropdown @onClose={{handleClose}} @open={{true}} as |dd|>
<dd.Trigger />
</Dropdown>
<button type="button" class="outside" />
</template>);
await focus('.dropdown__trigger');
assert.dom('.dropdown').hasAttribute('data-open', 'true');
await focus('.outside');
assert.dom('.dropdown').hasAttribute('data-open', 'false');
assert.verifySteps(['close FOCUS_LEAVE']);
});
test('focus leaving an interactive element inside the dropdown', async function (assert) {
assert.expect(4);
await render(<template>
<Dropdown @onClose={{handleClose}} as |dd|>
<dd.Trigger />
<button type="button" class="inside" />
</Dropdown>
<button type="button" class="outside" />
</template>);
await click('.dropdown__trigger');
await focus('.inside');
assert.dom('.dropdown').hasAttribute('data-open', 'true');
await focus('.outside');
assert.dom('.dropdown').hasAttribute('data-open', 'false');
assert.verifySteps(['close FOCUS_LEAVE']);
});
test('focus leaving an interactive element inside the content', async function (assert) {
assert.expect(4);
await render(<template>
<Dropdown @onClose={{handleClose}} as |dd|>
<dd.Trigger />
<dd.Content>
<button
type="button"
class="inside"
{{on "click" handleClick}}
{{on "click" dd.close}}
/>
<button type="button" class="inside" />
</dd.Content>
</Dropdown>
{{#if state.showInput}}
<input class="outside" aria-label="Example" {{autoFocus}} />
{{/if}}
<button type="button" class="outside" />
</template>);
await click('.dropdown__trigger');
await click('.inside');
await focus('.inside');
assert.dom('.dropdown').hasAttribute('data-open', 'true');
assert.dom('.outside').isFocused();
await focus('.outside');
assert.dom('.dropdown').hasAttribute('data-open', 'false');
assert.verifySteps(['close FOCUS_LEAVE']);
});
});
40 changes: 40 additions & 0 deletions tests/integration/components/dropdown/index/edge-case-test.gjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { module, skip } from 'qunit';
import { setupRenderingTest } from 'dummy/tests/helpers';
import { render, focus, triggerKeyEvent } from '@ember/test-helpers';
import Dropdown from '@zestia/ember-select-box/components/dropdown';
import { tracked } from '@glimmer/tracking';
import { on } from '@ember/modifier';

module('dropdown (edge cases)', function (hooks) {
setupRenderingTest(hooks);

skip('destroying a dropdown with a focus inside (and no trigger)', async function (assert) {
assert.expect(1);

const state = new (class {
@tracked show = true;
})();

const destroy = (value) => {
state.show = false;
};

await render(<template>
{{#if state.show}}
<Dropdown as |dd|>
<input aria-label="example" />
<button type="button" {{on "keydown" dd.open}} class="open" />
<button type="button" {{on "keydown" destroy}} class="close" />
</Dropdown>
{{/if}}
</template>);

await triggerKeyEvent('.open', 'keydown', 'Enter');
await focus('input');
await triggerKeyEvent('.close', 'keydown', 'Enter');

assert
.dom('.select-box')
.doesNotExist('does not cause infinite revalidation bug');
});
});
Loading

0 comments on commit fedeb4c

Please sign in to comment.