Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5abf5ef752 | ||
|
|
e01c7d7285 | ||
|
|
c3e822ae6d | ||
|
|
e89ce6b57a | ||
|
|
fcefcebe56 | ||
|
|
4df0e0f4c0 | ||
|
|
2149ba2c8d | ||
|
|
b6b43ae292 | ||
|
|
7794a11215 | ||
|
|
8eaeb22e7a | ||
|
|
cd5c27a12a | ||
|
|
71e25ef549 | ||
|
|
14b6538d98 | ||
|
|
21e0f607ef | ||
|
|
4b41bb9ce3 | ||
|
|
9717acb7bb |
4
.github/workflows/assign-issues.yml
vendored
@@ -11,8 +11,8 @@ jobs:
|
||||
issues: write
|
||||
steps:
|
||||
- name: 'Auto-assign issue'
|
||||
uses: pozil/auto-assign-issue@c015a6a3f410f12f58255c3d085fd774312f7a2f # v2.1.2
|
||||
uses: pozil/auto-assign-issue@39c06395cbac76e79afc4ad4e5c5c6db6ecfdd2e # v2.2.0
|
||||
with:
|
||||
assignees: brandyscarney, thetaPC, joselrio, rugoncalves, BenOsodrac, JoaoFerreira-FrontEnd, OS-giulianasilva, tanner-reits
|
||||
assignees: brandyscarney, thetaPC, ShaneK
|
||||
numOfAssignee: 1
|
||||
allowSelfAssign: false
|
||||
|
||||
@@ -403,7 +403,6 @@ 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
|
||||
@@ -1347,7 +1346,6 @@ ion-radio-group,none
|
||||
ion-radio-group,prop,allowEmptySelection,boolean,false,false,false
|
||||
ion-radio-group,prop,compareWith,((currentValue: any, compareValue: any) => boolean) | null | string | undefined,undefined,false,false
|
||||
ion-radio-group,prop,name,string,this.inputId,false,false
|
||||
ion-radio-group,prop,required,boolean,false,false,false
|
||||
ion-radio-group,prop,value,any,undefined,false,false
|
||||
ion-radio-group,event,ionChange,RadioGroupChangeEventDetail<any>,true
|
||||
|
||||
@@ -1633,7 +1631,6 @@ 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
|
||||
@@ -1947,7 +1944,6 @@ 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
|
||||
@@ -2004,7 +2000,4 @@ 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,part,background
|
||||
ion-toolbar,part,container
|
||||
ion-toolbar,part,content
|
||||
ion-toolbar,css-prop,--padding-top,md
|
||||
80
core/package-lock.json
generated
@@ -15,11 +15,11 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@axe-core/playwright": "^4.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",
|
||||
"@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",
|
||||
"@ionic/eslint-config": "^0.3.0",
|
||||
"@ionic/prettier-config": "^2.0.0",
|
||||
"@playwright/test": "^1.46.1",
|
||||
@@ -663,39 +663,43 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@capacitor/core": {
|
||||
"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==",
|
||||
"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==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@capacitor/haptics": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-6.0.2.tgz",
|
||||
"integrity": "sha512-xcFdIH4iIIeW2+1lzmlYMVicqB9ytaiuZ9NE3a9laKFPvMGC7hdj6i6tHFezwPJ/96xkHOwXT2b0F8Mh9xtTWg==",
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-7.0.0.tgz",
|
||||
"integrity": "sha512-8uI8rWyAbq8EzkjS+sHZSncyzujHzVbuLKgj8J5H0yUL6+r26F16gVA2iuQuIBvzbSMy7Y0/pUuWlwZr/H8AKg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@capacitor/core": "^6.0.0"
|
||||
"@capacitor/core": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@capacitor/keyboard": {
|
||||
"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==",
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-7.0.0.tgz",
|
||||
"integrity": "sha512-Tqwy8wG+sx4UqiFCX4Q+bFw6uKgG7BiHKAPpeefoIgoEB8H8Jf3xZNZoVPnJIMuPsCdSvuyHXZbJXH9IEEirGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@capacitor/core": "^6.0.0"
|
||||
"@capacitor/core": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@capacitor/status-bar": {
|
||||
"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==",
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-7.0.0.tgz",
|
||||
"integrity": "sha512-wsvPkWkoSOXMIgVHu4c6P1sOuDSZ1ClUo5OpLRwj7u8DYzlV4jlmNzztQn2Lvsiqx1z4kfukSaqe40k1Lo4c9g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@capacitor/core": "^6.0.0"
|
||||
"@capacitor/core": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@clack/core": {
|
||||
@@ -709,9 +713,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@clack/prompts": {
|
||||
"version": "0.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.9.1.tgz",
|
||||
"integrity": "sha512-JIpyaboYZeWYlyP0H+OoPPxd6nqueG/CmN6ixBiNFsIDHREevjIf0n0Ohh5gr5C8pEDknzgvz+pIJ8dMhzWIeg==",
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.10.0.tgz",
|
||||
"integrity": "sha512-H3rCl6CwW1NdQt9rE3n373t7o5cthPv7yUoxF2ytZvyvlJv89C5RYMJu83Hed8ODgys5vpBU0GKxIRG83jd8NQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@clack/core": "0.4.1",
|
||||
@@ -10977,32 +10981,32 @@
|
||||
"dev": true
|
||||
},
|
||||
"@capacitor/core": {
|
||||
"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==",
|
||||
"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==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"@capacitor/haptics": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-6.0.2.tgz",
|
||||
"integrity": "sha512-xcFdIH4iIIeW2+1lzmlYMVicqB9ytaiuZ9NE3a9laKFPvMGC7hdj6i6tHFezwPJ/96xkHOwXT2b0F8Mh9xtTWg==",
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-7.0.0.tgz",
|
||||
"integrity": "sha512-8uI8rWyAbq8EzkjS+sHZSncyzujHzVbuLKgj8J5H0yUL6+r26F16gVA2iuQuIBvzbSMy7Y0/pUuWlwZr/H8AKg==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"@capacitor/keyboard": {
|
||||
"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==",
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-7.0.0.tgz",
|
||||
"integrity": "sha512-Tqwy8wG+sx4UqiFCX4Q+bFw6uKgG7BiHKAPpeefoIgoEB8H8Jf3xZNZoVPnJIMuPsCdSvuyHXZbJXH9IEEirGA==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"@capacitor/status-bar": {
|
||||
"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==",
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-7.0.0.tgz",
|
||||
"integrity": "sha512-wsvPkWkoSOXMIgVHu4c6P1sOuDSZ1ClUo5OpLRwj7u8DYzlV4jlmNzztQn2Lvsiqx1z4kfukSaqe40k1Lo4c9g==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
@@ -11017,9 +11021,9 @@
|
||||
}
|
||||
},
|
||||
"@clack/prompts": {
|
||||
"version": "0.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.9.1.tgz",
|
||||
"integrity": "sha512-JIpyaboYZeWYlyP0H+OoPPxd6nqueG/CmN6ixBiNFsIDHREevjIf0n0Ohh5gr5C8pEDknzgvz+pIJ8dMhzWIeg==",
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.10.0.tgz",
|
||||
"integrity": "sha512-H3rCl6CwW1NdQt9rE3n373t7o5cthPv7yUoxF2ytZvyvlJv89C5RYMJu83Hed8ODgys5vpBU0GKxIRG83jd8NQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@clack/core": "0.4.1",
|
||||
|
||||
@@ -37,11 +37,11 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@axe-core/playwright": "^4.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",
|
||||
"@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",
|
||||
"@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,10 +643,6 @@ 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>`.
|
||||
@@ -2307,10 +2303,6 @@ export namespace Components {
|
||||
* The name of the control, which is submitted with the form data.
|
||||
*/
|
||||
"name": string;
|
||||
/**
|
||||
* If `true`, the user must fill in a value before submitting a form.
|
||||
*/
|
||||
"required": boolean;
|
||||
"setFocus": () => Promise<void>;
|
||||
/**
|
||||
* the value of the radio group.
|
||||
@@ -2816,10 +2808,6 @@ 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.
|
||||
*/
|
||||
@@ -3292,10 +3280,6 @@ 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>`.
|
||||
*/
|
||||
@@ -5451,10 +5435,6 @@ 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>`.
|
||||
*/
|
||||
@@ -7087,10 +7067,6 @@ declare namespace LocalJSX {
|
||||
* Emitted when the `value` property has changed. This is used to ensure that `ion-radio` can respond to any value property changes from the group.
|
||||
*/
|
||||
"onIonValueChange"?: (event: IonRadioGroupCustomEvent<RadioGroupChangeEventDetail>) => void;
|
||||
/**
|
||||
* If `true`, the user must fill in a value before submitting a form.
|
||||
*/
|
||||
"required"?: boolean;
|
||||
/**
|
||||
* the value of the radio group.
|
||||
*/
|
||||
@@ -7664,10 +7640,6 @@ 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.
|
||||
*/
|
||||
@@ -8183,10 +8155,6 @@ 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,6 +237,18 @@ 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 (
|
||||
@@ -400,7 +412,19 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
|
||||
await this.delegateController.attachViewToDom();
|
||||
|
||||
await present(this, 'alertEnter', iosEnterAnimation, mdEnterAnimation);
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
unlock();
|
||||
}
|
||||
@@ -725,8 +749,8 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
const { overlayIndex, header, subHeader, message, htmlAttributes } = this;
|
||||
const mode = getIonMode(this);
|
||||
const hdrId = `alert-${overlayIndex}-hdr`;
|
||||
const subHdrId = `alert-${overlayIndex}-sub-hdr`;
|
||||
const msgId = `alert-${overlayIndex}-msg`;
|
||||
const subHdrId = `alert-${overlayIndex}-sub-hdr`;
|
||||
const role = this.inputs.length > 0 || this.buttons.length > 0 ? 'alertdialog' : 'alert';
|
||||
|
||||
/**
|
||||
@@ -739,12 +763,7 @@ 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}`,
|
||||
}}
|
||||
@@ -761,7 +780,16 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
|
||||
<div tabindex="0" aria-hidden="true"></div>
|
||||
|
||||
<div class="alert-wrapper ion-overlay-wrapper" ref={(el) => (this.wrapperEl = el)}>
|
||||
<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-head">
|
||||
{header && (
|
||||
<h2 id={hdrId} class="alert-title">
|
||||
|
||||
@@ -16,6 +16,7 @@ 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');
|
||||
@@ -42,8 +43,8 @@ const testAria = async (
|
||||
* expect().toHaveAttribute() can't check for a null value, so grab and check
|
||||
* the values manually instead.
|
||||
*/
|
||||
const ariaLabelledBy = await alert.getAttribute('aria-labelledby');
|
||||
const ariaDescribedBy = await alert.getAttribute('aria-describedby');
|
||||
const ariaLabelledBy = await alertwrapper.getAttribute('aria-labelledby');
|
||||
const ariaDescribedBy = await alertwrapper.getAttribute('aria-describedby');
|
||||
|
||||
expect(ariaLabelledBy).toBe(expectedAriaLabelledBy);
|
||||
expect(ariaDescribedBy).toBe(expectedAriaDescribedBy);
|
||||
|
||||
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 32 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: 20 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 35 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 alert = page.body.querySelector('ion-alert')!;
|
||||
expect(alert.id).toBe(id);
|
||||
const alertwrapper = page.body.querySelector('.alert-wrapper')!;
|
||||
expect(alertwrapper.id).toBe(id);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ 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();
|
||||
|
||||
@@ -30,7 +31,7 @@ configs({ directions: ['ltr'] }).forEach(({ config, screenshot, title }) => {
|
||||
const alertFixture = new AlertFixture(page, screenshot);
|
||||
|
||||
const alert = await alertFixture.open('#basic');
|
||||
await expect(alert).toHaveAttribute('data-testid', 'basic-alert');
|
||||
await expect(alert.locator('.alert-wrapper')).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: 19 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 10 KiB |
@@ -98,13 +98,6 @@ 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.
|
||||
*
|
||||
@@ -189,7 +182,6 @@ export class Checkbox implements ComponentInterface {
|
||||
name,
|
||||
value,
|
||||
alignment,
|
||||
required,
|
||||
} = this;
|
||||
const mode = getIonMode(this);
|
||||
const path = getSVGPath(mode, indeterminate);
|
||||
@@ -226,7 +218,6 @@ export class Checkbox implements ComponentInterface {
|
||||
onFocus={() => this.onFocus()}
|
||||
onBlur={() => this.onBlur()}
|
||||
ref={(focusEl) => (this.focusEl = focusEl)}
|
||||
required={required}
|
||||
{...inheritedAttributes}
|
||||
/>
|
||||
<div
|
||||
|
||||
@@ -54,33 +54,3 @@ 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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -34,11 +34,6 @@ export class RadioGroup implements ComponentInterface {
|
||||
*/
|
||||
@Prop() name: string = this.inputId;
|
||||
|
||||
/**
|
||||
* If `true`, the user must fill in a value before submitting a form.
|
||||
*/
|
||||
@Prop() required = false;
|
||||
|
||||
/**
|
||||
* the value of the radio group.
|
||||
*/
|
||||
@@ -230,12 +225,12 @@ export class RadioGroup implements ComponentInterface {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { label, labelId, el, name, value, required } = this;
|
||||
const { label, labelId, el, name, value } = this;
|
||||
const mode = getIonMode(this);
|
||||
|
||||
renderHiddenInput(true, el, name, value, false);
|
||||
|
||||
return <Host role="radiogroup" aria-labelledby={label ? labelId : null} onClick={this.onClick} class={mode} aria-required={`${required}`}></Host>;
|
||||
return <Host role="radiogroup" aria-labelledby={label ? labelId : null} onClick={this.onClick} class={mode}></Host>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 } from '@utils/helpers';
|
||||
import { inheritAriaAttributes, clamp, debounceEvent, renderHiddenInput, isSafeNumber } from '@utils/helpers';
|
||||
import { printIonWarning } from '@utils/logging';
|
||||
import { isRTL } from '@utils/rtl';
|
||||
import { createColorClasses, hostContext } from '@utils/theme';
|
||||
@@ -109,7 +109,11 @@ export class Range implements ComponentInterface {
|
||||
*/
|
||||
@Prop() min = 0;
|
||||
@Watch('min')
|
||||
protected minChanged() {
|
||||
protected minChanged(newValue: number) {
|
||||
if (!isSafeNumber(newValue)) {
|
||||
this.min = 0;
|
||||
}
|
||||
|
||||
if (!this.noUpdate) {
|
||||
this.updateRatio();
|
||||
}
|
||||
@@ -120,7 +124,11 @@ export class Range implements ComponentInterface {
|
||||
*/
|
||||
@Prop() max = 100;
|
||||
@Watch('max')
|
||||
protected maxChanged() {
|
||||
protected maxChanged(newValue: number) {
|
||||
if (!isSafeNumber(newValue)) {
|
||||
this.max = 100;
|
||||
}
|
||||
|
||||
if (!this.noUpdate) {
|
||||
this.updateRatio();
|
||||
}
|
||||
@@ -151,6 +159,12 @@ 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.
|
||||
@@ -300,6 +314,11 @@ 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,6 +28,25 @@ 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 } from '@utils/helpers';
|
||||
import { addEventListener, removeEventListener, inheritAttributes, getNextSiblingOfType } from '@utils/helpers';
|
||||
import { hostContext } from '@utils/theme';
|
||||
|
||||
import { getIonMode } from '../../global/ionic-global';
|
||||
@@ -65,7 +65,41 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
|
||||
this.updateState();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
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() {
|
||||
const segmentEl = (this.segmentEl = this.el.closest('ion-segment'));
|
||||
if (segmentEl) {
|
||||
this.updateState();
|
||||
@@ -76,12 +110,13 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
|
||||
// Return if there is no contentId defined
|
||||
if (!this.contentId) return;
|
||||
|
||||
// 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}".`);
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -123,6 +123,8 @@
|
||||
<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>
|
||||
@@ -158,6 +160,34 @@
|
||||
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,13 +196,6 @@ 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.
|
||||
*
|
||||
@@ -317,19 +310,10 @@ 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();
|
||||
});
|
||||
|
||||
await overlay.present();
|
||||
|
||||
// focus selected option for popovers and modals
|
||||
if (this.interface === 'popover' || this.interface === 'modal') {
|
||||
// Add logic to scroll selected item into view before presenting
|
||||
const scrollSelectedIntoView = () => {
|
||||
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})`
|
||||
@@ -352,6 +336,7 @@ 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();
|
||||
@@ -379,8 +364,40 @@ 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;
|
||||
}
|
||||
|
||||
@@ -981,7 +998,7 @@ export class Select implements ComponentInterface {
|
||||
}
|
||||
|
||||
private renderListbox() {
|
||||
const { disabled, inputId, isExpanded, required } = this;
|
||||
const { disabled, inputId, isExpanded } = this;
|
||||
|
||||
return (
|
||||
<button
|
||||
@@ -990,7 +1007,6 @@ 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,6 +61,169 @@
|
||||
</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 }) => {
|
||||
configs({ directions: ['ltr'] }).forEach(({ title, config, screenshot }) => {
|
||||
test.describe(title('select: basic'), () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/src/components/select/test/basic', config);
|
||||
@@ -24,6 +24,16 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
|
||||
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', () => {
|
||||
@@ -36,6 +46,16 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
|
||||
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', () => {
|
||||
@@ -57,6 +77,16 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
|
||||
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', () => {
|
||||
@@ -75,6 +105,16 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
|
||||
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`));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 33 KiB |
@@ -125,35 +125,3 @@ describe('select: slot interactivity', () => {
|
||||
expect(divSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ion-select: required', () => {
|
||||
it('should have a aria-required attribute as true in inner button', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Select],
|
||||
html: `
|
||||
<ion-select required="true"></ion-select>
|
||||
`,
|
||||
});
|
||||
|
||||
const select = page.body.querySelector('ion-select')!;
|
||||
|
||||
const nativeButton = select.shadowRoot!.querySelector('button')!;
|
||||
|
||||
expect(nativeButton.getAttribute('aria-required')).toBe('true');
|
||||
});
|
||||
|
||||
it('should not have a aria-required attribute as false in inner button', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Select],
|
||||
html: `
|
||||
<ion-select required="false"></ion-select>
|
||||
`,
|
||||
});
|
||||
|
||||
const select = page.body.querySelector('ion-select')!;
|
||||
|
||||
const nativeButton = select.shadowRoot!.querySelector('button')!;
|
||||
|
||||
expect(nativeButton.getAttribute('aria-required')).toBe('false');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -75,33 +75,3 @@ describe('ion-toggle: disabled', () => {
|
||||
expect(toggle.checked).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ion-toggle: required', () => {
|
||||
it('should have a required attribute in inner input when true', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Toggle],
|
||||
html: `
|
||||
<ion-toggle required="true">Toggle</ion-toggle>
|
||||
`,
|
||||
});
|
||||
|
||||
const toggle = page.body.querySelector('ion-toggle')!;
|
||||
const nativeInput = toggle.shadowRoot?.querySelector('input[role=switch]')!;
|
||||
|
||||
expect(nativeInput.hasAttribute('required')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not have a required attribute in inner input when false', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Toggle],
|
||||
html: `
|
||||
<ion-toggle required="false">Toggle</ion-toggle>
|
||||
`,
|
||||
});
|
||||
|
||||
const toggle = page.body.querySelector('ion-toggle')!;
|
||||
const nativeInput = toggle.shadowRoot?.querySelector('input[role=switch]')!;
|
||||
|
||||
expect(nativeInput.hasAttribute('required')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -108,13 +108,6 @@ export class Toggle 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 user switches the toggle on or off.
|
||||
*
|
||||
@@ -297,8 +290,7 @@ export class Toggle implements ComponentInterface {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { activated, color, checked, disabled, el, justify, labelPlacement, inputId, name, alignment, required } =
|
||||
this;
|
||||
const { activated, color, checked, disabled, el, justify, labelPlacement, inputId, name, alignment } = this;
|
||||
|
||||
const mode = getIonMode(this);
|
||||
const value = this.getValue();
|
||||
@@ -335,7 +327,6 @@ export class Toggle implements ComponentInterface {
|
||||
onFocus={() => this.onFocus()}
|
||||
onBlur={() => this.onBlur()}
|
||||
ref={(focusEl) => (this.focusEl = focusEl)}
|
||||
required={required}
|
||||
{...this.inheritedAttributes}
|
||||
/>
|
||||
<div
|
||||
|
||||
@@ -13,10 +13,6 @@ import type { Color, CssClassMap, StyleEventDetail } from '../../interface';
|
||||
* @slot secondary - Content is placed to the left of the toolbar text in `ios` mode, and directly to the right in `md` mode.
|
||||
* @slot primary - Content is placed to the right of the toolbar text in `ios` mode, and to the far right in `md` mode.
|
||||
* @slot end - Content is placed to the right of the toolbar text in LTR, and to the left in RTL.
|
||||
*
|
||||
* @part background - The background of the toolbar, covering the entire area behind the toolbar content.
|
||||
* @part container - The container that wraps all toolbar content, including the default slot and named slot content.
|
||||
* @part content - The container for the default slot, wrapping content provided without a named slot.
|
||||
*/
|
||||
@Component({
|
||||
tag: 'ion-toolbar',
|
||||
@@ -101,11 +97,11 @@ export class Toolbar implements ComponentInterface {
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<div class="toolbar-background" part="background"></div>
|
||||
<div class="toolbar-container" part="container">
|
||||
<div class="toolbar-background"></div>
|
||||
<div class="toolbar-container">
|
||||
<slot name="start"></slot>
|
||||
<slot name="secondary"></slot>
|
||||
<div class="toolbar-content" part="content">
|
||||
<div class="toolbar-content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<slot name="primary"></slot>
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { SpinnerTypes } from '../components/spinner/spinner-configs';
|
||||
import type { TabButtonLayout } from '../components/tab-bar/tab-bar-interface';
|
||||
import type { AnimationBuilder, Mode } from '../interface';
|
||||
|
||||
import type { LogLevel } from './logging';
|
||||
import type { PlatformConfig } from './platform';
|
||||
|
||||
export interface IonicConfig {
|
||||
@@ -221,15 +220,6 @@ export interface IonicConfig {
|
||||
*/
|
||||
experimentalCloseWatcher?: boolean;
|
||||
|
||||
/**
|
||||
* Configures the logging level for Ionic Framework:
|
||||
*
|
||||
* - `'OFF'`: No errors or warnings are logged.
|
||||
* - `'ERROR'`: Logs only errors.
|
||||
* - `'WARN'`: Logs errors and warnings.
|
||||
*/
|
||||
logLevel?: LogLevel;
|
||||
|
||||
// PRIVATE configs
|
||||
keyboardHeight?: number;
|
||||
inputShims?: boolean;
|
||||
|
||||
@@ -11,6 +11,12 @@ describe('floating point utils', () => {
|
||||
const n = getDecimalPlaces(5);
|
||||
expect(n).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle nullish values', () => {
|
||||
expect(getDecimalPlaces(undefined as any)).toBe(0);
|
||||
expect(getDecimalPlaces(null as any)).toBe(0);
|
||||
expect(getDecimalPlaces(NaN as any)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('roundToMaxDecimalPlaces', () => {
|
||||
@@ -18,5 +24,11 @@ describe('floating point utils', () => {
|
||||
const n = roundToMaxDecimalPlaces(5.12345, 1.12, 2.123);
|
||||
expect(n).toBe(5.123);
|
||||
});
|
||||
|
||||
it('should handle nullish values', () => {
|
||||
expect(roundToMaxDecimalPlaces(undefined as any)).toBe(0);
|
||||
expect(roundToMaxDecimalPlaces(null as any)).toBe(0);
|
||||
expect(roundToMaxDecimalPlaces(NaN as any)).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { isSafeNumber } from '@utils/helpers';
|
||||
|
||||
export function getDecimalPlaces(n: number) {
|
||||
if (!isSafeNumber(n)) return 0;
|
||||
if (n % 1 === 0) return 0;
|
||||
return n.toString().split('.')[1].length;
|
||||
}
|
||||
@@ -36,6 +39,7 @@ export function getDecimalPlaces(n: number) {
|
||||
* be used as a reference for the desired specificity.
|
||||
*/
|
||||
export function roundToMaxDecimalPlaces(n: number, ...references: number[]) {
|
||||
if (!isSafeNumber(n)) return 0;
|
||||
const maxPlaces = Math.max(...references.map((r) => getDecimalPlaces(r)));
|
||||
return Number(n.toFixed(maxPlaces));
|
||||
}
|
||||
|
||||