Skip to content

Commit d7638a6

Browse files
committed
Merge remote-tracking branch 'origin/feature-8.1' into FW-1891
2 parents 33bd32d + 2220d83 commit d7638a6

File tree

12 files changed

+209
-14
lines changed

12 files changed

+209
-14
lines changed

core/api.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,7 @@ ion-col,css-prop,--ion-grid-columns
361361

362362
ion-content,shadow
363363
ion-content,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,undefined,false,true
364+
ion-content,prop,fixedSlotPlacement,"after" | "before",'after',false,false
364365
ion-content,prop,forceOverscroll,boolean | undefined,undefined,false,false
365366
ion-content,prop,fullscreen,boolean,false,false,false
366367
ion-content,prop,scrollEvents,boolean,false,false,false
@@ -553,6 +554,7 @@ ion-input,prop,autocomplete,"name" | "email" | "tel" | "url" | "on" | "off" | "h
553554
ion-input,prop,autocorrect,"off" | "on",'off',false,false
554555
ion-input,prop,autofocus,boolean,false,false,false
555556
ion-input,prop,clearInput,boolean,false,false,false
557+
ion-input,prop,clearInputIcon,string | undefined,undefined,false,false
556558
ion-input,prop,clearOnEdit,boolean | undefined,undefined,false,false
557559
ion-input,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,undefined,false,true
558560
ion-input,prop,counter,boolean,false,false,false

core/src/components.d.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -762,6 +762,10 @@ export namespace Components {
762762
* The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics).
763763
*/
764764
"color"?: Color;
765+
/**
766+
* Controls where the fixed content is placed relative to the main content in the DOM. This can be used to control the order in which fixed elements receive keyboard focus. For example, if a FAB in the fixed slot should receive keyboard focus before the main page content, set this property to `'before'`.
767+
*/
768+
"fixedSlotPlacement": 'after' | 'before';
765769
/**
766770
* If `true` and the content does not cause an overflow scroll, the scroll interaction will cause a bounce. If the content exceeds the bounds of ionContent, nothing will change. Note, this does not disable the system bounce on iOS. That is an OS level setting.
767771
*/
@@ -1162,6 +1166,10 @@ export namespace Components {
11621166
* If `true`, a clear icon will appear in the input when there is a value. Clicking it clears the input.
11631167
*/
11641168
"clearInput": boolean;
1169+
/**
1170+
* The icon to use for the clear button. Only applies when `clearInput` is set to `true`.
1171+
*/
1172+
"clearInputIcon"?: string;
11651173
/**
11661174
* If `true`, the value will be cleared after focus upon edit. Defaults to `true` when `type` is `"password"`, `false` for all other types.
11671175
*/
@@ -5478,6 +5486,10 @@ declare namespace LocalJSX {
54785486
* The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics).
54795487
*/
54805488
"color"?: Color;
5489+
/**
5490+
* Controls where the fixed content is placed relative to the main content in the DOM. This can be used to control the order in which fixed elements receive keyboard focus. For example, if a FAB in the fixed slot should receive keyboard focus before the main page content, set this property to `'before'`.
5491+
*/
5492+
"fixedSlotPlacement"?: 'after' | 'before';
54815493
/**
54825494
* If `true` and the content does not cause an overflow scroll, the scroll interaction will cause a bounce. If the content exceeds the bounds of ionContent, nothing will change. Note, this does not disable the system bounce on iOS. That is an OS level setting.
54835495
*/
@@ -5886,6 +5898,10 @@ declare namespace LocalJSX {
58865898
* If `true`, a clear icon will appear in the input when there is a value. Clicking it clears the input.
58875899
*/
58885900
"clearInput"?: boolean;
5901+
/**
5902+
* The icon to use for the clear button. Only applies when `clearInput` is set to `true`.
5903+
*/
5904+
"clearInputIcon"?: string;
58895905
/**
58905906
* If `true`, the value will be cleared after focus upon edit. Defaults to `true` when `type` is `"password"`, `false` for all other types.
58915907
*/

core/src/components/content/content.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,15 @@ export class Content implements ComponentInterface {
7575
*/
7676
@Prop() fullscreen = false;
7777

78+
/**
79+
* Controls where the fixed content is placed relative to the main content
80+
* in the DOM. This can be used to control the order in which fixed elements
81+
* receive keyboard focus.
82+
* For example, if a FAB in the fixed slot should receive keyboard focus before
83+
* the main page content, set this property to `'before'`.
84+
*/
85+
@Prop() fixedSlotPlacement: 'after' | 'before' = 'after';
86+
7887
/**
7988
* If `true` and the content does not cause an overflow scroll, the scroll interaction will cause a bounce.
8089
* If the content exceeds the bounds of ionContent, nothing will change.
@@ -423,7 +432,7 @@ export class Content implements ComponentInterface {
423432
}
424433

425434
render() {
426-
const { isMainContent, scrollX, scrollY, el } = this;
435+
const { fixedSlotPlacement, isMainContent, scrollX, scrollY, el } = this;
427436
const rtl = isRTL(el) ? 'rtl' : 'ltr';
428437
const mode = getIonMode(this);
429438
const forceOverscroll = this.shouldForceOverscroll();
@@ -446,6 +455,9 @@ export class Content implements ComponentInterface {
446455
}}
447456
>
448457
<div ref={(el) => (this.backgroundContentEl = el)} id="background-content" part="background"></div>
458+
459+
{fixedSlotPlacement === 'before' ? <slot name="fixed"></slot> : null}
460+
449461
<div
450462
class={{
451463
'inner-scroll': true,
@@ -467,7 +479,7 @@ export class Content implements ComponentInterface {
467479
</div>
468480
) : null}
469481

470-
<slot name="fixed"></slot>
482+
{fixedSlotPlacement === 'after' ? <slot name="fixed"></slot> : null}
471483
</Host>
472484
);
473485
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { newSpecPage } from '@stencil/core/testing';
2+
3+
import { Content } from '../content';
4+
5+
describe('content: fixed slot placement', () => {
6+
it('should should fixed slot after content', async () => {
7+
const page = await newSpecPage({
8+
components: [Content],
9+
html: '<ion-content></ion-content>',
10+
});
11+
12+
const content = page.body.querySelector('ion-content')!;
13+
const fixedSlot = content.shadowRoot!.querySelector('slot[name="fixed"]')!;
14+
const scrollEl = content.shadowRoot!.querySelector('[part="scroll"]')!;
15+
16+
expect(fixedSlot.nextElementSibling).not.toBe(scrollEl);
17+
});
18+
19+
it('should should fixed slot before content', async () => {
20+
const page = await newSpecPage({
21+
components: [Content],
22+
html: `<ion-content fixed-slot-placement="before"></ion-content>`,
23+
});
24+
25+
const content = page.body.querySelector('ion-content')!;
26+
const fixedSlot = content.shadowRoot!.querySelector('slot[name="fixed"]')!;
27+
const scrollEl = content.shadowRoot!.querySelector('[part="scroll"]')!;
28+
29+
expect(fixedSlot.nextElementSibling).toBe(scrollEl);
30+
});
31+
});

core/src/components/datetime/datetime.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -541,7 +541,7 @@ export class Datetime implements ComponentInterface {
541541
}
542542

543543
if (closeOverlay) {
544-
this.closeParentOverlay();
544+
this.closeParentOverlay(CONFIRM_ROLE);
545545
}
546546
}
547547

@@ -566,7 +566,7 @@ export class Datetime implements ComponentInterface {
566566
this.ionCancel.emit();
567567

568568
if (closeOverlay) {
569-
this.closeParentOverlay();
569+
this.closeParentOverlay(CANCEL_ROLE);
570570
}
571571
}
572572

@@ -616,13 +616,13 @@ export class Datetime implements ComponentInterface {
616616
return Array.isArray(activeParts) ? activeParts[0] : activeParts;
617617
};
618618

619-
private closeParentOverlay = () => {
619+
private closeParentOverlay = (role: string) => {
620620
const popoverOrModal = this.el.closest('ion-modal, ion-popover') as
621621
| HTMLIonModalElement
622622
| HTMLIonPopoverElement
623623
| null;
624624
if (popoverOrModal) {
625-
popoverOrModal.dismiss();
625+
popoverOrModal.dismiss(undefined, role);
626626
}
627627
};
628628

@@ -2652,5 +2652,7 @@ export class Datetime implements ComponentInterface {
26522652
}
26532653

26542654
let datetimeIds = 0;
2655+
const CANCEL_ROLE = 'datetime-cancel';
2656+
const CONFIRM_ROLE = 'datetime-confirm';
26552657
const WHEEL_ITEM_PART = 'wheel-item';
26562658
const WHEEL_ITEM_ACTIVE_PART = `active`;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { expect } from '@playwright/test';
2+
import { configs, test } from '@utils/test/playwright';
3+
4+
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ config, title }) => {
5+
test.describe(title('datetime: overlay roles'), () => {
6+
test.beforeEach(async ({ page }) => {
7+
await page.setContent(
8+
`
9+
<ion-modal>
10+
<ion-datetime></ion-datetime>
11+
</ion-modal>
12+
`,
13+
config
14+
);
15+
});
16+
test('should pass role to overlay when calling confirm method', async ({ page }) => {
17+
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
18+
const modal = page.locator('ion-modal');
19+
const datetime = page.locator('ion-datetime');
20+
21+
await modal.evaluate((el: HTMLIonModalElement) => el.present());
22+
23+
await datetime.evaluate((el: HTMLIonDatetimeElement) => el.confirm(true));
24+
25+
await ionModalDidDismiss.next();
26+
expect(ionModalDidDismiss).toHaveReceivedEventDetail({ data: undefined, role: 'datetime-confirm' });
27+
});
28+
test('should pass role to overlay when calling cancel method', async ({ page }) => {
29+
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
30+
const modal = page.locator('ion-modal');
31+
const datetime = page.locator('ion-datetime');
32+
33+
await modal.evaluate((el: HTMLIonModalElement) => el.present());
34+
35+
await datetime.evaluate((el: HTMLIonDatetimeElement) => el.cancel(true));
36+
37+
await ionModalDidDismiss.next();
38+
expect(ionModalDidDismiss).toHaveReceivedEventDetail({ data: undefined, role: 'datetime-cancel' });
39+
});
40+
});
41+
});
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<title>Datetime - Overlay Roles</title>
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
7+
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
8+
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
9+
<script src="../../../../../scripts/testing/scripts.js"></script>
10+
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
11+
<style>
12+
ion-modal.ios,
13+
ion-popover.datetime-popover.ios {
14+
--width: 350px;
15+
--height: 420px;
16+
}
17+
18+
ion-modal.md,
19+
ion-popover.datetime-popover.md {
20+
--width: 350px;
21+
--height: 450px;
22+
}
23+
24+
ion-datetime {
25+
width: 350px;
26+
}
27+
</style>
28+
</head>
29+
<body>
30+
<ion-app>
31+
<ion-header translucent="true">
32+
<ion-toolbar>
33+
<ion-title>Datetime - Overlay Roles</ion-title>
34+
</ion-toolbar>
35+
</ion-header>
36+
<ion-content class="ion-padding">
37+
<ion-button onclick="presentModal()">Present Modal</ion-button>
38+
</ion-content>
39+
</ion-app>
40+
<script>
41+
const presentModal = async () => {
42+
const modal = await createModal();
43+
44+
await modal.present();
45+
console.log(await modal.onDidDismiss());
46+
};
47+
48+
const createModal = () => {
49+
// create component to open
50+
const element = document.createElement('div');
51+
element.innerHTML = `
52+
<ion-datetime show-default-buttons="true"></ion-datetime>
53+
`;
54+
55+
// present the modal
56+
const modalElement = Object.assign(document.createElement('ion-modal'), {
57+
component: element,
58+
});
59+
60+
const app = document.querySelector('ion-app');
61+
app.appendChild(modalElement);
62+
return modalElement;
63+
};
64+
</script>
65+
</body>
66+
</html>

core/src/components/input/input.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,11 @@ export class Input implements ComponentInterface {
9292
*/
9393
@Prop() clearInput = false;
9494

95+
/**
96+
* The icon to use for the clear button. Only applies when `clearInput` is set to `true`.
97+
*/
98+
@Prop() clearInputIcon?: string;
99+
95100
/**
96101
* If `true`, the value will be cleared after focus upon edit. Defaults to `true` when `type` is `"password"`, `false` for all other types.
97102
*/
@@ -681,11 +686,13 @@ export class Input implements ComponentInterface {
681686
}
682687

683688
render() {
684-
const { disabled, fill, readonly, shape, inputId, labelPlacement, el, hasFocus } = this;
689+
const { disabled, fill, readonly, shape, inputId, labelPlacement, el, hasFocus, clearInputIcon } = this;
685690
const mode = getIonMode(this);
686691
const value = this.getValue();
687692
const inItem = hostContext('ion-item', this.el);
688693
const shouldRenderHighlight = mode === 'md' && fill !== 'outline' && !inItem;
694+
const defaultClearIcon = mode === 'ios' ? closeCircle : closeSharp;
695+
const clearIconData = clearInputIcon ?? defaultClearIcon;
689696

690697
const hasValue = this.hasValue();
691698
const hasStartEndSlots = el.querySelector('[slot="start"], [slot="end"]') !== null;
@@ -784,7 +791,7 @@ export class Input implements ComponentInterface {
784791
}}
785792
onClick={this.clearTextInput}
786793
>
787-
<ion-icon aria-hidden="true" icon={mode === 'ios' ? closeCircle : closeSharp}></ion-icon>
794+
<ion-icon aria-hidden="true" icon={clearIconData}></ion-icon>
788795
</button>
789796
)}
790797
<slot name="end"></slot>

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,19 @@ describe('input: label rendering', () => {
9999
expect(labelText.textContent).toBe('Label Prop Text');
100100
});
101101
});
102+
103+
// https://github.com/ionic-team/ionic-framework/issues/26974
104+
describe('input: clear icon', () => {
105+
it('should render custom icon', async () => {
106+
const page = await newSpecPage({
107+
components: [Input],
108+
html: `
109+
<ion-input clear-input-icon="foo" clear-input="true"></ion-input>
110+
`,
111+
});
112+
113+
const icon = page.body.querySelector<HTMLIonIconElement>('ion-input ion-icon')!;
114+
115+
expect(icon.getAttribute('icon')).toBe('foo');
116+
});
117+
});

packages/angular/src/directives/proxies.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -592,15 +592,15 @@ export declare interface IonCol extends Components.IonCol {}
592592

593593

594594
@ProxyCmp({
595-
inputs: ['color', 'forceOverscroll', 'fullscreen', 'scrollEvents', 'scrollX', 'scrollY'],
595+
inputs: ['color', 'fixedSlotPlacement', 'forceOverscroll', 'fullscreen', 'scrollEvents', 'scrollX', 'scrollY'],
596596
methods: ['getScrollElement', 'scrollToTop', 'scrollToBottom', 'scrollByPoint', 'scrollToPoint']
597597
})
598598
@Component({
599599
selector: 'ion-content',
600600
changeDetection: ChangeDetectionStrategy.OnPush,
601601
template: '<ng-content></ng-content>',
602602
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
603-
inputs: ['color', 'forceOverscroll', 'fullscreen', 'scrollEvents', 'scrollX', 'scrollY'],
603+
inputs: ['color', 'fixedSlotPlacement', 'forceOverscroll', 'fullscreen', 'scrollEvents', 'scrollX', 'scrollY'],
604604
})
605605
export class IonContent {
606606
protected el: HTMLElement;
@@ -955,15 +955,15 @@ export declare interface IonInfiniteScrollContent extends Components.IonInfinite
955955

956956

957957
@ProxyCmp({
958-
inputs: ['autocapitalize', 'autocomplete', 'autocorrect', 'autofocus', 'clearInput', 'clearOnEdit', 'color', 'counter', 'counterFormatter', 'debounce', 'disabled', 'enterkeyhint', 'errorText', 'fill', 'helperText', 'inputmode', 'label', 'labelPlacement', 'max', 'maxlength', 'min', 'minlength', 'mode', 'multiple', 'name', 'pattern', 'placeholder', 'readonly', 'required', 'shape', 'spellcheck', 'step', 'type', 'value'],
958+
inputs: ['autocapitalize', 'autocomplete', 'autocorrect', 'autofocus', 'clearInput', 'clearInputIcon', 'clearOnEdit', 'color', 'counter', 'counterFormatter', 'debounce', 'disabled', 'enterkeyhint', 'errorText', 'fill', 'helperText', 'inputmode', 'label', 'labelPlacement', 'max', 'maxlength', 'min', 'minlength', 'mode', 'multiple', 'name', 'pattern', 'placeholder', 'readonly', 'required', 'shape', 'spellcheck', 'step', 'type', 'value'],
959959
methods: ['setFocus', 'getInputElement']
960960
})
961961
@Component({
962962
selector: 'ion-input',
963963
changeDetection: ChangeDetectionStrategy.OnPush,
964964
template: '<ng-content></ng-content>',
965965
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
966-
inputs: ['autocapitalize', 'autocomplete', 'autocorrect', 'autofocus', 'clearInput', 'clearOnEdit', 'color', 'counter', 'counterFormatter', 'debounce', 'disabled', 'enterkeyhint', 'errorText', 'fill', 'helperText', 'inputmode', 'label', 'labelPlacement', 'max', 'maxlength', 'min', 'minlength', 'mode', 'multiple', 'name', 'pattern', 'placeholder', 'readonly', 'required', 'shape', 'spellcheck', 'step', 'type', 'value'],
966+
inputs: ['autocapitalize', 'autocomplete', 'autocorrect', 'autofocus', 'clearInput', 'clearInputIcon', 'clearOnEdit', 'color', 'counter', 'counterFormatter', 'debounce', 'disabled', 'enterkeyhint', 'errorText', 'fill', 'helperText', 'inputmode', 'label', 'labelPlacement', 'max', 'maxlength', 'min', 'minlength', 'mode', 'multiple', 'name', 'pattern', 'placeholder', 'readonly', 'required', 'shape', 'spellcheck', 'step', 'type', 'value'],
967967
})
968968
export class IonInput {
969969
protected el: HTMLElement;

packages/angular/standalone/src/directives/proxies.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -659,15 +659,15 @@ export declare interface IonCol extends Components.IonCol {}
659659

660660
@ProxyCmp({
661661
defineCustomElementFn: defineIonContent,
662-
inputs: ['color', 'forceOverscroll', 'fullscreen', 'scrollEvents', 'scrollX', 'scrollY'],
662+
inputs: ['color', 'fixedSlotPlacement', 'forceOverscroll', 'fullscreen', 'scrollEvents', 'scrollX', 'scrollY'],
663663
methods: ['getScrollElement', 'scrollToTop', 'scrollToBottom', 'scrollByPoint', 'scrollToPoint']
664664
})
665665
@Component({
666666
selector: 'ion-content',
667667
changeDetection: ChangeDetectionStrategy.OnPush,
668668
template: '<ng-content></ng-content>',
669669
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
670-
inputs: ['color', 'forceOverscroll', 'fullscreen', 'scrollEvents', 'scrollX', 'scrollY'],
670+
inputs: ['color', 'fixedSlotPlacement', 'forceOverscroll', 'fullscreen', 'scrollEvents', 'scrollX', 'scrollY'],
671671
standalone: true
672672
})
673673
export class IonContent {

0 commit comments

Comments
 (0)