refactor: 💡 simplify dropdown component and clean up styles

This commit is contained in:
Randall Morey committed 2020-05-26 14:46:55 -04:00
1 parent 63e426c4a4
commit d7b044b2f7
20 files changed
+132 -305

No files matched your search

@@ -14,13 +14,12 @@ A header component that allows three distinct sections to be configured: brand,
<header.brand @logo="logo" @text="Product Name" />
<header.nav as |nav|>
<nav.dropdown as |dropdown|>
<dropdown.trigger>Org name</dropdown.trigger>
<dropdown.content as |content|>
<content.link @route="about">link</content.link>
<content.button>button</content.button>
</dropdown.content>
<nav.dropdown @text="org-name" as |dropdown|>
<dropdown.link @route="index">Menu item</dropdown.link>
<dropdown.link @route="about">Menu item</dropdown.link>
<dropdown.button>Button menu item</dropdown.button>
</nav.dropdown>
<nav.link @route="index">Section</nav.link>
<nav.link @route="about">Section</nav.link>
</header.nav>
@@ -42,14 +41,13 @@ A header component that allows three distinct sections to be configured: brand,
<Rose::Header as |header|>
<header.brand @logo="logo" @text="Product Name" />
<header.nav as |nav|>
<nav.dropdown as |dropdown|>
<dropdown.trigger>{{nav-dropdown-label}}</dropdown.trigger>
<dropdown.content as |content|>
<content.link @route="about">{{nav-dropdown-link-label}}</content.link>
<content.button>{{nav-dropdown-button-label}}</content.button>
</dropdown.content>
<nav.dropdown @text="org-name" as |dropdown|>
<dropdown.link @route="index">Menu item</dropdown.link>
<dropdown.link @route="about">Menu item</dropdown.link>
<dropdown.button>Button menu item</dropdown.button>
</nav.dropdown>
<nav.link @route="about">{{nav-link-label}}</nav.link>
<nav.link @route="index">Section</nav.link>
<nav.link @route="about">Section</nav.link>
</header.nav>
<header.utilities as |utilities|>
<utilities.dropdown @alignRight={{true}} as |dropdown|>
@@ -63,7 +61,7 @@ A header component that allows three distinct sections to be configured: brand,
</Rose::Header>
`,
context: {
'nav-dropdown-label': text('nav-dropdown-label', 'Org Name'),
'nav-dropdown-label': text('nav-dropdown-label', 'org-name'),
'nav-dropdown-link-label': text('nav-dropdown-link-label', 'Menu Item'),
'nav-dropdown-button-label': text('nav-dropdown-button-label', 'Menu Item'),
'nav-link-label': text('nav-link-label', 'Section'),
@@ -1,7 +0,0 @@
import Component from '@ember/component';
import layout from '../../../../templates/components/rose/header/dropdown/content';
export default Component.extend({
layout,
tagName: '',
});
@@ -1,7 +0,0 @@
import Component from '@ember/component';
import layout from '../../../../../templates/components/rose/header/dropdown/content/button';
export default Component.extend({
layout,
tagName: '',
});
@@ -1,7 +0,0 @@
import Component from '@ember/component';
import layout from '../../../../templates/components/rose/header/dropdown/trigger';
export default Component.extend({
layout,
tagName: '',
});
@@ -1,5 +1,10 @@
import { modifier } from 'ember-modifier';
/**
* Used to detect clicks occuring _outside_ of the modified element.
* This is useful especially for dropdowns to toggle content visibility
* when the user clicks away.
*/
export default modifier((element, [callback]) => {
function handleClick(event) {
if (!element.contains(event.target)) {
@@ -12,37 +12,6 @@
.rose-header-nav,
.rose-header-utilities {
.rose-header-dropdown {
&[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;
}
.rose-header-dropdown-chevron {
background-image: url(map-get($dropdown-chevron, 'default'));
width: sizing.rems(m);
height: sizing.rems(m);
background-repeat: no-repeat;
display: inline-block;
vertical-align: middle;
}
}
.rose-header-dropdown-content {
position: absolute;
@@ -56,27 +25,6 @@
right: sizing.rems(s);
}
.rose-header-dropdown-content-link {
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(xs) sizing.rems(s);
color: var(--ui-gray-starker-4);
display: inline-block;
border: none;
text-decoration: none;
font-size: inherit;
font-weight: inherit;
-webkit-appearance: none;
}
}
}
}
}
@@ -0,0 +1,76 @@
@use 'sass:map';
@use '../../variables/sizing';
@use '../../utilities/type';
$dropdown-chevron: (
'default':
"data:image/svg+xml;utf-8,<svg viewBox='0 0 24 24' fill='%23ebeef2' xmlns='http://www.w3.org/2000/svg'><path fill-rule='evenodd' clip-rule='evenodd' d='M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6-1.41-1.41z'></path></svg>",
);
.rose-header-dropdown {
position: relative;
.rose-header-dropdown-trigger {
cursor: default;
color: var(--white);
background: url(map.get($dropdown-chevron, default)) no-repeat right center;
margin-right: sizing.rems(s);
padding-right: sizing.rems(l);
// Hide default twistie triangle
// Webkit / Chromium
&::-webkit-details-marker {
display: none;
}
// Firefox
list-style-type: none;
&::marker {
display: none;
}
}
.rose-header-dropdown-content {
$xs: sizing.rems(xs); // 8
$xxs: sizing.rems(xxs); // 4
position: absolute;
padding: $xxs 0;
border: 1px solid var(--ui-gray-subtler-2);
border-radius: sizing.rems(xxxs);
background: var(--subtle);
box-shadow: 0 $xxs $xxs rgba(var(--stark-components), var(--opacity-3)),
0 $xxs $xs rgba(var(--stark-components), var(--opacity-4));
}
&.dropdown-right {
.rose-header-dropdown-content {
right: 0;
}
}
.rose-header-dropdown-link,
.rose-button-header-dropdown {
@include type.type(m, normal);
display: block;
// Targeting min-width of 200
min-width: (sizing.rems(m) + sizing.rems(xxs)) * 10;
padding: 0 sizing.rems(s);
margin: 0;
border: 0;
color: var(--stark);
background-color: transparent;
// target line height is 36, which is not a standard line height for this
// font size
$lineHeight: 36 / 16;
line-height: $lineHeight;
text-align: left;
text-decoration: none;
&:hover {
background-color: var(--ui-gray-subtler-4);
}
}
.rose-button-header-dropdown {
width: 100%;
}
}
@@ -5,13 +5,7 @@
@import 'brand';
@import 'nav';
@import 'link';
$dropdown-chevron: (
'default':
"data:image/svg+xml;utf-8,<svg viewBox='0 0 24 24' fill='%23ebeef2' xmlns='http://www.w3.org/2000/svg'><path fill-rule='evenodd' clip-rule='evenodd' d='M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6-1.41-1.41z'></path></svg>",
'open':
"data:image/svg+xml;utf-8,<svg viewBox='0 0 24 24' fill='%231f2124' xmlns='http://www.w3.org/2000/svg'><path fill-rule='evenodd' clip-rule='evenodd' d='M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6-1.41-1.41z'></path></svg>",
);
@import 'dropdown';
.rose-header {
display: grid;
@@ -1,8 +1,18 @@
<details class="rose-header-dropdown" ...attributes
{{on-click-outside this.close}}>
<details
...attributes
class="rose-header-dropdown{{if @alignRight " dropdown-right"}}"
{{on-click-outside this.close}}
>
<summary class="rose-header-dropdown-trigger">
{{@text}}
</summary>
<div class="rose-header-dropdown-content">
{{yield (hash
link=(component "link-to" class="rose-header-dropdown-link")
button=(component "rose/button" style="header-dropdown")
)}}
</div>
{{yield (hash
trigger=(component 'rose/header/dropdown/trigger')
content=(component 'rose/header/dropdown/content' alignRight=@alignRight)
)}}
</details>
@@ -1,6 +0,0 @@
<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')
)}}
</span>
@@ -1,3 +0,0 @@
<span class="rose-header-dropdown-content-button">
<button type="button" ...attributes>{{yield}}</button>
</span>
@@ -1,4 +0,0 @@
<summary class="rose-header-dropdown-trigger" ...attributes>
{{yield}}
<span class="rose-header-dropdown-chevron" />
</summary>
@@ -1 +0,0 @@
export { default } from 'rose/components/rose/header/dropdown/content';
@@ -1 +0,0 @@
export { default } from 'rose/components/rose/header/dropdown/content/button';
@@ -1 +0,0 @@
export { default } from 'rose/components/rose/header/dropdown/trigger';
@@ -3,22 +3,16 @@
<header.brand @logo="logo" @text="Product Name" />
<header.nav as |nav|>
<nav.dropdown @text="org-name" as |dropdown|>
<dropdown.link @route="index">Menu item</dropdown.link>
<dropdown.link @route="about">Menu item</dropdown.link>
<dropdown.button>Button menu item</dropdown.button>
</nav.dropdown>
<nav.link @route="index">Section</nav.link>
<nav.link @route="about">Section</nav.link>
</header.nav>
{{!--
<header.nav as |nav|>
<nav.dropdown as |dropdown|>
<dropdown.trigger>Org name</dropdown.trigger>
<dropdown.content as |content|>
<content.link @route="about">link</content.link>
<content.button>button</content.button>
</dropdown.content>
</nav.dropdown>
<nav.link @route="about">Section</nav.link>
</header.nav>
<header.utilities as |utilities|>
<utilities.dropdown @alignRight={{true}} as |dropdown|>
<dropdown.trigger><Rose::Icon @name="user-square-outline" /></dropdown.trigger>
@@ -20,10 +20,7 @@ module('Integration | Component | rose/header/dropdown', function (hooks) {
});
test('it renders with trigger', async function (assert) {
await render(hbs`<Rose::Header::Dropdown as |dropdown|>
<dropdown.trigger>Click me</dropdown.trigger>
</Rose::Header::Dropdown>`);
assert.ok(find('.rose-header-dropdown-trigger'));
await render(hbs`<Rose::Header::Dropdown @text="Click me" />`);
assert.equal(
find('.rose-header-dropdown-trigger').textContent.trim(),
'Click me'
@@ -31,14 +28,12 @@ module('Integration | Component | rose/header/dropdown', function (hooks) {
});
test('it renders with content', async function (assert) {
await render(hbs`<Rose::Header::Dropdown as |dropdown|>
<dropdown.content as |content|>
<content.link @route="about"/>
<content.link @route="about"/>
<content.button />
<content.button />
<content.button />
</dropdown.content>
await render(hbs`<Rose::Header::Dropdown @text="Click me" as |dropdown|>
<dropdown.link @route="about"/>
<dropdown.link @route="about"/>
<dropdown.button />
<dropdown.button />
<dropdown.button />
</Rose::Header::Dropdown>`);
assert.ok(find('.rose-header-dropdown-content'));
assert.equal(findAll('a').length, 2);
@@ -46,32 +41,24 @@ module('Integration | Component | rose/header/dropdown', function (hooks) {
});
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>
<dropdown.content as |content|>
<content.button />
</dropdown.content>
</Rose::Header::Dropdown>`);
await render(hbs`<Rose::Header::Dropdown id="dropdown" />`);
assert.notOk(find('#dropdown').open);
await click('#trigger');
await click('summary');
assert.ok(find('#dropdown').open);
await click('#trigger');
await click('summary');
assert.notOk(find('#dropdown').open);
});
test('it is closed with outside click is triggered', async function (assert) {
await render(hbs`<div id="wrapper"><Rose::Header::Dropdown id="dropdown" as |dropdown|>
<dropdown.trigger id="trigger">Click me</dropdown.trigger>
<dropdown.content as |content|>
<content.button />
</dropdown.content>
</Rose::Header::Dropdown></div>`);
await render(hbs`
<div id="wrapper">
<Rose::Header::Dropdown id="dropdown" />
</div>
`);
await click('#trigger');
await click('summary');
assert.ok(find('#dropdown').open);
await click('#wrapper');
assert.notOk(find('#dropdown').open);
});
@@ -1,75 +0,0 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, find } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
module('Integration | Component | rose/header/dropdown/content', function (
hooks
) {
setupRenderingTest(hooks);
test('it renders', async function (assert) {
await render(hbs`<Rose::Header::Dropdown::Content />`);
assert.ok(find('.rose-header-dropdown-content'));
});
test('it renders with content', async function (assert) {
await render(hbs`<Rose::Header::Dropdown::Content>
content
</Rose::Header::Dropdown::Content>`);
assert.equal(
find('.rose-header-dropdown-content').textContent.trim(),
'content'
);
});
test('it renders with html attributes', async function (assert) {
await render(
hbs`<Rose::Header::Dropdown::Content id="dropdown-content" class="universe" />`
);
assert.equal(find('.rose-header-dropdown-content').id, 'dropdown-content');
assert.ok(
find('.rose-header-dropdown-content').className.match('universe')
);
});
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 @alignRight="{{true}}"/>`
);
assert.ok(find('.rose-header-dropdown-content-right'));
});
test('it render link element', async function (assert) {
await render(hbs`<Rose::Header::Dropdown::Content as |content|>
<content.link @route="about" />
</Rose::Header::Dropdown::Content>`);
assert.ok(find('a'));
assert.ok(find('.rose-header-dropdown-content-link'));
});
test('it render link element with content', async function (assert) {
await render(hbs`<Rose::Header::Dropdown::Content as |content|>
<content.link @route="about">linkify</content.link>
</Rose::Header::Dropdown::Content>`);
assert.equal(find('a').textContent.trim(), 'linkify');
});
test('it render button element', async function (assert) {
await render(hbs`<Rose::Header::Dropdown::Content as |content|>
<content.button />
</Rose::Header::Dropdown::Content>`);
assert.ok(find('button'));
assert.ok(find('.rose-header-dropdown-content-button'));
});
test('it render button element with content', async function (assert) {
await render(hbs`<Rose::Header::Dropdown::Content as |content|>
<content.button>dropdown button</content.button>
</Rose::Header::Dropdown::Content>`);
assert.equal(find('button').textContent.trim(), 'dropdown button');
});
});
@@ -1,32 +0,0 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, find } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
module(
'Integration | Component | rose/header/dropdown/content/button',
function (hooks) {
setupRenderingTest(hooks);
test('it renders', async function (assert) {
await render(hbs`<Rose::Header::Dropdown::Content::Button />`);
assert.ok(find('.rose-header-dropdown-content-button'));
});
test('it renders with content', async function (assert) {
await render(
hbs`<Rose::Header::Dropdown::Content::Button>Button content</Rose::Header::Dropdown::Content::Button>`
);
assert.equal(
find('.rose-header-dropdown-content-button').textContent.trim(),
'Button content'
);
});
test('it render button element', async function (assert) {
await render(hbs`<Rose::Header::Dropdown::Content::Button />`);
assert.ok(find('button'));
assert.equal(find('button').type, 'button');
});
}
);
@@ -1,41 +0,0 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, find } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
module('Integration | Component | rose/header/dropdown/trigger', function (
hooks
) {
setupRenderingTest(hooks);
test('it renders', async function (assert) {
await render(hbs`<Rose::Header::Dropdown::Trigger />`);
assert.ok(find('.rose-header-dropdown-trigger'));
assert.notOk(find('.rose-header-dropdown-open'));
});
test('it renders with content', async function (assert) {
await render(hbs`<Rose::Header::Dropdown::Trigger>
trigger content
</Rose::Header::Dropdown::Trigger>`);
assert.equal(
find('.rose-header-dropdown-trigger').textContent.trim(),
'trigger content'
);
});
test('it renders with html attributes', async function (assert) {
await render(
hbs`<Rose::Header::Dropdown::Trigger id="trigger" class="dropdown-trigger" />`
);
assert.equal(find('.rose-header-dropdown-trigger').id, 'trigger');
assert.ok(
find('.rose-header-dropdown-trigger').className.match('dropdown-trigger')
);
});
test('it renders chevron', async function (assert) {
await render(hbs`<Rose::Header::Dropdown::Trigger />`);
assert.ok(find('.rose-header-dropdown-chevron'));
});
});