Skip to content

Commit

Permalink
Invoke oncomplete if date has passed (#194)
Browse files Browse the repository at this point in the history
* Invoke onComplete if date is already passed on initialization

* Add second argument to specify whether the time was passed before or after initialization

* Add more specific test assertions

* Invoke onComplete from start method instead of constructor

* Update on complete handling and docs

* Fix prettier

* Fix lint

* Fix test

Co-authored-by: Martin V <ndresx@gmail.com>
  • Loading branch information
bendikjohansen and ndresx committed Oct 22, 2022
1 parent 5154d41 commit 8a31928
Show file tree
Hide file tree
Showing 3 changed files with 54 additions and 13 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ If the current date and time (determined via a reference to `Date.now`) is not t
`onTick` is a callback and triggered every time a new period is started, based on what the [`intervalDelay`](#intervaldelay)'s value is. It only gets triggered when the countdown's [`controlled`](#controlled) prop is set to `false`, meaning that the countdown has full control over its interval. It receives a [time delta object](#calctimedelta) as the first argument.

### `onComplete`
`onComplete` is a callback and triggered whenever the countdown ends. In contrast to [`onTick`](#ontick), the [`onComplete`](#oncomplete) callback also gets triggered in case [`controlled`](#controlled) is set to `true`. It receives a [time delta object](#calctimedelta) as the first argument.
`onComplete` is a callback and triggered whenever the countdown ends. In contrast to [`onTick`](#ontick), the [`onComplete`](#oncomplete) callback also gets triggered in case [`controlled`](#controlled) is set to `true`. It receives a [time delta object](#calctimedelta) as the first argument and a `boolean` as a second argument, indicating whether the countdown transitioned into the completed state (`false`) or completed on start (`true`).

## API Reference

Expand Down
43 changes: 41 additions & 2 deletions src/Countdown.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,50 @@ describe('<Countdown />', () => {
seconds: 1,
});

expect(onComplete.mock.calls.length).toBe(1);
expect(onComplete).toBeCalledWith({ ...defaultStats, completed: true });
expect(onComplete).toBeCalledTimes(1);
expect(onComplete).toBeCalledWith({ ...defaultStats, completed: true }, false);
expect(wrapper.state().timeDelta.completed).toBe(true);
});

it('should trigger various callbacks before onComplete is called', () => {
const calls: string[] = [];

const onStart = jest.fn().mockImplementation(() => calls.push('onStart'));
const onTick = jest.fn().mockImplementation(() => calls.push('onTick'));
const onComplete = jest.fn().mockImplementation(() => calls.push('onComplete'));
wrapper = mount(
<Countdown date={countdownDate} onStart={onStart} onTick={onTick} onComplete={onComplete} />
);

expect(calls).toEqual(['onStart']);

for (let i = 1; i <= 10; i += 1) {
now.mockReturnValue(countdownDate - countdownMs + i * 1000);
jest.runTimersToTime(1000);
}

expect(calls).toEqual(['onStart', ...Array(9).fill('onTick'), 'onComplete']);
});

it('should trigger onComplete callback on start if date is in the past when countdown starts', () => {
const calls: string[] = [];

const onStart = jest.fn().mockImplementation(() => calls.push('onStart'));
const onTick = jest.fn().mockImplementation(() => calls.push('onTick'));
const onComplete = jest.fn().mockImplementation(() => calls.push('onComplete'));

countdownDate = Date.now() - 10000;
wrapper = mount(
<Countdown date={countdownDate} onStart={onStart} onTick={onTick} onComplete={onComplete} />
);

expect(onStart).toHaveBeenCalledTimes(1);
expect(onTick).not.toHaveBeenCalled();
expect(onComplete).toHaveBeenCalledTimes(1);
expect(onComplete).toBeCalledWith({ ...defaultStats, completed: true }, true);
expect(calls).toEqual(['onStart', 'onComplete']);
});

it('should run through the controlled component by updating the date prop', () => {
const root = document.createElement('div');
wrapper = mount(<Countdown date={1000} controlled />, { attachTo: root });
Expand Down
22 changes: 12 additions & 10 deletions src/Countdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ export interface CountdownProps
readonly onPause?: CountdownTimeDeltaFn;
readonly onStop?: CountdownTimeDeltaFn;
readonly onTick?: CountdownTimeDeltaFn;
readonly onComplete?: CountdownTimeDeltaFn | LegacyCountdownProps['onComplete'];
readonly onComplete?: (
timeDelta: CountdownTimeDelta,
completedOnStart: boolean
) => void | LegacyCountdownProps['onComplete'];
}

export interface CountdownRenderProps extends CountdownTimeDelta {
Expand Down Expand Up @@ -246,27 +249,26 @@ export default class Countdown extends React.Component<CountdownProps, Countdown
return this.state.status === status;
}

handleOnComplete = (timeDelta: CountdownTimeDelta): void => {
if (this.props.onComplete) this.props.onComplete(timeDelta);
};

setTimeDeltaState(
timeDelta: CountdownTimeDelta,
status?: CountdownStatus,
callback?: (timeDelta: CountdownTimeDelta) => void
): void {
if (!this.mounted) return;

let completedCallback: this['handleOnComplete'] | undefined;
const completing = timeDelta.completed && !this.state.timeDelta.completed;
const completedOnStart = timeDelta.completed && status === CountdownStatus.STARTED;

if (!this.state.timeDelta.completed && timeDelta.completed) {
if (!this.props.overtime) this.clearTimer();
completedCallback = this.handleOnComplete;
if (completing && !this.props.overtime) {
this.clearTimer();
}

const onDone = () => {
if (callback) callback(this.state.timeDelta);
if (completedCallback) completedCallback(this.state.timeDelta);

if (this.props.onComplete && (completing || completedOnStart)) {
this.props.onComplete(timeDelta, completedOnStart);
}
};

return this.setState(prevState => {
Expand Down

0 comments on commit 8a31928

Please sign in to comment.