feat(button): add color attribute and unit tests

This commit is contained in:
Kara Erickson
2016-02-09 10:22:21 -08:00
parent 0359c5eb70
commit 9337954c2d
13 changed files with 391 additions and 258 deletions
+108
View File
@@ -0,0 +1,108 @@
@import "variables";
@import "shadows";
// TODO(jelbourn): This goes away.
@import "default-theme";
@import "button-theme";
// Flat and raised button standards
$md-button-padding: 0 16px !default;
$md-button-min-width: 88px !default;
$md-button-margin: 0 !default;
$md-button-line-height: 36px !default;
$md-button-border-radius: 3px !default;
// Fab standards
$md-fab-border-radius: 50% !default;
$md-fab-size: 56px !default;
$md-fab-padding: 16px !default;
$md-mini-fab-size: 40px !default;
$md-mini-fab-padding: 8px !default;
// Applies base styles to all button types.
%md-button-base {
@include md-button-theme('color');
box-sizing: border-box;
position: relative;
// Reset browser <button> styles.
background: transparent;
text-align: center;
overflow: hidden;
cursor: pointer;
user-select: none;
outline: none;
border: none;
// Make anchors render like buttons.
display: inline-block;
white-space: nowrap;
text-decoration: none;
vertical-align: middle;
// Typography.
font-size: $md-body-font-size-base;
font-family: $md-font-family;
font-weight: 500;
color: md-color($md-foreground, text);
// Sizing.
margin: $md-button-margin;
min-width: $md-button-min-width;
line-height: $md-button-line-height;
padding: $md-button-padding;
border-radius: $md-button-border-radius;
&[disabled] {
cursor: default;
}
}
// Applies styles to buttons with backgrounds: raised, fab, and mini-fab
%md-raised-button {
@extend %md-button-base;
@include md-button-theme('color', default-contrast);
@include md-button-theme('background-color');
background-color: md-color($md-background, background);
box-shadow: $md-shadow-bottom-z-1;
// Force hardware acceleration.
transform: translate3d(0, 0, 0);
// Animation.
transition: background $swift-ease-out-duration $swift-ease-out-timing-function,
box-shadow $swift-ease-out-duration $swift-ease-out-timing-function;
&:active {
box-shadow: $md-shadow-bottom-z-2;
}
&.md-button-focus {
background-color: md-color($md-foreground, base, 0.12);
@include md-button-theme('background-color', 600);
}
&[disabled] {
box-shadow: none;
}
}
// Applies styles to fab and mini-fab button types only
@mixin md-fab($size, $padding) {
@extend %md-raised-button;
min-width: 0;
border-radius: $md-fab-border-radius;
background-color: md-color($md-accent);
width: $size;
height: $size;
padding: $padding;
&.md-button-focus {
background-color: md-color($md-accent, 600);
}
}
+20
View File
@@ -0,0 +1,20 @@
// Applies specified coloring to three supported palettes
@mixin md-button-theme ($property, $color:500, $opacity: 1) {
&.md-primary {
#{$property}: md-color($md-primary, $color, $opacity);
}
&.md-accent {
#{$property}: md-color($md-accent, $color, $opacity);
}
&.md-warn {
#{$property}: md-color($md-warn, $color, $opacity);
}
&.md-primary, &.md-accent, &.md-warn, &[disabled] {
&[disabled] {
$palette: if($property == 'color', $md-foreground, $md-background);
#{$property}: md-color($palette, disabled-button);
}
}
}
+23 -179
View File
@@ -1,195 +1,39 @@
@import "variables";
@import "shadows";
// TODO(jelbourn): This goes away.
@import "default-theme";
// TODO(jelbourn): Move variables and mixins into a partial file.
// TODO(jelbourn): Measure perf benefits for translate3d and will-change.
// TODO(jelbourn): Figure out if anchor hover underline actually happens in any browser.
@import "button-base";
// Standard button sizing.
$md-button-padding: 0 rem(0.600) !default;
$md-button-min-width: rem(8.800) !default;
$md-button-margin: rem(0.600) rem(0.800) !default;
$md-button-line-height: rem(3.60) !default;
$md-button-border-radius: 3px !default;
// FAB sizing.
$md-fab-size: rem(5.600) !default;
$md-fab-line-height: rem(5.600) !default;
$md-fab-padding: rem(1.60) !default;
$md-fab-mini-size: rem(4.00) !default;
$md-fab-mini-line-height: rem(4.00) !default;
/** Mixin to create distinct classes for fab positions, e.g. ".md-fab-position-bottom-right". */
@mixin md-fab-position($spot, $top: auto, $right: auto, $bottom: auto, $left: auto) {
.md-fab-position-#{$spot} {
top: $top;
right: $right;
bottom: $bottom;
left: $left;
position: absolute;
}
}
/** Styles for all disabled buttons. */
@mixin md-button-disabled() {
color: md-color($md-foreground, disabled);
background-color: transparent;
cursor: default;
}
/** Base styles for all buttons. */
@mixin md-button-base() {
box-sizing: border-box;
position: relative;
// Reset browser <button> styles.
background: transparent;
text-align: center;
overflow: hidden;
cursor: pointer;
user-select: none;
outline: none;
border: none;
// Make anchors render like buttons.
display: inline-block;
white-space: nowrap;
text-decoration: none;
vertical-align: middle;
// Typography.
font-size: $md-body-font-size-base;
font-weight: 500;
// Sizing.
padding: $md-button-padding;
margin: $md-button-margin;
min-width: $md-button-min-width;
line-height: $md-button-line-height;
border-radius: $md-button-border-radius;
// Animation.
transition: background $swift-ease-out-duration $swift-ease-out-timing-function,
box-shadow $swift-ease-out-duration $swift-ease-out-timing-function;
// Hide the browser focus indicator, instead applying our own focus style on background-color.
&:focus {
outline: none;
}
&:hover, &:focus {
// Remove anchor underline again for more specific modifiers.
text-decoration: none;
}
// Use a CSS class for focus style because we only want to render the focus style when
// the focus originated from a keyboard event (see JS source for more details).
&:hover, &.md-button-focus {
background: md-color($md-background, background, 0.2);
}
&.md-primary {
color: md-color($md-primary);
}
&.md-accent {
color: md-color($md-accent);
}
// Use the [disabled] attribute instead of the :disabled pseudo-class because anchors
// cannot technically be :disabled.
&[disabled] {
@include md-button-disabled();
}
}
/** Base styles for raised buttons, including FABs. */
@mixin md-raised-button() {
@include md-button-base();
// Force hardware acceleration.
transform: translate3d(0, 0, 0);
box-shadow: $md-shadow-bottom-z-1;
&:active {
box-shadow: $md-shadow-bottom-z-2;
}
&[disabled] {
box-shadow: none;
}
&.md-primary {
color: md-color($md-primary, default-contrast);
background-color: md-color($md-primary);
&:hover, &.md-button-focus {
background-color: md-color($md-primary, 600);
}
}
&.md-accent {
color: md-color($md-accent, default-contrast);
background-color: md-color($md-accent);
&:hover, &.md-button-focus {
background-color: md-color($md-accent, A700);
}
}
&.md-primary, &.md-accent {
&[disabled] {
@include md-button-disabled();
}
}
}
// TODO(kara): Replace attribute selectors with class selectors when possible
[md-button] {
@include md-button-base();
@extend %md-button-base;
&:hover, &.md-button-focus {
background-color: md-color($md-foreground, base, 0.12);
@include md-button-theme('background-color', 0.12);
}
&[disabled]:hover {
&.md-primary, &.md-accent, &.md-warn, &:hover {
background-color: transparent;
}
}
}
[md-raised-button] {
@include md-raised-button();
color: md-color($md-foreground, text);
background-color: md-color($md-background, background);
@extend %md-raised-button;
}
[md-fab] {
@include md-raised-button();
z-index: $z-index-fab;
border-radius: 50%;
min-width: 0;
width: $md-fab-size;
height: $md-fab-size;
line-height: $md-fab-line-height;
vertical-align: middle;
&.md-mini {
line-height: $md-fab-mini-line-height;
width: $md-fab-mini-size;
height: $md-fab-mini-size;
}
@include md-fab($md-fab-size, $md-fab-padding);
}
// Styles for high contrast mode.
[md-mini-fab] {
@include md-fab($md-mini-fab-size, $md-mini-fab-padding)
}
// Applies a clearer border for high-contrast mode (a11y)
@media screen and (-ms-high-contrast: active) {
[md-raised],
[md-fab] {
.md-raised-button, .md-fab, .md-mini-fab {
border: 1px solid #fff;
}
}
$md-fab-pos-offset: ($md-fab-size - $md-fab-padding) / 2;
@include md-fab-position(bottom-right, auto, $md-fab-pos-offset, $md-fab-pos-offset, auto);
@include md-fab-position(bottom-left, auto, auto, $md-fab-pos-offset, $md-fab-pos-offset);
@include md-fab-position(top-right, $md-fab-pos-offset, $md-fab-pos-offset, auto, auto);
@include md-fab-position(top-left, $md-fab-pos-offset, auto, auto, $md-fab-pos-offset);
}
+77 -5
View File
@@ -13,7 +13,7 @@ import {
import {provide, Component, DebugElement} from 'angular2/core';
import {By} from 'angular2/platform/browser';
import {MdButton} from './button';
import {MdButton, MdAnchor} from './button';
import {AsyncTestFn, FunctionWithParamTokens} from 'angular2/testing';
export function main() {
@@ -24,6 +24,27 @@ export function main() {
builder = tcb;
}));
// General button tests
it('should apply class based on color attribute', (done:() => void) => {
return builder.createAsync(TestApp).then((fixture) => {
let testComponent = fixture.debugElement.componentInstance;
let buttonDebugElement = fixture.debugElement.query(By.css('button'));
let aDebugElement = fixture.debugElement.query(By.css('a'));
testComponent.buttonColor = 'primary';
fixture.detectChanges();
expect(buttonDebugElement.nativeElement.classList.contains('md-primary')).toBe(true);
expect(aDebugElement.nativeElement.classList.contains('md-primary')).toBe(true);
testComponent.buttonColor = 'accent';
fixture.detectChanges();
expect(buttonDebugElement.nativeElement.classList.contains('md-accent')).toBe(true);
expect(aDebugElement.nativeElement.classList.contains('md-accent')).toBe(true);
done();
});
});
// Regular button tests
describe('button[md-button]', () => {
it('should handle a click on the button', (done:() => void) => {
return builder.createAsync(TestApp).then((fixture) => {
@@ -48,7 +69,52 @@ export function main() {
expect(testComponent.clickCount).toBe(0);
done();
})
});
});
});
// Anchor button tests
describe('a[md-button]', () => {
it('should not redirect if disabled',(done:() => void)=>{
return builder.createAsync(TestApp).then((fixture) => {
let testComponent = fixture.debugElement.componentInstance;
let buttonDebugElement = fixture.debugElement.query(By.css('a'));
testComponent.isDisabled = true;
fixture.detectChanges();
buttonDebugElement.nativeElement.click();
// will error if page reloads
done();
});
});
it('should remove tabindex if disabled', (done:() => void) => {
return builder.createAsync(TestApp).then((fixture) => {
let testComponent = fixture.debugElement.componentInstance;
let buttonDebugElement = fixture.debugElement.query(By.css('a'));
expect(buttonDebugElement.nativeElement.getAttribute('tabIndex')).toBe(null);
testComponent.isDisabled = true;
fixture.detectChanges();
expect(buttonDebugElement.nativeElement.getAttribute('tabIndex')).toBe('-1');
done();
});
});
it('should add aria-disabled attribute if disabled', (done:() => void) => {
return builder.createAsync(TestApp).then((fixture) => {
let testComponent = fixture.debugElement.componentInstance;
let buttonDebugElement = fixture.debugElement.query(By.css('a'));
fixture.detectChanges();
expect(buttonDebugElement.nativeElement.getAttribute('aria-disabled')).toBe('false');
testComponent.isDisabled = true;
fixture.detectChanges();
expect(buttonDebugElement.nativeElement.getAttribute('aria-disabled')).toBe('true');
done();
});
});
});
@@ -63,9 +129,13 @@ function testAsync(fn: Function): FunctionWithParamTokens {
/** Test component that contains an MdButton. */
@Component({
selector: 'test-app',
directives: [MdButton],
template:
`<button md-button type="button" (click)="increment()" [disabled]="isDisabled">Go</button>`,
template: `
<button md-button type="button" (click)="increment()" [disabled]="isDisabled" [color]="buttonColor">
Go
</button>
<a href="http://www.google.com" md-button [disabled]="isDisabled" [color]="buttonColor">Link</a>
`,
directives: [MdButton, MdAnchor]
})
class TestApp {
clickCount: number = 0;
@@ -75,3 +145,5 @@ class TestApp {
this.clickCount++;
}
}
+49 -45
View File
@@ -1,29 +1,36 @@
import {Component, View, ViewEncapsulation, OnChanges, SimpleChange} from 'angular2/core';
import {Component, View, ViewEncapsulation, Input, Attribute, HostBinding, HostListener} from 'angular2/core';
// TODO(jelbourn): Ink ripples.
// TODO(jelbourn): Make the `isMouseDown` stuff done with one global listener.
// TODO(kara): Convert attribute selectors to classes when attr maps become available
@Component({
selector: '[md-button]:not(a), [md-raised-button]:not(a), [md-fab]:not(a)',
selector: '[md-button]:not(a), [md-raised-button]:not(a), [md-fab]:not(a), [md-mini-fab]:not(a)',
inputs: ['color'],
host: {
'(mousedown)': 'onMousedown()',
'(focus)': 'onFocus()',
'(blur)': 'onBlur()',
'[class.md-button-focus]': 'isKeyboardFocused',
'[class]': 'setClassList()',
'(mousedown)': 'setMousedown()',
'(focus)': 'setKeyboardFocus()',
'(blur)': 'removeKeyboardFocus()'
},
templateUrl: './components/button/button.html',
styleUrls: ['./components/button/button.css'],
encapsulation: ViewEncapsulation.None,
encapsulation: ViewEncapsulation.None
})
export class MdButton {
/** Whether a mousedown has occured on this element in the last 100ms. */
isMouseDown: boolean = false;
color: string;
/** Whether the button has focus from the keyboard (not the mouse). Used for class binding. */
isKeyboardFocused: boolean = false;
onMousedown() {
/** Whether a mousedown has occurred on this element in the last 100ms. */
isMouseDown: boolean = false;
setClassList() {return `md-${this.color}`;}
setMousedown() {
// We only *show* the focus style when focus has come to the button via the keyboard.
// The Material Design spec is silent on this topic, and without doing this, the
// button continues to look :active after clicking.
@@ -32,61 +39,58 @@ export class MdButton {
setTimeout(() => { this.isMouseDown = false; }, 100);
}
onFocus() {
setKeyboardFocus($event: any) {
this.isKeyboardFocused = !this.isMouseDown;
}
onBlur() {
removeKeyboardFocus() {
this.isKeyboardFocused = false;
}
}
@Component({
selector: 'a[md-button], a[md-raised-button], a[md-fab]',
inputs: ['disabled'],
selector: 'a[md-button], a[md-raised-button], a[md-fab], a[md-mini-fab]',
inputs: ['color'],
host: {
'(click)': 'onClick($event)',
'(mousedown)': 'onMousedown()',
'(focus)': 'onFocus()',
'(blur)': 'onBlur()',
'[tabIndex]': 'tabIndex',
'[class.md-button-focus]': 'isKeyboardFocused',
'[attr.aria-disabled]': 'isAriaDisabled',
'[class]': 'setClassList()',
'(mousedown)': 'setMousedown()',
'(focus)': 'setKeyboardFocus()',
'(blur)': 'removeKeyboardFocus()'
},
templateUrl: './components/button/button.html',
styleUrls: ['./components/button/button.css'],
encapsulation: ViewEncapsulation.None
})
export class MdAnchor extends MdButton implements OnChanges {
tabIndex: number;
disabled_: boolean;
export class MdAnchor extends MdButton {
disabled_: boolean = null;
get disabled(): boolean {
return this.disabled_;
}
set disabled(value) {
// The presence of *any* disabled value makes the component disabled, *except* for false.
this.disabled_ = value != null && this.disabled !== false;
}
onClick(event: Event) {
// A disabled anchor shouldn't navigate anywhere.
if (this.disabled) {
event.preventDefault();
}
}
/** Invoked when a change is detected. */
ngOnChanges(changes: {[propName: string]: SimpleChange}) {
// A disabled anchor should not be in the tab flow.
this.tabIndex = this.disabled ? -1 : 0;
@HostBinding('tabIndex')
get tabIndex():number {
return this.disabled ? -1 : 0;
}
@HostBinding('attr.aria-disabled')
/** Gets the aria-disabled value for the component, which must be a string for Dart. */
get isAriaDisabled(): string {
return this.disabled ? 'true' : 'false';
}
}
@HostBinding('attr.disabled')
@Input('disabled')
get disabled() { return this.disabled_; }
set disabled(value: boolean) {
// The presence of *any* disabled value makes the component disabled, *except* for false.
this.disabled_ = (value != null && value != false) ? true : null;
}
@HostListener('click', ['$event'])
haltDisabledEvents(event: Event) {
// A disabled button shouldn't apply any actions
if (this.disabled) {
event.preventDefault();
event.stopImmediatePropagation();
}
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ md-card {
padding: $md-card-default-padding;
border-radius: $md-card-border-radius;
box-shadow: $md-shadow-bottom-z-1;
font-family: $font-family;
font-family: $md-font-family;
background: md-color($md-background, card);
}
+1 -1
View File
@@ -8,6 +8,6 @@ $md-is-dark-theme: false;
$md-primary: md-palette($md-indigo, 500, 100, 700, $md-contrast-palettes);
$md-accent: md-palette($md-red, A200, A100, A400, $md-contrast-palettes);
$md-warn: md-palette($md-red, 500, 300, 800, $md-contrast-palettes);
$md-warn: md-palette($md-orange, 500, 300, 800, $md-contrast-palettes);
$md-foreground: if($md-is-dark-theme, $md-dark-theme-foreground, $md-light-theme-foreground);
$md-background: if($md-is-dark-theme, $md-dark-theme-background, $md-light-theme-background);
+24 -20
View File
@@ -334,6 +334,7 @@ $md-light-theme-background: (
background: map_get($md-grey, 50),
card: white,
dialog: white,
disabled-button: rgba(black, 0.12)
);
// Background palette for dark themes.
@@ -343,34 +344,37 @@ $md-dark-theme-background: (
background: #303030,
card: map_get($md-grey, 800),
dialog: map_get($md-grey, 800),
disabled-button: rgba(white, 0.12)
);
// Foreground palette for light themes.
$md-light-theme-foreground: (
base: black,
divider: rgba(black, 0.12),
dividers: rgba(black, 0.12),
disabled: rgba(black, 0.38),
disabled-text: rgba(black, 0.38),
hint-text: rgba(black, 0.38),
secondary-text: rgba(black, 0.54),
icon: rgba(black, 0.54),
icons: rgba(black, 0.54),
text: rgba(black, 0.87)
base: black,
divider: rgba(black, 0.12),
dividers: rgba(black, 0.12),
disabled: rgba(black, 0.38),
disabled-button: rgba(black, 0.38),
disabled-text: rgba(black, 0.38),
hint-text: rgba(black, 0.38),
secondary-text: rgba(black, 0.54),
icon: rgba(black, 0.54),
icons: rgba(black, 0.54),
text: rgba(black, 0.87)
);
// Foreground palette for dark themes.
$md-dark-theme-foreground: (
base: white,
divider: rgba(white, 0.12),
dividers: rgba(white, 0.12),
disabled: rgba(white, 0.30),
disabled-text: rgba(white, 0.30),
hint-text: rgba(white, 0.30),
secondary-text: rgba(white, 0.70),
icon: white,
icons: white,
text: white
base: white,
divider: rgba(white, 0.12),
dividers: rgba(white, 0.12),
disabled: rgba(white, 0.30),
disabled-button: rgba(white,0.30),
disabled-text: rgba(white, 0.30),
hint-text: rgba(white, 0.30),
secondary-text: rgba(white, 0.70),
icon: white,
icons: white,
text: white
);
+1 -1
View File
@@ -3,7 +3,7 @@
// Typography
$md-body-font-size-base: rem(1.400) !default;
$font-family: Roboto, 'Helvetica Neue', sans-serif !default;
$md-font-family: Roboto, 'Helvetica Neue', sans-serif !default;
// Media queries
$md-xsmall: "max-width: 600px";
+60 -3
View File
@@ -1,3 +1,60 @@
<button md-button>HELLO</button>
<button md-raised-button class="md-primary">HELLO</button>
<button md-fab class="md-accent">HI</button>
<section>
<button md-button>flat</button>
<button md-raised-button>raised</button>
<button md-fab>
<i class="material-icons md-24">check circle</i>
</button>
<button md-mini-fab>
<i class="material-icons md-24">check circle</i>
</button>
</section>
<section>
<a href="http://www.google.com" md-button color="primary">link</a>
<a href="http://www.google.com" md-raised-button>link</a>
<a href="http://www.google.com" md-fab>
<i class="material-icons md-24">check circle</i>
</a>
<a href="http://www.google.com" md-mini-fab>
<i class="material-icons md-24">check circle</i>
</a>
</section>
<section>
<button md-button color="primary">primary</button>
<button md-button color="accent">accent</button>
<button md-button color="warn">warn</button>
</section>
<section>
<button md-raised-button color="primary">primary</button>
<button md-raised-button color="accent">accent</button>
<button md-raised-button color="warn">warn</button>
</section>
<section>
<button md-fab color="primary">
<i class="material-icons md-24">home</i>
</button>
<button md-fab color="accent">
<i class="material-icons md-24">favorite</i>
</button>
<button md-fab color="warn">
<i class="material-icons md-24">feedback</i>
</button>
</section>
<section>
<div>
<span>isDisabled: {{isDisabled}}</span>
<button md-raised-button (click)="isDisabled=!isDisabled">Disable buttons</button>
</div>
<button md-button [disabled]="isDisabled">off</button>
<button md-button color="primary" [disabled]="isDisabled">off</button>
<a href="http://www.google.com" md-button color="accent" [disabled]="isDisabled">off</a>
<button md-raised-button color="primary" [disabled]="isDisabled">off</button>
<button md-mini-fab [disabled]="isDisabled">
<i class="material-icons md-24">check circle</i>
</button>
</section>
+19
View File
@@ -0,0 +1,19 @@
// Helps fonts on OSX looks more consistent with other systems
// Isn't currently in button styles due to performance concerns
* {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
button, a, {
margin: 8px;
text-transform: uppercase;
}
section {
background-color: #f7f7f7;
margin: 8px;
}
+6 -3
View File
@@ -1,10 +1,13 @@
import {Component} from 'angular2/core';
import {MdButton} from '../../components/button/button';
import {MdButton, MdAnchor} from '../../components/button/button';
@Component({
selector: 'button-demo',
templateUrl: 'demo-app/button/button-demo.html',
styleUrls: ['demo-app/button/button-demo.css'],
directives: [MdButton]
directives: [MdButton, MdAnchor]
})
export class ButtonDemo {}
export class ButtonDemo {
isDisabled: boolean = false;
}
+2
View File
@@ -7,6 +7,8 @@
<script src="vendor/angular2/bundles/angular2-polyfills.js"></script>
{{content-for 'head'}}
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons"
rel="stylesheet">
</head>
<body>
<demo-app>Loading...</demo-app>