Compare commits

...

8 Commits

Author SHA1 Message Date
Liam DeBeasi
726e4aeae7 add spinbutton poc 2023-10-10 16:22:34 -04:00
Liam DeBeasi
eee2115fd2 fix(select): do not focus disabled popover option (#28309)
Issue number: resolves #28284

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

Select focuses the first popover option when no value is provided. This
means that the first option is focused even if it disabled.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- Select focuses the first **enabled** popover option when no value is
provided.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

---------

Co-authored-by: Amanda Johnston <90629384+amandaejohnston@users.noreply.github.com>
2023-10-10 19:02:50 +00:00
dependabot[bot]
d0057352fe chore(deps): Bump @stencil/core from 4.4.0 to 4.4.1 in /core (#28317)
Bumps [@stencil/core](https://github.com/ionic-team/stencil) from 4.4.0
to 4.4.1.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/ionic-team/stencil/blob/main/CHANGELOG.md"><code>@​stencil/core</code>'s
changelog</a>.</em></p>
<blockquote>
<h2>❤️ <a
href="https://github.com/ionic-team/stencil/compare/v4.4.0...v4.4.1">4.4.1</a>
(2023-10-09)</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>screenshot:</strong> alert user when toMatchScreenshot uses
NaN (<a
href="https://redirect.github.com/ionic-team/stencil/issues/4891">#4891</a>)
(<a
href="a251946106">a251946</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="0f522244fa"><code>0f52224</code></a>
v4.4.1 (<a
href="https://redirect.github.com/ionic-team/stencil/issues/4918">#4918</a>)</li>
<li><a
href="2874e9d8d9"><code>2874e9d</code></a>
chore(deps): update dependency terser to v5.21.0 (<a
href="https://redirect.github.com/ionic-team/stencil/issues/4913">#4913</a>)</li>
<li><a
href="f008213dae"><code>f008213</code></a>
chore(deps): update typescript-eslint to v6.7.4 (<a
href="https://redirect.github.com/ionic-team/stencil/issues/4911">#4911</a>)</li>
<li><a
href="0aae1d8fe2"><code>0aae1d8</code></a>
chore(deps): update dependency <code>@​rollup/plugin-json</code> to
v6.0.1 (<a
href="https://redirect.github.com/ionic-team/stencil/issues/4906">#4906</a>)</li>
<li><a
href="c523f1c678"><code>c523f1c</code></a>
chore(deps): update dependency <code>@​types/node</code> to v20.8.2 (<a
href="https://redirect.github.com/ionic-team/stencil/issues/4910">#4910</a>)</li>
<li><a
href="abe407d49c"><code>abe407d</code></a>
chore(compiler): add namespace to validated config (<a
href="https://redirect.github.com/ionic-team/stencil/issues/4902">#4902</a>)</li>
<li><a
href="8d0360666e"><code>8d03606</code></a>
chore(test): document dynamic import of testing submodule (<a
href="https://redirect.github.com/ionic-team/stencil/issues/4885">#4885</a>)</li>
<li><a
href="a251946106"><code>a251946</code></a>
fix(screenshot): alert user when toMatchScreenshot uses NaN (<a
href="https://redirect.github.com/ionic-team/stencil/issues/4891">#4891</a>)</li>
<li><a
href="8129089603"><code>8129089</code></a>
chore(deps): update dependency eslint-plugin-jest to v27.4.2 (<a
href="https://redirect.github.com/ionic-team/stencil/issues/4894">#4894</a>)</li>
<li><a
href="50fdccdd42"><code>50fdccd</code></a>
chore(deps): update nick-fields/retry action to v2.9.0 (<a
href="https://redirect.github.com/ionic-team/stencil/issues/4896">#4896</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/ionic-team/stencil/compare/v4.4.0...v4.4.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@stencil/core&package-manager=npm_and_yarn&previous-version=4.4.0&new-version=4.4.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-10 16:23:08 +00:00
Liam DeBeasi
c70432e693 fix(checkbox, radio, toggle): disabled elements are not interactive (#28294)
Issue number: resolves #28293

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

Disabled toggles, radios, and checkboxes can still be enabled by
manually dispatching a click event on them.


## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- Toggles, radios, and checkboxes no longer activate if `disabled` is
set to `true`

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

Dev build: `7.4.4-dev.11696545130.1171e7a9`
2023-10-09 21:05:09 +00:00
Liam DeBeasi
a1690441e5 fix(menu): do not error if disabled or swipeGesture is changed mid-animation (#28268)
Issue number: resolves #20092, resolves #19676, resolves #19000

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

Menu is currently throwing errors because it expects no animations to be
running when any state changes happen (such as changing `disabled` or
`swipeGesture`).

For example, if you set `swipeGesture="false"` mid-gesture then the menu
will error. Alternatively, if you set `disabled="true"` mid-open
animation then the menu will error also. This is undesirable because it
can cause visual flickering and other undesirable behaviors as noted in
the linked threads.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- Any in-progress animation is cancelled if the state updates such that
the animation is no longer relevant (i.e. `disabled` is set to `true`
while the menu is opening)
- Removed relevant assertions
- Added tests

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

Dev build: `7.4.3-dev.11696264821.1755dd6a`
2023-10-09 16:52:53 +00:00
Liam DeBeasi
e6031fbef0 fix(animation): play method resolves when animation is stopped (#28264)
Issue number: N/A

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

When trying to fix
https://github.com/ionic-team/ionic-framework/issues/20092, I discovered
that
ac2c8e6c22/core/src/components/menu/menu.tsx (L483)
was never resolving when the animation was aborted in
ac2c8e6c22/core/src/components/menu/menu.tsx (L699).
This can happen if `menu.disabled` is set to `true` mid-animation.

In order to fix the menu bug, I need this promise to resolve when the
animation is stopped.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- The `play` method now correctly resolves when the animation is
cancelled.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

The `play` method resolves when a particular run of the animation is
finished. The `stop` method ensures that this run never finishes which
is why I've chosen to have `play` resolve. Note that `onFinish`
callbacks should not be fired because the animation run did not
complete.
2023-10-09 15:16:39 +00:00
Alexander Harding
d5f0c776df fix(core): swipe to go back gesture has priority over other horizontal swipe gestures (#28304)
Issue number: resolves #28303

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?

- Swipe back gesture is inconsistently clobbered by ion-item-sliding's
gesture.

## What is the new behavior?

- Swipe back gesture now has a higher priority than ion-item-sliding
-
-

## Does this introduce a breaking change?

- [ ] Yes
- [X] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

This patch has been in use in
[Voyager](https://github.com/aeharding/voyager) for the past couple
months to great success!

---------

Co-authored-by: Liam DeBeasi <liamdebeasi@users.noreply.github.com>
2023-10-09 15:03:23 +00:00
Ryan Waskiewicz
00767a02e4 chore(repo): parameterize stencil nightly (#28308)
Issue number: N/A

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

when the stencil team wants to test a dev build of stencil against the
framework team's ci, they push a branch to this repo that overrides the
stencil nightly job's npm tag (replacing 'nightly' with the dev build
version) and manually kick off the nightly workflow. this commit would
eliminate the need for that first step there

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

update the stencil nightly job to accept any npm release tag, rather
than always default to 'nightly'. doing so allows the stencil team to
reuse this workflow for cases where we'd like to test a _dev_ build of
stencil by running it through the framework's ci process, without
landing the feature in stencil first.

I was able to test that `nightly` gets set by default (and the field is
required) by running the workflow from this branch.

I then also tested this against a dev build of Stencil:
![Screenshot 2023-10-09 at 9 45
41 AM](https://github.com/ionic-team/ionic-framework/assets/1930213/65613327-e5b6-475d-9bcd-b3c0d27019d9)

Interestingly enough, it helped me catch something to consider if we
were to accept the PR this dev build is accepted!


## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

I considered making a separate workflow for this (rather than
override/use nightly) - it didn't quite seem worth the maintenance
effort 🤷
<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->
2023-10-09 14:21:33 +00:00
19 changed files with 436 additions and 60 deletions

View File

@@ -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
View File

@@ -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",

View File

@@ -31,7 +31,7 @@
"loader/"
],
"dependencies": {
"@stencil/core": "^4.4.0",
"@stencil/core": "^4.4.1",
"ionicons": "7.1.0",
"tslib": "^2.1.0"
},

View File

@@ -211,6 +211,10 @@ export class Checkbox implements ComponentInterface {
};
private onClick = (ev: MouseEvent) => {
if (this.disabled) {
return;
}
this.toggleChecked(ev);
};

View 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);
});
});

View File

@@ -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() {

View 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>

View 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);
});
});
});

View File

@@ -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, {

View File

@@ -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) {

View File

@@ -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

View File

@@ -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);
});
});

View File

@@ -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();
}
}
}

View 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();
});
});
});

View File

@@ -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);
});
});

View File

@@ -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()) {

View File

@@ -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) => {

View File

@@ -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(() => {

View File

@@ -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,