diff --git a/src/lib/button/button.ts b/src/lib/button/button.ts index e62a36ede756..2a393d4715a2 100644 --- a/src/lib/button/button.ts +++ b/src/lib/button/button.ts @@ -24,6 +24,7 @@ import { import {coerceBooleanProperty, FocusOriginMonitor, Platform} from '../core'; import {mixinDisabled, CanDisable} from '../core/common-behaviors/disabled'; import {CanColor, mixinColor} from '../core/common-behaviors/color'; +import {CanDisableRipple, mixinDisableRipple} from '../core/common-behaviors/disable-ripple'; // TODO(kara): Convert attribute selectors to classes when attr maps become available @@ -104,7 +105,7 @@ export class MdMiniFab { export class MdButtonBase { constructor(public _renderer: Renderer2, public _elementRef: ElementRef) {} } -export const _MdButtonMixinBase = mixinColor(mixinDisabled(MdButtonBase)); +export const _MdButtonMixinBase = mixinColor(mixinDisabled(mixinDisableRipple(MdButtonBase))); /** @@ -121,25 +122,19 @@ export const _MdButtonMixinBase = mixinColor(mixinDisabled(MdButtonBase)); }, templateUrl: 'button.html', styleUrls: ['button.css'], - inputs: ['disabled', 'color'], + inputs: ['disabled', 'disableRipple', 'color'], encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, }) -export class MdButton extends _MdButtonMixinBase implements OnDestroy, CanDisable, CanColor { +export class MdButton extends _MdButtonMixinBase + implements OnDestroy, CanDisable, CanColor, CanDisableRipple { + /** Whether the button is round. */ _isRoundButton: boolean = this._hasAttributeWithPrefix('fab', 'mini-fab'); /** Whether the button is icon button. */ _isIconButton: boolean = this._hasAttributeWithPrefix('icon-button'); - /** Whether the ripple effect on click should be disabled. */ - private _disableRipple: boolean = false; - - /** Whether the ripple effect for this button is disabled. */ - @Input() - get disableRipple() { return this._disableRipple; } - set disableRipple(v) { this._disableRipple = coerceBooleanProperty(v); } - constructor(renderer: Renderer2, elementRef: ElementRef, private _platform: Platform, @@ -197,7 +192,7 @@ export class MdButton extends _MdButtonMixinBase implements OnDestroy, CanDisabl '[attr.aria-disabled]': 'disabled.toString()', '(click)': '_haltDisabledEvents($event)', }, - inputs: ['disabled', 'color'], + inputs: ['disabled', 'disableRipple', 'color'], templateUrl: 'button.html', styleUrls: ['button.css'], encapsulation: ViewEncapsulation.None diff --git a/src/lib/checkbox/checkbox.ts b/src/lib/checkbox/checkbox.ts index 83d7df83cc27..da3edc9b31c6 100644 --- a/src/lib/checkbox/checkbox.ts +++ b/src/lib/checkbox/checkbox.ts @@ -26,6 +26,7 @@ import {coerceBooleanProperty} from '@angular/cdk'; import {FocusOrigin, FocusOriginMonitor, MdRipple, RippleRef} from '../core'; import {mixinDisabled, CanDisable} from '../core/common-behaviors/disabled'; import {CanColor, mixinColor} from '../core/common-behaviors/color'; +import {CanDisableRipple, mixinDisableRipple} from '../core/common-behaviors/disable-ripple'; /** Monotonically increasing integer used to auto-generate unique ids for checkbox components. */ @@ -70,7 +71,8 @@ export class MdCheckboxChange { export class MdCheckboxBase { constructor(public _renderer: Renderer2, public _elementRef: ElementRef) {} } -export const _MdCheckboxMixinBase = mixinColor(mixinDisabled(MdCheckboxBase), 'accent'); +export const _MdCheckboxMixinBase = + mixinColor(mixinDisableRipple(mixinDisabled(MdCheckboxBase)), 'accent'); /** @@ -94,12 +96,13 @@ export const _MdCheckboxMixinBase = mixinColor(mixinDisabled(MdCheckboxBase), 'a '[class.mat-checkbox-label-before]': 'labelPosition == "before"', }, providers: [MD_CHECKBOX_CONTROL_VALUE_ACCESSOR], - inputs: ['disabled', 'color'], + inputs: ['disabled', 'disableRipple', 'color'], encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush }) -export class MdCheckbox extends _MdCheckboxMixinBase - implements ControlValueAccessor, AfterViewInit, OnDestroy, CanColor, CanDisable { +export class MdCheckbox extends _MdCheckboxMixinBase implements ControlValueAccessor, AfterViewInit, + OnDestroy, CanColor, CanDisable, CanDisableRipple { + /** * Attached to the aria-label attribute of the host element. In most cases, arial-labelledby will * take precedence so this may be omitted. @@ -114,14 +117,6 @@ export class MdCheckbox extends _MdCheckboxMixinBase /** A unique id for the checkbox. If one is not supplied, it is auto-generated. */ @Input() id: string = `md-checkbox-${++nextId}`; - /** Whether the ripple effect on click should be disabled. */ - private _disableRipple: boolean; - - /** Whether the ripple effect for this checkbox is disabled. */ - @Input() - get disableRipple(): boolean { return this._disableRipple; } - set disableRipple(value) { this._disableRipple = coerceBooleanProperty(value); } - /** ID of the native input element inside `` */ get inputId(): string { return `input-${this.id}`; diff --git a/src/lib/core/common-behaviors/disable-ripple.spec.ts b/src/lib/core/common-behaviors/disable-ripple.spec.ts new file mode 100644 index 000000000000..7e2be6d4550d --- /dev/null +++ b/src/lib/core/common-behaviors/disable-ripple.spec.ts @@ -0,0 +1,35 @@ +import {mixinDisableRipple} from './disable-ripple'; + +describe('mixinDisableRipple', () => { + + it('should augment an existing class with a disableRipple property', () => { + const classWithMixin = mixinDisableRipple(TestClass); + const instance = new classWithMixin(); + + expect(instance.disableRipple) + .toBe(false, 'Expected the mixed-into class to have a disable-ripple property'); + + instance.disableRipple = true; + + expect(instance.disableRipple) + .toBe(true, 'Expected the mixed-into class to have an updated disable-ripple property'); + }); + + it('should coerce values being passed to the disableRipple property', () => { + const classWithMixin = mixinDisableRipple(TestClass); + const instance = new classWithMixin(); + + expect(instance.disableRipple) + .toBe(false, 'Expected disableRipple to be set to false initially'); + + // Setting string values to the disableRipple property should be prevented by TypeScript's + // type checking, but developers can still set string values from their template bindings. + (instance as any).disableRipple = ''; + + expect(instance.disableRipple) + .toBe(true, 'Expected disableRipple to be set to true if an empty string is set as value'); + }); + +}); + +class TestClass {} diff --git a/src/lib/core/common-behaviors/disable-ripple.ts b/src/lib/core/common-behaviors/disable-ripple.ts new file mode 100644 index 000000000000..1bb631c6d454 --- /dev/null +++ b/src/lib/core/common-behaviors/disable-ripple.ts @@ -0,0 +1,29 @@ +/** + * @license + * Copyright Google Inc. 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 {coerceBooleanProperty} from '@angular/cdk'; +import {Constructor} from './constructor'; + +/** @docs-private */ +export interface CanDisableRipple { + disableRipple: boolean; +} + +/** Mixin to augment a directive with a `disableRipple` property. */ +export function mixinDisableRipple>(base: T) + : Constructor & T { + return class extends base { + private _disableRipple: boolean = false; + + /** Whether the ripple effect is disabled or not. */ + get disableRipple() { return this._disableRipple; } + set disableRipple(value: any) { this._disableRipple = coerceBooleanProperty(value); } + + constructor(...args: any[]) { super(...args); } + }; +} diff --git a/src/lib/list/list.ts b/src/lib/list/list.ts index 0306814458b4..3db6080e575e 100644 --- a/src/lib/list/list.ts +++ b/src/lib/list/list.ts @@ -21,6 +21,17 @@ import { ChangeDetectionStrategy, } from '@angular/core'; import {coerceBooleanProperty, MdLine, MdLineSetter} from '../core'; +import {CanDisableRipple, mixinDisableRipple} from '../core/common-behaviors/disable-ripple'; + +// Boilerplate for applying mixins to MdList. +/** @docs-private */ +export class MdListBase {} +export const _MdListMixinBase = mixinDisableRipple(MdListBase); + +// Boilerplate for applying mixins to MdListItem. +/** @docs-private */ +export class MdListItemBase {} +export const _MdListItemMixinBase = mixinDisableRipple(MdListItemBase); @Directive({ selector: 'md-divider, mat-divider', @@ -37,20 +48,11 @@ export class MdListDivider {} host: {'role': 'list'}, template: '', styleUrls: ['list.css'], + inputs: ['disableRipple'], encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, }) -export class MdList { - private _disableRipple: boolean = false; - - /** - * Whether the ripple effect should be disabled on the list-items or not. - * This flag only has an effect for `md-nav-list` components. - */ - @Input() - get disableRipple() { return this._disableRipple; } - set disableRipple(value: boolean) { this._disableRipple = coerceBooleanProperty(value); } -} +export class MdList extends _MdListMixinBase implements CanDisableRipple {} /** * Directive whose purpose is to add the mat- CSS styling to this selector. @@ -121,23 +123,15 @@ export class MdListSubheaderCssMatStyler {} '(focus)': '_handleFocus()', '(blur)': '_handleBlur()', }, + inputs: ['disableRipple'], templateUrl: 'list-item.html', encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, }) -export class MdListItem implements AfterContentInit { +export class MdListItem extends _MdListItemMixinBase implements AfterContentInit, CanDisableRipple { private _lineSetter: MdLineSetter; - private _disableRipple: boolean = false; private _isNavList: boolean = false; - /** - * Whether the ripple effect on click should be disabled. This applies only to list items that are - * part of a nav list. The value of `disableRipple` on the `md-nav-list` overrides this flag. - */ - @Input() - get disableRipple() { return this._disableRipple; } - set disableRipple(value: boolean) { this._disableRipple = coerceBooleanProperty(value); } - @ContentChildren(MdLine) _lines: QueryList; @ContentChild(MdListAvatarCssMatStyler) @@ -153,6 +147,7 @@ export class MdListItem implements AfterContentInit { private _element: ElementRef, @Optional() private _list: MdList, @Optional() navList: MdNavListCssMatStyler) { + super(); this._isNavList = !!navList; } diff --git a/src/lib/radio/radio.ts b/src/lib/radio/radio.ts index febc665e636a..1d617dc18b87 100644 --- a/src/lib/radio/radio.ts +++ b/src/lib/radio/radio.ts @@ -38,6 +38,7 @@ import { import {coerceBooleanProperty} from '@angular/cdk'; import {mixinDisabled, CanDisable} from '../core/common-behaviors/disabled'; import {CanColor, mixinColor} from '../core/common-behaviors/color'; +import {CanDisableRipple, mixinDisableRipple} from '../core/common-behaviors/disable-ripple'; /** @@ -303,7 +304,7 @@ export class MdRadioButtonBase { } // As per Material design specifications the selection control radio should use the accent color // palette by default. https://material.io/guidelines/components/selection-controls.html -export const _MdRadioButtonMixinBase = mixinColor(MdRadioButtonBase, 'accent'); +export const _MdRadioButtonMixinBase = mixinColor(mixinDisableRipple(MdRadioButtonBase), 'accent'); /** * A radio-button. May be inside of @@ -313,7 +314,7 @@ export const _MdRadioButtonMixinBase = mixinColor(MdRadioButtonBase, 'accent'); selector: 'md-radio-button, mat-radio-button', templateUrl: 'radio.html', styleUrls: ['radio.css'], - inputs: ['color'], + inputs: ['color', 'disableRipple'], encapsulation: ViewEncapsulation.None, host: { 'class': 'mat-radio-button', @@ -324,7 +325,7 @@ export const _MdRadioButtonMixinBase = mixinColor(MdRadioButtonBase, 'accent'); changeDetection: ChangeDetectionStrategy.OnPush, }) export class MdRadioButton extends _MdRadioButtonMixinBase - implements OnInit, AfterViewInit, OnDestroy, CanColor { + implements OnInit, AfterViewInit, OnDestroy, CanColor, CanDisableRipple { /** The unique ID for the radio button. */ @Input() id: string = `md-radio-${_uniqueIdCounter++}`; @@ -338,11 +339,6 @@ export class MdRadioButton extends _MdRadioButtonMixinBase /** The 'aria-labelledby' attribute takes precedence as the element's text alternative. */ @Input('aria-labelledby') ariaLabelledby: string; - /** Whether the ripple effect for this radio button is disabled. */ - @Input() - get disableRipple(): boolean { return this._disableRipple; } - set disableRipple(value) { this._disableRipple = coerceBooleanProperty(value); } - /** Whether this radio button is checked. */ @Input() get checked(): boolean { @@ -450,9 +446,6 @@ export class MdRadioButton extends _MdRadioButtonMixinBase /** Value assigned to this radio.*/ private _value: any = null; - /** Whether the ripple effect on click should be disabled. */ - private _disableRipple: boolean; - /** The child ripple instance. */ @ViewChild(MdRipple) _ripple: MdRipple; diff --git a/src/lib/slide-toggle/slide-toggle.ts b/src/lib/slide-toggle/slide-toggle.ts index fb6646080609..f864aa1c1599 100644 --- a/src/lib/slide-toggle/slide-toggle.ts +++ b/src/lib/slide-toggle/slide-toggle.ts @@ -34,6 +34,7 @@ import { import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; import {mixinDisabled, CanDisable} from '../core/common-behaviors/disabled'; import {CanColor, mixinColor} from '../core/common-behaviors/color'; +import {CanDisableRipple, mixinDisableRipple} from '../core/common-behaviors/disable-ripple'; export const MD_SLIDE_TOGGLE_VALUE_ACCESSOR: any = { @@ -58,7 +59,8 @@ let nextId = 0; export class MdSlideToggleBase { constructor(public _renderer: Renderer2, public _elementRef: ElementRef) {} } -export const _MdSlideToggleMixinBase = mixinColor(mixinDisabled(MdSlideToggleBase), 'accent'); +export const _MdSlideToggleMixinBase = + mixinColor(mixinDisableRipple(mixinDisabled(MdSlideToggleBase)), 'accent'); /** Represents a slidable "switch" toggle that can be moved between on and off. */ @Component({ @@ -73,12 +75,13 @@ export const _MdSlideToggleMixinBase = mixinColor(mixinDisabled(MdSlideToggleBas templateUrl: 'slide-toggle.html', styleUrls: ['slide-toggle.css'], providers: [MD_SLIDE_TOGGLE_VALUE_ACCESSOR], - inputs: ['disabled', 'color'], + inputs: ['disabled', 'disableRipple', 'color'], encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush }) -export class MdSlideToggle extends _MdSlideToggleMixinBase - implements OnDestroy, AfterContentInit, ControlValueAccessor, CanDisable, CanColor { +export class MdSlideToggle extends _MdSlideToggleMixinBase implements OnDestroy, AfterContentInit, + ControlValueAccessor, CanDisable, CanColor, CanDisableRipple { + private onChange = (_: any) => {}; private onTouched = () => {}; @@ -87,7 +90,6 @@ export class MdSlideToggle extends _MdSlideToggleMixinBase private _checked: boolean = false; private _slideRenderer: SlideToggleRenderer; private _required: boolean = false; - private _disableRipple: boolean = false; /** Reference to the focus state ripple. */ private _focusRipple: RippleRef | null; @@ -115,11 +117,6 @@ export class MdSlideToggle extends _MdSlideToggleMixinBase get required(): boolean { return this._required; } set required(value) { this._required = coerceBooleanProperty(value); } - /** Whether the ripple effect for this slide-toggle is disabled. */ - @Input() - get disableRipple(): boolean { return this._disableRipple; } - set disableRipple(value) { this._disableRipple = coerceBooleanProperty(value); } - /** An event will be dispatched each time the slide-toggle changes its value. */ @Output() change: EventEmitter = new EventEmitter(); diff --git a/src/lib/tabs/tab-group.ts b/src/lib/tabs/tab-group.ts index 1e1b6d07ceef..d520da5a47e8 100644 --- a/src/lib/tabs/tab-group.ts +++ b/src/lib/tabs/tab-group.ts @@ -21,6 +21,7 @@ import {coerceBooleanProperty} from '../core'; import {Observable} from 'rxjs/Observable'; import {MdTab} from './tab'; import {map} from '../core/rxjs/index'; +import {CanDisableRipple, mixinDisableRipple} from '../core/common-behaviors/disable-ripple'; /** Used to generate unique ID's for each tab component */ @@ -35,6 +36,11 @@ export class MdTabChangeEvent { /** Possible positions for the tab header. */ export type MdTabHeaderPosition = 'above' | 'below'; +// Boilerplate for applying mixins to MdTabGroup. +/** @docs-private */ +export class MdTabGroupBase {} +export const _MdTabGroupMixinBase = mixinDisableRipple(MdTabGroupBase); + /** * Material design tab-group component. Supports basic tab pairs (label + content) and includes * animated ink-bar, keyboard navigation, and screen reader. @@ -45,13 +51,14 @@ export type MdTabHeaderPosition = 'above' | 'below'; selector: 'md-tab-group, mat-tab-group', templateUrl: 'tab-group.html', styleUrls: ['tab-group.css'], + inputs: ['disableRipple'], host: { 'class': 'mat-tab-group', '[class.mat-tab-group-dynamic-height]': 'dynamicHeight', '[class.mat-tab-group-inverted-header]': 'headerPosition === "below"', } }) -export class MdTabGroup { +export class MdTabGroup extends _MdTabGroupMixinBase implements CanDisableRipple { @ContentChildren(MdTab) _tabs: QueryList; @ViewChild('tabBodyWrapper') _tabBodyWrapper: ElementRef; @@ -76,13 +83,6 @@ export class MdTabGroup { get _dynamicHeightDeprecated(): boolean { return this._dynamicHeight; } set _dynamicHeightDeprecated(value: boolean) { this._dynamicHeight = value; } - /** Whether ripples for the tab-group should be disabled or not. */ - @Input() - get disableRipple(): boolean { return this._disableRipple; } - set disableRipple(value) { this._disableRipple = coerceBooleanProperty(value); } - private _disableRipple: boolean = false; - - private _selectedIndex: number | null = null; /** The index of the active tab. */ @@ -108,6 +108,7 @@ export class MdTabGroup { private _groupId: number; constructor(private _renderer: Renderer2) { + super(); this._groupId = nextId++; } diff --git a/src/lib/tabs/tab-header.ts b/src/lib/tabs/tab-header.ts index f7c323c857c7..aece66fceabb 100644 --- a/src/lib/tabs/tab-header.ts +++ b/src/lib/tabs/tab-header.ts @@ -38,6 +38,7 @@ import {auditTime, startWith} from '../core/rxjs/index'; import {of as observableOf} from 'rxjs/observable/of'; import {merge} from 'rxjs/observable/merge'; import {fromEvent} from 'rxjs/observable/fromEvent'; +import {CanDisableRipple, mixinDisableRipple} from '../core/common-behaviors/disable-ripple'; /** @@ -53,6 +54,11 @@ export type ScrollDirection = 'after' | 'before'; */ const EXAGGERATED_OVERSCROLL = 60; +// Boilerplate for applying mixins to MdTabHeader. +/** @docs-private */ +export class MdTabHeaderBase {} +export const _MdTabHeaderMixinBase = mixinDisableRipple(MdTabHeaderBase); + /** * The header of the tab group which displays a list of all the tabs in the tab group. Includes * an ink bar that follows the currently selected tab. When the tabs list's width exceeds the @@ -65,6 +71,7 @@ const EXAGGERATED_OVERSCROLL = 60; selector: 'md-tab-header, mat-tab-header', templateUrl: 'tab-header.html', styleUrls: ['tab-header.css'], + inputs: ['disableRipple'], encapsulation: ViewEncapsulation.None, host: { 'class': 'mat-tab-header', @@ -72,7 +79,9 @@ const EXAGGERATED_OVERSCROLL = 60; '[class.mat-tab-header-rtl]': "_getLayoutDirection() == 'rtl'", } }) -export class MdTabHeader implements AfterContentChecked, AfterContentInit, OnDestroy { +export class MdTabHeader extends _MdTabHeaderMixinBase + implements AfterContentChecked, AfterContentInit, OnDestroy, CanDisableRipple { + @ContentChildren(MdTabLabelWrapper) _labelWrappers: QueryList; @ViewChild(MdInkBar) _inkBar: MdInkBar; @@ -121,23 +130,18 @@ export class MdTabHeader implements AfterContentChecked, AfterContentInit, OnDes this._focusIndex = value; } - /** Whether ripples for the tab-header labels should be disabled or not. */ - @Input() - get disableRipple(): boolean { return this._disableRipple; } - set disableRipple(value) { this._disableRipple = coerceBooleanProperty(value); } - private _disableRipple: boolean = false; - /** Event emitted when the option is selected. */ @Output() selectFocusedIndex = new EventEmitter(); /** Event emitted when a label is focused. */ @Output() indexFocused = new EventEmitter(); - constructor( - private _elementRef: ElementRef, - private _ngZone: NgZone, - private _renderer: Renderer2, - @Optional() private _dir: Directionality) { } + constructor(private _elementRef: ElementRef, + private _ngZone: NgZone, + private _renderer: Renderer2, + @Optional() private _dir: Directionality) { + super(); + } ngAfterContentChecked(): void { // If the number of tab labels have changed, check if scrolling should be enabled