diff --git a/.github/COMPONENT-GUIDE.md b/.github/COMPONENT-GUIDE.md index 28206dc9ab..c4f4979880 100644 --- a/.github/COMPONENT-GUIDE.md +++ b/.github/COMPONENT-GUIDE.md @@ -2,10 +2,10 @@ - [Button States](#button-states) * [Component Structure](#component-structure) - * [Activated](#activated) * [Disabled](#disabled) * [Focused](#focused) * [Hover](#hover) + * [Activated](#activated) * [Ripple Effect](#ripple-effect) * [Example Components](#example-components) * [References](#references) @@ -21,7 +21,7 @@ ## Button States -Any component that renders a button should have the following states: [`activated`](#activated), [`disabled`](#disabled), [`focused`](#focused), [`hover`](#hover). It should also have a [Ripple Effect](#ripple-effect) component added for Material Design. +Any component that renders a button should have the following states: [`disabled`](#disabled), [`focused`](#focused), [`hover`](#hover), [`activated`](#activated). It should also have a [Ripple Effect](#ripple-effect) component added for Material Design. ### Component Structure @@ -89,78 +89,6 @@ The following styles should be set for the CSS to work properly. Note that the ` ``` -### Activated - -The activated state should be enabled for elements with actions on "press". It usually changes the opacity or background of an element. - -> [!WARNING] ->`:active` should not be used here as it is not received on mobile Safari unless the element has a `touchstart` listener (which we don't necessarily want to have to add to every element). From [Safari Web Content Guide](https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/AdjustingtheTextSize/AdjustingtheTextSize.html): -> ->> On iOS, mouse events are sent so quickly that the down or active state is never received. Therefore, the `:active` pseudo state is triggered only when there is a touch event set on the HTML element - -> Make sure the component has the correct [component structure](#component-structure) before continuing. - -#### JavaScript - -The `ion-activatable` class needs to be set on an element that can be activated: - -```jsx -render() { - return ( - - - - ); -} -``` - -Once that is done, the element will get the `ion-activated` class added on press after a small delay. This delay exists so that the active state does not show up when an activatable element is tapped while scrolling. - -In addition to setting that class, `ion-activatable-instant` can be set in order to have an instant press with no delay: - -```jsx - -``` - -#### 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); - } -} -``` - -> Order is important! 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; -} -``` - - ### Disabled The disabled state should be set via prop on all components that render a native button. Setting a disabled state will change the opacity or color of the button and remove click events from firing. @@ -197,7 +125,8 @@ render() { } ``` -> Note: if the class being added was for `ion-back-button` it would be `back-button-disabled`. +> [!NOTE] +> If the class being added was for `ion-back-button` it would be `back-button-disabled`. #### CSS @@ -215,9 +144,10 @@ The following CSS _at the bare minimum_ should be added for the disabled class, TODO + ### Focused -The focused state should be enabled for elements with actions when tabbed to via the keyboard. This will only work inside of an `ion-app`. It usually changes the opacity or background of an element. +The focused state should be enabled for elements with actions when tabbed to via the keyboard. This will only work inside of an `ion-app`. It usually changes the opacity or background of an element. > [!WARNING] > Do not use `:focus` because that will cause the focus to apply even when an element is tapped (because the element is now focused). Instead, we only want the focus state to be shown when it makes sense which is what the `.ion-focusable` utility mentioned below does. @@ -225,6 +155,7 @@ The focused state should be enabled for elements with actions when tabbed to via > [!NOTE] > The [`:focus-visible`](https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible) pseudo-class mostly does the same thing as our JavaScript-driven utility. However, it does not work well with Shadow DOM components as the element that receives focus is typically inside of the Shadow DOM, but we usually want to set the `:focus-visible` state on the host so we can style other parts of the component. Using other combinations such as `:has(:focus-visible)` does not work because `:has` does not pierce the Shadow DOM (as that would leak implementation details about the Shadow DOM contents). `:focus-within` does work with the Shadow DOM, but that has the same problem as `:focus` that was mentioned before. Unfortunately, a [`:focus-visible-within` pseudo-class does not exist yet](https://github.com/WICG/focus-visible/issues/151). +> [!IMPORTANT] > Make sure the component has the correct [component structure](#component-structure) before continuing. #### JavaScript @@ -234,7 +165,7 @@ The `ion-focusable` class needs to be set on an element that can be focused: ```jsx render() { return ( - + ); @@ -269,7 +200,8 @@ Style the `ion-focused` class based on the spec for that element: } ``` -> Order is important! Focused should be after the activated and before the hover state. +> [!IMPORTANT] +> Order matters! Focused should be **before** the activated and hover states. #### User Customization @@ -286,11 +218,12 @@ ion-button { ### Hover -The [hover state](https://developer.mozilla.org/en-US/docs/Web/CSS/:hover) happens when a user moves their cursor on top of an element without pressing on it. It should not happen on mobile, only on desktop devices that support hover. +The [hover state](https://developer.mozilla.org/en-US/docs/Web/CSS/:hover) happens when a user moves their cursor on top of an element without pressing on it. It should not happen on mobile, only on desktop devices that support hover. > [!NOTE] > Some Android devices [incorrectly report their inputs](https://issues.chromium.org/issues/40855702) which can result in certain devices receiving hover events when they should not. +> [!IMPORTANT] > Make sure the component has the correct [component structure](#component-structure) before continuing. #### CSS @@ -321,7 +254,8 @@ Style the `:hover` based on the spec for that element: } ``` -> Order is important! Hover should be before the activated state. +> [!IMPORTANT] +> Order matters! Hover should be **before** the activated state. #### User Customization @@ -336,6 +270,79 @@ ion-button { ``` +### Activated + +The activated state should be enabled for elements with actions on "press". It usually changes the opacity or background of an element. + +> [!WARNING] +>`:active` should not be used here as it is not received on mobile Safari unless the element has a `touchstart` listener (which we don't necessarily want to have to add to every element). From [Safari Web Content Guide](https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/AdjustingtheTextSize/AdjustingtheTextSize.html): +> +>> On iOS, mouse events are sent so quickly that the down or active state is never received. Therefore, the `:active` pseudo state is triggered only when there is a touch event set on the HTML element + +> [!IMPORTANT] +> Make sure the component has the correct [component structure](#component-structure) before continuing. + +#### JavaScript + +The `ion-activatable` class needs to be set on an element that can be activated: + +```jsx +render() { + return ( + + + + ); +} +``` + +Once that is done, the element will get the `ion-activated` class added on press after a small delay. This delay exists so that the active state does not show up when an activatable element is tapped while scrolling. + +In addition to setting that class, `ion-activatable-instant` can be set in order to have an instant press with no delay: + +```jsx + +``` + +#### 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( + ` + + + Header + + + + + + Header + + + + + + Welcome + + + + `, + config + ); + + const header = page.locator('ion-header#main-header'); + + /** + * The existence of the iOS header in an MD app should not cause the main MD header + * to be hidden. We do not have toHaveVisible because the behavior that hides + * the header under correct circumstances does it using opacity: 0. + * Playwright considers an element with opacity: 0 to still be visible + * because it has a non-zero bounding box. + */ + await expect(header).toHaveScreenshot(screenshot('header-md-visibility-ios-descendant')); + }); + test('should not hide MD headers when using a root 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( + ` + + + Header + + + + + + Header + + + + + + Welcome + + + + `, + config + ); + + const header = page.locator('ion-header#main-header'); + + /** + * The existence of the iOS header in an MD app should not cause the main MD header + * to be hidden. We do not have toHaveVisible because the behavior that hides + * the header under correct circumstances does it using opacity: 0. + * Playwright considers an element with opacity: 0 to still be visible + * because it has a non-zero bounding box. + */ + await expect(header).toHaveScreenshot(screenshot('header-md-visibility-ios-main')); + }); + }); +}); diff --git a/core/src/components/header/test/basic/header.e2e.ts-snapshots/header-md-visibility-ios-descendant-md-ltr-Mobile-Chrome-linux.png b/core/src/components/header/test/basic/header.e2e.ts-snapshots/header-md-visibility-ios-descendant-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 0000000000..06b4e5e7c7 Binary files /dev/null and b/core/src/components/header/test/basic/header.e2e.ts-snapshots/header-md-visibility-ios-descendant-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/header/test/basic/header.e2e.ts-snapshots/header-md-visibility-ios-descendant-md-ltr-Mobile-Firefox-linux.png b/core/src/components/header/test/basic/header.e2e.ts-snapshots/header-md-visibility-ios-descendant-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 0000000000..6f6ca4833e Binary files /dev/null and b/core/src/components/header/test/basic/header.e2e.ts-snapshots/header-md-visibility-ios-descendant-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/header/test/basic/header.e2e.ts-snapshots/header-md-visibility-ios-descendant-md-ltr-Mobile-Safari-linux.png b/core/src/components/header/test/basic/header.e2e.ts-snapshots/header-md-visibility-ios-descendant-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 0000000000..2a12565288 Binary files /dev/null and b/core/src/components/header/test/basic/header.e2e.ts-snapshots/header-md-visibility-ios-descendant-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/header/test/basic/header.e2e.ts-snapshots/header-md-visibility-ios-main-md-ltr-Mobile-Chrome-linux.png b/core/src/components/header/test/basic/header.e2e.ts-snapshots/header-md-visibility-ios-main-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 0000000000..dab34d6786 Binary files /dev/null and b/core/src/components/header/test/basic/header.e2e.ts-snapshots/header-md-visibility-ios-main-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/header/test/basic/header.e2e.ts-snapshots/header-md-visibility-ios-main-md-ltr-Mobile-Firefox-linux.png b/core/src/components/header/test/basic/header.e2e.ts-snapshots/header-md-visibility-ios-main-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 0000000000..5b9c0f573d Binary files /dev/null and b/core/src/components/header/test/basic/header.e2e.ts-snapshots/header-md-visibility-ios-main-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/header/test/basic/header.e2e.ts-snapshots/header-md-visibility-ios-main-md-ltr-Mobile-Safari-linux.png b/core/src/components/header/test/basic/header.e2e.ts-snapshots/header-md-visibility-ios-main-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 0000000000..83c2b8a50e Binary files /dev/null and b/core/src/components/header/test/basic/header.e2e.ts-snapshots/header-md-visibility-ios-main-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/header/test/condense/header.e2e.ts-snapshots/header-condense-large-title-collapsed-diff-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/header/test/condense/header.e2e.ts-snapshots/header-condense-large-title-collapsed-diff-ios-ltr-Mobile-Chrome-linux.png index 39c8b4e1c8..67bfa600ad 100644 Binary files a/core/src/components/header/test/condense/header.e2e.ts-snapshots/header-condense-large-title-collapsed-diff-ios-ltr-Mobile-Chrome-linux.png and b/core/src/components/header/test/condense/header.e2e.ts-snapshots/header-condense-large-title-collapsed-diff-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/item/test/buttons/index.html b/core/src/components/item/test/buttons/index.html index d28eafee33..fb8d5449ca 100644 --- a/core/src/components/item/test/buttons/index.html +++ b/core/src/components/item/test/buttons/index.html @@ -12,165 +12,6 @@ - - @@ -347,12 +188,6 @@