diff --git a/addons/rose/addon/components/rose/header.stories.mdx b/addons/rose/addon/components/rose/header.stories.mdx index aa5126a23..05d376e64 100644 --- a/addons/rose/addon/components/rose/header.stories.mdx +++ b/addons/rose/addon/components/rose/header.stories.mdx @@ -42,7 +42,7 @@ A header component that allows three distinct sections to be configured: brand, - + {{navigation-dropdown-label}} {{navigation-dropdown-link-label}} @@ -52,7 +52,7 @@ A header component that allows three distinct sections to be configured: brand, {{navigation-link-label}} - + link @@ -67,7 +67,6 @@ A header component that allows three distinct sections to be configured: brand, 'navigation-dropdown-link-label': text('navigation-dropdown-link-label', 'Menu Item'), 'navigation-dropdown-button-label': text('navigation-dropdown-button-label', 'Menu Item'), 'navigation-link-label': text('navigation-link-label', 'Section'), - 'open-dropdown': boolean('open-dropdown', false), }, }} diff --git a/addons/rose/addon/components/rose/header/dropdown.js b/addons/rose/addon/components/rose/header/dropdown.js index 2808ab8a7..bfc8ec9e1 100644 --- a/addons/rose/addon/components/rose/header/dropdown.js +++ b/addons/rose/addon/components/rose/header/dropdown.js @@ -1,34 +1,16 @@ import Component from '@ember/component'; import layout from '../../../templates/components/rose/header/dropdown'; -import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; export default Component.extend({ layout, tagName: '', - @tracked isOpen: false, - + // Uses details widget state to close dropdown @action - toggle(event) { - this.isOpen = event.target.open; + close(element) { + if(element.open) { + element.open = false; + } }, - - @action - addClickOutside(element) { - document.addEventListener('click', event => this.clickOutside(event, element), true); - }, - - @action - removeClickOutside(element) { - document.removeEventListener('click', event => this.clickOutside(event, element), true); - }, - - @action - clickOutside(event, element) { - // Only close dropdown when non-nested elements are clicked - if(element.contains(event.target)) { return; } - this.removeClickOutside(element); - this.isOpen = false; - } }); diff --git a/addons/rose/addon/modifiers/on-click-outside.js b/addons/rose/addon/modifiers/on-click-outside.js new file mode 100644 index 000000000..8feb0335e --- /dev/null +++ b/addons/rose/addon/modifiers/on-click-outside.js @@ -0,0 +1,15 @@ +import { modifier } from 'ember-modifier'; + +export default modifier((element, [callback]) => { + function handleClick(event) { + if (!element.contains(event.target)) { + callback(element, event); + } + } + + document.addEventListener('click', handleClick, false); + + return () => { + document.removeEventListener('click', handleClick, false); + }; +}); diff --git a/addons/rose/addon/styles/rose/components/_header.scss b/addons/rose/addon/styles/rose/components/_header.scss index dbd1740e3..a6970359f 100644 --- a/addons/rose/addon/styles/rose/components/_header.scss +++ b/addons/rose/addon/styles/rose/components/_header.scss @@ -1,4 +1,5 @@ @use '../variables/sizing'; +@use '../variables/media'; @use '../utilities/type'; $brand-hashicorp: "data:image/svg+xml;utf-8,"; @@ -14,38 +15,38 @@ $dropdown-chevron: ( grid-template-areas: 'brand navigation utilities'; grid-template-columns: 0.01fr auto 1fr; grid-gap: sizing.rems(xxxs); - background: var(--stark); - color: var(--subtle); - padding: sizing.rems(xxs) 0; + background: var(--black); + color: var(--white); .rose-header-brand { grid-area: brand; - margin: sizing.rems(xs) 0 sizing.rems(xs) sizing.rems(m); - padding: 0 sizing.rems(m); - height: sizing.rems(l) + sizing.rems(xxs); + width: sizing.rems(l); + height: sizing.rems(l); background-image: url($brand-hashicorp); background-repeat: no-repeat; + margin: sizing.rems(xxs); } .rose-header-navigation { grid-area: navigation; - .wrapper { + .rose-header-navigation-flex { display: flex; flex-wrap: wrap; } .rose-header-navigation-link { - padding: sizing.rems(xs) sizing.rems(s) sizing.rems(xxxs) sizing.rems(s); text-decoration: none; display: block; - color: var(--subtle); - height: sizing.rems(l); + color: var(--white); border: 1px solid var(--stark); border-radius: sizing.rems(xxs); text-align: center; - margin-top: sizing.rems(xxs); + padding: sizing.rems(xxs) sizing.rems(s); + position: relative; + top: sizing.rems(xxxs); cursor: pointer; + margin: 0 sizing.rems(xs); &:hover { background: var(--ui-gray-starker-3); @@ -60,7 +61,7 @@ $dropdown-chevron: ( .rose-header-utilities { grid-area: utilities; - .wrapper { + .rose-header-utilities-flex { margin-right: sizing.rems(s); display: flex; flex-wrap: wrap; @@ -70,25 +71,24 @@ $dropdown-chevron: ( .rose-header-navigation, .rose-header-utilities { .rose-header-dropdown { - height: sizing.rems(m) + sizing.rems(xxxs); - - .rose-header-dropdown-trigger { - height: sizing.rems(m) + sizing.rems(xxxs); - padding: sizing.rems(xs) sizing.rems(xxs); - border-radius: sizing.rems(xxxs); - color: var(--subtle); - margin: sizing.rems(xxs) sizing.rems(xxs) 0 0; - cursor: pointer; - - &.rose-header-dropdown-open { - background: var(--subtle); + &[open] { + .rose-header-dropdown-trigger { + background: var(--white); color: var(--ui-gray-starker-4); .rose-header-dropdown-chevron { background-image: url(map-get($dropdown-chevron, 'open')); } } + } + .rose-header-dropdown-trigger { + padding: sizing.rems(xxs) sizing.rems(s); + border-radius: sizing.rems(xxxs); + color: var(--white); + position: relative; + top: sizing.rems(xxxs); + cursor: pointer; &::-webkit-details-marker { display: none; } @@ -110,29 +110,29 @@ $dropdown-chevron: ( .rose-header-dropdown-content { position: absolute; - background-color: var(--subtle); + background-color: var(--white); border-radius: sizing.rems(xxxs); border: 1px solid var(--ui-gray-subtler-2); + padding-right: sizing.rems(xl); + margin-top: sizing.rems(xxxs); &.rose-header-dropdown-content-right { - right: sizing.rems(m); + right: sizing.rems(s); } .rose-header-dropdown-content-link{ - padding: sizing.rems(s) (sizing.rems(xl) * 3) sizing.rems(xxs) sizing.rems(s); + padding: sizing.rems(xs) sizing.rems(s); text-decoration: none; display: block; color: var(--ui-gray-starker-4); height: sizing.rems(l); } - .rose-header-dropdown-content-button { button { cursor: pointer; - padding: sizing.rems(s) (sizing.rems(xl) * 3) sizing.rems(xs) sizing.rems(s); + padding: sizing.rems(xs) sizing.rems(s); color: var(--ui-gray-starker-4); - box-shadow: none; display: inline-block; border: none; text-decoration: none; @@ -146,8 +146,7 @@ $dropdown-chevron: ( } } -//TODO: Where do media query sizes live? -@media (max-width: 768px) { +@media (max-width: media.width(small)) { .rose-header { grid-template-columns: 1fr 3fr; grid-template-areas: diff --git a/addons/rose/addon/styles/rose/variables/_index.scss b/addons/rose/addon/styles/rose/variables/_index.scss index 0f1c38707..c789019f7 100644 --- a/addons/rose/addon/styles/rose/variables/_index.scss +++ b/addons/rose/addon/styles/rose/variables/_index.scss @@ -2,3 +2,4 @@ @import 'typography'; @import 'color'; @import 'opacity'; +@import 'media'; diff --git a/addons/rose/addon/styles/rose/variables/_media.scss b/addons/rose/addon/styles/rose/variables/_media.scss new file mode 100644 index 000000000..d0f695bbf --- /dev/null +++ b/addons/rose/addon/styles/rose/variables/_media.scss @@ -0,0 +1,21 @@ +@use 'sass:map'; + +/** + * SASS variables various media query sizes + */ + +$width-breakpoints: ( + xxlarge: 1440px, + xlarge: 1200px, + large: 1024px, + medium: 768px, + small: 480px, + xsmall: 320px +); + +/** + * Returns a media width variable. + */ +@function width($name) { + @return map.get($width-breakpoints, $name); +} diff --git a/addons/rose/addon/templates/components/rose/header/dropdown.hbs b/addons/rose/addon/templates/components/rose/header/dropdown.hbs index d8b289a44..3f4810eea 100644 --- a/addons/rose/addon/templates/components/rose/header/dropdown.hbs +++ b/addons/rose/addon/templates/components/rose/header/dropdown.hbs @@ -1,14 +1,8 @@ -
+
+ {{yield (hash - isOpen=this.isOpen - trigger=(component 'rose/header/dropdown/trigger' - isOpen=this.isOpen) - content=(component 'rose/header/dropdown/content' isOpen=this.isOpen - position=@position) + trigger=(component 'rose/header/dropdown/trigger') + content=(component 'rose/header/dropdown/content' alignRight=@alignRight) )}}
diff --git a/addons/rose/addon/templates/components/rose/header/dropdown/content.hbs b/addons/rose/addon/templates/components/rose/header/dropdown/content.hbs index 48c30b03c..68af8036a 100644 --- a/addons/rose/addon/templates/components/rose/header/dropdown/content.hbs +++ b/addons/rose/addon/templates/components/rose/header/dropdown/content.hbs @@ -1,4 +1,4 @@ - + {{yield (hash link=(component 'link-to' class="rose-header-dropdown-content-link") button=(component 'rose/header/dropdown/content/button') diff --git a/addons/rose/addon/templates/components/rose/header/dropdown/trigger.hbs b/addons/rose/addon/templates/components/rose/header/dropdown/trigger.hbs index 960ad35f5..f513aa8bd 100644 --- a/addons/rose/addon/templates/components/rose/header/dropdown/trigger.hbs +++ b/addons/rose/addon/templates/components/rose/header/dropdown/trigger.hbs @@ -1,4 +1,4 @@ - + {{yield}} diff --git a/addons/rose/addon/templates/components/rose/header/navigation.hbs b/addons/rose/addon/templates/components/rose/header/navigation.hbs index 8ce8c7a72..9e2b4308b 100644 --- a/addons/rose/addon/templates/components/rose/header/navigation.hbs +++ b/addons/rose/addon/templates/components/rose/header/navigation.hbs @@ -1,5 +1,5 @@ - + {{yield (hash link=(component 'link-to' class="rose-header-navigation-link") dropdown=(component 'rose/header/dropdown') diff --git a/addons/rose/addon/templates/components/rose/header/utilities.hbs b/addons/rose/addon/templates/components/rose/header/utilities.hbs index c5b060286..eefb15e8d 100644 --- a/addons/rose/addon/templates/components/rose/header/utilities.hbs +++ b/addons/rose/addon/templates/components/rose/header/utilities.hbs @@ -1,5 +1,5 @@ - + {{yield (hash dropdown=(component 'rose/header/dropdown' position="true") )}} diff --git a/addons/rose/app/modifiers/on-click-outside.js b/addons/rose/app/modifiers/on-click-outside.js new file mode 100644 index 000000000..845e122c1 --- /dev/null +++ b/addons/rose/app/modifiers/on-click-outside.js @@ -0,0 +1 @@ +export { default } from 'rose/modifiers/on-click-outside'; diff --git a/addons/rose/tests/dummy/app/templates/index.hbs b/addons/rose/tests/dummy/app/templates/index.hbs index 236fd0598..d7aa67b35 100644 --- a/addons/rose/tests/dummy/app/templates/index.hbs +++ b/addons/rose/tests/dummy/app/templates/index.hbs @@ -13,7 +13,7 @@ - + Click me link diff --git a/addons/rose/tests/integration/components/rose/header/dropdown-test.js b/addons/rose/tests/integration/components/rose/header/dropdown-test.js index ea6f0cb29..690e5103d 100644 --- a/addons/rose/tests/integration/components/rose/header/dropdown-test.js +++ b/addons/rose/tests/integration/components/rose/header/dropdown-test.js @@ -40,22 +40,6 @@ module('Integration | Component | rose/header/dropdown', function(hooks) { assert.equal(findAll('button').length, 3); }); - test('it is opened when @isOpen={{true}}', async function(assert) { - this.set('isOpen', false); - await render(hbs` - Click me - - - - `); - assert.notOk(find('.rose-header-dropdown-trigger.rose-header-dropdown-open')); - assert.notOk(find('.rose-header-dropdown-content.rose-header-dropdown-open')); - - this.set('isOpen', true); - assert.ok(find('.rose-header-dropdown-trigger.rose-header-dropdown-open')); - assert.ok(find('.rose-header-dropdown-content.rose-header-dropdown-open')); - }); - test('it is toggled on click on trigger content', async function(assert) { await render(hbs` Click me @@ -64,19 +48,12 @@ module('Integration | Component | rose/header/dropdown', function(hooks) { `); assert.notOk(find('#dropdown').open); - assert.notOk(find('.rose-header-dropdown-trigger.rose-header-dropdown-open')); - assert.notOk(find('.rose-header-dropdown-content.rose-header-dropdown-open')); await click('#trigger'); - assert.ok(find('#dropdown').open); - assert.ok(find('.rose-header-dropdown-trigger.rose-header-dropdown-open')); - assert.ok(find('.rose-header-dropdown-content.rose-header-dropdown-open')); await click('#trigger'); assert.notOk(find('#dropdown').open); - assert.notOk(find('.rose-header-dropdown-trigger.rose-header-dropdown-open')); - assert.notOk(find('.rose-header-dropdown-content.rose-header-dropdown-open')); }); test('it is closed with outside click is triggered', async function(assert) { diff --git a/addons/rose/tests/integration/components/rose/header/dropdown/content-test.js b/addons/rose/tests/integration/components/rose/header/dropdown/content-test.js index 7ea669671..26fd3c413 100644 --- a/addons/rose/tests/integration/components/rose/header/dropdown/content-test.js +++ b/addons/rose/tests/integration/components/rose/header/dropdown/content-test.js @@ -24,20 +24,11 @@ module('Integration | Component | rose/header/dropdown/content', function(hooks) assert.ok(find('.rose-header-dropdown-content').className.match('universe')); }); - test('it is open when @isOpen={{true}}', async function(assert) { - this.set('isOpen', false); - await render(hbs``); - assert.notOk(find('.rose-header-dropdown-open')); - - this.set('isOpen', true); - assert.ok(find('.rose-header-dropdown-open')); - }); - - test('it is positioned right when @position={{true}}', async function(assert) { + test('it is positioned right when @alignRight={{true}}', async function(assert) { await render(hbs``); assert.notOk(find('.rose-header-dropdown-content-right')); - await render(hbs``); + await render(hbs``); assert.ok(find('.rose-header-dropdown-content-right')); }); diff --git a/addons/rose/tests/integration/components/rose/header/dropdown/trigger-test.js b/addons/rose/tests/integration/components/rose/header/dropdown/trigger-test.js index e2180c867..aca4e98b3 100644 --- a/addons/rose/tests/integration/components/rose/header/dropdown/trigger-test.js +++ b/addons/rose/tests/integration/components/rose/header/dropdown/trigger-test.js @@ -25,11 +25,6 @@ module('Integration | Component | rose/header/dropdown/trigger', function(hooks) assert.ok(find('.rose-header-dropdown-trigger').className.match('dropdown-trigger')); }); - test('it is open when @isOpen={{true}}', async function(assert) { - await render(hbs``); - assert.ok(find('.rose-header-dropdown-open')); - }); - test('it renders chevron', async function(assert) { await render(hbs``); assert.ok(find('.rose-header-dropdown-chevron')); diff --git a/addons/rose/tests/integration/modifiers/on-click-outside-test.js b/addons/rose/tests/integration/modifiers/on-click-outside-test.js new file mode 100644 index 000000000..859de9902 --- /dev/null +++ b/addons/rose/tests/integration/modifiers/on-click-outside-test.js @@ -0,0 +1,34 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render, click } from '@ember/test-helpers'; +import hbs from 'htmlbars-inline-precompile'; + +module('Integration | Modifier | on-click-outside', function(hooks) { + setupRenderingTest(hooks); + + test('it fires event when an outside element is clicked', async function(assert) { + this.set('clickedOutside', (element, event) => { + assert.ok(element); + assert.equal(event.target.id, 'outside'); + }); + + await render(hbs`
+
`); + + await click('#outside'); + }); + + test('it does not fire event when element is clicked', async function(assert) { + assert.expect(1); + this.set('clickedOutside', () => { assert.ok(true)}); + + await render(hbs`
+
+
+
`); + + await click('#inside'); + await click('#item'); + await click('#outside'); + }); +});