mirror of https://github.com/hashicorp/consul
ui: Tab Improvements (animations/branding) (#7772)
* ui: Adds a tab selection animation to our app tabs 1. Replace all mentions of `magenta` with a themeable CSS property. 2. Add an easy way to inline style DOM nodes 3. Use CSS properties to add tab animation * Fix up rendering test * Avoid DOM noodling as much as possiblepull/7344/head
parent
12e2c93e6b
commit
135d22586b
|
@ -1,17 +1,21 @@
|
||||||
<nav role="tablist" class="tab-nav">
|
<nav
|
||||||
|
style={{if selectedWidth (concat '--selected-width:' selectedWidth ';--selected-left:' selectedLeft ';--selected-height:' selectedHeight ';--selected-top:' selectedTop) undefined}}
|
||||||
|
role="tablist"
|
||||||
|
class={{concat 'tab-nav' (if isAnimatable ' animatable' '')}}
|
||||||
|
id={{guid}}>
|
||||||
<ul>
|
<ul>
|
||||||
{{#each items as |item|}}
|
{{#each items as |item|}}
|
||||||
<li
|
<li
|
||||||
data-test-tab={{concat name '_' (if item.label (slugify item.label) (slugify item))}}
|
data-test-tab={{concat name '_' (if item.label (slugify item.label) (slugify item))}}
|
||||||
class={{if (or item.selected (eq selected (if item.label (slugify item.label) (slugify item)))) 'selected'}}
|
class={{if (or item.selected (eq selected (if item.label (slugify item.label) (slugify item)))) 'selected'}}
|
||||||
>
|
>
|
||||||
<label role="tab" onkeydown={{action 'keydown'}} tabindex="0" aria-controls="radiogroup_{{name}}_{{if item.label (slugify item.label) (slugify item)}}_panel" for="radiogroup_{{name}}_{{if item.label (slugify item.label) (slugify item)}}" data-test-radiobutton="{{name}}_{{if item.label (slugify item.label) (slugify item)}}">
|
|
||||||
{{#if item.href }}
|
{{#if item.href }}
|
||||||
<a href={{item.href}}>{{item.label}}</a>
|
<a href={{item.href}}>{{item.label}}</a>
|
||||||
{{else}}
|
{{else}}
|
||||||
|
<label role="tab" onkeydown={{action 'keydown'}} tabindex="0" aria-controls="radiogroup_{{name}}_{{if item.label (slugify item.label) (slugify item)}}_panel" for="radiogroup_{{name}}_{{if item.label (slugify item.label) (slugify item)}}" data-test-radiobutton="{{name}}_{{if item.label (slugify item.label) (slugify item)}}">
|
||||||
<a>{{item}}</a>
|
<a>{{item}}</a>
|
||||||
{{/if}}
|
|
||||||
</label>
|
</label>
|
||||||
|
{{/if}}
|
||||||
</li>
|
</li>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -1,9 +1,39 @@
|
||||||
import Component from '@ember/component';
|
import Component from '@ember/component';
|
||||||
|
import { setProperties, set } from '@ember/object';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import { schedule } from '@ember/runloop';
|
||||||
|
|
||||||
const ENTER = 13;
|
const ENTER = 13;
|
||||||
|
const SELECTED = 'li.selected';
|
||||||
export default Component.extend({
|
export default Component.extend({
|
||||||
name: 'tab',
|
name: 'tab',
|
||||||
tagName: '',
|
tagName: '',
|
||||||
|
dom: service('dom'),
|
||||||
|
isAnimatable: false,
|
||||||
|
init: function() {
|
||||||
|
this._super(...arguments);
|
||||||
|
this.guid = this.dom.guid(this);
|
||||||
|
},
|
||||||
|
didInsertElement: function() {
|
||||||
|
this._super(...arguments);
|
||||||
|
this.$nav = this.dom.element(`#${this.guid}`);
|
||||||
|
this.select(this.dom.element(SELECTED, this.$nav));
|
||||||
|
set(this, 'isAnimatable', true);
|
||||||
|
},
|
||||||
|
didUpdateAttrs: function() {
|
||||||
|
schedule('afterRender', () => this.select(this.dom.element(SELECTED, this.$nav)));
|
||||||
|
},
|
||||||
|
select: function($el) {
|
||||||
|
if (!$el) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setProperties(this, {
|
||||||
|
selectedWidth: $el.offsetWidth,
|
||||||
|
selectedLeft: $el.offsetLeft,
|
||||||
|
selectedHeight: $el.offsetHeight,
|
||||||
|
selectedTop: $el.offsetTop,
|
||||||
|
});
|
||||||
|
},
|
||||||
actions: {
|
actions: {
|
||||||
keydown: function(e) {
|
keydown: function(e) {
|
||||||
if (e.keyCode === ENTER) {
|
if (e.keyCode === ENTER) {
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
%with-transition-500 {
|
||||||
|
transition-duration: 0.15s;
|
||||||
|
transition-timing-function: ease-out;
|
||||||
|
}
|
||||||
%blink-in-fade-out {
|
%blink-in-fade-out {
|
||||||
transition-property: opacity;
|
transition-property: opacity;
|
||||||
transition-duration: 0.1s;
|
transition-duration: 0.1s;
|
||||||
|
|
|
@ -6,6 +6,12 @@
|
||||||
/* same as decor-border-000 - but need to think about being able to import color on its own*/
|
/* same as decor-border-000 - but need to think about being able to import color on its own*/
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
}
|
}
|
||||||
|
%frame-brand-300 {
|
||||||
|
@extend %frame-border-000;
|
||||||
|
background-color: $transparent;
|
||||||
|
border-color: var(--decor-brand-600, inherit);
|
||||||
|
color: var(--typo-brand-600, inherit);
|
||||||
|
}
|
||||||
|
|
||||||
/* possibly filter bar */
|
/* possibly filter bar */
|
||||||
/* modal close button */
|
/* modal close button */
|
||||||
|
|
|
@ -40,6 +40,5 @@
|
||||||
}
|
}
|
||||||
%form-element-note > code {
|
%form-element-note > code {
|
||||||
background-color: $gray-200;
|
background-color: $gray-200;
|
||||||
/* consul color but its a code looking style?*/
|
color: var(--typo-brand-600, inherit);
|
||||||
color: $magenta-600;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,5 +31,5 @@
|
||||||
}
|
}
|
||||||
%menu-panel .is-active > *::after {
|
%menu-panel .is-active > *::after {
|
||||||
@extend %with-check-plain-mask, %as-pseudo;
|
@extend %with-check-plain-mask, %as-pseudo;
|
||||||
background-color: $magenta-600;
|
background-color: var(--swatch-brand-600, $black);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
@import './skin';
|
@import './skin';
|
||||||
@import './layout';
|
@import './layout';
|
||||||
|
%with-animated-tab-selection ul::after,
|
||||||
|
%tab-button-selected {
|
||||||
|
@extend %frame-brand-300;
|
||||||
|
}
|
||||||
%tab-nav li a {
|
%tab-nav li a {
|
||||||
@extend %tab-button;
|
@extend %tab-button;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,19 +2,25 @@
|
||||||
clear: both;
|
clear: both;
|
||||||
}
|
}
|
||||||
%tab-nav ul {
|
%tab-nav ul {
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
%tab-button {
|
%with-animated-tab-selection ul::after {
|
||||||
padding-left: 16px;
|
@extend %as-pseudo, %with-transition-500;
|
||||||
padding-right: 16px;
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
height: 0;
|
||||||
|
border-top: 0;
|
||||||
|
width: calc(var(--selected-width, 0) * 1px);
|
||||||
|
transform: translate(calc(var(--selected-left, 0) * 1px), 0);
|
||||||
|
transition-property: transform, width;
|
||||||
}
|
}
|
||||||
%tab-button {
|
%tab-button {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding-top: 13px;
|
padding: 16px 13px;
|
||||||
padding-bottom: 13px;
|
|
||||||
}
|
}
|
||||||
%tab-section section h3 {
|
%tab-section section h3 {
|
||||||
margin: 24px 0;
|
margin: 24px 0;
|
||||||
|
|
|
@ -1,13 +1,3 @@
|
||||||
%tab-nav {
|
|
||||||
/* %frame-gray-something */
|
|
||||||
border-bottom: $decor-border-100;
|
|
||||||
/* TODO: structure tabs don't actually have a top border */
|
|
||||||
border-top: $decor-border-200;
|
|
||||||
}
|
|
||||||
%tab-nav {
|
|
||||||
/* %frame-gray-something */
|
|
||||||
border-color: $gray-200;
|
|
||||||
}
|
|
||||||
%tab-nav ul {
|
%tab-nav ul {
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
}
|
}
|
||||||
|
@ -18,16 +8,29 @@
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
%tab-nav {
|
||||||
|
/* %frame-transparent-something */
|
||||||
|
border-bottom: $decor-border-100;
|
||||||
|
}
|
||||||
|
%tab-nav {
|
||||||
|
/* %frame-transparent-something */
|
||||||
|
border-color: $gray-200;
|
||||||
|
}
|
||||||
|
%with-animated-tab-selection ul::after,
|
||||||
%tab-button {
|
%tab-button {
|
||||||
border-bottom: $decor-border-300;
|
border-bottom: $decor-border-300;
|
||||||
}
|
}
|
||||||
%tab-button {
|
%tab-button {
|
||||||
border-color: $color-transparent;
|
@extend %with-transition-500;
|
||||||
|
transition-property: background-color;
|
||||||
|
border-color: $transparent;
|
||||||
color: $gray-500;
|
color: $gray-500;
|
||||||
}
|
}
|
||||||
%tab-button-intent,
|
%tab-button-intent,
|
||||||
%tab-button-active {
|
%tab-button-active {
|
||||||
/* %frame-gray-something */
|
/* %frame-gray-something */
|
||||||
border-color: $color-transparent;
|
|
||||||
background-color: $gray-100;
|
background-color: $gray-100;
|
||||||
}
|
}
|
||||||
|
%tab-nav.animatable .selected a {
|
||||||
|
border-color: $transparent !important;
|
||||||
|
}
|
||||||
|
|
|
@ -12,19 +12,19 @@
|
||||||
@extend %with-logo-github-monochrome-icon, %as-pseudo;
|
@extend %with-logo-github-monochrome-icon, %as-pseudo;
|
||||||
}
|
}
|
||||||
%main-header-horizontal::before {
|
%main-header-horizontal::before {
|
||||||
background-color: $magenta-600;
|
background-color: var(--swatch-brand-600, $black);
|
||||||
}
|
}
|
||||||
%main-nav-horizontal-action,
|
%main-nav-horizontal-action,
|
||||||
%main-nav-horizontal-toggle-button {
|
%main-nav-horizontal-toggle-button {
|
||||||
color: $magenta-050;
|
color: var(--typo-brand-050, $black);
|
||||||
}
|
}
|
||||||
@media #{$--lt-horizontal-nav} {
|
@media #{$--lt-horizontal-nav} {
|
||||||
%main-nav-horizontal-panel {
|
%main-nav-horizontal-panel {
|
||||||
background-color: $magenta-600;
|
background-color: var(---swatch-brand-600, $black);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media #{$--horizontal-nav} {
|
@media #{$--horizontal-nav} {
|
||||||
%main-nav-horizontal-action-active {
|
%main-nav-horizontal-action-active {
|
||||||
background-color: $magenta-800;
|
background-color: var(--swatch-brand-800, $black);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,11 @@
|
||||||
.tab-nav {
|
.tab-nav {
|
||||||
@extend %tab-nav;
|
@extend %tab-nav;
|
||||||
}
|
}
|
||||||
|
%tab-nav.animatable {
|
||||||
|
@extend %with-animated-tab-selection;
|
||||||
|
}
|
||||||
.tab-section {
|
.tab-section {
|
||||||
@extend %tab-section;
|
@extend %tab-section;
|
||||||
/* this keeps in-tab-section toolbars flush to the top, see Node Detail > Services */
|
/* this keeps in-tab-section toolbars flush to the top, see Node Detail > Services */
|
||||||
margin-top: 0 !important;
|
margin-top: 0 !important;
|
||||||
}
|
}
|
||||||
%tab-button-selected {
|
|
||||||
@extend %frame-magenta-300;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
td a.is-management::after {
|
td a.is-management::after {
|
||||||
@extend %with-star-fill-mask, %as-pseudo;
|
@extend %with-star-fill-mask, %as-pseudo;
|
||||||
background-color: $magenta-600;
|
background-color: var(--swatch-brand-600, $black);
|
||||||
height: 16px;
|
height: 16px;
|
||||||
top: 2px;
|
top: 2px;
|
||||||
padding-left: 32px;
|
padding-left: 32px;
|
||||||
|
|
|
@ -9,4 +9,23 @@
|
||||||
--decor-radius-200: #{$decor-radius-200};
|
--decor-radius-200: #{$decor-radius-200};
|
||||||
--gray-500: #{$gray-500};
|
--gray-500: #{$gray-500};
|
||||||
--decor-elevation-600: #{$decor-elevation-600};
|
--decor-elevation-600: #{$decor-elevation-600};
|
||||||
|
|
||||||
|
/* base brand colors */
|
||||||
|
--brand-050: #{$magenta-050};
|
||||||
|
// --brand-100: #{$magenta-100};
|
||||||
|
// --brand-200: #{$magenta-200};
|
||||||
|
// --brand-300: #{$magenta-300};
|
||||||
|
// --brand-400: #{$magenta-400};
|
||||||
|
// --brand-500: #{$magenta-500};
|
||||||
|
--brand-600: #{$magenta-600};
|
||||||
|
// --brand-700: #{$magenta-700};
|
||||||
|
--brand-800: #{$magenta-800};
|
||||||
|
// --brand-900: #{$magenta-900};
|
||||||
|
|
||||||
|
/* themeable brand colors */
|
||||||
|
--typo-brand-050: var(--brand-050);
|
||||||
|
--typo-brand-600: var(--brand-600);
|
||||||
|
--decor-brand-600: var(--brand-600);
|
||||||
|
--swatch-brand-600: var(--brand-600);
|
||||||
|
--swatch-brand-800: var(--brand-800);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,15 +10,15 @@ module('Integration | Component | tab nav', function(hooks) {
|
||||||
// Set any properties with this.set('myProperty', 'value');
|
// Set any properties with this.set('myProperty', 'value');
|
||||||
// Handle any actions with this.on('myAction', function(val) { ... });
|
// Handle any actions with this.on('myAction', function(val) { ... });
|
||||||
|
|
||||||
await render(hbs`{{tab-nav}}`);
|
|
||||||
|
|
||||||
assert.dom('*').hasText('');
|
|
||||||
|
|
||||||
// Template block usage:
|
|
||||||
await render(hbs`
|
await render(hbs`
|
||||||
{{#tab-nav}}{{/tab-nav}}
|
<TabNav @items={{array
|
||||||
|
(hash
|
||||||
|
label="Tab Label"
|
||||||
|
href="/"
|
||||||
|
)
|
||||||
|
}} />
|
||||||
`);
|
`);
|
||||||
|
|
||||||
assert.dom('*').hasText('');
|
assert.dom('*').hasText('Tab Label');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,7 +17,7 @@ export default function(name, items, blankKey = 'all') {
|
||||||
...prev,
|
...prev,
|
||||||
...{
|
...{
|
||||||
[`${key}IsSelected`]: is('.selected', `[data-test-tab="${name}_${item}"]`),
|
[`${key}IsSelected`]: is('.selected', `[data-test-tab="${name}_${item}"]`),
|
||||||
[key]: clickable(`[data-test-tab="${name}_${item}"] > label > a`),
|
[key]: clickable(`[data-test-tab="${name}_${item}"] a`),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}, {});
|
}, {});
|
||||||
|
|
Loading…
Reference in New Issue