chore(): sync with main

This commit is contained in:
Liam DeBeasi
2023-03-24 14:37:39 -04:00
79 changed files with 653 additions and 65 deletions

View File

@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [6.7.0](https://github.com/ionic-team/ionic-framework/compare/v6.6.3...v6.7.0) (2023-03-23)
### Features
* **config:** add option to disable custom html functionality ([#26956](https://github.com/ionic-team/ionic-framework/issues/26956)) ([3b0af7c](https://github.com/ionic-team/ionic-framework/commit/3b0af7c55d4fa039be33d6605414761494d5af8f))
# [7.0.0-rc.3](https://github.com/ionic-team/ionic-framework/compare/v7.0.0-rc.2...v7.0.0-rc.3) (2023-03-22)
**Note:** Version bump only for package ionic-framework
@ -13,6 +24,12 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
## [6.6.3](https://github.com/ionic-team/ionic-framework/compare/v6.6.2...v6.6.3) (2023-03-22)
**Note:** Version bump only for package ionic-framework
### Bug Fixes
* **menu:** main content is not scrollable while swiping ([#26976](https://github.com/ionic-team/ionic-framework/issues/26976)) ([88bd8a4](https://github.com/ionic-team/ionic-framework/commit/88bd8a47c5e844d1d3a2b3b13621826faf776afb)), closes [#21193](https://github.com/ionic-team/ionic-framework/issues/21193)

View File

@ -78,8 +78,12 @@ Already have an Ionic app? These guides will help you migrate to the latest vers
### Examples
The [Ionic Conference App](https://github.com/ionic-team/ionic-conference-app) is a full featured Ionic app.
It is the perfect starting point for learning and building your own app.
The Ionic Conference App is a full featured Ionic app. It is the perfect starting point for learning and building your own app.
- [Angular Ionic Conference App](https://github.com/ionic-team/ionic-conference-app)
- [React Ionic Conference App](https://github.com/ionic-team/ionic-react-conference-app)
<!-- TODO(FW-3811): add this when the vue conference app is updated -->
<!-- - [Vue Ionic Conference App](https://github.com/ionic-team/ionic-vue-conference-app) -->
### Contributing

View File

@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [6.7.0](https://github.com/ionic-team/ionic/compare/v6.6.3...v6.7.0) (2023-03-23)
**Note:** Version bump only for package @ionic/angular
# [7.0.0-rc.3](https://github.com/ionic-team/ionic/compare/v7.0.0-rc.2...v7.0.0-rc.3) (2023-03-22)
**Note:** Version bump only for package @ionic/angular

View File

@ -2,6 +2,18 @@
Ionic Framework supports multiple versions of Angular. As a result, we need to verify that Ionic works correctly with each of these Angular versions.
## Syncing Local Changes
The Angular test app supports syncing your locally built changes for validation.
1. Build the `core` and `packages/angular` projects using `npm run build`.
2. [Build the Angular test app](#test-app-build-structure).
3. Navigate to the built test app.
4. Install dependencies using `npm install`.
5. Sync your local changes using `npm run sync`.
From here you can either build the application or start a local dev server. When re-syncing changes, you will need to [wipe or disable the application cache](#application-cache).
## Application Cache
Angular CLI creates a cache of several files on disk by default in the `.angular` directory. This decreases the time taken to build the test application. However, the cache makes it difficult to quickly sync and check local changes of Ionic. As a result, the `.angular` cache is disabled by default in the test app projects.

View File

@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [6.7.0](https://github.com/ionic-team/ionic/compare/v6.6.3...v6.7.0) (2023-03-23)
### Features
* **config:** add option to disable custom html functionality ([#26956](https://github.com/ionic-team/ionic/issues/26956)) ([3b0af7c](https://github.com/ionic-team/ionic/commit/3b0af7c55d4fa039be33d6605414761494d5af8f))
# [7.0.0-rc.3](https://github.com/ionic-team/ionic/compare/v7.0.0-rc.2...v7.0.0-rc.3) (2023-03-22)
**Note:** Version bump only for package @ionic/core

46
core/package-lock.json generated
View File

@ -18,7 +18,7 @@
"@ionic/eslint-config": "^0.3.0",
"@ionic/prettier-config": "^2.0.0",
"@jest/core": "^27.5.1",
"@playwright/test": "^1.31.1",
"@playwright/test": "^1.32.0",
"@rollup/plugin-node-resolve": "^8.4.0",
"@rollup/plugin-virtual": "^2.0.3",
"@stencil/angular-output-target": "^0.5.0",
@ -1501,13 +1501,13 @@
}
},
"node_modules/@playwright/test": {
"version": "1.31.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.31.2.tgz",
"integrity": "sha512-BYVutxDI4JeZKV1+ups6dt5WiqKhjBtIYowyZIJ3kBDmJgsuPKsqqKNIMFbUePLSCmp2cZu+BDL427RcNKTRYw==",
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.32.0.tgz",
"integrity": "sha512-zOdGloaF0jeec7hqoLqM5S3L2rR4WxMJs6lgiAeR70JlH7Ml54ZPoIIf3X7cvnKde3Q9jJ/gaxkFh8fYI9s1rg==",
"dev": true,
"dependencies": {
"@types/node": "*",
"playwright-core": "1.31.2"
"playwright-core": "1.32.0"
},
"bin": {
"playwright": "cli.js"
@ -8150,14 +8150,14 @@
}
},
"node_modules/playwright": {
"version": "1.31.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.31.2.tgz",
"integrity": "sha512-jpC47n2PKQNtzB7clmBuWh6ftBRS/Bt5EGLigJ9k2QAKcNeYXZkEaDH5gmvb6+AbcE0DO6GnXdbl9ogG6Eh+og==",
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.32.0.tgz",
"integrity": "sha512-zQVzxTGoC/ak2Zu0l3CeBGQ6z5oOka5ecUOk+5QbmAerih6CaVsjY9BjjhiN+UOImd3xLiNeCcmLEWcXlz1Dlg==",
"dev": true,
"hasInstallScript": true,
"peer": true,
"dependencies": {
"playwright-core": "1.31.2"
"playwright-core": "1.32.0"
},
"bin": {
"playwright": "cli.js"
@ -8167,9 +8167,9 @@
}
},
"node_modules/playwright-core": {
"version": "1.31.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.31.2.tgz",
"integrity": "sha512-a1dFgCNQw4vCsG7bnojZjDnPewZcw7tZUNFN0ZkcLYKj+mPmXvg4MpaaKZ5SgqPsOmqIf2YsVRkgqiRDxD+fDQ==",
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.32.0.tgz",
"integrity": "sha512-Z9Ij17X5Z3bjpp6XKujGBp9Gv4eViESac9aDmwgQFUEJBW0K80T21m/Z+XJQlu4cNsvPygw33b6V1Va6Bda5zQ==",
"dev": true,
"bin": {
"playwright": "cli.js"
@ -11398,14 +11398,14 @@
}
},
"@playwright/test": {
"version": "1.31.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.31.2.tgz",
"integrity": "sha512-BYVutxDI4JeZKV1+ups6dt5WiqKhjBtIYowyZIJ3kBDmJgsuPKsqqKNIMFbUePLSCmp2cZu+BDL427RcNKTRYw==",
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.32.0.tgz",
"integrity": "sha512-zOdGloaF0jeec7hqoLqM5S3L2rR4WxMJs6lgiAeR70JlH7Ml54ZPoIIf3X7cvnKde3Q9jJ/gaxkFh8fYI9s1rg==",
"dev": true,
"requires": {
"@types/node": "*",
"fsevents": "2.3.2",
"playwright-core": "1.31.2"
"playwright-core": "1.32.0"
}
},
"@rollup/plugin-node-resolve": {
@ -16275,19 +16275,19 @@
}
},
"playwright": {
"version": "1.31.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.31.2.tgz",
"integrity": "sha512-jpC47n2PKQNtzB7clmBuWh6ftBRS/Bt5EGLigJ9k2QAKcNeYXZkEaDH5gmvb6+AbcE0DO6GnXdbl9ogG6Eh+og==",
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.32.0.tgz",
"integrity": "sha512-zQVzxTGoC/ak2Zu0l3CeBGQ6z5oOka5ecUOk+5QbmAerih6CaVsjY9BjjhiN+UOImd3xLiNeCcmLEWcXlz1Dlg==",
"dev": true,
"peer": true,
"requires": {
"playwright-core": "1.31.2"
"playwright-core": "1.32.0"
}
},
"playwright-core": {
"version": "1.31.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.31.2.tgz",
"integrity": "sha512-a1dFgCNQw4vCsG7bnojZjDnPewZcw7tZUNFN0ZkcLYKj+mPmXvg4MpaaKZ5SgqPsOmqIf2YsVRkgqiRDxD+fDQ==",
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.32.0.tgz",
"integrity": "sha512-Z9Ij17X5Z3bjpp6XKujGBp9Gv4eViESac9aDmwgQFUEJBW0K80T21m/Z+XJQlu4cNsvPygw33b6V1Va6Bda5zQ==",
"dev": true
},
"postcss": {

View File

@ -40,7 +40,7 @@
"@ionic/eslint-config": "^0.3.0",
"@ionic/prettier-config": "^2.0.0",
"@jest/core": "^27.5.1",
"@playwright/test": "^1.31.1",
"@playwright/test": "^1.32.0",
"@rollup/plugin-node-resolve": "^8.4.0",
"@rollup/plugin-virtual": "^2.0.3",
"@stencil/angular-output-target": "^0.5.0",

View File

@ -272,7 +272,7 @@ export namespace Components {
*/
"leaveAnimation"?: AnimationBuilder;
/**
* The main message to be displayed in the alert. `message` can accept either plaintext or HTML as a string. To display characters normally reserved for HTML, they must be escaped. For example `<Ionic>` would become `&lt;Ionic&gt;` For more information: [Security Documentation](https://ionicframework.com/docs/faq/security)
* The main message to be displayed in the alert. `message` can accept either plaintext or HTML as a string. To display characters normally reserved for HTML, they must be escaped. For example `<Ionic>` would become `&lt;Ionic&gt;` For more information: [Security Documentation](https://ionicframework.com/docs/faq/security) This property accepts custom HTML as a string. Developers who only want to pass plain text can disable the custom HTML functionality by setting `innerHTMLTemplatesEnabled: false` in the Ionic config.
*/
"message"?: string | IonicSafeString;
/**
@ -1135,7 +1135,7 @@ export namespace Components {
*/
"loadingSpinner"?: SpinnerTypes | null;
/**
* Optional text to display while loading. `loadingText` can accept either plaintext or HTML as a string. To display characters normally reserved for HTML, they must be escaped. For example `<Ionic>` would become `&lt;Ionic&gt;` For more information: [Security Documentation](https://ionicframework.com/docs/faq/security)
* Optional text to display while loading. `loadingText` can accept either plaintext or HTML as a string. To display characters normally reserved for HTML, they must be escaped. For example `<Ionic>` would become `&lt;Ionic&gt;` For more information: [Security Documentation](https://ionicframework.com/docs/faq/security) This property accepts custom HTML as a string. Developers who only want to pass plain text can disable the custom HTML functionality by setting `innerHTMLTemplatesEnabled: false` in the Ionic config.
*/
"loadingText"?: string | IonicSafeString;
}
@ -1554,7 +1554,7 @@ export namespace Components {
*/
"leaveAnimation"?: AnimationBuilder;
/**
* Optional text content to display in the loading indicator.
* Optional text content to display in the loading indicator. This property accepts custom HTML as a string. Developers who only want to pass plain text can disable the custom HTML functionality by setting `innerHTMLTemplatesEnabled: false` in the Ionic config.
*/
"message"?: string | IonicSafeString;
/**
@ -2382,7 +2382,7 @@ export namespace Components {
*/
"pullingIcon"?: SpinnerTypes | string | null;
/**
* The text you want to display when you begin to pull down. `pullingText` can accept either plaintext or HTML as a string. To display characters normally reserved for HTML, they must be escaped. For example `<Ionic>` would become `&lt;Ionic&gt;` For more information: [Security Documentation](https://ionicframework.com/docs/faq/security)
* The text you want to display when you begin to pull down. `pullingText` can accept either plaintext or HTML as a string. To display characters normally reserved for HTML, they must be escaped. For example `<Ionic>` would become `&lt;Ionic&gt;` For more information: [Security Documentation](https://ionicframework.com/docs/faq/security) This property accepts custom HTML as a string. Developers who only want to pass plain text can disable the custom HTML functionality by setting `innerHTMLTemplatesEnabled: false` in the Ionic config.
*/
"pullingText"?: string | IonicSafeString;
/**
@ -2390,7 +2390,7 @@ export namespace Components {
*/
"refreshingSpinner"?: SpinnerTypes | null;
/**
* The text you want to display when performing a refresh. `refreshingText` can accept either plaintext or HTML as a string. To display characters normally reserved for HTML, they must be escaped. For example `<Ionic>` would become `&lt;Ionic&gt;` For more information: [Security Documentation](https://ionicframework.com/docs/faq/security)
* The text you want to display when performing a refresh. `refreshingText` can accept either plaintext or HTML as a string. To display characters normally reserved for HTML, they must be escaped. For example `<Ionic>` would become `&lt;Ionic&gt;` For more information: [Security Documentation](https://ionicframework.com/docs/faq/security) This property accepts custom HTML as a string. Developers who only want to pass plain text can disable the custom HTML functionality by setting `innerHTMLTemplatesEnabled: false` in the Ionic config.
*/
"refreshingText"?: string | IonicSafeString;
}
@ -3118,7 +3118,7 @@ export namespace Components {
*/
"leaveAnimation"?: AnimationBuilder;
/**
* Message to be shown in the toast.
* Message to be shown in the toast. This property accepts custom HTML as a string. Developers who only want to pass plain text can disable the custom HTML functionality by setting `innerHTMLTemplatesEnabled: false` in the Ionic config.
*/
"message"?: string | IonicSafeString;
/**
@ -4235,7 +4235,7 @@ declare namespace LocalJSX {
*/
"leaveAnimation"?: AnimationBuilder;
/**
* The main message to be displayed in the alert. `message` can accept either plaintext or HTML as a string. To display characters normally reserved for HTML, they must be escaped. For example `<Ionic>` would become `&lt;Ionic&gt;` For more information: [Security Documentation](https://ionicframework.com/docs/faq/security)
* The main message to be displayed in the alert. `message` can accept either plaintext or HTML as a string. To display characters normally reserved for HTML, they must be escaped. For example `<Ionic>` would become `&lt;Ionic&gt;` For more information: [Security Documentation](https://ionicframework.com/docs/faq/security) This property accepts custom HTML as a string. Developers who only want to pass plain text can disable the custom HTML functionality by setting `innerHTMLTemplatesEnabled: false` in the Ionic config.
*/
"message"?: string | IonicSafeString;
/**
@ -5169,7 +5169,7 @@ declare namespace LocalJSX {
*/
"loadingSpinner"?: SpinnerTypes | null;
/**
* Optional text to display while loading. `loadingText` can accept either plaintext or HTML as a string. To display characters normally reserved for HTML, they must be escaped. For example `<Ionic>` would become `&lt;Ionic&gt;` For more information: [Security Documentation](https://ionicframework.com/docs/faq/security)
* Optional text to display while loading. `loadingText` can accept either plaintext or HTML as a string. To display characters normally reserved for HTML, they must be escaped. For example `<Ionic>` would become `&lt;Ionic&gt;` For more information: [Security Documentation](https://ionicframework.com/docs/faq/security) This property accepts custom HTML as a string. Developers who only want to pass plain text can disable the custom HTML functionality by setting `innerHTMLTemplatesEnabled: false` in the Ionic config.
*/
"loadingText"?: string | IonicSafeString;
}
@ -5584,7 +5584,7 @@ declare namespace LocalJSX {
*/
"leaveAnimation"?: AnimationBuilder;
/**
* Optional text content to display in the loading indicator.
* Optional text content to display in the loading indicator. This property accepts custom HTML as a string. Developers who only want to pass plain text can disable the custom HTML functionality by setting `innerHTMLTemplatesEnabled: false` in the Ionic config.
*/
"message"?: string | IonicSafeString;
/**
@ -6421,7 +6421,7 @@ declare namespace LocalJSX {
*/
"pullingIcon"?: SpinnerTypes | string | null;
/**
* The text you want to display when you begin to pull down. `pullingText` can accept either plaintext or HTML as a string. To display characters normally reserved for HTML, they must be escaped. For example `<Ionic>` would become `&lt;Ionic&gt;` For more information: [Security Documentation](https://ionicframework.com/docs/faq/security)
* The text you want to display when you begin to pull down. `pullingText` can accept either plaintext or HTML as a string. To display characters normally reserved for HTML, they must be escaped. For example `<Ionic>` would become `&lt;Ionic&gt;` For more information: [Security Documentation](https://ionicframework.com/docs/faq/security) This property accepts custom HTML as a string. Developers who only want to pass plain text can disable the custom HTML functionality by setting `innerHTMLTemplatesEnabled: false` in the Ionic config.
*/
"pullingText"?: string | IonicSafeString;
/**
@ -6429,7 +6429,7 @@ declare namespace LocalJSX {
*/
"refreshingSpinner"?: SpinnerTypes | null;
/**
* The text you want to display when performing a refresh. `refreshingText` can accept either plaintext or HTML as a string. To display characters normally reserved for HTML, they must be escaped. For example `<Ionic>` would become `&lt;Ionic&gt;` For more information: [Security Documentation](https://ionicframework.com/docs/faq/security)
* The text you want to display when performing a refresh. `refreshingText` can accept either plaintext or HTML as a string. To display characters normally reserved for HTML, they must be escaped. For example `<Ionic>` would become `&lt;Ionic&gt;` For more information: [Security Documentation](https://ionicframework.com/docs/faq/security) This property accepts custom HTML as a string. Developers who only want to pass plain text can disable the custom HTML functionality by setting `innerHTMLTemplatesEnabled: false` in the Ionic config.
*/
"refreshingText"?: string | IonicSafeString;
}
@ -7214,7 +7214,7 @@ declare namespace LocalJSX {
*/
"leaveAnimation"?: AnimationBuilder;
/**
* Message to be shown in the toast.
* Message to be shown in the toast. This property accepts custom HTML as a string. Developers who only want to pass plain text can disable the custom HTML functionality by setting `innerHTMLTemplatesEnabled: false` in the Ionic config.
*/
"message"?: string | IonicSafeString;
/**

View File

@ -1,8 +1,10 @@
import type { ComponentInterface, EventEmitter } from '@stencil/core';
import { Component, Element, Event, Host, Listen, Method, Prop, Watch, forceUpdate, h } from '@stencil/core';
import { config } from '../../global/config';
import { getIonMode } from '../../global/ionic-global';
import type { AnimationBuilder, CssClassMap, OverlayInterface, FrameworkDelegate } from '../../interface';
import { ENABLE_HTML_CONTENT_DEFAULT } from '../../utils/config';
import type { Gesture } from '../../utils/gesture';
import { createButtonActiveGesture } from '../../utils/gesture/button-active';
import {
@ -43,7 +45,7 @@ import { mdLeaveAnimation } from './animations/md.leave';
export class Alert implements ComponentInterface, OverlayInterface {
private readonly delegateController = createDelegateController(this);
private readonly triggerController = createTriggerController();
private customHTMLEnabled = config.get('innerHTMLTemplatesEnabled', ENABLE_HTML_CONTENT_DEFAULT);
private activeId?: string;
private inputType?: string;
private processedInputs: AlertInput[] = [];
@ -105,6 +107,11 @@ export class Alert implements ComponentInterface, OverlayInterface {
* `&lt;Ionic&gt;`
*
* For more information: [Security Documentation](https://ionicframework.com/docs/faq/security)
*
* This property accepts custom HTML as a string.
* Developers who only want to pass plain text
* can disable the custom HTML functionality
* by setting `innerHTMLTemplatesEnabled: false` in the Ionic config.
*/
@Prop() message?: string | IonicSafeString;
@ -671,6 +678,19 @@ export class Alert implements ComponentInterface, OverlayInterface {
);
}
private renderAlertMessage(msgId: string) {
const { customHTMLEnabled, message } = this;
if (customHTMLEnabled) {
return <div id={msgId} class="alert-message" innerHTML={sanitizeDOMString(message)}></div>;
}
return (
<div id={msgId} class="alert-message">
{message}
</div>
);
}
render() {
const { overlayIndex, header, subHeader, message, htmlAttributes } = this;
const mode = getIonMode(this);
@ -723,7 +743,7 @@ export class Alert implements ComponentInterface, OverlayInterface {
)}
</div>
<div id={msgId} class="alert-message" innerHTML={sanitizeDOMString(message)}></div>
{this.renderAlertMessage(msgId)}
{this.renderAlertInputs()}
{this.renderAlertButtons()}

View File

@ -0,0 +1,40 @@
import { newSpecPage } from '@stencil/core/testing';
import { Alert } from '../alert';
import { config } from '../../../global/config';
describe('alert: custom html', () => {
it('should allow for custom html by default', async () => {
const page = await newSpecPage({
components: [Alert],
html: `<ion-alert message="<button class='custom-html'>Custom Text</button>"></ion-alert>`,
});
const content = page.body.querySelector('.alert-message');
expect(content.textContent).toContain('Custom Text');
expect(content.querySelector('button.custom-html')).not.toBe(null);
});
it('should allow for custom html', async () => {
config.reset({ innerHTMLTemplatesEnabled: true });
const page = await newSpecPage({
components: [Alert],
html: `<ion-alert message="<button class='custom-html'>Custom Text</button>"></ion-alert>`,
});
const content = page.body.querySelector('.alert-message');
expect(content.textContent).toContain('Custom Text');
expect(content.querySelector('button.custom-html')).not.toBe(null);
});
it('should not allow for custom html', async () => {
config.reset({ innerHTMLTemplatesEnabled: false });
const page = await newSpecPage({
components: [Alert],
html: `<ion-alert message="<button class='custom-html'>Custom Text</button>"></ion-alert>`,
});
const content = page.body.querySelector('.alert-message');
expect(content.textContent).toContain('Custom Text');
expect(content.querySelector('button.custom-html')).toBe(null);
});
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1,4 +1,4 @@
import { clampDate, getPartsFromCalendarDay, parseAmPm, parseMinParts, parseMaxParts } from '../utils/parse';
import { clampDate, getPartsFromCalendarDay, parseAmPm, parseDate, parseMinParts, parseMaxParts } from '../utils/parse';
describe('getPartsFromCalendarDay()', () => {
it('should extract DatetimeParts from a calendar day element', () => {
@ -17,7 +17,54 @@ describe('getPartsFromCalendarDay()', () => {
});
});
// TODO FW-2794: parseDate()
describe('parseDate()', () => {
it('should return undefined when passed undefined', () => {
expect(parseDate(undefined)).toStrictEqual(undefined);
});
it('should return undefined when passed null', () => {
expect(parseDate(null)).toStrictEqual(undefined);
});
it('should return the correct date object when passed a date', () => {
expect(parseDate('2022-12-15T13:47')).toEqual({
ampm: 'pm',
day: 15,
hour: 13,
minute: 47,
month: 12,
tzOffset: 0,
year: 2022,
});
});
it('should return the correct time zone offset', () => {
expect(parseDate('2022-12-15T13:47:30-02:00').tzOffset).toEqual(-120);
});
it('should parse an array of dates', () => {
expect(parseDate(['2022-12-15T13:47', '2023-03-23T20:19:33.517Z'])).toEqual([
{
ampm: 'pm',
day: 15,
hour: 13,
minute: 47,
month: 12,
tzOffset: 0,
year: 2022,
},
{
ampm: 'pm',
day: 23,
hour: 20,
minute: 19,
month: 3,
tzOffset: 0,
year: 2023,
},
]);
});
});
describe('clampDate()', () => {
const minParts = {

View File

@ -3,6 +3,7 @@ import { Component, Host, Prop, h } from '@stencil/core';
import { config } from '../../global/config';
import { getIonMode } from '../../global/ionic-global';
import { ENABLE_HTML_CONTENT_DEFAULT } from '../../utils/config';
import type { IonicSafeString } from '../../utils/sanitization';
import { sanitizeDOMString } from '../../utils/sanitization';
import type { SpinnerTypes } from '../spinner/spinner-configs';
@ -15,6 +16,8 @@ import type { SpinnerTypes } from '../spinner/spinner-configs';
},
})
export class InfiniteScrollContent implements ComponentInterface {
private customHTMLEnabled = config.get('innerHTMLTemplatesEnabled', ENABLE_HTML_CONTENT_DEFAULT);
/**
* An animated SVG spinner that shows while loading.
*/
@ -28,6 +31,11 @@ export class InfiniteScrollContent implements ComponentInterface {
* `&lt;Ionic&gt;`
*
* For more information: [Security Documentation](https://ionicframework.com/docs/faq/security)
*
* This property accepts custom HTML as a string.
* Developers who only want to pass plain text
* can disable the custom HTML functionality
* by setting `innerHTMLTemplatesEnabled: false` in the Ionic config.
*/
@Prop() loadingText?: string | IonicSafeString;
@ -41,6 +49,15 @@ export class InfiniteScrollContent implements ComponentInterface {
}
}
private renderLoadingText() {
const { customHTMLEnabled, loadingText } = this;
if (customHTMLEnabled) {
return <div class="infinite-loading-text" innerHTML={sanitizeDOMString(loadingText)}></div>;
}
return <div class="infinite-loading-text">{this.loadingText}</div>;
}
render() {
const mode = getIonMode(this);
return (
@ -58,9 +75,7 @@ export class InfiniteScrollContent implements ComponentInterface {
<ion-spinner name={this.loadingSpinner} />
</div>
)}
{this.loadingText !== undefined && (
<div class="infinite-loading-text" innerHTML={sanitizeDOMString(this.loadingText)} />
)}
{this.loadingText !== undefined && this.renderLoadingText()}
</div>
</Host>
);

View File

@ -0,0 +1,40 @@
import { newSpecPage } from '@stencil/core/testing';
import { InfiniteScrollContent } from '../infinite-scroll-content';
import { config } from '../../../global/config';
describe('infinite-scroll-content: custom html', () => {
it('should allow for custom html by default', async () => {
const page = await newSpecPage({
components: [InfiniteScrollContent],
html: `<ion-infinite-scroll-content loading-text="<button class='custom-html'>Custom Text</button>"></ion-infinite-scroll-content>`,
});
const content = page.body.querySelector('.infinite-loading-text');
expect(content.textContent).toContain('Custom Text');
expect(content.querySelector('button.custom-html')).not.toBe(null);
});
it('should allow for custom html', async () => {
config.reset({ innerHTMLTemplatesEnabled: true });
const page = await newSpecPage({
components: [InfiniteScrollContent],
html: `<ion-infinite-scroll-content loading-text="<button class='custom-html'>Custom Text</button>"></ion-infinite-scroll-content>`,
});
const content = page.body.querySelector('.infinite-loading-text');
expect(content.textContent).toContain('Custom Text');
expect(content.querySelector('button.custom-html')).not.toBe(null);
});
it('should not allow for custom html', async () => {
config.reset({ innerHTMLTemplatesEnabled: false });
const page = await newSpecPage({
components: [InfiniteScrollContent],
html: `<ion-infinite-scroll-content loading-text="<button class='custom-html'>Custom Text2</button>"></ion-infinite-scroll-content>`,
});
const content = page.body.querySelector('.infinite-loading-text');
expect(content.textContent).toContain('Custom Text');
expect(content.querySelector('button.custom-html')).toBe(null);
});
});

View File

@ -4,6 +4,7 @@ import { Watch, Component, Element, Event, Host, Method, Prop, h } from '@stenci
import { config } from '../../global/config';
import { getIonMode } from '../../global/ionic-global';
import type { AnimationBuilder, FrameworkDelegate, OverlayInterface } from '../../interface';
import { ENABLE_HTML_CONTENT_DEFAULT } from '../../utils/config';
import { raf } from '../../utils/helpers';
import {
BACKDROP,
@ -41,6 +42,7 @@ import { mdLeaveAnimation } from './animations/md.leave';
export class Loading implements ComponentInterface, OverlayInterface {
private readonly delegateController = createDelegateController(this);
private readonly triggerController = createTriggerController();
private customHTMLEnabled = config.get('innerHTMLTemplatesEnabled', ENABLE_HTML_CONTENT_DEFAULT);
private durationTimeout?: ReturnType<typeof setTimeout>;
private currentTransition?: Promise<any>;
@ -75,6 +77,11 @@ export class Loading implements ComponentInterface, OverlayInterface {
/**
* Optional text content to display in the loading indicator.
*
* This property accepts custom HTML as a string.
* Developers who only want to pass plain text
* can disable the custom HTML functionality
* by setting `innerHTMLTemplatesEnabled: false` in the Ionic config.
*/
@Prop() message?: string | IonicSafeString;
@ -296,6 +303,19 @@ export class Loading implements ComponentInterface, OverlayInterface {
this.dismiss(undefined, BACKDROP);
};
private renderLoadingMessage(msgId: string) {
const { customHTMLEnabled, message } = this;
if (customHTMLEnabled) {
return <div class="loading-content" id={msgId} innerHTML={sanitizeDOMString(message)}></div>;
}
return (
<div class="loading-content" id={msgId}>
{message}
</div>
);
}
render() {
const { message, spinner, htmlAttributes, overlayIndex } = this;
const mode = getIonMode(this);
@ -335,9 +355,7 @@ export class Loading implements ComponentInterface, OverlayInterface {
</div>
)}
{message !== undefined && (
<div class="loading-content" id={msgId} innerHTML={sanitizeDOMString(message)}></div>
)}
{message !== undefined && this.renderLoadingMessage(msgId)}
</div>
<div tabindex="0"></div>

View File

@ -0,0 +1,40 @@
import { newSpecPage } from '@stencil/core/testing';
import { Loading } from '../loading';
import { config } from '../../../global/config';
describe('alert: custom html', () => {
it('should allow for custom html by default', async () => {
const page = await newSpecPage({
components: [Loading],
html: `<ion-loading message="<button class='custom-html'>Custom Text</button>"></ion-loading>`,
});
const content = page.body.querySelector('.loading-content');
expect(content.textContent).toContain('Custom Text');
expect(content.querySelector('button.custom-html')).not.toBe(null);
});
it('should allow for custom html', async () => {
config.reset({ innerHTMLTemplatesEnabled: true });
const page = await newSpecPage({
components: [Loading],
html: `<ion-loading message="<button class='custom-html'>Custom Text</button>"></ion-loading>`,
});
const content = page.body.querySelector('.loading-content');
expect(content.textContent).toContain('Custom Text');
expect(content.querySelector('button.custom-html')).not.toBe(null);
});
it('should not allow for custom html', async () => {
config.reset({ innerHTMLTemplatesEnabled: false });
const page = await newSpecPage({
components: [Loading],
html: `<ion-loading message="<button class='custom-html'>Custom Text</button>"></ion-loading>`,
});
const content = page.body.querySelector('.loading-content');
expect(content.textContent).toContain('Custom Text');
expect(content.querySelector('button.custom-html')).toBe(null);
});
});

View File

@ -162,9 +162,7 @@ test.describe('modal: incorrect usage', () => {
await ionModalDidPresent.next();
const modal = await page.locator('ion-modal');
await modal.evaluate((el: HTMLIonModalElement) => {
el.setCurrentBreakpoint(0.5);
});
await modal.evaluate((el: HTMLIonModalElement) => el.setCurrentBreakpoint(0.5));
expect(warnings.length).toBe(1);
expect(warnings[0]).toBe('[Ionic Warning]: setCurrentBreakpoint is only supported on sheet modals.');

View File

@ -2,6 +2,7 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core';
import { Component, Element, Event, Host, Listen, Prop, Watch, h } from '@stencil/core';
import { getIonMode } from '../../global/ionic-global';
import { renderHiddenInput } from '../../utils/helpers';
import type { RadioGroupChangeEventDetail } from './radio-group-interface';
@ -188,9 +189,11 @@ export class RadioGroup implements ComponentInterface {
}
render() {
const { label, labelId } = this;
const { label, labelId, el, name, value } = this;
const mode = getIonMode(this);
renderHiddenInput(true, el, name, value, false);
return <Host role="radiogroup" aria-labelledby={label ? labelId : null} onClick={this.onClick} class={mode}></Host>;
}
}

View File

@ -31,3 +31,37 @@ test.describe('radio-group: form', () => {
await expect(value).toHaveText('');
});
});
test.describe('radio-group: form submission', () => {
test('should submit radio data in a form', async ({ page, skip }) => {
skip.rtl();
skip.mode('md');
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/ionic-team/ionic-framework/issues/27016',
});
await page.setContent(`
<form>
<ion-radio-group value="a" name="my-group">
<ion-radio value="a"></ion-radio>
<ion-radio value="b"></ion-radio>
<ion-radio value="c"></ion-radio>
</ion-radio-group>
</form>
`);
const radioGroupData = await page.evaluate(() => {
const form = document.querySelector('form');
if (!form) {
return;
}
const formData = new FormData(form);
return formData.get('my-group');
});
await expect(radioGroupData).toBe('a');
});
});

View File

@ -4,6 +4,7 @@ import { arrowDown, caretBackSharp } from 'ionicons/icons';
import { config } from '../../global/config';
import { getIonMode } from '../../global/ionic-global';
import { ENABLE_HTML_CONTENT_DEFAULT } from '../../utils/config';
import { isPlatform } from '../../utils/platform';
import type { IonicSafeString } from '../../utils/sanitization';
import { sanitizeDOMString } from '../../utils/sanitization';
@ -14,6 +15,8 @@ import { SPINNERS } from '../spinner/spinner-configs';
tag: 'ion-refresher-content',
})
export class RefresherContent implements ComponentInterface {
private customHTMLEnabled = config.get('innerHTMLTemplatesEnabled', ENABLE_HTML_CONTENT_DEFAULT);
@Element() el!: HTMLIonRefresherContentElement;
/**
@ -31,6 +34,11 @@ export class RefresherContent implements ComponentInterface {
* `&lt;Ionic&gt;`
*
* For more information: [Security Documentation](https://ionicframework.com/docs/faq/security)
*
* This property accepts custom HTML as a string.
* Developers who only want to pass plain text
* can disable the custom HTML functionality
* by setting `innerHTMLTemplatesEnabled: false` in the Ionic config.
*/
@Prop() pullingText?: string | IonicSafeString;
@ -47,6 +55,11 @@ export class RefresherContent implements ComponentInterface {
* `&lt;Ionic&gt;`
*
* For more information: [Security Documentation](https://ionicframework.com/docs/faq/security)
*
* This property accepts custom HTML as a string.
* Developers who only want to pass plain text
* can disable the custom HTML functionality
* by setting `innerHTMLTemplatesEnabled: false` in the Ionic config.
*/
@Prop() refreshingText?: string | IonicSafeString;
@ -68,6 +81,24 @@ export class RefresherContent implements ComponentInterface {
}
}
private renderPullingText() {
const { customHTMLEnabled, pullingText } = this;
if (customHTMLEnabled) {
return <div class="refresher-pulling-text" innerHTML={sanitizeDOMString(pullingText)}></div>;
}
return <div class="refresher-pulling-text">{pullingText}</div>;
}
private renderRefreshingText() {
const { customHTMLEnabled, refreshingText } = this;
if (customHTMLEnabled) {
return <div class="refresher-refreshing-text" innerHTML={sanitizeDOMString(refreshingText)}></div>;
}
return <div class="refresher-refreshing-text">{refreshingText}</div>;
}
render() {
const pullingIcon = this.pullingIcon;
const hasSpinner = pullingIcon != null && (SPINNERS[pullingIcon] as any) !== undefined;
@ -93,9 +124,7 @@ export class RefresherContent implements ComponentInterface {
<ion-icon icon={this.pullingIcon} lazy={false} aria-hidden="true"></ion-icon>
</div>
)}
{this.pullingText !== undefined && (
<div class="refresher-pulling-text" innerHTML={sanitizeDOMString(this.pullingText)}></div>
)}
{this.pullingText !== undefined && this.renderPullingText()}
</div>
<div class="refresher-refreshing">
{this.refreshingSpinner && (
@ -103,9 +132,7 @@ export class RefresherContent implements ComponentInterface {
<ion-spinner name={this.refreshingSpinner}></ion-spinner>
</div>
)}
{this.refreshingText !== undefined && (
<div class="refresher-refreshing-text" innerHTML={sanitizeDOMString(this.refreshingText)}></div>
)}
{this.refreshingText !== undefined && this.renderRefreshingText()}
</div>
</Host>
);

View File

@ -0,0 +1,52 @@
import { newSpecPage } from '@stencil/core/testing';
import { RefresherContent } from '../refresher-content';
import { config } from '../../../global/config';
describe('refresher-content: custom html', () => {
it('should allow for custom html by default', async () => {
const page = await newSpecPage({
components: [RefresherContent],
html: `<ion-refresher-content pulling-text="<button class='custom-pulling-html'>Custom Pulling Text</button>" refreshing-text="<button class='custom-refreshing-html'>Custom Refreshing Text</button>"></ion-refresher-content>`,
});
const pullingContent = page.body.querySelector('.refresher-pulling-text');
expect(pullingContent.textContent).toContain('Custom Pulling Text');
expect(pullingContent.querySelector('button.custom-pulling-html')).not.toBe(null);
const refreshingContent = page.body.querySelector('.refresher-refreshing-text');
expect(refreshingContent.textContent).toContain('Custom Refreshing Text');
expect(refreshingContent.querySelector('button.custom-refreshing-html')).not.toBe(null);
});
it('should allow for custom html', async () => {
config.reset({ innerHTMLTemplatesEnabled: true });
const page = await newSpecPage({
components: [RefresherContent],
html: `<ion-refresher-content pulling-text="<button class='custom-pulling-html'>Custom Pulling Text</button>" refreshing-text="<button class='custom-refreshing-html'>Custom Refreshing Text</button>"></ion-refresher-content>`,
});
const pullingContent = page.body.querySelector('.refresher-pulling-text');
expect(pullingContent.textContent).toContain('Custom Pulling Text');
expect(pullingContent.querySelector('button.custom-pulling-html')).not.toBe(null);
const refreshingContent = page.body.querySelector('.refresher-refreshing-text');
expect(refreshingContent.textContent).toContain('Custom Refreshing Text');
expect(refreshingContent.querySelector('button.custom-refreshing-html')).not.toBe(null);
});
it('should not allow for custom html', async () => {
config.reset({ innerHTMLTemplatesEnabled: false });
const page = await newSpecPage({
components: [RefresherContent],
html: `<ion-refresher-content pulling-text="<button class='custom-pulling-html'>Custom Pulling Text</button>" refreshing-text="<button class='custom-html'>Custom Refreshing Text</button>"></ion-refresher-content>`,
});
const pullingContent = page.body.querySelector('.refresher-pulling-text');
expect(pullingContent.textContent).toContain('Custom Pulling Text');
expect(pullingContent.querySelector('button.custom-pulling-html')).toBe(null);
const refreshingContent = page.body.querySelector('.refresher-refreshing-text');
expect(refreshingContent.textContent).toContain('Custom Refreshing Text');
expect(refreshingContent.querySelector('button.custom-refreshing-html')).toBe(null);
});
});

View File

@ -19,6 +19,7 @@ test.describe('ripple-effect: basic', () => {
test.describe('ripple effect with nested ion-button', () => {
test('should add .ion-activated when the block is pressed', async ({ page }) => {
await page.goto('/src/components/ripple-effect/test/basic?ionic:_testing=false');
await isIdleCallbackComplete(page);
const el = page.locator('#ripple-with-button');
@ -45,6 +46,7 @@ test.describe('ripple-effect: basic', () => {
const verifyRippleEffect = async (page: E2EPage, selector: string) => {
await page.goto('/src/components/ripple-effect/test/basic?ionic:_testing=false');
await isIdleCallbackComplete(page);
const el = page.locator(selector);
@ -61,3 +63,24 @@ const verifyRippleEffect = async (page: E2EPage, selector: string) => {
await expect(el).toHaveClass(/ion-activated/);
};
/**
* This function is used to wait for the idle callback to be called.
* It mirrors the custom implementation in app.tsx for either
* using requestIdleCallback on supported browsers or a setTimeout
* of 32ms (~2 frames) on unsupported browsers (Safari).
*/
const isIdleCallbackComplete = async (page: E2EPage) => {
await page.waitForFunction(
() => {
return new Promise((resolve) => {
if ('requestIdleCallback' in window) {
window.requestIdleCallback(resolve);
} else {
setTimeout(resolve, 32);
}
});
},
{ timeout: 5000 }
);
};

View File

@ -50,8 +50,9 @@
font-family: $font-family-base;
cursor: pointer;
white-space: nowrap;
cursor: pointer;
z-index: $z-index-item-input;
}
@ -181,7 +182,7 @@ button {
text-overflow: ellipsis;
white-space: nowrap;
white-space: inherit;
overflow: hidden;
}

View File

@ -0,0 +1,48 @@
import { expect } from '@playwright/test';
import { test } from '@utils/test/playwright';
test.describe('select: wrapping', () => {
test('should not wrap text by default', async ({ page, skip }) => {
skip.rtl();
await page.setContent(`
<ion-select value="nowrap" aria-label="Should Not Wrap">
<ion-select-option value="nowrap">Should not wrap when no label exists and no class is added to make the text wrap</ion-select-option>
</ion-select>
`);
const select = page.locator('ion-select');
await expect(select).toHaveScreenshot(`select-nowrap-${page.getSnapshotSettings()}.png`);
});
test('should wrap text with class', async ({ page, skip }) => {
skip.rtl();
await page.setContent(`
<ion-select value="wrap" aria-label="Should Wrap" class="ion-text-wrap">
<ion-select-option value="wrap">Should wrap when no label exists and really long text exists to make it wrap the text</ion-select-option>
</ion-select>
`);
const select = page.locator('ion-select');
await expect(select).toHaveScreenshot(`select-wrap-${page.getSnapshotSettings()}.png`);
});
test('should not wrap label while wrapping text with class', async ({ page, skip }) => {
skip.rtl();
// TODO(FW-3787) Make label a property of select
await page.setContent(`
<ion-item>
<ion-label>Really long label should not wrap</ion-label>
<ion-select value="wrap" aria-label="Should Wrap" class="ion-text-wrap">
<ion-select-option value="wrap">Should wrap value only when label exists and really long text exists to make it wrap the text</ion-select-option>
</ion-select>
</ion-label>
</ion-item>
`);
const select = page.locator('ion-item');
await expect(select).toHaveScreenshot(`select-wrap-with-label-${page.getSnapshotSettings()}.png`);
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -0,0 +1,43 @@
import { newSpecPage } from '@stencil/core/testing';
import { Toast } from '../toast';
import { config } from '../../../global/config';
describe('alert: custom html', () => {
it('should allow for custom html by default', async () => {
const page = await newSpecPage({
components: [Toast],
html: `<ion-toast message="<button class='custom-html'>Custom Text</button>"></ion-toast>`,
});
const toast = page.body.querySelector('ion-toast');
const content = toast.shadowRoot.querySelector('.toast-message');
expect(content.textContent).toContain('Custom Text');
expect(content.querySelector('button.custom-html')).not.toBe(null);
});
it('should allow for custom html', async () => {
config.reset({ innerHTMLTemplatesEnabled: true });
const page = await newSpecPage({
components: [Toast],
html: `<ion-toast message="<button class='custom-html'>Custom Text</button>"></ion-toast>`,
});
const toast = page.body.querySelector('ion-toast');
const content = toast.shadowRoot.querySelector('.toast-message');
expect(content.textContent).toContain('Custom Text');
expect(content.querySelector('button.custom-html')).not.toBe(null);
});
it('should not allow for custom html', async () => {
config.reset({ innerHTMLTemplatesEnabled: false });
const page = await newSpecPage({
components: [Toast],
html: `<ion-toast message="<button class='custom-html'>Custom Text</button>"></ion-toast>`,
});
const toast = page.body.querySelector('ion-toast');
const content = toast.shadowRoot.querySelector('.toast-message');
expect(content.textContent).toContain('Custom Text');
expect(content.querySelector('button.custom-html')).toBe(null);
});
});

View File

@ -4,6 +4,7 @@ import { Watch, Component, Element, Event, h, Host, Method, Prop } from '@stenci
import { config } from '../../global/config';
import { getIonMode } from '../../global/ionic-global';
import type { AnimationBuilder, Color, CssClassMap, OverlayInterface, FrameworkDelegate } from '../../interface';
import { ENABLE_HTML_CONTENT_DEFAULT } from '../../utils/config';
import { printIonWarning } from '../../utils/logging';
import {
createDelegateController,
@ -49,6 +50,7 @@ export class Toast implements ComponentInterface, OverlayInterface {
private readonly delegateController = createDelegateController(this);
private readonly triggerController = createTriggerController();
private currentTransition?: Promise<any>;
private customHTMLEnabled = config.get('innerHTMLTemplatesEnabled', ENABLE_HTML_CONTENT_DEFAULT);
private durationTimeout?: ReturnType<typeof setTimeout>;
presented = false;
@ -111,6 +113,10 @@ export class Toast implements ComponentInterface, OverlayInterface {
/**
* Message to be shown in the toast.
* This property accepts custom HTML as a string.
* Developers who only want to pass plain text
* can disable the custom HTML functionality
* by setting `innerHTMLTemplatesEnabled: false` in the Ionic config.
*/
@Prop() message?: string | IonicSafeString;
@ -401,6 +407,19 @@ export class Toast implements ComponentInterface, OverlayInterface {
);
}
private renderToastMessage() {
const { customHTMLEnabled, message } = this;
if (customHTMLEnabled) {
return <div class="toast-message" part="message" innerHTML={sanitizeDOMString(message)}></div>;
}
return (
<div class="toast-message" part="message">
{message}
</div>
);
}
render() {
const { layout, el } = this;
const allButtons = this.getButtons();
@ -457,9 +476,7 @@ export class Toast implements ComponentInterface, OverlayInterface {
{this.header}
</div>
)}
{this.message !== undefined && (
<div class="toast-message" part="message" innerHTML={sanitizeDOMString(this.message)}></div>
)}
{this.message !== undefined && this.renderToastMessage()}
</div>
{this.renderButtons(endButtons, 'end')}

View File

@ -189,6 +189,14 @@ export interface IonicConfig {
*/
sanitizerEnabled?: boolean;
/**
* Relevant Components: ion-alert, ion-infinite-scroll-content, ion-loading, ion-refresher-content, ion-toast
* If `false`, all `innerHTML` usage will be disabled in Ionic, and
* custom HTML will not be usable in the relevant components.
* `innerHTML` usage is enabled by default.
*/
innerHTMLTemplatesEnabled?: boolean;
/**
* Overrides the default platform detection methods.
*/
@ -240,3 +248,5 @@ export const getMode = (): Mode => {
}
return 'md';
};
export const ENABLE_HTML_CONTENT_DEFAULT = true;

View File

@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [6.7.0](https://github.com/ionic-team/ionic-docs/compare/v6.6.3...v6.7.0) (2023-03-23)
**Note:** Version bump only for package @ionic/docs
# [7.0.0-rc.3](https://github.com/ionic-team/ionic-docs/compare/v7.0.0-rc.2...v7.0.0-rc.3) (2023-03-22)
**Note:** Version bump only for package @ionic/docs

View File

@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [6.7.0](https://github.com/ionic-team/ionic/compare/v6.6.3...v6.7.0) (2023-03-23)
**Note:** Version bump only for package @ionic/angular-server
# [7.0.0-rc.3](https://github.com/ionic-team/ionic/compare/v7.0.0-rc.2...v7.0.0-rc.3) (2023-03-22)
**Note:** Version bump only for package @ionic/angular-server

View File

@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [6.7.0](https://github.com/ionic-team/ionic/compare/v6.6.3...v6.7.0) (2023-03-23)
**Note:** Version bump only for package @ionic/react-router
# [7.0.0-rc.3](https://github.com/ionic-team/ionic/compare/v7.0.0-rc.2...v7.0.0-rc.3) (2023-03-22)
**Note:** Version bump only for package @ionic/react-router

View File

@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [6.7.0](https://github.com/ionic-team/ionic/compare/v6.6.3...v6.7.0) (2023-03-23)
**Note:** Version bump only for package @ionic/react
# [7.0.0-rc.3](https://github.com/ionic-team/ionic/compare/v7.0.0-rc.2...v7.0.0-rc.3) (2023-03-22)
**Note:** Version bump only for package @ionic/react

View File

@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [6.7.0](https://github.com/ionic-team/ionic/compare/v6.6.3...v6.7.0) (2023-03-23)
**Note:** Version bump only for package @ionic/vue-router
# [7.0.0-rc.3](https://github.com/ionic-team/ionic/compare/v7.0.0-rc.2...v7.0.0-rc.3) (2023-03-22)
**Note:** Version bump only for package @ionic/vue-router

View File

@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [6.7.0](https://github.com/ionic-team/ionic/compare/v6.6.3...v6.7.0) (2023-03-23)
**Note:** Version bump only for package @ionic/vue
# [7.0.0-rc.3](https://github.com/ionic-team/ionic/compare/v7.0.0-rc.2...v7.0.0-rc.3) (2023-03-22)
**Note:** Version bump only for package @ionic/vue

View File

@ -2,6 +2,18 @@
Ionic Framework supports multiple versions of Vue. As a result, we need to verify that Ionic works correctly with each of these Vue versions.
## Syncing Local Changes
The Vue test app supports syncing your locally built changes for validation.
1. Build the `core`, `packages/vue`, and `packages/vue-router` projects using `npm run build`.
2. [Build the Vue test app](#test-app-build-structure).
3. Navigate to the built test app.
4. Install dependencies using `npm install`.
5. Sync your local changes using `npm run sync`.
From here you can either build the application or start a local dev server. When re-syncing changes, you will need to wipe the build cache in `node_modules/.cache` and restart the dev server/re-build.
## Test App Build Structure
Unlike other test applications, these test apps are broken up into multiple directories. These directories are then combined to create a single application. This allows us to share common application code, tests, etc so that each app is being tested the same way. Below details the different pieces that help create a single test application.