Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dafb0b9005 | ||
|
|
6b164c8f44 | ||
|
|
5546e95288 | ||
|
|
52d6e09b27 | ||
|
|
a6e734cf1a | ||
|
|
ed77d81eaa | ||
|
|
e4957fa4d2 | ||
|
|
d9f8c1352f | ||
|
|
41da4c3565 | ||
|
|
0b549835b6 | ||
|
|
0bbb9f37b4 | ||
|
|
166e43554e | ||
|
|
621333d927 | ||
|
|
6cf454f7c4 | ||
|
|
ac4ea3232b | ||
|
|
efd3e0fd2b |
4
.github/workflows/assign-issues.yml
vendored
@@ -11,8 +11,8 @@ jobs:
|
||||
issues: write
|
||||
steps:
|
||||
- name: 'Auto-assign issue'
|
||||
uses: pozil/auto-assign-issue@39c06395cbac76e79afc4ad4e5c5c6db6ecfdd2e # v2.2.0
|
||||
uses: pozil/auto-assign-issue@c015a6a3f410f12f58255c3d085fd774312f7a2f # v2.1.2
|
||||
with:
|
||||
assignees: brandyscarney, thetaPC, ShaneK
|
||||
assignees: brandyscarney, thetaPC, joselrio, rugoncalves, BenOsodrac, JoaoFerreira-FrontEnd, OS-giulianasilva, tanner-reits
|
||||
numOfAssignee: 1
|
||||
allowSelfAssign: false
|
||||
|
||||
@@ -403,6 +403,7 @@ ion-checkbox,prop,justify,"end" | "space-between" | "start" | undefined,undefine
|
||||
ion-checkbox,prop,labelPlacement,"end" | "fixed" | "stacked" | "start",'start',false,false
|
||||
ion-checkbox,prop,mode,"ios" | "md",undefined,false,false
|
||||
ion-checkbox,prop,name,string,this.inputId,false,false
|
||||
ion-checkbox,prop,required,boolean,false,false,false
|
||||
ion-checkbox,prop,value,any,'on',false,false
|
||||
ion-checkbox,event,ionBlur,void,true
|
||||
ion-checkbox,event,ionChange,CheckboxChangeEventDetail<any>,true
|
||||
@@ -1074,6 +1075,7 @@ ion-modal,prop,backdropDismiss,boolean,true,false,false
|
||||
ion-modal,prop,breakpoints,number[] | undefined,undefined,false,false
|
||||
ion-modal,prop,canDismiss,((data?: any, role?: string | undefined) => Promise<boolean>) | boolean,true,false,false
|
||||
ion-modal,prop,enterAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false
|
||||
ion-modal,prop,expandToScroll,boolean,true,false,false
|
||||
ion-modal,prop,focusTrap,boolean,true,false,false
|
||||
ion-modal,prop,handle,boolean | undefined,undefined,false,false
|
||||
ion-modal,prop,handleBehavior,"cycle" | "none" | undefined,'none',false,false
|
||||
@@ -1631,6 +1633,7 @@ ion-select,prop,multiple,boolean,false,false,false
|
||||
ion-select,prop,name,string,this.inputId,false,false
|
||||
ion-select,prop,okText,string,'OK',false,false
|
||||
ion-select,prop,placeholder,string | undefined,undefined,false,false
|
||||
ion-select,prop,required,boolean,false,false,false
|
||||
ion-select,prop,selectedText,null | string | undefined,undefined,false,false
|
||||
ion-select,prop,shape,"round" | undefined,undefined,false,false
|
||||
ion-select,prop,toggleIcon,string | undefined,undefined,false,false
|
||||
@@ -1944,6 +1947,7 @@ ion-toggle,prop,justify,"end" | "space-between" | "start" | undefined,undefined,
|
||||
ion-toggle,prop,labelPlacement,"end" | "fixed" | "stacked" | "start",'start',false,false
|
||||
ion-toggle,prop,mode,"ios" | "md",undefined,false,false
|
||||
ion-toggle,prop,name,string,this.inputId,false,false
|
||||
ion-toggle,prop,required,boolean,false,false,false
|
||||
ion-toggle,prop,value,null | string | undefined,'on',false,false
|
||||
ion-toggle,event,ionBlur,void,true
|
||||
ion-toggle,event,ionChange,ToggleChangeEventDetail<any>,true
|
||||
@@ -2000,4 +2004,7 @@ ion-toolbar,css-prop,--padding-end,md
|
||||
ion-toolbar,css-prop,--padding-start,ios
|
||||
ion-toolbar,css-prop,--padding-start,md
|
||||
ion-toolbar,css-prop,--padding-top,ios
|
||||
ion-toolbar,css-prop,--padding-top,md
|
||||
ion-toolbar,css-prop,--padding-top,md
|
||||
ion-toolbar,part,background
|
||||
ion-toolbar,part,container
|
||||
ion-toolbar,part,content
|
||||
80
core/package-lock.json
generated
@@ -15,11 +15,11 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@axe-core/playwright": "^4.10.0",
|
||||
"@capacitor/core": "^7.0.0",
|
||||
"@capacitor/haptics": "^7.0.0",
|
||||
"@capacitor/keyboard": "^7.0.0",
|
||||
"@capacitor/status-bar": "^7.0.0",
|
||||
"@clack/prompts": "^0.10.0",
|
||||
"@capacitor/core": "^6.0.0",
|
||||
"@capacitor/haptics": "^6.0.0",
|
||||
"@capacitor/keyboard": "^6.0.0",
|
||||
"@capacitor/status-bar": "^6.0.0",
|
||||
"@clack/prompts": "^0.9.0",
|
||||
"@ionic/eslint-config": "^0.3.0",
|
||||
"@ionic/prettier-config": "^2.0.0",
|
||||
"@playwright/test": "^1.46.1",
|
||||
@@ -663,43 +663,39 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@capacitor/core": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-7.0.1.tgz",
|
||||
"integrity": "sha512-1Ob9bvA/p8g8aNwK6VesxEekGXowLVf6APjkW4LRnr05H+7z/bke+Q5pn9zqh/GgTbIxAQ/rwZrAZvvxkdm1UA==",
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-6.2.0.tgz",
|
||||
"integrity": "sha512-B9IlJtDpUqhhYb+T8+cp2Db/3RETX36STgjeU2kQZBs/SLAcFiMama227o+msRjLeo3DO+7HJjWVA1+XlyyPEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@capacitor/haptics": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-7.0.0.tgz",
|
||||
"integrity": "sha512-8uI8rWyAbq8EzkjS+sHZSncyzujHzVbuLKgj8J5H0yUL6+r26F16gVA2iuQuIBvzbSMy7Y0/pUuWlwZr/H8AKg==",
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-6.0.2.tgz",
|
||||
"integrity": "sha512-xcFdIH4iIIeW2+1lzmlYMVicqB9ytaiuZ9NE3a9laKFPvMGC7hdj6i6tHFezwPJ/96xkHOwXT2b0F8Mh9xtTWg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@capacitor/core": ">=7.0.0"
|
||||
"@capacitor/core": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@capacitor/keyboard": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-7.0.0.tgz",
|
||||
"integrity": "sha512-Tqwy8wG+sx4UqiFCX4Q+bFw6uKgG7BiHKAPpeefoIgoEB8H8Jf3xZNZoVPnJIMuPsCdSvuyHXZbJXH9IEEirGA==",
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-6.0.3.tgz",
|
||||
"integrity": "sha512-V/mURxBI68HvClYjrGBlOriWkwYN7r+cWid/igJz/3scNc/V81DgQ9fpoLr4W0I5NY7YxOesjIJLuLO+LT18mQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@capacitor/core": ">=7.0.0"
|
||||
"@capacitor/core": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@capacitor/status-bar": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-7.0.0.tgz",
|
||||
"integrity": "sha512-wsvPkWkoSOXMIgVHu4c6P1sOuDSZ1ClUo5OpLRwj7u8DYzlV4jlmNzztQn2Lvsiqx1z4kfukSaqe40k1Lo4c9g==",
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-6.0.2.tgz",
|
||||
"integrity": "sha512-AmRIX6QvFemItlY7/69ARkIAqitRQqJ2qwgZmD1KqgFb78pH+XFXm1guvS/a8CuOOm/IqZ4ddDbl20yxtBqzGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@capacitor/core": ">=7.0.0"
|
||||
"@capacitor/core": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@clack/core": {
|
||||
@@ -713,9 +709,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@clack/prompts": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.10.0.tgz",
|
||||
"integrity": "sha512-H3rCl6CwW1NdQt9rE3n373t7o5cthPv7yUoxF2ytZvyvlJv89C5RYMJu83Hed8ODgys5vpBU0GKxIRG83jd8NQ==",
|
||||
"version": "0.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.9.1.tgz",
|
||||
"integrity": "sha512-JIpyaboYZeWYlyP0H+OoPPxd6nqueG/CmN6ixBiNFsIDHREevjIf0n0Ohh5gr5C8pEDknzgvz+pIJ8dMhzWIeg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@clack/core": "0.4.1",
|
||||
@@ -10981,32 +10977,32 @@
|
||||
"dev": true
|
||||
},
|
||||
"@capacitor/core": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-7.0.1.tgz",
|
||||
"integrity": "sha512-1Ob9bvA/p8g8aNwK6VesxEekGXowLVf6APjkW4LRnr05H+7z/bke+Q5pn9zqh/GgTbIxAQ/rwZrAZvvxkdm1UA==",
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-6.2.0.tgz",
|
||||
"integrity": "sha512-B9IlJtDpUqhhYb+T8+cp2Db/3RETX36STgjeU2kQZBs/SLAcFiMama227o+msRjLeo3DO+7HJjWVA1+XlyyPEg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"@capacitor/haptics": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-7.0.0.tgz",
|
||||
"integrity": "sha512-8uI8rWyAbq8EzkjS+sHZSncyzujHzVbuLKgj8J5H0yUL6+r26F16gVA2iuQuIBvzbSMy7Y0/pUuWlwZr/H8AKg==",
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-6.0.2.tgz",
|
||||
"integrity": "sha512-xcFdIH4iIIeW2+1lzmlYMVicqB9ytaiuZ9NE3a9laKFPvMGC7hdj6i6tHFezwPJ/96xkHOwXT2b0F8Mh9xtTWg==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"@capacitor/keyboard": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-7.0.0.tgz",
|
||||
"integrity": "sha512-Tqwy8wG+sx4UqiFCX4Q+bFw6uKgG7BiHKAPpeefoIgoEB8H8Jf3xZNZoVPnJIMuPsCdSvuyHXZbJXH9IEEirGA==",
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-6.0.3.tgz",
|
||||
"integrity": "sha512-V/mURxBI68HvClYjrGBlOriWkwYN7r+cWid/igJz/3scNc/V81DgQ9fpoLr4W0I5NY7YxOesjIJLuLO+LT18mQ==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"@capacitor/status-bar": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-7.0.0.tgz",
|
||||
"integrity": "sha512-wsvPkWkoSOXMIgVHu4c6P1sOuDSZ1ClUo5OpLRwj7u8DYzlV4jlmNzztQn2Lvsiqx1z4kfukSaqe40k1Lo4c9g==",
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-6.0.2.tgz",
|
||||
"integrity": "sha512-AmRIX6QvFemItlY7/69ARkIAqitRQqJ2qwgZmD1KqgFb78pH+XFXm1guvS/a8CuOOm/IqZ4ddDbl20yxtBqzGA==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
@@ -11021,9 +11017,9 @@
|
||||
}
|
||||
},
|
||||
"@clack/prompts": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.10.0.tgz",
|
||||
"integrity": "sha512-H3rCl6CwW1NdQt9rE3n373t7o5cthPv7yUoxF2ytZvyvlJv89C5RYMJu83Hed8ODgys5vpBU0GKxIRG83jd8NQ==",
|
||||
"version": "0.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.9.1.tgz",
|
||||
"integrity": "sha512-JIpyaboYZeWYlyP0H+OoPPxd6nqueG/CmN6ixBiNFsIDHREevjIf0n0Ohh5gr5C8pEDknzgvz+pIJ8dMhzWIeg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@clack/core": "0.4.1",
|
||||
|
||||
@@ -37,11 +37,11 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@axe-core/playwright": "^4.10.0",
|
||||
"@capacitor/core": "^7.0.0",
|
||||
"@capacitor/haptics": "^7.0.0",
|
||||
"@capacitor/keyboard": "^7.0.0",
|
||||
"@capacitor/status-bar": "^7.0.0",
|
||||
"@clack/prompts": "^0.10.0",
|
||||
"@capacitor/core": "^6.0.0",
|
||||
"@capacitor/haptics": "^6.0.0",
|
||||
"@capacitor/keyboard": "^6.0.0",
|
||||
"@capacitor/status-bar": "^6.0.0",
|
||||
"@clack/prompts": "^0.9.0",
|
||||
"@ionic/eslint-config": "^0.3.0",
|
||||
"@ionic/prettier-config": "^2.0.0",
|
||||
"@playwright/test": "^1.46.1",
|
||||
|
||||
32
core/src/components.d.ts
vendored
@@ -643,6 +643,10 @@ export namespace Components {
|
||||
* The name of the control, which is submitted with the form data.
|
||||
*/
|
||||
"name": string;
|
||||
/**
|
||||
* If true, screen readers will announce it as a required field. This property works only for accessibility purposes, it will not prevent the form from submitting if the value is invalid.
|
||||
*/
|
||||
"required": boolean;
|
||||
"setFocus": () => Promise<void>;
|
||||
/**
|
||||
* The value of the checkbox does not mean if it's checked or not, use the `checked` property for that. The value of a checkbox is analogous to the value of an `<input type="checkbox">`, it's only used when the checkbox participates in a native `<form>`.
|
||||
@@ -1731,6 +1735,10 @@ export namespace Components {
|
||||
* Animation to use when the modal is presented.
|
||||
*/
|
||||
"enterAnimation"?: AnimationBuilder;
|
||||
/**
|
||||
* Controls whether scrolling or dragging within the sheet modal expands it to a larger breakpoint. This only takes effect when `breakpoints` and `initialBreakpoint` are set. If `true`, scrolling or dragging anywhere in the modal will first expand it to the next breakpoint. Once fully expanded, scrolling will affect the content. If `false`, scrolling will always affect the content, and the modal will only expand when dragging the header or handle.
|
||||
*/
|
||||
"expandToScroll": boolean;
|
||||
/**
|
||||
* If `true`, focus will not be allowed to move outside of this overlay. If `false`, focus will be allowed to move outside of the overlay. In most scenarios this property should remain set to `true`. Setting this property to `false` can cause severe accessibility issues as users relying on assistive technologies may be able to move focus into a confusing state. We recommend only setting this to `false` when absolutely necessary. Developers may want to consider disabling focus trapping if this overlay presents a non-Ionic overlay from a 3rd party library. Developers would disable focus trapping on the Ionic overlay when presenting the 3rd party overlay and then re-enable focus trapping when dismissing the 3rd party overlay and moving focus back to the Ionic overlay.
|
||||
*/
|
||||
@@ -2808,6 +2816,10 @@ export namespace Components {
|
||||
* The text to display when the select is empty.
|
||||
*/
|
||||
"placeholder"?: string;
|
||||
/**
|
||||
* If true, screen readers will announce it as a required field. This property works only for accessibility purposes, it will not prevent the form from submitting if the value is invalid.
|
||||
*/
|
||||
"required": boolean;
|
||||
/**
|
||||
* The text to display instead of the selected option's value.
|
||||
*/
|
||||
@@ -3280,6 +3292,10 @@ export namespace Components {
|
||||
* The name of the control, which is submitted with the form data.
|
||||
*/
|
||||
"name": string;
|
||||
/**
|
||||
* If true, screen readers will announce it as a required field. This property works only for accessibility purposes, it will not prevent the form from submitting if the value is invalid.
|
||||
*/
|
||||
"required": boolean;
|
||||
/**
|
||||
* The value of the toggle does not mean if it's checked or not, use the `checked` property for that. The value of a toggle is analogous to the value of a `<input type="checkbox">`, it's only used when the toggle participates in a native `<form>`.
|
||||
*/
|
||||
@@ -5435,6 +5451,10 @@ declare namespace LocalJSX {
|
||||
* Emitted when the checkbox has focus.
|
||||
*/
|
||||
"onIonFocus"?: (event: IonCheckboxCustomEvent<void>) => void;
|
||||
/**
|
||||
* If true, screen readers will announce it as a required field. This property works only for accessibility purposes, it will not prevent the form from submitting if the value is invalid.
|
||||
*/
|
||||
"required"?: boolean;
|
||||
/**
|
||||
* The value of the checkbox does not mean if it's checked or not, use the `checked` property for that. The value of a checkbox is analogous to the value of an `<input type="checkbox">`, it's only used when the checkbox participates in a native `<form>`.
|
||||
*/
|
||||
@@ -6532,6 +6552,10 @@ declare namespace LocalJSX {
|
||||
* Animation to use when the modal is presented.
|
||||
*/
|
||||
"enterAnimation"?: AnimationBuilder;
|
||||
/**
|
||||
* Controls whether scrolling or dragging within the sheet modal expands it to a larger breakpoint. This only takes effect when `breakpoints` and `initialBreakpoint` are set. If `true`, scrolling or dragging anywhere in the modal will first expand it to the next breakpoint. Once fully expanded, scrolling will affect the content. If `false`, scrolling will always affect the content, and the modal will only expand when dragging the header or handle.
|
||||
*/
|
||||
"expandToScroll"?: boolean;
|
||||
/**
|
||||
* If `true`, focus will not be allowed to move outside of this overlay. If `false`, focus will be allowed to move outside of the overlay. In most scenarios this property should remain set to `true`. Setting this property to `false` can cause severe accessibility issues as users relying on assistive technologies may be able to move focus into a confusing state. We recommend only setting this to `false` when absolutely necessary. Developers may want to consider disabling focus trapping if this overlay presents a non-Ionic overlay from a 3rd party library. Developers would disable focus trapping on the Ionic overlay when presenting the 3rd party overlay and then re-enable focus trapping when dismissing the 3rd party overlay and moving focus back to the Ionic overlay.
|
||||
*/
|
||||
@@ -7640,6 +7664,10 @@ declare namespace LocalJSX {
|
||||
* The text to display when the select is empty.
|
||||
*/
|
||||
"placeholder"?: string;
|
||||
/**
|
||||
* If true, screen readers will announce it as a required field. This property works only for accessibility purposes, it will not prevent the form from submitting if the value is invalid.
|
||||
*/
|
||||
"required"?: boolean;
|
||||
/**
|
||||
* The text to display instead of the selected option's value.
|
||||
*/
|
||||
@@ -8155,6 +8183,10 @@ declare namespace LocalJSX {
|
||||
* Emitted when the toggle has focus.
|
||||
*/
|
||||
"onIonFocus"?: (event: IonToggleCustomEvent<void>) => void;
|
||||
/**
|
||||
* If true, screen readers will announce it as a required field. This property works only for accessibility purposes, it will not prevent the form from submitting if the value is invalid.
|
||||
*/
|
||||
"required"?: boolean;
|
||||
/**
|
||||
* The value of the toggle does not mean if it's checked or not, use the `checked` property for that. The value of a toggle is analogous to the value of a `<input type="checkbox">`, it's only used when the toggle participates in a native `<form>`.
|
||||
*/
|
||||
|
||||
@@ -237,18 +237,6 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure when alert container is being focused, and the user presses the tab + shift keys, the focus will be set to the last alert button.
|
||||
*/
|
||||
if (ev.target.classList.contains('alert-wrapper')) {
|
||||
if (ev.key === 'Tab' && ev.shiftKey) {
|
||||
ev.preventDefault();
|
||||
const lastChildBtn = this.wrapperEl?.querySelector('.alert-button:last-child') as HTMLButtonElement;
|
||||
lastChildBtn.focus();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// The only inputs we want to navigate between using arrow keys are the radios
|
||||
// ignore the keydown event if it is not on a radio button
|
||||
if (
|
||||
@@ -412,19 +400,7 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
|
||||
await this.delegateController.attachViewToDom();
|
||||
|
||||
await present(this, 'alertEnter', iosEnterAnimation, mdEnterAnimation).then(() => {
|
||||
/**
|
||||
* Check if alert has only one button and no inputs.
|
||||
* If so, then focus on the button. Otherwise, focus the alert wrapper.
|
||||
* This will map to the default native alert behavior.
|
||||
*/
|
||||
if (this.buttons.length === 1 && this.inputs.length === 0) {
|
||||
const queryBtn = this.wrapperEl?.querySelector('.alert-button') as HTMLButtonElement;
|
||||
queryBtn.focus();
|
||||
} else {
|
||||
this.wrapperEl?.focus();
|
||||
}
|
||||
});
|
||||
await present(this, 'alertEnter', iosEnterAnimation, mdEnterAnimation);
|
||||
|
||||
unlock();
|
||||
}
|
||||
@@ -749,8 +725,8 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
const { overlayIndex, header, subHeader, message, htmlAttributes } = this;
|
||||
const mode = getIonMode(this);
|
||||
const hdrId = `alert-${overlayIndex}-hdr`;
|
||||
const msgId = `alert-${overlayIndex}-msg`;
|
||||
const subHdrId = `alert-${overlayIndex}-sub-hdr`;
|
||||
const msgId = `alert-${overlayIndex}-msg`;
|
||||
const role = this.inputs.length > 0 || this.buttons.length > 0 ? 'alertdialog' : 'alert';
|
||||
|
||||
/**
|
||||
@@ -763,7 +739,12 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
|
||||
return (
|
||||
<Host
|
||||
role={role}
|
||||
aria-modal="true"
|
||||
aria-labelledby={ariaLabelledBy}
|
||||
aria-describedby={message !== undefined ? msgId : null}
|
||||
tabindex="-1"
|
||||
{...(htmlAttributes as any)}
|
||||
style={{
|
||||
zIndex: `${20000 + overlayIndex}`,
|
||||
}}
|
||||
@@ -780,16 +761,7 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
|
||||
<div tabindex="0" aria-hidden="true"></div>
|
||||
|
||||
<div
|
||||
class="alert-wrapper ion-overlay-wrapper"
|
||||
role={role}
|
||||
aria-modal="true"
|
||||
aria-labelledby={ariaLabelledBy}
|
||||
aria-describedby={message !== undefined ? msgId : null}
|
||||
tabindex="0"
|
||||
ref={(el) => (this.wrapperEl = el)}
|
||||
{...(htmlAttributes as any)}
|
||||
>
|
||||
<div class="alert-wrapper ion-overlay-wrapper" ref={(el) => (this.wrapperEl = el)}>
|
||||
<div class="alert-head">
|
||||
{header && (
|
||||
<h2 id={hdrId} class="alert-title">
|
||||
|
||||
@@ -16,7 +16,6 @@ const testAria = async (
|
||||
await didPresent.next();
|
||||
|
||||
const alert = page.locator('ion-alert');
|
||||
const alertwrapper = alert.locator('.alert-wrapper');
|
||||
|
||||
const header = alert.locator('.alert-title');
|
||||
const subHeader = alert.locator('.alert-sub-title');
|
||||
@@ -43,8 +42,8 @@ const testAria = async (
|
||||
* expect().toHaveAttribute() can't check for a null value, so grab and check
|
||||
* the values manually instead.
|
||||
*/
|
||||
const ariaLabelledBy = await alertwrapper.getAttribute('aria-labelledby');
|
||||
const ariaDescribedBy = await alertwrapper.getAttribute('aria-describedby');
|
||||
const ariaLabelledBy = await alert.getAttribute('aria-labelledby');
|
||||
const ariaDescribedBy = await alert.getAttribute('aria-describedby');
|
||||
|
||||
expect(ariaLabelledBy).toBe(expectedAriaLabelledBy);
|
||||
expect(ariaDescribedBy).toBe(expectedAriaDescribedBy);
|
||||
|
||||
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
@@ -1,7 +1,7 @@
|
||||
import { h } from '@stencil/core';
|
||||
import { newSpecPage } from '@stencil/core/testing';
|
||||
|
||||
import { Alert } from '../alert';
|
||||
import { h } from '@stencil/core';
|
||||
|
||||
describe('alert: id', () => {
|
||||
it('alert should be assigned an incrementing id', async () => {
|
||||
@@ -49,7 +49,7 @@ describe('alert: id', () => {
|
||||
template: () => <ion-alert htmlAttributes={{ id }} overlayIndex={-1}></ion-alert>,
|
||||
});
|
||||
|
||||
const alertwrapper = page.body.querySelector('.alert-wrapper')!;
|
||||
expect(alertwrapper.id).toBe(id);
|
||||
const alert = page.body.querySelector('ion-alert')!;
|
||||
expect(alert.id).toBe(id);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,7 +19,6 @@ configs({ directions: ['ltr'] }).forEach(({ config, screenshot, title }) => {
|
||||
await page.keyboard.press(tabKey);
|
||||
await expect(alertBtns.nth(0)).toBeFocused();
|
||||
|
||||
await page.keyboard.press(`Shift+${tabKey}`); // this will focus the alert-wrapper
|
||||
await page.keyboard.press(`Shift+${tabKey}`);
|
||||
await expect(alertBtns.nth(2)).toBeFocused();
|
||||
|
||||
@@ -31,7 +30,7 @@ configs({ directions: ['ltr'] }).forEach(({ config, screenshot, title }) => {
|
||||
const alertFixture = new AlertFixture(page, screenshot);
|
||||
|
||||
const alert = await alertFixture.open('#basic');
|
||||
await expect(alert.locator('.alert-wrapper')).toHaveAttribute('data-testid', 'basic-alert');
|
||||
await expect(alert).toHaveAttribute('data-testid', 'basic-alert');
|
||||
});
|
||||
|
||||
test('should dismiss when async handler resolves', async ({ page }) => {
|
||||
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 9.8 KiB |
@@ -98,6 +98,13 @@ export class Checkbox implements ComponentInterface {
|
||||
*/
|
||||
@Prop() alignment?: 'start' | 'center';
|
||||
|
||||
/**
|
||||
* If true, screen readers will announce it as a required field. This property
|
||||
* works only for accessibility purposes, it will not prevent the form from
|
||||
* submitting if the value is invalid.
|
||||
*/
|
||||
@Prop() required = false;
|
||||
|
||||
/**
|
||||
* Emitted when the checked property has changed as a result of a user action such as a click.
|
||||
*
|
||||
@@ -182,6 +189,7 @@ export class Checkbox implements ComponentInterface {
|
||||
name,
|
||||
value,
|
||||
alignment,
|
||||
required,
|
||||
} = this;
|
||||
const mode = getIonMode(this);
|
||||
const path = getSVGPath(mode, indeterminate);
|
||||
@@ -218,6 +226,7 @@ export class Checkbox implements ComponentInterface {
|
||||
onFocus={() => this.onFocus()}
|
||||
onBlur={() => this.onBlur()}
|
||||
ref={(focusEl) => (this.focusEl = focusEl)}
|
||||
required={required}
|
||||
{...inheritedAttributes}
|
||||
/>
|
||||
<div
|
||||
|
||||
@@ -54,3 +54,33 @@ describe('ion-checkbox: indeterminate', () => {
|
||||
expect(checkbox.getAttribute('aria-checked')).toBe('mixed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ion-checkbox: required', () => {
|
||||
it('should have a required attribute in inner input when true', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Checkbox],
|
||||
html: `
|
||||
<ion-checkbox required="true">Checkbox</ion-checkbox>
|
||||
`,
|
||||
});
|
||||
|
||||
const checkbox = page.body.querySelector('ion-checkbox')!;
|
||||
const nativeInput = checkbox.shadowRoot?.querySelector('input[type=checkbox]')!;
|
||||
|
||||
expect(nativeInput.hasAttribute('required')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not have a required attribute in inner input when false', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Checkbox],
|
||||
html: `
|
||||
<ion-checkbox required="false">Checkbox</ion-checkbox>
|
||||
`,
|
||||
});
|
||||
|
||||
const checkbox = page.body.querySelector('ion-checkbox')!;
|
||||
const nativeInput = checkbox.shadowRoot?.querySelector('input[type=checkbox]')!;
|
||||
|
||||
expect(nativeInput.hasAttribute('required')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -455,6 +455,7 @@ export class Content implements ComponentInterface {
|
||||
overscroll: forceOverscroll,
|
||||
[`content-${rtl}`]: true,
|
||||
})}
|
||||
tabIndex={'0'}
|
||||
style={{
|
||||
'--offset-top': `${this.cTop}px`,
|
||||
'--offset-bottom': `${this.cBottom}px`,
|
||||
|
||||
@@ -17,27 +17,78 @@ const createEnterAnimation = () => {
|
||||
|
||||
const wrapperAnimation = createAnimation().fromTo('transform', 'translateY(100vh)', 'translateY(0vh)');
|
||||
|
||||
return { backdropAnimation, wrapperAnimation };
|
||||
return { backdropAnimation, wrapperAnimation, contentAnimation: undefined };
|
||||
};
|
||||
|
||||
/**
|
||||
* iOS Modal Enter Animation for the Card presentation style
|
||||
*/
|
||||
export const iosEnterAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptions): Animation => {
|
||||
const { presentingEl, currentBreakpoint } = opts;
|
||||
const { presentingEl, currentBreakpoint, expandToScroll } = opts;
|
||||
const root = getElementRoot(baseEl);
|
||||
const { wrapperAnimation, backdropAnimation } =
|
||||
const { wrapperAnimation, backdropAnimation, contentAnimation } =
|
||||
currentBreakpoint !== undefined ? createSheetEnterAnimation(opts) : createEnterAnimation();
|
||||
|
||||
backdropAnimation.addElement(root.querySelector('ion-backdrop')!);
|
||||
|
||||
wrapperAnimation.addElement(root.querySelectorAll('.modal-wrapper, .modal-shadow')!).beforeStyles({ opacity: 1 });
|
||||
|
||||
// The content animation is only added if scrolling is enabled for
|
||||
// all the breakpoints.
|
||||
!expandToScroll && contentAnimation?.addElement(baseEl.querySelector('.ion-page')!);
|
||||
|
||||
const baseAnimation = createAnimation('entering-base')
|
||||
.addElement(baseEl)
|
||||
.easing('cubic-bezier(0.32,0.72,0,1)')
|
||||
.duration(500)
|
||||
.addAnimation(wrapperAnimation);
|
||||
.addAnimation([wrapperAnimation])
|
||||
.beforeAddWrite(() => {
|
||||
if (expandToScroll) {
|
||||
// Scroll can only be done when the modal is fully expanded.
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* There are some browsers that causes flickering when
|
||||
* dragging the content when scroll is enabled at every
|
||||
* breakpoint. This is due to the wrapper element being
|
||||
* transformed off the screen and having a snap animation.
|
||||
*
|
||||
* A workaround is to clone the footer element and append
|
||||
* it outside of the wrapper element. This way, the footer
|
||||
* is still visible and the drag can be done without
|
||||
* flickering. The original footer is hidden until the modal
|
||||
* is dismissed. This maintains the animation of the footer
|
||||
* when the modal is dismissed.
|
||||
*
|
||||
* The workaround needs to be done before the animation starts
|
||||
* so there are no flickering issues.
|
||||
*/
|
||||
const ionFooter = baseEl.querySelector('ion-footer');
|
||||
/**
|
||||
* This check is needed to prevent more than one footer
|
||||
* from being appended to the shadow root.
|
||||
* Otherwise, iOS and MD enter animations would append
|
||||
* the footer twice.
|
||||
*/
|
||||
const ionFooterAlreadyAppended = baseEl.shadowRoot!.querySelector('ion-footer');
|
||||
if (ionFooter && !ionFooterAlreadyAppended) {
|
||||
const footerHeight = ionFooter.clientHeight;
|
||||
const clonedFooter = ionFooter.cloneNode(true) as HTMLIonFooterElement;
|
||||
|
||||
baseEl.shadowRoot!.appendChild(clonedFooter);
|
||||
ionFooter.style.setProperty('display', 'none');
|
||||
ionFooter.setAttribute('aria-hidden', 'true');
|
||||
|
||||
// Padding is added to prevent some content from being hidden.
|
||||
const page = baseEl.querySelector('.ion-page') as HTMLElement;
|
||||
page.style.setProperty('padding-bottom', `${footerHeight}px`);
|
||||
}
|
||||
});
|
||||
|
||||
if (contentAnimation) {
|
||||
baseAnimation.addAnimation(contentAnimation);
|
||||
}
|
||||
|
||||
if (presentingEl) {
|
||||
const isMobile = window.innerWidth < 768;
|
||||
|
||||
@@ -19,7 +19,7 @@ const createLeaveAnimation = () => {
|
||||
* iOS Modal Leave Animation
|
||||
*/
|
||||
export const iosLeaveAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptions, duration = 500): Animation => {
|
||||
const { presentingEl, currentBreakpoint } = opts;
|
||||
const { presentingEl, currentBreakpoint, expandToScroll } = opts;
|
||||
const root = getElementRoot(baseEl);
|
||||
const { wrapperAnimation, backdropAnimation } =
|
||||
currentBreakpoint !== undefined ? createSheetLeaveAnimation(opts) : createLeaveAnimation();
|
||||
@@ -32,7 +32,33 @@ export const iosLeaveAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptio
|
||||
.addElement(baseEl)
|
||||
.easing('cubic-bezier(0.32,0.72,0,1)')
|
||||
.duration(duration)
|
||||
.addAnimation(wrapperAnimation);
|
||||
.addAnimation(wrapperAnimation)
|
||||
.beforeAddWrite(() => {
|
||||
if (expandToScroll) {
|
||||
// Scroll can only be done when the modal is fully expanded.
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* If expandToScroll is disabled, we need to swap
|
||||
* the visibility to the original, so the footer
|
||||
* dismisses with the modal and doesn't stay
|
||||
* until the modal is removed from the DOM.
|
||||
*/
|
||||
const ionFooter = baseEl.querySelector('ion-footer');
|
||||
if (ionFooter) {
|
||||
const clonedFooter = baseEl.shadowRoot!.querySelector('ion-footer')!;
|
||||
|
||||
ionFooter.style.removeProperty('display');
|
||||
ionFooter.removeAttribute('aria-hidden');
|
||||
|
||||
clonedFooter.style.setProperty('display', 'none');
|
||||
clonedFooter.setAttribute('aria-hidden', 'true');
|
||||
|
||||
const page = baseEl.querySelector('.ion-page') as HTMLElement;
|
||||
page.style.removeProperty('padding-bottom');
|
||||
}
|
||||
});
|
||||
|
||||
if (presentingEl) {
|
||||
const isMobile = window.innerWidth < 768;
|
||||
|
||||
@@ -19,25 +19,78 @@ const createEnterAnimation = () => {
|
||||
{ offset: 1, opacity: 1, transform: `translateY(0px)` },
|
||||
]);
|
||||
|
||||
return { backdropAnimation, wrapperAnimation };
|
||||
return { backdropAnimation, wrapperAnimation, contentAnimation: undefined };
|
||||
};
|
||||
|
||||
/**
|
||||
* Md Modal Enter Animation
|
||||
*/
|
||||
export const mdEnterAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptions): Animation => {
|
||||
const { currentBreakpoint } = opts;
|
||||
const { currentBreakpoint, expandToScroll } = opts;
|
||||
const root = getElementRoot(baseEl);
|
||||
const { wrapperAnimation, backdropAnimation } =
|
||||
const { wrapperAnimation, backdropAnimation, contentAnimation } =
|
||||
currentBreakpoint !== undefined ? createSheetEnterAnimation(opts) : createEnterAnimation();
|
||||
|
||||
backdropAnimation.addElement(root.querySelector('ion-backdrop')!);
|
||||
|
||||
wrapperAnimation.addElement(root.querySelector('.modal-wrapper')!);
|
||||
|
||||
return createAnimation()
|
||||
// The content animation is only added if scrolling is enabled for
|
||||
// all the breakpoints.
|
||||
expandToScroll && contentAnimation?.addElement(baseEl.querySelector('.ion-page')!);
|
||||
|
||||
const baseAnimation = createAnimation()
|
||||
.addElement(baseEl)
|
||||
.easing('cubic-bezier(0.36,0.66,0.04,1)')
|
||||
.duration(280)
|
||||
.addAnimation([backdropAnimation, wrapperAnimation]);
|
||||
.addAnimation([backdropAnimation, wrapperAnimation])
|
||||
.beforeAddWrite(() => {
|
||||
if (expandToScroll) {
|
||||
// Scroll can only be done when the modal is fully expanded.
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* There are some browsers that causes flickering when
|
||||
* dragging the content when scroll is enabled at every
|
||||
* breakpoint. This is due to the wrapper element being
|
||||
* transformed off the screen and having a snap animation.
|
||||
*
|
||||
* A workaround is to clone the footer element and append
|
||||
* it outside of the wrapper element. This way, the footer
|
||||
* is still visible and the drag can be done without
|
||||
* flickering. The original footer is hidden until the modal
|
||||
* is dismissed. This maintains the animation of the footer
|
||||
* when the modal is dismissed.
|
||||
*
|
||||
* The workaround needs to be done before the animation starts
|
||||
* so there are no flickering issues.
|
||||
*/
|
||||
const ionFooter = baseEl.querySelector('ion-footer');
|
||||
/**
|
||||
* This check is needed to prevent more than one footer
|
||||
* from being appended to the shadow root.
|
||||
* Otherwise, iOS and MD enter animations would append
|
||||
* the footer twice.
|
||||
*/
|
||||
const ionFooterAlreadyAppended = baseEl.shadowRoot!.querySelector('ion-footer');
|
||||
if (ionFooter && !ionFooterAlreadyAppended) {
|
||||
const footerHeight = ionFooter.clientHeight;
|
||||
const clonedFooter = ionFooter.cloneNode(true) as HTMLIonFooterElement;
|
||||
|
||||
baseEl.shadowRoot!.appendChild(clonedFooter);
|
||||
ionFooter.style.setProperty('display', 'none');
|
||||
ionFooter.setAttribute('aria-hidden', 'true');
|
||||
|
||||
// Padding is added to prevent some content from being hidden.
|
||||
const page = baseEl.querySelector('.ion-page') as HTMLElement;
|
||||
page.style.setProperty('padding-bottom', `${footerHeight}px`);
|
||||
}
|
||||
});
|
||||
|
||||
if (contentAnimation) {
|
||||
baseAnimation.addAnimation(contentAnimation);
|
||||
}
|
||||
|
||||
return baseAnimation;
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ const createLeaveAnimation = () => {
|
||||
* Md Modal Leave Animation
|
||||
*/
|
||||
export const mdLeaveAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptions): Animation => {
|
||||
const { currentBreakpoint } = opts;
|
||||
const { currentBreakpoint, expandToScroll } = opts;
|
||||
const root = getElementRoot(baseEl);
|
||||
const { wrapperAnimation, backdropAnimation } =
|
||||
currentBreakpoint !== undefined ? createSheetLeaveAnimation(opts) : createLeaveAnimation();
|
||||
@@ -29,8 +29,36 @@ export const mdLeaveAnimation = (baseEl: HTMLElement, opts: ModalAnimationOption
|
||||
backdropAnimation.addElement(root.querySelector('ion-backdrop')!);
|
||||
wrapperAnimation.addElement(root.querySelector('.modal-wrapper')!);
|
||||
|
||||
return createAnimation()
|
||||
const baseAnimation = createAnimation()
|
||||
.easing('cubic-bezier(0.47,0,0.745,0.715)')
|
||||
.duration(200)
|
||||
.addAnimation([backdropAnimation, wrapperAnimation]);
|
||||
.addAnimation([backdropAnimation, wrapperAnimation])
|
||||
.beforeAddWrite(() => {
|
||||
if (expandToScroll) {
|
||||
// Scroll can only be done when the modal is fully expanded.
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* If expandToScroll is disabled, we need to swap
|
||||
* the visibility to the original, so the footer
|
||||
* dismisses with the modal and doesn't stay
|
||||
* until the modal is removed from the DOM.
|
||||
*/
|
||||
const ionFooter = baseEl.querySelector('ion-footer');
|
||||
if (ionFooter) {
|
||||
const clonedFooter = baseEl.shadowRoot!.querySelector('ion-footer')!;
|
||||
|
||||
ionFooter.style.removeProperty('display');
|
||||
ionFooter.removeAttribute('aria-hidden');
|
||||
|
||||
clonedFooter.style.setProperty('display', 'none');
|
||||
clonedFooter.setAttribute('aria-hidden', 'true');
|
||||
|
||||
const page = baseEl.querySelector('.ion-page') as HTMLElement;
|
||||
page.style.removeProperty('padding-bottom');
|
||||
}
|
||||
});
|
||||
|
||||
return baseAnimation;
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { ModalAnimationOptions } from '../modal-interface';
|
||||
import { getBackdropValueForSheet } from '../utils';
|
||||
|
||||
export const createSheetEnterAnimation = (opts: ModalAnimationOptions) => {
|
||||
const { currentBreakpoint, backdropBreakpoint } = opts;
|
||||
const { currentBreakpoint, backdropBreakpoint, expandToScroll } = opts;
|
||||
|
||||
/**
|
||||
* If the backdropBreakpoint is undefined, then the backdrop
|
||||
@@ -29,7 +29,17 @@ export const createSheetEnterAnimation = (opts: ModalAnimationOptions) => {
|
||||
{ offset: 1, opacity: 1, transform: `translateY(${100 - currentBreakpoint! * 100}%)` },
|
||||
]);
|
||||
|
||||
return { wrapperAnimation, backdropAnimation };
|
||||
/**
|
||||
* This allows the content to be scrollable at any breakpoint.
|
||||
*/
|
||||
const contentAnimation = !expandToScroll
|
||||
? createAnimation('contentAnimation').keyframes([
|
||||
{ offset: 0, opacity: 1, maxHeight: `${(1 - currentBreakpoint!) * 100}%` },
|
||||
{ offset: 1, opacity: 1, maxHeight: `${currentBreakpoint! * 100}%` },
|
||||
])
|
||||
: undefined;
|
||||
|
||||
return { wrapperAnimation, backdropAnimation, contentAnimation };
|
||||
};
|
||||
|
||||
export const createSheetLeaveAnimation = (opts: ModalAnimationOptions) => {
|
||||
|
||||
@@ -49,6 +49,7 @@ export const createSheetGesture = (
|
||||
backdropBreakpoint: number,
|
||||
animation: Animation,
|
||||
breakpoints: number[] = [],
|
||||
expandToScroll: boolean,
|
||||
getCurrentBreakpoint: () => number,
|
||||
onDismiss: () => void,
|
||||
onBreakpointChange: (breakpoint: number) => void
|
||||
@@ -71,6 +72,10 @@ export const createSheetGesture = (
|
||||
{ offset: 1, transform: 'translateY(100%)' },
|
||||
],
|
||||
BACKDROP_KEYFRAMES: backdropBreakpoint !== 0 ? customBackdrop : defaultBackdrop,
|
||||
CONTENT_KEYFRAMES: [
|
||||
{ offset: 0, maxHeight: '100%' },
|
||||
{ offset: 1, maxHeight: '0%' },
|
||||
],
|
||||
};
|
||||
|
||||
const contentEl = baseEl.querySelector('ion-content');
|
||||
@@ -79,10 +84,11 @@ export const createSheetGesture = (
|
||||
let offset = 0;
|
||||
let canDismissBlocksGesture = false;
|
||||
const canDismissMaxStep = 0.95;
|
||||
const wrapperAnimation = animation.childAnimations.find((ani) => ani.id === 'wrapperAnimation');
|
||||
const backdropAnimation = animation.childAnimations.find((ani) => ani.id === 'backdropAnimation');
|
||||
const maxBreakpoint = breakpoints[breakpoints.length - 1];
|
||||
const minBreakpoint = breakpoints[0];
|
||||
const wrapperAnimation = animation.childAnimations.find((ani) => ani.id === 'wrapperAnimation');
|
||||
const backdropAnimation = animation.childAnimations.find((ani) => ani.id === 'backdropAnimation');
|
||||
const contentAnimation = animation.childAnimations.find((ani) => ani.id === 'contentAnimation');
|
||||
|
||||
const enableBackdrop = () => {
|
||||
baseEl.style.setProperty('pointer-events', 'auto');
|
||||
@@ -110,6 +116,36 @@ export const createSheetGesture = (
|
||||
baseEl.classList.add(FOCUS_TRAP_DISABLE_CLASS);
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggles the visible modal footer when `expandToScroll` is disabled.
|
||||
* @param footer The footer to show.
|
||||
*/
|
||||
const swapFooterVisibility = (footer: 'original' | 'cloned') => {
|
||||
const originalFooter = baseEl.querySelector('ion-footer') as HTMLIonFooterElement | null;
|
||||
|
||||
if (!originalFooter) {
|
||||
return;
|
||||
}
|
||||
|
||||
const clonedFooter = wrapperEl.nextElementSibling as HTMLIonFooterElement;
|
||||
const footerToHide = footer === 'original' ? clonedFooter : originalFooter;
|
||||
const footerToShow = footer === 'original' ? originalFooter : clonedFooter;
|
||||
|
||||
footerToShow.style.removeProperty('display');
|
||||
footerToShow.removeAttribute('aria-hidden');
|
||||
|
||||
const page = baseEl.querySelector('.ion-page') as HTMLElement;
|
||||
if (footer === 'original') {
|
||||
page.style.removeProperty('padding-bottom');
|
||||
} else {
|
||||
const pagePadding = footerToShow.clientHeight;
|
||||
page.style.setProperty('padding-bottom', `${pagePadding}px`);
|
||||
}
|
||||
|
||||
footerToHide.style.setProperty('display', 'none');
|
||||
footerToHide.setAttribute('aria-hidden', 'true');
|
||||
};
|
||||
|
||||
/**
|
||||
* After the entering animation completes,
|
||||
* we need to set the animation to go from
|
||||
@@ -121,6 +157,7 @@ export const createSheetGesture = (
|
||||
if (wrapperAnimation && backdropAnimation) {
|
||||
wrapperAnimation.keyframes([...SheetDefaults.WRAPPER_KEYFRAMES]);
|
||||
backdropAnimation.keyframes([...SheetDefaults.BACKDROP_KEYFRAMES]);
|
||||
contentAnimation?.keyframes([...SheetDefaults.CONTENT_KEYFRAMES]);
|
||||
animation.progressStart(true, 1 - currentBreakpoint);
|
||||
|
||||
/**
|
||||
@@ -138,7 +175,7 @@ export const createSheetGesture = (
|
||||
}
|
||||
}
|
||||
|
||||
if (contentEl && currentBreakpoint !== maxBreakpoint) {
|
||||
if (contentEl && currentBreakpoint !== maxBreakpoint && expandToScroll) {
|
||||
contentEl.scrollY = false;
|
||||
}
|
||||
|
||||
@@ -154,6 +191,14 @@ export const createSheetGesture = (
|
||||
const contentEl = findClosestIonContent(detail.event.target! as HTMLElement);
|
||||
currentBreakpoint = getCurrentBreakpoint();
|
||||
|
||||
/**
|
||||
* If we have expandToScroll disabled, we should not allow the swipe gesture to start
|
||||
* if the content is being swiped.
|
||||
*/
|
||||
if (!expandToScroll && contentEl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (currentBreakpoint === 1 && contentEl) {
|
||||
/**
|
||||
* The modal should never swipe to close on the content with a refresher.
|
||||
@@ -187,6 +232,16 @@ export const createSheetGesture = (
|
||||
*/
|
||||
canDismissBlocksGesture = baseEl.canDismiss !== undefined && baseEl.canDismiss !== true && minBreakpoint === 0;
|
||||
|
||||
/**
|
||||
* If expandToScroll is disabled, we need to swap
|
||||
* the footer visibility to the original, so if the modal
|
||||
* is dismissed, the footer dismisses with the modal
|
||||
* and doesn't stay on the screen after the modal is gone.
|
||||
*/
|
||||
if (!expandToScroll) {
|
||||
swapFooterVisibility('original');
|
||||
}
|
||||
|
||||
/**
|
||||
* If we are pulling down, then it is possible we are pulling on the content.
|
||||
* We do not want scrolling to happen at the same time as the gesture.
|
||||
@@ -323,6 +378,20 @@ export const createSheetGesture = (
|
||||
},
|
||||
]);
|
||||
|
||||
if (contentAnimation) {
|
||||
/**
|
||||
* The modal content should scroll at any breakpoint when expandToScroll
|
||||
* is disabled. In order to do this, the content needs to be completely
|
||||
* viewable so scrolling can access everything. Otherwise, the default
|
||||
* behavior would show the content off the screen and only allow
|
||||
* scrolling when the sheet is fully expanded.
|
||||
*/
|
||||
contentAnimation.keyframes([
|
||||
{ offset: 0, maxHeight: `${(1 - breakpointOffset) * 100}%` },
|
||||
{ offset: 1, maxHeight: `${snapToBreakpoint * 100}%` },
|
||||
]);
|
||||
}
|
||||
|
||||
animation.progressStep(0);
|
||||
}
|
||||
|
||||
@@ -332,6 +401,15 @@ export const createSheetGesture = (
|
||||
*/
|
||||
gesture.enable(false);
|
||||
|
||||
/**
|
||||
* If expandToScroll is disabled, we need to swap
|
||||
* the footer visibility to the cloned one so the footer
|
||||
* doesn't flicker when the sheet's height is animated.
|
||||
*/
|
||||
if (!expandToScroll && shouldRemainOpen) {
|
||||
swapFooterVisibility('cloned');
|
||||
}
|
||||
|
||||
if (shouldPreventDismiss) {
|
||||
handleCanDismiss(baseEl, animation);
|
||||
} else if (!shouldRemainOpen) {
|
||||
@@ -339,13 +417,13 @@ export const createSheetGesture = (
|
||||
}
|
||||
|
||||
/**
|
||||
* If the sheet is going to be fully expanded then we should enable
|
||||
* scrolling immediately. The sheet modal animation takes ~500ms to finish
|
||||
* so if we wait until then there is a visible delay for when scrolling is
|
||||
* re-enabled. Native iOS allows for scrolling on the sheet modal as soon
|
||||
* as the gesture is released, so we align with that.
|
||||
* Enables scrolling immediately if the sheet is about to fully expand
|
||||
* or if it allows scrolling at any breakpoint. Without this, there would
|
||||
* be a ~500ms delay while the modal animation completes, causing a
|
||||
* noticeable lag. Native iOS allows scrolling as soon as the gesture is
|
||||
* released, so we align with that behavior.
|
||||
*/
|
||||
if (contentEl && snapToBreakpoint === breakpoints[breakpoints.length - 1]) {
|
||||
if (contentEl && (snapToBreakpoint === breakpoints[breakpoints.length - 1] || !expandToScroll)) {
|
||||
contentEl.scrollY = true;
|
||||
}
|
||||
|
||||
@@ -365,6 +443,7 @@ export const createSheetGesture = (
|
||||
raf(() => {
|
||||
wrapperAnimation.keyframes([...SheetDefaults.WRAPPER_KEYFRAMES]);
|
||||
backdropAnimation.keyframes([...SheetDefaults.BACKDROP_KEYFRAMES]);
|
||||
contentAnimation?.keyframes([...SheetDefaults.CONTENT_KEYFRAMES]);
|
||||
animation.progressStart(true, 1 - snapToBreakpoint);
|
||||
currentBreakpoint = snapToBreakpoint;
|
||||
onBreakpointChange(currentBreakpoint);
|
||||
|
||||
@@ -31,6 +31,7 @@ export interface ModalAnimationOptions {
|
||||
presentingEl?: HTMLElement;
|
||||
currentBreakpoint?: number;
|
||||
backdropBreakpoint?: number;
|
||||
expandToScroll: boolean;
|
||||
}
|
||||
|
||||
export interface ModalBreakpointChangeEventDetail {
|
||||
|
||||
@@ -87,3 +87,16 @@
|
||||
:host(.modal-sheet) .modal-wrapper {
|
||||
@include border-radius(var(--border-radius), var(--border-radius), 0, 0);
|
||||
}
|
||||
|
||||
// iOS Sheet Modal - Scroll at all breakpoints
|
||||
// --------------------------------------------------
|
||||
|
||||
/**
|
||||
* Sheet modals require an additional padding as mentioned in the
|
||||
* `core.scss` file. However, there's a workaround that requires
|
||||
* a cloned footer to be added to the modal. This is only necessary
|
||||
* because the core styles are not being applied to the cloned footer.
|
||||
*/
|
||||
:host(.modal-sheet.modal-no-expand-scroll) ion-footer ion-toolbar:first-of-type {
|
||||
padding-top: $modal-sheet-padding-top;
|
||||
}
|
||||
|
||||
@@ -166,3 +166,13 @@ ion-backdrop {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
// Sheet Modal - Scroll at all breakpoints
|
||||
// --------------------------------------------------
|
||||
|
||||
:host(.modal-sheet.modal-no-expand-scroll) ion-footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
|
||||
width: var(--width);
|
||||
}
|
||||
|
||||
@@ -130,6 +130,18 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
*/
|
||||
@Prop() breakpoints?: number[];
|
||||
|
||||
/**
|
||||
* Controls whether scrolling or dragging within the sheet modal expands
|
||||
* it to a larger breakpoint. This only takes effect when `breakpoints`
|
||||
* and `initialBreakpoint` are set.
|
||||
*
|
||||
* If `true`, scrolling or dragging anywhere in the modal will first expand
|
||||
* it to the next breakpoint. Once fully expanded, scrolling will affect the content.
|
||||
* If `false`, scrolling will always affect the content, and the modal will only expand
|
||||
* when dragging the header or handle.
|
||||
*/
|
||||
@Prop() expandToScroll = true;
|
||||
|
||||
/**
|
||||
* A decimal value between 0 and 1 that indicates the
|
||||
* initial point the modal will open at when creating a
|
||||
@@ -562,6 +574,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
presentingEl: presentingElement,
|
||||
currentBreakpoint: this.initialBreakpoint,
|
||||
backdropBreakpoint: this.backdropBreakpoint,
|
||||
expandToScroll: this.expandToScroll,
|
||||
});
|
||||
|
||||
/* tslint:disable-next-line */
|
||||
@@ -616,7 +629,10 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
// should be in the DOM and referenced by now, except
|
||||
// for the presenting el
|
||||
const animationBuilder = this.leaveAnimation || config.get('modalLeave', iosLeaveAnimation);
|
||||
const ani = (this.animation = animationBuilder(el, { presentingEl: this.presentingElement }));
|
||||
const ani = (this.animation = animationBuilder(el, {
|
||||
presentingEl: this.presentingElement,
|
||||
expandToScroll: this.expandToScroll,
|
||||
}));
|
||||
|
||||
const contentEl = findIonContent(el);
|
||||
if (!contentEl) {
|
||||
@@ -668,6 +684,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
presentingEl: this.presentingElement,
|
||||
currentBreakpoint: initialBreakpoint,
|
||||
backdropBreakpoint,
|
||||
expandToScroll: this.expandToScroll,
|
||||
}));
|
||||
|
||||
ani.progressStart(true, 1);
|
||||
@@ -680,6 +697,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
backdropBreakpoint,
|
||||
ani,
|
||||
this.sortedBreakpoints,
|
||||
this.expandToScroll,
|
||||
() => this.currentBreakpoint ?? 0,
|
||||
() => this.sheetOnDismiss(),
|
||||
(breakpoint: number) => {
|
||||
@@ -778,6 +796,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
presentingEl: presentingElement,
|
||||
currentBreakpoint: this.currentBreakpoint ?? this.initialBreakpoint,
|
||||
backdropBreakpoint: this.backdropBreakpoint,
|
||||
expandToScroll: this.expandToScroll,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -927,9 +946,16 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { handle, isSheetModal, presentingElement, htmlAttributes, handleBehavior, inheritedAttributes, focusTrap } =
|
||||
this;
|
||||
|
||||
const {
|
||||
handle,
|
||||
isSheetModal,
|
||||
presentingElement,
|
||||
htmlAttributes,
|
||||
handleBehavior,
|
||||
inheritedAttributes,
|
||||
focusTrap,
|
||||
expandToScroll,
|
||||
} = this;
|
||||
const showHandle = handle !== false && isSheetModal;
|
||||
const mode = getIonMode(this);
|
||||
const isCardModal = presentingElement !== undefined && mode === 'ios';
|
||||
@@ -948,6 +974,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
['modal-default']: !isCardModal && !isSheetModal,
|
||||
[`modal-card`]: isCardModal,
|
||||
[`modal-sheet`]: isSheetModal,
|
||||
[`modal-no-expand-scroll`]: isSheetModal && !expandToScroll,
|
||||
'overlay-hidden': true,
|
||||
[FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false,
|
||||
...getClassMap(this.cssClass),
|
||||
@@ -1019,6 +1046,12 @@ interface ModalOverlayOptions {
|
||||
* to fade in when using a sheet modal.
|
||||
*/
|
||||
backdropBreakpoint: number;
|
||||
|
||||
/**
|
||||
* Whether or not the modal should scroll/drag
|
||||
* the content only when fully expanded.
|
||||
*/
|
||||
expandToScroll?: boolean;
|
||||
}
|
||||
|
||||
type ModalPresentOptions = ModalOverlayOptions;
|
||||
|
||||
@@ -23,3 +23,9 @@ $modal-inset-height-large: 600px;
|
||||
|
||||
/// @prop - Text color of the modal content
|
||||
$modal-text-color: $text-color;
|
||||
|
||||
/// @prop - Padding top of the sheet modal
|
||||
$modal-sheet-padding-top: 6px;
|
||||
|
||||
/// @prop - Padding bottom of the sheet modal
|
||||
$modal-sheet-padding-bottom: 6px;
|
||||
|
||||
@@ -100,6 +100,12 @@
|
||||
>
|
||||
Present Sheet Modal (Max breakpoint is not 1)
|
||||
</button>
|
||||
<button
|
||||
id="scroll-at-edge-modal"
|
||||
onclick="presentModal({ initialBreakpoint: 0.5, breakpoints: [0, 0.25, 0.5, 0.75, 1], expandToScroll: false })"
|
||||
>
|
||||
Present Sheet Modal (Scroll at any breakpoint)
|
||||
</button>
|
||||
<button
|
||||
id="custom-backdrop-modal"
|
||||
onclick="presentModal({ backdropBreakpoint: 0.5, initialBreakpoint: 0.5 })"
|
||||
@@ -184,6 +190,11 @@
|
||||
${items}
|
||||
</ion-list>
|
||||
</ion-content>
|
||||
<ion-footer>
|
||||
<ion-toolbar>
|
||||
<ion-title>Footer</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-footer>
|
||||
`;
|
||||
|
||||
let extraOptions = {
|
||||
@@ -209,6 +220,7 @@
|
||||
button.addEventListener('click', () => {
|
||||
modalElement.dismiss();
|
||||
});
|
||||
|
||||
document.body.appendChild(modalElement);
|
||||
return modalElement;
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
@@ -2,7 +2,7 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core';
|
||||
import { Component, Element, Event, Host, Prop, State, Watch, h } from '@stencil/core';
|
||||
import { findClosestIonContent, disableContentScrollY, resetContentScrollY } from '@utils/content';
|
||||
import type { Attributes } from '@utils/helpers';
|
||||
import { inheritAriaAttributes, clamp, debounceEvent, renderHiddenInput, isSafeNumber } from '@utils/helpers';
|
||||
import { inheritAriaAttributes, clamp, debounceEvent, renderHiddenInput } from '@utils/helpers';
|
||||
import { printIonWarning } from '@utils/logging';
|
||||
import { isRTL } from '@utils/rtl';
|
||||
import { createColorClasses, hostContext } from '@utils/theme';
|
||||
@@ -109,11 +109,7 @@ export class Range implements ComponentInterface {
|
||||
*/
|
||||
@Prop() min = 0;
|
||||
@Watch('min')
|
||||
protected minChanged(newValue: number) {
|
||||
if (!isSafeNumber(newValue)) {
|
||||
this.min = 0;
|
||||
}
|
||||
|
||||
protected minChanged() {
|
||||
if (!this.noUpdate) {
|
||||
this.updateRatio();
|
||||
}
|
||||
@@ -124,11 +120,7 @@ export class Range implements ComponentInterface {
|
||||
*/
|
||||
@Prop() max = 100;
|
||||
@Watch('max')
|
||||
protected maxChanged(newValue: number) {
|
||||
if (!isSafeNumber(newValue)) {
|
||||
this.max = 100;
|
||||
}
|
||||
|
||||
protected maxChanged() {
|
||||
if (!this.noUpdate) {
|
||||
this.updateRatio();
|
||||
}
|
||||
@@ -159,12 +151,6 @@ export class Range implements ComponentInterface {
|
||||
* Specifies the value granularity.
|
||||
*/
|
||||
@Prop() step = 1;
|
||||
@Watch('step')
|
||||
protected stepChanged(newValue: number) {
|
||||
if (!isSafeNumber(newValue)) {
|
||||
this.step = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If `true`, tick marks are displayed based on the step value.
|
||||
@@ -314,11 +300,6 @@ export class Range implements ComponentInterface {
|
||||
}
|
||||
|
||||
this.inheritedAttributes = inheritAriaAttributes(this.el);
|
||||
// If the min or max is not safe, set it to 0 or 100 respectively.
|
||||
// Our watch does this, but not before the initial load.
|
||||
this.min = isSafeNumber(this.min) ? this.min : 0;
|
||||
this.max = isSafeNumber(this.max) ? this.max : 100;
|
||||
this.step = isSafeNumber(this.step) ? this.step : 1;
|
||||
}
|
||||
|
||||
componentDidLoad() {
|
||||
|
||||
@@ -28,25 +28,6 @@ describe('Range', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle undefined min and max values by falling back to defaults', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Range],
|
||||
html: `<ion-range id="my-custom-range">
|
||||
<div slot="label">Range</div>
|
||||
</ion-range>`,
|
||||
});
|
||||
|
||||
const range = page.body.querySelector('ion-range')!;
|
||||
// Here we have to cast this to any, but in its react wrapper it accepts undefined as a valid value
|
||||
range.min = undefined as any;
|
||||
range.max = undefined as any;
|
||||
range.step = undefined as any;
|
||||
await page.waitForChanges();
|
||||
expect(range.min).toBe(0);
|
||||
expect(range.max).toBe(100);
|
||||
expect(range.step).toBe(1);
|
||||
});
|
||||
|
||||
it('should return the clamped value for a range dual knob component', () => {
|
||||
sharedRange.min = 0;
|
||||
sharedRange.max = 100;
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ComponentInterface } from '@stencil/core';
|
||||
import { Component, Element, Host, Prop, Method, State, Watch, forceUpdate, h } from '@stencil/core';
|
||||
import type { ButtonInterface } from '@utils/element-interface';
|
||||
import type { Attributes } from '@utils/helpers';
|
||||
import { addEventListener, removeEventListener, inheritAttributes, getNextSiblingOfType } from '@utils/helpers';
|
||||
import { addEventListener, removeEventListener, inheritAttributes } from '@utils/helpers';
|
||||
import { hostContext } from '@utils/theme';
|
||||
|
||||
import { getIonMode } from '../../global/ionic-global';
|
||||
@@ -65,41 +65,7 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
|
||||
this.updateState();
|
||||
}
|
||||
|
||||
private waitForSegmentContent(ionSegment: HTMLIonSegmentElement | null, contentId: string): Promise<HTMLElement> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let timeoutId: NodeJS.Timeout | undefined = undefined;
|
||||
let animationFrameId: number;
|
||||
|
||||
const check = () => {
|
||||
if (!ionSegment) {
|
||||
reject(new Error(`Segment not found when looking for Segment Content`));
|
||||
return;
|
||||
}
|
||||
|
||||
const segmentView = getNextSiblingOfType<HTMLIonSegmentViewElement>(ionSegment); // Skip the text nodes
|
||||
const segmentContent = segmentView?.querySelector(
|
||||
`ion-segment-content[id="${contentId}"]`
|
||||
) as HTMLIonSegmentContentElement | null;
|
||||
if (segmentContent && timeoutId) {
|
||||
clearTimeout(timeoutId); // Clear the timeout if the segmentContent is found
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
resolve(segmentContent);
|
||||
} else {
|
||||
animationFrameId = requestAnimationFrame(check); // Keep checking on the next animation frame
|
||||
}
|
||||
};
|
||||
|
||||
check();
|
||||
|
||||
// Set a timeout to reject the promise
|
||||
timeoutId = setTimeout(() => {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
reject(new Error(`Unable to find Segment Content with id="${contentId} within 1000 ms`));
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
connectedCallback() {
|
||||
const segmentEl = (this.segmentEl = this.el.closest('ion-segment'));
|
||||
if (segmentEl) {
|
||||
this.updateState();
|
||||
@@ -110,13 +76,12 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
|
||||
// Return if there is no contentId defined
|
||||
if (!this.contentId) return;
|
||||
|
||||
let segmentContent;
|
||||
try {
|
||||
// Attempt to find the Segment Content by its contentId
|
||||
segmentContent = await this.waitForSegmentContent(segmentEl, this.contentId);
|
||||
} catch (error) {
|
||||
// If no associated Segment Content exists, log an error and return
|
||||
console.error('Segment Button: ', (error as Error).message);
|
||||
// Attempt to find the Segment Content by its contentId
|
||||
const segmentContent = document.getElementById(this.contentId) as HTMLIonSegmentContentElement | null;
|
||||
|
||||
// If no associated Segment Content exists, log an error and return
|
||||
if (!segmentContent) {
|
||||
console.error(`Segment Button: Unable to find Segment Content with id="${this.contentId}".`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -123,8 +123,6 @@
|
||||
<button class="expand" onClick="changeSegmentContent()">Change Segment Content</button>
|
||||
|
||||
<button class="expand" onClick="clearSegmentValue()">Clear Segment Value</button>
|
||||
|
||||
<button class="expand" onClick="addSegmentButtonAndContent()">Add New Segment Button & Content</button>
|
||||
</ion-content>
|
||||
|
||||
<ion-footer>
|
||||
@@ -160,34 +158,6 @@
|
||||
segment.value = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
async function addSegmentButtonAndContent() {
|
||||
const segment = document.querySelector('ion-segment');
|
||||
const segmentView = document.querySelector('ion-segment-view');
|
||||
|
||||
const newButton = document.createElement('ion-segment-button');
|
||||
const newId = `new-${Date.now()}`;
|
||||
newButton.setAttribute('content-id', newId);
|
||||
newButton.setAttribute('value', newId);
|
||||
newButton.innerHTML = '<ion-label>New Button</ion-label>';
|
||||
|
||||
segment.appendChild(newButton);
|
||||
|
||||
setTimeout(() => {
|
||||
// Timeout to test waitForSegmentContent() in segment-button
|
||||
const newContent = document.createElement('ion-segment-content');
|
||||
newContent.setAttribute('id', newId);
|
||||
newContent.innerHTML = 'New Content';
|
||||
|
||||
segmentView.appendChild(newContent);
|
||||
|
||||
// Necessary timeout to ensure the value is set after the content is added.
|
||||
// Otherwise, the transition is unsuccessful and the content is not shown.
|
||||
setTimeout(() => {
|
||||
segment.setAttribute('value', newId);
|
||||
}, 200);
|
||||
}, 200);
|
||||
}
|
||||
</script>
|
||||
</ion-app>
|
||||
</body>
|
||||
|
||||
@@ -196,6 +196,13 @@ export class Select implements ComponentInterface {
|
||||
*/
|
||||
@Prop({ mutable: true }) value?: any | null;
|
||||
|
||||
/**
|
||||
* If true, screen readers will announce it as a required field. This property
|
||||
* works only for accessibility purposes, it will not prevent the form from
|
||||
* submitting if the value is invalid.
|
||||
*/
|
||||
@Prop() required = false;
|
||||
|
||||
/**
|
||||
* Emitted when the value has changed.
|
||||
*
|
||||
@@ -310,10 +317,19 @@ export class Select implements ComponentInterface {
|
||||
}
|
||||
this.isExpanded = true;
|
||||
const overlay = (this.overlay = await this.createOverlay(event));
|
||||
overlay.onDidDismiss().then(() => {
|
||||
this.overlay = undefined;
|
||||
this.isExpanded = false;
|
||||
this.ionDismiss.emit();
|
||||
this.setFocus();
|
||||
});
|
||||
|
||||
// Add logic to scroll selected item into view before presenting
|
||||
const scrollSelectedIntoView = () => {
|
||||
await overlay.present();
|
||||
|
||||
// focus selected option for popovers and modals
|
||||
if (this.interface === 'popover' || this.interface === 'modal') {
|
||||
const indexOfSelected = this.childOpts.findIndex((o) => o.value === this.value);
|
||||
|
||||
if (indexOfSelected > -1) {
|
||||
const selectedItem = overlay.querySelector<HTMLElement>(
|
||||
`.select-interface-option:nth-child(${indexOfSelected + 1})`
|
||||
@@ -336,7 +352,6 @@ export class Select implements ComponentInterface {
|
||||
| HTMLIonCheckboxElement
|
||||
| null;
|
||||
if (interactiveEl) {
|
||||
selectedItem.scrollIntoView({ block: 'nearest' });
|
||||
// Needs to be called before `focusVisibleElement` to prevent issue with focus event bubbling
|
||||
// and removing `ion-focused` style
|
||||
interactiveEl.setFocus();
|
||||
@@ -364,40 +379,8 @@ export class Select implements ComponentInterface {
|
||||
focusVisibleElement(firstEnabledOption.closest('ion-item')!);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// For modals and popovers, we can scroll before they're visible
|
||||
if (this.interface === 'modal') {
|
||||
overlay.addEventListener('ionModalWillPresent', scrollSelectedIntoView, { once: true });
|
||||
} else if (this.interface === 'popover') {
|
||||
overlay.addEventListener('ionPopoverWillPresent', scrollSelectedIntoView, { once: true });
|
||||
} else {
|
||||
/**
|
||||
* For alerts and action sheets, we need to wait a frame after willPresent
|
||||
* because these overlays don't have their content in the DOM immediately
|
||||
* when willPresent fires. By waiting a frame, we ensure the content is
|
||||
* rendered and can be properly scrolled into view.
|
||||
*/
|
||||
const scrollAfterRender = () => {
|
||||
requestAnimationFrame(() => {
|
||||
scrollSelectedIntoView();
|
||||
});
|
||||
};
|
||||
if (this.interface === 'alert') {
|
||||
overlay.addEventListener('ionAlertWillPresent', scrollAfterRender, { once: true });
|
||||
} else if (this.interface === 'action-sheet') {
|
||||
overlay.addEventListener('ionActionSheetWillPresent', scrollAfterRender, { once: true });
|
||||
}
|
||||
}
|
||||
|
||||
overlay.onDidDismiss().then(() => {
|
||||
this.overlay = undefined;
|
||||
this.isExpanded = false;
|
||||
this.ionDismiss.emit();
|
||||
this.setFocus();
|
||||
});
|
||||
|
||||
await overlay.present();
|
||||
return overlay;
|
||||
}
|
||||
|
||||
@@ -998,7 +981,7 @@ export class Select implements ComponentInterface {
|
||||
}
|
||||
|
||||
private renderListbox() {
|
||||
const { disabled, inputId, isExpanded } = this;
|
||||
const { disabled, inputId, isExpanded, required } = this;
|
||||
|
||||
return (
|
||||
<button
|
||||
@@ -1007,6 +990,7 @@ export class Select implements ComponentInterface {
|
||||
aria-label={this.ariaLabel}
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded={`${isExpanded}`}
|
||||
aria-required={`${required}`}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
ref={(focusEl) => (this.focusEl = focusEl)}
|
||||
|
||||
@@ -61,169 +61,6 @@
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
|
||||
<ion-list>
|
||||
<ion-list-header>
|
||||
<ion-label>Single Value - Overflowing Options</ion-label>
|
||||
</ion-list-header>
|
||||
|
||||
<ion-item>
|
||||
<ion-select id="alert-select-scroll-to-selected" label="Alert" interface="alert" value="watermelon">
|
||||
<ion-select-option value="apple">Apple</ion-select-option>
|
||||
<ion-select-option value="apricot">Apricot</ion-select-option>
|
||||
<ion-select-option value="avocado">Avocado</ion-select-option>
|
||||
<ion-select-option value="banana">Banana</ion-select-option>
|
||||
<ion-select-option value="blackberry">Blackberry</ion-select-option>
|
||||
<ion-select-option value="blueberry">Blueberry</ion-select-option>
|
||||
<ion-select-option value="cantaloupe">Cantaloupe</ion-select-option>
|
||||
<ion-select-option value="cherry">Cherry</ion-select-option>
|
||||
<ion-select-option value="coconut">Coconut</ion-select-option>
|
||||
<ion-select-option value="cranberry">Cranberry</ion-select-option>
|
||||
<ion-select-option value="dragonfruit">Dragonfruit</ion-select-option>
|
||||
<ion-select-option value="fig">Fig</ion-select-option>
|
||||
<ion-select-option value="grape">Grape</ion-select-option>
|
||||
<ion-select-option value="grapefruit">Grapefruit</ion-select-option>
|
||||
<ion-select-option value="guava">Guava</ion-select-option>
|
||||
<ion-select-option value="kiwi">Kiwi</ion-select-option>
|
||||
<ion-select-option value="lemon">Lemon</ion-select-option>
|
||||
<ion-select-option value="lime">Lime</ion-select-option>
|
||||
<ion-select-option value="lychee">Lychee</ion-select-option>
|
||||
<ion-select-option value="mango">Mango</ion-select-option>
|
||||
<ion-select-option value="nectarine">Nectarine</ion-select-option>
|
||||
<ion-select-option value="orange">Orange</ion-select-option>
|
||||
<ion-select-option value="papaya">Papaya</ion-select-option>
|
||||
<ion-select-option value="passion-fruit">Passion Fruit</ion-select-option>
|
||||
<ion-select-option value="peach">Peach</ion-select-option>
|
||||
<ion-select-option value="pear">Pear</ion-select-option>
|
||||
<ion-select-option value="pineapple">Pineapple</ion-select-option>
|
||||
<ion-select-option value="plum">Plum</ion-select-option>
|
||||
<ion-select-option value="pomegranate">Pomegranate</ion-select-option>
|
||||
<ion-select-option value="raspberry">Raspberry</ion-select-option>
|
||||
<ion-select-option value="strawberry">Strawberry</ion-select-option>
|
||||
<ion-select-option value="tangerine">Tangerine</ion-select-option>
|
||||
<ion-select-option value="watermelon">Watermelon</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-select
|
||||
id="action-sheet-select-scroll-to-selected"
|
||||
label="Action Sheet"
|
||||
interface="action-sheet"
|
||||
value="watermelon"
|
||||
>
|
||||
<ion-select-option value="apple">Apple</ion-select-option>
|
||||
<ion-select-option value="apricot">Apricot</ion-select-option>
|
||||
<ion-select-option value="avocado">Avocado</ion-select-option>
|
||||
<ion-select-option value="banana">Banana</ion-select-option>
|
||||
<ion-select-option value="blackberry">Blackberry</ion-select-option>
|
||||
<ion-select-option value="blueberry">Blueberry</ion-select-option>
|
||||
<ion-select-option value="cantaloupe">Cantaloupe</ion-select-option>
|
||||
<ion-select-option value="cherry">Cherry</ion-select-option>
|
||||
<ion-select-option value="coconut">Coconut</ion-select-option>
|
||||
<ion-select-option value="cranberry">Cranberry</ion-select-option>
|
||||
<ion-select-option value="dragonfruit">Dragonfruit</ion-select-option>
|
||||
<ion-select-option value="fig">Fig</ion-select-option>
|
||||
<ion-select-option value="grape">Grape</ion-select-option>
|
||||
<ion-select-option value="grapefruit">Grapefruit</ion-select-option>
|
||||
<ion-select-option value="guava">Guava</ion-select-option>
|
||||
<ion-select-option value="kiwi">Kiwi</ion-select-option>
|
||||
<ion-select-option value="lemon">Lemon</ion-select-option>
|
||||
<ion-select-option value="lime">Lime</ion-select-option>
|
||||
<ion-select-option value="lychee">Lychee</ion-select-option>
|
||||
<ion-select-option value="mango">Mango</ion-select-option>
|
||||
<ion-select-option value="nectarine">Nectarine</ion-select-option>
|
||||
<ion-select-option value="orange">Orange</ion-select-option>
|
||||
<ion-select-option value="papaya">Papaya</ion-select-option>
|
||||
<ion-select-option value="passion-fruit">Passion Fruit</ion-select-option>
|
||||
<ion-select-option value="peach">Peach</ion-select-option>
|
||||
<ion-select-option value="pear">Pear</ion-select-option>
|
||||
<ion-select-option value="pineapple">Pineapple</ion-select-option>
|
||||
<ion-select-option value="plum">Plum</ion-select-option>
|
||||
<ion-select-option value="pomegranate">Pomegranate</ion-select-option>
|
||||
<ion-select-option value="raspberry">Raspberry</ion-select-option>
|
||||
<ion-select-option value="strawberry">Strawberry</ion-select-option>
|
||||
<ion-select-option value="tangerine">Tangerine</ion-select-option>
|
||||
<ion-select-option value="watermelon">Watermelon</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-select id="popover-select-scroll-to-selected" label="Popover" interface="popover" value="watermelon">
|
||||
<ion-select-option value="apple">Apple</ion-select-option>
|
||||
<ion-select-option value="apricot">Apricot</ion-select-option>
|
||||
<ion-select-option value="avocado">Avocado</ion-select-option>
|
||||
<ion-select-option value="banana">Banana</ion-select-option>
|
||||
<ion-select-option value="blackberry">Blackberry</ion-select-option>
|
||||
<ion-select-option value="blueberry">Blueberry</ion-select-option>
|
||||
<ion-select-option value="cantaloupe">Cantaloupe</ion-select-option>
|
||||
<ion-select-option value="cherry">Cherry</ion-select-option>
|
||||
<ion-select-option value="coconut">Coconut</ion-select-option>
|
||||
<ion-select-option value="cranberry">Cranberry</ion-select-option>
|
||||
<ion-select-option value="dragonfruit">Dragonfruit</ion-select-option>
|
||||
<ion-select-option value="fig">Fig</ion-select-option>
|
||||
<ion-select-option value="grape">Grape</ion-select-option>
|
||||
<ion-select-option value="grapefruit">Grapefruit</ion-select-option>
|
||||
<ion-select-option value="guava">Guava</ion-select-option>
|
||||
<ion-select-option value="kiwi">Kiwi</ion-select-option>
|
||||
<ion-select-option value="lemon">Lemon</ion-select-option>
|
||||
<ion-select-option value="lime">Lime</ion-select-option>
|
||||
<ion-select-option value="lychee">Lychee</ion-select-option>
|
||||
<ion-select-option value="mango">Mango</ion-select-option>
|
||||
<ion-select-option value="nectarine">Nectarine</ion-select-option>
|
||||
<ion-select-option value="orange">Orange</ion-select-option>
|
||||
<ion-select-option value="papaya">Papaya</ion-select-option>
|
||||
<ion-select-option value="passion-fruit">Passion Fruit</ion-select-option>
|
||||
<ion-select-option value="peach">Peach</ion-select-option>
|
||||
<ion-select-option value="pear">Pear</ion-select-option>
|
||||
<ion-select-option value="pineapple">Pineapple</ion-select-option>
|
||||
<ion-select-option value="plum">Plum</ion-select-option>
|
||||
<ion-select-option value="pomegranate">Pomegranate</ion-select-option>
|
||||
<ion-select-option value="raspberry">Raspberry</ion-select-option>
|
||||
<ion-select-option value="strawberry">Strawberry</ion-select-option>
|
||||
<ion-select-option value="tangerine">Tangerine</ion-select-option>
|
||||
<ion-select-option value="watermelon">Watermelon</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-select id="modal-select-scroll-to-selected" label="Modal" interface="modal" value="watermelon">
|
||||
<ion-select-option value="apple">Apple</ion-select-option>
|
||||
<ion-select-option value="apricot">Apricot</ion-select-option>
|
||||
<ion-select-option value="avocado">Avocado</ion-select-option>
|
||||
<ion-select-option value="banana">Banana</ion-select-option>
|
||||
<ion-select-option value="blackberry">Blackberry</ion-select-option>
|
||||
<ion-select-option value="blueberry">Blueberry</ion-select-option>
|
||||
<ion-select-option value="cantaloupe">Cantaloupe</ion-select-option>
|
||||
<ion-select-option value="cherry">Cherry</ion-select-option>
|
||||
<ion-select-option value="coconut">Coconut</ion-select-option>
|
||||
<ion-select-option value="cranberry">Cranberry</ion-select-option>
|
||||
<ion-select-option value="dragonfruit">Dragonfruit</ion-select-option>
|
||||
<ion-select-option value="fig">Fig</ion-select-option>
|
||||
<ion-select-option value="grape">Grape</ion-select-option>
|
||||
<ion-select-option value="grapefruit">Grapefruit</ion-select-option>
|
||||
<ion-select-option value="guava">Guava</ion-select-option>
|
||||
<ion-select-option value="kiwi">Kiwi</ion-select-option>
|
||||
<ion-select-option value="lemon">Lemon</ion-select-option>
|
||||
<ion-select-option value="lime">Lime</ion-select-option>
|
||||
<ion-select-option value="lychee">Lychee</ion-select-option>
|
||||
<ion-select-option value="mango">Mango</ion-select-option>
|
||||
<ion-select-option value="nectarine">Nectarine</ion-select-option>
|
||||
<ion-select-option value="orange">Orange</ion-select-option>
|
||||
<ion-select-option value="papaya">Papaya</ion-select-option>
|
||||
<ion-select-option value="passion-fruit">Passion Fruit</ion-select-option>
|
||||
<ion-select-option value="peach">Peach</ion-select-option>
|
||||
<ion-select-option value="pear">Pear</ion-select-option>
|
||||
<ion-select-option value="pineapple">Pineapple</ion-select-option>
|
||||
<ion-select-option value="plum">Plum</ion-select-option>
|
||||
<ion-select-option value="pomegranate">Pomegranate</ion-select-option>
|
||||
<ion-select-option value="raspberry">Raspberry</ion-select-option>
|
||||
<ion-select-option value="strawberry">Strawberry</ion-select-option>
|
||||
<ion-select-option value="tangerine">Tangerine</ion-select-option>
|
||||
<ion-select-option value="watermelon">Watermelon</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
|
||||
<ion-list>
|
||||
<ion-list-header>
|
||||
<ion-label>Multiple Value Select</ion-label>
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { E2ELocator } from '@utils/test/playwright';
|
||||
* does not. The overlay rendering is already tested in the respective
|
||||
* test files.
|
||||
*/
|
||||
configs({ directions: ['ltr'] }).forEach(({ title, config, screenshot }) => {
|
||||
configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('select: basic'), () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/src/components/select/test/basic', config);
|
||||
@@ -24,16 +24,6 @@ configs({ directions: ['ltr'] }).forEach(({ title, config, screenshot }) => {
|
||||
|
||||
await expect(page.locator('ion-alert')).toBeVisible();
|
||||
});
|
||||
|
||||
test('it should scroll to selected option when opened', async ({ page }) => {
|
||||
const ionAlertDidPresent = await page.spyOnEvent('ionAlertDidPresent');
|
||||
|
||||
await page.click('#alert-select-scroll-to-selected');
|
||||
await ionAlertDidPresent.next();
|
||||
|
||||
const alert = page.locator('ion-alert');
|
||||
await expect(alert).toHaveScreenshot(screenshot(`select-basic-alert-scroll-to-selected`));
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('select: action sheet', () => {
|
||||
@@ -46,16 +36,6 @@ configs({ directions: ['ltr'] }).forEach(({ title, config, screenshot }) => {
|
||||
|
||||
await expect(page.locator('ion-action-sheet')).toBeVisible();
|
||||
});
|
||||
|
||||
test('it should scroll to selected option when opened', async ({ page }) => {
|
||||
const ionActionSheetDidPresent = await page.spyOnEvent('ionActionSheetDidPresent');
|
||||
|
||||
await page.click('#action-sheet-select-scroll-to-selected');
|
||||
await ionActionSheetDidPresent.next();
|
||||
|
||||
const actionSheet = page.locator('ion-action-sheet');
|
||||
await expect(actionSheet).toHaveScreenshot(screenshot(`select-basic-action-sheet-scroll-to-selected`));
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('select: popover', () => {
|
||||
@@ -77,16 +57,6 @@ configs({ directions: ['ltr'] }).forEach(({ title, config, screenshot }) => {
|
||||
|
||||
await expect(popover).toBeVisible();
|
||||
});
|
||||
|
||||
test('it should scroll to selected option when opened', async ({ page }) => {
|
||||
const ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent');
|
||||
|
||||
await page.click('#popover-select-scroll-to-selected');
|
||||
await ionPopoverDidPresent.next();
|
||||
|
||||
const popover = page.locator('ion-popover');
|
||||
await expect(popover).toHaveScreenshot(screenshot(`select-basic-popover-scroll-to-selected`));
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('select: modal', () => {
|
||||
@@ -105,16 +75,6 @@ configs({ directions: ['ltr'] }).forEach(({ title, config, screenshot }) => {
|
||||
|
||||
await expect(modal).toBeVisible();
|
||||
});
|
||||
|
||||
test('it should scroll to selected option when opened', async ({ page }) => {
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
|
||||
await page.click('#modal-select-scroll-to-selected');
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
const modal = page.locator('ion-modal');
|
||||
await expect(modal).toHaveScreenshot(screenshot(`select-basic-modal-scroll-to-selected`));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 22 KiB |