diff --git a/addons/rose/addon/components/rose/form/checkbox/group/index.hbs b/addons/rose/addon/components/rose/form/checkbox/group/index.hbs new file mode 100644 index 000000000..795df326d --- /dev/null +++ b/addons/rose/addon/components/rose/form/checkbox/group/index.hbs @@ -0,0 +1,12 @@ +{{#each @items as |item|}} + {{yield (hash + item=item + checkbox=(component 'rose/form/checkbox' + name=@name + disabled=@disabled + inline=@inline + checked=(includes item @selectedItems) + onChange=(fn this.toggleItem item) + ) + )}} +{{/each}} diff --git a/addons/rose/addon/components/rose/form/checkbox/group/index.js b/addons/rose/addon/components/rose/form/checkbox/group/index.js new file mode 100644 index 000000000..bd240645a --- /dev/null +++ b/addons/rose/addon/components/rose/form/checkbox/group/index.js @@ -0,0 +1,37 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; + +/** + * A checkbox group represents an array of arbitrary items, which differs from + * single checkboxes which represent boolean values. When checkboxes within a + * checkbox are toggled, their associated item is toggled within the group's + * array value. + * + * The term "item" is used to disambiguate it from "value", which has an + * existing meaning in the context of form fields. However, items + * are just arbitrary values. + */ +export default class RoseFormCheckboxGroupComponent extends Component { + // =actions + + /** + * Returns a new copy of the @selectedItems array where the given item is: + * - removed from the array, if it was already present + * - or added to the array, if it was not already present + */ + @action + toggleItem(item) { + const selectedItems = this.args.selectedItems || []; + const currentItems = [...selectedItems]; + + if (currentItems.includes(item)) { + const i = currentItems.indexOf(item); + currentItems.splice(i, 1); + } else { + currentItems.push(item); + } + + if (this.args.onChange) this.args.onChange(currentItems); + } +} diff --git a/addons/rose/addon/components/rose/form/checkbox/group/index.stories.mdx b/addons/rose/addon/components/rose/form/checkbox/group/index.stories.mdx new file mode 100644 index 000000000..e5a87308d --- /dev/null +++ b/addons/rose/addon/components/rose/form/checkbox/group/index.stories.mdx @@ -0,0 +1,54 @@ +import { Meta, Story, Preview } from '@storybook/addon-docs/blocks'; +import { hbs } from 'ember-cli-htmlbars'; +import { action } from '@storybook/addon-actions'; +import { text, boolean, select, object } from '@storybook/addon-knobs'; + + + +# Form checkboxfield group + +Renders a form checkboxfield group. + +```htmlbars + + + +``` + + {{ + template: hbs` + + + + `, + context: { + inline: boolean('inline', true), + disabled: boolean('disabled', false), + items: object('items', [ + { id: 1, name: 'Foo' }, + { id: 2, name: 'Bar' }, + { id: 3, name: 'Baz' }, + ]), + selectedItems: object('selectedItems', []), + onChange: action('changed'), + } + }} + diff --git a/addons/rose/app/components/rose/form/checkbox/group.js b/addons/rose/app/components/rose/form/checkbox/group.js new file mode 100644 index 000000000..4986bf171 --- /dev/null +++ b/addons/rose/app/components/rose/form/checkbox/group.js @@ -0,0 +1 @@ +export { default } from 'rose/components/rose/form/checkbox/group'; diff --git a/addons/rose/package.json b/addons/rose/package.json index d4ade2f5b..834dc18eb 100644 --- a/addons/rose/package.json +++ b/addons/rose/package.json @@ -65,6 +65,7 @@ "ember-cli-sass": "^10.0.1", "ember-cli-sri": "^2.1.1", "ember-cli-terser": "^4.0.2", + "ember-composable-helpers": "^4.5.0", "ember-disable-prototype-extensions": "^1.1.3", "ember-export-application-global": "^2.0.1", "ember-inline-svg": "^1.0.0", diff --git a/addons/rose/tests/dummy/app/controllers/index.js b/addons/rose/tests/dummy/app/controllers/index.js index 45faa7e5a..08fa2b4f1 100644 --- a/addons/rose/tests/dummy/app/controllers/index.js +++ b/addons/rose/tests/dummy/app/controllers/index.js @@ -1,12 +1,28 @@ import Controller from '@ember/controller'; +import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; +const possibleItems = [ + { id: 1, name: 'foo' }, + { id: 2, name: 'bar' }, + { id: 3, name: 'baz' }, +]; + export default class IndexController extends Controller { - color = 'green'; + // =attributes + + @tracked items = [...possibleItems]; + @tracked selectedItems = [possibleItems[2]]; + + // =actions @action - actionHandler(/*value*/) { - // eslint-disable-next-line no-console - //console.log('action handled'); + checkboxGroupChanged(selected) { + this.selectedItems = [...selected]; + } + + @action + clearSelected() { + this.selectedItems = []; } } diff --git a/addons/rose/tests/dummy/app/templates/index.hbs b/addons/rose/tests/dummy/app/templates/index.hbs index ed8345c31..812e1ef45 100644 --- a/addons/rose/tests/dummy/app/templates/index.hbs +++ b/addons/rose/tests/dummy/app/templates/index.hbs @@ -1,3 +1,28 @@ +

Checkbox Group

+ + + + +

+ Items selected: + {{#each this.selectedItems as |selected|}} + {{selected.id}}, + {{/each}} +

+ + + Clear Selected Items + + + +

KeyValue Dropdown

+ + + `); + assert.equal(findAll('input').length, 3); + }); + + test('it checks checkboxes for items present in a selectedItems array', async function (assert) { + assert.expect(1); + this.items = items; + this.selectedItems = selectedItems; + await render(hbs` + + + + `); + assert.equal(findAll(':checked').length, 1); + }); + + test('it fires onChange when items are checked, passing an array of selected items', async function (assert) { + assert.expect(2); + this.items = items; + this.selectedItems = selectedItems; + this.onChange = (newlySelectedItems) => { + assert.equal(selectedItems.length, 1); + assert.equal(newlySelectedItems.length, 2); + }; + await render(hbs` + + + + `); + await click('.rose-form-checkbox:first-child input'); + }); + + test('it fires onChange when items are unchecked, passing an array of selected items', async function (assert) { + assert.expect(2); + this.items = items; + this.selectedItems = selectedItems; + this.onChange = (newlySelectedItems) => { + assert.equal(selectedItems.length, 1); + assert.equal(newlySelectedItems.length, 0); + }; + await render(hbs` + + + + `); + await click('.rose-form-checkbox:last-child input'); + }); +}); diff --git a/yarn.lock b/yarn.lock index 196c15c1b..c3824f22a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -78,6 +78,27 @@ semver "^6.3.0" source-map "^0.5.0" +"@babel/core@^7.0.0", "@babel/core@^7.15.8": + version "7.15.8" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.15.8.tgz#195b9f2bffe995d2c6c159e72fe525b4114e8c10" + integrity sha512-3UG9dsxvYBMYwRv+gS41WKHno4K60/9GPy1CJaH6xy3Elq8CTtvtjT5R5jmNhXfCYLX2mTw+7/aq5ak/gOE0og== + dependencies: + "@babel/code-frame" "^7.15.8" + "@babel/generator" "^7.15.8" + "@babel/helper-compilation-targets" "^7.15.4" + "@babel/helper-module-transforms" "^7.15.8" + "@babel/helpers" "^7.15.4" + "@babel/parser" "^7.15.8" + "@babel/template" "^7.15.4" + "@babel/traverse" "^7.15.4" + "@babel/types" "^7.15.6" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.1.2" + semver "^6.3.0" + source-map "^0.5.0" + "@babel/core@^7.14.8", "@babel/core@^7.15.5": version "7.15.5" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.15.5.tgz#f8ed9ace730722544609f90c9bb49162dc3bf5b9" @@ -99,27 +120,6 @@ semver "^6.3.0" source-map "^0.5.0" -"@babel/core@^7.15.8": - version "7.15.8" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.15.8.tgz#195b9f2bffe995d2c6c159e72fe525b4114e8c10" - integrity sha512-3UG9dsxvYBMYwRv+gS41WKHno4K60/9GPy1CJaH6xy3Elq8CTtvtjT5R5jmNhXfCYLX2mTw+7/aq5ak/gOE0og== - dependencies: - "@babel/code-frame" "^7.15.8" - "@babel/generator" "^7.15.8" - "@babel/helper-compilation-targets" "^7.15.4" - "@babel/helper-module-transforms" "^7.15.8" - "@babel/helpers" "^7.15.4" - "@babel/parser" "^7.15.8" - "@babel/template" "^7.15.4" - "@babel/traverse" "^7.15.4" - "@babel/types" "^7.15.6" - convert-source-map "^1.7.0" - debug "^4.1.0" - gensync "^1.0.0-beta.2" - json5 "^2.1.2" - semver "^6.3.0" - source-map "^0.5.0" - "@babel/generator@^7.12.11", "@babel/generator@^7.12.5", "@babel/generator@^7.15.0": version "7.15.0" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.15.0.tgz#a7d0c172e0d814974bad5aa77ace543b97917f15" @@ -6832,6 +6832,25 @@ broccoli-funnel-reducer@^1.0.0: resolved "https://registry.yarnpkg.com/broccoli-funnel-reducer/-/broccoli-funnel-reducer-1.0.0.tgz#11365b2a785aec9b17972a36df87eef24c5cc0ea" integrity sha1-ETZbKnha7JsXlyo234fu8kxcwOo= +broccoli-funnel@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/broccoli-funnel/-/broccoli-funnel-2.0.1.tgz#6823c73b675ef78fffa7ab800f083e768b51d449" + integrity sha512-C8Lnp9TVsSSiZMGEF16C0dCiNg2oJqUKwuZ1K4kVC6qRPG/2Cj/rtB5kRCC9qEbwqhX71bDbfHROx0L3J7zXQg== + dependencies: + array-equal "^1.0.0" + blank-object "^1.0.1" + broccoli-plugin "^1.3.0" + debug "^2.2.0" + fast-ordered-set "^1.0.0" + fs-tree-diff "^0.5.3" + heimdalljs "^0.2.0" + minimatch "^3.0.0" + mkdirp "^0.5.0" + path-posix "^1.0.0" + rimraf "^2.4.3" + symlink-or-copy "^1.0.0" + walk-sync "^0.3.1" + broccoli-funnel@^1.0.1: version "1.2.0" resolved "https://registry.yarnpkg.com/broccoli-funnel/-/broccoli-funnel-1.2.0.tgz#cddc3afc5ff1685a8023488fff74ce6fb5a51296" @@ -7651,7 +7670,7 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001157, caniuse-lite@^1.0.30001265: +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001157, caniuse-lite@^1.0.30001271: version "1.0.30001271" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001271.tgz#0dda0c9bcae2cf5407cd34cac304186616cc83e8" integrity sha512-BBruZFWmt3HFdVPS8kceTBIguKxu4f99n5JNp06OlPD/luoAMIaIK5ieV5YjnBLH3Nysai9sxj9rpJj4ZisXOA== @@ -9491,10 +9510,10 @@ electron-rebuild@^2.0.3: tar "^6.0.5" yargs "^16.0.0" -electron-to-chromium@^1.3.867: - version "1.3.868" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.868.tgz#ed835023b57ecf0ba63dfe7d50e16b53758ab1da" - integrity sha512-kZYCHqwJ1ctGrYDlOcWQH+/AftAm/KD4lEnLDNwS0kKwx1x6dU4zv+GuDjsPPOGn/2TjnKBaZjDyjXaoix0q/A== +electron-to-chromium@^1.3.878: + version "1.3.878" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.878.tgz#baa9fb5c24b9b580f08fb245cbb52a22f8fc8fa8" + integrity sha512-O6yxWCN9ph2AdspAIszBnd9v8s11hQx8ub9w4UGApzmNRnoKhbulOWqbO8THEQec/aEHtvy+donHZMlh6l1rbA== element-resize-detector@^1.2.2: version "1.2.3" @@ -10153,6 +10172,16 @@ ember-compatibility-helpers@^1.1.1, ember-compatibility-helpers@^1.1.2, ember-co fs-extra "^9.1.0" semver "^5.4.1" +ember-composable-helpers@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/ember-composable-helpers/-/ember-composable-helpers-4.5.0.tgz#94febbdf4348e64f45f7a6f993f326e32540a61e" + integrity sha512-XjpDLyVPsLCy6kd5dIxZonOECCO6AA5sY5Hr6tYUbJg3s5ghFAiFWaNcYraYC+fL2yPJQAswwpfwGlQORUJZkw== + dependencies: + "@babel/core" "^7.0.0" + broccoli-funnel "2.0.1" + ember-cli-babel "^7.26.3" + resolve "^1.10.0" + ember-concurrency@^0.10.0: version "0.10.1" resolved "https://registry.yarnpkg.com/ember-concurrency/-/ember-concurrency-0.10.1.tgz#aae4528540391a7278e2c193a146b5474cad5ec2" @@ -16603,13 +16632,20 @@ minipass@^2.2.0, minipass@^2.3.5: safe-buffer "^5.1.2" yallist "^3.0.0" -minipass@^3.0.0, minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3: +minipass@^3.0.0, minipass@^3.1.1: version "3.1.3" resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd" integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg== dependencies: yallist "^4.0.0" +minipass@^3.1.0, minipass@^3.1.3: + version "3.1.5" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.5.tgz#71f6251b0a33a49c01b3cf97ff77eda030dff732" + integrity sha512-+8NzxD82XQoNKNrl1d/FSi+X8wAEWR+sbYAfIvub4Nz0d22plFG72CEVVaufV8PNf4qSslFTD8VMOxNVhHCjTw== + dependencies: + yallist "^4.0.0" + minizlib@^2.0.0, minizlib@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" @@ -17023,10 +17059,10 @@ node-pre-gyp@^0.11.0: semver "^5.3.0" tar "^4" -node-releases@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.0.tgz#67dc74903100a7deb044037b8a2e5f453bb05400" - integrity sha512-aA87l0flFYMzCHpTM3DERFSYxc6lv/BltdbRTOMZuxZ0cwZCD3mejE5n9vLhSJCN++/eOqr77G1IO5uXxlQYWA== +node-releases@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.1.tgz#3d1d395f204f1f2f29a54358b9fb678765ad2fc5" + integrity sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA== node-uuid@~1.4.7: version "1.4.8"