Skip to content

refactor(sort): state should be determined and provided by container #15171

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions src/material/sort/sort-container.ts
Original file line number Diff line number Diff line change
@@ -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<any>>('MatSortContainer');

/** Container that is responsible for the state management of a set of registered Sortables. */
export interface MatSortContainer<T> {
/**
* 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<void>;

/** 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;
}
75 changes: 42 additions & 33 deletions src/material/sort/sort-header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

/**
Expand All @@ -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).
Expand Down Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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<SortableState>) {
// 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();
}
Expand All @@ -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();
}

Expand All @@ -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;

Expand Down Expand Up @@ -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') {
Expand All @@ -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). */
Expand All @@ -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;
}

/**
Expand All @@ -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. */
Expand Down
8 changes: 4 additions & 4 deletions src/material/sort/sort-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@
* 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],
exports: [MatSort, MatSortHeader],
declarations: [MatSort, MatSortHeader],
providers: [MAT_SORT_HEADER_INTL_PROVIDER]
})
export class MatSortModule {}
export class MatSortModule {
}
59 changes: 59 additions & 0 deletions src/material/sort/sort.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading