Skip to content

Commit 6c72476

Browse files
committed
Merge branch 'feature-8.5' into ROU-11551
2 parents 62d23fb + 41da4c3 commit 6c72476

31 files changed

+521
-40
lines changed

core/api.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,7 @@ ion-checkbox,prop,justify,"end" | "space-between" | "start" | undefined,undefine
403403
ion-checkbox,prop,labelPlacement,"end" | "fixed" | "stacked" | "start",'start',false,false
404404
ion-checkbox,prop,mode,"ios" | "md",undefined,false,false
405405
ion-checkbox,prop,name,string,this.inputId,false,false
406+
ion-checkbox,prop,required,boolean,false,false,false
406407
ion-checkbox,prop,value,any,'on',false,false
407408
ion-checkbox,event,ionBlur,void,true
408409
ion-checkbox,event,ionChange,CheckboxChangeEventDetail<any>,true
@@ -1074,6 +1075,7 @@ ion-modal,prop,backdropDismiss,boolean,true,false,false
10741075
ion-modal,prop,breakpoints,number[] | undefined,undefined,false,false
10751076
ion-modal,prop,canDismiss,((data?: any, role?: string | undefined) => Promise<boolean>) | boolean,true,false,false
10761077
ion-modal,prop,enterAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false
1078+
ion-modal,prop,expandToScroll,boolean,true,false,false
10771079
ion-modal,prop,focusTrap,boolean,true,false,false
10781080
ion-modal,prop,handle,boolean | undefined,undefined,false,false
10791081
ion-modal,prop,handleBehavior,"cycle" | "none" | undefined,'none',false,false
@@ -1633,6 +1635,7 @@ ion-select,prop,multiple,boolean,false,false,false
16331635
ion-select,prop,name,string,this.inputId,false,false
16341636
ion-select,prop,okText,string,'OK',false,false
16351637
ion-select,prop,placeholder,string | undefined,undefined,false,false
1638+
ion-select,prop,required,boolean,false,false,false
16361639
ion-select,prop,selectedText,null | string | undefined,undefined,false,false
16371640
ion-select,prop,shape,"round" | undefined,undefined,false,false
16381641
ion-select,prop,toggleIcon,string | undefined,undefined,false,false
@@ -1949,6 +1952,7 @@ ion-toggle,prop,justify,"end" | "space-between" | "start" | undefined,undefined,
19491952
ion-toggle,prop,labelPlacement,"end" | "fixed" | "stacked" | "start",'start',false,false
19501953
ion-toggle,prop,mode,"ios" | "md",undefined,false,false
19511954
ion-toggle,prop,name,string,this.inputId,false,false
1955+
ion-toggle,prop,required,boolean,false,false,false
19521956
ion-toggle,prop,value,null | string | undefined,'on',false,false
19531957
ion-toggle,event,ionBlur,void,true
19541958
ion-toggle,event,ionChange,ToggleChangeEventDetail<any>,true

core/src/components.d.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,10 @@ export namespace Components {
643643
* The name of the control, which is submitted with the form data.
644644
*/
645645
"name": string;
646+
/**
647+
* 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.
648+
*/
649+
"required": boolean;
646650
"setFocus": () => Promise<void>;
647651
/**
648652
* 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 {
17311735
* Animation to use when the modal is presented.
17321736
*/
17331737
"enterAnimation"?: AnimationBuilder;
1738+
/**
1739+
* 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.
1740+
*/
1741+
"expandToScroll": boolean;
17341742
/**
17351743
* 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.
17361744
*/
@@ -2816,6 +2824,10 @@ export namespace Components {
28162824
* The text to display when the select is empty.
28172825
*/
28182826
"placeholder"?: string;
2827+
/**
2828+
* 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.
2829+
*/
2830+
"required": boolean;
28192831
/**
28202832
* The text to display instead of the selected option's value.
28212833
*/
@@ -3288,6 +3300,10 @@ export namespace Components {
32883300
* The name of the control, which is submitted with the form data.
32893301
*/
32903302
"name": string;
3303+
/**
3304+
* 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.
3305+
*/
3306+
"required": boolean;
32913307
/**
32923308
* 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>`.
32933309
*/
@@ -5443,6 +5459,10 @@ declare namespace LocalJSX {
54435459
* Emitted when the checkbox has focus.
54445460
*/
54455461
"onIonFocus"?: (event: IonCheckboxCustomEvent<void>) => void;
5462+
/**
5463+
* 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.
5464+
*/
5465+
"required"?: boolean;
54465466
/**
54475467
* 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>`.
54485468
*/
@@ -6540,6 +6560,10 @@ declare namespace LocalJSX {
65406560
* Animation to use when the modal is presented.
65416561
*/
65426562
"enterAnimation"?: AnimationBuilder;
6563+
/**
6564+
* 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.
6565+
*/
6566+
"expandToScroll"?: boolean;
65436567
/**
65446568
* 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.
65456569
*/
@@ -7656,6 +7680,10 @@ declare namespace LocalJSX {
76567680
* The text to display when the select is empty.
76577681
*/
76587682
"placeholder"?: string;
7683+
/**
7684+
* 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.
7685+
*/
7686+
"required"?: boolean;
76597687
/**
76607688
* The text to display instead of the selected option's value.
76617689
*/
@@ -8171,6 +8199,10 @@ declare namespace LocalJSX {
81718199
* Emitted when the toggle has focus.
81728200
*/
81738201
"onIonFocus"?: (event: IonToggleCustomEvent<void>) => void;
8202+
/**
8203+
* 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.
8204+
*/
8205+
"required"?: boolean;
81748206
/**
81758207
* 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>`.
81768208
*/

core/src/components/checkbox/checkbox.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,13 @@ export class Checkbox implements ComponentInterface {
9898
*/
9999
@Prop() alignment?: 'start' | 'center';
100100

101+
/**
102+
* If true, screen readers will announce it as a required field. This property
103+
* works only for accessibility purposes, it will not prevent the form from
104+
* submitting if the value is invalid.
105+
*/
106+
@Prop() required = false;
107+
101108
/**
102109
* Emitted when the checked property has changed as a result of a user action such as a click.
103110
*
@@ -182,6 +189,7 @@ export class Checkbox implements ComponentInterface {
182189
name,
183190
value,
184191
alignment,
192+
required,
185193
} = this;
186194
const mode = getIonMode(this);
187195
const path = getSVGPath(mode, indeterminate);
@@ -218,6 +226,7 @@ export class Checkbox implements ComponentInterface {
218226
onFocus={() => this.onFocus()}
219227
onBlur={() => this.onBlur()}
220228
ref={(focusEl) => (this.focusEl = focusEl)}
229+
required={required}
221230
{...inheritedAttributes}
222231
/>
223232
<div

core/src/components/checkbox/test/checkbox.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,33 @@ describe('ion-checkbox: indeterminate', () => {
5454
expect(checkbox.getAttribute('aria-checked')).toBe('mixed');
5555
});
5656
});
57+
58+
describe('ion-checkbox: required', () => {
59+
it('should have a required attribute in inner input when true', async () => {
60+
const page = await newSpecPage({
61+
components: [Checkbox],
62+
html: `
63+
<ion-checkbox required="true">Checkbox</ion-checkbox>
64+
`,
65+
});
66+
67+
const checkbox = page.body.querySelector('ion-checkbox')!;
68+
const nativeInput = checkbox.shadowRoot?.querySelector('input[type=checkbox]')!;
69+
70+
expect(nativeInput.hasAttribute('required')).toBeTruthy();
71+
});
72+
73+
it('should not have a required attribute in inner input when false', async () => {
74+
const page = await newSpecPage({
75+
components: [Checkbox],
76+
html: `
77+
<ion-checkbox required="false">Checkbox</ion-checkbox>
78+
`,
79+
});
80+
81+
const checkbox = page.body.querySelector('ion-checkbox')!;
82+
const nativeInput = checkbox.shadowRoot?.querySelector('input[type=checkbox]')!;
83+
84+
expect(nativeInput.hasAttribute('required')).toBeFalsy();
85+
});
86+
});

core/src/components/modal/animations/ios.enter.ts

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,27 +17,78 @@ const createEnterAnimation = () => {
1717

1818
const wrapperAnimation = createAnimation().fromTo('transform', 'translateY(100vh)', 'translateY(0vh)');
1919

20-
return { backdropAnimation, wrapperAnimation };
20+
return { backdropAnimation, wrapperAnimation, contentAnimation: undefined };
2121
};
2222

2323
/**
2424
* iOS Modal Enter Animation for the Card presentation style
2525
*/
2626
export const iosEnterAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptions): Animation => {
27-
const { presentingEl, currentBreakpoint } = opts;
27+
const { presentingEl, currentBreakpoint, expandToScroll } = opts;
2828
const root = getElementRoot(baseEl);
29-
const { wrapperAnimation, backdropAnimation } =
29+
const { wrapperAnimation, backdropAnimation, contentAnimation } =
3030
currentBreakpoint !== undefined ? createSheetEnterAnimation(opts) : createEnterAnimation();
3131

3232
backdropAnimation.addElement(root.querySelector('ion-backdrop')!);
3333

3434
wrapperAnimation.addElement(root.querySelectorAll('.modal-wrapper, .modal-shadow')!).beforeStyles({ opacity: 1 });
3535

36+
// The content animation is only added if scrolling is enabled for
37+
// all the breakpoints.
38+
!expandToScroll && contentAnimation?.addElement(baseEl.querySelector('.ion-page')!);
39+
3640
const baseAnimation = createAnimation('entering-base')
3741
.addElement(baseEl)
3842
.easing('cubic-bezier(0.32,0.72,0,1)')
3943
.duration(500)
40-
.addAnimation(wrapperAnimation);
44+
.addAnimation([wrapperAnimation])
45+
.beforeAddWrite(() => {
46+
if (expandToScroll) {
47+
// Scroll can only be done when the modal is fully expanded.
48+
return;
49+
}
50+
51+
/**
52+
* There are some browsers that causes flickering when
53+
* dragging the content when scroll is enabled at every
54+
* breakpoint. This is due to the wrapper element being
55+
* transformed off the screen and having a snap animation.
56+
*
57+
* A workaround is to clone the footer element and append
58+
* it outside of the wrapper element. This way, the footer
59+
* is still visible and the drag can be done without
60+
* flickering. The original footer is hidden until the modal
61+
* is dismissed. This maintains the animation of the footer
62+
* when the modal is dismissed.
63+
*
64+
* The workaround needs to be done before the animation starts
65+
* so there are no flickering issues.
66+
*/
67+
const ionFooter = baseEl.querySelector('ion-footer');
68+
/**
69+
* This check is needed to prevent more than one footer
70+
* from being appended to the shadow root.
71+
* Otherwise, iOS and MD enter animations would append
72+
* the footer twice.
73+
*/
74+
const ionFooterAlreadyAppended = baseEl.shadowRoot!.querySelector('ion-footer');
75+
if (ionFooter && !ionFooterAlreadyAppended) {
76+
const footerHeight = ionFooter.clientHeight;
77+
const clonedFooter = ionFooter.cloneNode(true) as HTMLIonFooterElement;
78+
79+
baseEl.shadowRoot!.appendChild(clonedFooter);
80+
ionFooter.style.setProperty('display', 'none');
81+
ionFooter.setAttribute('aria-hidden', 'true');
82+
83+
// Padding is added to prevent some content from being hidden.
84+
const page = baseEl.querySelector('.ion-page') as HTMLElement;
85+
page.style.setProperty('padding-bottom', `${footerHeight}px`);
86+
}
87+
});
88+
89+
if (contentAnimation) {
90+
baseAnimation.addAnimation(contentAnimation);
91+
}
4192

4293
if (presentingEl) {
4394
const isMobile = window.innerWidth < 768;

core/src/components/modal/animations/ios.leave.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const createLeaveAnimation = () => {
1919
* iOS Modal Leave Animation
2020
*/
2121
export const iosLeaveAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptions, duration = 500): Animation => {
22-
const { presentingEl, currentBreakpoint } = opts;
22+
const { presentingEl, currentBreakpoint, expandToScroll } = opts;
2323
const root = getElementRoot(baseEl);
2424
const { wrapperAnimation, backdropAnimation } =
2525
currentBreakpoint !== undefined ? createSheetLeaveAnimation(opts) : createLeaveAnimation();
@@ -32,7 +32,33 @@ export const iosLeaveAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptio
3232
.addElement(baseEl)
3333
.easing('cubic-bezier(0.32,0.72,0,1)')
3434
.duration(duration)
35-
.addAnimation(wrapperAnimation);
35+
.addAnimation(wrapperAnimation)
36+
.beforeAddWrite(() => {
37+
if (expandToScroll) {
38+
// Scroll can only be done when the modal is fully expanded.
39+
return;
40+
}
41+
42+
/**
43+
* If expandToScroll is disabled, we need to swap
44+
* the visibility to the original, so the footer
45+
* dismisses with the modal and doesn't stay
46+
* until the modal is removed from the DOM.
47+
*/
48+
const ionFooter = baseEl.querySelector('ion-footer');
49+
if (ionFooter) {
50+
const clonedFooter = baseEl.shadowRoot!.querySelector('ion-footer')!;
51+
52+
ionFooter.style.removeProperty('display');
53+
ionFooter.removeAttribute('aria-hidden');
54+
55+
clonedFooter.style.setProperty('display', 'none');
56+
clonedFooter.setAttribute('aria-hidden', 'true');
57+
58+
const page = baseEl.querySelector('.ion-page') as HTMLElement;
59+
page.style.removeProperty('padding-bottom');
60+
}
61+
});
3662

3763
if (presentingEl) {
3864
const isMobile = window.innerWidth < 768;

core/src/components/modal/animations/md.enter.ts

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,25 +19,78 @@ const createEnterAnimation = () => {
1919
{ offset: 1, opacity: 1, transform: `translateY(0px)` },
2020
]);
2121

22-
return { backdropAnimation, wrapperAnimation };
22+
return { backdropAnimation, wrapperAnimation, contentAnimation: undefined };
2323
};
2424

2525
/**
2626
* Md Modal Enter Animation
2727
*/
2828
export const mdEnterAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptions): Animation => {
29-
const { currentBreakpoint } = opts;
29+
const { currentBreakpoint, expandToScroll } = opts;
3030
const root = getElementRoot(baseEl);
31-
const { wrapperAnimation, backdropAnimation } =
31+
const { wrapperAnimation, backdropAnimation, contentAnimation } =
3232
currentBreakpoint !== undefined ? createSheetEnterAnimation(opts) : createEnterAnimation();
3333

3434
backdropAnimation.addElement(root.querySelector('ion-backdrop')!);
3535

3636
wrapperAnimation.addElement(root.querySelector('.modal-wrapper')!);
3737

38-
return createAnimation()
38+
// The content animation is only added if scrolling is enabled for
39+
// all the breakpoints.
40+
expandToScroll && contentAnimation?.addElement(baseEl.querySelector('.ion-page')!);
41+
42+
const baseAnimation = createAnimation()
3943
.addElement(baseEl)
4044
.easing('cubic-bezier(0.36,0.66,0.04,1)')
4145
.duration(280)
42-
.addAnimation([backdropAnimation, wrapperAnimation]);
46+
.addAnimation([backdropAnimation, wrapperAnimation])
47+
.beforeAddWrite(() => {
48+
if (expandToScroll) {
49+
// Scroll can only be done when the modal is fully expanded.
50+
return;
51+
}
52+
53+
/**
54+
* There are some browsers that causes flickering when
55+
* dragging the content when scroll is enabled at every
56+
* breakpoint. This is due to the wrapper element being
57+
* transformed off the screen and having a snap animation.
58+
*
59+
* A workaround is to clone the footer element and append
60+
* it outside of the wrapper element. This way, the footer
61+
* is still visible and the drag can be done without
62+
* flickering. The original footer is hidden until the modal
63+
* is dismissed. This maintains the animation of the footer
64+
* when the modal is dismissed.
65+
*
66+
* The workaround needs to be done before the animation starts
67+
* so there are no flickering issues.
68+
*/
69+
const ionFooter = baseEl.querySelector('ion-footer');
70+
/**
71+
* This check is needed to prevent more than one footer
72+
* from being appended to the shadow root.
73+
* Otherwise, iOS and MD enter animations would append
74+
* the footer twice.
75+
*/
76+
const ionFooterAlreadyAppended = baseEl.shadowRoot!.querySelector('ion-footer');
77+
if (ionFooter && !ionFooterAlreadyAppended) {
78+
const footerHeight = ionFooter.clientHeight;
79+
const clonedFooter = ionFooter.cloneNode(true) as HTMLIonFooterElement;
80+
81+
baseEl.shadowRoot!.appendChild(clonedFooter);
82+
ionFooter.style.setProperty('display', 'none');
83+
ionFooter.setAttribute('aria-hidden', 'true');
84+
85+
// Padding is added to prevent some content from being hidden.
86+
const page = baseEl.querySelector('.ion-page') as HTMLElement;
87+
page.style.setProperty('padding-bottom', `${footerHeight}px`);
88+
}
89+
});
90+
91+
if (contentAnimation) {
92+
baseAnimation.addAnimation(contentAnimation);
93+
}
94+
95+
return baseAnimation;
4396
};

0 commit comments

Comments
 (0)