feat(badge): add badge component (#7483)

This commit is contained in:
Austin
2018-02-13 09:55:21 -06:00
committed by Jeremy Elbourn
parent 2f891e64f6
commit c98d217dfe
21 changed files with 707 additions and 0 deletions
+2
View File
@@ -32,6 +32,7 @@
/src/lib/tabs/** @andrewseguin
/src/lib/toolbar/** @devversion
/src/lib/tooltip/** @andrewseguin
/src/lib/badge/** @amcdnl
# Angular Material core
/src/lib/core/* @jelbourn
@@ -129,6 +130,7 @@
/src/demo-app/toolbar/** @devversion
/src/demo-app/tooltip/** @andrewseguin
/src/demo-app/typography/** @crisbeto
/src/demo-app/badge/** @amcdnl
# E2E app
/e2e/* @jelbourn
+90
View File
@@ -0,0 +1,90 @@
<div class="badge-demo">
<div class="badge-examples">
<h3>Text</h3>
<span [matBadge]="badgeContent" matBadgeOverlap="false" *ngIf="visible">
Hello
</span>
<span [matBadge]="11111" matBadgeOverlap="false">
Hello
</span>
<span matBadge="22" matBadgeOverlap="false" matBadgePosition="below after" matBadgeColor="accent">
Hello
</span>
<span matBadge="22" matBadgeOverlap="false" matBadgePosition="above before" matBadgeColor="warn">
Hello
</span>
<span matBadge="⚡️" matBadgeOverlap="false" matBadgePosition="below before">
Hello
</span>
<span [matBadge]="badgeContent" matBadgeDescription="I've got {{badgeContent}} problems">
Aria
</span>
<span [matBadge]="badgeContent" matBadgeHidden="true">
Hidden
</span>
<input type="text" [(ngModel)]="badgeContent" />
<button (click)="visible = !visible">Toggle</button>
</div>
<div class="badge-examples">
<h3>Buttons</h3>
<button mat-raised-button [matBadge]="badgeContent">
<mat-icon color="primary">home</mat-icon>
</button>
<button mat-raised-button matBadge="22" matBadgePosition="below after" color="primary" matBadgeColor="accent">
<mat-icon color="accent">home</mat-icon>
</button>
<button mat-raised-button matBadge="22" matBadgePosition="above before">
<mat-icon color="primary">home</mat-icon>
</button>
<button mat-raised-button matBadge="22" matBadgePosition="below before">
<mat-icon color="primary">home</mat-icon>
</button>
<button mat-raised-button>
<mat-icon color="primary" matBadge="22" color="accent">home</mat-icon>
</button>
</div>
<div class="badge-examples">
<h3>Icons</h3>
<mat-icon [matBadge]="badgeContent">
home
</mat-icon>
<mat-icon color="primary" matBadge="22" matBadgePosition="below after" matBadgeColor="accent">
home
</mat-icon>
<mat-icon color="primary" matBadge="22" matBadgePosition="above before" matBadgeColor="warn">
home
</mat-icon>
<mat-icon color="primary" matBadge="22" matBadgePosition="below before">
home
</mat-icon>
</div>
<div class="badge-examples">
<h3>Size</h3>
<mat-icon [matBadge]="badgeContent" matBadgeSize="small">
home
</mat-icon>
<mat-icon [matBadge]="badgeContent" matBadgeSize="large">
home
</mat-icon>
</div>
</div>
+3
View File
@@ -0,0 +1,3 @@
.badge-examples {
margin-bottom: 25px;
}
+20
View File
@@ -0,0 +1,20 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Component} from '@angular/core';
@Component({
moduleId: module.id,
selector: 'badge-demo',
templateUrl: 'badge-demo.html',
styleUrls: ['badge-demo.css'],
})
export class BadgeDemo {
visible = true;
badgeContent = '0';
}
+1
View File
@@ -51,6 +51,7 @@ export class DemoApp {
dark = false;
navItems = [
{name: 'Autocomplete', route: '/autocomplete'},
{name: 'Badge', route: '/badge'},
{name: 'Bottom sheet', route: '/bottom-sheet'},
{name: 'Button Toggle', route: '/button-toggle'},
{name: 'Button', route: '/button'},
+2
View File
@@ -61,6 +61,7 @@ import {TypographyDemo} from '../typography/typography-demo';
import {DemoApp, Home} from './demo-app';
import {DEMO_APP_ROUTES} from './routes';
import {TableDemoModule} from '../table/table-demo-module';
import {BadgeDemo} from 'badge/badge-demo';
@NgModule({
imports: [
@@ -78,6 +79,7 @@ import {TableDemoModule} from '../table/table-demo-module';
BaselineDemo,
ButtonDemo,
ButtonToggleDemo,
BadgeDemo,
CardDemo,
CheckboxDemo,
ChipsDemo,
+2
View File
@@ -51,11 +51,13 @@ import {TypographyDemo} from '../typography/typography-demo';
import {DemoApp, Home} from './demo-app';
import {TableDemoPage} from '../table/table-demo-page';
import {TABLE_DEMO_ROUTES} from '../table/routes';
import {BadgeDemo} from 'badge/badge-demo';
export const DEMO_APP_ROUTES: Routes = [
{path: '', component: DemoApp, children: [
{path: '', component: Home},
{path: 'autocomplete', component: AutocompleteDemo},
{path: 'badge', component: BadgeDemo},
{path: 'bottom-sheet', component: BottomSheetDemo},
{path: 'baseline', component: BaselineDemo},
{path: 'button', component: ButtonDemo},
+2
View File
@@ -17,6 +17,7 @@ import {CdkTableModule} from '@angular/cdk/table';
import {NgModule} from '@angular/core';
import {
MatAutocompleteModule,
MatBadgeModule,
MatBottomSheetModule,
MatButtonModule,
MatButtonToggleModule,
@@ -58,6 +59,7 @@ import {
@NgModule({
exports: [
MatAutocompleteModule,
MatBadgeModule,
MatBottomSheetModule,
MatButtonModule,
MatButtonToggleModule,
+1
View File
@@ -91,6 +91,7 @@ System.config({
'@angular/material/tabs': 'dist/packages/material/tabs/index.js',
'@angular/material/toolbar': 'dist/packages/material/toolbar/index.js',
'@angular/material/tooltip': 'dist/packages/material/tooltip/index.js',
'@angular/material/badge': 'dist/packages/material/badge/index.js',
},
packages: {
// Thirdparty barrels.
+167
View File
@@ -0,0 +1,167 @@
// This contains all of the styles for the badge
// rather than just the color/theme because of
// no style sheet support for directives.
@import '../core/theming/palette';
@import '../core/theming/theming';
@import '../core/typography/typography-utils';
$mat-badge-font-size: 12px;
$mat-badge-font-weight: 600;
$mat-badge-default-size: 22px !default;
$mat-badge-small-size: $mat-badge-default-size - 6;
$mat-badge-large-size: $mat-badge-default-size + 6;
// Mixin for building offset given different sizes
@mixin _mat-badge-size($size) {
.mat-badge-content {
width: $size;
height: $size;
line-height: $size;
}
&.mat-badge-above {
.mat-badge-content {
top: -$size / 2;
}
}
&.mat-badge-below {
.mat-badge-content {
bottom: -$size / 2;
}
}
&.mat-badge-before {
margin-left: $size;
&[dir='rtl'] {
margin-left: 0;
margin-right: $size;
}
.mat-badge-content {
left: -$size;
}
}
&.mat-badge-after {
margin-right: $size;
&[dir='rtl'] {
margin-right: 0;
margin-left: $size;
}
.mat-badge-content {
right: -$size;
}
}
&.mat-badge-overlap {
&.mat-badge-before {
margin-left: $size / 2;
&[dir='rtl'] {
margin-left: 0;
margin-right: $size / 2;
}
.mat-badge-content {
left: -$size / 2;
}
}
&.mat-badge-after {
margin-right: $size / 2;
&[dir='rtl'] {
margin-right: 0;
margin-left: $size;
}
.mat-badge-content {
right: -$size / 2;
}
}
}
}
@mixin mat-badge-theme($theme) {
$accent: map-get($theme, accent);
$warn: map-get($theme, warn);
$primary: map-get($theme, primary);
.mat-badge-content {
color: mat-color($primary, default-contrast);
background: mat-color($primary);
}
&.mat-badge-accent {
.mat-badge-content {
background: mat-color($accent);
color: mat-color($accent, default-contrast);
}
}
&.mat-badge-warn {
.mat-badge-content {
color: mat-color($warn, default-contrast);
background: mat-color($warn);
}
}
}
@mixin mat-badge-typography($config) {
.mat-badge-content {
font-weight: $mat-badge-font-weight;
font-size: $mat-badge-font-size;
font-family: mat-font-family($config);
}
.mat-badge-small .mat-badge-content {
font-size: $mat-badge-font-size / 2;
}
.mat-badge-large .mat-badge-content {
font-size: $mat-badge-font-size * 2;
}
}
.mat-badge {
position: relative;
}
.mat-badge-hidden {
.mat-badge-content {
display: none;
}
}
.mat-badge-content {
position: absolute;
text-align: center;
display: inline-block;
border-radius: 50%;
transition: all 0.2 ease-in-out;
transform: scale(0.6);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
pointer-events: none;
}
// The active class is added after the element is added
// so it can animate scale to default
.mat-badge-content.mat-badge-active {
transform: scale(1);
}
.mat-badge-small {
@include _mat-badge-size($mat-badge-small-size);
}
.mat-badge-medium {
@include _mat-badge-size($mat-badge-default-size);
}
.mat-badge-large {
@include _mat-badge-size($mat-badge-large-size);
}
+27
View File
@@ -0,0 +1,27 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {NgModule} from '@angular/core';
import {MatCommonModule} from '@angular/material/core';
import {A11yModule} from '@angular/cdk/a11y';
import {MatBadge} from './badge';
@NgModule({
imports: [
MatCommonModule,
A11yModule,
],
exports: [
MatBadge,
],
declarations: [
MatBadge,
],
})
export class MatBadgeModule {}
+53
View File
@@ -0,0 +1,53 @@
Badges are small status descriptors for UI elements. A badge consists of a small circle,
typically containing a number or other short set of characters, that appears in proximity to another object.
### Badge position
By default, the badge will be placed `above after`. The direction can be changed by defining
the attribute `matBadgePosition` follow by `above|below` and `before|after`.
```html
<mat-icon matBadge="22" matBadgePosition="above after">home</mat-icon>
```
The overlap of the badge in relation to its inner contents can also be defined
using the `matBadgeOverlap` tag. Typically, you want the badge to overlap an icon and not
a text phrase. By default it will overlap.
```html
<h1 matBadge="11" matBadgeOverlap="false">
Email
</h1>
```
### Badge sizing
The badge has 3 sizes: `small`, `medium` and `large`. By default, the badge is set to `medium`.
You can change the size by adding `matBadgeSize` to the host element.
```html
<h1 matBadge="11" matBadgeSize="large">
Email
</h1>
```
### Badge visibility
The badge visibility can be toggled programmatically by defining `matBadgeHidden`.
```html
<h1 matBadge="11" [matBadgeHidden]="!visible">
Email
</h1>
```
### Theming
Badges can be colored in terms of the current theme using the `matBadgeColor` property to set the
background color to `primary`, `accent`, or `warn`.
```html
<mat-icon matBadge="22" matBadgeColor="accent">
home
</mat-icon>
```
### Accessibility
Badges should be given a meaningful description via `matBadgeDescription`. This description will be
applied, via `aria-describedby` to the element decorated by `matBadge`.
+132
View File
@@ -0,0 +1,132 @@
import {ComponentFixture, TestBed, async} from '@angular/core/testing';
import {Component, DebugElement} from '@angular/core';
import {By} from '@angular/platform-browser';
import {MatBadge, MatBadgeModule} from './index';
import {ThemePalette} from '@angular/material/core';
describe('MatBadge', () => {
let fixture: ComponentFixture<any>;
let testComponent: BadgeWithTextContent;
let badgeNativeElement;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [MatBadgeModule],
declarations: [BadgeWithTextContent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(BadgeWithTextContent);
testComponent = fixture.debugElement.componentInstance;
fixture.detectChanges();
});
describe('MatBadge Text', () => {
let badgeDebugElement: DebugElement;
beforeEach(() => {
badgeDebugElement = fixture.debugElement.query(By.directive(MatBadge));
badgeNativeElement = badgeDebugElement.nativeElement;
fixture.detectChanges();
});
it('should update the badge based on attribute', () => {
let badgeContentDebugElement = badgeNativeElement.querySelector('.mat-badge-content');
expect(badgeContentDebugElement.textContent).toContain('1');
testComponent.badgeContent = '22';
fixture.detectChanges();
badgeContentDebugElement = badgeNativeElement.querySelector('.mat-badge-content');
expect(badgeContentDebugElement.textContent).toContain('22');
});
it('should apply class based on color attribute', () => {
testComponent.badgeColor = 'primary';
fixture.detectChanges();
expect(badgeNativeElement.classList.contains('mat-badge-primary')).toBe(true);
testComponent.badgeColor = 'accent';
fixture.detectChanges();
expect(badgeNativeElement.classList.contains('mat-badge-accent')).toBe(true);
testComponent.badgeColor = 'warn';
fixture.detectChanges();
expect(badgeNativeElement.classList.contains('mat-badge-warn')).toBe(true);
testComponent.badgeColor = undefined;
fixture.detectChanges();
expect(badgeNativeElement.classList).not.toContain('mat-badge-accent');
});
it('should update the badge position on direction change', () => {
expect(badgeNativeElement.classList.contains('mat-badge-above')).toBe(true);
expect(badgeNativeElement.classList.contains('mat-badge-after')).toBe(true);
testComponent.badgeDirection = 'below before';
fixture.detectChanges();
expect(badgeNativeElement.classList.contains('mat-badge-below')).toBe(true);
expect(badgeNativeElement.classList.contains('mat-badge-before')).toBe(true);
});
it('should change visibility to hidden', () => {
expect(badgeNativeElement.classList.contains('mat-badge-hidden')).toBe(false);
testComponent.badgeHidden = true;
fixture.detectChanges();
expect(badgeNativeElement.classList.contains('mat-badge-hidden')).toBe(true);
});
it('should change badge sizes', () => {
expect(badgeNativeElement.classList.contains('mat-badge-medium')).toBe(true);
testComponent.badgeSize = 'small';
fixture.detectChanges();
expect(badgeNativeElement.classList.contains('mat-badge-small')).toBe(true);
testComponent.badgeSize = 'large';
fixture.detectChanges();
expect(badgeNativeElement.classList.contains('mat-badge-large')).toBe(true);
});
it('should change badge overlap', () => {
expect(badgeNativeElement.classList.contains('mat-badge-overlap')).toBe(false);
testComponent.badgeOverlap = true;
fixture.detectChanges();
expect(badgeNativeElement.classList.contains('mat-badge-overlap')).toBe(true);
});
});
});
/** Test component that contains a MatBadge. */
@Component({
selector: 'test-app',
template: `
<span [matBadge]="badgeContent"
[matBadgeColor]="badgeColor"
[matBadgePosition]="badgeDirection"
[matBadgeHidden]="badgeHidden"
[matBadgeSize]="badgeSize"
[matBadgeOverlap]="badgeOverlap">
home
</span>
`
})
class BadgeWithTextContent {
badgeColor: ThemePalette;
badgeContent = '1';
badgeDirection = 'above after';
badgeHidden = false;
badgeSize = 'medium';
badgeOverlap = false;
}
+167
View File
@@ -0,0 +1,167 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Directive, Input, ElementRef, Inject, Optional, NgZone} from '@angular/core';
import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {ThemePalette} from '@angular/material/core';
import {AriaDescriber} from '@angular/cdk/a11y';
import {DOCUMENT} from '@angular/common';
let nextId = 0;
export type MatBadgePosition = 'above after' | 'above before' | 'below before' | 'below after';
export type MatBadgeSize = 'small' | 'medium' | 'large';
/** Directive to display a text badge. */
@Directive({
selector: '[matBadge]',
host: {
'class': 'mat-badge',
'[class.mat-badge-overlap]': '_overlap',
'[class.mat-badge-above]': 'isAbove()',
'[class.mat-badge-below]': '!isAbove()',
'[class.mat-badge-before]': '!isAfter()',
'[class.mat-badge-after]': 'isAfter()',
'[class.mat-badge-small]': 'size === "small"',
'[class.mat-badge-medium]': 'size === "medium"',
'[class.mat-badge-large]': 'size === "large"',
'[class.mat-badge-hidden]': 'hidden',
},
})
export class MatBadge {
/** The color of the badge. Can be `primary`, `accent`, or `warn`. */
@Input('matBadgeColor')
get color(): ThemePalette { return this._color; }
set color(value: ThemePalette) {
this._setColor(value);
this._color = value;
}
private _color: ThemePalette = 'primary';
/** Whether the badge should overlap its contents or not */
@Input('matBadgeOverlap')
get overlap(): boolean { return this._overlap; }
set overlap(val: boolean) {
this._overlap = coerceBooleanProperty(val);
}
private _overlap: boolean = true;
/**
* Position the badge should reside.
* Accepts any combination of 'above'|'below' and 'before'|'after'
*/
@Input('matBadgePosition') position: MatBadgePosition = 'above after';
/** The content for the badge */
@Input('matBadge')
get content(): string { return this._content; }
set content(val: string) {
this._content = val;
this._updateTextContent();
}
private _content: string;
/** Message used to describe the decorated element via aria-describedby */
@Input('matBadgeDescription')
get description(): string { return this._description; }
set description(val: string) {
if (this._description) {
this._updateHostAriaDescription(val, this._description);
}
this._description = val;
}
private _description: string;
/** Size of the badge. Can be 'small', 'medium', or 'large'. */
@Input('matBadgeSize') size: MatBadgeSize = 'medium';
/** Whether the badge is hidden. */
@Input('matBadgeHidden')
get hidden(): boolean { return this._hidden; }
set hidden(val: boolean) {
this._hidden = coerceBooleanProperty(val);
}
private _hidden: boolean;
/** Unique id for the badge */
_id: number = nextId++;
private _badgeElement: HTMLElement;
constructor(
@Optional() @Inject(DOCUMENT) private _document: any,
private _ngZone: NgZone,
private _elementRef: ElementRef,
private _ariaDescriber: AriaDescriber) {}
/** Whether the badge is above the host or not */
isAbove(): boolean {
return this.position.indexOf('below') === -1;
}
/** Whether the badge is after the host or not */
isAfter(): boolean {
return this.position.indexOf('before') === -1;
}
/** Injects a span element into the DOM with the content. */
private _updateTextContent(): HTMLSpanElement {
if (!this._badgeElement) {
this._badgeElement = this._createBadgeElement();
} else {
this._badgeElement.textContent = this.content;
}
return this._badgeElement;
}
/** Creates the badge element */
private _createBadgeElement(): HTMLElement {
const badgeElement = this._document.createElement('span');
badgeElement.setAttribute('id', `mat-badge-content-${this._id}`);
badgeElement.classList.add('mat-badge-content');
badgeElement.textContent = this.content;
if (this.description) {
badgeElement.setAttribute('aria-label', this.description);
}
this._elementRef.nativeElement.appendChild(badgeElement);
// animate in after insertion
this._ngZone.runOutsideAngular(() => requestAnimationFrame(() => {
// ensure content available
if (badgeElement) {
badgeElement.classList.add('mat-badge-active');
}
}));
return badgeElement;
}
/** Sets the aria-label property on the element */
private _updateHostAriaDescription(val: string, prevVal: string): void {
// ensure content available before setting label
const content = this._updateTextContent();
this._ariaDescriber.removeDescription(content, prevVal);
this._ariaDescriber.describe(content, val);
}
/** Adds css theme class given the color to the component host */
private _setColor(colorPalette: ThemePalette) {
if (colorPalette !== this._color) {
if (this._color) {
this._elementRef.nativeElement.classList.remove(`mat-badge-${this._color}`);
}
if (colorPalette) {
this._elementRef.nativeElement.classList.add(`mat-badge-${colorPalette}`);
}
}
}
}
+9
View File
@@ -0,0 +1,9 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export * from './public-api';
+10
View File
@@ -0,0 +1,10 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export * from './badge-module';
export * from './badge';
+14
View File
@@ -0,0 +1,14 @@
{
"extends": "../tsconfig-build",
"files": [
"public-api.ts",
"../typings.d.ts"
],
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"strictMetadataEmit": true,
"flatModuleOutFile": "index.js",
"flatModuleId": "@angular/material/badge",
"skipTemplateCodegen": true
}
}
+2
View File
@@ -1,6 +1,7 @@
// Import all the theming functionality.
@import '../core';
@import '../../autocomplete/autocomplete-theme';
@import '../../badge/badge-theme';
@import '../../bottom-sheet/bottom-sheet-theme';
@import '../../button/button-theme';
@import '../../button-toggle/button-toggle-theme';
@@ -37,6 +38,7 @@
@mixin angular-material-theme($theme) {
@include mat-core-theme($theme);
@include mat-autocomplete-theme($theme);
@include mat-badge-theme($theme);
@include mat-bottom-sheet-theme($theme);
@include mat-button-theme($theme);
@include mat-button-toggle-theme($theme);
@@ -39,6 +39,7 @@
$config: mat-typography-config();
}
@include mat-badge-typography($config);
@include mat-base-typography($config);
@include mat-autocomplete-typography($config);
@include mat-bottom-sheet-typography($config);
+1
View File
@@ -8,6 +8,7 @@
export * from './version';
export * from '@angular/material/autocomplete';
export * from '@angular/material/badge';
export * from '@angular/material/bottom-sheet';
export * from '@angular/material/button';
export * from '@angular/material/button-toggle';
+1
View File
@@ -71,6 +71,7 @@ System.config({
'@angular/cdk/testing': 'dist/packages/cdk/testing/index.js',
'@angular/material/autocomplete': 'dist/packages/material/autocomplete/index.js',
'@angular/material/badge': 'dist/packages/material/badge/index.js',
'@angular/material/bottom-sheet': 'dist/packages/material/bottom-sheet/index.js',
'@angular/material/button': 'dist/packages/material/button/index.js',
'@angular/material/button-toggle': 'dist/packages/material/button-toggle/index.js',