+```
+
+#### CSS
+
+```css
+ /**
+ * @prop --color-activated: Color of the button when pressed
+ * @prop --background-activated: Background of the button when pressed
+ * @prop --background-activated-opacity: Opacity of the background when pressed
+ */
+```
+
+Style the `ion-activated` class based on the spec for that element:
+
+```scss
+:host(.ion-activated) .button-native {
+ color: var(--color-activated);
+
+ &::after {
+ background: var(--background-activated);
+
+ opacity: var(--background-activated-opacity);
+ }
+}
+```
+
+> [!IMPORTANT]
+> Order matters! Activated should be **after** the focused & hover states.
+
+#### User Customization
+
+Setting the activated state on the `::after` pseudo-element allows the user to customize the activated state without knowing what the default opacity is set at. A user can customize in the following ways to have a solid red background on press, or they can leave out `--background-activated-opacity` and the button will use the default activated opacity to match the spec.
+
+```css
+ion-button {
+ --background-activated: red;
+ --background-activated-opacity: 1;
+}
+```
+
+
### Ripple Effect
The ripple effect should be added to elements for Material Design. It *requires* the `ion-activatable` class to be set on the parent element to work, and relative positioning on the parent.
diff --git a/.github/workflows/actions/test-core-screenshot/action.yml b/.github/workflows/actions/test-core-screenshot/action.yml
index d66bbaeae3..3ff2250ecd 100644
--- a/.github/workflows/actions/test-core-screenshot/action.yml
+++ b/.github/workflows/actions/test-core-screenshot/action.yml
@@ -21,33 +21,13 @@ runs:
name: ionic-core
path: ./core
filename: CoreBuild.zip
- - name: Install Playwright Dependencies
- run: npm install && npx playwright install && npx playwright install-deps
+ - name: Install Dependencies
+ run: npm install
shell: bash
working-directory: ./core
- - id: clean-component-name
- name: Clean Component Name
- # Remove `ion-` prefix from the `component` variable if it exists.
- run: |
- echo "component=$(echo ${{ inputs.component }} | sed 's/ion-//g')" >> $GITHUB_OUTPUT
- shell: bash
- - id: set-test-file
- name: Set Test File
- # Screenshots can be updated for all components or specified component(s).
- # If the `component` variable is set, then the test has the option to
- # - run all the file paths that are in a component folder.
- # -- For example: if the `component` value is "item", then the test will run all the file paths that are in the "src/components/item" folder.
- # -- For example: if the `component` value is "item chip", then the test will run all the file paths that are in the "src/components/item" and "src/components/chip" folders.
- run: |
- if [ -n "${{ steps.clean-component-name.outputs.component }}" ]; then
- echo "testFile=\$(echo '${{ steps.clean-component-name.outputs.component }}' | awk '{for(i=1;i<=NF;i++) \$i=\"src/components/\"\$i}1')" >> $GITHUB_OUTPUT
- else
- echo "testFile=$(echo '')" >> $GITHUB_OUTPUT
- fi
- shell: bash
- name: Test
if: inputs.update != 'true'
- run: npm run test.e2e ${{ steps.set-test-file.outputs.testFile }} -- --shard=${{ inputs.shard }}/${{ inputs.totalShards }}
+ run: npm run test.e2e.docker.ci ${{ inputs.component }} -- --shard=${{ inputs.shard }}/${{ inputs.totalShards }}
shell: bash
working-directory: ./core
- name: Test and Update
@@ -69,7 +49,7 @@ runs:
# which is why we not using the upload-archive
# composite step here.
run: |
- npm run test.e2e ${{ steps.set-test-file.outputs.testFile }} -- --shard=${{ inputs.shard }}/${{ inputs.totalShards }} --update-snapshots
+ npm run test.e2e.docker.ci ${{ inputs.component }} -- --shard=${{ inputs.shard }}/${{ inputs.totalShards }} --update-snapshots
git add src/\*.png --force
mkdir updated-screenshots
cd ../ && rsync -R --progress $(git diff --name-only --cached) core/updated-screenshots
diff --git a/.github/workflows/actions/update-reference-screenshots/action.yml b/.github/workflows/actions/update-reference-screenshots/action.yml
index d33c5e55ce..50f12b858b 100644
--- a/.github/workflows/actions/update-reference-screenshots/action.yml
+++ b/.github/workflows/actions/update-reference-screenshots/action.yml
@@ -25,12 +25,9 @@ runs:
# Configure user as Ionitron
# and push only the changed .png snapshots
# to the remote branch.
- # Screenshots are in .gitignore
+ # Non-Linux screenshots are in .gitignore
# to prevent local screenshots from getting
- # pushed to Git. As a result, we need --force
- # here so that CI generated screenshots can
- # get added to git. Screenshot ground truths
- # should only be added via this CI process.
+ # pushed to Git.
run: |
git config user.name ionitron
git config user.email hi@ionicframework.com
@@ -38,7 +35,7 @@ runs:
# This adds an empty entry for new
# screenshot files so we can track them with
# git diff
- git add src/\*.png --force -N
+ git add src/\*.png -N
if git diff --exit-code; then
echo -e "\033[1;31m⚠️ Error: No new screenshots generated ⚠️\033[0m"
@@ -48,7 +45,7 @@ runs:
else
# This actually adds the contents
# of the screenshots (including new ones)
- git add src/\*.png --force
+ git add src/\*.png
git commit -m "chore(): add updated snapshots"
git push
fi
diff --git a/.github/workflows/update-screenshots.yml b/.github/workflows/update-screenshots.yml
index bce7f324f2..5bb35c5905 100644
--- a/.github/workflows/update-screenshots.yml
+++ b/.github/workflows/update-screenshots.yml
@@ -3,6 +3,20 @@ name: 'Update Reference Screenshots'
on:
workflow_dispatch:
inputs:
+ # Screenshots can be updated for all components or specified component(s).
+ # If the `component` variable is set, then the test has the option to
+ # - run all the instances of the specified component(s) in the `src/components` folder
+ # -- For example: if the `component` value is "item", then the following command will be: `npm run test.e2e item`
+ # - run the specified file path
+ # -- For example: if the `component` value is "src/components/item/test/basic", then the following command will be: `npm run test.e2e src/components/item/test/basic`
+ # - run multiple specified components based on the space-separated value
+ # -- For example: if the `component` value is "item basic", then the following command will be: `npm run test.e2e item basic`
+ # -- For example: if the `component` value is "src/components/item/test/basic src/components/item/test/a11y", then the following command will be: `npm run test.e2e src/components/item/test/basic src/components/item/test/a11y`
+ #
+ # If the `component` variable is not set, then the test will run all the instances of the components in the `src/components` folder.
+ # - For example: `npm run test.e2e`
+ #
+ # More common options can be found at the Playwright Command line page: https://playwright.dev/docs/test-cli
component:
description: 'What component(s) should be updated? (leave blank to update all or use a space-separated list for multiple components)'
required: false
diff --git a/.gitignore b/.gitignore
index ec9a5f272d..e610d8a11d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -68,7 +68,16 @@ core/www/
# playwright
core/test-results/
core/playwright-report/
-core/**/*-snapshots
+
+# ground truths generated outside of docker should not be committed to the repo
+core/**/*-snapshots/*
+
+# new ground truths should only be generated inside of docker which will result in -linux.png screenshots
+!core/**/*-snapshots/*-linux.png
+
+# these files are going to be different per-developer environment so do not add them to the repo
+core/docker-display.txt
+core/docker-display-volume.txt
# angular
packages/angular/css/
diff --git a/core/Dockerfile b/core/Dockerfile
new file mode 100644
index 0000000000..d214b9cc3f
--- /dev/null
+++ b/core/Dockerfile
@@ -0,0 +1,5 @@
+# Get Playwright
+FROM mcr.microsoft.com/playwright:v1.42.1
+
+# Set the working directory
+WORKDIR /ionic
diff --git a/core/package-lock.json b/core/package-lock.json
index e81992b4c8..5e13b5ca8d 100644
--- a/core/package-lock.json
+++ b/core/package-lock.json
@@ -633,9 +633,9 @@
"dev": true
},
"node_modules/@capacitor/core": {
- "version": "5.7.2",
- "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-5.7.2.tgz",
- "integrity": "sha512-/OUtfINmk7ke32VtKIHRAy8NlunbeK+aCqCHOS+fvtr7nUsOJXPkYgbgqZp/CWXET/gSK1xxMecaVBzpE98UKA==",
+ "version": "5.7.3",
+ "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-5.7.3.tgz",
+ "integrity": "sha512-xEuQmP+h0tugl2N+qRcdrUavZydvTnnmtvqu/OtCBb/bKZo2PDRFft7MxuQRE2GxXs6kLy3cvwzhDAHB3a+9mw==",
"dev": true,
"dependencies": {
"tslib": "^2.1.0"
@@ -1664,12 +1664,12 @@
}
},
"node_modules/@playwright/test": {
- "version": "1.41.2",
- "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.2.tgz",
- "integrity": "sha512-qQB9h7KbibJzrDpkXkYvsmiDJK14FULCCZgEcoe2AvFAS64oCirWTwzTlAYEbKaRxWs5TFesE1Na6izMv3HfGg==",
+ "version": "1.42.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.42.1.tgz",
+ "integrity": "sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ==",
"dev": true,
"dependencies": {
- "playwright": "1.41.2"
+ "playwright": "1.42.1"
},
"bin": {
"playwright": "cli.js"
@@ -1759,9 +1759,9 @@
}
},
"node_modules/@stencil/core": {
- "version": "4.12.5",
- "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.12.5.tgz",
- "integrity": "sha512-vSyFjY7XSEx0ufa9SebOd437CvnneaTXlCpuGDhjUDxAjGBlu6ie5qHyubobVGBth//aErc6wZPHc6W75Vp3iQ==",
+ "version": "4.12.6",
+ "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.12.6.tgz",
+ "integrity": "sha512-15JO2TdaxGVKNdLZb/2TtDa+juj3XGD/V0y/disgdzYYSnajgSh06nwODfdHz9eTUh1Hisz+KIo857I1rCZrfg==",
"bin": {
"stencil": "bin/stencil"
},
@@ -7986,12 +7986,12 @@
}
},
"node_modules/playwright": {
- "version": "1.41.2",
- "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.41.2.tgz",
- "integrity": "sha512-v0bOa6H2GJChDL8pAeLa/LZC4feoAMbSQm1/jF/ySsWWoaNItvrMP7GEkvEEFyCTUYKMxjQKaTSg5up7nR6/8A==",
+ "version": "1.42.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.42.1.tgz",
+ "integrity": "sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==",
"dev": true,
"dependencies": {
- "playwright-core": "1.41.2"
+ "playwright-core": "1.42.1"
},
"bin": {
"playwright": "cli.js"
@@ -8004,9 +8004,9 @@
}
},
"node_modules/playwright-core": {
- "version": "1.41.2",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.41.2.tgz",
- "integrity": "sha512-VaTvwCA4Y8kxEe+kfm2+uUUw5Lubf38RxF7FpBxLPmGe5sdNkSg5e3ChEigaGrX7qdqT3pt2m/98LiyvU2x6CA==",
+ "version": "1.42.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.42.1.tgz",
+ "integrity": "sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==",
"dev": true,
"bin": {
"playwright-core": "cli.js"
@@ -10416,9 +10416,9 @@
"dev": true
},
"@capacitor/core": {
- "version": "5.7.2",
- "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-5.7.2.tgz",
- "integrity": "sha512-/OUtfINmk7ke32VtKIHRAy8NlunbeK+aCqCHOS+fvtr7nUsOJXPkYgbgqZp/CWXET/gSK1xxMecaVBzpE98UKA==",
+ "version": "5.7.3",
+ "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-5.7.3.tgz",
+ "integrity": "sha512-xEuQmP+h0tugl2N+qRcdrUavZydvTnnmtvqu/OtCBb/bKZo2PDRFft7MxuQRE2GxXs6kLy3cvwzhDAHB3a+9mw==",
"dev": true,
"requires": {
"tslib": "^2.1.0"
@@ -11156,12 +11156,12 @@
}
},
"@playwright/test": {
- "version": "1.41.2",
- "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.2.tgz",
- "integrity": "sha512-qQB9h7KbibJzrDpkXkYvsmiDJK14FULCCZgEcoe2AvFAS64oCirWTwzTlAYEbKaRxWs5TFesE1Na6izMv3HfGg==",
+ "version": "1.42.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.42.1.tgz",
+ "integrity": "sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ==",
"dev": true,
"requires": {
- "playwright": "1.41.2"
+ "playwright": "1.42.1"
}
},
"@rollup/plugin-node-resolve": {
@@ -11229,9 +11229,9 @@
"requires": {}
},
"@stencil/core": {
- "version": "4.12.5",
- "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.12.5.tgz",
- "integrity": "sha512-vSyFjY7XSEx0ufa9SebOd437CvnneaTXlCpuGDhjUDxAjGBlu6ie5qHyubobVGBth//aErc6wZPHc6W75Vp3iQ=="
+ "version": "4.12.6",
+ "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.12.6.tgz",
+ "integrity": "sha512-15JO2TdaxGVKNdLZb/2TtDa+juj3XGD/V0y/disgdzYYSnajgSh06nwODfdHz9eTUh1Hisz+KIo857I1rCZrfg=="
},
"@stencil/react-output-target": {
"version": "0.5.3",
@@ -15756,19 +15756,19 @@
}
},
"playwright": {
- "version": "1.41.2",
- "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.41.2.tgz",
- "integrity": "sha512-v0bOa6H2GJChDL8pAeLa/LZC4feoAMbSQm1/jF/ySsWWoaNItvrMP7GEkvEEFyCTUYKMxjQKaTSg5up7nR6/8A==",
+ "version": "1.42.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.42.1.tgz",
+ "integrity": "sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==",
"dev": true,
"requires": {
"fsevents": "2.3.2",
- "playwright-core": "1.41.2"
+ "playwright-core": "1.42.1"
}
},
"playwright-core": {
- "version": "1.41.2",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.41.2.tgz",
- "integrity": "sha512-VaTvwCA4Y8kxEe+kfm2+uUUw5Lubf38RxF7FpBxLPmGe5sdNkSg5e3ChEigaGrX7qdqT3pt2m/98LiyvU2x6CA==",
+ "version": "1.42.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.42.1.tgz",
+ "integrity": "sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==",
"dev": true
},
"postcss": {
diff --git a/core/package.json b/core/package.json
index 3d03c1e848..de786370d6 100644
--- a/core/package.json
+++ b/core/package.json
@@ -94,7 +94,12 @@
"test.e2e.update-snapshots": "npm run test.e2e -- --update-snapshots",
"test.watch": "jest --watch --no-cache",
"test.treeshake": "node scripts/treeshaking.js dist/index.js",
- "validate": "npm run lint && npm run test && npm run build && npm run test.treeshake"
+ "validate": "npm run lint && npm run test && npm run build && npm run test.treeshake",
+ "docker.build": "docker build -t ionic-playwright .",
+ "test.e2e.docker": "npm run docker.build && docker run -it --rm -e DISPLAY=$(cat docker-display.txt) -v $(cat docker-display-volume.txt) --ipc=host --mount=type=bind,source=./,target=/ionic ionic-playwright npm run test.e2e --",
+ "test.e2e.docker.update-snapshots": "npm run test.e2e.docker -- --update-snapshots",
+ "test.e2e.docker.ci": "npm run docker.build && docker run -e CI='true' --rm --ipc=host --mount=type=bind,source=./,target=/ionic ionic-playwright npm run test.e2e --",
+ "test.report": "npx playwright show-report"
},
"author": "Ionic Team",
"license": "MIT",
diff --git a/core/src/components.d.ts b/core/src/components.d.ts
index 61f44b7dc1..de24def6b8 100644
--- a/core/src/components.d.ts
+++ b/core/src/components.d.ts
@@ -3179,7 +3179,7 @@ export namespace Components {
}
interface IonToggle {
/**
- * How to control the alignment of the toggle and label on the cross axis. ``"start"`: The label and control will appear on the left of the cross axis in LTR, and on the right side in RTL. `"center"`: The label and control will appear at the center of the cross axis in both LTR and RTL.
+ * How to control the alignment of the toggle and label on the cross axis. `"start"`: The label and control will appear on the left of the cross axis in LTR, and on the right side in RTL. `"center"`: The label and control will appear at the center of the cross axis in both LTR and RTL.
*/
"alignment": 'start' | 'center';
/**
@@ -7953,7 +7953,7 @@ declare namespace LocalJSX {
}
interface IonToggle {
/**
- * How to control the alignment of the toggle and label on the cross axis. ``"start"`: The label and control will appear on the left of the cross axis in LTR, and on the right side in RTL. `"center"`: The label and control will appear at the center of the cross axis in both LTR and RTL.
+ * How to control the alignment of the toggle and label on the cross axis. `"start"`: The label and control will appear on the left of the cross axis in LTR, and on the right side in RTL. `"center"`: The label and control will appear at the center of the cross axis in both LTR and RTL.
*/
"alignment"?: 'start' | 'center';
/**
diff --git a/core/src/components/alert/test/a11y/alert.e2e.ts b/core/src/components/alert/test/a11y/alert.e2e.ts
index 845476d309..65d579c898 100644
--- a/core/src/components/alert/test/a11y/alert.e2e.ts
+++ b/core/src/components/alert/test/a11y/alert.e2e.ts
@@ -306,6 +306,10 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
await alert.evaluate((el: HTMLIonAlertElement) => el.present());
await ionAlertDidPresent.next();
+ /**
+ * The borders on the text fields may not be visible in the screenshot
+ * when using Safari, this is due to a WebKit rendering quirk.
+ */
await expect(page).toHaveScreenshot(screenshot(`alert-text-fields-scale`));
});
});
diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx
index a9c9ea7b84..b11e949c4f 100644
--- a/core/src/components/datetime/datetime.tsx
+++ b/core/src/components/datetime/datetime.tsx
@@ -107,6 +107,7 @@ export class Datetime implements ComponentInterface {
private inputId = `ion-dt-${datetimeIds++}`;
private calendarBodyRef?: HTMLElement;
private popoverRef?: HTMLIonPopoverElement;
+ private intersectionTrackerRef?: HTMLElement;
private clearFocusVisible?: () => void;
private parsedMinuteValues?: number[];
private parsedHourValues?: number[];
@@ -1078,6 +1079,8 @@ export class Datetime implements ComponentInterface {
}
componentDidLoad() {
+ const { el, intersectionTrackerRef } = this;
+
/**
* If a scrollable element is hidden using `display: none`,
* it will not have a scroll height meaning we cannot scroll elements
@@ -1105,7 +1108,7 @@ export class Datetime implements ComponentInterface {
this.el.classList.add('datetime-ready');
});
};
- const visibleIO = new IntersectionObserver(visibleCallback, { threshold: 0.01 });
+ const visibleIO = new IntersectionObserver(visibleCallback, { threshold: 0.01, root: el });
/**
* Use raf to avoid a race condition between the component loading and
@@ -1113,7 +1116,7 @@ export class Datetime implements ComponentInterface {
* could cause the datetime to start at a visibility of 0, erroneously
* triggering the `hiddenIO` observer below.
*/
- raf(() => visibleIO?.observe(this.el));
+ raf(() => visibleIO?.observe(intersectionTrackerRef!));
/**
* We need to clean up listeners when the datetime is hidden
@@ -1143,8 +1146,8 @@ export class Datetime implements ComponentInterface {
this.el.classList.remove('datetime-ready');
});
};
- const hiddenIO = new IntersectionObserver(hiddenCallback, { threshold: 0 });
- raf(() => hiddenIO?.observe(this.el));
+ const hiddenIO = new IntersectionObserver(hiddenCallback, { threshold: 0, root: el });
+ raf(() => hiddenIO?.observe(intersectionTrackerRef!));
/**
* Datetime uses Ionic components that emit
@@ -2621,6 +2624,20 @@ export class Datetime implements ComponentInterface {
}),
}}
>
+ {/*
+ WebKit has a quirk where IntersectionObserver callbacks are delayed until after
+ an accelerated animation finishes if the "root" specified in the config is the
+ browser viewport (the default behavior if "root" is not specified). This means
+ that when presenting a datetime in a modal on iOS the calendar body appears
+ blank until the modal animation finishes.
+
+ We can work around this by observing .intersection-tracker and using the host
+ (ion-datetime) as the "root". This allows the IO callback to fire the moment
+ the datetime is visible. The .intersection-tracker element should not have
+ dimensions or additional styles, and it should not be positioned absolutely
+ otherwise the IO callback may fire at unexpected times.
+ */}
+ (this.intersectionTrackerRef = el)}>
{this.renderDatetime(mode)}
);
diff --git a/core/src/components/grid/test/basic/grid.e2e.ts-snapshots/grid-basic-md-ltr-Mobile-Firefox-linux.png b/core/src/components/grid/test/basic/grid.e2e.ts-snapshots/grid-basic-md-ltr-Mobile-Firefox-linux.png
index d8da3db436..7cac4c5982 100644
Binary files a/core/src/components/grid/test/basic/grid.e2e.ts-snapshots/grid-basic-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/grid/test/basic/grid.e2e.ts-snapshots/grid-basic-md-ltr-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/grid/test/basic/grid.e2e.ts-snapshots/grid-basic-md-rtl-Mobile-Firefox-linux.png b/core/src/components/grid/test/basic/grid.e2e.ts-snapshots/grid-basic-md-rtl-Mobile-Firefox-linux.png
index 08bf93cb06..bbbb81fc5c 100644
Binary files a/core/src/components/grid/test/basic/grid.e2e.ts-snapshots/grid-basic-md-rtl-Mobile-Firefox-linux.png and b/core/src/components/grid/test/basic/grid.e2e.ts-snapshots/grid-basic-md-rtl-Mobile-Firefox-linux.png differ
diff --git a/core/src/components/header/header.ios.scss b/core/src/components/header/header.ios.scss
index 634303ff8e..cda084da7c 100644
--- a/core/src/components/header/header.ios.scss
+++ b/core/src/components/header/header.ios.scss
@@ -132,8 +132,11 @@
* We use opacity: 0 to avoid a layout shift.
* We target both the attribute and the class in the event that the attribute
* is not reflected on the host in some frameworks.
+ *
+ * Both headers should be scoped to iOS mode otherwise an MD app that uses an
+ * iOS header may cause other MD headers to be unexpectedly hidden.
*/
-ion-header:not(.header-collapse-main):has(~ ion-content ion-header[collapse="condense"],
-~ ion-content ion-header.header-collapse-condense) {
+ion-header.header-ios:not(.header-collapse-main):has(~ ion-content ion-header.header-ios[collapse="condense"],
+~ ion-content ion-header.header-ios.header-collapse-condense) {
opacity: 0;
}
diff --git a/core/src/components/header/test/basic/header.e2e.ts b/core/src/components/header/test/basic/header.e2e.ts
index 5550bc575d..67f3be9217 100644
--- a/core/src/components/header/test/basic/header.e2e.ts
+++ b/core/src/components/header/test/basic/header.e2e.ts
@@ -85,3 +85,91 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, c
});
});
});
+
+/**
+ * This test only impacts MD applications
+ */
+configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
+ test.describe(title('header: translucent'), () => {
+ test('should not hide MD headers when using a descendant iOS header in an MD app', async ({ page }) => {
+ test.info().annotations.push({
+ type: 'issue',
+ description: 'https://github.com/ionic-team/ionic-framework/issues/28867',
+ });
+ await page.setContent(
+ `
+