Merge branch 'feature-8.5' into ROU-11554
2
.github/workflows/assign-issues.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
issues: write
|
||||
steps:
|
||||
- name: 'Auto-assign issue'
|
||||
uses: pozil/auto-assign-issue@c015a6a3f410f12f58255c3d085fd774312f7a2f # v2.1.2
|
||||
uses: pozil/auto-assign-issue@39c06395cbac76e79afc4ad4e5c5c6db6ecfdd2e # v2.2.0
|
||||
with:
|
||||
assignees: brandyscarney, thetaPC, joselrio, rugoncalves, BenOsodrac, JoaoFerreira-FrontEnd, OS-giulianasilva, tanner-reits
|
||||
numOfAssignee: 1
|
||||
|
||||
2
.github/workflows/build.yml
vendored
@@ -198,7 +198,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
apps: [react17, react18]
|
||||
apps: [react17, react18, react19]
|
||||
needs: [build-react, build-react-router]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
2
.github/workflows/stencil-nightly.yml
vendored
@@ -208,7 +208,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
apps: [react17, react18]
|
||||
apps: [react17, react18, react19]
|
||||
needs: [build-react, build-react-router]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
14
core/package-lock.json
generated
@@ -19,7 +19,7 @@
|
||||
"@capacitor/haptics": "^6.0.0",
|
||||
"@capacitor/keyboard": "^6.0.0",
|
||||
"@capacitor/status-bar": "^6.0.0",
|
||||
"@clack/prompts": "^0.9.0",
|
||||
"@clack/prompts": "^0.10.0",
|
||||
"@ionic/eslint-config": "^0.3.0",
|
||||
"@ionic/prettier-config": "^2.0.0",
|
||||
"@playwright/test": "^1.46.1",
|
||||
@@ -709,9 +709,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@clack/prompts": {
|
||||
"version": "0.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.9.1.tgz",
|
||||
"integrity": "sha512-JIpyaboYZeWYlyP0H+OoPPxd6nqueG/CmN6ixBiNFsIDHREevjIf0n0Ohh5gr5C8pEDknzgvz+pIJ8dMhzWIeg==",
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.10.0.tgz",
|
||||
"integrity": "sha512-H3rCl6CwW1NdQt9rE3n373t7o5cthPv7yUoxF2ytZvyvlJv89C5RYMJu83Hed8ODgys5vpBU0GKxIRG83jd8NQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@clack/core": "0.4.1",
|
||||
@@ -11017,9 +11017,9 @@
|
||||
}
|
||||
},
|
||||
"@clack/prompts": {
|
||||
"version": "0.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.9.1.tgz",
|
||||
"integrity": "sha512-JIpyaboYZeWYlyP0H+OoPPxd6nqueG/CmN6ixBiNFsIDHREevjIf0n0Ohh5gr5C8pEDknzgvz+pIJ8dMhzWIeg==",
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.10.0.tgz",
|
||||
"integrity": "sha512-H3rCl6CwW1NdQt9rE3n373t7o5cthPv7yUoxF2ytZvyvlJv89C5RYMJu83Hed8ODgys5vpBU0GKxIRG83jd8NQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@clack/core": "0.4.1",
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"@capacitor/haptics": "^6.0.0",
|
||||
"@capacitor/keyboard": "^6.0.0",
|
||||
"@capacitor/status-bar": "^6.0.0",
|
||||
"@clack/prompts": "^0.9.0",
|
||||
"@clack/prompts": "^0.10.0",
|
||||
"@ionic/eslint-config": "^0.3.0",
|
||||
"@ionic/prettier-config": "^2.0.0",
|
||||
"@playwright/test": "^1.46.1",
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ComponentInterface } from '@stencil/core';
|
||||
import { Component, Element, Host, Prop, Method, State, Watch, forceUpdate, h } from '@stencil/core';
|
||||
import type { ButtonInterface } from '@utils/element-interface';
|
||||
import type { Attributes } from '@utils/helpers';
|
||||
import { addEventListener, removeEventListener, inheritAttributes } from '@utils/helpers';
|
||||
import { addEventListener, removeEventListener, inheritAttributes, getNextSiblingOfType } from '@utils/helpers';
|
||||
import { hostContext } from '@utils/theme';
|
||||
|
||||
import { getIonMode } from '../../global/ionic-global';
|
||||
@@ -65,7 +65,41 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
|
||||
this.updateState();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
private waitForSegmentContent(ionSegment: HTMLIonSegmentElement | null, contentId: string): Promise<HTMLElement> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let timeoutId: NodeJS.Timeout | undefined = undefined;
|
||||
let animationFrameId: number;
|
||||
|
||||
const check = () => {
|
||||
if (!ionSegment) {
|
||||
reject(new Error(`Segment not found when looking for Segment Content`));
|
||||
return;
|
||||
}
|
||||
|
||||
const segmentView = getNextSiblingOfType<HTMLIonSegmentViewElement>(ionSegment); // Skip the text nodes
|
||||
const segmentContent = segmentView?.querySelector(
|
||||
`ion-segment-content[id="${contentId}"]`
|
||||
) as HTMLIonSegmentContentElement | null;
|
||||
if (segmentContent && timeoutId) {
|
||||
clearTimeout(timeoutId); // Clear the timeout if the segmentContent is found
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
resolve(segmentContent);
|
||||
} else {
|
||||
animationFrameId = requestAnimationFrame(check); // Keep checking on the next animation frame
|
||||
}
|
||||
};
|
||||
|
||||
check();
|
||||
|
||||
// Set a timeout to reject the promise
|
||||
timeoutId = setTimeout(() => {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
reject(new Error(`Unable to find Segment Content with id="${contentId} within 1000 ms`));
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
const segmentEl = (this.segmentEl = this.el.closest('ion-segment'));
|
||||
if (segmentEl) {
|
||||
this.updateState();
|
||||
@@ -76,12 +110,13 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
|
||||
// Return if there is no contentId defined
|
||||
if (!this.contentId) return;
|
||||
|
||||
// Attempt to find the Segment Content by its contentId
|
||||
const segmentContent = document.getElementById(this.contentId) as HTMLIonSegmentContentElement | null;
|
||||
|
||||
// If no associated Segment Content exists, log an error and return
|
||||
if (!segmentContent) {
|
||||
console.error(`Segment Button: Unable to find Segment Content with id="${this.contentId}".`);
|
||||
let segmentContent;
|
||||
try {
|
||||
// Attempt to find the Segment Content by its contentId
|
||||
segmentContent = await this.waitForSegmentContent(segmentEl, this.contentId);
|
||||
} catch (error) {
|
||||
// If no associated Segment Content exists, log an error and return
|
||||
console.error('Segment Button: ', (error as Error).message);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -123,6 +123,8 @@
|
||||
<button class="expand" onClick="changeSegmentContent()">Change Segment Content</button>
|
||||
|
||||
<button class="expand" onClick="clearSegmentValue()">Clear Segment Value</button>
|
||||
|
||||
<button class="expand" onClick="addSegmentButtonAndContent()">Add New Segment Button & Content</button>
|
||||
</ion-content>
|
||||
|
||||
<ion-footer>
|
||||
@@ -158,6 +160,34 @@
|
||||
segment.value = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
async function addSegmentButtonAndContent() {
|
||||
const segment = document.querySelector('ion-segment');
|
||||
const segmentView = document.querySelector('ion-segment-view');
|
||||
|
||||
const newButton = document.createElement('ion-segment-button');
|
||||
const newId = `new-${Date.now()}`;
|
||||
newButton.setAttribute('content-id', newId);
|
||||
newButton.setAttribute('value', newId);
|
||||
newButton.innerHTML = '<ion-label>New Button</ion-label>';
|
||||
|
||||
segment.appendChild(newButton);
|
||||
|
||||
setTimeout(() => {
|
||||
// Timeout to test waitForSegmentContent() in segment-button
|
||||
const newContent = document.createElement('ion-segment-content');
|
||||
newContent.setAttribute('id', newId);
|
||||
newContent.innerHTML = 'New Content';
|
||||
|
||||
segmentView.appendChild(newContent);
|
||||
|
||||
// Necessary timeout to ensure the value is set after the content is added.
|
||||
// Otherwise, the transition is unsuccessful and the content is not shown.
|
||||
setTimeout(() => {
|
||||
segment.setAttribute('value', newId);
|
||||
}, 200);
|
||||
}, 200);
|
||||
}
|
||||
</script>
|
||||
</ion-app>
|
||||
</body>
|
||||
|
||||
@@ -317,19 +317,10 @@ export class Select implements ComponentInterface {
|
||||
}
|
||||
this.isExpanded = true;
|
||||
const overlay = (this.overlay = await this.createOverlay(event));
|
||||
overlay.onDidDismiss().then(() => {
|
||||
this.overlay = undefined;
|
||||
this.isExpanded = false;
|
||||
this.ionDismiss.emit();
|
||||
this.setFocus();
|
||||
});
|
||||
|
||||
await overlay.present();
|
||||
|
||||
// focus selected option for popovers and modals
|
||||
if (this.interface === 'popover' || this.interface === 'modal') {
|
||||
// Add logic to scroll selected item into view before presenting
|
||||
const scrollSelectedIntoView = () => {
|
||||
const indexOfSelected = this.childOpts.findIndex((o) => o.value === this.value);
|
||||
|
||||
if (indexOfSelected > -1) {
|
||||
const selectedItem = overlay.querySelector<HTMLElement>(
|
||||
`.select-interface-option:nth-child(${indexOfSelected + 1})`
|
||||
@@ -352,6 +343,7 @@ export class Select implements ComponentInterface {
|
||||
| HTMLIonCheckboxElement
|
||||
| null;
|
||||
if (interactiveEl) {
|
||||
selectedItem.scrollIntoView({ block: 'nearest' });
|
||||
// Needs to be called before `focusVisibleElement` to prevent issue with focus event bubbling
|
||||
// and removing `ion-focused` style
|
||||
interactiveEl.setFocus();
|
||||
@@ -379,8 +371,40 @@ export class Select implements ComponentInterface {
|
||||
focusVisibleElement(firstEnabledOption.closest('ion-item')!);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// For modals and popovers, we can scroll before they're visible
|
||||
if (this.interface === 'modal') {
|
||||
overlay.addEventListener('ionModalWillPresent', scrollSelectedIntoView, { once: true });
|
||||
} else if (this.interface === 'popover') {
|
||||
overlay.addEventListener('ionPopoverWillPresent', scrollSelectedIntoView, { once: true });
|
||||
} else {
|
||||
/**
|
||||
* For alerts and action sheets, we need to wait a frame after willPresent
|
||||
* because these overlays don't have their content in the DOM immediately
|
||||
* when willPresent fires. By waiting a frame, we ensure the content is
|
||||
* rendered and can be properly scrolled into view.
|
||||
*/
|
||||
const scrollAfterRender = () => {
|
||||
requestAnimationFrame(() => {
|
||||
scrollSelectedIntoView();
|
||||
});
|
||||
};
|
||||
if (this.interface === 'alert') {
|
||||
overlay.addEventListener('ionAlertWillPresent', scrollAfterRender, { once: true });
|
||||
} else if (this.interface === 'action-sheet') {
|
||||
overlay.addEventListener('ionActionSheetWillPresent', scrollAfterRender, { once: true });
|
||||
}
|
||||
}
|
||||
|
||||
overlay.onDidDismiss().then(() => {
|
||||
this.overlay = undefined;
|
||||
this.isExpanded = false;
|
||||
this.ionDismiss.emit();
|
||||
this.setFocus();
|
||||
});
|
||||
|
||||
await overlay.present();
|
||||
return overlay;
|
||||
}
|
||||
|
||||
|
||||
@@ -61,6 +61,169 @@
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
|
||||
<ion-list>
|
||||
<ion-list-header>
|
||||
<ion-label>Single Value - Overflowing Options</ion-label>
|
||||
</ion-list-header>
|
||||
|
||||
<ion-item>
|
||||
<ion-select id="alert-select-scroll-to-selected" label="Alert" interface="alert" value="watermelon">
|
||||
<ion-select-option value="apple">Apple</ion-select-option>
|
||||
<ion-select-option value="apricot">Apricot</ion-select-option>
|
||||
<ion-select-option value="avocado">Avocado</ion-select-option>
|
||||
<ion-select-option value="banana">Banana</ion-select-option>
|
||||
<ion-select-option value="blackberry">Blackberry</ion-select-option>
|
||||
<ion-select-option value="blueberry">Blueberry</ion-select-option>
|
||||
<ion-select-option value="cantaloupe">Cantaloupe</ion-select-option>
|
||||
<ion-select-option value="cherry">Cherry</ion-select-option>
|
||||
<ion-select-option value="coconut">Coconut</ion-select-option>
|
||||
<ion-select-option value="cranberry">Cranberry</ion-select-option>
|
||||
<ion-select-option value="dragonfruit">Dragonfruit</ion-select-option>
|
||||
<ion-select-option value="fig">Fig</ion-select-option>
|
||||
<ion-select-option value="grape">Grape</ion-select-option>
|
||||
<ion-select-option value="grapefruit">Grapefruit</ion-select-option>
|
||||
<ion-select-option value="guava">Guava</ion-select-option>
|
||||
<ion-select-option value="kiwi">Kiwi</ion-select-option>
|
||||
<ion-select-option value="lemon">Lemon</ion-select-option>
|
||||
<ion-select-option value="lime">Lime</ion-select-option>
|
||||
<ion-select-option value="lychee">Lychee</ion-select-option>
|
||||
<ion-select-option value="mango">Mango</ion-select-option>
|
||||
<ion-select-option value="nectarine">Nectarine</ion-select-option>
|
||||
<ion-select-option value="orange">Orange</ion-select-option>
|
||||
<ion-select-option value="papaya">Papaya</ion-select-option>
|
||||
<ion-select-option value="passion-fruit">Passion Fruit</ion-select-option>
|
||||
<ion-select-option value="peach">Peach</ion-select-option>
|
||||
<ion-select-option value="pear">Pear</ion-select-option>
|
||||
<ion-select-option value="pineapple">Pineapple</ion-select-option>
|
||||
<ion-select-option value="plum">Plum</ion-select-option>
|
||||
<ion-select-option value="pomegranate">Pomegranate</ion-select-option>
|
||||
<ion-select-option value="raspberry">Raspberry</ion-select-option>
|
||||
<ion-select-option value="strawberry">Strawberry</ion-select-option>
|
||||
<ion-select-option value="tangerine">Tangerine</ion-select-option>
|
||||
<ion-select-option value="watermelon">Watermelon</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-select
|
||||
id="action-sheet-select-scroll-to-selected"
|
||||
label="Action Sheet"
|
||||
interface="action-sheet"
|
||||
value="watermelon"
|
||||
>
|
||||
<ion-select-option value="apple">Apple</ion-select-option>
|
||||
<ion-select-option value="apricot">Apricot</ion-select-option>
|
||||
<ion-select-option value="avocado">Avocado</ion-select-option>
|
||||
<ion-select-option value="banana">Banana</ion-select-option>
|
||||
<ion-select-option value="blackberry">Blackberry</ion-select-option>
|
||||
<ion-select-option value="blueberry">Blueberry</ion-select-option>
|
||||
<ion-select-option value="cantaloupe">Cantaloupe</ion-select-option>
|
||||
<ion-select-option value="cherry">Cherry</ion-select-option>
|
||||
<ion-select-option value="coconut">Coconut</ion-select-option>
|
||||
<ion-select-option value="cranberry">Cranberry</ion-select-option>
|
||||
<ion-select-option value="dragonfruit">Dragonfruit</ion-select-option>
|
||||
<ion-select-option value="fig">Fig</ion-select-option>
|
||||
<ion-select-option value="grape">Grape</ion-select-option>
|
||||
<ion-select-option value="grapefruit">Grapefruit</ion-select-option>
|
||||
<ion-select-option value="guava">Guava</ion-select-option>
|
||||
<ion-select-option value="kiwi">Kiwi</ion-select-option>
|
||||
<ion-select-option value="lemon">Lemon</ion-select-option>
|
||||
<ion-select-option value="lime">Lime</ion-select-option>
|
||||
<ion-select-option value="lychee">Lychee</ion-select-option>
|
||||
<ion-select-option value="mango">Mango</ion-select-option>
|
||||
<ion-select-option value="nectarine">Nectarine</ion-select-option>
|
||||
<ion-select-option value="orange">Orange</ion-select-option>
|
||||
<ion-select-option value="papaya">Papaya</ion-select-option>
|
||||
<ion-select-option value="passion-fruit">Passion Fruit</ion-select-option>
|
||||
<ion-select-option value="peach">Peach</ion-select-option>
|
||||
<ion-select-option value="pear">Pear</ion-select-option>
|
||||
<ion-select-option value="pineapple">Pineapple</ion-select-option>
|
||||
<ion-select-option value="plum">Plum</ion-select-option>
|
||||
<ion-select-option value="pomegranate">Pomegranate</ion-select-option>
|
||||
<ion-select-option value="raspberry">Raspberry</ion-select-option>
|
||||
<ion-select-option value="strawberry">Strawberry</ion-select-option>
|
||||
<ion-select-option value="tangerine">Tangerine</ion-select-option>
|
||||
<ion-select-option value="watermelon">Watermelon</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-select id="popover-select-scroll-to-selected" label="Popover" interface="popover" value="watermelon">
|
||||
<ion-select-option value="apple">Apple</ion-select-option>
|
||||
<ion-select-option value="apricot">Apricot</ion-select-option>
|
||||
<ion-select-option value="avocado">Avocado</ion-select-option>
|
||||
<ion-select-option value="banana">Banana</ion-select-option>
|
||||
<ion-select-option value="blackberry">Blackberry</ion-select-option>
|
||||
<ion-select-option value="blueberry">Blueberry</ion-select-option>
|
||||
<ion-select-option value="cantaloupe">Cantaloupe</ion-select-option>
|
||||
<ion-select-option value="cherry">Cherry</ion-select-option>
|
||||
<ion-select-option value="coconut">Coconut</ion-select-option>
|
||||
<ion-select-option value="cranberry">Cranberry</ion-select-option>
|
||||
<ion-select-option value="dragonfruit">Dragonfruit</ion-select-option>
|
||||
<ion-select-option value="fig">Fig</ion-select-option>
|
||||
<ion-select-option value="grape">Grape</ion-select-option>
|
||||
<ion-select-option value="grapefruit">Grapefruit</ion-select-option>
|
||||
<ion-select-option value="guava">Guava</ion-select-option>
|
||||
<ion-select-option value="kiwi">Kiwi</ion-select-option>
|
||||
<ion-select-option value="lemon">Lemon</ion-select-option>
|
||||
<ion-select-option value="lime">Lime</ion-select-option>
|
||||
<ion-select-option value="lychee">Lychee</ion-select-option>
|
||||
<ion-select-option value="mango">Mango</ion-select-option>
|
||||
<ion-select-option value="nectarine">Nectarine</ion-select-option>
|
||||
<ion-select-option value="orange">Orange</ion-select-option>
|
||||
<ion-select-option value="papaya">Papaya</ion-select-option>
|
||||
<ion-select-option value="passion-fruit">Passion Fruit</ion-select-option>
|
||||
<ion-select-option value="peach">Peach</ion-select-option>
|
||||
<ion-select-option value="pear">Pear</ion-select-option>
|
||||
<ion-select-option value="pineapple">Pineapple</ion-select-option>
|
||||
<ion-select-option value="plum">Plum</ion-select-option>
|
||||
<ion-select-option value="pomegranate">Pomegranate</ion-select-option>
|
||||
<ion-select-option value="raspberry">Raspberry</ion-select-option>
|
||||
<ion-select-option value="strawberry">Strawberry</ion-select-option>
|
||||
<ion-select-option value="tangerine">Tangerine</ion-select-option>
|
||||
<ion-select-option value="watermelon">Watermelon</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-select id="modal-select-scroll-to-selected" label="Modal" interface="modal" value="watermelon">
|
||||
<ion-select-option value="apple">Apple</ion-select-option>
|
||||
<ion-select-option value="apricot">Apricot</ion-select-option>
|
||||
<ion-select-option value="avocado">Avocado</ion-select-option>
|
||||
<ion-select-option value="banana">Banana</ion-select-option>
|
||||
<ion-select-option value="blackberry">Blackberry</ion-select-option>
|
||||
<ion-select-option value="blueberry">Blueberry</ion-select-option>
|
||||
<ion-select-option value="cantaloupe">Cantaloupe</ion-select-option>
|
||||
<ion-select-option value="cherry">Cherry</ion-select-option>
|
||||
<ion-select-option value="coconut">Coconut</ion-select-option>
|
||||
<ion-select-option value="cranberry">Cranberry</ion-select-option>
|
||||
<ion-select-option value="dragonfruit">Dragonfruit</ion-select-option>
|
||||
<ion-select-option value="fig">Fig</ion-select-option>
|
||||
<ion-select-option value="grape">Grape</ion-select-option>
|
||||
<ion-select-option value="grapefruit">Grapefruit</ion-select-option>
|
||||
<ion-select-option value="guava">Guava</ion-select-option>
|
||||
<ion-select-option value="kiwi">Kiwi</ion-select-option>
|
||||
<ion-select-option value="lemon">Lemon</ion-select-option>
|
||||
<ion-select-option value="lime">Lime</ion-select-option>
|
||||
<ion-select-option value="lychee">Lychee</ion-select-option>
|
||||
<ion-select-option value="mango">Mango</ion-select-option>
|
||||
<ion-select-option value="nectarine">Nectarine</ion-select-option>
|
||||
<ion-select-option value="orange">Orange</ion-select-option>
|
||||
<ion-select-option value="papaya">Papaya</ion-select-option>
|
||||
<ion-select-option value="passion-fruit">Passion Fruit</ion-select-option>
|
||||
<ion-select-option value="peach">Peach</ion-select-option>
|
||||
<ion-select-option value="pear">Pear</ion-select-option>
|
||||
<ion-select-option value="pineapple">Pineapple</ion-select-option>
|
||||
<ion-select-option value="plum">Plum</ion-select-option>
|
||||
<ion-select-option value="pomegranate">Pomegranate</ion-select-option>
|
||||
<ion-select-option value="raspberry">Raspberry</ion-select-option>
|
||||
<ion-select-option value="strawberry">Strawberry</ion-select-option>
|
||||
<ion-select-option value="tangerine">Tangerine</ion-select-option>
|
||||
<ion-select-option value="watermelon">Watermelon</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
|
||||
<ion-list>
|
||||
<ion-list-header>
|
||||
<ion-label>Multiple Value Select</ion-label>
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { E2ELocator } from '@utils/test/playwright';
|
||||
* does not. The overlay rendering is already tested in the respective
|
||||
* test files.
|
||||
*/
|
||||
configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
configs({ directions: ['ltr'] }).forEach(({ title, config, screenshot }) => {
|
||||
test.describe(title('select: basic'), () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/src/components/select/test/basic', config);
|
||||
@@ -24,6 +24,16 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
|
||||
await expect(page.locator('ion-alert')).toBeVisible();
|
||||
});
|
||||
|
||||
test('it should scroll to selected option when opened', async ({ page }) => {
|
||||
const ionAlertDidPresent = await page.spyOnEvent('ionAlertDidPresent');
|
||||
|
||||
await page.click('#alert-select-scroll-to-selected');
|
||||
await ionAlertDidPresent.next();
|
||||
|
||||
const alert = page.locator('ion-alert');
|
||||
await expect(alert).toHaveScreenshot(screenshot(`select-basic-alert-scroll-to-selected`));
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('select: action sheet', () => {
|
||||
@@ -36,6 +46,16 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
|
||||
await expect(page.locator('ion-action-sheet')).toBeVisible();
|
||||
});
|
||||
|
||||
test('it should scroll to selected option when opened', async ({ page }) => {
|
||||
const ionActionSheetDidPresent = await page.spyOnEvent('ionActionSheetDidPresent');
|
||||
|
||||
await page.click('#action-sheet-select-scroll-to-selected');
|
||||
await ionActionSheetDidPresent.next();
|
||||
|
||||
const actionSheet = page.locator('ion-action-sheet');
|
||||
await expect(actionSheet).toHaveScreenshot(screenshot(`select-basic-action-sheet-scroll-to-selected`));
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('select: popover', () => {
|
||||
@@ -57,6 +77,16 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
|
||||
await expect(popover).toBeVisible();
|
||||
});
|
||||
|
||||
test('it should scroll to selected option when opened', async ({ page }) => {
|
||||
const ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent');
|
||||
|
||||
await page.click('#popover-select-scroll-to-selected');
|
||||
await ionPopoverDidPresent.next();
|
||||
|
||||
const popover = page.locator('ion-popover');
|
||||
await expect(popover).toHaveScreenshot(screenshot(`select-basic-popover-scroll-to-selected`));
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('select: modal', () => {
|
||||
@@ -75,6 +105,16 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
|
||||
await expect(modal).toBeVisible();
|
||||
});
|
||||
|
||||
test('it should scroll to selected option when opened', async ({ page }) => {
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
|
||||
await page.click('#modal-select-scroll-to-selected');
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
const modal = page.locator('ion-modal');
|
||||
await expect(modal).toHaveScreenshot(screenshot(`select-basic-modal-scroll-to-selected`));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 33 KiB |
@@ -413,3 +413,14 @@ export const shallowEqualStringMap = (
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const getNextSiblingOfType = <T extends Element>(element: Element): T | null => {
|
||||
let sibling = element.nextSibling;
|
||||
while (sibling) {
|
||||
if (sibling.nodeType === Node.ELEMENT_NODE && (sibling as T) !== null) {
|
||||
return sibling as T;
|
||||
}
|
||||
sibling = sibling.nextSibling;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { JSX as LocalJSX } from '@ionic/core/components';
|
||||
import React from 'react';
|
||||
import React, { type PropsWithChildren } from 'react';
|
||||
|
||||
import type { IonContextInterface } from '../contexts/IonContext';
|
||||
import { IonContext } from '../contexts/IonContext';
|
||||
@@ -9,53 +9,49 @@ import { IonOverlayManager } from './IonOverlayManager';
|
||||
import type { IonicReactProps } from './IonicReactProps';
|
||||
import { IonAppInner } from './inner-proxies';
|
||||
|
||||
type Props = LocalJSX.IonApp &
|
||||
IonicReactProps & {
|
||||
ref?: React.Ref<HTMLIonAppElement>;
|
||||
type Props = PropsWithChildren<
|
||||
LocalJSX.IonApp &
|
||||
IonicReactProps & {
|
||||
ref?: React.Ref<HTMLIonAppElement>;
|
||||
}
|
||||
>;
|
||||
|
||||
export class IonApp extends React.Component<Props> {
|
||||
addOverlayCallback?: (id: string, overlay: ReactComponentOrElement, containerElement: HTMLDivElement) => void;
|
||||
removeOverlayCallback?: (id: string) => void;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
ionContext: IonContextInterface = {
|
||||
addOverlay: (id: string, overlay: ReactComponentOrElement, containerElement: HTMLDivElement) => {
|
||||
if (this.addOverlayCallback) {
|
||||
this.addOverlayCallback(id, overlay, containerElement);
|
||||
}
|
||||
},
|
||||
removeOverlay: (id: string) => {
|
||||
if (this.removeOverlayCallback) {
|
||||
this.removeOverlayCallback(id);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const IonApp = /*@__PURE__*/ (() =>
|
||||
class extends React.Component<Props> {
|
||||
addOverlayCallback?: (id: string, overlay: ReactComponentOrElement, containerElement: HTMLDivElement) => void;
|
||||
removeOverlayCallback?: (id: string) => void;
|
||||
render() {
|
||||
return (
|
||||
<IonContext.Provider value={this.ionContext}>
|
||||
<IonAppInner {...this.props}>{this.props.children}</IonAppInner>
|
||||
<IonOverlayManager
|
||||
onAddOverlay={(callback) => {
|
||||
this.addOverlayCallback = callback;
|
||||
}}
|
||||
onRemoveOverlay={(callback) => {
|
||||
this.removeOverlayCallback = callback;
|
||||
}}
|
||||
/>
|
||||
</IonContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
/*
|
||||
Wire up methods to call into IonOverlayManager
|
||||
*/
|
||||
ionContext: IonContextInterface = {
|
||||
addOverlay: (id: string, overlay: ReactComponentOrElement, containerElement: HTMLDivElement) => {
|
||||
if (this.addOverlayCallback) {
|
||||
this.addOverlayCallback(id, overlay, containerElement);
|
||||
}
|
||||
},
|
||||
removeOverlay: (id: string) => {
|
||||
if (this.removeOverlayCallback) {
|
||||
this.removeOverlayCallback(id);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<IonContext.Provider value={this.ionContext}>
|
||||
<IonAppInner {...this.props}>{this.props.children}</IonAppInner>
|
||||
<IonOverlayManager
|
||||
onAddOverlay={(callback) => {
|
||||
this.addOverlayCallback = callback;
|
||||
}}
|
||||
onRemoveOverlay={(callback) => {
|
||||
this.removeOverlayCallback = callback;
|
||||
}}
|
||||
/>
|
||||
</IonContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
static get displayName() {
|
||||
return 'IonApp';
|
||||
}
|
||||
})();
|
||||
static displayName = 'IonApp';
|
||||
}
|
||||
|
||||
@@ -1,54 +1,53 @@
|
||||
import type { JSX as LocalJSX } from '@ionic/core/components';
|
||||
import React from 'react';
|
||||
import React, { type PropsWithChildren } from 'react';
|
||||
|
||||
import { NavContext } from '../../contexts/NavContext';
|
||||
import type { IonicReactProps } from '../IonicReactProps';
|
||||
import { IonBackButtonInner } from '../inner-proxies';
|
||||
|
||||
type Props = Omit<LocalJSX.IonBackButton, 'icon'> &
|
||||
IonicReactProps & {
|
||||
icon?:
|
||||
| {
|
||||
ios: string;
|
||||
md: string;
|
||||
}
|
||||
| string;
|
||||
ref?: React.Ref<HTMLIonBackButtonElement>;
|
||||
type Props = PropsWithChildren<
|
||||
LocalJSX.IonBackButton &
|
||||
IonicReactProps & {
|
||||
ref?: React.Ref<HTMLIonBackButtonElement>;
|
||||
}
|
||||
>;
|
||||
|
||||
export class IonBackButton extends React.Component<Props> {
|
||||
context!: React.ContextType<typeof NavContext>;
|
||||
|
||||
clickButton = (e: React.MouseEvent) => {
|
||||
/**
|
||||
* If ion-back-button is being used inside
|
||||
* of ion-nav then we should not interact with
|
||||
* the router.
|
||||
*/
|
||||
if (e.target && (e.target as HTMLElement).closest('ion-nav') !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { defaultHref, routerAnimation } = this.props;
|
||||
|
||||
if (this.context.hasIonicRouter()) {
|
||||
e.stopPropagation();
|
||||
this.context.goBack(defaultHref, routerAnimation);
|
||||
} else if (defaultHref !== undefined) {
|
||||
window.location.href = defaultHref;
|
||||
}
|
||||
};
|
||||
|
||||
export const IonBackButton = /*@__PURE__*/ (() =>
|
||||
class extends React.Component<Props> {
|
||||
context!: React.ContextType<typeof NavContext>;
|
||||
render() {
|
||||
return <IonBackButtonInner onClick={this.clickButton} {...this.props}></IonBackButtonInner>;
|
||||
}
|
||||
|
||||
clickButton = (e: React.MouseEvent) => {
|
||||
/**
|
||||
* If ion-back-button is being used inside
|
||||
* of ion-nav then we should not interact with
|
||||
* the router.
|
||||
*/
|
||||
if (e.target && (e.target as HTMLElement).closest('ion-nav') !== null) {
|
||||
return;
|
||||
}
|
||||
static get displayName() {
|
||||
return 'IonBackButton';
|
||||
}
|
||||
|
||||
const { defaultHref, routerAnimation } = this.props;
|
||||
static get contextType() {
|
||||
return NavContext;
|
||||
}
|
||||
|
||||
if (this.context.hasIonicRouter()) {
|
||||
e.stopPropagation();
|
||||
this.context.goBack(defaultHref, routerAnimation);
|
||||
} else if (defaultHref !== undefined) {
|
||||
window.location.href = defaultHref;
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return <IonBackButtonInner onClick={this.clickButton} {...this.props}></IonBackButtonInner>;
|
||||
}
|
||||
|
||||
static get displayName() {
|
||||
return 'IonBackButton';
|
||||
}
|
||||
|
||||
static get contextType() {
|
||||
return NavContext;
|
||||
}
|
||||
})();
|
||||
shouldComponentUpdate(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,41 +13,45 @@ type Props = LocalJSX.IonTabButton &
|
||||
onPointerDown?: React.PointerEventHandler<HTMLIonTabButtonElement>;
|
||||
onTouchEnd?: React.TouchEventHandler<HTMLIonTabButtonElement>;
|
||||
onTouchMove?: React.TouchEventHandler<HTMLIonTabButtonElement>;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const IonTabButton = /*@__PURE__*/ (() =>
|
||||
class extends React.Component<Props> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.handleIonTabButtonClick = this.handleIonTabButtonClick.bind(this);
|
||||
}
|
||||
export class IonTabButton extends React.Component<Props> {
|
||||
shouldComponentUpdate(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
handleIonTabButtonClick() {
|
||||
if (this.props.onClick) {
|
||||
this.props.onClick(
|
||||
new CustomEvent('ionTabButtonClick', {
|
||||
detail: {
|
||||
tab: this.props.tab,
|
||||
href: this.props.href,
|
||||
routeOptions: this.props.routerOptions,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.handleIonTabButtonClick = this.handleIonTabButtonClick.bind(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
/**
|
||||
* onClick is excluded from the props, since it has a custom
|
||||
* implementation within IonTabBar.tsx. Calling onClick within this
|
||||
* component would result in duplicate handler calls.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { onClick, ...rest } = this.props;
|
||||
return <IonTabButtonInner onIonTabButtonClick={this.handleIonTabButtonClick} {...rest}></IonTabButtonInner>;
|
||||
handleIonTabButtonClick() {
|
||||
if (this.props.onClick) {
|
||||
this.props.onClick(
|
||||
new CustomEvent('ionTabButtonClick', {
|
||||
detail: {
|
||||
tab: this.props.tab,
|
||||
href: this.props.href,
|
||||
routeOptions: this.props.routerOptions,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static get displayName() {
|
||||
return 'IonTabButton';
|
||||
}
|
||||
})();
|
||||
render() {
|
||||
/**
|
||||
* onClick is excluded from the props, since it has a custom
|
||||
* implementation within IonTabBar.tsx. Calling onClick within this
|
||||
* component would result in duplicate handler calls.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { onClick, ...rest } = this.props;
|
||||
return <IonTabButtonInner onIonTabButtonClick={this.handleIonTabButtonClick} {...rest}></IonTabButtonInner>;
|
||||
}
|
||||
|
||||
static get displayName() {
|
||||
return 'IonTabButton';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Components } from '@ionic/core';
|
||||
import type { JSX as LocalJSX } from '@ionic/core/components';
|
||||
import React, { Fragment } from 'react';
|
||||
|
||||
@@ -26,12 +27,14 @@ if (typeof (window as any) !== 'undefined' && window.customElements) {
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace JSX {
|
||||
interface IntrinsicElements {
|
||||
'ion-tabs': any;
|
||||
}
|
||||
export interface IonTabsProps extends React.HTMLAttributes<Components.IonTabs> {
|
||||
onIonTabsWillChange?: (event: CustomEvent<{ tab: string }>) => void;
|
||||
onIonTabsDidChange?: (event: CustomEvent<{ tab: string }>) => void;
|
||||
}
|
||||
|
||||
declare module 'react' {
|
||||
interface HTMLElements {
|
||||
'ion-tabs': IonTabsProps;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,169 +43,174 @@ type ChildFunction = (ionTabContext: IonTabsContextState) => React.ReactNode;
|
||||
interface Props extends LocalJSX.IonTabs {
|
||||
className?: string;
|
||||
children: ChildFunction | React.ReactNode;
|
||||
onIonTabsWillChange?: (event: CustomEvent<{ tab: string }>) => void;
|
||||
onIonTabsDidChange?: (event: CustomEvent<{ tab: string }>) => void;
|
||||
}
|
||||
|
||||
export const IonTabs = /*@__PURE__*/ (() =>
|
||||
class extends React.Component<Props> {
|
||||
context!: React.ContextType<typeof NavContext>;
|
||||
export class IonTabs extends React.Component<Props> {
|
||||
shouldComponentUpdate(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
context!: React.ContextType<typeof NavContext>;
|
||||
/**
|
||||
* `routerOutletRef` allows users to add a `ref` to `IonRouterOutlet`.
|
||||
* Without this, `ref.current` will be `undefined` in the user's app,
|
||||
* breaking their ability to access the `IonRouterOutlet` instance.
|
||||
* Do not remove this ref.
|
||||
*/
|
||||
routerOutletRef: React.Ref<Components.IonRouterOutlet> = React.createRef();
|
||||
selectTabHandler?: (tag: string) => boolean;
|
||||
tabBarRef = React.createRef<any>();
|
||||
|
||||
ionTabContextState: IonTabsContextState = {
|
||||
activeTab: undefined,
|
||||
selectTab: () => false,
|
||||
hasRouterOutlet: false,
|
||||
/**
|
||||
* `routerOutletRef` allows users to add a `ref` to `IonRouterOutlet`.
|
||||
* Without this, `ref.current` will be `undefined` in the user's app,
|
||||
* breaking their ability to access the `IonRouterOutlet` instance.
|
||||
* Do not remove this ref.
|
||||
* Tab bar can be used as a standalone component,
|
||||
* so the props can not be passed directly to the
|
||||
* tab bar component. Instead, props will be
|
||||
* passed through the context.
|
||||
*/
|
||||
routerOutletRef: React.Ref<HTMLIonRouterOutletElement> = React.createRef();
|
||||
selectTabHandler?: (tag: string) => boolean;
|
||||
tabBarRef = React.createRef<any>();
|
||||
tabBarProps: { ref: this.tabBarRef },
|
||||
};
|
||||
|
||||
ionTabContextState: IonTabsContextState = {
|
||||
activeTab: undefined,
|
||||
selectTab: () => false,
|
||||
hasRouterOutlet: false,
|
||||
/**
|
||||
* Tab bar can be used as a standalone component,
|
||||
* so the props can not be passed directly to the
|
||||
* tab bar component. Instead, props will be
|
||||
* passed through the context.
|
||||
*/
|
||||
tabBarProps: { ref: this.tabBarRef },
|
||||
};
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
componentDidMount() {
|
||||
if (this.tabBarRef.current) {
|
||||
// Grab initial value
|
||||
this.ionTabContextState.activeTab = this.tabBarRef.current.state.activeTab;
|
||||
// Override method
|
||||
this.tabBarRef.current.setActiveTabOnContext = (tab: string) => {
|
||||
this.ionTabContextState.activeTab = tab;
|
||||
};
|
||||
this.ionTabContextState.selectTab = this.tabBarRef.current.selectTab;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.tabBarRef.current) {
|
||||
// Grab initial value
|
||||
this.ionTabContextState.activeTab = this.tabBarRef.current.state.activeTab;
|
||||
// Override method
|
||||
this.tabBarRef.current.setActiveTabOnContext = (tab: string) => {
|
||||
this.ionTabContextState.activeTab = tab;
|
||||
};
|
||||
this.ionTabContextState.selectTab = this.tabBarRef.current.selectTab;
|
||||
}
|
||||
}
|
||||
renderTabsInner(children: React.ReactNode, outlet: React.ReactElement<{}> | undefined) {
|
||||
return (
|
||||
<IonTabsInner {...this.props}>
|
||||
{React.Children.map(children, (child: React.ReactNode) => {
|
||||
if (React.isValidElement(child)) {
|
||||
const isRouterOutlet =
|
||||
child.type === IonRouterOutlet ||
|
||||
(child.type as any).isRouterOutlet ||
|
||||
(child.type === Fragment && child.props.children[0].type === IonRouterOutlet);
|
||||
|
||||
renderTabsInner(children: React.ReactNode, outlet: React.ReactElement<{}> | undefined) {
|
||||
return (
|
||||
<IonTabsInner {...this.props}>
|
||||
{React.Children.map(children, (child: React.ReactNode) => {
|
||||
if (React.isValidElement(child)) {
|
||||
const isRouterOutlet =
|
||||
child.type === IonRouterOutlet ||
|
||||
(child.type as any).isRouterOutlet ||
|
||||
(child.type === Fragment && child.props.children[0].type === IonRouterOutlet);
|
||||
|
||||
if (isRouterOutlet) {
|
||||
/**
|
||||
* The modified outlet needs to be returned to include
|
||||
* the ref.
|
||||
*/
|
||||
return outlet;
|
||||
}
|
||||
if (isRouterOutlet) {
|
||||
/**
|
||||
* The modified outlet needs to be returned to include
|
||||
* the ref.
|
||||
*/
|
||||
return outlet;
|
||||
}
|
||||
return child;
|
||||
})}
|
||||
</IonTabsInner>
|
||||
);
|
||||
}
|
||||
}
|
||||
return child;
|
||||
})}
|
||||
</IonTabsInner>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
let outlet: React.ReactElement<{}> | undefined;
|
||||
// Check if IonTabs has any IonTab children
|
||||
let hasTab = false;
|
||||
const { className, onIonTabsDidChange, onIonTabsWillChange, ...props } = this.props;
|
||||
render() {
|
||||
let outlet: React.ReactElement<{}> | undefined;
|
||||
// Check if IonTabs has any IonTab children
|
||||
let hasTab = false;
|
||||
const { className, onIonTabsDidChange, onIonTabsWillChange, ...props } = this.props;
|
||||
|
||||
const children =
|
||||
typeof this.props.children === 'function'
|
||||
? (this.props.children as ChildFunction)(this.ionTabContextState)
|
||||
: this.props.children;
|
||||
|
||||
React.Children.forEach(children, (child: any) => {
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (child == null || typeof child !== 'object' || !child.hasOwnProperty('type')) {
|
||||
return;
|
||||
}
|
||||
if (child.type === IonRouterOutlet || child.type.isRouterOutlet) {
|
||||
outlet = React.cloneElement(child);
|
||||
} else if (child.type === Fragment && child.props.children[0].type === IonRouterOutlet) {
|
||||
outlet = React.cloneElement(child.props.children[0]);
|
||||
} else if (child.type === IonTab) {
|
||||
/**
|
||||
* This indicates that IonTabs will be using a basic tab-based navigation
|
||||
* without the history stack or URL updates associated with the router.
|
||||
*/
|
||||
hasTab = true;
|
||||
}
|
||||
|
||||
this.ionTabContextState.hasRouterOutlet = !!outlet;
|
||||
|
||||
let childProps: any = {
|
||||
...this.ionTabContextState.tabBarProps,
|
||||
};
|
||||
const children =
|
||||
typeof this.props.children === 'function'
|
||||
? (this.props.children as ChildFunction)(this.ionTabContextState)
|
||||
: this.props.children;
|
||||
|
||||
React.Children.forEach(children, (child: any) => {
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (child == null || typeof child !== 'object' || !child.hasOwnProperty('type')) {
|
||||
return;
|
||||
}
|
||||
if (child.type === IonRouterOutlet || child.type.isRouterOutlet) {
|
||||
outlet = React.cloneElement(child);
|
||||
} else if (child.type === Fragment && child.props.children[0].type === IonRouterOutlet) {
|
||||
outlet = React.cloneElement(child.props.children[0]);
|
||||
} else if (child.type === IonTab) {
|
||||
/**
|
||||
* Only pass these props
|
||||
* down from IonTabs to IonTabBar
|
||||
* if they are defined, otherwise
|
||||
* if you have a handler set on
|
||||
* IonTabBar it will be overridden.
|
||||
* This indicates that IonTabs will be using a basic tab-based navigation
|
||||
* without the history stack or URL updates associated with the router.
|
||||
*/
|
||||
if (onIonTabsDidChange !== undefined) {
|
||||
childProps = {
|
||||
...childProps,
|
||||
onIonTabsDidChange,
|
||||
};
|
||||
}
|
||||
|
||||
if (onIonTabsWillChange !== undefined) {
|
||||
childProps = {
|
||||
...childProps,
|
||||
onIonTabsWillChange,
|
||||
};
|
||||
}
|
||||
|
||||
this.ionTabContextState.tabBarProps = childProps;
|
||||
});
|
||||
|
||||
if (!outlet && !hasTab) {
|
||||
throw new Error('IonTabs must contain an IonRouterOutlet or an IonTab');
|
||||
}
|
||||
if (outlet && hasTab) {
|
||||
throw new Error('IonTabs cannot contain an IonRouterOutlet and an IonTab at the same time');
|
||||
hasTab = true;
|
||||
}
|
||||
|
||||
if (hasTab) {
|
||||
return <IonTabsInner {...this.props}></IonTabsInner>;
|
||||
}
|
||||
this.ionTabContextState.hasRouterOutlet = !!outlet;
|
||||
|
||||
let childProps: any = {
|
||||
...this.ionTabContextState.tabBarProps,
|
||||
};
|
||||
|
||||
/**
|
||||
* TODO(ROU-11051)
|
||||
*
|
||||
* There is no error handling for the case where there
|
||||
* is no associated Route for the given IonTabButton.
|
||||
*
|
||||
* More investigation is needed to determine how to
|
||||
* handle this to prevent any overwriting of the
|
||||
* IonTabButton's onClick handler and how the routing
|
||||
* is handled.
|
||||
* Only pass these props
|
||||
* down from IonTabs to IonTabBar
|
||||
* if they are defined, otherwise
|
||||
* if you have a handler set on
|
||||
* IonTabBar it will be overridden.
|
||||
*/
|
||||
if (onIonTabsDidChange !== undefined) {
|
||||
childProps = {
|
||||
...childProps,
|
||||
onIonTabsDidChange,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<IonTabsContext.Provider value={this.ionTabContextState}>
|
||||
{this.context.hasIonicRouter() ? (
|
||||
<PageManager className={className ? `${className}` : ''} routeInfo={this.context.routeInfo} {...props}>
|
||||
{this.renderTabsInner(children, outlet)}
|
||||
</PageManager>
|
||||
) : (
|
||||
this.renderTabsInner(children, outlet)
|
||||
)}
|
||||
</IonTabsContext.Provider>
|
||||
);
|
||||
if (onIonTabsWillChange !== undefined) {
|
||||
childProps = {
|
||||
...childProps,
|
||||
onIonTabsWillChange,
|
||||
};
|
||||
}
|
||||
|
||||
this.ionTabContextState.tabBarProps = childProps;
|
||||
});
|
||||
|
||||
if (!outlet && !hasTab) {
|
||||
throw new Error('IonTabs must contain an IonRouterOutlet or an IonTab');
|
||||
}
|
||||
if (outlet && hasTab) {
|
||||
throw new Error('IonTabs cannot contain an IonRouterOutlet and an IonTab at the same time');
|
||||
}
|
||||
|
||||
static get contextType() {
|
||||
return NavContext;
|
||||
if (hasTab) {
|
||||
return <IonTabsInner {...this.props}></IonTabsInner>;
|
||||
}
|
||||
})();
|
||||
|
||||
/**
|
||||
* TODO(ROU-11051)
|
||||
*
|
||||
* There is no error handling for the case where there
|
||||
* is no associated Route for the given IonTabButton.
|
||||
*
|
||||
* More investigation is needed to determine how to
|
||||
* handle this to prevent any overwriting of the
|
||||
* IonTabButton's onClick handler and how the routing
|
||||
* is handled.
|
||||
*/
|
||||
|
||||
return (
|
||||
<IonTabsContext.Provider value={this.ionTabContextState}>
|
||||
{this.context.hasIonicRouter() ? (
|
||||
<PageManager className={className ? `${className}` : ''} routeInfo={this.context.routeInfo} {...props}>
|
||||
{this.renderTabsInner(children, outlet)}
|
||||
</PageManager>
|
||||
) : (
|
||||
this.renderTabsInner(children, outlet)
|
||||
)}
|
||||
</IonTabsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
static get contextType() {
|
||||
return NavContext;
|
||||
}
|
||||
}
|
||||
|
||||
14
packages/react/test/apps/react17/src/OutputTarget.test.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { IonButton } from '@ionic/react';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
test('should support onDoubleClick bindings', () => {
|
||||
const mockFn = jest.fn();
|
||||
|
||||
render(<IonButton onDoubleClick={mockFn}>Click me</IonButton>);
|
||||
|
||||
// Simulate a double click on the button
|
||||
fireEvent.dblClick(screen.getByText('Click me'));
|
||||
|
||||
expect(mockFn).toBeCalled();
|
||||
});
|
||||
30
packages/react/test/apps/react19/index.html
Normal file
@@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Ionic App</title>
|
||||
|
||||
<base href="/" />
|
||||
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta name="msapplication-tap-highlight" content="no" />
|
||||
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="shortcut icon" type="image/png" href="/favicon.png" />
|
||||
|
||||
<!-- add to homescreen for ios -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-title" content="Ionic App" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
10603
packages/react/test/apps/react19/package-lock.json
generated
Normal file
49
packages/react/test/apps/react19/package.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "test-app",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@ionic/react": "^8.4.0",
|
||||
"@ionic/react-router": "^8.4.0",
|
||||
"ionicons": "^7.4.0",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-router": "^5.3.4",
|
||||
"react-router-dom": "^5.3.4"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"start": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"test": "vitest",
|
||||
"sync": "sh ./scripts/sync.sh",
|
||||
"cypress": "cypress run --headless --browser chrome",
|
||||
"cypress.open": "cypress open",
|
||||
"e2e": "concurrently \"serve -s dist -l 3000\" \"wait-on http-get://localhost:3000 && npm run cypress\" --kill-others --success first"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/react": "19.0.10",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"@types/react-router": "^5.1.20",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@vitejs/plugin-legacy": "^4.0.2",
|
||||
"@vitejs/plugin-react": "^4.0.1",
|
||||
"concurrently": "^6.3.0",
|
||||
"cypress": "^13.2.0",
|
||||
"eslint": "^8.35.0",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"jsdom": "^22.1.0",
|
||||
"serve": "^14.0.1",
|
||||
"typescript": "^5.1.6",
|
||||
"vite": "^4.3.9",
|
||||
"vitest": "^0.32.2",
|
||||
"wait-on": "^6.0.0"
|
||||
},
|
||||
"description": "An Ionic project",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
}
|
||||
}
|
||||
11
packages/react/test/apps/react19/src/index.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
const container = document.getElementById('root');
|
||||
const root = createRoot(container!);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
1
packages/react/test/apps/react19/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
21
packages/react/test/apps/react19/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
9
packages/react/test/apps/react19/tsconfig.node.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
19
packages/react/test/apps/react19/vite.config.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import legacy from '@vitejs/plugin-legacy'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
legacy()
|
||||
],
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
setupFiles: './src/setupTests.ts',
|
||||
},
|
||||
server: {
|
||||
port: 3000
|
||||
}
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Route } from 'react-router-dom';
|
||||
import { IonApp, IonRouterOutlet, setupIonicReact } from '@ionic/react';
|
||||
import { IonReactRouter } from '@ionic/react-router';
|
||||
import React from 'react';
|
||||
import { Route } from 'react-router-dom';
|
||||
|
||||
/* Core CSS required for Ionic components to work properly */
|
||||
import '@ionic/react/css/core.css';
|
||||
@@ -21,19 +21,19 @@ import '@ionic/react/css/display.css';
|
||||
|
||||
/* Theme variables */
|
||||
import './theme/variables.css';
|
||||
import Icons from './pages/Icons';
|
||||
import Main from './pages/Main';
|
||||
import OverlayHooks from './pages/overlay-hooks/OverlayHooks';
|
||||
import OverlayComponents from './pages/overlay-components/OverlayComponents';
|
||||
import KeepContentsMounted from './pages/overlay-components/KeepContentsMounted';
|
||||
import Tabs from './pages/Tabs';
|
||||
import TabsBasic from './pages/TabsBasic';
|
||||
import Icons from './pages/Icons';
|
||||
import NavComponent from './pages/navigation/NavComponent';
|
||||
import IonModalConditionalSibling from './pages/overlay-components/IonModalConditionalSibling';
|
||||
import IonModalConditional from './pages/overlay-components/IonModalConditional';
|
||||
import IonModalConditionalSibling from './pages/overlay-components/IonModalConditionalSibling';
|
||||
import IonModalDatetimeButton from './pages/overlay-components/IonModalDatetimeButton';
|
||||
import IonPopoverNested from './pages/overlay-components/IonPopoverNested';
|
||||
import IonModalMultipleChildren from './pages/overlay-components/IonModalMultipleChildren';
|
||||
import IonPopoverNested from './pages/overlay-components/IonPopoverNested';
|
||||
import KeepContentsMounted from './pages/overlay-components/KeepContentsMounted';
|
||||
import OverlayComponents from './pages/overlay-components/OverlayComponents';
|
||||
import OverlayHooks from './pages/overlay-hooks/OverlayHooks';
|
||||
|
||||
setupIonicReact();
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { IonButton } from '@ionic/react';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { vi, test, expect } from 'vitest';
|
||||
|
||||
test('should support onDoubleClick bindings', () => {
|
||||
const mockFn = jest.fn();
|
||||
const mockFn = vi.fn();
|
||||
|
||||
render(<IonButton onDoubleClick={mockFn}>Click me</IonButton>);
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ const PageThree = ({ nav }: { nav: React.MutableRefObject<HTMLIonNavElement> })
|
||||
};
|
||||
|
||||
const NavComponent: React.FC = () => {
|
||||
const ref = useRef<any>();
|
||||
const ref = useRef<any>(null);
|
||||
return (
|
||||
<IonPage>
|
||||
<IonNav
|
||||
|
||||
@@ -7,7 +7,11 @@
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["dom", "es2015"],
|
||||
"lib": [
|
||||
"dom",
|
||||
"es2020",
|
||||
"dom.iterable"
|
||||
],
|
||||
"importHelpers": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
@@ -20,8 +24,8 @@
|
||||
"removeComments": false,
|
||||
"inlineSources": true,
|
||||
"sourceMap": true,
|
||||
"jsx": "react",
|
||||
"target": "es2017"
|
||||
"jsx": "react-jsx",
|
||||
"target": "es2020",
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"compileOnSave": false,
|
||||
|
||||
@@ -7,9 +7,10 @@
|
||||
"declaration": true,
|
||||
"experimentalDecorators": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"importHelpers": true,
|
||||
"lib": [
|
||||
"dom",
|
||||
"es2017"
|
||||
"es2017",
|
||||
],
|
||||
"module": "es2015",
|
||||
"moduleResolution": "node",
|
||||
|
||||