Compare commits

..

16 Commits

Author SHA1 Message Date
José Rio
dafb0b9005 Update tabs.tsx 2025-02-06 17:31:46 +00:00
José Rio
6b164c8f44 fix(tab): rollback a11y implementation 2025-02-06 17:28:12 +00:00
José Rio
5546e95288 fix(content): enable for a11y focus 2025-02-06 17:27:24 +00:00
José Rio
52d6e09b27 fix(tab): A11Y - tabindex with string values. 2025-02-05 16:13:05 +00:00
José Rio
a6e734cf1a Merge remote-tracking branch 'origin/feature-8.5' into ROU-11564 2025-02-05 15:58:01 +00:00
José Rio
ed77d81eaa chore(): update snapshots 2025-02-05 14:59:44 +00:00
José Rio
e4957fa4d2 fix(tab): A11Y - manage tabindex and set focus when active. 2025-02-05 14:59:08 +00:00
José Rio
d9f8c1352f fix(tabbutton): A11Y - enable tabindex. 2025-02-05 14:58:29 +00:00
Giuliana Silva
41da4c3565 feat(checkbox): add required prop (#30157)
Issue number: resolves internal

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
- Currently, the screen reader do not announce the component as required
when `required={true}`.

## What is the new behavior?
- Added a new `required` prop to be used for accessibility purposes that
adds the `required` attribute to checkbox's inner native input.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer
for more information.
-->
2025-02-04 11:54:23 +00:00
Giuliana Silva
0b549835b6 feat(select): add required prop (#30155)
Issue number: resolves internal

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
- Currently, the screen reader do not announce the component as required
when `required={true}`.

## What is the new behavior?
- Added a new `required` prop to be used for accessibility purposes that
adds the `aria-required` attribute to select's inner native button.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer
for more information.
-->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->
2025-02-04 10:18:40 +00:00
Giuliana Silva
0bbb9f37b4 feat(toggle): add required prop (#30156)
Issue number: resolves internal

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
- Currently, the screen reader do not announce the component as required
when `required={true}`.

## What is the new behavior?
- Added a new `required` prop to be used for accessibility purposes that
adds the `required` attribute to toggle's inner native input.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer
for more information.
-->
2025-02-04 09:59:01 +00:00
Israel de la Barrera
166e43554e feat(modal): add expandToScroll property to allow scrolling at all breakpoints (#30097)
Issue number: resolves #24631

Co-authored-by: Maria Hutt <13530427+thetaPC@users.noreply.github.com>
Co-authored-by: Brandy Smith <brandyscarney@users.noreply.github.com>
2025-02-03 10:45:15 -08:00
Brandy Smith
621333d927 chore(git): update feature-8.5 from main (#30168) 2025-01-29 14:34:11 -05:00
Brandy Smith
6cf454f7c4 Merge branch 'main' into chore-main-to-8.5 2025-01-29 14:20:00 -05:00
fudom
ac4ea3232b feat(toolbar): add shadow parts for background, container, and content (#30069)
Resolves #30068

---------

Add `part` attributes to toolbar for the `background`, `container` and `content`.
2025-01-23 15:48:58 -05:00
Sean Perkins
efd3e0fd2b feat(config): add logLevel option to suppress ionic warnings and errors (#30015)
resolves #29814

---------

- Developers can assign a value to `logLevel` in the Ionic config to control the log level that Ionic Framework will produce logs for.
  - `OFF` will completely disable all warnings and errors from Ionic
  - `WARN` will log warnings and errors
  - `ERROR` will log only errors
- Default behavior is that developers receive both Ionic warnings and errors
- Configuration only applies to usages of `printIonWarning` and `printIonError`

---------

Co-authored-by: Brandy Smith <6577830+brandyscarney@users.noreply.github.com>
2025-01-20 16:15:02 -05:00
134 changed files with 765 additions and 542 deletions

View File

@@ -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

View File

@@ -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
View File

@@ -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",

View File

@@ -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",

View File

@@ -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>`.
*/

View File

@@ -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">

View File

@@ -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);

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -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);
});
});

View File

@@ -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 }) => {

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

@@ -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

View File

@@ -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();
});
});

View File

@@ -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`,

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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) => {

View File

@@ -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);

View File

@@ -31,6 +31,7 @@ export interface ModalAnimationOptions {
presentingEl?: HTMLElement;
currentBreakpoint?: number;
backdropBreakpoint?: number;
expandToScroll: boolean;
}
export interface ModalBreakpointChangeEventDetail {

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -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() {

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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)}

View File

@@ -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>

View File

@@ -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`));
});
});
});
});

Some files were not shown because too many files have changed in this diff Show More