Merge pull request #358 from hashicorp/feature-dark-mode-toggle

Feature dark mode toggle
This commit is contained in:
Susmitha Girumala authored and GitHub committed 2020-11-04 16:45:41 -05:00
commit 999fbb6720
23 files changed
+334 -85

No files matched your search

@@ -28,6 +28,7 @@
button=(component "rose/dropdown/button" style=this.style)
item=(component "rose/anonymous" class="rose-dropdown-item")
separator=(component "rose/anonymous" tagName="hr" class="rose-dropdown-separator")
section=(component "rose/dropdown/section" style=this.style)
)}}
</div>
@@ -9,13 +9,17 @@ import { boolean } from '@storybook/addon-knobs';
A simple dropdown component with support for links and buttons.
<Preview>
<Story name="Basic Dropdown" height="5rem">{{
<Story name="Basic Dropdown" height="7rem">{{
template: hbs`
<Rose::Dropdown @text="Choose One&hellip;" @icon="user-organization" @showCaret={{showCaret}} as |dropdown|>
<dropdown.link @route="index">Menu item</dropdown.link>
<dropdown.link @route="about">Menu item</dropdown.link>
<dropdown.separator />
<dropdown.button>Button menu item</dropdown.button>
<dropdown.separator />
<dropdown.section @title="Dropdown Section Title">
<dropdown.link @route="index">Menu item</dropdown.link>
</dropdown.section>
</Rose::Dropdown>
`,
context: {
@@ -65,6 +69,10 @@ A simple dropdown component with support for links and buttons.
<dropdown.link @route="index">Menu item</dropdown.link>
<dropdown.link @route="about">Menu item</dropdown.link>
<dropdown.button>Button menu item</dropdown.button>
<dropdown.separator />
<dropdown.section @title="Dropdown Section Title">
<dropdown.link @route="index">Menu item</dropdown.link>
</dropdown.section>
</Rose::Dropdown>
```
@@ -0,0 +1,12 @@
<section class="rose-dropdown-section" aria-labelledby={{concat "dropdown-section-title-" this.id}}>
{{#if @title}}
<h2
id={{concat "dropdown-section-title-" this.id}}
class="rose-dropdown-section-title"
>
{{@title}}
</h2>
{{/if}}
{{yield}}
</section>
@@ -0,0 +1,14 @@
import Component from '@glimmer/component';
import { generateComponentID } from '../../../../utilities/component-auto-id';
import { action } from '@ember/object';
export default class RoseDialogComponent extends Component {
// =attributes
/**
* @type {string}
*/
id = generateComponentID();
}
@@ -1,3 +1,3 @@
{{yield (hash
radio=(component 'rose/form/radio/radio' name=@name selectedValue=@selectedValue disabled=@disabled)
radio=(component 'rose/form/radio/radio' name=@name selectedValue=@selectedValue disabled=@disabled changed=@changed)
)}}
@@ -1,5 +1,6 @@
import { Meta, Story, Preview } from '@storybook/addon-docs/blocks';
import { hbs } from 'ember-cli-htmlbars';
import { action } from '@storybook/addon-actions';
import { text, boolean, select } from '@storybook/addon-knobs';
<Meta title="Rose|Form/Radio/Group" component="Form/Radio/Group" />
@@ -13,6 +14,7 @@ Renders a form radiofield group.
@disabled={{false}}
@name={{name}}
@selectedValue="disagree"
@changed={{fn}}
as |radioGroup|
>
<radioGroup.radio
@@ -36,6 +38,7 @@ Renders a form radiofield group.
@disabled={{disabled}}
@name={{name}}
@selectedValue={{selectedValue}}
@changed={{changed}}
as |radioGroup|
>
<radioGroup.radio
@@ -73,6 +76,7 @@ Renders a form radiofield group.
secondFieldValue: text('second value', 'maybe'),
thirdFieldLabel: text('third label', 'Disagree'),
thirdFieldValue: text('third value', 'disagree'),
changed: action('changed')
}
}}</Story>
</Preview>
@@ -9,7 +9,8 @@
@name={{@name}}
@value={{@value}}
@groupValue={{@selectedValue}}
@disabled={{@disabled}}/>
@disabled={{@disabled}}
@changed={{@changed}} />
<label class="rose-form-radio-label" for={{this.id}}>
@@ -15,6 +15,7 @@ Renders a form radiofield.
@value="yes-agree"
@selectedValue="yes-agree"
@disabled={{false}}
@changed={{fn}}
/>
```
@@ -6,6 +6,24 @@
$xxxs: sizing.rems(xxxs); // 2
.rose-card-link {
--background-color-light: transparent;
--background-color-dark: var(--ui-gray-subtler-6);
--background-color: var(--background-color-light);
.ember-application.rose-theme-light & {
--background-color: var(--background-color-light);
}
@media (prefers-color-scheme: dark) {
--background-color: var(--background-color-dark);
}
.ember-application.rose-theme-dark & {
--background-color: var(--background-color-dark);
}
@include type.type(s);
border: sizing.rems(xxxxs) solid var(--ui-gray-subtler-2);
border-radius: sizing.rems(xxxs);
@@ -15,10 +33,7 @@ $xxxs: sizing.rems(xxxs); // 2
margin: 0 sizing.rems(m) sizing.rems(m) 0;
box-shadow: 0 $xxxs $xxxs rgba(var(--stark-components), var(--opacity-4)),
0 $xxxs $xxxs rgba(var(--stark-components), var(--opacity-4));
@media (prefers-color-scheme: dark) {
background-color: var(--ui-gray-subtler-6);
}
background-color: var(--background-color);
a {
text-decoration: none;
@@ -3,10 +3,27 @@
$xs: sizing.rems(xs); // 8
$xxs: sizing.rems(xxs); // 4
$chevron: "data:image/svg+xml;utf-8,<svg viewBox='0 0 24 24' fill='%231b1f26' 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>";
$chevronWhite: "data:image/svg+xml;utf-8,<svg viewBox='0 0 24 24' fill='%23ffffff' 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>";
$chevron: url("data:image/svg+xml;utf-8,<svg viewBox='0 0 24 24' fill='%231b1f26' 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>");
$chevronWhite: url("data:image/svg+xml;utf-8,<svg viewBox='0 0 24 24' fill='%23ffffff' 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-dropdown {
--background-image-light: #{$chevron};
--background-image-dark: #{$chevronWhite};
--background-image: var(--background-image-light);
.ember-application.rose-theme-light & {
--background-image: var(--background-image-light);
}
@media (prefers-color-scheme: dark) {
--background-image: var(--background-image-dark);
}
.ember-application.rose-theme-dark & {
--background-image: var(--background-image-dark);
}
position: relative;
display: inline-block;
@@ -59,14 +76,10 @@ $chevronWhite: "data:image/svg+xml;utf-8,<svg viewBox='0 0 24 24' fill='%23fffff
&.show-caret {
padding-right: sizing.rems(xl);
background-image: url($chevron);
background: var(--background-image);
background-repeat: no-repeat;
background-position: right sizing.rems(m) center;
background-size: sizing.rems(m);
@media (prefers-color-scheme: dark) {
background-image: url($chevronWhite);
}
}
// Hide default twistie triangle
@@ -2,3 +2,4 @@
@import 'button';
@import 'link';
@import 'item';
@import 'section';
@@ -0,0 +1,13 @@
@use '../../variables/sizing';
@use '../../utilities/type';
.rose-dropdown-section {
.rose-dropdown-section-title {
@include type.type(s, bold);
padding: 0 sizing.rems(s);
margin: 0;
$line-height: 36 / 14;
line-height: $line-height;
color: var(--ui-gray)
}
}
@@ -9,15 +9,29 @@
@import 'dropdown';
.rose-header {
--box-shadow-light: none;
--box-shadow-dark: inset 0 -#{sizing.rems(xxxxs)} 0 var(--ui-border-subtler-1);
--box-shadow: var(--box-shadow-light);
.ember-application.rose-theme-light & {
--box-shadow: var(--box-shadow-light);
}
@media (prefers-color-scheme: dark) {
--box-shadow: var(--box-shadow-dark);
}
.ember-application.rose-theme-dark & {
--box-shadow: var(--box-shadow-dark);
}
display: grid;
grid-template-areas: 'brand nav utilities';
grid-template-columns: 0.01fr auto 1fr;
background: var(--black);
box-shadow: inset 0 -#{sizing.rems(xxxxs)} 0 var(--ui-border-subtler-1);
@media (prefers-color-scheme: light) {
box-shadow: none;
}
box-shadow: var(--box-shadow);
}
@media (max-width: media.width(small)) {
@@ -4,7 +4,7 @@
*/
// Light Mode
:root {
@mixin light-mode {
--opacity-5: 0.025;
--opacity-4: 0.05;
--opacity-3: 0.1;
@@ -15,12 +15,26 @@
// Dark Mode
// Subtler opacities look better in dark mode, so these are halved compared to
// light mode.
@mixin dark-mode {
--opacity-5: 0.0125;
--opacity-4: 0.0125;
--opacity-3: 0.025;
--opacity-2: 0.025;
--opacity-1: 0.125;
}
// Light mode is the default, hence it is applied to `html` by default
.ember-application,
.ember-application.rose-theme-light {
@include light-mode;
}
@media (prefers-color-scheme: dark) {
:root {
--opacity-5: 0.0125;
--opacity-4: 0.0125;
--opacity-3: 0.025;
--opacity-2: 0.025;
--opacity-1: 0.125;
.ember-application {
@include dark-mode;
}
}
.ember-application.rose-theme-dark {
@include dark-mode;
}
@@ -4,7 +4,7 @@
*/
// Light Mode
:root {
@mixin light-mode {
// absolutes
--white: #{color(white)};
--black: #{color(black)};
@@ -80,62 +80,76 @@
}
// Dark Mode
@mixin dark-mode {
// poles
--subtle: var(--black);
--stark: #{color(ui-gray, 100)};
--stark-components: 255, 255, 255;
// theme colors
--info-subtler-3: #{color(blue, 800)};
--info-subtler-2: #{color(blue, 700)};
--info-subtler-1: #{color(blue, 600)};
--info: #{color(blue)};
--info-starker-1: #{color(blue, 300)};
--info-starker-2: #{color(blue, 200)};
--alert-subtler-2: #{color(orange, 900)};
--alert-subtler-1: #{color(orange, 800)};
--alert: #{color(orange)};
--alert-starker-1: #{color(orange, 200)};
--warning-subtler-2: #{color(yellow, 900)};
--warning-subtler-1: #{color(yellow, 800)};
--warning: #{color(yellow)};
--warning-starker-1: #{color(yellow, 200)};
--failure-subtler-2: #{color(red, 900)};
--failure-subtler-1: #{color(red, 800)};
--failure: #{color(red, 300)};
--failure-starker-1: #{color(red, 200)};
--success-subtler-2: #{color(green, 900)};
--success-subtler-1: #{color(green, 800)};
--success: #{color(green, 700)};
--success-starker-1: #{color(green, 400)};
--action-subtler-2: #{darken(color(blue, 900), 10%)};
--action-subtler-1h: #{darken(color(blue, 900), 5%)};
--action-subtler-1: #{color(blue, 800)};
--action: #{color(blue, 400)};
--action-starker-1: #{color(blue, 200)};
--ui-border-subtler-1: #{color(ui-gray, 800)};
--ui-border: #{color(ui-gray, 700)};
--ui-gray-subtler-6: #{color(ui-gray, 900)};
--ui-gray-subtler-5: #{color(ui-gray, 800)};
--ui-gray-subtler-4: #{color(ui-gray, 700)};
--ui-gray-subtler-3: #{color(ui-gray, 600)};
--ui-gray-subtler-2: #{color(ui-gray, 500)};
--ui-gray-subtler-1: #{color(ui-gray, 400)};
--ui-gray: #{color(ui-gray, 300)};
--ui-gray-starker-1: #{color(ui-gray, 200)};
--ui-gray-starker-2: #{color(ui-gray, 100)};
--ui-gray-starker-3: #{color(ui-gray, 050)};
--ui-gray-starker-4: #{color(ui-gray, 010)};
--ui-header-subtler: #{color(ui-cool-gray, 100)};
}
// Light mode is the default, hence it is applied to `html` by default
.ember-application,
.ember-application.rose-theme-light {
@include light-mode;
}
@media (prefers-color-scheme: dark) {
:root {
// poles
--subtle: var(--black);
--stark: #{color(ui-gray, 100)};
--stark-components: 255, 255, 255;
// theme colors
--info-subtler-3: #{color(blue, 800)};
--info-subtler-2: #{color(blue, 700)};
--info-subtler-1: #{color(blue, 600)};
--info: #{color(blue)};
--info-starker-1: #{color(blue, 300)};
--info-starker-2: #{color(blue, 200)};
--alert-subtler-2: #{color(orange, 900)};
--alert-subtler-1: #{color(orange, 800)};
--alert: #{color(orange)};
--alert-starker-1: #{color(orange, 200)};
--warning-subtler-2: #{color(yellow, 900)};
--warning-subtler-1: #{color(yellow, 800)};
--warning: #{color(yellow)};
--warning-starker-1: #{color(yellow, 200)};
--failure-subtler-2: #{color(red, 900)};
--failure-subtler-1: #{color(red, 800)};
--failure: #{color(red, 300)};
--failure-starker-1: #{color(red, 200)};
--success-subtler-2: #{color(green, 900)};
--success-subtler-1: #{color(green, 800)};
--success: #{color(green, 700)};
--success-starker-1: #{color(green, 400)};
--action-subtler-2: #{darken(color(blue, 900), 10%)};
--action-subtler-1h: #{darken(color(blue, 900), 5%)};
--action-subtler-1: #{color(blue, 800)};
--action: #{color(blue, 400)};
--action-starker-1: #{color(blue, 200)};
--ui-border-subtler-1: #{color(ui-gray, 800)};
--ui-border: #{color(ui-gray, 700)};
--ui-gray-subtler-6: #{color(ui-gray, 900)};
--ui-gray-subtler-5: #{color(ui-gray, 800)};
--ui-gray-subtler-4: #{color(ui-gray, 700)};
--ui-gray-subtler-3: #{color(ui-gray, 600)};
--ui-gray-subtler-2: #{color(ui-gray, 500)};
--ui-gray-subtler-1: #{color(ui-gray, 400)};
--ui-gray: #{color(ui-gray, 300)};
--ui-gray-starker-1: #{color(ui-gray, 200)};
--ui-gray-starker-2: #{color(ui-gray, 100)};
--ui-gray-starker-3: #{color(ui-gray, 050)};
--ui-gray-starker-4: #{color(ui-gray, 010)};
--ui-header-subtler: #{color(ui-cool-gray, 100)};
.ember-application {
@include dark-mode;
}
}
.ember-application.rose-theme-dark {
@include dark-mode;
}
@@ -0,0 +1 @@
export { default } from 'rose/components/rose/dropdown/section';
@@ -35,6 +35,11 @@
<dropdown.button>Button menu item</dropdown.button>
<dropdown.button @style="danger">Button - danger</dropdown.button>
<dropdown.separator />
<dropdown.section @title="Dropdown Section Title">
<dropdown.link @route="index">Menu item</dropdown.link>
</dropdown.section>
</Rose::Dropdown>
<Rose::Dropdown @showCaret={{false}} @icon="more-horizontal" as |dropdown|>
@@ -0,0 +1,27 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, find } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Component | rose/dropdown/section', function(hooks) {
setupRenderingTest(hooks);
test('it renders', async function(assert) {
await render(hbs`
<Rose::Dropdown::Section>
template block text
</Rose::Dropdown::Section>
`);
assert.ok(find('.rose-dropdown-section'));
assert.equal(this.element.textContent.trim(), 'template block text');
});
test('it renders with optional @title', async function(assert) {
await render(hbs`
<Rose::Dropdown::Section @title="Section Title">
template block text
</Rose::Dropdown::Section>
`);
assert.equal(find('.rose-dropdown-section-title').textContent.trim(), 'Section Title');
});
});
@@ -52,9 +52,15 @@ module('Integration | Component | rose/form/radio/group', function (hooks) {
assert.equal(findAll('[type="radio"]')[1].checked, false);
});
test('it reflects active radiofield value in @selectedValue', async function (assert) {
test('it reflects active radiofield value in @selectedValue and triggers a @changed function', async function (assert) {
assert.expect(5);
this.changed = (value) => assert.ok(value);
await render(hbs`
<Rose::Form::Radio::Group @name="bird" @selectedValue={{this.selectedValue}} as |radioGroup|>
<Rose::Form::Radio::Group
@name="bird"
@selectedValue={{this.selectedValue}}
@changed={{this.changed}}
as |radioGroup|>
<radioGroup.radio
@name="bird"
@label="Pegion"
+30
View File
@@ -26,6 +26,11 @@ export default class ApplicationRoute extends Route.extend(
// =methods
beforeModel() {
const theme = this.session.get('data.theme');
this.toggleTheme(theme);
}
/**
* After becoming authenticated, does nothing. This overrides the default
* behavior of the ApplicationRouteMixin, which is to redirect after auth.
@@ -115,4 +120,29 @@ export default class ApplicationRoute extends Route.extend(
}
}
/**
* Applies the specified color theme to the root ember element.
* @param {string} theme - "light", "dark", or nullish (system default)
*/
@action
toggleTheme(theme) {
const rootElementSelector = getOwner(this).rootElement;
const rootEl = getOwner(this).lookup('service:-document')
.querySelector(rootElementSelector);
this.session.set('data.theme', theme);
switch (theme) {
case 'light':
rootEl.classList.add('rose-theme-light');
rootEl.classList.remove('rose-theme-dark');
break;
case 'dark':
rootEl.classList.add('rose-theme-dark');
rootEl.classList.remove('rose-theme-light');
break;
default:
rootEl.classList.remove('rose-theme-dark');
rootEl.classList.remove('rose-theme-light');
}
}
}
+24
View File
@@ -23,11 +23,35 @@
{{t 'actions.change-password'}}
</dropdown.link>
<dropdown.separator />
<dropdown.button
{{on "click" (route-action "invalidateSession")}}
>
{{t 'actions.deauthenticate'}}
</dropdown.button>
<dropdown.separator />
<dropdown.section @title={{t "titles.toggle-color-theme"}}>
<Rose::Form as |form|>
<form.radioGroup
@name="theme"
@selectedValue={{this.session.data.theme}}
@changed={{route-action "toggleTheme"}}
as |radioGroup|>
<dropdown.item>
<radioGroup.radio @label={{t "themes.system"}} />
</dropdown.item>
<dropdown.item>
<radioGroup.radio @label={{t "themes.light"}} @value="light" />
</dropdown.item>
<dropdown.item>
<radioGroup.radio @label={{t "themes.dark"}} @value="dark" />
</dropdown.item>
</form.radioGroup>
</Rose::Form>
</dropdown.section>
</utilities.dropdown>
</header.utilities>
@@ -6,6 +6,7 @@ import {
click,
find,
findAll,
getRootElement
//setupOnerror,
} from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
@@ -315,4 +316,29 @@ module('Acceptance | authentication', function (hooks) {
'Session is unauthenticated, after encountering 401'
);
});
test('color theme is applied from session data', async function (assert) {
assert.expect(12);
authenticateSession({ scope: { id: globalScope.id, type: globalScope.type } });
// system default
await visit(orgsURL);
assert.notOk(currentSession().get('data.theme'));
assert.notOk(getRootElement().classList.contains('rose-theme-light'));
assert.notOk(getRootElement().classList.contains('rose-theme-dark'));
// toggle light mode
await click('[name="theme"][value="light"]');
assert.equal(currentSession().get('data.theme'), 'light');
assert.ok(getRootElement().classList.contains('rose-theme-light'));
assert.notOk(getRootElement().classList.contains('rose-theme-dark'));
// toggle dark mode
await click('[name="theme"][value="dark"]');
assert.equal(currentSession().get('data.theme'), 'dark');
assert.notOk(getRootElement().classList.contains('rose-theme-light'));
assert.ok(getRootElement().classList.contains('rose-theme-dark'));
// toggle system default
await click('[name="theme"][value=""]');
assert.notOk(currentSession().get('data.theme'));
assert.notOk(getRootElement().classList.contains('rose-theme-light'));
assert.notOk(getRootElement().classList.contains('rose-theme-dark'));
});
});
+5
View File
@@ -19,9 +19,14 @@ titles:
actions: Actions
documentation: Documentation
abandon-confirm: Discard unsaved changes?
toggle-color-theme: Toggle Color Theme
descriptions:
empty-set: There are no items to display yet. You may be able to add items or try back later.
questions:
delete-confirm: Are you sure you want to delete this resource?
remove-confirm: Are you sure you want to remove this association?
abandon-confirm: Are you sure you want to navigate away? Your unsaved changes will be lost.
themes:
system: System Default
light: Light
dark: Dark