diff --git a/src/material/sort/sort-container.ts b/src/material/sort/sort-container.ts new file mode 100644 index 000000000000..37ce9dbf506f --- /dev/null +++ b/src/material/sort/sort-container.ts @@ -0,0 +1,35 @@ +/** + * @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 {InjectionToken} from '@angular/core'; +import {Subject} from 'rxjs'; +import {MatSortable} from './sortable'; + +/** Injection token for the MatSortContainer. */ +export const MAT_SORT_CONTAINER = new InjectionToken>('MatSortContainer'); + +/** Container that is responsible for the state management of a set of registered Sortables. */ +export interface MatSortContainer { + /** + * Stream that emits when the state has changed for any Sortable in the set of + * Sortables, e.g. the active Sortable has changed. + */ + stateChanges: Subject; + + /** Registers a sortable to the set of managed Sortables. */ + register(sortable: MatSortable): void; + + /** Deregisters a sortable to the set of managed Sortables. */ + deregister(sortable: MatSortable): void; + + /** Performs the sort action for this sortable with relation to this sort container. */ + sort(sortable: MatSortable): void; + + /** Provides the current state of the sortable. */ + getSortableState(sortable: MatSortable): T; +} diff --git a/src/material/sort/sort-header.ts b/src/material/sort/sort-header.ts index 1fc2e46f7916..216b3e67a95e 100644 --- a/src/material/sort/sort-header.ts +++ b/src/material/sort/sort-header.ts @@ -11,26 +11,27 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, + Inject, Input, OnDestroy, OnInit, Optional, - ViewEncapsulation, - Inject, + ViewEncapsulation } from '@angular/core'; import {CanDisable, CanDisableCtor, mixinDisabled} from '@angular/material/core'; import {merge, Subscription} from 'rxjs'; -import {MatSort, MatSortable} from './sort'; import {matSortAnimations} from './sort-animations'; +import {MAT_SORT_CONTAINER, MatSortContainer} from './sort-container'; import {SortDirection} from './sort-direction'; import {getSortHeaderNotContainedWithinSortError} from './sort-errors'; import {MatSortHeaderIntl} from './sort-header-intl'; +import {MatSortable, SortableState} from './sortable'; // Boilerplate for applying mixins to the sort header. /** @docs-private */ class MatSortHeaderBase {} -const _MatSortHeaderMixinBase: CanDisableCtor & typeof MatSortHeaderBase = +const _MatSortHeaderMixinBase: CanDisableCtor&typeof MatSortHeaderBase = mixinDisabled(MatSortHeaderBase); /** @@ -41,7 +42,7 @@ const _MatSortHeaderMixinBase: CanDisableCtor & typeof MatSortHeaderBase = * * @docs-private */ -export type ArrowViewState = SortDirection | 'hint' | 'active'; +export type ArrowViewState = SortDirection|'hint'|'active'; /** * States describing the arrow's animated position (animating fromState to toState). @@ -93,8 +94,8 @@ interface MatSortHeaderColumnDef { matSortAnimations.allowChildren, ] }) -export class MatSortHeader extends _MatSortHeaderMixinBase - implements CanDisable, MatSortable, OnDestroy, OnInit { +export class MatSortHeader extends _MatSortHeaderMixinBase implements CanDisable, MatSortable, + OnDestroy, OnInit { private _rerenderSubscription: Subscription; /** @@ -125,34 +126,38 @@ export class MatSortHeader extends _MatSortHeaderMixinBase @Input('mat-sort-header') id: string; /** Sets the position of the arrow that displays when sorted. */ - @Input() arrowPosition: 'before' | 'after' = 'after'; + @Input() arrowPosition: 'before'|'after' = 'after'; /** Overrides the sort start value of the containing MatSort for this MatSortable. */ - @Input() start: 'asc' | 'desc'; + @Input() start: 'asc'|'desc'; /** Overrides the disable clear value of the containing MatSort for this MatSortable. */ @Input() - get disableClear(): boolean { return this._disableClear; } - set disableClear(v) { this._disableClear = coerceBooleanProperty(v); } + get disableClear(): boolean { + return this._disableClear; + } + set disableClear(v) { + this._disableClear = coerceBooleanProperty(v); + } private _disableClear: boolean; - constructor(public _intl: MatSortHeaderIntl, - changeDetectorRef: ChangeDetectorRef, - @Optional() public _sort: MatSort, - @Inject('MAT_SORT_HEADER_COLUMN_DEF') @Optional() - public _columnDef: MatSortHeaderColumnDef) { + constructor( + public _intl: MatSortHeaderIntl, changeDetectorRef: ChangeDetectorRef, + @Inject('MAT_SORT_HEADER_COLUMN_DEF') @Optional() public _columnDef: MatSortHeaderColumnDef, + @Inject(MAT_SORT_CONTAINER) @Optional() protected sortContainer: + MatSortContainer) { // Note that we use a string token for the `_columnDef`, because the value is provided both by // `material/table` and `cdk/table` and we can't have the CDK depending on Material, // and we want to avoid having the sort header depending on the CDK table because // of this single reference. super(); - if (!_sort) { + if (!this.sortContainer) { throw getSortHeaderNotContainedWithinSortError(); } - this._rerenderSubscription = merge(_sort.sortChange, _sort._stateChanges, _intl.changes) - .subscribe(() => { + this._rerenderSubscription = + merge(this.sortContainer.stateChanges, _intl.changes).subscribe(() => { if (this._isSorted()) { this._updateArrowDirection(); } @@ -177,11 +182,11 @@ export class MatSortHeader extends _MatSortHeaderMixinBase this._setAnimationTransitionState( {toState: this._isSorted() ? 'active' : this._arrowDirection}); - this._sort.register(this); + this.sortContainer.register(this); } ngOnDestroy() { - this._sort.deregister(this); + this.sortContainer.deregister(this); this._rerenderSubscription.unsubscribe(); } @@ -191,7 +196,9 @@ export class MatSortHeader extends _MatSortHeaderMixinBase */ _setIndicatorHintVisible(visible: boolean) { // No-op if the sort header is disabled - should not make the hint visible. - if (this._isDisabled() && visible) { return; } + if (this._isDisabled() && visible) { + return; + } this._showIndicatorHint = visible; @@ -222,9 +229,11 @@ export class MatSortHeader extends _MatSortHeaderMixinBase /** Triggers the sort on this sort header and removes the indicator hint. */ _handleClick() { - if (this._isDisabled()) { return; } + if (this._isDisabled()) { + return; + } - this._sort.sort(this); + this.sortContainer.sort(this); // Do not show the animation if the header was already shown in the right position. if (this._viewState.toState === 'hint' || this._viewState.toState === 'active') { @@ -243,8 +252,7 @@ export class MatSortHeader extends _MatSortHeaderMixinBase /** Whether this MatSortHeader is currently sorted in either ascending or descending order. */ _isSorted() { - return this._sort.active == this.id && - (this._sort.direction === 'asc' || this._sort.direction === 'desc'); + return this.sortContainer.getSortableState(this).isSorted; } /** Returns the animation state for the arrow direction (indicator and pointers). */ @@ -269,13 +277,12 @@ export class MatSortHeader extends _MatSortHeaderMixinBase * only be changed once the arrow displays again (hint or activation). */ _updateArrowDirection() { - this._arrowDirection = this._isSorted() ? - this._sort.direction : - (this.start || this._sort.start); + const state = this.sortContainer.getSortableState(this); + this._arrowDirection = state.isSorted ? state.direction : state.nextDirection; } _isDisabled() { - return this._sort.disabled || this.disabled; + return this.sortContainer.getSortableState(this).isDisabled; } /** @@ -285,9 +292,11 @@ export class MatSortHeader extends _MatSortHeaderMixinBase * ensures this is true. */ _getAriaSortAttribute() { - if (!this._isSorted()) { return null; } - - return this._sort.direction == 'asc' ? 'ascending' : 'descending'; + if (!this._isSorted()) { + return null; + } + const direction = this.sortContainer.getSortableState(this).direction; + return direction == 'asc' ? 'ascending' : 'descending'; } /** Whether the arrow inside the sort header should be rendered. */ diff --git a/src/material/sort/sort-module.ts b/src/material/sort/sort-module.ts index dc8151dda729..78765945817d 100644 --- a/src/material/sort/sort-module.ts +++ b/src/material/sort/sort-module.ts @@ -6,12 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ +import {CommonModule} from '@angular/common'; import {NgModule} from '@angular/core'; -import {MatSortHeader} from './sort-header'; import {MatSort} from './sort'; +import {MatSortHeader} from './sort-header'; import {MAT_SORT_HEADER_INTL_PROVIDER} from './sort-header-intl'; -import {CommonModule} from '@angular/common'; - @NgModule({ imports: [CommonModule], @@ -19,4 +18,5 @@ import {CommonModule} from '@angular/common'; declarations: [MatSort, MatSortHeader], providers: [MAT_SORT_HEADER_INTL_PROVIDER] }) -export class MatSortModule {} +export class MatSortModule { +} diff --git a/src/material/sort/sort.spec.ts b/src/material/sort/sort.spec.ts index 970dfa4ae347..46058696b331 100644 --- a/src/material/sort/sort.spec.ts +++ b/src/material/sort/sort.spec.ts @@ -354,6 +354,65 @@ describe('MatSort', () => { expect(header._showIndicatorHint).toBeFalsy(); }); + describe('sortable state', () => { + it('should represent the right active and directions when cycling sort', () => { + const matSort = fixture.componentInstance.matSort; + const sortable = matSort.sortables.get('defaultA')!; + + expect(matSort.getSortableState(sortable)).toEqual({ + active: '', + isSorted: false, + isDisabled: false, + direction: '', + nextDirection: 'asc' + }); + + // Should expect it to be sorted with 'asc' direction + matSort.sort(sortable); + expect(matSort.getSortableState(sortable)).toEqual({ + active: sortable.id, + isSorted: true, + isDisabled: false, + direction: 'asc', + nextDirection: 'desc' + }); + + // Should expect it to be sorted with 'desc' direction + matSort.sort(sortable); + expect(matSort.getSortableState(sortable)).toEqual({ + active: sortable.id, + isSorted: true, + isDisabled: false, + direction: 'desc', + nextDirection: '' + }); + + // Should expect it to no longer be sorted + matSort.sort(sortable); + expect(matSort.getSortableState(sortable)).toEqual({ + active: sortable.id, + isSorted: false, + isDisabled: false, + direction: '', + nextDirection: 'asc' + }); + }); + + it('should be disabled if sort container or sortable is disabled', () => { + const matSort = fixture.componentInstance.matSort; + const sortable = matSort.sortables.get('defaultA')!; + + sortable.disabled = true; + expect(matSort.getSortableState(sortable).isDisabled).toBe(true); + + sortable.disabled = false; + expect(matSort.getSortableState(sortable).isDisabled).toBe(false); + + matSort.disabled = true; + expect(matSort.getSortableState(sortable).isDisabled).toBe(true); + }); + }); + it('should apply the aria-sort label to the header when sorted', () => { const sortHeaderElement = fixture.nativeElement.querySelector('#defaultA'); expect(sortHeaderElement.getAttribute('aria-sort')).toBe(null); diff --git a/src/material/sort/sort.ts b/src/material/sort/sort.ts index 4cf9a59076a4..3ec6a996ef43 100644 --- a/src/material/sort/sort.ts +++ b/src/material/sort/sort.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ -import {coerceBooleanProperty} from '@angular/cdk/coercion'; import { Directive, EventEmitter, @@ -15,7 +14,7 @@ import { OnChanges, OnDestroy, OnInit, - Output, + Output } from '@angular/core'; import { CanDisable, @@ -23,26 +22,24 @@ import { HasInitialized, HasInitializedCtor, mixinDisabled, - mixinInitialized, + mixinInitialized } from '@angular/material/core'; import {Subject} from 'rxjs'; + import {SortDirection} from './sort-direction'; import { getSortDuplicateSortableIdError, getSortHeaderMissingIdError, - getSortInvalidDirectionError, + getSortInvalidDirectionError } from './sort-errors'; +import {MatSortable, SortableState} from './sortable'; +import {MAT_SORT_CONTAINER, MatSortContainer} from './sort-container'; +import {coerceBooleanProperty} from '@angular/cdk/coercion'; -/** Interface for a directive that holds sorting state consumed by `MatSortHeader`. */ -export interface MatSortable { - /** The id of the column being sorted. */ - id: string; - - /** Starting sort direction. */ - start: 'asc' | 'desc'; - - /** Whether to disable clearing the sorting state. */ - disableClear: boolean; +/** State provided to a `MatSortable`, includes the current `MatSort` active sortable. */ +export interface MatSortSortableState extends SortableState { + /** The id of the currently activated sortable. */ + active: string; } /** The current sort state. */ @@ -64,15 +61,17 @@ const _MatSortMixinBase: HasInitializedCtor & CanDisableCtor & typeof MatSortBas @Directive({ selector: '[matSort]', exportAs: 'matSort', - inputs: ['disabled: matSortDisabled'] + inputs: ['disabled: matSortDisabled'], + providers: [{provide: MAT_SORT_CONTAINER, useExisting: MatSort}] }) -export class MatSort extends _MatSortMixinBase - implements CanDisable, HasInitialized, OnChanges, OnDestroy, OnInit { +export class MatSort extends _MatSortMixinBase implements CanDisable, HasInitialized, OnChanges, + OnDestroy, OnInit, + MatSortContainer { /** Collection of all registered sortables that this directive manages. */ sortables = new Map(); /** Used to notify any child components listening to state changes. */ - readonly _stateChanges = new Subject(); + readonly stateChanges = new Subject(); /** The id of the most recently sorted MatSortable. */ @Input('matSortActive') active: string; @@ -81,11 +80,13 @@ export class MatSort extends _MatSortMixinBase * The direction to set when an MatSortable is initially sorted. * May be overriden by the MatSortable's sort start. */ - @Input('matSortStart') start: 'asc' | 'desc' = 'asc'; + @Input('matSortStart') start: 'asc'|'desc' = 'asc'; /** The sort direction of the currently active MatSortable. */ @Input('matSortDirection') - get direction(): SortDirection { return this._direction; } + get direction(): SortDirection { + return this._direction; + } set direction(direction: SortDirection) { if (isDevMode() && direction && direction !== 'asc' && direction !== 'desc') { throw getSortInvalidDirectionError(direction); @@ -99,8 +100,12 @@ export class MatSort extends _MatSortMixinBase * May be overriden by the MatSortable's disable clear input. */ @Input('matSortDisableClear') - get disableClear(): boolean { return this._disableClear; } - set disableClear(v: boolean) { this._disableClear = coerceBooleanProperty(v); } + get disableClear(): boolean { + return this._disableClear; + } + set disableClear(v: boolean) { + this._disableClear = coerceBooleanProperty(v); + } private _disableClear: boolean; /** Event emitted when the user changes either the active sort or sort direction. */ @@ -139,11 +144,14 @@ export class MatSort extends _MatSortMixinBase } this.sortChange.emit({active: this.active, direction: this.direction}); + this.stateChanges.next(); } /** Returns the next sort direction of the active sortable, checking for potential overrides. */ getNextSortDirection(sortable: MatSortable): SortDirection { - if (!sortable) { return ''; } + if (!sortable) { + return ''; + } // Get the sort direction cycle with the potential sortable overrides. const disableClear = sortable.disableClear != null ? sortable.disableClear : this.disableClear; @@ -151,29 +159,49 @@ export class MatSort extends _MatSortMixinBase // Get and return the next direction in the cycle let nextDirectionIndex = sortDirectionCycle.indexOf(this.direction) + 1; - if (nextDirectionIndex >= sortDirectionCycle.length) { nextDirectionIndex = 0; } + if (nextDirectionIndex >= sortDirectionCycle.length) { + nextDirectionIndex = 0; + } return sortDirectionCycle[nextDirectionIndex]; } + /** Provides the current state of the sortable. */ + getSortableState(sortable: MatSortable): MatSortSortableState { + const active = this.active || ''; + const isDisabled = this.disabled || sortable.disabled; + + const hasDirection = (this.direction === 'asc' || this.direction === 'desc'); + const isSorted = this.active == sortable.id && hasDirection; + + const direction: SortDirection = isSorted ? this.direction : ''; + const start = sortable.start || this.start; + const nextDirection = isSorted ? this.getNextSortDirection(sortable) : start; + + return {active, isSorted, direction, isDisabled, nextDirection}; + } + ngOnInit() { this._markInitialized(); } ngOnChanges() { - this._stateChanges.next(); + this.stateChanges.next(); } ngOnDestroy() { - this._stateChanges.complete(); + this.stateChanges.complete(); } } /** Returns the sort direction cycle to use given the provided parameters of order and clear. */ -function getSortDirectionCycle(start: 'asc' | 'desc', - disableClear: boolean): SortDirection[] { +function getSortDirectionCycle(start: 'asc'|'desc', disableClear: boolean): SortDirection[] { let sortOrder: SortDirection[] = ['asc', 'desc']; - if (start == 'desc') { sortOrder.reverse(); } - if (!disableClear) { sortOrder.push(''); } + if (start == 'desc') { + sortOrder.reverse(); + } + if (!disableClear) { + sortOrder.push(''); + } return sortOrder; } diff --git a/src/material/sort/sortable.ts b/src/material/sort/sortable.ts new file mode 100644 index 000000000000..432123564984 --- /dev/null +++ b/src/material/sort/sortable.ts @@ -0,0 +1,41 @@ +/** + * @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 {SortDirection} from './sort-direction'; + +/** + * Interface for a directive that triggers sort state changes and is contained by a `SortContainer`. + */ +export interface MatSortable { + /** The id of the sortable being sorted. */ + id: string; + + /** Starting sort direction. */ + start: 'asc'|'desc'; + + /** Whether to disable clearing the sorting state. */ + disableClear: boolean; + + /** Whether the sortable is disabled. */ + disabled: boolean; +} + +/** Interface for the current state of a sortable, which is determined by its `SortContainer`. */ +export interface SortableState { + /** Whether this sortable is currently sorted. */ + isSorted: boolean; + + /** Whether this sortable is disabled. */ + isDisabled: boolean; + + /** The direction that this sortable is oriented if sorted, otherwise an empty string. */ + direction: SortDirection; + + /** The sort direction that will be next if this sortable is triggered again. */ + nextDirection: SortDirection; +} diff --git a/tools/public_api_guard/material/sort.d.ts b/tools/public_api_guard/material/sort.d.ts index 92d49dd81acc..8307af9fdfc5 100644 --- a/tools/public_api_guard/material/sort.d.ts +++ b/tools/public_api_guard/material/sort.d.ts @@ -13,16 +13,17 @@ export declare const MAT_SORT_HEADER_INTL_PROVIDER: { export declare function MAT_SORT_HEADER_INTL_PROVIDER_FACTORY(parentIntl: MatSortHeaderIntl): MatSortHeaderIntl; -export declare class MatSort extends _MatSortMixinBase implements CanDisable, HasInitialized, OnChanges, OnDestroy, OnInit { - readonly _stateChanges: Subject; +export declare class MatSort extends _MatSortMixinBase implements CanDisable, HasInitialized, OnChanges, OnDestroy, OnInit, MatSortContainer { active: string; direction: SortDirection; disableClear: boolean; readonly sortChange: EventEmitter; sortables: Map; start: 'asc' | 'desc'; + readonly stateChanges: Subject; deregister(sortable: MatSortable): void; getNextSortDirection(sortable: MatSortable): SortDirection; + getSortableState(sortable: MatSortable): MatSortSortableState; ngOnChanges(): void; ngOnDestroy(): void; ngOnInit(): void; @@ -30,12 +31,6 @@ export declare class MatSort extends _MatSortMixinBase implements CanDisable, Ha sort(sortable: MatSortable): void; } -export interface MatSortable { - disableClear: boolean; - id: string; - start: 'asc' | 'desc'; -} - export declare const matSortAnimations: { readonly indicator: AnimationTriggerMetadata; readonly leftPointer: AnimationTriggerMetadata; @@ -51,13 +46,13 @@ export declare class MatSortHeader extends _MatSortHeaderMixinBase implements Ca _disableViewStateAnimation: boolean; _intl: MatSortHeaderIntl; _showIndicatorHint: boolean; - _sort: MatSort; _viewState: ArrowViewStateTransition; arrowPosition: 'before' | 'after'; disableClear: boolean; id: string; + protected sortContainer: MatSortContainer; start: 'asc' | 'desc'; - constructor(_intl: MatSortHeaderIntl, changeDetectorRef: ChangeDetectorRef, _sort: MatSort, _columnDef: MatSortHeaderColumnDef); + constructor(_intl: MatSortHeaderIntl, changeDetectorRef: ChangeDetectorRef, _columnDef: MatSortHeaderColumnDef, sortContainer: MatSortContainer); _getAriaSortAttribute(): "ascending" | "descending" | null; _getArrowDirectionState(): string; _getArrowViewState(): string; @@ -80,6 +75,10 @@ export declare class MatSortHeaderIntl { export declare class MatSortModule { } +export interface MatSortSortableState extends SortableState { + active: string; +} + export interface Sort { active: string; direction: SortDirection;