diff --git a/.github/ionic-issue-bot.yml b/.github/ionic-issue-bot.yml index 8db735ce2d..25fab64cb6 100644 --- a/.github/ionic-issue-bot.yml +++ b/.github/ionic-issue-bot.yml @@ -93,6 +93,7 @@ stale: - "triage" - "type: bug" - "type: feature request" + - "needs: investigation" exemptAssigned: true exemptProjects: true exemptMilestones: true diff --git a/CHANGELOG.md b/CHANGELOG.md index ae829e6068..8874ca0c0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,40 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.5.9](https://github.com/ionic-team/ionic-framework/compare/v8.5.8...v8.5.9) (2025-06-04) + + +### Bug Fixes + +* **datetime:** display the correct month when multiple values are set ([#29610](https://github.com/ionic-team/ionic-framework/issues/29610)) ([14f32f8](https://github.com/ionic-team/ionic-framework/commit/14f32f8feea7b3880367868ff0a2134b0c28cc07)), closes [#29094](https://github.com/ionic-team/ionic-framework/issues/29094) +* **modal:** move sheet footers instead of cloning while dragging ([#30433](https://github.com/ionic-team/ionic-framework/issues/30433)) ([4cbbbb0](https://github.com/ionic-team/ionic-framework/commit/4cbbbb053ad36d176f1d79ad09777f94ca8076d2)), closes [#30315](https://github.com/ionic-team/ionic-framework/issues/30315) [#30341](https://github.com/ionic-team/ionic-framework/issues/30341) [#30312](https://github.com/ionic-team/ionic-framework/issues/30312) + + + + + +## [8.5.8](https://github.com/ionic-team/ionic-framework/compare/v8.5.7...v8.5.8) (2025-05-28) + + +### Bug Fixes + +* **input-password-toggle, button:** force update aria attributes ([#30411](https://github.com/ionic-team/ionic-framework/issues/30411)) ([4e38700](https://github.com/ionic-team/ionic-framework/commit/4e387005663b4e8425cb28e41608bb4f924b3864)) + + + + + +## [8.5.7](https://github.com/ionic-team/ionic-framework/compare/v8.5.6...v8.5.7) (2025-05-07) + + +### Bug Fixes + +* **labels:** prevent clicking a label from triggering onClick twice on several components ([#30384](https://github.com/ionic-team/ionic-framework/issues/30384)) ([7d639b0](https://github.com/ionic-team/ionic-framework/commit/7d639b0412120523f758942c855cb69f9a52e9d9)), closes [#30165](https://github.com/ionic-team/ionic-framework/issues/30165) + + + + + ## [8.5.6](https://github.com/ionic-team/ionic-framework/compare/v8.5.5...v8.5.6) (2025-04-30) diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index ed973dc865..0ae3e61282 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -3,6 +3,40 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.5.9](https://github.com/ionic-team/ionic-framework/compare/v8.5.8...v8.5.9) (2025-06-04) + + +### Bug Fixes + +* **datetime:** display the correct month when multiple values are set ([#29610](https://github.com/ionic-team/ionic-framework/issues/29610)) ([14f32f8](https://github.com/ionic-team/ionic-framework/commit/14f32f8feea7b3880367868ff0a2134b0c28cc07)), closes [#29094](https://github.com/ionic-team/ionic-framework/issues/29094) +* **modal:** move sheet footers instead of cloning while dragging ([#30433](https://github.com/ionic-team/ionic-framework/issues/30433)) ([4cbbbb0](https://github.com/ionic-team/ionic-framework/commit/4cbbbb053ad36d176f1d79ad09777f94ca8076d2)), closes [#30315](https://github.com/ionic-team/ionic-framework/issues/30315) [#30341](https://github.com/ionic-team/ionic-framework/issues/30341) [#30312](https://github.com/ionic-team/ionic-framework/issues/30312) + + + + + +## [8.5.8](https://github.com/ionic-team/ionic-framework/compare/v8.5.7...v8.5.8) (2025-05-28) + + +### Bug Fixes + +* **input-password-toggle, button:** force update aria attributes ([#30411](https://github.com/ionic-team/ionic-framework/issues/30411)) ([4e38700](https://github.com/ionic-team/ionic-framework/commit/4e387005663b4e8425cb28e41608bb4f924b3864)) + + + + + +## [8.5.7](https://github.com/ionic-team/ionic-framework/compare/v8.5.6...v8.5.7) (2025-05-07) + + +### Bug Fixes + +* **labels:** prevent clicking a label from triggering onClick twice on several components ([#30384](https://github.com/ionic-team/ionic-framework/issues/30384)) ([7d639b0](https://github.com/ionic-team/ionic-framework/commit/7d639b0412120523f758942c855cb69f9a52e9d9)), closes [#30165](https://github.com/ionic-team/ionic-framework/issues/30165) + + + + + ## [8.5.6](https://github.com/ionic-team/ionic-framework/compare/v8.5.5...v8.5.6) (2025-04-30) diff --git a/core/package-lock.json b/core/package-lock.json index 54b2109ee4..b31dfc1da1 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -1,12 +1,12 @@ { "name": "@ionic/core", - "version": "8.5.6", + "version": "8.5.9", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@ionic/core", - "version": "8.5.6", + "version": "8.5.9", "license": "MIT", "dependencies": { "@stencil/core": "4.33.1", @@ -19,7 +19,7 @@ "@capacitor/haptics": "^7.0.0", "@capacitor/keyboard": "^7.0.0", "@capacitor/status-bar": "^7.0.0", - "@clack/prompts": "^0.10.0", + "@clack/prompts": "^0.11.0", "@ionic/eslint-config": "^0.3.0", "@ionic/prettier-config": "^2.0.0", "@playwright/test": "^1.52.0", @@ -699,9 +699,9 @@ } }, "node_modules/@clack/core": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@clack/core/-/core-0.4.2.tgz", - "integrity": "sha512-NYQfcEy8MWIxrT5Fj8nIVchfRFA26yYKJcvBS7WlUIlw2OmQOY9DhGGXMovyI5J5PpxrCPGkgUi207EBrjpBvg==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-0.5.0.tgz", + "integrity": "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==", "dev": true, "dependencies": { "picocolors": "^1.0.0", @@ -709,12 +709,12 @@ } }, "node_modules/@clack/prompts": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.10.1.tgz", - "integrity": "sha512-Q0T02vx8ZM9XSv9/Yde0jTmmBQufZhPJfYAg2XrrrxWWaZgq1rr8nU8Hv710BQ1dhoP8rtY7YUdpGej2Qza/cw==", + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.11.0.tgz", + "integrity": "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==", "dev": true, "dependencies": { - "@clack/core": "0.4.2", + "@clack/core": "0.5.0", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } @@ -11132,9 +11132,9 @@ "requires": {} }, "@clack/core": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@clack/core/-/core-0.4.2.tgz", - "integrity": "sha512-NYQfcEy8MWIxrT5Fj8nIVchfRFA26yYKJcvBS7WlUIlw2OmQOY9DhGGXMovyI5J5PpxrCPGkgUi207EBrjpBvg==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-0.5.0.tgz", + "integrity": "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==", "dev": true, "requires": { "picocolors": "^1.0.0", @@ -11142,12 +11142,12 @@ } }, "@clack/prompts": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.10.1.tgz", - "integrity": "sha512-Q0T02vx8ZM9XSv9/Yde0jTmmBQufZhPJfYAg2XrrrxWWaZgq1rr8nU8Hv710BQ1dhoP8rtY7YUdpGej2Qza/cw==", + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.11.0.tgz", + "integrity": "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==", "dev": true, "requires": { - "@clack/core": "0.4.2", + "@clack/core": "0.5.0", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } diff --git a/core/package.json b/core/package.json index 0494169123..8f03ed3416 100644 --- a/core/package.json +++ b/core/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/core", - "version": "8.5.6", + "version": "8.5.9", "description": "Base components for Ionic", "keywords": [ "ionic", @@ -41,7 +41,7 @@ "@capacitor/haptics": "^7.0.0", "@capacitor/keyboard": "^7.0.0", "@capacitor/status-bar": "^7.0.0", - "@clack/prompts": "^0.10.0", + "@clack/prompts": "^0.11.0", "@ionic/eslint-config": "^0.3.0", "@ionic/prettier-config": "^2.0.0", "@playwright/test": "^1.52.0", diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 84bed21fa5..3fc70d62b0 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -2928,7 +2928,7 @@ export namespace Components { */ "cancelButtonIcon": string; /** - * Set the the cancel button text. Only applies to `ios` mode. + * Set the cancel button text. Only applies to `ios` mode. * @default 'Cancel' */ "cancelButtonText": string; @@ -8188,7 +8188,7 @@ declare namespace LocalJSX { */ "cancelButtonIcon"?: string; /** - * Set the the cancel button text. Only applies to `ios` mode. + * Set the cancel button text. Only applies to `ios` mode. * @default 'Cancel' */ "cancelButtonText"?: string; diff --git a/core/src/components/button/button.tsx b/core/src/components/button/button.tsx index 4e63b8413e..47326abfea 100644 --- a/core/src/components/button/button.tsx +++ b/core/src/components/button/button.tsx @@ -1,5 +1,5 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core'; -import { Component, Element, Event, Host, Prop, Watch, State, h } from '@stencil/core'; +import { Component, Element, Event, Host, Prop, Watch, State, forceUpdate, h } from '@stencil/core'; import type { AnchorInterface, ButtonInterface } from '@utils/element-interface'; import type { Attributes } from '@utils/helpers'; import { inheritAriaAttributes, hasShadowDom } from '@utils/helpers'; @@ -158,6 +158,26 @@ export class Button implements ComponentInterface, AnchorInterface, ButtonInterf */ @Event() ionBlur!: EventEmitter; + /** + * This component is used within the `ion-input-password-toggle` component + * to toggle the visibility of the password input. + * These attributes need to update based on the state of the password input. + * Otherwise, the values will be stale. + * + * @param newValue + * @param _oldValue + * @param propName + */ + @Watch('aria-checked') + @Watch('aria-label') + onAriaChanged(newValue: string, _oldValue: string, propName: string) { + this.inheritedAttributes = { + ...this.inheritedAttributes, + [propName]: newValue, + }; + forceUpdate(this); + } + /** * This is responsible for rendering a hidden native * button element inside the associated form. This allows diff --git a/core/src/components/checkbox/checkbox.tsx b/core/src/components/checkbox/checkbox.tsx index 3b9c3e6724..4a9132665f 100644 --- a/core/src/components/checkbox/checkbox.tsx +++ b/core/src/components/checkbox/checkbox.tsx @@ -199,6 +199,14 @@ export class Checkbox implements ComponentInterface { this.toggleChecked(ev); }; + /** + * Stops propagation when the display label is clicked, + * otherwise, two clicks will be triggered. + */ + private onDivLabelClick = (ev: MouseEvent) => { + ev.stopPropagation(); + }; + private getHintTextID(): string | undefined { const { el, helperText, errorText, helperTextId, errorTextId } = this; @@ -314,6 +322,7 @@ export class Checkbox implements ComponentInterface { }} part="label" id={this.inputLabelId} + onClick={this.onDivLabelClick} > {this.renderHintText()} diff --git a/core/src/components/checkbox/test/basic/checkbox.e2e.ts b/core/src/components/checkbox/test/basic/checkbox.e2e.ts index 1a41b33959..159e86f9a3 100644 --- a/core/src/components/checkbox/test/basic/checkbox.e2e.ts +++ b/core/src/components/checkbox/test/basic/checkbox.e2e.ts @@ -99,4 +99,38 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => expect(ionChange).not.toHaveReceivedEvent(); }); }); + + test.describe(title('checkbox: click'), () => { + test('should trigger onclick only once when clicking the label', async ({ page }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://github.com/ionic-team/ionic-framework/issues/30165', + }); + + // Create a spy function in page context + await page.setContent(`Test Checkbox`, config); + + // Track calls to the exposed function + let clickCount = 0; + page.on('console', (msg) => { + if (msg.text().includes('click called')) { + clickCount++; + } + }); + + const input = page.locator('div.label-text-wrapper'); + + // Use position to make sure we click into the label enough to trigger + // what would be the double click + await input.click({ + position: { + x: 5, + y: 5, + }, + }); + + // Verify the click was triggered exactly once + expect(clickCount).toBe(1); + }); + }); }); diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx index 5eb8afb408..71a9512e56 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -1276,21 +1276,20 @@ export class Datetime implements ComponentInterface { } /** - * If there are multiple values, pick an arbitrary one to clamp to. This way, - * if the values are across months, we always show at least one of them. Note - * that the values don't necessarily have to be in order. + * If there are multiple values, clamp to the last one. + * This is because the last value is the one that the user + * has most recently interacted with. */ - const singleValue = Array.isArray(valueToProcess) ? valueToProcess[0] : valueToProcess; + const singleValue = Array.isArray(valueToProcess) ? valueToProcess[valueToProcess.length - 1] : valueToProcess; const targetValue = clampDate(singleValue, minParts, maxParts); const { month, day, year, hour, minute } = targetValue; const ampm = parseAmPm(hour!); /** - * Since `activeParts` indicates a value that - * been explicitly selected either by the - * user or the app, only update `activeParts` - * if the `value` property is set. + * Since `activeParts` indicates a value that been explicitly selected + * either by the user or the app, only update `activeParts` if the + * `value` property is set. */ if (hasValue) { if (Array.isArray(valueToProcess)) { @@ -1314,53 +1313,29 @@ export class Datetime implements ComponentInterface { this.activeParts = []; } - /** - * Only animate if: - * 1. We're using grid style (wheel style pickers should just jump to new value) - * 2. The month and/or year actually changed, and both are defined (otherwise there's nothing to animate to) - * 3. The calendar body is visible (prevents animation when in collapsed datetime-button, for example) - * 4. The month/year picker is not open (since you wouldn't see the animation anyway) - */ const didChangeMonth = (month !== undefined && month !== workingParts.month) || (year !== undefined && year !== workingParts.year); const bodyIsVisible = el.classList.contains('datetime-ready'); const { isGridStyle, showMonthAndYear } = this; - let areAllSelectedDatesInSameMonth = true; - if (Array.isArray(valueToProcess)) { - const firstMonth = valueToProcess[0].month; - for (const date of valueToProcess) { - if (date.month !== firstMonth) { - areAllSelectedDatesInSameMonth = false; - break; - } - } - } - - /** - * If there is more than one date selected - * and the dates aren't all in the same month, - * then we should neither animate to the date - * nor update the working parts because we do - * not know which date the user wants to view. - */ - if (areAllSelectedDatesInSameMonth) { - if (isGridStyle && didChangeMonth && bodyIsVisible && !showMonthAndYear) { - this.animateToDate(targetValue); - } else { - /** - * We only need to do this if we didn't just animate to a new month, - * since that calls prevMonth/nextMonth which calls setWorkingParts for us. - */ - this.setWorkingParts({ - month, - day, - year, - hour, - minute, - ampm, - }); - } + if (isGridStyle && didChangeMonth && bodyIsVisible && !showMonthAndYear) { + /** + * Only animate if: + * 1. We're using grid style (wheel style pickers should just jump to new value) + * 2. The month and/or year actually changed, and both are defined (otherwise there's nothing to animate to) + * 3. The calendar body is visible (prevents animation when in collapsed datetime-button, for example) + * 4. The month/year picker is not open (since you wouldn't see the animation anyway) + */ + this.animateToDate(targetValue); + } else { + this.setWorkingParts({ + month, + day, + year, + hour, + minute, + ampm, + }); } }; diff --git a/core/src/components/datetime/test/multiple/datetime.e2e.ts b/core/src/components/datetime/test/multiple/datetime.e2e.ts index 0e2c0efb38..55a386f6f2 100644 --- a/core/src/components/datetime/test/multiple/datetime.e2e.ts +++ b/core/src/components/datetime/test/multiple/datetime.e2e.ts @@ -174,18 +174,6 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { await expect(monthYear).toHaveText(/June 2022/); }); - test('should not scroll to new month when value is updated with dates in different months', async ({ page }) => { - const datetime = await datetimeFixture.goto(config, MULTIPLE_DATES); - await datetime.evaluate((el: HTMLIonDatetimeElement, dates: string[]) => { - el.value = dates; - }, MULTIPLE_DATES_SEPARATE_MONTHS); - - await page.waitForChanges(); - - const monthYear = datetime.locator('.calendar-month-year'); - await expect(monthYear).toHaveText(/June 2022/); - }); - test('with buttons, should only update value when confirm is called', async ({ page }) => { const datetime = await datetimeFixture.goto(config, SINGLE_DATE, { showDefaultButtons: true }); const june2Button = datetime.locator('[data-month="6"][data-day="2"]'); @@ -311,4 +299,41 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { await expect(header).toHaveText('Mon, Oct 10'); }); }); + + test.describe('with selected days in different months', () => { + test(`set the active month view to the latest value's month`, async ({ page }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://github.com/ionic-team/ionic-framework/issues/29094', + }); + + const datetime = await new DatetimeMultipleFixture(page).goto(config, MULTIPLE_DATES_SEPARATE_MONTHS); + const calendarMonthYear = datetime.locator('.calendar-month-year'); + + await expect(calendarMonthYear).toHaveText(/May 2022/); + }); + + test('does not change the active month view when selecting a day in a different month', async ({ + page, + }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://github.com/ionic-team/ionic-framework/issues/29094', + }); + + const datetime = await new DatetimeMultipleFixture(page).goto(config, MULTIPLE_DATES_SEPARATE_MONTHS); + const nextButton = page.locator('.calendar-next-prev ion-button:nth-child(2)'); + const calendarMonthYear = datetime.locator('.calendar-month-year'); + + await nextButton.click(); + + await expect(calendarMonthYear).toHaveText(/June 2022/); + + const june8Button = datetime.locator('[data-month="6"][data-day="8"]'); + + await june8Button.click(); + + await expect(calendarMonthYear).toHaveText(/June 2022/); + }); + }); }); diff --git a/core/src/components/input-password-toggle/input-password-toggle.tsx b/core/src/components/input-password-toggle/input-password-toggle.tsx index 44b8e0828a..90b743b41e 100644 --- a/core/src/components/input-password-toggle/input-password-toggle.tsx +++ b/core/src/components/input-password-toggle/input-password-toggle.tsx @@ -127,7 +127,7 @@ export class InputPasswordToggle implements ComponentInterface { fill="clear" shape="round" aria-checked={isPasswordVisible ? 'true' : 'false'} - aria-label="show password" + aria-label={isPasswordVisible ? 'Hide password' : 'Show password'} role="switch" type="button" onPointerDown={(ev) => { diff --git a/core/src/components/input-password-toggle/test/a11y/input-password-toggle.e2e.ts b/core/src/components/input-password-toggle/test/a11y/input-password-toggle.e2e.ts index 76b9f3d283..0493509dc2 100644 --- a/core/src/components/input-password-toggle/test/a11y/input-password-toggle.e2e.ts +++ b/core/src/components/input-password-toggle/test/a11y/input-password-toggle.e2e.ts @@ -20,4 +20,38 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => { expect(results.violations).toEqual([]); }); }); + + test.describe(title('input password toggle: aria attributes'), () => { + test('should inherit aria attributes to inner button on load', async ({ page }) => { + await page.setContent( + ` + + + + `, + config + ); + + const nativeButton = page.locator('ion-input-password-toggle button'); + + await expect(nativeButton).toHaveAttribute('aria-label', 'Show password'); + await expect(nativeButton).toHaveAttribute('aria-checked', 'false'); + }); + test('should inherit aria attributes to inner button after toggle', async ({ page }) => { + await page.setContent( + ` + + + + `, + config + ); + + const nativeButton = page.locator('ion-input-password-toggle button'); + await nativeButton.click(); + + await expect(nativeButton).toHaveAttribute('aria-label', 'Hide password'); + await expect(nativeButton).toHaveAttribute('aria-checked', 'true'); + }); + }); }); diff --git a/core/src/components/input/input.tsx b/core/src/components/input/input.tsx index afdaf6a79d..69281c5e89 100644 --- a/core/src/components/input/input.tsx +++ b/core/src/components/input/input.tsx @@ -720,6 +720,18 @@ export class Input implements ComponentInterface { return this.label !== undefined || this.labelSlot !== null; } + /** + * Stops propagation when the label is clicked, + * otherwise, two clicks will be triggered. + */ + private onLabelClick = (ev: MouseEvent) => { + // Only stop propagation if the click was directly on the label + // and not on the input or other child elements + if (ev.target === ev.currentTarget) { + ev.stopPropagation(); + } + }; + /** * Renders the border container * when fill="outline". @@ -815,9 +827,9 @@ export class Input implements ComponentInterface { * interactable, clicking the label would focus that instead * since it comes before the input in the DOM. */} -