From 6f550ca05f16631b3df7771a9e77128b5d313b69 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 9 Jan 2019 17:53:39 -0800 Subject: [PATCH 01/13] Freestyle entry for the new multi-select-dropdown component --- .../freestyle/sg-multi-select-dropdown.js | 14 ++++++++++++++ .../freestyle/sg-multi-select-dropdown.hbs | 9 +++++++++ ui/app/templates/freestyle.hbs | 4 ++++ 3 files changed, 27 insertions(+) create mode 100644 ui/app/components/freestyle/sg-multi-select-dropdown.js create mode 100644 ui/app/templates/components/freestyle/sg-multi-select-dropdown.hbs diff --git a/ui/app/components/freestyle/sg-multi-select-dropdown.js b/ui/app/components/freestyle/sg-multi-select-dropdown.js new file mode 100644 index 000000000000..6422a3d5d6cb --- /dev/null +++ b/ui/app/components/freestyle/sg-multi-select-dropdown.js @@ -0,0 +1,14 @@ +import Component from '@ember/component'; +import { computed } from '@ember/object'; + +export default Component.extend({ + options: computed(() => [ + { key: 'option-1', label: 'Option One' }, + { key: 'option-2', label: 'Option Two' }, + { key: 'option-3', label: 'Option Three' }, + { key: 'option-4', label: 'Option Four' }, + { key: 'option-5', label: 'Option Five' }, + ]), + + selection: computed(() => ['option-2', 'option-4', 'option-5']), +}); diff --git a/ui/app/templates/components/freestyle/sg-multi-select-dropdown.hbs b/ui/app/templates/components/freestyle/sg-multi-select-dropdown.hbs new file mode 100644 index 000000000000..4736b04a589c --- /dev/null +++ b/ui/app/templates/components/freestyle/sg-multi-select-dropdown.hbs @@ -0,0 +1,9 @@ +{{#freestyle-usage "multi-select-dropdown" title="Multi-select dropdown"}} + {{multi-select-dropdown + label="Example Dropdown" + options=options + selection=selection}} +{{/freestyle-usage}} +{{#freestyle-annotation}} + A wrapper around basic-dropdown for creating a list of checkboxes and tracking the state thereof. +{{/freestyle-annotation}} \ No newline at end of file diff --git a/ui/app/templates/freestyle.hbs b/ui/app/templates/freestyle.hbs index 87bdb7c7685f..a68982d1f3d2 100644 --- a/ui/app/templates/freestyle.hbs +++ b/ui/app/templates/freestyle.hbs @@ -67,6 +67,10 @@ {{freestyle/sg-metrics}} {{/section.subsection}} + {{#section.subsection name="Multi-select dropdown"}} + {{freestyle/sg-multi-select-dropdown}} + {{/section.subsection}} + {{#section.subsection name="Page tabs"}} {{freestyle/sg-page-tabs}} {{/section.subsection}} From dd4bb73de80e1ee106af18a46d72f31577e433d3 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 9 Jan 2019 17:54:01 -0800 Subject: [PATCH 02/13] Simple includes helper that works like Array#includes --- ui/app/helpers/includes.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 ui/app/helpers/includes.js diff --git a/ui/app/helpers/includes.js b/ui/app/helpers/includes.js new file mode 100644 index 000000000000..7c53197d22ac --- /dev/null +++ b/ui/app/helpers/includes.js @@ -0,0 +1,14 @@ +import Helper from '@ember/component/helper'; + +/** + * Includes + * + * Usage: {{includes needle haystack}} + * + * Returns true if an element (needle) is found in an array (haystack). + */ +export function includes([needle, haystack]) { + return haystack.includes(needle); +} + +export default Helper.helper(includes); From 48945a306cdb4997a27cb68a39407726777d7389 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 9 Jan 2019 17:54:28 -0800 Subject: [PATCH 03/13] Templating and styling the multi-select-dropdown component --- ui/app/styles/components/dropdown.scss | 48 +++++++++++++++++-- .../components/multi-select-dropdown.hbs | 18 +++++++ 2 files changed, 61 insertions(+), 5 deletions(-) create mode 100644 ui/app/templates/components/multi-select-dropdown.hbs diff --git a/ui/app/styles/components/dropdown.scss b/ui/app/styles/components/dropdown.scss index 0be310dd37bb..0643d07d3abf 100644 --- a/ui/app/styles/components/dropdown.scss +++ b/ui/app/styles/components/dropdown.scss @@ -1,8 +1,11 @@ -.ember-power-select-trigger { +.ember-power-select-trigger, +.dropdown-trigger { + position: relative; padding: 0.3em 16px 0.3em 0.3em; border-radius: $radius; box-shadow: $button-box-shadow-standard; background: $white-bis; + border: 1px solid $grey-light; &.is-outlined { border-color: rgba($white, 0.5); @@ -20,17 +23,52 @@ } } -.ember-power-select-selected-item { +.dropdown-trigger-label { + margin-left: 8px; +} + +.ember-power-select-selected-item, +.dropdown-item { text-overflow: ellipsis; white-space: nowrap; } -.ember-power-select-prefix { +.ember-power-select-prefix, +.dropdown-prefix { color: $grey; } -.ember-power-select-option { - .ember-power-select-prefix { +.ember-power-select-option, +.dropdown-option { + .ember-power-select-prefix, + .dropdown-prefix { display: none; } } + +.dropdown-options { + border: 1px solid $grey-light; + margin-top: -1px; + + & > ul { + width: 100%; + } + + .dropdown-option { + line-height: 1.75; + + label { + display: block; + padding: 3px 8px; + cursor: pointer; + } + + & + .dropdown-option { + border-top: 1px solid $grey-lighter; + } + + &:hover { + background: $white-bis; + } + } +} diff --git a/ui/app/templates/components/multi-select-dropdown.hbs b/ui/app/templates/components/multi-select-dropdown.hbs new file mode 100644 index 000000000000..fe75067bc6ac --- /dev/null +++ b/ui/app/templates/components/multi-select-dropdown.hbs @@ -0,0 +1,18 @@ +{{#basic-dropdown matchTriggerWidth=true as |dd|}} + {{#dd.trigger class="dropdown-trigger"}} + {{label}} + + {{/dd.trigger}} + {{#dd.content class="dropdown-options"}} +
    + {{#each options key="key" as |option|}} + + {{/each}} +
+ {{/dd.content}} +{{/basic-dropdown}} \ No newline at end of file From 7de062e0fb319bdf4ce02c1d2532cec331bd9a3d Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 11 Jan 2019 15:41:56 -0800 Subject: [PATCH 04/13] Persistence and onSelect handler for the multi-select-dropdown --- ui/app/components/multi-select-dropdown.js | 23 +++++++++++++++++++ ui/app/styles/components/dropdown.scss | 1 + .../freestyle/sg-multi-select-dropdown.hbs | 3 ++- .../components/multi-select-dropdown.hbs | 12 ++++++++-- 4 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 ui/app/components/multi-select-dropdown.js diff --git a/ui/app/components/multi-select-dropdown.js b/ui/app/components/multi-select-dropdown.js new file mode 100644 index 000000000000..5c03ea41385a --- /dev/null +++ b/ui/app/components/multi-select-dropdown.js @@ -0,0 +1,23 @@ +import Component from '@ember/component'; +import { computed } from '@ember/object'; + +export default Component.extend({ + classNames: ['dropdown'], + + options: computed(() => []), + selection: computed(() => []), + + onSelect() {}, + + actions: { + toggle({ key }) { + const newSelection = this.get('selection').slice(); + if (newSelection.includes(key)) { + newSelection.removeObject(key); + } else { + newSelection.addObject(key); + } + this.get('onSelect')(newSelection); + }, + }, +}); diff --git a/ui/app/styles/components/dropdown.scss b/ui/app/styles/components/dropdown.scss index 0643d07d3abf..655de31e3b15 100644 --- a/ui/app/styles/components/dropdown.scss +++ b/ui/app/styles/components/dropdown.scss @@ -25,6 +25,7 @@ .dropdown-trigger-label { margin-left: 8px; + margin-right: 8px; } .ember-power-select-selected-item, diff --git a/ui/app/templates/components/freestyle/sg-multi-select-dropdown.hbs b/ui/app/templates/components/freestyle/sg-multi-select-dropdown.hbs index 4736b04a589c..66221ad418ca 100644 --- a/ui/app/templates/components/freestyle/sg-multi-select-dropdown.hbs +++ b/ui/app/templates/components/freestyle/sg-multi-select-dropdown.hbs @@ -2,7 +2,8 @@ {{multi-select-dropdown label="Example Dropdown" options=options - selection=selection}} + selection=selection + onSelect=(action (mut selection))}} {{/freestyle-usage}} {{#freestyle-annotation}} A wrapper around basic-dropdown for creating a list of checkboxes and tracking the state thereof. diff --git a/ui/app/templates/components/multi-select-dropdown.hbs b/ui/app/templates/components/multi-select-dropdown.hbs index fe75067bc6ac..3f0ffde417e3 100644 --- a/ui/app/templates/components/multi-select-dropdown.hbs +++ b/ui/app/templates/components/multi-select-dropdown.hbs @@ -1,6 +1,11 @@ {{#basic-dropdown matchTriggerWidth=true as |dd|}} {{#dd.trigger class="dropdown-trigger"}} - {{label}} + + {{label}} + {{#if selection.length}} + {{selection.length}} + {{/if}} + {{/dd.trigger}} {{#dd.content class="dropdown-options"}} @@ -8,7 +13,10 @@ {{#each options key="key" as |option|}} From 36fd51eb855ad6222af3f955aff3ffceef056ab7 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 11 Jan 2019 15:42:32 -0800 Subject: [PATCH 05/13] Color alias for tags --- ui/app/styles/core/tag.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/app/styles/core/tag.scss b/ui/app/styles/core/tag.scss index cbf2bbe3bf89..0cf9fd678827 100644 --- a/ui/app/styles/core/tag.scss +++ b/ui/app/styles/core/tag.scss @@ -8,7 +8,8 @@ line-height: 1; height: 1.5em; - &.is-pending { + &.is-pending, + &.is-light { background: $grey-blue; color: findColorInvert(darken($grey-blue, 10%)); } From aadadf2ca560cca90ecab71fd2b2a6a1df4d8416 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 11 Jan 2019 16:40:28 -0800 Subject: [PATCH 06/13] Multi-select button-bar support --- ui/app/styles/components/dropdown.scss | 39 +++++++++++++++++++ .../components/multi-select-dropdown.hbs | 2 +- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/ui/app/styles/components/dropdown.scss b/ui/app/styles/components/dropdown.scss index 655de31e3b15..e991f3371b1e 100644 --- a/ui/app/styles/components/dropdown.scss +++ b/ui/app/styles/components/dropdown.scss @@ -23,6 +23,43 @@ } } +.button-bar { + display: inline-flex; + flex-direction: row; + box-shadow: $button-box-shadow-standard; + + .dropdown { + display: flex; + position: relative; + + & + .dropdown { + margin-left: -1px; + } + } + + .ember-power-select-trigger, + .dropdown-trigger { + border-radius: 0; + box-shadow: none; + } + + .dropdown:first-child { + .ember-power-select-trigger, + .dropdown-trigger { + border-top-left-radius: $radius; + border-bottom-left-radius: $radius; + } + } + + .dropdown:last-child { + .ember-power-select-trigger, + .dropdown-trigger { + border-top-right-radius: $radius; + border-bottom-right-radius: $radius; + } + } +} + .dropdown-trigger-label { margin-left: 8px; margin-right: 8px; @@ -50,6 +87,8 @@ .dropdown-options { border: 1px solid $grey-light; margin-top: -1px; + max-height: 400px; + overflow: auto; & > ul { width: 100%; diff --git a/ui/app/templates/components/multi-select-dropdown.hbs b/ui/app/templates/components/multi-select-dropdown.hbs index 3f0ffde417e3..34d65160b800 100644 --- a/ui/app/templates/components/multi-select-dropdown.hbs +++ b/ui/app/templates/components/multi-select-dropdown.hbs @@ -1,4 +1,4 @@ -{{#basic-dropdown matchTriggerWidth=true as |dd|}} +{{#basic-dropdown as |dd|}} {{#dd.trigger class="dropdown-trigger"}} {{label}} From e3860040b080892caf685bb1832addab682ab8d8 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 11 Jan 2019 16:40:46 -0800 Subject: [PATCH 07/13] Round out the freestyle entry --- .../freestyle/sg-multi-select-dropdown.js | 35 ++++++++++++++- .../freestyle/sg-multi-select-dropdown.hbs | 44 +++++++++++++++++-- 2 files changed, 74 insertions(+), 5 deletions(-) diff --git a/ui/app/components/freestyle/sg-multi-select-dropdown.js b/ui/app/components/freestyle/sg-multi-select-dropdown.js index 6422a3d5d6cb..cbc51b730091 100644 --- a/ui/app/components/freestyle/sg-multi-select-dropdown.js +++ b/ui/app/components/freestyle/sg-multi-select-dropdown.js @@ -2,7 +2,7 @@ import Component from '@ember/component'; import { computed } from '@ember/object'; export default Component.extend({ - options: computed(() => [ + options1: computed(() => [ { key: 'option-1', label: 'Option One' }, { key: 'option-2', label: 'Option Two' }, { key: 'option-3', label: 'Option Three' }, @@ -10,5 +10,36 @@ export default Component.extend({ { key: 'option-5', label: 'Option Five' }, ]), - selection: computed(() => ['option-2', 'option-4', 'option-5']), + selection1: computed(() => ['option-2', 'option-4', 'option-5']), + + optionsMany: computed(() => + Array(100) + .fill(null) + .map((_, i) => ({ label: `Option ${i}`, key: `option-${i}` })) + ), + selectionMany: computed(() => []), + + optionsDatacenter: computed(() => [ + { key: 'pdx-1', label: 'pdx-1' }, + { key: 'jfk-1', label: 'jfk-1' }, + { key: 'jfk-2', label: 'jfk-2' }, + { key: 'muc-1', label: 'muc-1' }, + ]), + selectionDatacenter: computed(() => ['jfk-1', 'jfk-2']), + + optionsType: computed(() => [ + { key: 'batch', label: 'Batch' }, + { key: 'service', label: 'Service' }, + { key: 'system', label: 'System' }, + { key: 'periodic', label: 'Periodic' }, + { key: 'parameterized', label: 'Parameterized' }, + ]), + selectionType: computed(() => ['system', 'service']), + + optionsStatus: computed(() => [ + { key: 'pending', label: 'Pending' }, + { key: 'running', label: 'Running' }, + { key: 'dead', label: 'Dead' }, + ]), + selectionStatus: computed(() => []), }); diff --git a/ui/app/templates/components/freestyle/sg-multi-select-dropdown.hbs b/ui/app/templates/components/freestyle/sg-multi-select-dropdown.hbs index 66221ad418ca..2f5b183854f8 100644 --- a/ui/app/templates/components/freestyle/sg-multi-select-dropdown.hbs +++ b/ui/app/templates/components/freestyle/sg-multi-select-dropdown.hbs @@ -1,10 +1,48 @@ {{#freestyle-usage "multi-select-dropdown" title="Multi-select dropdown"}} {{multi-select-dropdown label="Example Dropdown" - options=options - selection=selection - onSelect=(action (mut selection))}} + options=options1 + selection=selection1 + onSelect=(action (mut selection1))}} {{/freestyle-usage}} {{#freestyle-annotation}} A wrapper around basic-dropdown for creating a list of checkboxes and tracking the state thereof. +{{/freestyle-annotation}} + +{{#freestyle-usage "multi-select-dropdown-may" title="Multi-select dropdown with many options"}} + {{multi-select-dropdown + label="Lots of options in here" + options=optionsMany + selection=selectionMany + onSelect=(action (mut selectionMany))}} +{{/freestyle-usage}} +{{#freestyle-annotation}} + A strength of the multi-select-dropdown is its simple presentation. It is quick to select options and it is quick to remove options. + However, this strength becomes a weakness when there are too many options. Since the selection isn't pinned in any way, removing a selection + can become an adventure of scrolling up and down. Also since the selection isn't pinned, this component can't support search, since search would + entirely mask the selection. +{{/freestyle-annotation}} + +{{#freestyle-usage "multi-select-dropdown-bar" title="Multi-select dropdown bar"}} +
+ {{multi-select-dropdown + label="Datacenter" + options=optionsDatacenter + selection=selectionDatacenter + onSelect=(action (mut selectionDatacenter))}} + {{multi-select-dropdown + label="Type" + options=optionsType + selection=selectionType + onSelect=(action (mut selectionType))}} + {{multi-select-dropdown + label="Status" + options=optionsStatus + selection=selectionStatus + onSelect=(action (mut selectionStatus))}} +
+{{/freestyle-usage}} +{{#freestyle-annotation}} + Since this is a core component for faceted search, it makes sense to construct an arrangement of multi-select dropdowns. + Do this by wrapping all the options in a .button-bar container. {{/freestyle-annotation}} \ No newline at end of file From 810392aac2911af081770b29d61204bc166c3c27 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Mon, 14 Jan 2019 17:59:41 -0800 Subject: [PATCH 08/13] Tab and keyboard navigation for multi-select --- ui/app/components/multi-select-dropdown.js | 60 +++++++++++++++++++ ui/app/styles/components/dropdown.scss | 18 +++++- .../components/multi-select-dropdown.hbs | 10 +++- 3 files changed, 84 insertions(+), 4 deletions(-) diff --git a/ui/app/components/multi-select-dropdown.js b/ui/app/components/multi-select-dropdown.js index 5c03ea41385a..a995ac195dd4 100644 --- a/ui/app/components/multi-select-dropdown.js +++ b/ui/app/components/multi-select-dropdown.js @@ -1,6 +1,12 @@ import Component from '@ember/component'; import { computed } from '@ember/object'; +const TAB = 9; +const ESC = 27; +const SPACE = 32; +const ARROW_UP = 38; +const ARROW_DOWN = 40; + export default Component.extend({ classNames: ['dropdown'], @@ -9,6 +15,9 @@ export default Component.extend({ onSelect() {}, + isOpen: false, + dropdown: null, + actions: { toggle({ key }) { const newSelection = this.get('selection').slice(); @@ -19,5 +28,56 @@ export default Component.extend({ } this.get('onSelect')(newSelection); }, + + openOnArrowDown(dropdown, e) { + // It's not a good idea to grab a dropdown reference like this, but it's necessary + // in order to invoke dropdown.actions.close in traverseList + this.set('dropdown', dropdown); + + if (!this.get('isOpen') && e.keyCode === ARROW_DOWN) { + dropdown.actions.open(e); + e.preventDefault(); + } else if (this.get('isOpen') && (e.keyCode === TAB || e.keyCode === ARROW_DOWN)) { + const optionsId = this.element.querySelector('.dropdown-trigger').getAttribute('aria-owns'); + const firstElement = document.querySelector(`#${optionsId} .dropdown-option`); + + if (firstElement) { + firstElement.focus(); + e.preventDefault(); + } + } + }, + + traverseList(option, e) { + if (e.keyCode === ESC) { + // Close the dropdown + const dropdown = this.get('dropdown'); + if (dropdown) { + dropdown.actions.close(e); + // Return focus to the trigger so tab works as expected + const trigger = this.element.querySelector('.dropdown-trigger'); + if (trigger) trigger.focus(); + e.preventDefault(); + this.set('dropdown', null); + } + } else if (e.keyCode === ARROW_UP) { + // previous item + const prev = e.target.previousElementSibling; + if (prev) { + prev.focus(); + e.preventDefault(); + } + } else if (e.keyCode === ARROW_DOWN) { + // next item + const next = e.target.nextElementSibling; + if (next) { + next.focus(); + e.preventDefault(); + } + } else if (e.keyCode === SPACE) { + this.send('toggle', option); + e.preventDefault(); + } + }, }, }); diff --git a/ui/app/styles/components/dropdown.scss b/ui/app/styles/components/dropdown.scss index e991f3371b1e..6f37e0bf44a3 100644 --- a/ui/app/styles/components/dropdown.scss +++ b/ui/app/styles/components/dropdown.scss @@ -6,6 +6,12 @@ box-shadow: $button-box-shadow-standard; background: $white-bis; border: 1px solid $grey-light; + outline: none; + cursor: pointer; + + &:focus { + box-shadow: $button-box-shadow-standard, inset 0 0 0 2px $grey-lighter; + } &.is-outlined { border-color: rgba($white, 0.5); @@ -41,6 +47,10 @@ .dropdown-trigger { border-radius: 0; box-shadow: none; + + &:focus { + box-shadow: inset 0 0 0 2px $grey-lighter; + } } .dropdown:first-child { @@ -107,8 +117,14 @@ border-top: 1px solid $grey-lighter; } - &:hover { + &:hover, + &:focus { background: $white-bis; + outline: none; + border-left: 2px solid $blue; + label { + padding-left: 6px; + } } } } diff --git a/ui/app/templates/components/multi-select-dropdown.hbs b/ui/app/templates/components/multi-select-dropdown.hbs index 34d65160b800..2aea751b69ab 100644 --- a/ui/app/templates/components/multi-select-dropdown.hbs +++ b/ui/app/templates/components/multi-select-dropdown.hbs @@ -1,5 +1,8 @@ -{{#basic-dropdown as |dd|}} - {{#dd.trigger class="dropdown-trigger"}} +{{#basic-dropdown + onOpen=(action (mut isOpen) true) + onClose=(action (mut isOpen) false) + as |dd|}} + {{#dd.trigger class="dropdown-trigger" onKeyDown=(action "openOnArrowDown")}} {{label}} {{#if selection.length}} @@ -11,10 +14,11 @@ {{#dd.content class="dropdown-options"}}
    {{#each options key="key" as |option|}} -