Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
918edf2f72 | ||
|
|
ba894d05a8 | ||
|
|
9313a914b7 | ||
|
|
27a9aaaedc | ||
|
|
35256d70ec | ||
|
|
ce0767bbb0 | ||
|
|
b1369a94ae | ||
|
|
c98ad6f16a | ||
|
|
f27c899d13 | ||
|
|
16ee234258 | ||
|
|
4804b67785 |
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -2,13 +2,9 @@ Issue number: #
|
||||
|
||||
---------
|
||||
|
||||
<!-- Please refer to our contributing documentation for any questions on submitting a pull request, or let us know here if you need any help: https://ionicframework.com/docs/building/contributing -->
|
||||
<!-- Please do not submit updates to dependencies unless it fixes an issue. -->
|
||||
|
||||
<!-- Some docs updates need to be made in the `ionic-docs` repo, in a separate PR. See https://github.com/ionic-team/ionic-framework/blob/main/.github/CONTRIBUTING.md#modifying-documentation for details. -->
|
||||
|
||||
<!-- Please do not submit updates to dependencies unless it fixes an issue. -->
|
||||
|
||||
<!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. -->
|
||||
<!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. -->
|
||||
|
||||
## What is the current behavior?
|
||||
<!-- Please describe the current behavior that you are modifying. -->
|
||||
|
||||
@@ -54,7 +54,7 @@ runs:
|
||||
mkdir updated-screenshots
|
||||
cd ../ && rsync -R --progress $(git diff --name-only --cached) core/updated-screenshots
|
||||
if [ -d core/updated-screenshots/core ]; then
|
||||
echo "::set-output name=hasUpdatedScreenshots::$(echo 'true')"
|
||||
echo "hasUpdatedScreenshots=$(echo 'true')" >> $GITHUB_OUTPUT
|
||||
cd core/updated-screenshots
|
||||
zip -q -r ../../UpdatedScreenshots${{ inputs.commandModifier }}-${{ inputs.shard }}-${{ inputs.totalShards }}.zip core
|
||||
fi
|
||||
|
||||
28
angular/package-lock.json
generated
@@ -1227,19 +1227,19 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@ionic/core": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-7.0.4.tgz",
|
||||
"integrity": "sha512-tS1RfWn+L3jVpU833d7QQs1FQO1MlbDQL6sMaYBOYcE1nNZuDTd++zcOS264eklNnZu6z7PJmd04dj3DnQjsmA==",
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-7.0.5.tgz",
|
||||
"integrity": "sha512-dcuE/PEF+GHsPEsLppUASSwWnzVcxFZE7uPMDzTwUPMOFTTaRgWbPxIly/4/VRbV6GSL6MEcuVVxhEdWjSODTg==",
|
||||
"dependencies": {
|
||||
"@stencil/core": "^3.2.1",
|
||||
"@stencil/core": "^3.2.2",
|
||||
"ionicons": "^7.1.0",
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ionic/core/node_modules/@stencil/core": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-3.2.1.tgz",
|
||||
"integrity": "sha512-Ybm4NteQBScLq3H0JML/uqo4nWjNpZw1HAAURtR5LlRm7ptzNKO5S8EnHp3m05/uyTzeh9yLpUFHY7bxGNdYLg==",
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-3.2.2.tgz",
|
||||
"integrity": "sha512-wXb9cVWL0T3cTwYLveekdTFCRGx6+9hpVDEXna+N8K8OPoW6xtFAHRLv+LjOM7k59PkA8MG3IinAfV7Y+xa0Hw==",
|
||||
"bin": {
|
||||
"stencil": "bin/stencil"
|
||||
},
|
||||
@@ -8104,19 +8104,19 @@
|
||||
"dev": true
|
||||
},
|
||||
"@ionic/core": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-7.0.4.tgz",
|
||||
"integrity": "sha512-tS1RfWn+L3jVpU833d7QQs1FQO1MlbDQL6sMaYBOYcE1nNZuDTd++zcOS264eklNnZu6z7PJmd04dj3DnQjsmA==",
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-7.0.5.tgz",
|
||||
"integrity": "sha512-dcuE/PEF+GHsPEsLppUASSwWnzVcxFZE7uPMDzTwUPMOFTTaRgWbPxIly/4/VRbV6GSL6MEcuVVxhEdWjSODTg==",
|
||||
"requires": {
|
||||
"@stencil/core": "^3.2.1",
|
||||
"@stencil/core": "^3.2.2",
|
||||
"ionicons": "^7.1.0",
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@stencil/core": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-3.2.1.tgz",
|
||||
"integrity": "sha512-Ybm4NteQBScLq3H0JML/uqo4nWjNpZw1HAAURtR5LlRm7ptzNKO5S8EnHp3m05/uyTzeh9yLpUFHY7bxGNdYLg=="
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-3.2.2.tgz",
|
||||
"integrity": "sha512-wXb9cVWL0T3cTwYLveekdTFCRGx6+9hpVDEXna+N8K8OPoW6xtFAHRLv+LjOM7k59PkA8MG3IinAfV7Y+xa0Hw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -116,7 +116,7 @@ export class IonModal {
|
||||
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
|
||||
this.el = r.nativeElement;
|
||||
|
||||
this.el.addEventListener('willPresent', () => {
|
||||
this.el.addEventListener('ionMount', () => {
|
||||
this.isCmpOpen = true;
|
||||
c.detectChanges();
|
||||
});
|
||||
@@ -124,7 +124,6 @@ export class IonModal {
|
||||
this.isCmpOpen = false;
|
||||
c.detectChanges();
|
||||
});
|
||||
|
||||
proxyOutputs(this, this.el, [
|
||||
'ionModalDidPresent',
|
||||
'ionModalWillPresent',
|
||||
|
||||
@@ -977,7 +977,7 @@ export declare interface IonInfiniteScrollContent extends Components.IonInfinite
|
||||
|
||||
|
||||
@ProxyCmp({
|
||||
inputs: ['accept', 'autocapitalize', 'autocomplete', 'autocorrect', 'autofocus', 'clearInput', 'clearOnEdit', 'color', 'counter', 'counterFormatter', 'debounce', 'disabled', 'enterkeyhint', 'errorText', 'fill', 'helperText', 'inputmode', 'label', 'labelPlacement', 'legacy', 'max', 'maxlength', 'min', 'minlength', 'mode', 'multiple', 'name', 'pattern', 'placeholder', 'readonly', 'required', 'shape', 'size', 'spellcheck', 'step', 'type', 'value'],
|
||||
inputs: ['accept', 'autocapitalize', 'autocomplete', 'autocorrect', 'autofocus', 'clearInput', 'clearOnEdit', 'color', 'counter', 'counterFormatter', 'debounce', 'disabled', 'enterkeyhint', 'errorText', 'fill', 'helperText', 'inputmode', 'label', 'labelPlacement', 'legacy', 'mask', 'maskPlaceholder', 'maskVisibility', 'max', 'maxlength', 'min', 'minlength', 'mode', 'multiple', 'name', 'pattern', 'placeholder', 'readonly', 'required', 'shape', 'size', 'spellcheck', 'step', 'type', 'value'],
|
||||
methods: ['setFocus', 'getInputElement']
|
||||
})
|
||||
@Component({
|
||||
@@ -985,7 +985,7 @@ export declare interface IonInfiniteScrollContent extends Components.IonInfinite
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: '<ng-content></ng-content>',
|
||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
||||
inputs: ['accept', 'autocapitalize', 'autocomplete', 'autocorrect', 'autofocus', 'clearInput', 'clearOnEdit', 'color', 'counter', 'counterFormatter', 'debounce', 'disabled', 'enterkeyhint', 'errorText', 'fill', 'helperText', 'inputmode', 'label', 'labelPlacement', 'legacy', 'max', 'maxlength', 'min', 'minlength', 'mode', 'multiple', 'name', 'pattern', 'placeholder', 'readonly', 'required', 'shape', 'size', 'spellcheck', 'step', 'type', 'value'],
|
||||
inputs: ['accept', 'autocapitalize', 'autocomplete', 'autocorrect', 'autofocus', 'clearInput', 'clearOnEdit', 'color', 'counter', 'counterFormatter', 'debounce', 'disabled', 'enterkeyhint', 'errorText', 'fill', 'helperText', 'inputmode', 'label', 'labelPlacement', 'legacy', 'mask', 'maskPlaceholder', 'maskVisibility', 'max', 'maxlength', 'min', 'minlength', 'mode', 'multiple', 'name', 'pattern', 'placeholder', 'readonly', 'required', 'shape', 'size', 'spellcheck', 'step', 'type', 'value'],
|
||||
})
|
||||
export class IonInput {
|
||||
protected el: HTMLElement;
|
||||
|
||||
@@ -121,4 +121,4 @@ describe('when in a modal', () => {
|
||||
cy.get('#set-to-null').click();
|
||||
cy.get('#inputWithFloatingLabel').should('not.have.class', 'item-has-value');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -17,4 +17,4 @@
|
||||
</ion-list>
|
||||
</ion-content>
|
||||
</ng-template>
|
||||
</ion-modal>
|
||||
</ion-modal>
|
||||
@@ -24,4 +24,5 @@ export class ModalInlineComponent implements AfterViewInit {
|
||||
onBreakpointDidChange() {
|
||||
this.breakpointDidChangeCounter++;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -553,6 +553,9 @@ ion-input,prop,inputmode,"decimal" | "email" | "none" | "numeric" | "search" | "
|
||||
ion-input,prop,label,string | undefined,undefined,false,false
|
||||
ion-input,prop,labelPlacement,"end" | "fixed" | "floating" | "stacked" | "start",'start',false,false
|
||||
ion-input,prop,legacy,boolean | undefined,undefined,false,false
|
||||
ion-input,prop,mask,(string | RegExp)[] | RegExp | undefined,undefined,false,false
|
||||
ion-input,prop,maskPlaceholder,null | string | undefined,'_',false,false
|
||||
ion-input,prop,maskVisibility,"always" | "focus" | "never" | undefined,'always',false,false
|
||||
ion-input,prop,max,number | string | undefined,undefined,false,false
|
||||
ion-input,prop,maxlength,number | undefined,undefined,false,false
|
||||
ion-input,prop,min,number | string | undefined,undefined,false,false
|
||||
|
||||
30
core/src/components.d.ts
vendored
@@ -17,6 +17,7 @@ import { CheckboxChangeEventDetail } from "./components/checkbox/checkbox-interf
|
||||
import { ScrollBaseDetail, ScrollDetail } from "./components/content/content-interface";
|
||||
import { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimePresentation, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface";
|
||||
import { SpinnerTypes } from "./components/spinner/spinner-configs";
|
||||
import { MaskExpression, MaskPlaceholder, MaskVisibility } from "./utils/input-masking/public-api";
|
||||
import { InputChangeEventDetail, InputInputEventDetail } from "./components/input/input-interface";
|
||||
import { CounterFormatter } from "./components/item/item-interface";
|
||||
import { MenuChangeEventDetail, Side } from "./components/menu/menu-interface";
|
||||
@@ -53,6 +54,7 @@ export { CheckboxChangeEventDetail } from "./components/checkbox/checkbox-interf
|
||||
export { ScrollBaseDetail, ScrollDetail } from "./components/content/content-interface";
|
||||
export { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimePresentation, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface";
|
||||
export { SpinnerTypes } from "./components/spinner/spinner-configs";
|
||||
export { MaskExpression, MaskPlaceholder, MaskVisibility } from "./utils/input-masking/public-api";
|
||||
export { InputChangeEventDetail, InputInputEventDetail } from "./components/input/input-interface";
|
||||
export { CounterFormatter } from "./components/item/item-interface";
|
||||
export { MenuChangeEventDetail, Side } from "./components/menu/menu-interface";
|
||||
@@ -1225,6 +1227,18 @@ export namespace Components {
|
||||
* Set the `legacy` property to `true` to forcibly use the legacy form control markup. Ionic will only opt components in to the modern form markup when they are using either the `aria-label` attribute or the `label` property. As a result, the `legacy` property should only be used as an escape hatch when you want to avoid this automatic opt-in behavior. Note that this property will be removed in an upcoming major release of Ionic, and all form components will be opted-in to using the modern form markup.
|
||||
*/
|
||||
"legacy"?: boolean;
|
||||
/**
|
||||
* The predefined format of the user's input. For example, you can set a mask that only accepts digits, or you can configure a more complex pattern like a phone number or credit card number. The mask supports two formats: 1. A valid [regular expression pattern](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions) 2. An array containing regular expression and fixed character patterns The fixed characters in the mask cannot be erased or replaced by the user. For example in a phone number mask, the `(`, `)` and `-` are examples of fixed characters.
|
||||
*/
|
||||
"mask"?: MaskExpression;
|
||||
/**
|
||||
* Character or string to cover unfilled parts of the mask. The default character is `_`. If set to `null`, `undefined` or an empty string, unfilled parts will be empty as in a regular input.
|
||||
*/
|
||||
"maskPlaceholder"?: MaskPlaceholder;
|
||||
/**
|
||||
* The visibility of the mask placeholder. With `always`, the placeholder will be visible even when the control does not have focus. With `focus`, the placeholder will only be visible when the control has focus. With `never`, the placeholder will never be visibly displayed.
|
||||
*/
|
||||
"maskVisibility"?: MaskVisibility;
|
||||
/**
|
||||
* The maximum value, which must not be less than its minimum (min attribute) value.
|
||||
*/
|
||||
@@ -5255,6 +5269,18 @@ declare namespace LocalJSX {
|
||||
* Set the `legacy` property to `true` to forcibly use the legacy form control markup. Ionic will only opt components in to the modern form markup when they are using either the `aria-label` attribute or the `label` property. As a result, the `legacy` property should only be used as an escape hatch when you want to avoid this automatic opt-in behavior. Note that this property will be removed in an upcoming major release of Ionic, and all form components will be opted-in to using the modern form markup.
|
||||
*/
|
||||
"legacy"?: boolean;
|
||||
/**
|
||||
* The predefined format of the user's input. For example, you can set a mask that only accepts digits, or you can configure a more complex pattern like a phone number or credit card number. The mask supports two formats: 1. A valid [regular expression pattern](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions) 2. An array containing regular expression and fixed character patterns The fixed characters in the mask cannot be erased or replaced by the user. For example in a phone number mask, the `(`, `)` and `-` are examples of fixed characters.
|
||||
*/
|
||||
"mask"?: MaskExpression;
|
||||
/**
|
||||
* Character or string to cover unfilled parts of the mask. The default character is `_`. If set to `null`, `undefined` or an empty string, unfilled parts will be empty as in a regular input.
|
||||
*/
|
||||
"maskPlaceholder"?: MaskPlaceholder;
|
||||
/**
|
||||
* The visibility of the mask placeholder. With `always`, the placeholder will be visible even when the control does not have focus. With `focus`, the placeholder will only be visible when the control has focus. With `never`, the placeholder will never be visibly displayed.
|
||||
*/
|
||||
"maskVisibility"?: MaskVisibility;
|
||||
/**
|
||||
* The maximum value, which must not be less than its minimum (min attribute) value.
|
||||
*/
|
||||
@@ -5830,6 +5856,10 @@ declare namespace LocalJSX {
|
||||
* Emitted before the modal has presented.
|
||||
*/
|
||||
"onIonModalWillPresent"?: (event: IonModalCustomEvent<void>) => void;
|
||||
/**
|
||||
* Emitted before the modal has presented, but after the component has been mounted in the DOM. This event exists so iOS can run the entering transition properly
|
||||
*/
|
||||
"onIonMount"?: (event: IonModalCustomEvent<void>) => void;
|
||||
/**
|
||||
* Emitted before the modal has dismissed. Shorthand for ionModalWillDismiss.
|
||||
*/
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
prepareOverlay,
|
||||
present,
|
||||
safeCall,
|
||||
setOverlayId,
|
||||
} from '../../utils/overlays';
|
||||
import type { OverlayEventDetail } from '../../utils/overlays-interface';
|
||||
import { getClassMap } from '../../utils/theme';
|
||||
@@ -311,6 +312,10 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
|
||||
this.triggerController.removeClickListener();
|
||||
}
|
||||
|
||||
componentWillLoad() {
|
||||
setOverlayId(this.el);
|
||||
}
|
||||
|
||||
componentDidLoad() {
|
||||
/**
|
||||
* Do not create gesture if:
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { newSpecPage } from '@stencil/core/testing';
|
||||
|
||||
import { ActionSheet } from '../action-sheet';
|
||||
|
||||
it('action sheet should be assigned an incrementing id', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [ActionSheet],
|
||||
html: `<ion-action-sheet is-open="true"></ion-action-sheet>`,
|
||||
});
|
||||
let actionSheet: HTMLIonActionSheetElement;
|
||||
|
||||
actionSheet = page.body.querySelector('ion-action-sheet')!;
|
||||
|
||||
expect(actionSheet).not.toBe(null);
|
||||
expect(actionSheet.getAttribute('id')).toBe('ion-overlay-1');
|
||||
|
||||
// Remove the action sheet from the DOM
|
||||
actionSheet.remove();
|
||||
await page.waitForChanges();
|
||||
|
||||
// Create a new action sheet to verify the id is incremented
|
||||
actionSheet = document.createElement('ion-action-sheet');
|
||||
actionSheet.isOpen = true;
|
||||
page.body.appendChild(actionSheet);
|
||||
await page.waitForChanges();
|
||||
|
||||
actionSheet = page.body.querySelector('ion-action-sheet')!;
|
||||
|
||||
expect(actionSheet.getAttribute('id')).toBe('ion-overlay-2');
|
||||
|
||||
// Presenting the same action sheet again should reuse the existing id
|
||||
|
||||
actionSheet.isOpen = false;
|
||||
await page.waitForChanges();
|
||||
actionSheet.isOpen = true;
|
||||
await page.waitForChanges();
|
||||
|
||||
actionSheet = page.body.querySelector('ion-action-sheet')!;
|
||||
|
||||
expect(actionSheet.getAttribute('id')).toBe('ion-overlay-2');
|
||||
});
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
prepareOverlay,
|
||||
present,
|
||||
safeCall,
|
||||
setOverlayId,
|
||||
} from '../../utils/overlays';
|
||||
import type { OverlayEventDetail } from '../../utils/overlays-interface';
|
||||
import type { IonicSafeString } from '../../utils/sanitization';
|
||||
@@ -329,6 +330,7 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
}
|
||||
|
||||
componentWillLoad() {
|
||||
setOverlayId(this.el);
|
||||
this.inputsChanged();
|
||||
this.buttonsChanged();
|
||||
}
|
||||
|
||||
41
core/src/components/alert/test/alert-id.spec.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { newSpecPage } from '@stencil/core/testing';
|
||||
|
||||
import { Alert } from '../alert';
|
||||
|
||||
it('alert should be assigned an incrementing id', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Alert],
|
||||
html: `<ion-alert is-open="true"></ion-alert>`,
|
||||
});
|
||||
let alert: HTMLIonAlertElement;
|
||||
|
||||
alert = page.body.querySelector('ion-alert')!;
|
||||
|
||||
expect(alert).not.toBe(null);
|
||||
expect(alert.getAttribute('id')).toBe('ion-overlay-1');
|
||||
|
||||
// Remove the alert from the DOM
|
||||
alert.remove();
|
||||
await page.waitForChanges();
|
||||
|
||||
// Create a new alert to verify the id is incremented
|
||||
alert = document.createElement('ion-alert');
|
||||
alert.isOpen = true;
|
||||
page.body.appendChild(alert);
|
||||
await page.waitForChanges();
|
||||
|
||||
alert = page.body.querySelector('ion-alert')!;
|
||||
|
||||
expect(alert.getAttribute('id')).toBe('ion-overlay-2');
|
||||
|
||||
// Presenting the same alert again should reuse the existing id
|
||||
|
||||
alert.isOpen = false;
|
||||
await page.waitForChanges();
|
||||
alert.isOpen = true;
|
||||
await page.waitForChanges();
|
||||
|
||||
alert = page.body.querySelector('ion-alert')!;
|
||||
|
||||
expect(alert.getAttribute('id')).toBe('ion-overlay-2');
|
||||
});
|
||||
@@ -8,6 +8,8 @@ import type { LegacyFormController } from '../../utils/forms';
|
||||
import { createLegacyFormController } from '../../utils/forms';
|
||||
import type { Attributes } from '../../utils/helpers';
|
||||
import { inheritAriaAttributes, debounceEvent, findItemLabel, inheritAttributes } from '../../utils/helpers';
|
||||
import { MaskController } from '../../utils/input-masking';
|
||||
import type { MaskExpression, MaskPlaceholder, MaskVisibility } from '../../utils/input-masking/public-api';
|
||||
import { printIonWarning } from '../../utils/logging';
|
||||
import { createColorClasses, hostContext } from '../../utils/theme';
|
||||
|
||||
@@ -31,6 +33,7 @@ export class Input implements ComponentInterface {
|
||||
private inheritedAttributes: Attributes = {};
|
||||
private isComposing = false;
|
||||
private legacyFormController!: LegacyFormController;
|
||||
private maskController?: MaskController;
|
||||
|
||||
// This flag ensures we log the deprecation warning at most once.
|
||||
private hasLoggedDeprecationWarning = false;
|
||||
@@ -272,6 +275,35 @@ export class Input implements ComponentInterface {
|
||||
*/
|
||||
@Prop({ mutable: true }) value?: string | number | null = '';
|
||||
|
||||
/**
|
||||
* The predefined format of the user's input. For example, you can set a mask
|
||||
* that only accepts digits, or you can configure a more complex pattern like
|
||||
* a phone number or credit card number.
|
||||
*
|
||||
* The mask supports two formats:
|
||||
* 1. A valid [regular expression pattern](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions)
|
||||
* 2. An array containing regular expression and fixed character patterns
|
||||
*
|
||||
* The fixed characters in the mask cannot be erased or replaced by the user. For example
|
||||
* in a phone number mask, the `(`, `)` and `-` are examples of fixed characters.
|
||||
*
|
||||
*/
|
||||
@Prop() mask?: MaskExpression;
|
||||
|
||||
/**
|
||||
* The visibility of the mask placeholder. With `always`, the placeholder will be
|
||||
* visible even when the control does not have focus. With `focus`, the placeholder
|
||||
* will only be visible when the control has focus. With `never`, the placeholder will
|
||||
* never be visibly displayed.
|
||||
*/
|
||||
@Prop() maskVisibility?: MaskVisibility = 'always';
|
||||
|
||||
/**
|
||||
* Character or string to cover unfilled parts of the mask. The default character is `_`.
|
||||
* If set to `null`, `undefined` or an empty string, unfilled parts will be empty as in a regular input.
|
||||
*/
|
||||
@Prop() maskPlaceholder?: MaskPlaceholder = '_';
|
||||
|
||||
/**
|
||||
* The `ionInput` event fires when the `value` of an `<ion-input>` element
|
||||
* has been changed.
|
||||
@@ -343,9 +375,11 @@ export class Input implements ComponentInterface {
|
||||
}
|
||||
|
||||
componentWillLoad() {
|
||||
const { el } = this;
|
||||
|
||||
this.inheritedAttributes = {
|
||||
...inheritAriaAttributes(this.el),
|
||||
...inheritAttributes(this.el, ['tabindex', 'title', 'data-form-type']),
|
||||
...inheritAriaAttributes(el),
|
||||
...inheritAttributes(el, ['tabindex', 'title', 'data-form-type']),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -366,7 +400,15 @@ export class Input implements ComponentInterface {
|
||||
}
|
||||
|
||||
componentDidLoad() {
|
||||
const { mask, nativeInput } = this;
|
||||
|
||||
this.originalIonInput = this.ionInput;
|
||||
|
||||
if (mask !== undefined && nativeInput) {
|
||||
this.maskController = new MaskController(nativeInput, {
|
||||
mask,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
@@ -377,6 +419,9 @@ export class Input implements ComponentInterface {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Todo - need to evaluate if I need to recreate this in connectedCallback after first load
|
||||
this.maskController?.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { newSpecPage } from '@stencil/core/testing';
|
||||
|
||||
import { Input } from '../input';
|
||||
|
||||
it('should inherit attributes', async () => {
|
||||
@@ -7,7 +8,7 @@ it('should inherit attributes', async () => {
|
||||
html: '<ion-input title="my title" tabindex="-1" data-form-type="password"></ion-input>',
|
||||
});
|
||||
|
||||
const nativeEl = page.body.querySelector('ion-input input');
|
||||
const nativeEl = page.body.querySelector('ion-input input')!;
|
||||
expect(nativeEl.getAttribute('title')).toBe('my title');
|
||||
expect(nativeEl.getAttribute('tabindex')).toBe('-1');
|
||||
expect(nativeEl.getAttribute('data-form-type')).toBe('password');
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { newSpecPage } from '@stencil/core/testing';
|
||||
|
||||
import { Input } from '../input';
|
||||
|
||||
it('should render bottom content when helper text is defined', async () => {
|
||||
|
||||
81
core/src/components/input/test/mask/index.html
Normal file
@@ -0,0 +1,81 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Input - Mask</title>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
|
||||
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
|
||||
<script src="../../../../../scripts/testing/scripts.js"></script>
|
||||
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
|
||||
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||
<style>
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(250px, 1fr));
|
||||
grid-row-gap: 20px;
|
||||
grid-column-gap: 20px;
|
||||
}
|
||||
h2 {
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
|
||||
color: #6f7378;
|
||||
|
||||
margin-top: 10px;
|
||||
}
|
||||
@media screen and (max-width: 800px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<ion-app>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Input - Item</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content id="content" class="ion-padding" color="light">
|
||||
<div class="grid">
|
||||
<div class="grid-item">
|
||||
<h2>US Phone Number</h2>
|
||||
|
||||
<ion-input id="input-phone-us" label="Phone"></ion-input>
|
||||
</div>
|
||||
</div>
|
||||
</ion-content>
|
||||
</ion-app>
|
||||
|
||||
<script>
|
||||
const inputPhoneUS = document.querySelector('#input-phone-us');
|
||||
inputPhoneUS.mask = [
|
||||
'+',
|
||||
'1',
|
||||
' ',
|
||||
'(',
|
||||
/\d/,
|
||||
/\d/,
|
||||
/\d/,
|
||||
')',
|
||||
' ',
|
||||
/\d/,
|
||||
/\d/,
|
||||
/\d/,
|
||||
'-',
|
||||
/\d/,
|
||||
/\d/,
|
||||
/\d/,
|
||||
/\d/,
|
||||
];
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
present,
|
||||
createDelegateController,
|
||||
createTriggerController,
|
||||
setOverlayId,
|
||||
} from '../../utils/overlays';
|
||||
import type { OverlayEventDetail } from '../../utils/overlays-interface';
|
||||
import type { IonicSafeString } from '../../utils/sanitization';
|
||||
@@ -212,6 +213,7 @@ export class Loading implements ComponentInterface, OverlayInterface {
|
||||
const mode = getIonMode(this);
|
||||
this.spinner = config.get('loadingSpinner', config.get('spinner', mode === 'ios' ? 'lines' : 'crescent'));
|
||||
}
|
||||
setOverlayId(this.el);
|
||||
}
|
||||
|
||||
componentDidLoad() {
|
||||
|
||||
41
core/src/components/loading/test/loading-id.spec.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { newSpecPage } from '@stencil/core/testing';
|
||||
|
||||
import { Loading } from '../loading';
|
||||
|
||||
it('loading should be assigned an incrementing id', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Loading],
|
||||
html: `<ion-loading is-open="true"></ion-loading>`,
|
||||
});
|
||||
let loading: HTMLIonLoadingElement;
|
||||
|
||||
loading = page.body.querySelector('ion-loading')!;
|
||||
|
||||
expect(loading).not.toBe(null);
|
||||
expect(loading.getAttribute('id')).toBe('ion-overlay-1');
|
||||
|
||||
// Remove the loading from the DOM
|
||||
loading.remove();
|
||||
await page.waitForChanges();
|
||||
|
||||
// Create a new loading to verify the id is incremented
|
||||
loading = document.createElement('ion-loading');
|
||||
loading.isOpen = true;
|
||||
page.body.appendChild(loading);
|
||||
await page.waitForChanges();
|
||||
|
||||
loading = page.body.querySelector('ion-loading')!;
|
||||
|
||||
expect(loading.getAttribute('id')).toBe('ion-overlay-2');
|
||||
|
||||
// Presenting the same loading again should reuse the existing id
|
||||
|
||||
loading.isOpen = false;
|
||||
await page.waitForChanges();
|
||||
loading.isOpen = true;
|
||||
await page.waitForChanges();
|
||||
|
||||
loading = page.body.querySelector('ion-loading')!;
|
||||
|
||||
expect(loading.getAttribute('id')).toBe('ion-overlay-2');
|
||||
});
|
||||
@@ -1,15 +1,16 @@
|
||||
import { newSpecPage } from '@stencil/core/testing';
|
||||
import { Loading } from '../loading';
|
||||
import { config } from '../../../global/config';
|
||||
|
||||
describe('alert: custom html', () => {
|
||||
import { config } from '../../../global/config';
|
||||
import { Loading } from '../loading';
|
||||
|
||||
describe('loading: custom html', () => {
|
||||
it('should not 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');
|
||||
const content = page.body.querySelector('.loading-content')!;
|
||||
expect(content.textContent).toContain('Custom Text');
|
||||
expect(content.querySelector('button.custom-html')).toBe(null);
|
||||
});
|
||||
@@ -21,7 +22,7 @@ describe('alert: custom html', () => {
|
||||
html: `<ion-loading message="<button class='custom-html'>Custom Text</button>"></ion-loading>`,
|
||||
});
|
||||
|
||||
const content = page.body.querySelector('.loading-content');
|
||||
const content = page.body.querySelector('.loading-content')!;
|
||||
expect(content.textContent).toContain('Custom Text');
|
||||
expect(content.querySelector('button.custom-html')).not.toBe(null);
|
||||
});
|
||||
@@ -33,7 +34,7 @@ describe('alert: custom html', () => {
|
||||
html: `<ion-loading message="<button class='custom-html'>Custom Text</button>"></ion-loading>`,
|
||||
});
|
||||
|
||||
const content = page.body.querySelector('.loading-content');
|
||||
const content = page.body.querySelector('.loading-content')!;
|
||||
expect(content.textContent).toContain('Custom Text');
|
||||
expect(content.querySelector('button.custom-html')).toBe(null);
|
||||
});
|
||||
|
||||
@@ -28,10 +28,11 @@ import {
|
||||
prepareOverlay,
|
||||
present,
|
||||
createTriggerController,
|
||||
setOverlayId,
|
||||
} from '../../utils/overlays';
|
||||
import type { OverlayEventDetail } from '../../utils/overlays-interface';
|
||||
import { getClassMap } from '../../utils/theme';
|
||||
import { deepReady } from '../../utils/transition';
|
||||
import { deepReady, waitForMount } from '../../utils/transition';
|
||||
|
||||
import { iosEnterAnimation } from './animations/ios.enter';
|
||||
import { iosLeaveAnimation } from './animations/ios.leave';
|
||||
@@ -65,8 +66,6 @@ import { setCardStatusBarDark, setCardStatusBarDefault } from './utils';
|
||||
export class Modal implements ComponentInterface, OverlayInterface {
|
||||
private readonly triggerController = createTriggerController();
|
||||
private gesture?: Gesture;
|
||||
private modalIndex = modalIds++;
|
||||
private modalId?: string;
|
||||
private coreDelegate: FrameworkDelegate = CoreDelegate();
|
||||
private currentTransition?: Promise<any>;
|
||||
private sheetTransition?: Promise<any>;
|
||||
@@ -316,6 +315,16 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
*/
|
||||
@Event({ eventName: 'didDismiss' }) didDismissShorthand!: EventEmitter<OverlayEventDetail>;
|
||||
|
||||
/**
|
||||
* Emitted before the modal has presented, but after the component
|
||||
* has been mounted in the DOM.
|
||||
* This event exists so iOS can run the entering
|
||||
* transition properly
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
@Event() ionMount!: EventEmitter<void>;
|
||||
|
||||
breakpointsChanged(breakpoints: number[] | undefined) {
|
||||
if (breakpoints !== undefined) {
|
||||
this.sortedBreakpoints = breakpoints.sort((a, b) => a - b);
|
||||
@@ -334,16 +343,10 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
|
||||
componentWillLoad() {
|
||||
const { breakpoints, initialBreakpoint, el } = this;
|
||||
const isSheetModal = (this.isSheetModal = breakpoints !== undefined && initialBreakpoint !== undefined);
|
||||
|
||||
this.inheritedAttributes = inheritAttributes(el, ['aria-label', 'role']);
|
||||
|
||||
/**
|
||||
* If user has custom ID set then we should
|
||||
* not assign the default incrementing ID.
|
||||
*/
|
||||
this.modalId = this.el.hasAttribute('id') ? this.el.getAttribute('id')! : `ion-modal-${this.modalIndex}`;
|
||||
const isSheetModal = (this.isSheetModal = breakpoints !== undefined && initialBreakpoint !== undefined);
|
||||
|
||||
if (isSheetModal) {
|
||||
this.currentBreakpoint = this.initialBreakpoint;
|
||||
}
|
||||
@@ -351,6 +354,8 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
if (breakpoints !== undefined && initialBreakpoint !== undefined && !breakpoints.includes(initialBreakpoint)) {
|
||||
printIonWarning('Your breakpoints array must include the initialBreakpoint value.');
|
||||
}
|
||||
|
||||
setOverlayId(el);
|
||||
}
|
||||
|
||||
componentDidLoad() {
|
||||
@@ -443,7 +448,30 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
|
||||
const { inline, delegate } = this.getDelegate(true);
|
||||
this.usersElement = await attachComponent(delegate, el, this.component, ['ion-page'], this.componentProps, inline);
|
||||
hasLazyBuild(el) && (await deepReady(this.usersElement));
|
||||
|
||||
this.ionMount.emit();
|
||||
|
||||
/**
|
||||
* When using the lazy loaded build of Stencil, we need to wait
|
||||
* for every Stencil component instance to be ready before presenting
|
||||
* otherwise there can be a flash of unstyled content. With the
|
||||
* custom elements bundle we need to wait for the JS framework
|
||||
* mount the inner contents of the overlay otherwise WebKit may
|
||||
* get the transition incorrect.
|
||||
*/
|
||||
if (hasLazyBuild(el)) {
|
||||
await deepReady(this.usersElement);
|
||||
/**
|
||||
* If keepContentsMounted="true" then the
|
||||
* JS Framework has already mounted the inner
|
||||
* contents so there is no need to wait.
|
||||
* Otherwise, we need to wait for the JS
|
||||
* Framework to mount the inner contents
|
||||
* of this component.
|
||||
*/
|
||||
} else if (!this.keepContentsMounted) {
|
||||
await waitForMount();
|
||||
}
|
||||
|
||||
writeTask(() => this.el.classList.add('show-modal'));
|
||||
|
||||
@@ -828,7 +856,6 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
|
||||
const showHandle = handle !== false && isSheetModal;
|
||||
const mode = getIonMode(this);
|
||||
const { modalId } = this;
|
||||
const isCardModal = presentingElement !== undefined && mode === 'ios';
|
||||
const isHandleCycle = handleBehavior === 'cycle';
|
||||
|
||||
@@ -848,7 +875,6 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
'overlay-hidden': true,
|
||||
...getClassMap(this.cssClass),
|
||||
}}
|
||||
id={modalId}
|
||||
onIonBackdropTap={this.onBackdropTap}
|
||||
onIonModalDidPresent={this.onLifecycle}
|
||||
onIonModalWillPresent={this.onLifecycle}
|
||||
@@ -902,8 +928,6 @@ const LIFECYCLE_MAP: any = {
|
||||
ionModalDidDismiss: 'ionViewDidLeave',
|
||||
};
|
||||
|
||||
let modalIds = 0;
|
||||
|
||||
interface ModalOverlayOptions {
|
||||
/**
|
||||
* The element that presented the modal.
|
||||
|
||||
41
core/src/components/modal/test/modal-id.spec.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { newSpecPage } from '@stencil/core/testing';
|
||||
|
||||
import { Modal } from '../modal';
|
||||
|
||||
it('modal should be assigned an incrementing id', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Modal],
|
||||
html: `<ion-modal is-open="true"></ion-modal>`,
|
||||
});
|
||||
let modal: HTMLIonModalElement;
|
||||
|
||||
modal = page.body.querySelector('ion-modal')!;
|
||||
|
||||
expect(modal).not.toBe(null);
|
||||
expect(modal.getAttribute('id')).toBe('ion-overlay-1');
|
||||
|
||||
// Remove the modal from the DOM
|
||||
modal.remove();
|
||||
await page.waitForChanges();
|
||||
|
||||
// Create a new modal to verify the id is incremented
|
||||
modal = document.createElement('ion-modal');
|
||||
modal.isOpen = true;
|
||||
page.body.appendChild(modal);
|
||||
await page.waitForChanges();
|
||||
|
||||
modal = page.body.querySelector('ion-modal')!;
|
||||
|
||||
expect(modal.getAttribute('id')).toBe('ion-overlay-2');
|
||||
|
||||
// Presenting the same modal again should reuse the existing id
|
||||
|
||||
modal.isOpen = false;
|
||||
await page.waitForChanges();
|
||||
modal.isOpen = true;
|
||||
await page.waitForChanges();
|
||||
|
||||
modal = page.body.querySelector('ion-modal')!;
|
||||
|
||||
expect(modal.getAttribute('id')).toBe('ion-overlay-2');
|
||||
});
|
||||
@@ -1,76 +0,0 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { test } from '@utils/test/playwright';
|
||||
|
||||
test.describe('picker-column-internal', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/src/components/picker-column-internal/test/basic');
|
||||
});
|
||||
|
||||
test('should render a picker item for each item', async ({ page }) => {
|
||||
const columns = page.locator('ion-picker-column-internal .picker-item:not(.picker-item-empty)');
|
||||
await expect(columns).toHaveCount(24);
|
||||
});
|
||||
|
||||
test('should render 6 empty picker items', async ({ page }) => {
|
||||
const columns = page.locator('ion-picker-column-internal .picker-item-empty');
|
||||
await expect(columns).toHaveCount(6);
|
||||
});
|
||||
|
||||
test('should not have an active item when value is not set', async ({ page }) => {
|
||||
const activeColumn = page.locator('ion-picker-column-internal .picker-item-active');
|
||||
await expect(activeColumn).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('should have an active item when value is set', async ({ page }) => {
|
||||
await page.locator('#default').evaluate((el: HTMLIonPickerColumnInternalElement) => {
|
||||
el.value = '12';
|
||||
});
|
||||
await page.waitForChanges();
|
||||
|
||||
const activeColumn = page.locator('ion-picker-column-internal .picker-item-active');
|
||||
|
||||
expect(activeColumn).not.toBeNull();
|
||||
});
|
||||
|
||||
// TODO FW-3616
|
||||
test.skip('scrolling should change the active item', async ({ page, skip }) => {
|
||||
skip.browser('firefox', 'https://bugzilla.mozilla.org/show_bug.cgi?id=1766890');
|
||||
|
||||
await page.locator('#default').evaluate((el: HTMLIonPickerColumnInternalElement) => {
|
||||
el.scrollTop = 801;
|
||||
});
|
||||
await page.waitForChanges();
|
||||
|
||||
const activeColumn = page.locator('ion-picker-column-internal .picker-item-active');
|
||||
|
||||
expect(await activeColumn?.innerText()).toEqual('23');
|
||||
});
|
||||
|
||||
test('should not emit ionChange when the value is modified externally', async ({ page, skip }) => {
|
||||
skip.browser('firefox', 'https://bugzilla.mozilla.org/show_bug.cgi?id=1766890');
|
||||
|
||||
const ionChangeSpy = await page.spyOnEvent('ionChange');
|
||||
|
||||
await page.locator('#default').evaluate((el: HTMLIonPickerColumnInternalElement) => {
|
||||
el.value = '12';
|
||||
});
|
||||
|
||||
expect(ionChangeSpy).not.toHaveReceivedEvent();
|
||||
});
|
||||
|
||||
// TODO FW-3616
|
||||
test.skip('should emit ionChange when the picker is scrolled', async ({ page, skip }) => {
|
||||
skip.browser('firefox', 'https://bugzilla.mozilla.org/show_bug.cgi?id=1766890');
|
||||
|
||||
const ionChangeSpy = await page.spyOnEvent('ionChange');
|
||||
|
||||
await page.locator('#default').evaluate((el: HTMLIonPickerColumnInternalElement) => {
|
||||
el.scrollTo(0, el.scrollHeight);
|
||||
});
|
||||
await page.waitForChanges();
|
||||
|
||||
await ionChangeSpy.next();
|
||||
|
||||
expect(ionChangeSpy).toHaveReceivedEvent();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
/**
|
||||
* This behavior does not vary across modes/directions.
|
||||
*/
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('picker-column-internal'), () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/src/components/picker-column-internal/test/basic', config);
|
||||
});
|
||||
|
||||
test('should render a picker item for each item', async ({ page }) => {
|
||||
const columns = page.locator('ion-picker-column-internal .picker-item:not(.picker-item-empty)');
|
||||
await expect(columns).toHaveCount(24);
|
||||
});
|
||||
|
||||
test('should render 6 empty picker items', async ({ page }) => {
|
||||
const columns = page.locator('ion-picker-column-internal .picker-item-empty');
|
||||
await expect(columns).toHaveCount(6);
|
||||
});
|
||||
|
||||
test('should not have an active item when value is not set', async ({ page }) => {
|
||||
const activeColumn = page.locator('ion-picker-column-internal .picker-item-active');
|
||||
await expect(activeColumn).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('should have an active item when value is set', async ({ page }) => {
|
||||
await page.locator('#default').evaluate((el: HTMLIonPickerColumnInternalElement) => {
|
||||
el.value = '12';
|
||||
});
|
||||
await page.waitForChanges();
|
||||
|
||||
const activeColumn = page.locator('ion-picker-column-internal .picker-item-active');
|
||||
|
||||
expect(activeColumn).not.toBeNull();
|
||||
});
|
||||
|
||||
// TODO FW-3616
|
||||
test.skip('scrolling should change the active item', async ({ page, skip }) => {
|
||||
skip.browser('firefox', 'https://bugzilla.mozilla.org/show_bug.cgi?id=1766890');
|
||||
|
||||
await page.locator('#default').evaluate((el: HTMLIonPickerColumnInternalElement) => {
|
||||
el.scrollTop = 801;
|
||||
});
|
||||
await page.waitForChanges();
|
||||
|
||||
const activeColumn = page.locator('ion-picker-column-internal .picker-item-active');
|
||||
|
||||
expect(await activeColumn?.innerText()).toEqual('23');
|
||||
});
|
||||
|
||||
test('should not emit ionChange when the value is modified externally', async ({ page, skip }) => {
|
||||
skip.browser('firefox', 'https://bugzilla.mozilla.org/show_bug.cgi?id=1766890');
|
||||
|
||||
const ionChangeSpy = await page.spyOnEvent('ionChange');
|
||||
|
||||
await page.locator('#default').evaluate((el: HTMLIonPickerColumnInternalElement) => {
|
||||
el.value = '12';
|
||||
});
|
||||
|
||||
expect(ionChangeSpy).not.toHaveReceivedEvent();
|
||||
});
|
||||
|
||||
// TODO FW-3616
|
||||
test.skip('should emit ionChange when the picker is scrolled', async ({ page, skip }) => {
|
||||
skip.browser('firefox', 'https://bugzilla.mozilla.org/show_bug.cgi?id=1766890');
|
||||
|
||||
const ionChangeSpy = await page.spyOnEvent('ionChange');
|
||||
|
||||
await page.locator('#default').evaluate((el: HTMLIonPickerColumnInternalElement) => {
|
||||
el.scrollTo(0, el.scrollHeight);
|
||||
});
|
||||
await page.waitForChanges();
|
||||
|
||||
await ionChangeSpy.next();
|
||||
|
||||
expect(ionChangeSpy).toHaveReceivedEvent();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,130 +0,0 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { test } from '@utils/test/playwright';
|
||||
|
||||
test.describe('picker-column-internal: disabled', () => {
|
||||
test('should not have visual regressions', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-picker-internal>
|
||||
<ion-picker-column-internal value="b"></ion-picker-column-internal>
|
||||
</ion-picker-internal>
|
||||
|
||||
<script>
|
||||
const column = document.querySelector('ion-picker-column-internal');
|
||||
column.items = [
|
||||
{ text: 'A', value: 'a', disabled: true },
|
||||
{ text: 'B', value: 'b' },
|
||||
{ text: 'C', value: 'c', disabled: true }
|
||||
]
|
||||
</script>
|
||||
`);
|
||||
|
||||
const picker = page.locator('ion-picker-internal');
|
||||
await expect(picker).toHaveScreenshot(`picker-internal-disabled-${page.getSnapshotSettings()}.png`);
|
||||
});
|
||||
test('all picker items should be enabled by default', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-picker-internal>
|
||||
<ion-picker-column-internal></ion-picker-column-internal>
|
||||
</ion-picker-internal>
|
||||
|
||||
<script>
|
||||
const column = document.querySelector('ion-picker-column-internal');
|
||||
column.items = [
|
||||
{ text: 'A', value: 'a' },
|
||||
{ text: 'B', value: 'b' },
|
||||
{ text: 'C', value: 'c' }
|
||||
]
|
||||
</script>
|
||||
`);
|
||||
|
||||
const pickerItems = page.locator(
|
||||
'ion-picker-column-internal .picker-item:not(.picker-item-empty, .picker-item-disabled)'
|
||||
);
|
||||
|
||||
expect(await pickerItems.count()).toBe(3);
|
||||
});
|
||||
test('disabled picker item should not be interactive', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-picker-internal>
|
||||
<ion-picker-column-internal></ion-picker-column-internal>
|
||||
</ion-picker-internal>
|
||||
|
||||
<script>
|
||||
const column = document.querySelector('ion-picker-column-internal');
|
||||
column.items = [
|
||||
{ text: 'A', value: 'a' },
|
||||
{ text: 'B', value: 'b', disabled: true },
|
||||
{ text: 'C', value: 'c' }
|
||||
]
|
||||
</script>
|
||||
`);
|
||||
|
||||
const disabledItem = page.locator('ion-picker-column-internal .picker-item.picker-item-disabled');
|
||||
await expect(disabledItem).not.toBeEnabled();
|
||||
});
|
||||
test('disabled picker item should not be considered active', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-picker-internal>
|
||||
<ion-picker-column-internal value="b"></ion-picker-column-internal>
|
||||
</ion-picker-internal>
|
||||
|
||||
<script>
|
||||
const column = document.querySelector('ion-picker-column-internal');
|
||||
column.items = [
|
||||
{ text: 'A', value: 'a' },
|
||||
{ text: 'B', value: 'b', disabled: true },
|
||||
{ text: 'C', value: 'c' }
|
||||
]
|
||||
</script>
|
||||
`);
|
||||
|
||||
const disabledItem = page.locator('ion-picker-column-internal .picker-item[data-value="b"]');
|
||||
await expect(disabledItem).not.toHaveClass(/picker-item-active/);
|
||||
});
|
||||
test('setting the value to a disabled item should not cause that item to be active', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-picker-internal>
|
||||
<ion-picker-column-internal></ion-picker-column-internal>
|
||||
</ion-picker-internal>
|
||||
|
||||
<script>
|
||||
const column = document.querySelector('ion-picker-column-internal');
|
||||
column.items = [
|
||||
{ text: 'A', value: 'a' },
|
||||
{ text: 'B', value: 'b', disabled: true },
|
||||
{ text: 'C', value: 'c' }
|
||||
]
|
||||
</script>
|
||||
`);
|
||||
|
||||
const pickerColumn = page.locator('ion-picker-column-internal');
|
||||
await pickerColumn.evaluate((el: HTMLIonPickerColumnInternalElement) => (el.value = 'b'));
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
const disabledItem = page.locator('ion-picker-column-internal .picker-item[data-value="b"]');
|
||||
await expect(disabledItem).toHaveClass(/picker-item-disabled/);
|
||||
await expect(disabledItem).not.toHaveClass(/picker-item-active/);
|
||||
});
|
||||
test('defaulting the value to a disabled item should not cause that item to be active', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-picker-internal>
|
||||
<ion-picker-column-internal></ion-picker-column-internal>
|
||||
</ion-picker-internal>
|
||||
|
||||
<script>
|
||||
const column = document.querySelector('ion-picker-column-internal');
|
||||
column.items = [
|
||||
{ text: 'A', value: 'a' },
|
||||
{ text: 'B', value: 'b', disabled: true },
|
||||
{ text: 'C', value: 'c' }
|
||||
]
|
||||
column.value = 'b'
|
||||
</script>
|
||||
`);
|
||||
|
||||
const disabledItem = page.locator('ion-picker-column-internal .picker-item[data-value="b"]');
|
||||
await expect(disabledItem).toHaveClass(/picker-item-disabled/);
|
||||
await expect(disabledItem).not.toHaveClass(/picker-item-active/);
|
||||
});
|
||||
});
|
||||
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,161 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
/**
|
||||
* This behavior does not vary across directions.
|
||||
*/
|
||||
configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('picker-column-internal: disabled rendering'), () => {
|
||||
test('should not have visual regressions', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-picker-internal>
|
||||
<ion-picker-column-internal value="b"></ion-picker-column-internal>
|
||||
</ion-picker-internal>
|
||||
|
||||
<script>
|
||||
const column = document.querySelector('ion-picker-column-internal');
|
||||
column.items = [
|
||||
{ text: 'A', value: 'a', disabled: true },
|
||||
{ text: 'B', value: 'b' },
|
||||
{ text: 'C', value: 'c', disabled: true }
|
||||
]
|
||||
</script>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const picker = page.locator('ion-picker-internal');
|
||||
await expect(picker).toHaveScreenshot(screenshot(`picker-internal-disabled`));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* This behavior does not vary across modes/directions.
|
||||
*/
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('picker-column-internal: disabled'), () => {
|
||||
test('all picker items should be enabled by default', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-picker-internal>
|
||||
<ion-picker-column-internal></ion-picker-column-internal>
|
||||
</ion-picker-internal>
|
||||
|
||||
<script>
|
||||
const column = document.querySelector('ion-picker-column-internal');
|
||||
column.items = [
|
||||
{ text: 'A', value: 'a' },
|
||||
{ text: 'B', value: 'b' },
|
||||
{ text: 'C', value: 'c' }
|
||||
]
|
||||
</script>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const pickerItems = page.locator(
|
||||
'ion-picker-column-internal .picker-item:not(.picker-item-empty, .picker-item-disabled)'
|
||||
);
|
||||
|
||||
expect(await pickerItems.count()).toBe(3);
|
||||
});
|
||||
test('disabled picker item should not be interactive', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-picker-internal>
|
||||
<ion-picker-column-internal></ion-picker-column-internal>
|
||||
</ion-picker-internal>
|
||||
|
||||
<script>
|
||||
const column = document.querySelector('ion-picker-column-internal');
|
||||
column.items = [
|
||||
{ text: 'A', value: 'a' },
|
||||
{ text: 'B', value: 'b', disabled: true },
|
||||
{ text: 'C', value: 'c' }
|
||||
]
|
||||
</script>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const disabledItem = page.locator('ion-picker-column-internal .picker-item.picker-item-disabled');
|
||||
await expect(disabledItem).not.toBeEnabled();
|
||||
});
|
||||
test('disabled picker item should not be considered active', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-picker-internal>
|
||||
<ion-picker-column-internal value="b"></ion-picker-column-internal>
|
||||
</ion-picker-internal>
|
||||
|
||||
<script>
|
||||
const column = document.querySelector('ion-picker-column-internal');
|
||||
column.items = [
|
||||
{ text: 'A', value: 'a' },
|
||||
{ text: 'B', value: 'b', disabled: true },
|
||||
{ text: 'C', value: 'c' }
|
||||
]
|
||||
</script>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const disabledItem = page.locator('ion-picker-column-internal .picker-item[data-value="b"]');
|
||||
await expect(disabledItem).not.toHaveClass(/picker-item-active/);
|
||||
});
|
||||
test('setting the value to a disabled item should not cause that item to be active', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-picker-internal>
|
||||
<ion-picker-column-internal></ion-picker-column-internal>
|
||||
</ion-picker-internal>
|
||||
|
||||
<script>
|
||||
const column = document.querySelector('ion-picker-column-internal');
|
||||
column.items = [
|
||||
{ text: 'A', value: 'a' },
|
||||
{ text: 'B', value: 'b', disabled: true },
|
||||
{ text: 'C', value: 'c' }
|
||||
]
|
||||
</script>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const pickerColumn = page.locator('ion-picker-column-internal');
|
||||
await pickerColumn.evaluate((el: HTMLIonPickerColumnInternalElement) => (el.value = 'b'));
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
const disabledItem = page.locator('ion-picker-column-internal .picker-item[data-value="b"]');
|
||||
await expect(disabledItem).toHaveClass(/picker-item-disabled/);
|
||||
await expect(disabledItem).not.toHaveClass(/picker-item-active/);
|
||||
});
|
||||
test('defaulting the value to a disabled item should not cause that item to be active', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-picker-internal>
|
||||
<ion-picker-column-internal></ion-picker-column-internal>
|
||||
</ion-picker-internal>
|
||||
|
||||
<script>
|
||||
const column = document.querySelector('ion-picker-column-internal');
|
||||
column.items = [
|
||||
{ text: 'A', value: 'a' },
|
||||
{ text: 'B', value: 'b', disabled: true },
|
||||
{ text: 'C', value: 'c' }
|
||||
]
|
||||
column.value = 'b'
|
||||
</script>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const disabledItem = page.locator('ion-picker-column-internal .picker-item[data-value="b"]');
|
||||
await expect(disabledItem).toHaveClass(/picker-item-disabled/);
|
||||
await expect(disabledItem).not.toHaveClass(/picker-item-active/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
@@ -1,13 +0,0 @@
|
||||
import AxeBuilder from '@axe-core/playwright';
|
||||
import { expect } from '@playwright/test';
|
||||
import { test } from '@utils/test/playwright';
|
||||
|
||||
test.describe('picker-internal: a11y', () => {
|
||||
test('should not have accessibility violations', async ({ page }) => {
|
||||
await page.goto(`/src/components/picker-internal/test/a11y`);
|
||||
|
||||
const results = await new AxeBuilder({ page }).analyze();
|
||||
|
||||
expect(results.violations).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import AxeBuilder from '@axe-core/playwright';
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
configs().forEach(({ title, config }) => {
|
||||
test.describe(title('picker-internal: a11y'), () => {
|
||||
test('should not have accessibility violations', async ({ page }) => {
|
||||
await page.goto(`/src/components/picker-internal/test/a11y`, config);
|
||||
|
||||
const results = await new AxeBuilder({ page }).analyze();
|
||||
|
||||
expect(results.violations).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 14 KiB |
@@ -1,21 +1,88 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { test } from '@utils/test/playwright';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
test.describe('picker-internal', () => {
|
||||
// TODO: FW-3020
|
||||
test.skip('inline pickers should not have visual regression', async ({ page }) => {
|
||||
await page.goto(`/src/components/picker-internal/test/basic`);
|
||||
// TODO: FW-3020
|
||||
configs().forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('picker-internal: rendering'), () => {
|
||||
test.skip('inline pickers should not have visual regression', async ({ page }) => {
|
||||
await page.goto(`/src/components/picker-internal/test/basic`, config);
|
||||
|
||||
await page.setIonViewport();
|
||||
await page.setIonViewport();
|
||||
|
||||
await expect(page).toHaveScreenshot(`picker-internal-inline-diff-${page.getSnapshotSettings()}.png`, {
|
||||
fullPage: true,
|
||||
await expect(page).toHaveScreenshot(screenshot(`picker-internal-inline-diff`), {
|
||||
fullPage: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('picker-internal: focus', () => {
|
||||
/**
|
||||
* This behavior does not vary across modes.
|
||||
*/
|
||||
configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('picker-internal: overlay rendering'), () => {
|
||||
test('popover: should not have visual regression', async ({ page }) => {
|
||||
await page.goto(`/src/components/picker-internal/test/basic`, config);
|
||||
|
||||
const button = page.locator('#popover');
|
||||
const didPresent = await page.spyOnEvent('ionPopoverDidPresent');
|
||||
const pickerInternal = page.locator('ion-popover ion-picker-internal');
|
||||
|
||||
await button.click();
|
||||
await didPresent.next();
|
||||
|
||||
await expect(pickerInternal).toBeVisible();
|
||||
|
||||
const popoverContent = page.locator('ion-popover .ion-delegate-host');
|
||||
|
||||
await expect(popoverContent).toHaveScreenshot(screenshot(`picker-internal-popover-diff`), {
|
||||
/**
|
||||
* Animations must be enabled to capture the screenshot.
|
||||
* By default, animations are disabled with toHaveScreenshot,
|
||||
* and when capturing the screenshot will call animation.finish().
|
||||
* This will cause the popover to close and the screenshot capture
|
||||
* to be invalid.
|
||||
*/
|
||||
animations: 'allow',
|
||||
});
|
||||
});
|
||||
|
||||
test('modal: should not have visual regression', async ({ page }) => {
|
||||
await page.goto('/src/components/picker-internal/test/basic', config);
|
||||
|
||||
const button = page.locator('#modal');
|
||||
const didPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
const pickerInternal = page.locator('ion-modal ion-picker-internal');
|
||||
|
||||
await button.click();
|
||||
await didPresent.next();
|
||||
|
||||
await expect(pickerInternal).toBeVisible();
|
||||
|
||||
const modalContent = page.locator('ion-modal .ion-delegate-host');
|
||||
|
||||
await expect(modalContent).toHaveScreenshot(screenshot(`picker-internal-modal-diff`), {
|
||||
/**
|
||||
* Animations must be enabled to capture the screenshot.
|
||||
* By default, animations are disabled with toHaveScreenshot,
|
||||
* and when capturing the screenshot will call animation.finish().
|
||||
* This will cause the modal to close and the screenshot capture
|
||||
* to be invalid.
|
||||
*/
|
||||
animations: 'allow',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* This behavior does not vary across modes/directions.
|
||||
*/
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('picker-internal: focus'), () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.setContent(`
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-picker-internal>
|
||||
<ion-picker-column-internal value="full-stack" id="first"></ion-picker-column-internal>
|
||||
<ion-picker-column-internal value="onion" id="second"></ion-picker-column-internal>
|
||||
@@ -39,7 +106,9 @@ test.describe('picker-internal', () => {
|
||||
{ text: 'Artichoke', value: 'artichoke' },
|
||||
];
|
||||
</script>
|
||||
`);
|
||||
`,
|
||||
config
|
||||
);
|
||||
});
|
||||
|
||||
test('tabbing should correctly move focus between columns', async ({ page }) => {
|
||||
@@ -71,58 +140,4 @@ test.describe('picker-internal', () => {
|
||||
await expect(firstColumn).toBeFocused();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('within overlay:', () => {
|
||||
test('popover: should not have visual regression', async ({ page }) => {
|
||||
await page.goto(`/src/components/picker-internal/test/basic`);
|
||||
|
||||
const button = page.locator('#popover');
|
||||
const didPresent = await page.spyOnEvent('ionPopoverDidPresent');
|
||||
const pickerInternal = page.locator('ion-popover ion-picker-internal');
|
||||
|
||||
await button.click();
|
||||
await didPresent.next();
|
||||
|
||||
await expect(pickerInternal).toBeVisible();
|
||||
|
||||
const popoverContent = page.locator('ion-popover .ion-delegate-host');
|
||||
|
||||
await expect(popoverContent).toHaveScreenshot(`picker-internal-popover-diff-${page.getSnapshotSettings()}.png`, {
|
||||
/**
|
||||
* Animations must be enabled to capture the screenshot.
|
||||
* By default, animations are disabled with toHaveScreenshot,
|
||||
* and when capturing the screenshot will call animation.finish().
|
||||
* This will cause the popover to close and the screenshot capture
|
||||
* to be invalid.
|
||||
*/
|
||||
animations: 'allow',
|
||||
});
|
||||
});
|
||||
|
||||
test('modal: should not have visual regression', async ({ page }) => {
|
||||
await page.goto('/src/components/picker-internal/test/basic');
|
||||
|
||||
const button = page.locator('#modal');
|
||||
const didPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
const pickerInternal = page.locator('ion-modal ion-picker-internal');
|
||||
|
||||
await button.click();
|
||||
await didPresent.next();
|
||||
|
||||
await expect(pickerInternal).toBeVisible();
|
||||
|
||||
const modalContent = page.locator('ion-modal .ion-delegate-host');
|
||||
|
||||
await expect(modalContent).toHaveScreenshot(`picker-internal-modal-diff-${page.getSnapshotSettings()}.png`, {
|
||||
/**
|
||||
* Animations must be enabled to capture the screenshot.
|
||||
* By default, animations are disabled with toHaveScreenshot,
|
||||
* and when capturing the screenshot will call animation.finish().
|
||||
* This will cause the modal to close and the screenshot capture
|
||||
* to be invalid.
|
||||
*/
|
||||
animations: 'allow',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
@@ -1,123 +0,0 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { test } from '@utils/test/playwright';
|
||||
import type { E2ELocator } from '@utils/test/playwright/page/utils/locator';
|
||||
|
||||
test.describe('picker-internal: keyboard entry', () => {
|
||||
test('should scroll to and update the value prop for a single column', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-picker-internal>
|
||||
<ion-picker-column-internal></ion-picker-column-internal>
|
||||
</ion-picker-internal>
|
||||
|
||||
<script>
|
||||
const column = document.querySelector('ion-picker-column-internal');
|
||||
column.items = [
|
||||
{ text: '01', value: 1 },
|
||||
{ text: '02', value: 2 },
|
||||
{ text: '03', value: 3 },
|
||||
{ text: '04', value: 4 },
|
||||
{ text: '05', value: 5 }
|
||||
];
|
||||
column.value = 5;
|
||||
column.numericInput = true;
|
||||
</script>
|
||||
`);
|
||||
|
||||
const column = page.locator('ion-picker-column-internal');
|
||||
const ionChange = await page.spyOnEvent('ionChange');
|
||||
await column.focus();
|
||||
|
||||
await page.keyboard.press('Digit2');
|
||||
|
||||
await expect(ionChange).toHaveReceivedEventDetail({ text: '02', value: 2 });
|
||||
await expect(column).toHaveJSProperty('value', 2);
|
||||
});
|
||||
|
||||
test('should scroll to and update the value prop for multiple columns', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-picker-internal>
|
||||
<ion-picker-column-internal id="first"></ion-picker-column-internal>
|
||||
<ion-picker-column-internal id="second"></ion-picker-column-internal>
|
||||
</ion-picker-internal>
|
||||
|
||||
<script>
|
||||
const firstColumn = document.querySelector('ion-picker-column-internal#first');
|
||||
firstColumn.items = [
|
||||
{ text: '01', value: 1 },
|
||||
{ text: '02', value: 2 },
|
||||
{ text: '03', value: 3 },
|
||||
{ text: '04', value: 4 },
|
||||
{ text: '05', value: 5 }
|
||||
];
|
||||
firstColumn.value = 5;
|
||||
firstColumn.numericInput = true;
|
||||
|
||||
const secondColumn = document.querySelector('ion-picker-column-internal#second');
|
||||
secondColumn.items = [
|
||||
{ text: '20', value: 20 },
|
||||
{ text: '21', value: 21 },
|
||||
{ text: '22', value: 22 },
|
||||
{ text: '23', value: 23 },
|
||||
{ text: '24', value: 24 }
|
||||
];
|
||||
secondColumn.value = 22;
|
||||
secondColumn.numericInput = true;
|
||||
</script>
|
||||
`);
|
||||
|
||||
const firstColumn = page.locator('ion-picker-column-internal#first');
|
||||
const secondColumn = page.locator('ion-picker-column-internal#second');
|
||||
const highlight = page.locator('ion-picker-internal .picker-highlight');
|
||||
const firstIonChange = await (firstColumn as E2ELocator).spyOnEvent('ionChange');
|
||||
const secondIonChange = await (secondColumn as E2ELocator).spyOnEvent('ionChange');
|
||||
|
||||
const box = await highlight.boundingBox();
|
||||
if (box !== null) {
|
||||
await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);
|
||||
}
|
||||
|
||||
await expect(firstColumn).toHaveClass(/picker-column-active/);
|
||||
await expect(secondColumn).toHaveClass(/picker-column-active/);
|
||||
|
||||
await page.keyboard.press('Digit2');
|
||||
|
||||
await expect(firstIonChange).toHaveReceivedEventDetail({ text: '02', value: 2 });
|
||||
await expect(firstColumn).toHaveJSProperty('value', 2);
|
||||
|
||||
await page.keyboard.press('Digit2+Digit4');
|
||||
|
||||
await expect(secondIonChange).toHaveReceivedEventDetail({ text: '24', value: 24 });
|
||||
await expect(secondColumn).toHaveJSProperty('value', 24);
|
||||
});
|
||||
|
||||
test('should select 00', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-picker-internal>
|
||||
<ion-picker-column-internal></ion-picker-column-internal>
|
||||
</ion-picker-internal>
|
||||
|
||||
<script>
|
||||
const column = document.querySelector('ion-picker-column-internal');
|
||||
column.items = [
|
||||
{ text: '00', value: 12 },
|
||||
{ text: '01', value: 1 },
|
||||
{ text: '02', value: 2 },
|
||||
{ text: '03', value: 3 },
|
||||
{ text: '04', value: 4 },
|
||||
{ text: '05', value: 5 }
|
||||
];
|
||||
column.value = 5;
|
||||
column.numericInput = true;
|
||||
</script>
|
||||
`);
|
||||
|
||||
const column = page.locator('ion-picker-column-internal');
|
||||
const ionChange = await page.spyOnEvent('ionChange');
|
||||
await column.focus();
|
||||
|
||||
await page.keyboard.press('Digit0');
|
||||
|
||||
await expect(ionChange).toHaveReceivedEventDetail({ text: '00', value: 12 });
|
||||
await expect(column).toHaveJSProperty('value', 12);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,137 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
import type { E2ELocator } from '@utils/test/playwright/page/utils/locator';
|
||||
|
||||
/**
|
||||
* This behavior does not vary across modes/directions.
|
||||
*/
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('picker-internal: keyboard entry'), () => {
|
||||
test('should scroll to and update the value prop for a single column', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-picker-internal>
|
||||
<ion-picker-column-internal></ion-picker-column-internal>
|
||||
</ion-picker-internal>
|
||||
|
||||
<script>
|
||||
const column = document.querySelector('ion-picker-column-internal');
|
||||
column.items = [
|
||||
{ text: '01', value: 1 },
|
||||
{ text: '02', value: 2 },
|
||||
{ text: '03', value: 3 },
|
||||
{ text: '04', value: 4 },
|
||||
{ text: '05', value: 5 }
|
||||
];
|
||||
column.value = 5;
|
||||
column.numericInput = true;
|
||||
</script>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const column = page.locator('ion-picker-column-internal');
|
||||
const ionChange = await page.spyOnEvent('ionChange');
|
||||
await column.focus();
|
||||
|
||||
await page.keyboard.press('Digit2');
|
||||
|
||||
await expect(ionChange).toHaveReceivedEventDetail({ text: '02', value: 2 });
|
||||
await expect(column).toHaveJSProperty('value', 2);
|
||||
});
|
||||
|
||||
test('should scroll to and update the value prop for multiple columns', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-picker-internal>
|
||||
<ion-picker-column-internal id="first"></ion-picker-column-internal>
|
||||
<ion-picker-column-internal id="second"></ion-picker-column-internal>
|
||||
</ion-picker-internal>
|
||||
|
||||
<script>
|
||||
const firstColumn = document.querySelector('ion-picker-column-internal#first');
|
||||
firstColumn.items = [
|
||||
{ text: '01', value: 1 },
|
||||
{ text: '02', value: 2 },
|
||||
{ text: '03', value: 3 },
|
||||
{ text: '04', value: 4 },
|
||||
{ text: '05', value: 5 }
|
||||
];
|
||||
firstColumn.value = 5;
|
||||
firstColumn.numericInput = true;
|
||||
|
||||
const secondColumn = document.querySelector('ion-picker-column-internal#second');
|
||||
secondColumn.items = [
|
||||
{ text: '20', value: 20 },
|
||||
{ text: '21', value: 21 },
|
||||
{ text: '22', value: 22 },
|
||||
{ text: '23', value: 23 },
|
||||
{ text: '24', value: 24 }
|
||||
];
|
||||
secondColumn.value = 22;
|
||||
secondColumn.numericInput = true;
|
||||
</script>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const firstColumn = page.locator('ion-picker-column-internal#first');
|
||||
const secondColumn = page.locator('ion-picker-column-internal#second');
|
||||
const highlight = page.locator('ion-picker-internal .picker-highlight');
|
||||
const firstIonChange = await (firstColumn as E2ELocator).spyOnEvent('ionChange');
|
||||
const secondIonChange = await (secondColumn as E2ELocator).spyOnEvent('ionChange');
|
||||
|
||||
const box = await highlight.boundingBox();
|
||||
if (box !== null) {
|
||||
await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);
|
||||
}
|
||||
|
||||
await expect(firstColumn).toHaveClass(/picker-column-active/);
|
||||
await expect(secondColumn).toHaveClass(/picker-column-active/);
|
||||
|
||||
await page.keyboard.press('Digit2');
|
||||
|
||||
await expect(firstIonChange).toHaveReceivedEventDetail({ text: '02', value: 2 });
|
||||
await expect(firstColumn).toHaveJSProperty('value', 2);
|
||||
|
||||
await page.keyboard.press('Digit2+Digit4');
|
||||
|
||||
await expect(secondIonChange).toHaveReceivedEventDetail({ text: '24', value: 24 });
|
||||
await expect(secondColumn).toHaveJSProperty('value', 24);
|
||||
});
|
||||
|
||||
test('should select 00', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-picker-internal>
|
||||
<ion-picker-column-internal></ion-picker-column-internal>
|
||||
</ion-picker-internal>
|
||||
|
||||
<script>
|
||||
const column = document.querySelector('ion-picker-column-internal');
|
||||
column.items = [
|
||||
{ text: '00', value: 12 },
|
||||
{ text: '01', value: 1 },
|
||||
{ text: '02', value: 2 },
|
||||
{ text: '03', value: 3 },
|
||||
{ text: '04', value: 4 },
|
||||
{ text: '05', value: 5 }
|
||||
];
|
||||
column.value = 5;
|
||||
column.numericInput = true;
|
||||
</script>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const column = page.locator('ion-picker-column-internal');
|
||||
const ionChange = await page.spyOnEvent('ionChange');
|
||||
await column.focus();
|
||||
|
||||
await page.keyboard.press('Digit0');
|
||||
|
||||
await expect(ionChange).toHaveReceivedEventDetail({ text: '00', value: 12 });
|
||||
await expect(column).toHaveJSProperty('value', 12);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
prepareOverlay,
|
||||
present,
|
||||
safeCall,
|
||||
setOverlayId,
|
||||
} from '../../utils/overlays';
|
||||
import type { OverlayEventDetail } from '../../utils/overlays-interface';
|
||||
import { getClassMap } from '../../utils/theme';
|
||||
@@ -194,6 +195,10 @@ export class Picker implements ComponentInterface, OverlayInterface {
|
||||
this.triggerController.removeClickListener();
|
||||
}
|
||||
|
||||
componentWillLoad() {
|
||||
setOverlayId(this.el);
|
||||
}
|
||||
|
||||
/**
|
||||
* Present the picker overlay after it has been created.
|
||||
*/
|
||||
|
||||
41
core/src/components/picker/test/picker-id.spec.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { newSpecPage } from '@stencil/core/testing';
|
||||
|
||||
import { Picker } from '../picker';
|
||||
|
||||
it('picker should be assigned an incrementing id', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Picker],
|
||||
html: `<ion-picker is-open="true"></ion-picker>`,
|
||||
});
|
||||
let picker: HTMLIonPickerElement;
|
||||
|
||||
picker = page.body.querySelector('ion-picker')!;
|
||||
|
||||
expect(picker).not.toBe(null);
|
||||
expect(picker.getAttribute('id')).toBe('ion-overlay-1');
|
||||
|
||||
// Remove the picker from the DOM
|
||||
picker.remove();
|
||||
await page.waitForChanges();
|
||||
|
||||
// Create a new picker to verify the id is incremented
|
||||
picker = document.createElement('ion-picker');
|
||||
picker.isOpen = true;
|
||||
page.body.appendChild(picker);
|
||||
await page.waitForChanges();
|
||||
|
||||
picker = page.body.querySelector('ion-picker')!;
|
||||
|
||||
expect(picker.getAttribute('id')).toBe('ion-overlay-2');
|
||||
|
||||
// Presenting the same picker again should reuse the existing id
|
||||
|
||||
picker.isOpen = false;
|
||||
await page.waitForChanges();
|
||||
picker.isOpen = true;
|
||||
await page.waitForChanges();
|
||||
|
||||
picker = page.body.querySelector('ion-picker')!;
|
||||
|
||||
expect(picker.getAttribute('id')).toBe('ion-overlay-2');
|
||||
});
|
||||
@@ -6,11 +6,19 @@ import type { AnimationBuilder, ComponentProps, ComponentRef, FrameworkDelegate
|
||||
import { CoreDelegate, attachComponent, detachComponent } from '../../utils/framework-delegate';
|
||||
import { addEventListener, raf, hasLazyBuild } from '../../utils/helpers';
|
||||
import { printIonWarning } from '../../utils/logging';
|
||||
import { BACKDROP, dismiss, eventMethod, focusFirstDescendant, prepareOverlay, present } from '../../utils/overlays';
|
||||
import {
|
||||
BACKDROP,
|
||||
dismiss,
|
||||
eventMethod,
|
||||
focusFirstDescendant,
|
||||
prepareOverlay,
|
||||
present,
|
||||
setOverlayId,
|
||||
} from '../../utils/overlays';
|
||||
import type { OverlayEventDetail } from '../../utils/overlays-interface';
|
||||
import { isPlatform } from '../../utils/platform';
|
||||
import { getClassMap } from '../../utils/theme';
|
||||
import { deepReady } from '../../utils/transition';
|
||||
import { deepReady, waitForMount } from '../../utils/transition';
|
||||
|
||||
import { iosEnterAnimation } from './animations/ios.enter';
|
||||
import { iosLeaveAnimation } from './animations/ios.leave';
|
||||
@@ -49,8 +57,6 @@ export class Popover implements ComponentInterface, PopoverInterface {
|
||||
private usersElement?: HTMLElement;
|
||||
private triggerEl?: HTMLElement | null;
|
||||
private parentPopover: HTMLIonPopoverElement | null = null;
|
||||
private popoverIndex = popoverIds++;
|
||||
private popoverId?: string;
|
||||
private coreDelegate: FrameworkDelegate = CoreDelegate();
|
||||
private currentTransition?: Promise<any>;
|
||||
private destroyTriggerInteraction?: () => void;
|
||||
@@ -338,13 +344,10 @@ export class Popover implements ComponentInterface, PopoverInterface {
|
||||
}
|
||||
|
||||
componentWillLoad() {
|
||||
/**
|
||||
* If user has custom ID set then we should
|
||||
* not assign the default incrementing ID.
|
||||
*/
|
||||
this.popoverId = this.el.hasAttribute('id') ? this.el.getAttribute('id')! : `ion-popover-${this.popoverIndex}`;
|
||||
const { el } = this;
|
||||
const popoverId = setOverlayId(el);
|
||||
|
||||
this.parentPopover = this.el.closest(`ion-popover:not(#${this.popoverId})`) as HTMLIonPopoverElement | null;
|
||||
this.parentPopover = el.closest(`ion-popover:not(#${popoverId})`) as HTMLIonPopoverElement | null;
|
||||
|
||||
if (this.alignment === undefined) {
|
||||
this.alignment = getIonMode(this) === 'ios' ? 'center' : 'start';
|
||||
@@ -455,7 +458,6 @@ export class Popover implements ComponentInterface, PopoverInterface {
|
||||
this.componentProps,
|
||||
inline
|
||||
);
|
||||
hasLazyBuild(el) && (await deepReady(this.usersElement));
|
||||
|
||||
if (!this.keyboardEvents) {
|
||||
this.configureKeyboardInteraction();
|
||||
@@ -464,52 +466,50 @@ export class Popover implements ComponentInterface, PopoverInterface {
|
||||
|
||||
this.ionMount.emit();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
/**
|
||||
* When using the lazy loaded build of Stencil, we need to wait
|
||||
* for every Stencil component instance to be ready before presenting
|
||||
* otherwise there can be a flash of unstyled content. With the
|
||||
* custom elements bundle we need to wait for the JS framework
|
||||
* mount the inner contents of the overlay otherwise WebKit may
|
||||
* get the transition incorrect.
|
||||
*/
|
||||
if (hasLazyBuild(el)) {
|
||||
await deepReady(this.usersElement);
|
||||
/**
|
||||
* Wait two request animation frame loops before presenting the popover.
|
||||
* This allows the framework implementations enough time to mount
|
||||
* the popover contents, so the bounding box is set when the popover
|
||||
* transition starts.
|
||||
*
|
||||
* On Angular and React, a single raf is enough time, but for Vue
|
||||
* we need to wait two rafs. As a result we are using two rafs for
|
||||
* all frameworks to ensure the popover is presented correctly.
|
||||
* If keepContentsMounted="true" then the
|
||||
* JS Framework has already mounted the inner
|
||||
* contents so there is no need to wait.
|
||||
* Otherwise, we need to wait for the JS
|
||||
* Framework to mount the inner contents
|
||||
* of this component.
|
||||
*/
|
||||
raf(() => {
|
||||
raf(async () => {
|
||||
this.currentTransition = present<PopoverPresentOptions>(
|
||||
this,
|
||||
'popoverEnter',
|
||||
iosEnterAnimation,
|
||||
mdEnterAnimation,
|
||||
{
|
||||
event: event || this.event,
|
||||
size: this.size,
|
||||
trigger: this.triggerEl,
|
||||
reference: this.reference,
|
||||
side: this.side,
|
||||
align: this.alignment,
|
||||
}
|
||||
);
|
||||
} else if (!this.keepContentsMounted) {
|
||||
await waitForMount();
|
||||
}
|
||||
|
||||
await this.currentTransition;
|
||||
|
||||
this.currentTransition = undefined;
|
||||
|
||||
/**
|
||||
* If popover is nested and was
|
||||
* presented using the "Right" arrow key,
|
||||
* we need to move focus to the first
|
||||
* descendant inside of the popover.
|
||||
*/
|
||||
if (this.focusDescendantOnPresent) {
|
||||
focusFirstDescendant(this.el, this.el);
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
this.currentTransition = present<PopoverPresentOptions>(this, 'popoverEnter', iosEnterAnimation, mdEnterAnimation, {
|
||||
event: event || this.event,
|
||||
size: this.size,
|
||||
trigger: this.triggerEl,
|
||||
reference: this.reference,
|
||||
side: this.side,
|
||||
align: this.alignment,
|
||||
});
|
||||
|
||||
await this.currentTransition;
|
||||
|
||||
this.currentTransition = undefined;
|
||||
|
||||
/**
|
||||
* If popover is nested and was
|
||||
* presented using the "Right" arrow key,
|
||||
* we need to move focus to the first
|
||||
* descendant inside of the popover.
|
||||
*/
|
||||
if (this.focusDescendantOnPresent) {
|
||||
focusFirstDescendant(this.el, this.el);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -663,7 +663,7 @@ export class Popover implements ComponentInterface, PopoverInterface {
|
||||
|
||||
render() {
|
||||
const mode = getIonMode(this);
|
||||
const { onLifecycle, popoverId, parentPopover, dismissOnSelect, side, arrow, htmlAttributes } = this;
|
||||
const { onLifecycle, parentPopover, dismissOnSelect, side, arrow, htmlAttributes } = this;
|
||||
const desktop = isPlatform('desktop');
|
||||
const enableArrow = arrow && !parentPopover;
|
||||
|
||||
@@ -676,7 +676,6 @@ export class Popover implements ComponentInterface, PopoverInterface {
|
||||
style={{
|
||||
zIndex: `${20000 + this.overlayIndex}`,
|
||||
}}
|
||||
id={popoverId}
|
||||
class={{
|
||||
...getClassMap(this.cssClass),
|
||||
[mode]: true,
|
||||
@@ -712,8 +711,6 @@ const LIFECYCLE_MAP: any = {
|
||||
ionPopoverDidDismiss: 'ionViewDidLeave',
|
||||
};
|
||||
|
||||
let popoverIds = 0;
|
||||
|
||||
interface PopoverPresentOptions {
|
||||
/**
|
||||
* The original target event that presented the popover.
|
||||
|
||||
41
core/src/components/popover/test/popover-id.spec.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { newSpecPage } from '@stencil/core/testing';
|
||||
|
||||
import { Popover } from '../popover';
|
||||
|
||||
it('popover should be assigned an incrementing id', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Popover],
|
||||
html: `<ion-popover is-open="true"></ion-popover>`,
|
||||
});
|
||||
let popover: HTMLIonPopoverElement;
|
||||
|
||||
popover = page.body.querySelector('ion-popover')!;
|
||||
|
||||
expect(popover).not.toBe(null);
|
||||
expect(popover.getAttribute('id')).toBe('ion-overlay-1');
|
||||
|
||||
// Remove the popover from the DOM
|
||||
popover.remove();
|
||||
await page.waitForChanges();
|
||||
|
||||
// Create a new popover to verify the id is incremented
|
||||
popover = document.createElement('ion-popover');
|
||||
popover.isOpen = true;
|
||||
page.body.appendChild(popover);
|
||||
await page.waitForChanges();
|
||||
|
||||
popover = page.body.querySelector('ion-popover')!;
|
||||
|
||||
expect(popover.getAttribute('id')).toBe('ion-overlay-2');
|
||||
|
||||
// Presenting the same popover again should reuse the existing id
|
||||
|
||||
popover.isOpen = false;
|
||||
await page.waitForChanges();
|
||||
popover.isOpen = true;
|
||||
await page.waitForChanges();
|
||||
|
||||
popover = page.body.querySelector('ion-popover')!;
|
||||
|
||||
expect(popover.getAttribute('id')).toBe('ion-overlay-2');
|
||||
});
|
||||
@@ -1,142 +0,0 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import type { Locator } from '@playwright/test';
|
||||
import { test } from '@utils/test/playwright';
|
||||
import type { E2EPage } from '@utils/test/playwright';
|
||||
|
||||
test.describe('radio-group: basic', () => {
|
||||
test('should not have visual regressions', async ({ page }) => {
|
||||
await page.goto(`/src/components/radio-group/test/basic`);
|
||||
|
||||
const list = page.locator('ion-list');
|
||||
|
||||
await expect(list).toHaveScreenshot(`radio-group-diff-${page.getSnapshotSettings()}.png`);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('radio-group: interaction', () => {
|
||||
let radioFixture: RadioFixture;
|
||||
|
||||
test.beforeEach(({ page, skip }) => {
|
||||
skip.rtl();
|
||||
radioFixture = new RadioFixture(page);
|
||||
});
|
||||
|
||||
test('spacebar should not deselect without allowEmptySelection', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-radio-group value="one" allow-empty-selection="false">
|
||||
<ion-item>
|
||||
<ion-label>One</ion-label>
|
||||
<ion-radio id="one" value="one"></ion-radio>
|
||||
</ion-item>
|
||||
</ion-radio-group>
|
||||
`);
|
||||
|
||||
await radioFixture.checkRadio('keyboard');
|
||||
await radioFixture.expectChecked(true);
|
||||
});
|
||||
|
||||
test('spacebar should deselect with allowEmptySelection', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-radio-group value="one" allow-empty-selection="true">
|
||||
<ion-item>
|
||||
<ion-label>One</ion-label>
|
||||
<ion-radio id="one" value="one"></ion-radio>
|
||||
</ion-item>
|
||||
</ion-radio-group>
|
||||
`);
|
||||
|
||||
await radioFixture.checkRadio('keyboard');
|
||||
await radioFixture.expectChecked(false);
|
||||
});
|
||||
|
||||
test('click should not deselect without allowEmptySelection', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-radio-group value="one" allow-empty-selection="false">
|
||||
<ion-item>
|
||||
<ion-label>One</ion-label>
|
||||
<ion-radio id="one" value="one"></ion-radio>
|
||||
</ion-item>
|
||||
</ion-radio-group>
|
||||
`);
|
||||
|
||||
await radioFixture.checkRadio('mouse');
|
||||
await radioFixture.expectChecked(true);
|
||||
});
|
||||
|
||||
test('click should deselect with allowEmptySelection', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-radio-group value="one" allow-empty-selection="true">
|
||||
<ion-item>
|
||||
<ion-label>One</ion-label>
|
||||
<ion-radio id="one" value="one"></ion-radio>
|
||||
</ion-item>
|
||||
</ion-radio-group>
|
||||
`);
|
||||
|
||||
await radioFixture.checkRadio('mouse');
|
||||
await radioFixture.expectChecked(false);
|
||||
});
|
||||
|
||||
test('programmatically assigning a value should update the checked radio', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ion-radio-group value="1">
|
||||
<ion-item>
|
||||
<ion-label>Item 1</ion-label>
|
||||
<ion-radio value="1" slot="start"></ion-radio>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Item 2</ion-label>
|
||||
<ion-radio value="2" slot="start"></ion-radio>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Item 3</ion-label>
|
||||
<ion-radio value="3" slot="start"></ion-radio>
|
||||
</ion-item>
|
||||
</ion-radio-group>
|
||||
`);
|
||||
|
||||
const radioGroup = page.locator('ion-radio-group');
|
||||
const radioOne = page.locator('ion-radio[value="1"]');
|
||||
const radioTwo = page.locator('ion-radio[value="2"]');
|
||||
|
||||
await radioGroup.evaluate((el: HTMLIonRadioGroupElement) => (el.value = '2'));
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(radioOne).not.toHaveClass(/radio-checked/);
|
||||
await expect(radioTwo).toHaveClass(/radio-checked/);
|
||||
});
|
||||
});
|
||||
|
||||
class RadioFixture {
|
||||
readonly page: E2EPage;
|
||||
|
||||
private radio!: Locator;
|
||||
|
||||
constructor(page: E2EPage) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
async checkRadio(method: 'keyboard' | 'mouse', selector = 'ion-radio') {
|
||||
const { page } = this;
|
||||
const radio = (this.radio = page.locator(selector));
|
||||
|
||||
if (method === 'keyboard') {
|
||||
await radio.focus();
|
||||
await page.keyboard.press('Space');
|
||||
} else {
|
||||
await radio.click();
|
||||
}
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
return radio;
|
||||
}
|
||||
|
||||
async expectChecked(state: boolean) {
|
||||
const { radio } = this;
|
||||
await expect(radio.locator('input')).toHaveJSProperty('checked', state);
|
||||
}
|
||||
}
|
||||
163
core/src/components/radio-group/test/basic/radio-group.e2e.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import type { Locator } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
import type { E2EPage } from '@utils/test/playwright';
|
||||
|
||||
configs().forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('radio-group: basic'), () => {
|
||||
test('should not have visual regressions', async ({ page }) => {
|
||||
await page.goto(`/src/components/radio-group/test/basic`, config);
|
||||
|
||||
const list = page.locator('ion-list');
|
||||
|
||||
await expect(list).toHaveScreenshot(screenshot(`radio-group-diff`));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* This behavior does not vary across modes/directions.
|
||||
*/
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('radio-group: interaction'), () => {
|
||||
let radioFixture: RadioFixture;
|
||||
|
||||
test.beforeEach(({ page }) => {
|
||||
radioFixture = new RadioFixture(page);
|
||||
});
|
||||
|
||||
test('spacebar should not deselect without allowEmptySelection', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-radio-group value="one" allow-empty-selection="false">
|
||||
<ion-item>
|
||||
<ion-label>One</ion-label>
|
||||
<ion-radio id="one" value="one"></ion-radio>
|
||||
</ion-item>
|
||||
</ion-radio-group>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
await radioFixture.checkRadio('keyboard');
|
||||
await radioFixture.expectChecked(true);
|
||||
});
|
||||
|
||||
test('spacebar should deselect with allowEmptySelection', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-radio-group value="one" allow-empty-selection="true">
|
||||
<ion-item>
|
||||
<ion-label>One</ion-label>
|
||||
<ion-radio id="one" value="one"></ion-radio>
|
||||
</ion-item>
|
||||
</ion-radio-group>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
await radioFixture.checkRadio('keyboard');
|
||||
await radioFixture.expectChecked(false);
|
||||
});
|
||||
|
||||
test('click should not deselect without allowEmptySelection', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-radio-group value="one" allow-empty-selection="false">
|
||||
<ion-item>
|
||||
<ion-label>One</ion-label>
|
||||
<ion-radio id="one" value="one"></ion-radio>
|
||||
</ion-item>
|
||||
</ion-radio-group>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
await radioFixture.checkRadio('mouse');
|
||||
await radioFixture.expectChecked(true);
|
||||
});
|
||||
|
||||
test('click should deselect with allowEmptySelection', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-radio-group value="one" allow-empty-selection="true">
|
||||
<ion-item>
|
||||
<ion-label>One</ion-label>
|
||||
<ion-radio id="one" value="one"></ion-radio>
|
||||
</ion-item>
|
||||
</ion-radio-group>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
await radioFixture.checkRadio('mouse');
|
||||
await radioFixture.expectChecked(false);
|
||||
});
|
||||
|
||||
test('programmatically assigning a value should update the checked radio', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-radio-group value="1">
|
||||
<ion-item>
|
||||
<ion-label>Item 1</ion-label>
|
||||
<ion-radio value="1" slot="start"></ion-radio>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Item 2</ion-label>
|
||||
<ion-radio value="2" slot="start"></ion-radio>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Item 3</ion-label>
|
||||
<ion-radio value="3" slot="start"></ion-radio>
|
||||
</ion-item>
|
||||
</ion-radio-group>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const radioGroup = page.locator('ion-radio-group');
|
||||
const radioOne = page.locator('ion-radio[value="1"]');
|
||||
const radioTwo = page.locator('ion-radio[value="2"]');
|
||||
|
||||
await radioGroup.evaluate((el: HTMLIonRadioGroupElement) => (el.value = '2'));
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(radioOne).not.toHaveClass(/radio-checked/);
|
||||
await expect(radioTwo).toHaveClass(/radio-checked/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class RadioFixture {
|
||||
readonly page: E2EPage;
|
||||
|
||||
private radio!: Locator;
|
||||
|
||||
constructor(page: E2EPage) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
async checkRadio(method: 'keyboard' | 'mouse', selector = 'ion-radio') {
|
||||
const { page } = this;
|
||||
const radio = (this.radio = page.locator(selector));
|
||||
|
||||
if (method === 'keyboard') {
|
||||
await radio.focus();
|
||||
await page.keyboard.press('Space');
|
||||
} else {
|
||||
await radio.click();
|
||||
}
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
return radio;
|
||||
}
|
||||
|
||||
async expectChecked(state: boolean) {
|
||||
const { radio } = this;
|
||||
await expect(radio.locator('input')).toHaveJSProperty('checked', state);
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
@@ -1,67 +0,0 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { test } from '@utils/test/playwright';
|
||||
|
||||
test.describe('radio-group: form', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/src/components/radio-group/test/form');
|
||||
});
|
||||
|
||||
test('selecting an option should update the value', async ({ page }) => {
|
||||
const radioGroup = page.locator('ion-radio-group');
|
||||
const ionChange = await page.spyOnEvent('ionChange');
|
||||
const griffRadio = page.locator('ion-radio[value="griff"]');
|
||||
await expect(radioGroup).toHaveAttribute('value', 'biff');
|
||||
|
||||
await griffRadio.click();
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(ionChange).toHaveReceivedEventDetail({ value: 'griff', event: { isTrusted: true } });
|
||||
});
|
||||
|
||||
test('selecting a disabled option should not update the value', async ({ page }) => {
|
||||
const value = page.locator('#value');
|
||||
const disabledRadio = page.locator('ion-radio[value="george"]');
|
||||
|
||||
await expect(value).toHaveText('');
|
||||
await expect(disabledRadio).toHaveAttribute('disabled', '');
|
||||
|
||||
await disabledRadio.click({ force: true });
|
||||
await page.waitForChanges();
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
69
core/src/components/radio-group/test/form/radio-group.e2e.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('radio-group: form'), () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/src/components/radio-group/test/form', config);
|
||||
});
|
||||
|
||||
test('selecting an option should update the value', async ({ page }) => {
|
||||
const radioGroup = page.locator('ion-radio-group');
|
||||
const ionChange = await page.spyOnEvent('ionChange');
|
||||
const griffRadio = page.locator('ion-radio[value="griff"]');
|
||||
await expect(radioGroup).toHaveAttribute('value', 'biff');
|
||||
|
||||
await griffRadio.click();
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(ionChange).toHaveReceivedEventDetail({ value: 'griff', event: { isTrusted: true } });
|
||||
});
|
||||
|
||||
test('selecting a disabled option should not update the value', async ({ page }) => {
|
||||
const value = page.locator('#value');
|
||||
const disabledRadio = page.locator('ion-radio[value="george"]');
|
||||
|
||||
await expect(value).toHaveText('');
|
||||
await expect(disabledRadio).toHaveAttribute('disabled', '');
|
||||
|
||||
await disabledRadio.click({ force: true });
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(value).toHaveText('');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe(title('radio-group: form submission'), () => {
|
||||
test('should submit radio data in a form', async ({ page }) => {
|
||||
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>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||