Skip to content

Commit

Permalink
feat(menu): implement search functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
alexlafroscia committed Jun 26, 2021
1 parent 4ff9fec commit 449d3ed
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 11 deletions.
3 changes: 2 additions & 1 deletion addon/components/menu.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
goToNextItem=this.goToNextItem
goToPreviousItem=this.goToPreviousItem
goToItem=this.goToItem
search=(perform this.searchTask)
)
)
}}
</div>
</div>
21 changes: 21 additions & 0 deletions addon/components/menu.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { restartableTask, timeout } from 'ember-concurrency';
import { guidFor } from '@ember/object/internals';
import { next } from '@ember/runloop';

Expand All @@ -9,6 +10,7 @@ export default class Menu extends Component {
@tracked isOpen = false;
@tracked items = [];
@tracked activeItem;
@tracked searchTerm = '';

get activeItemIndex() {
return this.items.indexOf(this.activeItem);
Expand Down Expand Up @@ -86,6 +88,25 @@ export default class Menu extends Component {
this._setActiveItem(item);
}

@restartableTask
*searchTask(nextCharacter) {
this.searchTerm += nextCharacter.toLowerCase();

const searchResult = this.items.find((item) => {
const textValue = item.element.textContent.toLowerCase().trim();

return item.isEnabled && textValue.startsWith(this.searchTerm);
});

if (searchResult) {
this._setActiveItem(searchResult);
}

yield timeout(350);

this.searchTerm = '';
}

@action
registerItem(item) {
let { items } = this;
Expand Down
4 changes: 4 additions & 0 deletions addon/components/menu/items.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export default class Items extends Component {
return this.args.goToNextItem();
case 'ArrowUp':
return this.args.goToPreviousItem();
default:
if (event.key.length === 1) {
return this.args.search(event.key);
}
}
}
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"ember-cli-babel": "^7.26.3",
"ember-cli-htmlbars": "^5.7.1",
"ember-click-outside-modifier": "^2.0.0",
"ember-concurrency": "^2.1.0",
"ember-element-helper": "^0.3.1",
"ember-focus-trap": "^0.7.0",
"ember-truth-helpers": "^3.0.0"
Expand Down
128 changes: 123 additions & 5 deletions tests/integration/components/menu-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,20 @@ function assertNoActiveMenuItem(menuSelector) {
QUnit.assert.dom(menuSelector).doesNotHaveAria('activedescendant');
}

async function type(selector, value) {
let finalKeyEventProcessing;

for (const char of value) {
finalKeyEventProcessing = triggerKeyEvent(
selector,
'keydown',
char.toUpperCase()
);
}

await finalKeyEventProcessing;
}

module('Integration | Component | <Menu>', (hooks) => {
setupRenderingTest(hooks);

Expand Down Expand Up @@ -491,11 +505,115 @@ module('Integration | Component | <Menu>', (hooks) => {
// - it should be possible to use the PageUp key to go to the last menu item if that is the only non-disabled menu item
// - it should have no active menu item upon PageUp key press, when there are no non-disabled menu items

// TODO: 'Any` key aka search'
// - it should be possible to type a full word that has a perfect match
// - it should be possible to type a partial of a word
// - it should be possible to type words with spaces
// - it should not be possible to search for a disabled item
module('`Any` key aka search', function () {
test('it should be possible to type a full word that has a perfect match', async (assert) => {
await render(hbs`
<Menu data-test-menu as |menu|>
<menu.Button data-test-menu-button>Trigger</menu.Button>
<menu.Items data-test-menu-items as |items|>
<items.Item as |item|>
<item.Element data-test-a data-test-is-active={{item.isActive}}>
alice
</item.Element>
</items.Item>
<items.Item as |item|>
<item.Element data-test-b data-test-is-active={{item.isActive}}>
bob
</item.Element>
</items.Item>
</menu.Items>
</Menu>
`);

await click('[data-test-menu-button]');
await type('[data-test-menu-items]', 'bob');

assert.dom('[data-test-b]').hasAttribute('data-test-is-active');
});

test('it should be possible to type a partial of a word', async (assert) => {
await render(hbs`
<Menu data-test-menu as |menu|>
<menu.Button data-test-menu-button>Trigger</menu.Button>
<menu.Items data-test-menu-items as |items|>
<items.Item as |item|>
<item.Element data-test-a data-test-is-active={{item.isActive}}>
alice
</item.Element>
</items.Item>
<items.Item as |item|>
<item.Element data-test-b data-test-is-active={{item.isActive}}>
bob
</item.Element>
</items.Item>
</menu.Items>
</Menu>
`);

await click('[data-test-menu-button]');
await type('[data-test-menu-items]', 'bo');

assert.dom('[data-test-b]').hasAttribute('data-test-is-active');

await type('[data-test-menu-items]', 'ali');

assert.dom('[data-test-a]').hasAttribute('data-test-is-active');
});

test('it should be possible to type words with spaces', async (assert) => {
await render(hbs`
<Menu data-test-menu as |menu|>
<menu.Button data-test-menu-button>Trigger</menu.Button>
<menu.Items data-test-menu-items as |items|>
<items.Item as |item|>
<item.Element data-test-a data-test-is-active={{item.isActive}}>
value a
</item.Element>
</items.Item>
<items.Item as |item|>
<item.Element data-test-b data-test-is-active={{item.isActive}}>
value b
</item.Element>
</items.Item>
</menu.Items>
</Menu>
`);

await click('[data-test-menu-button]');
await type('[data-test-menu-items]', 'value b');

assert.dom('[data-test-b]').hasAttribute('data-test-is-active');

await type('[data-test-menu-items]', 'value a');

assert.dom('[data-test-a]').hasAttribute('data-test-is-active');
});

test('it should not be possible to search for a disabled item', async () => {
await render(hbs`
<Menu data-test-menu as |menu|>
<menu.Button data-test-menu-button>Trigger</menu.Button>
<menu.Items data-test-menu-items as |items|>
<items.Item as |item|>
<item.Element data-test-a data-test-is-active={{item.isActive}}>
value a
</item.Element>
</items.Item>
<items.Item @isDisabled={{true}} as |item|>
<item.Element data-test-b data-test-is-active={{item.isActive}}>
value b
</item.Element>
</items.Item>
</menu.Items>
</Menu>
`);

await click('[data-test-menu-button]');
await type('[data-test-menu-items]', 'value b');

assertNoActiveMenuItem('[data-test-menu-items]');
});
});

module('Mouse interactions', function () {
test('it should be possible to open and close a menu on click', async () => {
Expand Down
54 changes: 49 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1270,7 +1270,7 @@
"@handlebars/parser" "^1.1.0"
simple-html-tokenizer "^0.5.10"

"@glimmer/tracking@^1.0.4":
"@glimmer/tracking@^1.0.2", "@glimmer/tracking@^1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@glimmer/tracking/-/tracking-1.0.4.tgz#f1bc1412fe5e2236d0f8d502994a8f88af1bbb21"
integrity sha512-F+oT8I55ba2puSGIzInmVrv/8QA2PcK1VD+GWgFMhF6WC97D+uZX7BFg+a3s/2N4FVBq5KHE+QxZzgazM151Yw==
Expand Down Expand Up @@ -2556,7 +2556,7 @@ babel-plugin-ember-modules-api-polyfill@^2.6.0:
dependencies:
ember-rfc176-data "^0.3.13"

babel-plugin-ember-modules-api-polyfill@^3.5.0:
babel-plugin-ember-modules-api-polyfill@^3.4.0, babel-plugin-ember-modules-api-polyfill@^3.5.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/babel-plugin-ember-modules-api-polyfill/-/babel-plugin-ember-modules-api-polyfill-3.5.0.tgz#27b6087fac75661f779f32e60f94b14d0e9f6965"
integrity sha512-pJajN/DkQUnStw0Az8c6khVcMQHgzqWr61lLNtVeu0g61LRW0k9jyK7vaedrHDWGe/Qe8sxG5wpiyW9NsMqFzA==
Expand All @@ -2576,6 +2576,13 @@ babel-plugin-htmlbars-inline-precompile@^3.2.0:
resolved "https://registry.yarnpkg.com/babel-plugin-htmlbars-inline-precompile/-/babel-plugin-htmlbars-inline-precompile-3.2.0.tgz#c4882ea875d0f5683f0d91c1f72e29a4f14b5606"
integrity sha512-IUeZmgs9tMUGXYu1vfke5I18yYJFldFGdNFQOWslXTnDWXzpwPih7QFduUqvT+awDpDuNtXpdt5JAf43Q1Hhzg==

babel-plugin-htmlbars-inline-precompile@^4.2.0:
version "4.4.6"
resolved "https://registry.yarnpkg.com/babel-plugin-htmlbars-inline-precompile/-/babel-plugin-htmlbars-inline-precompile-4.4.6.tgz#9fd632ad2717226b90bde6940b4148b3a323fddb"
integrity sha512-h/HA2T+iKL/AmmOaaH5w107F8G/foMPyapuMWFtwqa+RqHYNiaNg73JCQ13XMa2SJGPYckHE9hKgjV699k1tVA==
dependencies:
babel-plugin-ember-modules-api-polyfill "^3.4.0"

babel-plugin-htmlbars-inline-precompile@^5.0.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/babel-plugin-htmlbars-inline-precompile/-/babel-plugin-htmlbars-inline-precompile-5.3.0.tgz#eeaff07c35415264aea4d6bafb5e71167f6ffb2f"
Expand Down Expand Up @@ -3560,7 +3567,7 @@ broccoli-persistent-filter@^2.1.0, broccoli-persistent-filter@^2.2.1, broccoli-p
sync-disk-cache "^1.3.3"
walk-sync "^1.0.0"

broccoli-persistent-filter@^3.1.2:
broccoli-persistent-filter@^3.1.0, broccoli-persistent-filter@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/broccoli-persistent-filter/-/broccoli-persistent-filter-3.1.2.tgz#41da6b9577be09a170ecde185f2c5a6099f99c4e"
integrity sha512-CbU95RXXVyy+eJV9XTiHUC7NnsY3EvdVrGzp3YgyvO2bzXZFE5/GzDp4X/VQqX+jsk4qyT1HvMOF0sD1DX68TQ==
Expand Down Expand Up @@ -5266,7 +5273,28 @@ ember-cli-htmlbars@^4.2.2:
strip-bom "^4.0.0"
walk-sync "^2.0.2"

ember-cli-htmlbars@^5.1.0, ember-cli-htmlbars@^5.3.1, ember-cli-htmlbars@^5.7.1:
ember-cli-htmlbars@^5.1.0:
version "5.3.1"
resolved "https://registry.yarnpkg.com/ember-cli-htmlbars/-/ember-cli-htmlbars-5.3.1.tgz#61793964fc2599ce750db9e972ab55c6dd177c48"
integrity sha512-ZjQTt44euDoqLvUkWbt1svgNCXgLzOztEbc2qqYMQvhQig416LMrWK7l3SSbNU+BtLD5UIxmwvLfF1tsO2CVyA==
dependencies:
"@ember/edition-utils" "^1.2.0"
babel-plugin-htmlbars-inline-precompile "^4.2.0"
broccoli-debug "^0.6.5"
broccoli-persistent-filter "^3.1.0"
broccoli-plugin "^4.0.3"
common-tags "^1.8.0"
ember-cli-babel-plugin-helpers "^1.1.0"
fs-tree-diff "^2.0.1"
hash-for-dep "^1.5.1"
heimdalljs-logger "^0.1.10"
json-stable-stringify "^1.0.1"
semver "^7.3.2"
silent-error "^1.1.1"
strip-bom "^4.0.0"
walk-sync "^2.2.0"

ember-cli-htmlbars@^5.3.1, ember-cli-htmlbars@^5.6.3, ember-cli-htmlbars@^5.7.1:
version "5.7.1"
resolved "https://registry.yarnpkg.com/ember-cli-htmlbars/-/ember-cli-htmlbars-5.7.1.tgz#eb5b88c7d9083bc27665fb5447a9b7503b32ce4f"
integrity sha512-9laCgL4tSy48orNoQgQKEHp93MaqAs9ZOl7or5q+8iyGGJHW6sVXIYrVv5/5O9HfV6Ts8/pW1rSoaeKyLUE+oA==
Expand Down Expand Up @@ -5587,6 +5615,17 @@ ember-compatibility-helpers@^1.1.2, ember-compatibility-helpers@^1.2.0, ember-co
fs-extra "^9.1.0"
semver "^5.4.1"

ember-concurrency@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/ember-concurrency/-/ember-concurrency-2.1.0.tgz#5e55c19f43fb245c4fbe0628cbf26cc6561af40c"
integrity sha512-NIJfphS9NvO3Fin+QPQkTvhD8rFDc9ydpy+my+VFLcCfC5F+yI6sr5tHVSgSVBh8UutloHvIbGvdfKuoY6Abyg==
dependencies:
"@glimmer/tracking" "^1.0.2"
ember-cli-babel "^7.22.1"
ember-cli-htmlbars "^5.6.3"
ember-compatibility-helpers "^1.2.0"
ember-destroyable-polyfill "^2.0.2"

ember-css-transitions@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ember-css-transitions/-/ember-css-transitions-2.1.1.tgz#616069b8adef0175ec310cf504587587e8789f65"
Expand Down Expand Up @@ -9278,12 +9317,17 @@ miller-rabin@^4.0.0:
bn.js "^4.0.0"
brorand "^1.0.1"

[email protected]:
version "1.44.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92"
integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==

[email protected]:
version "1.47.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.47.0.tgz#8cb313e59965d3c05cfbf898915a267af46a335c"
integrity sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw==

[email protected], "mime-db@>= 1.43.0 < 2":
"mime-db@>= 1.43.0 < 2":
version "1.48.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.48.0.tgz#e35b31045dd7eada3aaad537ed88a33afbef2d1d"
integrity sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==
Expand Down

0 comments on commit 449d3ed

Please sign in to comment.