Skip to content

Commit

Permalink
feat(module:back-top): add nzDuration property (#5892)
Browse files Browse the repository at this point in the history
close #5887
  • Loading branch information
cipchk authored Oct 23, 2020
1 parent d235589 commit b256461
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 29 deletions.
8 changes: 5 additions & 3 deletions components/anchor/anchor.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,9 +224,11 @@ export class NzAnchorComponent implements OnDestroy, AfterViewInit, OnChanges {
const containerScrollTop = this.scrollSrv.getScroll(this.getContainer());
const elOffsetTop = getOffsetTop(el, this.getContainer());
const targetScrollTop = containerScrollTop + elOffsetTop - (this.nzOffsetTop || 0);
this.scrollSrv.scrollTo(this.getContainer(), targetScrollTop, undefined, () => {
this.animating = false;
this.handleActive(linkComp);
this.scrollSrv.scrollTo(this.getContainer(), targetScrollTop, {
callback: () => {
this.animating = false;
this.handleActive(linkComp);
}
});
this.nzClick.emit(linkComp.nzHref);
}
Expand Down
10 changes: 4 additions & 6 deletions components/anchor/anchor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,11 @@ describe('anchor', () => {

describe('[default]', () => {
it(`should scolling to target via click a link`, () => {
spyOn(srv, 'scrollTo').and.callFake(
(_containerEl: Element | Window, _targetTopValue: number = 0, _easing?: any, callback?: () => void) => {
if (callback) {
callback();
}
spyOn(srv, 'scrollTo').and.callFake((_containerEl, _targetTopValue = 0, options = {}) => {
if (options.callback) {
options.callback();
}
);
});
expect(context._scroll).not.toHaveBeenCalled();
page.to('#何时使用');
expect(context._scroll).toHaveBeenCalled();
Expand Down
4 changes: 3 additions & 1 deletion components/back-top/back-top.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const NZ_CONFIG_MODULE_NAME: NzConfigKey = 'backTop';
export class NzBackTopComponent implements OnInit, OnDestroy, OnChanges {
readonly _nzModuleName: NzConfigKey = NZ_CONFIG_MODULE_NAME;
static ngAcceptInputType_nzVisibilityHeight: NumberInput;
static ngAcceptInputType_nzDuration: NumberInput;

private scrollListenerDestroy$ = new Subject();
private target: HTMLElement | null = null;
Expand All @@ -64,6 +65,7 @@ export class NzBackTopComponent implements OnInit, OnDestroy, OnChanges {
@Input() nzTemplate?: TemplateRef<void>;
@Input() @WithConfig() @InputNumber() nzVisibilityHeight: number = 400;
@Input() nzTarget?: string | HTMLElement;
@Input() @InputNumber() nzDuration: number = 450;
@Output() readonly nzClick: EventEmitter<boolean> = new EventEmitter();

constructor(
Expand All @@ -80,7 +82,7 @@ export class NzBackTopComponent implements OnInit, OnDestroy, OnChanges {
}

clickBackTop(): void {
this.scrollSrv.scrollTo(this.getTarget(), 0);
this.scrollSrv.scrollTo(this.getTarget(), 0, { duration: this.nzDuration });
this.nzClick.emit(true);
}

Expand Down
1 change: 1 addition & 0 deletions components/back-top/doc/index.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,6 @@ import { NzBackTopModule } from 'ng-zorro-antd/back-top';
| `[nzTemplate]` | custom content | `TemplateRef<void>` | - |
| `[nzVisibilityHeight]` | the `nz-back-top` button will not show until the scroll height reaches this value | `number` | `400` ||
| `[nzTarget]` | specifies the scrollable area dom node | `string \| Element` | `window` |
| `[nzDuration]` | Time to return to top (ms) | `number` | `450` |
| `(nzClick)` | a callback function, which can be executed when you click the button | `EventEmitter<boolean>` | - |

1 change: 1 addition & 0 deletions components/back-top/doc/index.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,5 @@ import { NzBackTopModule } from 'ng-zorro-antd/back-top';
| `[nzTemplate]` | 自定义内容,见示例 | `TemplateRef<void>` | - |
| `[nzVisibilityHeight]` | 滚动高度达到此参数值才出现 `nz-back-top` | `number` | `400` ||
| `[nzTarget]` | 设置需要监听其滚动事件的元素,值为一个返回对应 DOM 元素的函数 | `string \| Element` | `window` |
| `[nzDuration]` | 回到顶部所需时间(ms) | `number` | `450` |
| `(nzClick)` | 点击按钮的回调函数 | `EventEmitter<boolean>` | - |
41 changes: 41 additions & 0 deletions components/core/services/scroll.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,45 @@ describe('NzScrollService', () => {
scrollService.setScrollTop(el, 0);
});
});

describe('#getOffset', () => {
it(`should be working`, () => {
const ret = scrollService.getOffset({
ownerDocument: {
documentElement: {
clientTop: 1,
clientLeft: 1
}
},
getClientRects: () => [0],
getBoundingClientRect: () => ({ top: 10, left: 10, width: 100, height: 100 })
} as any);
expect(ret.left).toBe(9);
expect(ret.top).toBe(9);
});

it(`should be return 0 when is no getClientRects`, () => {
const ret = scrollService.getOffset({ getClientRects: () => [] } as any);
expect(ret.left).toBe(0);
expect(ret.top).toBe(0);
});

it(`should be return element top when element is not size`, () => {
const ret = scrollService.getOffset({ getClientRects: () => [0], getBoundingClientRect: () => ({ top: 1, left: 1 }) } as any);
expect(ret.left).toBe(1);
expect(ret.top).toBe(1);
});
});

describe('#getScroll', () => {
it('should be return scrollTop when target is window', () => {
const mockWin: any = { pageYOffset: 10 };
mockWin.window = mockWin;
expect(scrollService.getScroll(mockWin)).toBe(10);
});
it('should be return scrollTop when target is html element', () => {
const mockEl: any = { scrollTop: 10 };
expect(scrollService.getScroll(mockEl)).toBe(10);
});
});
});
61 changes: 42 additions & 19 deletions components/core/services/scroll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ function easeInOutCubic(t: number, b: number, c: number, d: number): number {
}
}

export interface NzScrollToOptions {
/** Scroll container, default as window */
easing?: EasyingFn;
/** Scroll end callback */
callback?(): void;
/** Animation duration, default as 450 */
duration?: number;
}

@Injectable({
providedIn: 'root'
})
Expand Down Expand Up @@ -65,41 +74,55 @@ export class NzScrollService {

/** Get the position of the scoll bar of `el`. */
// TODO: remove '| Window' as the fallback already happens here
getScroll(el?: Element | Window, top: boolean = true): number {
const target = el ? el : window;
const prop = top ? 'pageYOffset' : 'pageXOffset';
getScroll(target?: Element | HTMLElement | Window | Document | null, top: boolean = true): number {
if (typeof window === 'undefined') {
return 0;
}
const method = top ? 'scrollTop' : 'scrollLeft';
const isWindow = target === window;
// @ts-ignore
let ret = isWindow ? target[prop] : target[method];
if (isWindow && typeof ret !== 'number') {
ret = this.doc.documentElement![method];
let result = 0;
if (this.isWindow(target)) {
result = (target as Window)[top ? 'pageYOffset' : 'pageXOffset'];
} else if (target instanceof Document) {
result = target.documentElement[method];
} else if (target) {
result = (target as HTMLElement)[method];
}
return ret;
if (target && !this.isWindow(target) && typeof result !== 'number') {
result = ((target as HTMLElement).ownerDocument || (target as Document)).documentElement[method];
}
return result;
}

isWindow(obj: NzSafeAny): boolean {
return obj !== null && obj !== undefined && obj === obj.window;
}

/**
* Scroll `el` to some position with animation.
*
* @param containerEl container, `window` by default
* @param targetTopValue Scroll to `top`, 0 by default
* @param easing Transition curve, `easeInOutCubic` by default
* @param callback callback invoked when transition is done
* @param y Scroll to `top`, 0 by default
*/
scrollTo(containerEl: Element | Window, targetTopValue: number = 0, easing?: EasyingFn, callback?: () => void): void {
scrollTo(containerEl?: Element | HTMLElement | Window | Document | null, y: number = 0, options: NzScrollToOptions = {}): void {
const target = containerEl ? containerEl : window;
const scrollTop = this.getScroll(target);
const startTime = Date.now();
const { easing, callback, duration = 450 } = options;
const frameFunc = () => {
const timestamp = Date.now();
const time = timestamp - startTime;
this.setScrollTop(target, (easing || easeInOutCubic)(time, scrollTop, targetTopValue, 450));
if (time < 450) {
reqAnimFrame(frameFunc);
const nextScrollTop = (easing || easeInOutCubic)(time > duration ? duration : time, scrollTop, y, duration);
if (this.isWindow(target)) {
(target as Window).scrollTo(window.pageXOffset, nextScrollTop);
} else if (target instanceof HTMLDocument || target.constructor.name === 'HTMLDocument') {
(target as HTMLDocument).documentElement.scrollTop = nextScrollTop;
} else {
if (callback) {
callback();
}
(target as HTMLElement).scrollTop = nextScrollTop;
}
if (time < duration) {
reqAnimFrame(frameFunc);
} else if (typeof callback === 'function') {
callback();
}
};
reqAnimFrame(frameFunc);
Expand Down

0 comments on commit b256461

Please sign in to comment.