-Update dropdown to use modifiers -Cleanup colors, padding, and margin for header styles -Introduce media query width variables
This commit is contained in:
17 files changed
+122
-113
No files matched your search
@@ -42,7 +42,7 @@ A header component that allows three distinct sections to be configured: brand,
|
||||
<Rose::Header name="main header" as |header|>
|
||||
<header.brand />
|
||||
<header.navigation as |nav|>
|
||||
<nav.dropdown @isOpen={{open-dropdown}} as |dropdown|>
|
||||
<nav.dropdown as |dropdown|>
|
||||
<dropdown.trigger>{{navigation-dropdown-label}}</dropdown.trigger>
|
||||
<dropdown.content as |content|>
|
||||
<content.link @route="about">{{navigation-dropdown-link-label}}</content.link>
|
||||
@@ -52,7 +52,7 @@ A header component that allows three distinct sections to be configured: brand,
|
||||
<nav.link @route="about">{{navigation-link-label}}</nav.link>
|
||||
</header.navigation>
|
||||
<header.utilities as |utilities|>
|
||||
<utilities.dropdown @isOpen={{open-dropdown}} as |dropdown|>
|
||||
<utilities.dropdown @alignRight={{true}} as |dropdown|>
|
||||
<dropdown.trigger><Rose::Icon @name="user-square-outline" /></dropdown.trigger>
|
||||
<dropdown.content as |content|>
|
||||
<content.link @route="about">link</content.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),
|
||||
},
|
||||
}}</Story>
|
||||
</Preview>
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
@@ -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);
|
||||
};
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
@use '../variables/sizing';
|
||||
@use '../variables/media';
|
||||
@use '../utilities/type';
|
||||
|
||||
$brand-hashicorp: "data:image/svg+xml;utf-8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 106.86 113.14' fill='%23ffffff'><g id='Layer_2' data-name='Layer 2'><g id='Logo'><polygon points='44.54 0 0 25.69 0 25.71 0 87.41 16.73 97.07 16.73 35.35 44.54 19.3 44.54 0'/><polygon points='62.32 0 62.32 49.15 44.54 49.15 44.54 30.81 27.8 40.47 27.8 103.44 44.54 113.12 44.54 64.11 62.32 64.11 62.32 82.33 79.05 72.67 79.05 9.66 62.32 0'/><polygon points='62.32 113.14 106.86 87.45 106.86 87.43 106.86 25.73 90.12 16.07 90.12 77.79 62.32 93.84 62.32 113.14'/></g></g></svg>";
|
||||
@@ -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:
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
@import 'typography';
|
||||
@import 'color';
|
||||
@import 'opacity';
|
||||
@import 'media';
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,14 +1,8 @@
|
||||
<details class="rose-header-dropdown"
|
||||
open={{this.isOpen}}
|
||||
...attributes
|
||||
{{did-insert this.addClickOutside}}
|
||||
{{will-destroy this.removeClickOutside}}
|
||||
{{on 'toggle' this.toggle}}>
|
||||
<details class="rose-header-dropdown" ...attributes
|
||||
{{on-click-outside this.close}}>
|
||||
|
||||
{{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)
|
||||
)}}
|
||||
</details>
|
||||
@@ -1,4 +1,4 @@
|
||||
<span class="rose-header-dropdown-content {{if @isOpen "rose-header-dropdown-open"}} {{if @position "rose-header-dropdown-content-right"}}" ...attributes>
|
||||
<span class="rose-header-dropdown-content {{if @alignRight "rose-header-dropdown-content-right"}}" ...attributes>
|
||||
{{yield (hash
|
||||
link=(component 'link-to' class="rose-header-dropdown-content-link")
|
||||
button=(component 'rose/header/dropdown/content/button')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<summary class="rose-header-dropdown-trigger {{if @isOpen "rose-header-dropdown-open"}}" ...attributes>
|
||||
<summary class="rose-header-dropdown-trigger" ...attributes>
|
||||
{{yield}}
|
||||
<span class="rose-header-dropdown-chevron" />
|
||||
</summary>
|
||||
@@ -1,5 +1,5 @@
|
||||
<span class="rose-header-navigation">
|
||||
<span class="wrapper">
|
||||
<span class="rose-header-navigation-flex">
|
||||
{{yield (hash
|
||||
link=(component 'link-to' class="rose-header-navigation-link")
|
||||
dropdown=(component 'rose/header/dropdown')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<span class="rose-header-utilities">
|
||||
<span class="wrapper">
|
||||
<span class="rose-header-utilities-flex">
|
||||
{{yield (hash
|
||||
dropdown=(component 'rose/header/dropdown' position="true")
|
||||
)}}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'rose/modifiers/on-click-outside';
|
||||
@@ -13,7 +13,7 @@
|
||||
</header.navigation>
|
||||
|
||||
<header.utilities as |utilities|>
|
||||
<utilities.dropdown as |dropdown|>
|
||||
<utilities.dropdown @alignRight={{true}} as |dropdown|>
|
||||
<dropdown.trigger>Click me</dropdown.trigger>
|
||||
<dropdown.content as |content|>
|
||||
<content.link @route="about">link</content.link>
|
||||
|
||||
@@ -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`<Rose::Header::Dropdown @isOpen={{this.isOpen}} as |dropdown|>
|
||||
<dropdown.trigger>Click me</dropdown.trigger>
|
||||
<dropdown.content as |content|>
|
||||
<content.button />
|
||||
</dropdown.content>
|
||||
</Rose::Header::Dropdown>`);
|
||||
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`<Rose::Header::Dropdown id="dropdown" as |dropdown|>
|
||||
<dropdown.trigger id="trigger">Click me</dropdown.trigger>
|
||||
@@ -64,19 +48,12 @@ module('Integration | Component | rose/header/dropdown', function(hooks) {
|
||||
</dropdown.content>
|
||||
</Rose::Header::Dropdown>`);
|
||||
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) {
|
||||
|
||||
@@ -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`<Rose::Header::Dropdown::Content @isOpen={{this.isOpen}} />`);
|
||||
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`<Rose::Header::Dropdown::Content />`);
|
||||
assert.notOk(find('.rose-header-dropdown-content-right'));
|
||||
|
||||
await render(hbs`<Rose::Header::Dropdown::Content @position="{{true}}"/>`);
|
||||
await render(hbs`<Rose::Header::Dropdown::Content @alignRight="{{true}}"/>`);
|
||||
assert.ok(find('.rose-header-dropdown-content-right'));
|
||||
});
|
||||
|
||||
|
||||
@@ -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`<Rose::Header::Dropdown::Trigger @isOpen={{true}} />`);
|
||||
assert.ok(find('.rose-header-dropdown-open'));
|
||||
});
|
||||
|
||||
test('it renders chevron', async function(assert) {
|
||||
await render(hbs`<Rose::Header::Dropdown::Trigger />`);
|
||||
assert.ok(find('.rose-header-dropdown-chevron'));
|
||||
|
||||
@@ -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`<div id="outside"></div>
|
||||
<div id="inside" {{on-click-outside this.clickedOutside}}></div>`);
|
||||
|
||||
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`<div id="outside"></div>
|
||||
<div id="inside" {{on-click-outside this.clickedOutside}}>
|
||||
<div id="item" />
|
||||
</div>`);
|
||||
|
||||
await click('#inside');
|
||||
await click('#item');
|
||||
await click('#outside');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user