mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
Compare commits
8 Commits
FW-5331
...
ld/datetim
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
726e4aeae7 | ||
|
|
eee2115fd2 | ||
|
|
d0057352fe | ||
|
|
c70432e693 | ||
|
|
a1690441e5 | ||
|
|
e6031fbef0 | ||
|
|
d5f0c776df | ||
|
|
00767a02e4 |
11
.github/workflows/stencil-nightly.yml
vendored
11
.github/workflows/stencil-nightly.yml
vendored
@@ -8,7 +8,12 @@ on:
|
||||
# at 6:00 UTC (6:00 am UTC)
|
||||
- cron: '00 06 * * 1-5'
|
||||
workflow_dispatch:
|
||||
# allows for manual invocations in the GitHub UI
|
||||
inputs:
|
||||
npm_release_tag:
|
||||
required: true
|
||||
type: string
|
||||
description: What version should be pulled from NPM?
|
||||
default: nightly
|
||||
|
||||
# When pushing a new commit we should
|
||||
# cancel the previous test run to not
|
||||
@@ -24,7 +29,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/workflows/actions/build-core-stencil-prerelease
|
||||
with:
|
||||
stencil-version: nightly
|
||||
stencil-version: ${{ inputs.npm_release_tag || 'nightly' }}
|
||||
|
||||
test-core-clean-build:
|
||||
needs: [build-core-with-stencil-nightly]
|
||||
@@ -47,7 +52,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/workflows/actions/test-core-spec
|
||||
with:
|
||||
stencil-version: nightly
|
||||
stencil-version: ${{ inputs.npm_release_tag || 'nightly' }}
|
||||
|
||||
test-core-screenshot:
|
||||
strategy:
|
||||
|
||||
14
core/package-lock.json
generated
14
core/package-lock.json
generated
@@ -9,7 +9,7 @@
|
||||
"version": "7.4.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@stencil/core": "^4.4.0",
|
||||
"@stencil/core": "^4.4.1",
|
||||
"ionicons": "7.1.0",
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
@@ -1630,9 +1630,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@stencil/core": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.4.0.tgz",
|
||||
"integrity": "sha512-YlLyCqGBsMEuZb3XTO/STT0TX9eSwjoVhCJgtjVfQOF+ebIMVlojTh40CmDveWiWbth687cbr6S2heeussV8Sg==",
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.4.1.tgz",
|
||||
"integrity": "sha512-SirGcrb5yKHCn2BwdM7HGVXuvCdmwiXlVczEj8jJxQIm42CAUQCUECxtZidTzp+oZBZnWLnoAvfanchJsgkQzA==",
|
||||
"bin": {
|
||||
"stencil": "bin/stencil"
|
||||
},
|
||||
@@ -11536,9 +11536,9 @@
|
||||
"requires": {}
|
||||
},
|
||||
"@stencil/core": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.4.0.tgz",
|
||||
"integrity": "sha512-YlLyCqGBsMEuZb3XTO/STT0TX9eSwjoVhCJgtjVfQOF+ebIMVlojTh40CmDveWiWbth687cbr6S2heeussV8Sg=="
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.4.1.tgz",
|
||||
"integrity": "sha512-SirGcrb5yKHCn2BwdM7HGVXuvCdmwiXlVczEj8jJxQIm42CAUQCUECxtZidTzp+oZBZnWLnoAvfanchJsgkQzA=="
|
||||
},
|
||||
"@stencil/react-output-target": {
|
||||
"version": "0.5.3",
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"loader/"
|
||||
],
|
||||
"dependencies": {
|
||||
"@stencil/core": "^4.4.0",
|
||||
"@stencil/core": "^4.4.1",
|
||||
"ionicons": "7.1.0",
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
|
||||
@@ -211,6 +211,10 @@ export class Checkbox implements ComponentInterface {
|
||||
};
|
||||
|
||||
private onClick = (ev: MouseEvent) => {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.toggleChecked(ev);
|
||||
};
|
||||
|
||||
|
||||
24
core/src/components/checkbox/test/checkbox.spec.ts
Normal file
24
core/src/components/checkbox/test/checkbox.spec.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { newSpecPage } from '@stencil/core/testing';
|
||||
|
||||
import { Checkbox } from '../checkbox';
|
||||
|
||||
describe('ion-checkbox: disabled', () => {
|
||||
it('clicking disabled checkbox should not toggle checked state', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Checkbox],
|
||||
html: `
|
||||
<ion-checkbox disabled="true">Checkbox</ion-checkbox>
|
||||
`,
|
||||
});
|
||||
|
||||
const checkbox = page.body.querySelector('ion-checkbox');
|
||||
|
||||
expect(checkbox.checked).toBe(false);
|
||||
|
||||
checkbox.click();
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
expect(checkbox.checked).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -40,6 +40,15 @@ export class Menu implements ComponentInterface, MenuI {
|
||||
private blocker = GESTURE_CONTROLLER.createBlocker({ disableScroll: true });
|
||||
private didLoad = false;
|
||||
|
||||
/**
|
||||
* Flag used to determine if an open/close
|
||||
* operation was cancelled. For example, if
|
||||
* an app calls "menu.open" then disables the menu
|
||||
* part way through the animation, then this would
|
||||
* be considered a cancelled operation.
|
||||
*/
|
||||
private operationCancelled = false;
|
||||
|
||||
isAnimating = false;
|
||||
width!: number;
|
||||
_isOpen = false;
|
||||
@@ -432,6 +441,17 @@ export class Menu implements ComponentInterface, MenuI {
|
||||
|
||||
await this.loadAnimation();
|
||||
await this.startAnimation(shouldOpen, animated);
|
||||
|
||||
/**
|
||||
* If the animation was cancelled then
|
||||
* return false because the operation
|
||||
* did not succeed.
|
||||
*/
|
||||
if (this.operationCancelled) {
|
||||
this.operationCancelled = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
this.afterAnimation(shouldOpen);
|
||||
|
||||
return true;
|
||||
@@ -472,18 +492,24 @@ export class Menu implements ComponentInterface, MenuI {
|
||||
const easingReverse = mode === 'ios' ? iosEasingReverse : mdEasingReverse;
|
||||
const ani = (this.animation as Animation)!
|
||||
.direction(isReversed ? 'reverse' : 'normal')
|
||||
.easing(isReversed ? easingReverse : easing)
|
||||
.onFinish(() => {
|
||||
if (ani.getDirection() === 'reverse') {
|
||||
ani.direction('normal');
|
||||
}
|
||||
});
|
||||
.easing(isReversed ? easingReverse : easing);
|
||||
|
||||
if (animated) {
|
||||
await ani.play();
|
||||
} else {
|
||||
ani.play({ sync: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* We run this after the play invocation
|
||||
* instead of using ani.onFinish so that
|
||||
* multiple onFinish callbacks do not get
|
||||
* run if an animation is played, stopped,
|
||||
* and then played again.
|
||||
*/
|
||||
if (ani.getDirection() === 'reverse') {
|
||||
ani.direction('normal');
|
||||
}
|
||||
}
|
||||
|
||||
private _isActive() {
|
||||
@@ -643,8 +669,6 @@ export class Menu implements ComponentInterface, MenuI {
|
||||
}
|
||||
|
||||
private afterAnimation(isOpen: boolean) {
|
||||
assert(this.isAnimating, '_before() should be called while animating');
|
||||
|
||||
// keep opening/closing the menu disabled for a touch more yet
|
||||
// only add listeners/css if it's enabled and isOpen
|
||||
// and only remove listeners/css if it's not open
|
||||
@@ -713,10 +737,30 @@ export class Menu implements ComponentInterface, MenuI {
|
||||
this.gesture.enable(isActive && this.swipeGesture);
|
||||
}
|
||||
|
||||
// Close menu immediately
|
||||
if (!isActive && this._isOpen) {
|
||||
// close if this menu is open, and should not be enabled
|
||||
this.forceClosing();
|
||||
/**
|
||||
* If the menu is disabled but it is still open
|
||||
* then we should close the menu immediately.
|
||||
* Additionally, if the menu is in the process
|
||||
* of animating {open, close} and the menu is disabled
|
||||
* then it should still be closed immediately.
|
||||
*/
|
||||
if (!isActive) {
|
||||
/**
|
||||
* It is possible to disable the menu while
|
||||
* it is mid-animation. When this happens, we
|
||||
* need to set the operationCancelled flag
|
||||
* so that this._setOpen knows to return false
|
||||
* and not run the "afterAnimation" callback.
|
||||
*/
|
||||
if (this.isAnimating) {
|
||||
this.operationCancelled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the menu is disabled then we should
|
||||
* forcibly close the menu even if it is open.
|
||||
*/
|
||||
this.afterAnimation(false);
|
||||
}
|
||||
|
||||
if (doc?.contains(this.el)) {
|
||||
@@ -730,19 +774,6 @@ export class Menu implements ComponentInterface, MenuI {
|
||||
menuController._setActiveMenu(this);
|
||||
}
|
||||
}
|
||||
|
||||
assert(!this.isAnimating, 'can not be animating');
|
||||
}
|
||||
|
||||
private forceClosing() {
|
||||
assert(this._isOpen, 'menu cannot be closed');
|
||||
|
||||
this.isAnimating = true;
|
||||
|
||||
const ani = (this.animation as Animation)!.direction('reverse');
|
||||
ani.play({ sync: true });
|
||||
|
||||
this.afterAnimation(false);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
41
core/src/components/menu/test/disable/index.html
Normal file
41
core/src/components/menu/test/disable/index.html
Normal file
@@ -0,0 +1,41 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Menu - Disable</title>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
|
||||
/>
|
||||
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
|
||||
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
|
||||
<script src="../../../../../scripts/testing/scripts.js"></script>
|
||||
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
|
||||
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<ion-app>
|
||||
<ion-menu side="start" id="start-menu" menu-id="start-menu" content-id="main">
|
||||
<ion-header>
|
||||
<ion-toolbar color="primary">
|
||||
<ion-title>Menu</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content class="ion-padding"> Menu Content </ion-content>
|
||||
</ion-menu>
|
||||
|
||||
<div class="ion-page" id="main">
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-menu-button></ion-menu-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Menu - Disable</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content class="ion-padding">Content</ion-content>
|
||||
</div>
|
||||
</ion-app>
|
||||
</body>
|
||||
</html>
|
||||
66
core/src/components/menu/test/disable/menu.e2e.ts
Normal file
66
core/src/components/menu/test/disable/menu.e2e.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
/**
|
||||
* This behavior does not vary across modes/directions
|
||||
*/
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('menu: disable'), () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(`/src/components/menu/test/disable`, config);
|
||||
});
|
||||
|
||||
test('should disable when menu is fully open', async ({ page }) => {
|
||||
const logs: string[] = [];
|
||||
|
||||
page.on('console', (msg) => {
|
||||
if (msg.type() === 'error') {
|
||||
logs.push(msg.text());
|
||||
}
|
||||
});
|
||||
|
||||
const menu = page.locator('ion-menu');
|
||||
|
||||
// Should be visible on initial presentation
|
||||
await menu.evaluate((el: HTMLIonMenuElement) => el.open());
|
||||
await expect(menu).toBeVisible();
|
||||
|
||||
// Disabling menu should hide it
|
||||
await menu.evaluate((el: HTMLIonMenuElement) => (el.disabled = true));
|
||||
await expect(menu).toBeHidden();
|
||||
|
||||
// Re-enabling menu and opening it show make it visible
|
||||
await menu.evaluate((el: HTMLIonMenuElement) => (el.disabled = false));
|
||||
await menu.evaluate((el: HTMLIonMenuElement) => el.open());
|
||||
await expect(menu).toBeVisible();
|
||||
|
||||
expect(logs.length).toBe(0);
|
||||
});
|
||||
|
||||
test('should disable when menu is animating', async ({ page }) => {
|
||||
const logs: string[] = [];
|
||||
|
||||
page.on('console', (msg) => {
|
||||
if (msg.type() === 'error') {
|
||||
logs.push(msg.text());
|
||||
}
|
||||
});
|
||||
|
||||
const menu = page.locator('ion-menu');
|
||||
|
||||
// Opening and quickly disabling menu should hide it
|
||||
menu.evaluate((el: HTMLIonMenuElement) => {
|
||||
el.open();
|
||||
setTimeout(() => (el.disabled = true), 0);
|
||||
});
|
||||
await expect(menu).toBeHidden();
|
||||
|
||||
// Re-enabling menu and opening it show make it visible
|
||||
await menu.evaluate((el: HTMLIonMenuElement) => (el.disabled = false));
|
||||
await menu.evaluate((el: HTMLIonMenuElement) => el.open());
|
||||
await expect(menu).toBeVisible();
|
||||
|
||||
expect(logs.length).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -414,9 +414,11 @@ export class PickerColumnInternal implements ComponentInterface {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { items, color, isActive, numericInput } = this;
|
||||
const { items, color, isActive, numericInput, value } = this;
|
||||
const mode = getIonMode(this);
|
||||
|
||||
const activeItem = items.find(item => item.value === value);
|
||||
|
||||
/**
|
||||
* exportparts is needed so ion-datetime can expose the parts
|
||||
* from two layers of shadow nesting. If this causes problems,
|
||||
@@ -425,6 +427,11 @@ export class PickerColumnInternal implements ComponentInterface {
|
||||
*/
|
||||
return (
|
||||
<Host
|
||||
role="spinbutton"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="0"
|
||||
aria-valuenow={activeItem ? activeItem.value : null}
|
||||
aria-valuetext={activeItem ? activeItem.text : null}
|
||||
exportparts={`${PICKER_ITEM_PART}, ${PICKER_ITEM_ACTIVE_PART}`}
|
||||
tabindex={0}
|
||||
class={createColorClasses(color, {
|
||||
|
||||
@@ -113,7 +113,7 @@ export class RadioGroup implements ComponentInterface {
|
||||
* using the `name` attribute.
|
||||
*/
|
||||
const selectedRadio = ev.target && (ev.target as HTMLElement).closest('ion-radio');
|
||||
if (selectedRadio) {
|
||||
if (selectedRadio && selectedRadio.disabled === false) {
|
||||
const currentValue = this.value;
|
||||
const newValue = selectedRadio.value;
|
||||
if (newValue !== currentValue) {
|
||||
|
||||
@@ -200,7 +200,11 @@ export class Radio implements ComponentInterface {
|
||||
};
|
||||
|
||||
private onClick = () => {
|
||||
const { radioGroup, checked } = this;
|
||||
const { radioGroup, checked, disabled } = this;
|
||||
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* The legacy control uses a native input inside
|
||||
|
||||
@@ -31,3 +31,27 @@ describe('ion-radio', () => {
|
||||
expect(radio.classList.contains('radio-checked')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ion-radio: disabled', () => {
|
||||
it('clicking disabled radio should not set checked state', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Radio, RadioGroup],
|
||||
html: `
|
||||
<ion-radio-group>
|
||||
<ion-radio disabled="true" value="a">Radio</ion-radio>
|
||||
</ion-radio-group>
|
||||
`,
|
||||
});
|
||||
|
||||
const radio = page.body.querySelector('ion-radio');
|
||||
const radioGroup = page.body.querySelector('ion-radio-group');
|
||||
|
||||
expect(radioGroup.value).toBe(undefined);
|
||||
|
||||
radio.click();
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
expect(radioGroup.value).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -316,29 +316,46 @@ export class Select implements ComponentInterface {
|
||||
|
||||
// focus selected option for popovers
|
||||
if (this.interface === 'popover') {
|
||||
let indexOfSelected = this.childOpts.map((o) => o.value).indexOf(this.value);
|
||||
indexOfSelected = indexOfSelected > -1 ? indexOfSelected : 0; // default to first option if nothing selected
|
||||
const selectedItem = overlay.querySelector<HTMLElement>(
|
||||
`.select-interface-option:nth-child(${indexOfSelected + 1})`
|
||||
);
|
||||
const indexOfSelected = this.childOpts.map((o) => o.value).indexOf(this.value);
|
||||
|
||||
if (selectedItem) {
|
||||
focusElement(selectedItem);
|
||||
if (indexOfSelected > -1) {
|
||||
const selectedItem = overlay.querySelector<HTMLElement>(
|
||||
`.select-interface-option:nth-child(${indexOfSelected + 1})`
|
||||
);
|
||||
|
||||
if (selectedItem) {
|
||||
focusElement(selectedItem);
|
||||
|
||||
/**
|
||||
* Browsers such as Firefox do not
|
||||
* correctly delegate focus when manually
|
||||
* focusing an element with delegatesFocus.
|
||||
* We work around this by manually focusing
|
||||
* the interactive element.
|
||||
* ion-radio and ion-checkbox are the only
|
||||
* elements that ion-select-popover uses, so
|
||||
* we only need to worry about those two components
|
||||
* when focusing.
|
||||
*/
|
||||
const interactiveEl = selectedItem.querySelector<HTMLElement>('ion-radio, ion-checkbox');
|
||||
if (interactiveEl) {
|
||||
interactiveEl.focus();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/**
|
||||
* Browsers such as Firefox do not
|
||||
* correctly delegate focus when manually
|
||||
* focusing an element with delegatesFocus.
|
||||
* We work around this by manually focusing
|
||||
* the interactive element.
|
||||
* ion-radio and ion-checkbox are the only
|
||||
* elements that ion-select-popover uses, so
|
||||
* we only need to worry about those two components
|
||||
* when focusing.
|
||||
* If no value is set then focus the first enabled option.
|
||||
*/
|
||||
const interactiveEl = selectedItem.querySelector<HTMLElement>('ion-radio, ion-checkbox');
|
||||
if (interactiveEl) {
|
||||
interactiveEl.focus();
|
||||
const firstEnabledOption = overlay.querySelector<HTMLElement>(
|
||||
'ion-radio:not(.radio-disabled), ion-checkbox:not(.checkbox-disabled)'
|
||||
);
|
||||
if (firstEnabledOption) {
|
||||
focusElement(firstEnabledOption.closest('ion-item')!);
|
||||
|
||||
/**
|
||||
* Focus the option for the same reason as we do above.
|
||||
*/
|
||||
firstEnabledOption.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
36
core/src/components/select/test/disabled/select.e2e.ts
Normal file
36
core/src/components/select/test/disabled/select.e2e.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('select: disabled options'), () => {
|
||||
test('should not focus a disabled option when no value is set', async ({ page, skip }) => {
|
||||
// TODO (FW-2979)
|
||||
skip.browser('webkit', 'Safari 16 only allows text fields and pop-up menus to be focused.');
|
||||
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/ionic-team/ionic-framework/issues/28284',
|
||||
});
|
||||
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-select interface="popover">
|
||||
<ion-select-option value="a" disabled="true">A</ion-select-option>
|
||||
<ion-select-option value="b">B</ion-select-option>
|
||||
</ion-select>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const select = page.locator('ion-select');
|
||||
const popover = page.locator('ion-popover');
|
||||
const ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent');
|
||||
|
||||
await select.click();
|
||||
await ionPopoverDidPresent.next();
|
||||
|
||||
const popoverOption = popover.locator('.select-interface-option:nth-of-type(2) ion-radio');
|
||||
await expect(popoverOption).toBeFocused();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -41,3 +41,24 @@ describe('toggle', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ion-toggle: disabled', () => {
|
||||
it('clicking disabled toggle should not toggle checked state', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Toggle],
|
||||
html: `
|
||||
<ion-toggle disabled="true">Toggle</ion-toggle>
|
||||
`,
|
||||
});
|
||||
|
||||
const toggle = page.body.querySelector('ion-toggle');
|
||||
|
||||
expect(toggle.checked).toBe(false);
|
||||
|
||||
toggle.click();
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
expect(toggle.checked).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -259,6 +259,10 @@ export class Toggle implements ComponentInterface {
|
||||
}
|
||||
|
||||
private onClick = (ev: MouseEvent) => {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
ev.preventDefault();
|
||||
|
||||
if (this.lastDrag + 300 < Date.now()) {
|
||||
|
||||
@@ -30,6 +30,8 @@ interface AnimationOnFinishCallback {
|
||||
o?: AnimationCallbackOptions;
|
||||
}
|
||||
|
||||
type AnimationOnStopCallback = AnimationOnFinishCallback;
|
||||
|
||||
export const createAnimation = (animationId?: string): Animation => {
|
||||
let _delay: number | undefined;
|
||||
let _duration: number | undefined;
|
||||
@@ -63,6 +65,7 @@ export const createAnimation = (animationId?: string): Animation => {
|
||||
const id: string | undefined = animationId;
|
||||
const onFinishCallbacks: AnimationOnFinishCallback[] = [];
|
||||
const onFinishOneTimeCallbacks: AnimationOnFinishCallback[] = [];
|
||||
const onStopOneTimeCallbacks: AnimationOnStopCallback[] = [];
|
||||
const elements: HTMLElement[] = [];
|
||||
const childAnimations: Animation[] = [];
|
||||
const stylesheets: HTMLElement[] = [];
|
||||
@@ -134,6 +137,35 @@ export const createAnimation = (animationId?: string): Animation => {
|
||||
return numAnimationsRunning !== 0 && !paused;
|
||||
};
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Remove a callback from a chosen callback array
|
||||
* @param callbackToRemove: A reference to the callback that should be removed
|
||||
* @param callbackObjects: An array of callbacks that callbackToRemove should be removed from.
|
||||
*/
|
||||
const clearCallback = (
|
||||
callbackToRemove: AnimationLifecycle,
|
||||
callbackObjects: AnimationOnFinishCallback[] | AnimationOnStopCallback[]
|
||||
) => {
|
||||
const index = callbackObjects.findIndex((callbackObject) => callbackObject.c === callbackToRemove);
|
||||
|
||||
if (index > -1) {
|
||||
callbackObjects.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Add a callback to be fired when an animation is stopped/cancelled.
|
||||
* @param callback: A reference to the callback that should be fired
|
||||
* @param opts: Any options associated with this particular callback
|
||||
*/
|
||||
const onStop = (callback: AnimationLifecycle, opts?: AnimationCallbackOptions) => {
|
||||
onStopOneTimeCallbacks.push({ c: callback, o: opts });
|
||||
|
||||
return ani;
|
||||
};
|
||||
|
||||
const onFinish = (callback: AnimationLifecycle, opts?: AnimationCallbackOptions) => {
|
||||
const callbacks = opts?.oneTimeCallback ? onFinishOneTimeCallbacks : onFinishCallbacks;
|
||||
callbacks.push({ c: callback, o: opts });
|
||||
@@ -953,7 +985,34 @@ export const createAnimation = (animationId?: string): Animation => {
|
||||
shouldCalculateNumAnimations = false;
|
||||
}
|
||||
|
||||
onFinish(() => resolve(), { oneTimeCallback: true });
|
||||
/**
|
||||
* When one of these callbacks fires we
|
||||
* need to clear the other's callback otherwise
|
||||
* you can potentially get these callbacks
|
||||
* firing multiple times if the play method
|
||||
* is subsequently called.
|
||||
* Example:
|
||||
* animation.play() (onStop and onFinish callbacks are registered)
|
||||
* animation.stop() (onStop callback is fired, onFinish is not)
|
||||
* animation.play() (onStop and onFinish callbacks are registered)
|
||||
* Total onStop callbacks: 1
|
||||
* Total onFinish callbacks: 2
|
||||
*/
|
||||
const onStopCallback = () => {
|
||||
clearCallback(onFinishCallback, onFinishOneTimeCallbacks);
|
||||
resolve();
|
||||
};
|
||||
const onFinishCallback = () => {
|
||||
clearCallback(onStopCallback, onStopOneTimeCallbacks);
|
||||
resolve();
|
||||
};
|
||||
|
||||
/**
|
||||
* The play method resolves when an animation
|
||||
* run either finishes or is cancelled.
|
||||
*/
|
||||
onFinish(onFinishCallback, { oneTimeCallback: true });
|
||||
onStop(onStopCallback, { oneTimeCallback: true });
|
||||
|
||||
childAnimations.forEach((animation) => {
|
||||
animation.play();
|
||||
@@ -969,6 +1028,14 @@ export const createAnimation = (animationId?: string): Animation => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Stops an animation and resets it state to the
|
||||
* beginning. This does not fire any onFinish
|
||||
* callbacks because the animation did not finish.
|
||||
* However, since the animation was not destroyed
|
||||
* (i.e. the animation could run again) we do not
|
||||
* clear the onFinish callbacks.
|
||||
*/
|
||||
const stop = () => {
|
||||
childAnimations.forEach((animation) => {
|
||||
animation.stop();
|
||||
@@ -980,6 +1047,9 @@ export const createAnimation = (animationId?: string): Animation => {
|
||||
}
|
||||
|
||||
resetFlags();
|
||||
|
||||
onStopOneTimeCallbacks.forEach((onStopCallback) => onStopCallback.c(0, ani));
|
||||
onStopOneTimeCallbacks.length = 0;
|
||||
};
|
||||
|
||||
const from = (property: string, value: any) => {
|
||||
|
||||
@@ -4,6 +4,24 @@ import { processKeyframes } from '../animation-utils';
|
||||
import { getTimeGivenProgression } from '../cubic-bezier';
|
||||
|
||||
describe('Animation Class', () => {
|
||||
describe('play()', () => {
|
||||
it('should resolve when the animation is cancelled', async () => {
|
||||
// Tell Jest to expect 1 assertion for async code
|
||||
expect.assertions(1);
|
||||
const el = document.createElement('div');
|
||||
const animation = createAnimation()
|
||||
.addElement(el)
|
||||
.fromTo('transform', 'translateX(0px)', 'translateX(100px)')
|
||||
.duration(100000);
|
||||
|
||||
const animationPromise = animation.play();
|
||||
|
||||
animation.stop();
|
||||
|
||||
// Expect that the promise resolves and returns undefined
|
||||
expect(animationPromise).resolves.toEqual(undefined);
|
||||
});
|
||||
});
|
||||
describe('isRunning()', () => {
|
||||
let animation: Animation;
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -79,7 +79,11 @@ export const createSwipeBackGesture = (
|
||||
return createGesture({
|
||||
el,
|
||||
gestureName: 'goback-swipe',
|
||||
gesturePriority: 40,
|
||||
/**
|
||||
* Swipe to go back should have priority over other horizontal swipe
|
||||
* gestures. These gestures have a priority of 100 which is why 101 was chosen here.
|
||||
*/
|
||||
gesturePriority: 101,
|
||||
threshold: 10,
|
||||
canStart,
|
||||
onStart: onStartHandler,
|
||||
|
||||
Reference in New Issue
Block a user