diff --git a/ember-ui/app/components/meal-list.hbs b/ember-ui/app/components/meal-list.hbs index ebe8e8a..a677454 100644 --- a/ember-ui/app/components/meal-list.hbs +++ b/ember-ui/app/components/meal-list.hbs @@ -1,14 +1,20 @@ -
+ - {{#each this.sortedItems as |meal|}} - - - + {{#each this.sortedItems as |meal index|}} + + + + + {{/each}} -
+ diff --git a/ember-ui/app/components/sortable-group-accessible.hbs b/ember-ui/app/components/sortable-group-accessible.hbs new file mode 100644 index 0000000..98847ba --- /dev/null +++ b/ember-ui/app/components/sortable-group-accessible.hbs @@ -0,0 +1,11 @@ +
+ {{yield this.groupHasFocus this.selectedIndex}} +
diff --git a/ember-ui/app/components/sortable-group-accessible.js b/ember-ui/app/components/sortable-group-accessible.js new file mode 100644 index 0000000..506a84b --- /dev/null +++ b/ember-ui/app/components/sortable-group-accessible.js @@ -0,0 +1,30 @@ +import Component from '@glimmer/component'; +import { tracked } from "@glimmer/tracking"; +import { action } from "@ember/object"; + +export default class SortableGroupAccessibleComponent extends Component { + @tracked selectedIndex = 0; + @tracked groupHasFocus = false; + + @action + handleFocus() { this.groupHasFocus = true; } + + @action + handleBlur() { this.groupHasFocus = false; } + + @action + handleArrowUp() { + if (this.selectedIndex > 0) { + this.selectedIndex -= 1; + } + } + + @action + handleArrowDown() { + if (this.selectedIndex < this.args.models.length - 1) { + this.selectedIndex += 1; + } + } + + preventDefault(evt) { evt.preventDefault(); } +} diff --git a/ember-ui/app/components/sortable-item-accessible.hbs b/ember-ui/app/components/sortable-item-accessible.hbs new file mode 100644 index 0000000..a83aacf --- /dev/null +++ b/ember-ui/app/components/sortable-item-accessible.hbs @@ -0,0 +1,17 @@ +
+ {{#if (and @groupHasFocus (eq @currentIndex @selectedIndex))}} + + + {{/if}} + {{yield}} +
diff --git a/ember-ui/app/modifiers/key-down.js b/ember-ui/app/modifiers/key-down.js new file mode 100644 index 0000000..6a159be --- /dev/null +++ b/ember-ui/app/modifiers/key-down.js @@ -0,0 +1,15 @@ +import { modifier } from 'ember-modifier'; + +export default modifier(function keyUp(element, [handler], { key: desiredKey }) { + let keydownListener = (evt) => { + if (!desiredKey || desiredKey === evt.key) { + handler(evt); + } + } + + element.addEventListener('keydown', keydownListener); + + return () => { + element.removeEventListener('keydown', keydownListener); + } +}); diff --git a/ember-ui/app/modifiers/key-up.js b/ember-ui/app/modifiers/key-up.js new file mode 100644 index 0000000..3914cab --- /dev/null +++ b/ember-ui/app/modifiers/key-up.js @@ -0,0 +1,15 @@ +import { modifier } from 'ember-modifier'; + +export default modifier(function keyUp(element, [handler], { key: desiredKey }) { + let keyupListener = (evt) => { + if (!desiredKey || desiredKey === evt.key) { + handler(evt); + } + } + + element.addEventListener('keyup', keyupListener); + + return () => { + element.removeEventListener('keyup', keyupListener); + } +}); diff --git a/ember-ui/package-lock.json b/ember-ui/package-lock.json index baa662f..5271f08 100644 --- a/ember-ui/package-lock.json +++ b/ember-ui/package-lock.json @@ -13203,6 +13203,42 @@ } } }, + "ember-modifier": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ember-modifier/-/ember-modifier-1.0.3.tgz", + "integrity": "sha512-vWuFyvdkULUyasvEXxe5lcfuPZV/Uqe+b0IQ1yU+TY1RSJnFdVUu/CVHT8Bu4HUJInqzAihwPMTwty7fypzi5Q==", + "dev": true, + "requires": { + "ember-cli-babel": "^7.11.1", + "ember-cli-is-package-missing": "^1.0.0", + "ember-cli-normalize-entity-name": "^1.0.0", + "ember-cli-string-utils": "^1.1.0", + "ember-modifier-manager-polyfill": "^1.2.0" + } + }, + "ember-modifier-manager-polyfill": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ember-modifier-manager-polyfill/-/ember-modifier-manager-polyfill-1.2.0.tgz", + "integrity": "sha512-bnaKF1LLKMkBNeDoetvIJ4vhwRPKIIumWr6dbVuW6W6p4QV8ZiO+GdF8J7mxDNlog9CeL9Z/7wam4YS86G8BYA==", + "dev": true, + "requires": { + "ember-cli-babel": "^7.10.0", + "ember-cli-version-checker": "^2.1.2", + "ember-compatibility-helpers": "^1.2.0" + }, + "dependencies": { + "ember-cli-version-checker": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ember-cli-version-checker/-/ember-cli-version-checker-2.2.0.tgz", + "integrity": "sha512-G+KtYIVlSOWGcNaTFHk76xR4GdzDLzAS4uxZUKdASuFX0KJE43C6DaqL+y3VTpUFLI2FIkAS6HZ4I1YBi+S3hg==", + "dev": true, + "requires": { + "resolve": "^1.3.3", + "semver": "^5.3.0" + } + } + } + }, "ember-qunit": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/ember-qunit/-/ember-qunit-4.6.0.tgz", @@ -13653,6 +13689,152 @@ "ember-cli-babel": "^7.1.2" } }, + "ember-truth-helpers": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ember-truth-helpers/-/ember-truth-helpers-2.1.0.tgz", + "integrity": "sha512-BQlU8aTNl1XHKTYZ243r66yqtR9JU7XKWQcmMA+vkqfkE/c9WWQ9hQZM8YABihCmbyxzzZsngvldokmeX5GhAw==", + "dev": true, + "requires": { + "ember-cli-babel": "^6.6.0" + }, + "dependencies": { + "amd-name-resolver": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/amd-name-resolver/-/amd-name-resolver-1.2.0.tgz", + "integrity": "sha512-hlSTWGS1t6/xq5YCed7YALg7tKZL3rkl7UwEZ/eCIkn8JxmM6fU6Qs/1hwtjQqfuYxlffuUcgYEm0f5xP4YKaA==", + "dev": true, + "requires": { + "ensure-posix-path": "^1.0.1" + } + }, + "babel-plugin-debug-macros": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-debug-macros/-/babel-plugin-debug-macros-0.2.0.tgz", + "integrity": "sha512-Wpmw4TbhR3Eq2t3W51eBAQSdKlr+uAyF0GI4GtPfMCD12Y4cIdpKC9l0RjNTH/P9isFypSqqewMPm7//fnZlNA==", + "dev": true, + "requires": { + "semver": "^5.3.0" + } + }, + "broccoli-babel-transpiler": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/broccoli-babel-transpiler/-/broccoli-babel-transpiler-6.5.1.tgz", + "integrity": "sha512-w6GcnkxvHcNCte5FcLGEG1hUdQvlfvSN/6PtGWU/otg69Ugk8rUk51h41R0Ugoc+TNxyeFG1opRt2RlA87XzNw==", + "dev": true, + "requires": { + "babel-core": "^6.26.0", + "broccoli-funnel": "^2.0.1", + "broccoli-merge-trees": "^2.0.0", + "broccoli-persistent-filter": "^1.4.3", + "clone": "^2.0.0", + "hash-for-dep": "^1.2.3", + "heimdalljs-logger": "^0.1.7", + "json-stable-stringify": "^1.0.0", + "rsvp": "^4.8.2", + "workerpool": "^2.3.0" + } + }, + "broccoli-merge-trees": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/broccoli-merge-trees/-/broccoli-merge-trees-2.0.1.tgz", + "integrity": "sha512-WjaexJ+I8BxP5V5RNn6um/qDRSmKoiBC/QkRi79FT9ClHfldxRyCDs9mcV7mmoaPlsshmmPaUz5jdtcKA6DClQ==", + "dev": true, + "requires": { + "broccoli-plugin": "^1.3.0", + "merge-trees": "^1.0.1" + } + }, + "broccoli-persistent-filter": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/broccoli-persistent-filter/-/broccoli-persistent-filter-1.4.6.tgz", + "integrity": "sha512-0RejLwoC95kv4kta8KAa+FmECJCK78Qgm8SRDEK7YyU0N9Cx6KpY3UCDy9WELl3mCXLN8TokNxc7/hp3lL4lfw==", + "dev": true, + "requires": { + "async-disk-cache": "^1.2.1", + "async-promise-queue": "^1.0.3", + "broccoli-plugin": "^1.0.0", + "fs-tree-diff": "^0.5.2", + "hash-for-dep": "^1.0.2", + "heimdalljs": "^0.2.1", + "heimdalljs-logger": "^0.1.7", + "mkdirp": "^0.5.1", + "promise-map-series": "^0.2.1", + "rimraf": "^2.6.1", + "rsvp": "^3.0.18", + "symlink-or-copy": "^1.0.1", + "walk-sync": "^0.3.1" + }, + "dependencies": { + "rsvp": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-3.6.2.tgz", + "integrity": "sha512-OfWGQTb9vnwRjwtA2QwpG2ICclHC3pgXZO5xt8H2EfgDquO0qVdSb5T88L4qJVAEugbS56pAuV4XZM58UX8ulw==", + "dev": true + } + } + }, + "ember-cli-babel": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/ember-cli-babel/-/ember-cli-babel-6.18.0.tgz", + "integrity": "sha512-7ceC8joNYxY2wES16iIBlbPSxwKDBhYwC8drU3ZEvuPDMwVv1KzxCNu1fvxyFEBWhwaRNTUxSCsEVoTd9nosGA==", + "dev": true, + "requires": { + "amd-name-resolver": "1.2.0", + "babel-plugin-debug-macros": "^0.2.0-beta.6", + "babel-plugin-ember-modules-api-polyfill": "^2.6.0", + "babel-plugin-transform-es2015-modules-amd": "^6.24.0", + "babel-polyfill": "^6.26.0", + "babel-preset-env": "^1.7.0", + "broccoli-babel-transpiler": "^6.5.0", + "broccoli-debug": "^0.6.4", + "broccoli-funnel": "^2.0.0", + "broccoli-source": "^1.1.0", + "clone": "^2.0.0", + "ember-cli-version-checker": "^2.1.2", + "semver": "^5.5.0" + } + }, + "ember-cli-version-checker": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ember-cli-version-checker/-/ember-cli-version-checker-2.2.0.tgz", + "integrity": "sha512-G+KtYIVlSOWGcNaTFHk76xR4GdzDLzAS4uxZUKdASuFX0KJE43C6DaqL+y3VTpUFLI2FIkAS6HZ4I1YBi+S3hg==", + "dev": true, + "requires": { + "resolve": "^1.3.3", + "semver": "^5.3.0" + } + }, + "merge-trees": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-trees/-/merge-trees-1.0.1.tgz", + "integrity": "sha1-zL5nRWl4f53vF/1G5lJfVwC70j4=", + "dev": true, + "requires": { + "can-symlink": "^1.0.0", + "fs-tree-diff": "^0.5.4", + "heimdalljs": "^0.2.1", + "heimdalljs-logger": "^0.1.7", + "rimraf": "^2.4.3", + "symlink-or-copy": "^1.0.0" + } + }, + "rsvp": { + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", + "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", + "dev": true + }, + "workerpool": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-2.3.3.tgz", + "integrity": "sha512-L1ovlYHp6UObYqElXXpbd214GgbEKDED0d3sj7pRdFXjNkb2+un/AUcCkceHizO0IVI6SOGGncrcjozruCkRgA==", + "dev": true, + "requires": { + "object-assign": "4.1.1" + } + } + } + }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", diff --git a/ember-ui/package.json b/ember-ui/package.json index 2fc5390..ac0df6b 100644 --- a/ember-ui/package.json +++ b/ember-ui/package.json @@ -40,11 +40,13 @@ "ember-export-application-global": "^2.0.1", "ember-load-initializers": "^2.1.1", "ember-maybe-import-regenerator": "^0.1.6", + "ember-modifier": "^1.0.3", "ember-qunit": "^4.6.0", "ember-resolver": "^7.0.0", "ember-sortable": "^1.12.10", "ember-source": "^3.16.3", "ember-template-lint": "^2.0.1", + "ember-truth-helpers": "^2.1.0", "eslint": "^6.8.0", "eslint-plugin-ember": "^7.8.1", "eslint-plugin-node": "^11.0.0", diff --git a/ember-ui/tests/integration/components/sortable-group-accessible-test.js b/ember-ui/tests/integration/components/sortable-group-accessible-test.js new file mode 100644 index 0000000..0b229d6 --- /dev/null +++ b/ember-ui/tests/integration/components/sortable-group-accessible-test.js @@ -0,0 +1,26 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Component | sortable-group-accessible', function(hooks) { + setupRenderingTest(hooks); + + test('it renders', async function(assert) { + // Set any properties with this.set('myProperty', 'value'); + // Handle any actions with this.set('myAction', function(val) { ... }); + + await render(hbs``); + + assert.equal(this.element.textContent.trim(), ''); + + // Template block usage: + await render(hbs` + + template block text + + `); + + assert.equal(this.element.textContent.trim(), 'template block text'); + }); +}); diff --git a/ember-ui/tests/integration/components/sortable-item-accessible-test.js b/ember-ui/tests/integration/components/sortable-item-accessible-test.js new file mode 100644 index 0000000..234ea88 --- /dev/null +++ b/ember-ui/tests/integration/components/sortable-item-accessible-test.js @@ -0,0 +1,26 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Component | sortable-item-accessible', function(hooks) { + setupRenderingTest(hooks); + + test('it renders', async function(assert) { + // Set any properties with this.set('myProperty', 'value'); + // Handle any actions with this.set('myAction', function(val) { ... }); + + await render(hbs``); + + assert.equal(this.element.textContent.trim(), ''); + + // Template block usage: + await render(hbs` + + template block text + + `); + + assert.equal(this.element.textContent.trim(), 'template block text'); + }); +}); diff --git a/ember-ui/tests/integration/modifiers/key-down-test.js b/ember-ui/tests/integration/modifiers/key-down-test.js new file mode 100644 index 0000000..1fa40d2 --- /dev/null +++ b/ember-ui/tests/integration/modifiers/key-down-test.js @@ -0,0 +1,42 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render, triggerKeyEvent } from '@ember/test-helpers'; +import hbs from 'htmlbars-inline-precompile'; +import { set } from '@ember/object'; + +module('Integration | Modifier | key-down', function(hooks) { + setupRenderingTest(hooks); + + test('it fires off a function when a key down, passing the event along with it', async function(assert) { + set(this, 'keyDown', ({ key }) => { + assert.step('key down'); + assert.equal(key, 'Enter'); + }); + + await render(hbs` +
+
+ `); + await triggerKeyEvent('[data-test-id=keydown]', 'keydown', 'Enter'); + + assert.verifySteps(['key down']); + }); + + test('it can listen for a specific key', async function(assert) { + set(this, 'keyDown', ({ key }) => { + assert.step('enter key down'); + assert.equal(key, 'Enter'); + }); + + await render(hbs` +
+
+ `); + await triggerKeyEvent('[data-test-id=keydown]', 'keydown', 'Enter'); + await triggerKeyEvent('[data-test-id=keydown]', 'keydown', 'Spacebar'); + + assert.verifySteps(['enter key down']); + }); +}); diff --git a/ember-ui/tests/integration/modifiers/key-up-test.js b/ember-ui/tests/integration/modifiers/key-up-test.js new file mode 100644 index 0000000..92ad55e --- /dev/null +++ b/ember-ui/tests/integration/modifiers/key-up-test.js @@ -0,0 +1,42 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render, triggerKeyEvent } from '@ember/test-helpers'; +import hbs from 'htmlbars-inline-precompile'; +import { set } from '@ember/object'; + +module('Integration | Modifier | key-up', function(hooks) { + setupRenderingTest(hooks); + + test('it fires off a function when a key is pressed, passing the event along with it', async function(assert) { + set(this, 'keyUp', ({ key }) => { + assert.step('key up'); + assert.equal(key, 'Enter'); + }); + + await render(hbs` +
+
+ `); + await triggerKeyEvent('[data-test-id=keyup]', 'keyup', 'Enter'); + + assert.verifySteps(['key up']); + }); + + test('it can listen for a specific key', async function(assert) { + set(this, 'keyUp', ({ key }) => { + assert.step('enter key up'); + assert.equal(key, 'Enter'); + }); + + await render(hbs` +
+
+ `); + await triggerKeyEvent('[data-test-id=keyup]', 'keyup', 'Enter'); + await triggerKeyEvent('[data-test-id=keyup]', 'keyup', 'Spacebar'); + + assert.verifySteps(['enter key up']); + }); +});