diff --git a/CHANGELOG.md b/CHANGELOG.md index a6e49112c02..fe2edcce966 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,24 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [8.4.0](https://github.com/ionic-team/ionic-framework/compare/v8.3.4...v8.4.0) (2024-11-04) + + +### Bug Fixes + +* **alert:** use correct heading structure for subHeader when header exists ([#29964](https://github.com/ionic-team/ionic-framework/issues/29964)) ([0fdcb32](https://github.com/ionic-team/ionic-framework/commit/0fdcb32ce0f99b284b314f79f7d0b071bc37faec)) + + +### Features + +* **menu:** pass role to ionWillClose and ionDidClose events ([#29954](https://github.com/ionic-team/ionic-framework/issues/29954)) ([ee2fa19](https://github.com/ionic-team/ionic-framework/commit/ee2fa19a1e9f09d492c7c08340d95ba6a56ebb2b)) +* **segment-view:** adds support for new `ion-segment-view` component ([#29969](https://github.com/ionic-team/ionic-framework/issues/29969)) ([89508fb](https://github.com/ionic-team/ionic-framework/commit/89508fb89172900b1d11cc3fc18883f57a7fbab6)) +* **select:** add `modal` as interface ([#29972](https://github.com/ionic-team/ionic-framework/issues/29972)) ([3628ea8](https://github.com/ionic-team/ionic-framework/commit/3628ea875a66a717783de5e0a4df440872339040)) + + + + + ## [8.3.4](https://github.com/ionic-team/ionic-framework/compare/v8.3.3...v8.3.4) (2024-10-30) diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index 9f8cea162cd..07089bd9600 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -3,6 +3,24 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [8.4.0](https://github.com/ionic-team/ionic-framework/compare/v8.3.4...v8.4.0) (2024-11-04) + + +### Bug Fixes + +* **alert:** use correct heading structure for subHeader when header exists ([#29964](https://github.com/ionic-team/ionic-framework/issues/29964)) ([0fdcb32](https://github.com/ionic-team/ionic-framework/commit/0fdcb32ce0f99b284b314f79f7d0b071bc37faec)) + + +### Features + +* **menu:** pass role to ionWillClose and ionDidClose events ([#29954](https://github.com/ionic-team/ionic-framework/issues/29954)) ([ee2fa19](https://github.com/ionic-team/ionic-framework/commit/ee2fa19a1e9f09d492c7c08340d95ba6a56ebb2b)) +* **segment-view:** adds support for new `ion-segment-view` component ([#29969](https://github.com/ionic-team/ionic-framework/issues/29969)) ([89508fb](https://github.com/ionic-team/ionic-framework/commit/89508fb89172900b1d11cc3fc18883f57a7fbab6)) +* **select:** add `modal` as interface ([#29972](https://github.com/ionic-team/ionic-framework/issues/29972)) ([3628ea8](https://github.com/ionic-team/ionic-framework/commit/3628ea875a66a717783de5e0a4df440872339040)) + + + + + ## [8.3.4](https://github.com/ionic-team/ionic-framework/compare/v8.3.3...v8.3.4) (2024-10-30) diff --git a/core/api.txt b/core/api.txt index bd0490e45fc..67d49417556 100644 --- a/core/api.txt +++ b/core/api.txt @@ -1000,15 +1000,15 @@ ion-menu,prop,menuId,string | undefined,undefined,false,true ion-menu,prop,side,"end" | "start",'start',false,true ion-menu,prop,swipeGesture,boolean,true,false,false ion-menu,prop,type,"overlay" | "push" | "reveal" | undefined,undefined,false,false -ion-menu,method,close,close(animated?: boolean) => Promise +ion-menu,method,close,close(animated?: boolean, role?: string) => Promise ion-menu,method,isActive,isActive() => Promise ion-menu,method,isOpen,isOpen() => Promise ion-menu,method,open,open(animated?: boolean) => Promise -ion-menu,method,setOpen,setOpen(shouldOpen: boolean, animated?: boolean) => Promise +ion-menu,method,setOpen,setOpen(shouldOpen: boolean, animated?: boolean, role?: string) => Promise ion-menu,method,toggle,toggle(animated?: boolean) => Promise -ion-menu,event,ionDidClose,void,true +ion-menu,event,ionDidClose,MenuCloseEventDetail,true ion-menu,event,ionDidOpen,void,true -ion-menu,event,ionWillClose,void,true +ion-menu,event,ionWillClose,MenuCloseEventDetail,true ion-menu,event,ionWillOpen,void,true ion-menu,css-prop,--background,ios ion-menu,css-prop,--background,md @@ -1542,6 +1542,7 @@ ion-segment,css-prop,--background,ios ion-segment,css-prop,--background,md ion-segment-button,shadow +ion-segment-button,prop,contentId,string | undefined,undefined,false,true ion-segment-button,prop,disabled,boolean,false,false,false ion-segment-button,prop,layout,"icon-bottom" | "icon-end" | "icon-hide" | "icon-start" | "icon-top" | "label-hide" | undefined,'icon-top',false,false ion-segment-button,prop,mode,"ios" | "md",undefined,false,false @@ -1607,6 +1608,12 @@ ion-segment-button,part,indicator ion-segment-button,part,indicator-background ion-segment-button,part,native +ion-segment-content,shadow + +ion-segment-view,shadow +ion-segment-view,prop,disabled,boolean,false,false,false +ion-segment-view,event,ionSegmentViewScroll,SegmentViewScrollEvent,true + ion-select,shadow ion-select,prop,cancelText,string,'Cancel',false,false ion-select,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record | undefined,undefined,false,true @@ -1614,7 +1621,7 @@ ion-select,prop,compareWith,((currentValue: any, compareValue: any) => boolean) ion-select,prop,disabled,boolean,false,false,false ion-select,prop,expandedIcon,string | undefined,undefined,false,false ion-select,prop,fill,"outline" | "solid" | undefined,undefined,false,false -ion-select,prop,interface,"action-sheet" | "alert" | "popover",'alert',false,false +ion-select,prop,interface,"action-sheet" | "alert" | "modal" | "popover",'alert',false,false ion-select,prop,interfaceOptions,any,{},false,false ion-select,prop,justify,"end" | "space-between" | "start" | undefined,undefined,false,false ion-select,prop,label,string | undefined,undefined,false,false @@ -1672,6 +1679,11 @@ ion-select,part,label ion-select,part,placeholder ion-select,part,text +ion-select-modal,scoped +ion-select-modal,prop,header,string | undefined,undefined,false,false +ion-select-modal,prop,multiple,boolean | undefined,undefined,false,false +ion-select-modal,prop,options,SelectModalOption[],[],false,false + ion-select-option,shadow ion-select-option,prop,disabled,boolean,false,false,false ion-select-option,prop,value,any,undefined,false,false diff --git a/core/package-lock.json b/core/package-lock.json index 3a4608f31e9..7105341c21f 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -1,12 +1,12 @@ { "name": "@ionic/core", - "version": "8.3.4", + "version": "8.4.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@ionic/core", - "version": "8.3.4", + "version": "8.4.0", "license": "MIT", "dependencies": { "@stencil/core": "4.20.0", diff --git a/core/package.json b/core/package.json index cc99b374d65..2d92d1411e4 100644 --- a/core/package.json +++ b/core/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/core", - "version": "8.3.4", + "version": "8.4.0", "description": "Base components for Ionic", "keywords": [ "ionic", diff --git a/core/src/components.d.ts b/core/src/components.d.ts index db9530b3404..2e5163329dd 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -18,7 +18,7 @@ import { ScrollBaseDetail, ScrollDetail } from "./components/content/content-int import { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimeHourCycle, DatetimePresentation, FormatOptions, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface"; import { SpinnerTypes } from "./components/spinner/spinner-configs"; import { InputChangeEventDetail, InputInputEventDetail } from "./components/input/input-interface"; -import { MenuChangeEventDetail, MenuType, Side } from "./components/menu/menu-interface"; +import { MenuChangeEventDetail, MenuCloseEventDetail, MenuType, Side } from "./components/menu/menu-interface"; import { ModalBreakpointChangeEventDetail, ModalHandleBehavior } from "./components/modal/modal-interface"; import { NavComponent, NavComponentWithProps, NavOptions, RouterOutletOptions, SwipeGestureHandler, TransitionDoneFn, TransitionInstruction } from "./components/nav/nav-interface"; import { ViewController } from "./components/nav/view-controller"; @@ -34,7 +34,9 @@ import { NavigationHookCallback } from "./components/route/route-interface"; import { SearchbarChangeEventDetail, SearchbarInputEventDetail } from "./components/searchbar/searchbar-interface"; import { SegmentChangeEventDetail, SegmentValue } from "./components/segment/segment-interface"; import { SegmentButtonLayout } from "./components/segment-button/segment-button-interface"; +import { SegmentViewScrollEvent } from "./components/segment-view/segment-view-interface"; import { SelectChangeEventDetail, SelectCompareFn, SelectInterface } from "./components/select/select-interface"; +import { SelectModalOption } from "./components/select-modal/select-modal-interface"; import { SelectPopoverOption } from "./components/select-popover/select-popover-interface"; import { TabBarChangedEventDetail, TabButtonClickEventDetail, TabButtonLayout } from "./components/tab-bar/tab-bar-interface"; import { TextareaChangeEventDetail, TextareaInputEventDetail } from "./components/textarea/textarea-interface"; @@ -53,7 +55,7 @@ export { ScrollBaseDetail, ScrollDetail } from "./components/content/content-int export { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimeHourCycle, DatetimePresentation, FormatOptions, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface"; export { SpinnerTypes } from "./components/spinner/spinner-configs"; export { InputChangeEventDetail, InputInputEventDetail } from "./components/input/input-interface"; -export { MenuChangeEventDetail, MenuType, Side } from "./components/menu/menu-interface"; +export { MenuChangeEventDetail, MenuCloseEventDetail, MenuType, Side } from "./components/menu/menu-interface"; export { ModalBreakpointChangeEventDetail, ModalHandleBehavior } from "./components/modal/modal-interface"; export { NavComponent, NavComponentWithProps, NavOptions, RouterOutletOptions, SwipeGestureHandler, TransitionDoneFn, TransitionInstruction } from "./components/nav/nav-interface"; export { ViewController } from "./components/nav/view-controller"; @@ -69,7 +71,9 @@ export { NavigationHookCallback } from "./components/route/route-interface"; export { SearchbarChangeEventDetail, SearchbarInputEventDetail } from "./components/searchbar/searchbar-interface"; export { SegmentChangeEventDetail, SegmentValue } from "./components/segment/segment-interface"; export { SegmentButtonLayout } from "./components/segment-button/segment-button-interface"; +export { SegmentViewScrollEvent } from "./components/segment-view/segment-view-interface"; export { SelectChangeEventDetail, SelectCompareFn, SelectInterface } from "./components/select/select-interface"; +export { SelectModalOption } from "./components/select-modal/select-modal-interface"; export { SelectPopoverOption } from "./components/select-popover/select-popover-interface"; export { TabBarChangedEventDetail, TabButtonClickEventDetail, TabButtonLayout } from "./components/tab-bar/tab-bar-interface"; export { TextareaChangeEventDetail, TextareaInputEventDetail } from "./components/textarea/textarea-interface"; @@ -639,6 +643,7 @@ export namespace Components { * The name of the control, which is submitted with the form data. */ "name": string; + "setFocus": () => Promise; /** * 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 ``, it's only used when the checkbox participates in a native `
`. */ @@ -1596,7 +1601,7 @@ export namespace Components { /** * Closes the menu. If the menu is already closed or it can't be closed, it returns `false`. */ - "close": (animated?: boolean) => Promise; + "close": (animated?: boolean, role?: string) => Promise; /** * The `id` of the main content. When using a router this is typically `ion-router-outlet`. When not using a router, this is typically your main view's `ion-content`. This is not the id of the `ion-content` inside of your `ion-menu`. */ @@ -1628,7 +1633,7 @@ export namespace Components { /** * Opens or closes the button. If the operation can't be completed successfully, it returns `false`. */ - "setOpen": (shouldOpen: boolean, animated?: boolean) => Promise; + "setOpen": (shouldOpen: boolean, animated?: boolean, role?: string) => Promise; /** * Which side of the view the menu should be placed. */ @@ -2279,7 +2284,7 @@ export namespace Components { */ "name": string; "setButtonTabindex": (value: number) => Promise; - "setFocus": (ev: globalThis.Event) => Promise; + "setFocus": (ev?: globalThis.Event) => Promise; /** * the value of the radio. */ @@ -2693,6 +2698,10 @@ export namespace Components { "value"?: SegmentValue; } interface IonSegmentButton { + /** + * The `id` of the segment content. + */ + "contentId"?: string; /** * If `true`, the user cannot interact with the segment button. */ @@ -2715,6 +2724,19 @@ export namespace Components { */ "value": SegmentValue; } + interface IonSegmentContent { + } + interface IonSegmentView { + /** + * If `true`, the segment view cannot be interacted with. + */ + "disabled": boolean; + /** + * @param id : The id of the segment content to display. + * @param smoothScroll : Whether to animate the scroll transition. + */ + "setContent": (id: string, smoothScroll?: boolean) => Promise; + } interface IonSelect { /** * The text to display on the cancel button. @@ -2741,11 +2763,11 @@ export namespace Components { */ "fill"?: 'outline' | 'solid'; /** - * The interface the select should use: `action-sheet`, `popover` or `alert`. + * The interface the select should use: `action-sheet`, `popover`, `alert`, or `modal`. */ "interface": SelectInterface; /** - * Any additional options that the `alert`, `action-sheet` or `popover` interface can take. See the [ion-alert docs](./alert), the [ion-action-sheet docs](./action-sheet) and the [ion-popover docs](./popover) for the create options for each interface. Note: `interfaceOptions` will not override `inputs` or `buttons` with the `alert` interface. + * Any additional options that the `alert`, `action-sheet` or `popover` interface can take. See the [ion-alert docs](./alert), the [ion-action-sheet docs](./action-sheet), the [ion-popover docs](./popover), and the [ion-modal docs](./modal) for the create options for each interface. Note: `interfaceOptions` will not override `inputs` or `buttons` with the `alert` interface. */ "interfaceOptions": any; /** @@ -2802,6 +2824,11 @@ export namespace Components { */ "value"?: any | null; } + interface IonSelectModal { + "header"?: string; + "multiple"?: boolean; + "options": SelectModalOption[]; + } interface IonSelectOption { /** * If `true`, the user cannot interact with the select option. This property does not apply when `interface="action-sheet"` as `ion-action-sheet` does not allow for disabled buttons. @@ -3416,6 +3443,10 @@ export interface IonSegmentCustomEvent extends CustomEvent { detail: T; target: HTMLIonSegmentElement; } +export interface IonSegmentViewCustomEvent extends CustomEvent { + detail: T; + target: HTMLIonSegmentViewElement; +} export interface IonSelectCustomEvent extends CustomEvent { detail: T; target: HTMLIonSelectElement; @@ -3969,9 +4000,9 @@ declare global { }; interface HTMLIonMenuElementEventMap { "ionWillOpen": void; - "ionWillClose": void; + "ionWillClose": MenuCloseEventDetail; "ionDidOpen": void; - "ionDidClose": void; + "ionDidClose": MenuCloseEventDetail; "ionMenuChange": MenuChangeEventDetail; } interface HTMLIonMenuElement extends Components.IonMenu, HTMLStencilElement { @@ -4412,6 +4443,29 @@ declare global { prototype: HTMLIonSegmentButtonElement; new (): HTMLIonSegmentButtonElement; }; + interface HTMLIonSegmentContentElement extends Components.IonSegmentContent, HTMLStencilElement { + } + var HTMLIonSegmentContentElement: { + prototype: HTMLIonSegmentContentElement; + new (): HTMLIonSegmentContentElement; + }; + interface HTMLIonSegmentViewElementEventMap { + "ionSegmentViewScroll": SegmentViewScrollEvent; + } + interface HTMLIonSegmentViewElement extends Components.IonSegmentView, HTMLStencilElement { + addEventListener(type: K, listener: (this: HTMLIonSegmentViewElement, ev: IonSegmentViewCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; + removeEventListener(type: K, listener: (this: HTMLIonSegmentViewElement, ev: IonSegmentViewCustomEvent) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; + } + var HTMLIonSegmentViewElement: { + prototype: HTMLIonSegmentViewElement; + new (): HTMLIonSegmentViewElement; + }; interface HTMLIonSelectElementEventMap { "ionChange": SelectChangeEventDetail; "ionCancel": void; @@ -4434,6 +4488,12 @@ declare global { prototype: HTMLIonSelectElement; new (): HTMLIonSelectElement; }; + interface HTMLIonSelectModalElement extends Components.IonSelectModal, HTMLStencilElement { + } + var HTMLIonSelectModalElement: { + prototype: HTMLIonSelectModalElement; + new (): HTMLIonSelectModalElement; + }; interface HTMLIonSelectOptionElement extends Components.IonSelectOption, HTMLStencilElement { } var HTMLIonSelectOptionElement: { @@ -4721,7 +4781,10 @@ declare global { "ion-searchbar": HTMLIonSearchbarElement; "ion-segment": HTMLIonSegmentElement; "ion-segment-button": HTMLIonSegmentButtonElement; + "ion-segment-content": HTMLIonSegmentContentElement; + "ion-segment-view": HTMLIonSegmentViewElement; "ion-select": HTMLIonSelectElement; + "ion-select-modal": HTMLIonSelectModalElement; "ion-select-option": HTMLIonSelectOptionElement; "ion-select-popover": HTMLIonSelectPopoverElement; "ion-skeleton-text": HTMLIonSkeletonTextElement; @@ -6364,7 +6427,7 @@ declare namespace LocalJSX { /** * Emitted when the menu is closed. */ - "onIonDidClose"?: (event: IonMenuCustomEvent) => void; + "onIonDidClose"?: (event: IonMenuCustomEvent) => void; /** * Emitted when the menu is open. */ @@ -6376,7 +6439,7 @@ declare namespace LocalJSX { /** * Emitted when the menu is about to be closed. */ - "onIonWillClose"?: (event: IonMenuCustomEvent) => void; + "onIonWillClose"?: (event: IonMenuCustomEvent) => void; /** * Emitted when the menu is about to be opened. */ @@ -7450,6 +7513,10 @@ declare namespace LocalJSX { "value"?: SegmentValue; } interface IonSegmentButton { + /** + * The `id` of the segment content. + */ + "contentId"?: string; /** * If `true`, the user cannot interact with the segment button. */ @@ -7471,6 +7538,18 @@ declare namespace LocalJSX { */ "value"?: SegmentValue; } + interface IonSegmentContent { + } + interface IonSegmentView { + /** + * If `true`, the segment view cannot be interacted with. + */ + "disabled"?: boolean; + /** + * Emitted when the segment view is scrolled. + */ + "onIonSegmentViewScroll"?: (event: IonSegmentViewCustomEvent) => void; + } interface IonSelect { /** * The text to display on the cancel button. @@ -7497,11 +7576,11 @@ declare namespace LocalJSX { */ "fill"?: 'outline' | 'solid'; /** - * The interface the select should use: `action-sheet`, `popover` or `alert`. + * The interface the select should use: `action-sheet`, `popover`, `alert`, or `modal`. */ "interface"?: SelectInterface; /** - * Any additional options that the `alert`, `action-sheet` or `popover` interface can take. See the [ion-alert docs](./alert), the [ion-action-sheet docs](./action-sheet) and the [ion-popover docs](./popover) for the create options for each interface. Note: `interfaceOptions` will not override `inputs` or `buttons` with the `alert` interface. + * Any additional options that the `alert`, `action-sheet` or `popover` interface can take. See the [ion-alert docs](./alert), the [ion-action-sheet docs](./action-sheet), the [ion-popover docs](./popover), and the [ion-modal docs](./modal) for the create options for each interface. Note: `interfaceOptions` will not override `inputs` or `buttons` with the `alert` interface. */ "interfaceOptions"?: any; /** @@ -7577,6 +7656,11 @@ declare namespace LocalJSX { */ "value"?: any | null; } + interface IonSelectModal { + "header"?: string; + "multiple"?: boolean; + "options"?: SelectModalOption[]; + } interface IonSelectOption { /** * If `true`, the user cannot interact with the select option. This property does not apply when `interface="action-sheet"` as `ion-action-sheet` does not allow for disabled buttons. @@ -8162,7 +8246,10 @@ declare namespace LocalJSX { "ion-searchbar": IonSearchbar; "ion-segment": IonSegment; "ion-segment-button": IonSegmentButton; + "ion-segment-content": IonSegmentContent; + "ion-segment-view": IonSegmentView; "ion-select": IonSelect; + "ion-select-modal": IonSelectModal; "ion-select-option": IonSelectOption; "ion-select-popover": IonSelectPopover; "ion-skeleton-text": IonSkeletonText; @@ -8261,7 +8348,10 @@ declare module "@stencil/core" { "ion-searchbar": LocalJSX.IonSearchbar & JSXBase.HTMLAttributes; "ion-segment": LocalJSX.IonSegment & JSXBase.HTMLAttributes; "ion-segment-button": LocalJSX.IonSegmentButton & JSXBase.HTMLAttributes; + "ion-segment-content": LocalJSX.IonSegmentContent & JSXBase.HTMLAttributes; + "ion-segment-view": LocalJSX.IonSegmentView & JSXBase.HTMLAttributes; "ion-select": LocalJSX.IonSelect & JSXBase.HTMLAttributes; + "ion-select-modal": LocalJSX.IonSelectModal & JSXBase.HTMLAttributes; "ion-select-option": LocalJSX.IonSelectOption & JSXBase.HTMLAttributes; "ion-select-popover": LocalJSX.IonSelectPopover & JSXBase.HTMLAttributes; "ion-skeleton-text": LocalJSX.IonSkeletonText & JSXBase.HTMLAttributes; diff --git a/core/src/components/alert/alert.tsx b/core/src/components/alert/alert.tsx index 06348e8c065..ee2e07244ca 100644 --- a/core/src/components/alert/alert.tsx +++ b/core/src/components/alert/alert.tsx @@ -730,10 +730,12 @@ export class Alert implements ComponentInterface, OverlayInterface { const role = this.inputs.length > 0 || this.buttons.length > 0 ? 'alertdialog' : 'alert'; /** - * If the header is defined, use that. Otherwise, fall back to the subHeader. - * If neither is defined, don't set aria-labelledby. + * Use both the header and subHeader ids if they are defined. + * If only the header is defined, use the header id. + * If only the subHeader is defined, use the subHeader id. + * If neither are defined, do not set aria-labelledby. */ - const ariaLabelledBy = header ? hdrId : subHeader ? subHdrId : null; + const ariaLabelledBy = header && subHeader ? `${hdrId} ${subHdrId}` : header ? hdrId : subHeader ? subHdrId : null; return ( )} - {subHeader && ( + {/* If no header exists, subHeader should be the highest heading level, h2 */} + {subHeader && !header && (

{subHeader}

)} + {/* If a header exists, subHeader should be one level below it, h3 */} + {subHeader && header && ( +

+ {subHeader} +

+ )} {this.renderAlertMessage(msgId)} diff --git a/core/src/components/alert/test/a11y/alert.e2e.ts b/core/src/components/alert/test/a11y/alert.e2e.ts index 12add8b2eca..df6a8b47dbc 100644 --- a/core/src/components/alert/test/a11y/alert.e2e.ts +++ b/core/src/components/alert/test/a11y/alert.e2e.ts @@ -17,6 +17,27 @@ const testAria = async ( const alert = page.locator('ion-alert'); + const header = alert.locator('.alert-title'); + const subHeader = alert.locator('.alert-sub-title'); + + // If a header exists, it should be an h2 element + if ((await header.count()) > 0) { + const headerTagName = await header.evaluate((el) => el.tagName); + expect(headerTagName).toBe('H2'); + } + + // If a header and subHeader exist, the subHeader should be an h3 element + if ((await header.count()) > 0 && (await subHeader.count()) > 0) { + const subHeaderTagName = await subHeader.evaluate((el) => el.tagName); + expect(subHeaderTagName).toBe('H3'); + } + + // If a subHeader exists without a header, the subHeader should be an h2 element + if ((await header.count()) === 0 && (await subHeader.count()) > 0) { + const subHeaderTagName = await subHeader.evaluate((el) => el.tagName); + expect(subHeaderTagName).toBe('H2'); + } + /** * expect().toHaveAttribute() can't check for a null value, so grab and check * the values manually instead. @@ -124,16 +145,24 @@ configs({ directions: ['ltr'] }).forEach(({ config, title }) => { await page.goto(`/src/components/alert/test/a11y`, config); }); - test('should have aria-labelledby when header is set', async ({ page }) => { - await testAria(page, 'noMessage', 'alert-1-hdr', null); + test('should have aria-labelledby set to both when header and subHeader are set', async ({ page }) => { + await testAria(page, 'bothHeadersOnly', 'alert-1-hdr alert-1-sub-hdr', null); + }); + + test('should have aria-labelledby set when only header is set', async ({ page }) => { + await testAria(page, 'headerOnly', 'alert-1-hdr', null); + }); + + test('should fall back to subHeader for aria-labelledby if header is not defined', async ({ page }) => { + await testAria(page, 'subHeaderOnly', 'alert-1-sub-hdr', null); }); test('should have aria-describedby when message is set', async ({ page }) => { await testAria(page, 'noHeaders', null, 'alert-1-msg'); }); - test('should fall back to subHeader for aria-labelledby if header is not defined', async ({ page }) => { - await testAria(page, 'subHeaderOnly', 'alert-1-sub-hdr', 'alert-1-msg'); + test('should have aria-labelledby and aria-describedby when headers and message are set', async ({ page }) => { + await testAria(page, 'headersAndMessage', 'alert-1-hdr alert-1-sub-hdr', 'alert-1-msg'); }); test('should allow for manually specifying aria attributes', async ({ page }) => { @@ -279,7 +308,10 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => { await expect(page).toHaveScreenshot(screenshot(`alert-radio-scale`)); }); - test('should scale text on larger font sizes with text fields', async ({ page }) => { + test('should scale text on larger font sizes with text fields', async ({ page, skip }) => { + // TODO(ROU-8158): unskip this test when a solution is found + skip.browser('chromium', 'Rendering is flaky in Chrome.'); + await page.setContent( ` + + + + + + + Segment View - Basic + + + + + + + No + + + Value + + + + No + Value + + + + + Paid + + + Free + + + Top + + + + + Free + Top + + + + + Orange + + + Banana + + + Pear + + + Peach + + + Grape + + + Mango + + + Apple + + + Strawberry + + + Cherry + + + + Orange + Banana + Pear + Peach + Grape + Mango + Apple + Strawberry + Cherry + + + + + + + + + + Footer + + + + + + + diff --git a/core/src/components/segment-view/test/basic/segment-view.e2e.ts b/core/src/components/segment-view/test/basic/segment-view.e2e.ts new file mode 100644 index 00000000000..447a60be309 --- /dev/null +++ b/core/src/components/segment-view/test/basic/segment-view.e2e.ts @@ -0,0 +1,173 @@ +import { expect } from '@playwright/test'; +import { configs, test } from '@utils/test/playwright'; + +/** + * This behavior does not vary across modes/directions + */ +configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { + test.describe(title('segment-view: basic'), () => { + test('should show the first content with no initial value', async ({ page }) => { + await page.setContent( + ` + + + Paid + + + Free + + + Top + + + + + Free + Top + + `, + config + ); + + const segmentContent = page.locator('ion-segment-content[id="paid"]'); + await expect(segmentContent).toBeInViewport(); + }); + + test('should show the content matching the initial value', async ({ page }) => { + await page.setContent( + ` + + + Paid + + + Free + + + Top + + + + + Free + Top + + `, + config + ); + + const segmentContent = page.locator('ion-segment-content[id="free"]'); + await expect(segmentContent).toBeInViewport(); + }); + + test('should update the content when changing the value by clicking a segment button', async ({ page }) => { + await page.setContent( + ` + + + Paid + + + Free + + + Top + + + + + Free + Top + + `, + config + ); + + await page.locator('ion-segment-button[value="top"]').click(); + + const segmentContent = page.locator('ion-segment-content[id="top"]'); + await expect(segmentContent).toBeInViewport(); + }); + }); + + test('should set correct segment button as checked when changing the value by scrolling the segment content', async ({ + page, + }) => { + await page.setContent( + ` + + + Paid + + + Free + + + Top + + + + + Free + Top + + `, + config + ); + + await page + .locator('ion-segment-view') + .evaluate( + (segmentView: HTMLIonSegmentViewElement) => !segmentView.classList.contains('segment-view-scroll-disabled') + ); + + await page.waitForChanges(); + + await page.locator('ion-segment-content[id="top"]').scrollIntoViewIfNeeded(); + + const segmentButton = page.locator('ion-segment-button[value="top"]'); + await expect(segmentButton).toHaveClass(/segment-button-checked/); + }); + + test('should set correct segment button as checked and show correct content when programmatically setting the segment vale', async ({ + page, + }) => { + await page.setContent( + ` + + + Paid + + + Free + + + Top + + + + + Free + Top + + `, + config + ); + + await page + .locator('ion-segment-view') + .evaluate( + (segmentView: HTMLIonSegmentViewElement) => !segmentView.classList.contains('segment-view-scroll-disabled') + ); + + await page.waitForChanges(); + + await page.locator('ion-segment').evaluate((segment: HTMLIonSegmentElement) => (segment.value = 'top')); + + const segmentButton = page.locator('ion-segment-button[value="top"]'); + await expect(segmentButton).toHaveClass(/segment-button-checked/); + + const segmentContent = page.locator('ion-segment-content[id="top"]'); + await expect(segmentContent).toBeInViewport(); + }); +}); diff --git a/core/src/components/segment-view/test/disabled/index.html b/core/src/components/segment-view/test/disabled/index.html new file mode 100644 index 00000000000..d19722de6dc --- /dev/null +++ b/core/src/components/segment-view/test/disabled/index.html @@ -0,0 +1,103 @@ + + + + + Segment View - Disabled + + + + + + + + + + + + + + + Segment View - Disabled + + + + + + + All + + + Favorites + + + + All + Favorites + + + + + Paid + + + Free + + + Top + + + + + Free + Top + + + + + Bookmarks + + + Reading List + + + Shared Links + + + + Bookmarks + Reading List + Shared Links + + + + + diff --git a/core/src/components/segment-view/test/disabled/segment-view.e2e.ts b/core/src/components/segment-view/test/disabled/segment-view.e2e.ts new file mode 100644 index 00000000000..c7dead8943f --- /dev/null +++ b/core/src/components/segment-view/test/disabled/segment-view.e2e.ts @@ -0,0 +1,49 @@ +import { expect } from '@playwright/test'; +import { configs, test } from '@utils/test/playwright'; + +/** + * This behavior does not vary across directions + */ +configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => { + test.describe(title('segment-view: disabled'), () => { + test('should not have visual regressions', async ({ page }) => { + await page.goto('/src/components/segment-view/test/disabled', config); + + await expect(page).toHaveScreenshot(screenshot(`segment-view-disabled`)); + }); + }); +}); + +/** + * This behavior does not vary across modes/directions + */ +configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { + test.describe(title('segment-view: disabled'), () => { + test('should keep button enabled even when disabled prop is set', async ({ page }) => { + await page.setContent( + ` + + + Paid + + + Free + + + Top + + + + + Free + Top + + `, + config + ); + + const segmentButton = page.locator('ion-segment-button[value="free"]'); + await expect(segmentButton).not.toHaveClass(/segment-button-disabled/); + }); + }); +}); diff --git a/core/src/components/segment-view/test/disabled/segment-view.e2e.ts-snapshots/segment-view-disabled-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/segment-view/test/disabled/segment-view.e2e.ts-snapshots/segment-view-disabled-ios-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..58fa6b0d0ab Binary files /dev/null and b/core/src/components/segment-view/test/disabled/segment-view.e2e.ts-snapshots/segment-view-disabled-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/segment-view/test/disabled/segment-view.e2e.ts-snapshots/segment-view-disabled-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/segment-view/test/disabled/segment-view.e2e.ts-snapshots/segment-view-disabled-ios-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..6ad473ada44 Binary files /dev/null and b/core/src/components/segment-view/test/disabled/segment-view.e2e.ts-snapshots/segment-view-disabled-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/segment-view/test/disabled/segment-view.e2e.ts-snapshots/segment-view-disabled-ios-ltr-Mobile-Safari-linux.png b/core/src/components/segment-view/test/disabled/segment-view.e2e.ts-snapshots/segment-view-disabled-ios-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..521946d9f1b Binary files /dev/null and b/core/src/components/segment-view/test/disabled/segment-view.e2e.ts-snapshots/segment-view-disabled-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/segment-view/test/disabled/segment-view.e2e.ts-snapshots/segment-view-disabled-md-ltr-Mobile-Chrome-linux.png b/core/src/components/segment-view/test/disabled/segment-view.e2e.ts-snapshots/segment-view-disabled-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..e192269dd6f Binary files /dev/null and b/core/src/components/segment-view/test/disabled/segment-view.e2e.ts-snapshots/segment-view-disabled-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/segment-view/test/disabled/segment-view.e2e.ts-snapshots/segment-view-disabled-md-ltr-Mobile-Firefox-linux.png b/core/src/components/segment-view/test/disabled/segment-view.e2e.ts-snapshots/segment-view-disabled-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..1d84031d3e4 Binary files /dev/null and b/core/src/components/segment-view/test/disabled/segment-view.e2e.ts-snapshots/segment-view-disabled-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/segment-view/test/disabled/segment-view.e2e.ts-snapshots/segment-view-disabled-md-ltr-Mobile-Safari-linux.png b/core/src/components/segment-view/test/disabled/segment-view.e2e.ts-snapshots/segment-view-disabled-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..697b8ebfe9e Binary files /dev/null and b/core/src/components/segment-view/test/disabled/segment-view.e2e.ts-snapshots/segment-view-disabled-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/segment/segment.tsx b/core/src/components/segment/segment.tsx index 3ddf8645896..074c2586b87 100644 --- a/core/src/components/segment/segment.tsx +++ b/core/src/components/segment/segment.tsx @@ -7,6 +7,7 @@ import { createColorClasses, hostContext } from '@utils/theme'; import { getIonMode } from '../../global/ionic-global'; import type { Color, StyleEventDetail } from '../../interface'; +import type { SegmentViewScrollEvent } from '../segment-view/segment-view-interface'; import type { SegmentChangeEventDetail, SegmentValue } from './segment-interface'; @@ -27,6 +28,16 @@ export class Segment implements ComponentInterface { // Value before the segment is dragged private valueBeforeGesture?: SegmentValue; + private segmentViewEl?: HTMLIonSegmentViewElement | null = null; + private lastNextIndex?: number; + + /** + * Whether to update the segment view, if exists, when the value changes. + * This behavior is enabled by default, but is set false when scrolling content views + * since we don't want to "double scroll" the segment view. + */ + private triggerScrollOnValueChange?: boolean; + @Element() el!: HTMLIonSegmentElement; @State() activated = false; @@ -78,13 +89,41 @@ export class Segment implements ComponentInterface { @Prop({ mutable: true }) value?: SegmentValue; @Watch('value') - protected valueChanged(value: SegmentValue | undefined) { + protected valueChanged(value: SegmentValue | undefined, oldValue?: SegmentValue | undefined) { + // Force a value to exist if we're using a segment view + if (this.segmentViewEl && value === undefined) { + this.value = this.getButtons()[0].value; + return; + } + + if (oldValue !== undefined && value !== undefined) { + const buttons = this.getButtons(); + const previous = buttons.find((button) => button.value === oldValue); + const current = buttons.find((button) => button.value === value); + + if (previous && current) { + if (!this.segmentViewEl) { + this.checkButton(previous, current); + } else if (this.triggerScrollOnValueChange !== false) { + this.updateSegmentView(); + } + } + } else if (value !== undefined && oldValue === undefined && this.segmentViewEl) { + this.updateSegmentView(); + } + /** * `ionSelect` is emitted every time the value changes (internal or external changes). * Used by `ion-segment-button` to determine if the button should be checked. */ this.ionSelect.emit({ value }); - this.scrollActiveButtonIntoView(); + + // The scroll listener should handle scrolling the active button into view as needed + if (!this.segmentViewEl) { + this.scrollActiveButtonIntoView(); + } + + this.triggerScrollOnValueChange = undefined; } /** @@ -118,9 +157,13 @@ export class Segment implements ComponentInterface { disabledChanged() { this.gestureChanged(); - const buttons = this.getButtons(); - for (const button of buttons) { - button.disabled = this.disabled; + if (!this.segmentViewEl) { + const buttons = this.getButtons(); + for (const button of buttons) { + button.disabled = this.disabled; + } + } else { + this.segmentViewEl.disabled = this.disabled; } } @@ -132,6 +175,12 @@ export class Segment implements ComponentInterface { connectedCallback() { this.emitStyle(); + + this.segmentViewEl = this.getSegmentView(); + } + + disconnectedCallback() { + this.segmentViewEl = null; } componentWillLoad() { @@ -170,6 +219,10 @@ export class Segment implements ComponentInterface { if (this.disabled) { this.disabledChanged(); } + + // Update segment view based on the initial value, + // but do not animate the scroll + this.updateSegmentView(false); } onStart(detail: GestureDetail) { @@ -192,6 +245,7 @@ export class Segment implements ComponentInterface { if (value !== undefined) { if (this.valueBeforeGesture !== value) { this.emitValueChange(); + this.updateSegmentView(); } } this.valueBeforeGesture = undefined; @@ -208,7 +262,7 @@ export class Segment implements ComponentInterface { this.ionChange.emit({ value }); } - private getButtons() { + private getButtons(): HTMLIonSegmentButtonElement[] { return Array.from(this.el.querySelectorAll('ion-segment-button')); } @@ -224,11 +278,7 @@ export class Segment implements ComponentInterface { const buttons = this.getButtons(); buttons.forEach((button) => { - if (activated) { - button.classList.add('segment-button-activated'); - } else { - button.classList.remove('segment-button-activated'); - } + button.classList.toggle('segment-button-activated', activated); }); this.activated = activated; } @@ -293,6 +343,8 @@ export class Segment implements ComponentInterface { // Remove the transform to slide the indicator back to the button clicked currentIndicator.style.setProperty('transform', ''); + + this.scrollActiveButtonIntoView(true); }); this.value = current.value; @@ -312,6 +364,74 @@ export class Segment implements ComponentInterface { } } + private getSegmentView() { + const buttons = this.getButtons(); + // Get the first button with a contentId + const firstContentId = buttons.find((button: HTMLIonSegmentButtonElement) => button.contentId); + // Get the segment content with an id matching the button's contentId + const segmentContent = document.querySelector(`ion-segment-content[id="${firstContentId?.contentId}"]`); + // Return the segment view for that matching segment content + return segmentContent?.closest('ion-segment-view'); + } + + @Listen('ionSegmentViewScroll', { target: 'body' }) + handleSegmentViewScroll(ev: CustomEvent) { + const { scrollRatio, isManualScroll } = ev.detail; + + if (!isManualScroll) { + return; + } + + const dispatchedFrom = ev.target as HTMLElement; + const segmentViewEl = this.segmentViewEl as EventTarget; + const segmentEl = this.el; + + // Only update the indicator if the event was dispatched from the correct segment view + if (ev.composedPath().includes(segmentViewEl) || dispatchedFrom?.contains(segmentEl)) { + const buttons = this.getButtons(); + + // If no buttons are found or there is no value set then do nothing + if (!buttons.length) return; + + const index = buttons.findIndex((button) => button.value === this.value); + const current = buttons[index]; + + const nextIndex = Math.round(scrollRatio * (buttons.length - 1)); + + if (this.lastNextIndex === undefined || this.lastNextIndex !== nextIndex) { + this.lastNextIndex = nextIndex; + this.triggerScrollOnValueChange = false; + + this.checkButton(current, buttons[nextIndex]); + this.emitValueChange(); + } + } + } + + /** + * Finds the related segment view and sets its current content + * based on the selected segment button. This method + * should be called on initial load of the segment, + * after the gesture is completed (if dragging between segments) + * and when a segment button is clicked directly. + */ + private updateSegmentView(smoothScroll = true) { + const buttons = this.getButtons(); + const button = buttons.find((btn) => btn.value === this.value); + + // If the button does not have a contentId then there is + // no associated segment view to update + if (!button?.contentId) { + return; + } + + const segmentView = this.segmentViewEl; + + if (segmentView) { + segmentView.setContent(button.contentId, smoothScroll); + } + } + private scrollActiveButtonIntoView(smoothScroll = true) { const { scrollable, value, el } = this; @@ -492,7 +612,13 @@ export class Segment implements ComponentInterface { this.emitValueChange(); } - if (this.scrollable || !this.swipeGesture) { + if (this.segmentViewEl) { + this.updateSegmentView(); + + if (this.scrollable && previous) { + this.checkButton(previous, current); + } + } else if (this.scrollable || !this.swipeGesture) { if (previous) { this.checkButton(previous, current); } else { diff --git a/core/src/components/select-modal/select-modal-interface.ts b/core/src/components/select-modal/select-modal-interface.ts new file mode 100644 index 00000000000..2005400cb82 --- /dev/null +++ b/core/src/components/select-modal/select-modal-interface.ts @@ -0,0 +1,8 @@ +export interface SelectModalOption { + text: string; + value: string; + disabled: boolean; + checked: boolean; + cssClass?: string | string[]; + handler?: (value: any) => boolean | void | { [key: string]: any }; +} diff --git a/core/src/components/select-modal/select-modal.ios.scss b/core/src/components/select-modal/select-modal.ios.scss new file mode 100644 index 00000000000..7e60b693fcf --- /dev/null +++ b/core/src/components/select-modal/select-modal.ios.scss @@ -0,0 +1 @@ +@import "./select-modal"; diff --git a/core/src/components/select-modal/select-modal.md.scss b/core/src/components/select-modal/select-modal.md.scss new file mode 100644 index 00000000000..511c8d786ab --- /dev/null +++ b/core/src/components/select-modal/select-modal.md.scss @@ -0,0 +1,30 @@ +@import "./select-modal"; +@import "../../themes/ionic.mixins.scss"; +@import "../item/item.md.vars"; + +ion-list ion-radio::part(container) { + display: none; +} + +ion-list ion-radio::part(label) { + @include margin(0); +} + +ion-item { + --inner-border-width: 0; +} + +.item-radio-checked { + --background: #{ion-color(primary, base, 0.08)}; + --background-focused: #{ion-color(primary, base)}; + --background-focused-opacity: 0.2; + --background-hover: #{ion-color(primary, base)}; + --background-hover-opacity: 0.12; +} + +.item-checkbox-checked { + --background-activated: #{$item-md-color}; + --background-focused: #{$item-md-color}; + --background-hover: #{$item-md-color}; + --color: #{ion-color(primary, base)}; +} diff --git a/core/src/components/select-modal/select-modal.scss b/core/src/components/select-modal/select-modal.scss new file mode 100644 index 00000000000..683ae23faeb --- /dev/null +++ b/core/src/components/select-modal/select-modal.scss @@ -0,0 +1,3 @@ +:host { + height: 100%; +} diff --git a/core/src/components/select-modal/select-modal.tsx b/core/src/components/select-modal/select-modal.tsx new file mode 100644 index 00000000000..ad927860439 --- /dev/null +++ b/core/src/components/select-modal/select-modal.tsx @@ -0,0 +1,161 @@ +import { getIonMode } from '@global/ionic-global'; +import type { ComponentInterface } from '@stencil/core'; +import { Component, Element, Host, Prop, forceUpdate, h } from '@stencil/core'; +import { safeCall } from '@utils/overlays'; +import { getClassMap } from '@utils/theme'; + +import type { CheckboxCustomEvent } from '../checkbox/checkbox-interface'; +import type { RadioGroupCustomEvent } from '../radio-group/radio-group-interface'; + +import type { SelectModalOption } from './select-modal-interface'; + +@Component({ + tag: 'ion-select-modal', + styleUrls: { + ios: 'select-modal.ios.scss', + md: 'select-modal.md.scss', + ionic: 'select-modal.md.scss', + }, + scoped: true, +}) +export class SelectModal implements ComponentInterface { + @Element() el!: HTMLIonSelectModalElement; + + @Prop() header?: string; + + @Prop() multiple?: boolean; + + @Prop() options: SelectModalOption[] = []; + + private closeModal() { + const modal = this.el.closest('ion-modal'); + + if (modal) { + modal.dismiss(); + } + } + + private findOptionFromEvent(ev: CheckboxCustomEvent | RadioGroupCustomEvent) { + const { options } = this; + return options.find((o) => o.value === ev.target.value); + } + + private getValues(ev?: CheckboxCustomEvent | RadioGroupCustomEvent): string | string[] | undefined { + const { multiple, options } = this; + + if (multiple) { + // this is a modal with checkboxes (multiple value select) + // return an array of all the checked values + return options.filter((o) => o.checked).map((o) => o.value); + } + + // this is a modal with radio buttons (single value select) + // return the value that was clicked, otherwise undefined + const option = ev ? this.findOptionFromEvent(ev) : null; + return option ? option.value : undefined; + } + + private callOptionHandler(ev: CheckboxCustomEvent | RadioGroupCustomEvent) { + const option = this.findOptionFromEvent(ev); + const values = this.getValues(ev); + if (option?.handler) { + safeCall(option.handler, values); + } + } + + private setChecked(ev: CheckboxCustomEvent): void { + const { multiple } = this; + const option = this.findOptionFromEvent(ev); + + // this is a modal with checkboxes (multiple value select) + // we need to set the checked value for this option + if (multiple && option) { + option.checked = ev.detail.checked; + } + } + + private renderRadioOptions() { + const checked = this.options.filter((o) => o.checked).map((o) => o.value)[0]; + + return ( + this.callOptionHandler(ev)}> + {this.options.map((option) => ( + + this.closeModal()} + onKeyUp={(ev) => { + if (ev.key === ' ') { + /** + * Selecting a radio option with keyboard navigation, + * either through the Enter or Space keys, should + * dismiss the modal. + */ + this.closeModal(); + } + }} + > + {option.text} + + + ))} + + ); + } + + private renderCheckboxOptions() { + return this.options.map((option) => ( + + { + this.setChecked(ev); + this.callOptionHandler(ev); + // TODO FW-4784 + forceUpdate(this); + }} + > + {option.text} + + + )); + } + + render() { + return ( + + + + {this.header !== undefined && {this.header}} + + + this.closeModal()}>Close + + + + + {this.multiple === true ? this.renderCheckboxOptions() : this.renderRadioOptions()} + + + ); + } +} diff --git a/core/src/components/select-modal/test/basic/index.html b/core/src/components/select-modal/test/basic/index.html new file mode 100644 index 00000000000..8ddb3b54f0b --- /dev/null +++ b/core/src/components/select-modal/test/basic/index.html @@ -0,0 +1,40 @@ + + + + + Select - Modal + + + + + + + + + + + + + Select Modal - Basic + + + + + + + + + + + + + diff --git a/core/src/components/select-modal/test/basic/select-modal.e2e.ts b/core/src/components/select-modal/test/basic/select-modal.e2e.ts new file mode 100644 index 00000000000..126082712c3 --- /dev/null +++ b/core/src/components/select-modal/test/basic/select-modal.e2e.ts @@ -0,0 +1,101 @@ +import { expect } from '@playwright/test'; +import { configs, test } from '@utils/test/playwright'; + +import type { SelectModalOption } from '../../select-modal-interface'; +import { SelectModalPage } from '../fixtures'; + +const options: SelectModalOption[] = [ + { value: 'apple', text: 'Apple', disabled: false, checked: false }, + { value: 'banana', text: 'Banana', disabled: false, checked: false }, +]; + +const checkedOptions: SelectModalOption[] = [ + { value: 'apple', text: 'Apple', disabled: false, checked: true }, + { value: 'banana', text: 'Banana', disabled: false, checked: false }, +]; + +/** + * This behavior does not vary across modes/directions. + */ +configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { + test.describe(title('select-modal: basic'), () => { + test.beforeEach(({ browserName }) => { + test.skip(browserName === 'webkit', 'ROU-5437'); + }); + + test.describe('single selection', () => { + let selectModalPage: SelectModalPage; + + test.beforeEach(async ({ page }) => { + selectModalPage = new SelectModalPage(page); + }); + + test('clicking an unselected option should dismiss the modal', async () => { + await selectModalPage.setup(config, options, false); + + await selectModalPage.clickOption('apple'); + await selectModalPage.ionModalDidDismiss.next(); + await expect(selectModalPage.modal).not.toBeVisible(); + }); + + test('clicking a selected option should dismiss the modal', async () => { + await selectModalPage.setup(config, checkedOptions, false); + + await selectModalPage.clickOption('apple'); + await selectModalPage.ionModalDidDismiss.next(); + await expect(selectModalPage.modal).not.toBeVisible(); + }); + + test('pressing Space on an unselected option should dismiss the modal', async () => { + await selectModalPage.setup(config, options, false); + + await selectModalPage.pressSpaceOnOption('apple'); + await selectModalPage.ionModalDidDismiss.next(); + await expect(selectModalPage.modal).not.toBeVisible(); + }); + + test('pressing Space on a selected option should dismiss the modal', async ({ browserName }) => { + test.skip(browserName === 'firefox', 'Same behavior as ROU-5437'); + + await selectModalPage.setup(config, checkedOptions, false); + + await selectModalPage.pressSpaceOnOption('apple'); + await selectModalPage.ionModalDidDismiss.next(); + await expect(selectModalPage.modal).not.toBeVisible(); + }); + + test('clicking the close button should dismiss the modal', async () => { + await selectModalPage.setup(config, options, false); + + const closeButton = selectModalPage.modal.locator('ion-header ion-toolbar ion-button'); + await closeButton.click(); + await selectModalPage.ionModalDidDismiss.next(); + await expect(selectModalPage.modal).not.toBeVisible(); + }); + }); + }); +}); + +/** + * This behavior does not vary across directions. + * The components used inside of `ion-select-modal` + * do have RTL logic, but those are tested in their + * respective component test files. + */ +configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => { + test.describe(title('select-modal: rendering'), () => { + let selectModalPage: SelectModalPage; + + test.beforeEach(async ({ page }) => { + selectModalPage = new SelectModalPage(page); + }); + test('should not have visual regressions with single selection', async () => { + await selectModalPage.setup(config, checkedOptions, false); + await selectModalPage.screenshot(screenshot, 'select-modal-diff'); + }); + test('should not have visual regressions with multiple selection', async () => { + await selectModalPage.setup(config, checkedOptions, true); + await selectModalPage.screenshot(screenshot, 'select-modal-multiple-diff'); + }); + }); +}); diff --git a/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-diff-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-diff-ios-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..72f5453ea82 Binary files /dev/null and b/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-diff-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-diff-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-diff-ios-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..b69898dc6dc Binary files /dev/null and b/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-diff-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-diff-ios-ltr-Mobile-Safari-linux.png b/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-diff-ios-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..05425333c5f Binary files /dev/null and b/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-diff-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-diff-md-ltr-Mobile-Chrome-linux.png b/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-diff-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..7613855bd6d Binary files /dev/null and b/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-diff-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-diff-md-ltr-Mobile-Firefox-linux.png b/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-diff-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..0d71098bb5c Binary files /dev/null and b/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-diff-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-diff-md-ltr-Mobile-Safari-linux.png b/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-diff-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..c190809cd81 Binary files /dev/null and b/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-diff-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-multiple-diff-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-multiple-diff-ios-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..084a1cd1dc4 Binary files /dev/null and b/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-multiple-diff-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-multiple-diff-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-multiple-diff-ios-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..73ad7120073 Binary files /dev/null and b/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-multiple-diff-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-multiple-diff-ios-ltr-Mobile-Safari-linux.png b/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-multiple-diff-ios-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..aaf5d78f034 Binary files /dev/null and b/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-multiple-diff-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-multiple-diff-md-ltr-Mobile-Chrome-linux.png b/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-multiple-diff-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 00000000000..8482f64dbaa Binary files /dev/null and b/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-multiple-diff-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-multiple-diff-md-ltr-Mobile-Firefox-linux.png b/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-multiple-diff-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 00000000000..29c3709ef58 Binary files /dev/null and b/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-multiple-diff-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-multiple-diff-md-ltr-Mobile-Safari-linux.png b/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-multiple-diff-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 00000000000..b55b7bc77e5 Binary files /dev/null and b/core/src/components/select-modal/test/basic/select-modal.e2e.ts-snapshots/select-modal-multiple-diff-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/select-modal/test/fixtures.ts b/core/src/components/select-modal/test/fixtures.ts new file mode 100644 index 00000000000..2058848aa84 --- /dev/null +++ b/core/src/components/select-modal/test/fixtures.ts @@ -0,0 +1,73 @@ +import { expect } from '@playwright/test'; +import type { E2EPage, E2ELocator, EventSpy, E2EPageOptions, ScreenshotFn } from '@utils/test/playwright'; + +import type { SelectModalOption } from '../select-modal-interface'; + +export class SelectModalPage { + private page: E2EPage; + private multiple?: boolean; + private options: SelectModalOption[] = []; + + // Locators + modal!: E2ELocator; + selectModal!: E2ELocator; + + // Event spies + ionModalDidDismiss!: EventSpy; + + constructor(page: E2EPage) { + this.page = page; + } + + async setup(config: E2EPageOptions, options: SelectModalOption[], multiple = false) { + const { page } = this; + + await page.setContent( + ` + + + + + `, + config + ); + + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + this.ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss'); + + this.modal = page.locator('ion-modal'); + this.selectModal = page.locator('ion-select-modal'); + this.multiple = multiple; + this.options = options; + + await this.modal.evaluate((modal: HTMLIonModalElement) => modal.present()); + + await ionModalDidPresent.next(); + } + + async screenshot(screenshot: ScreenshotFn, name: string) { + await expect(this.selectModal).toHaveScreenshot(screenshot(name)); + } + + async clickOption(value: string) { + const option = this.getOption(value); + await option.click(); + } + + async pressSpaceOnOption(value: string) { + const option = this.getOption(value); + await option.press('Space'); + } + + private getOption(value: string) { + const { multiple, selectModal } = this; + const selector = multiple ? 'ion-checkbox' : 'ion-radio'; + const index = this.options.findIndex((o) => o.value === value); + + return selectModal.locator(selector).nth(index); + } +} diff --git a/core/src/components/select/select-interface.ts b/core/src/components/select/select-interface.ts index 4fa2a18828c..8e65377a825 100644 --- a/core/src/components/select/select-interface.ts +++ b/core/src/components/select/select-interface.ts @@ -1,4 +1,4 @@ -export type SelectInterface = 'action-sheet' | 'popover' | 'alert'; +export type SelectInterface = 'action-sheet' | 'popover' | 'alert' | 'modal'; export type SelectCompareFn = (currentValue: any, compareValue: any) => boolean; diff --git a/core/src/components/select/select.tsx b/core/src/components/select/select.tsx index bdd8cc0b2e5..3b4ef84f26f 100644 --- a/core/src/components/select/select.tsx +++ b/core/src/components/select/select.tsx @@ -4,7 +4,7 @@ import type { NotchController } from '@utils/forms'; import { compareOptions, createNotchController, isOptionSelected } from '@utils/forms'; import { focusVisibleElement, renderHiddenInput, inheritAttributes } from '@utils/helpers'; import type { Attributes } from '@utils/helpers'; -import { actionSheetController, alertController, popoverController } from '@utils/overlays'; +import { actionSheetController, alertController, popoverController, modalController } from '@utils/overlays'; import type { OverlaySelect } from '@utils/overlays-interface'; import { isRTL } from '@utils/rtl'; import { createColorClasses, hostContext } from '@utils/theme'; @@ -19,6 +19,7 @@ import type { CssClassMap, PopoverOptions, StyleEventDetail, + ModalOptions, } from '../../interface'; import type { ActionSheetButton } from '../action-sheet/action-sheet-interface'; import type { AlertInput } from '../alert/alert-interface'; @@ -98,15 +99,15 @@ export class Select implements ComponentInterface { @Prop() fill?: 'outline' | 'solid'; /** - * The interface the select should use: `action-sheet`, `popover` or `alert`. + * The interface the select should use: `action-sheet`, `popover`, `alert`, or `modal`. */ @Prop() interface: SelectInterface = 'alert'; /** * Any additional options that the `alert`, `action-sheet` or `popover` interface * can take. See the [ion-alert docs](./alert), the - * [ion-action-sheet docs](./action-sheet) and the - * [ion-popover docs](./popover) for the + * [ion-action-sheet docs](./action-sheet), the + * [ion-popover docs](./popover), and the [ion-modal docs](./modal) for the * create options for each interface. * * Note: `interfaceOptions` will not override `inputs` or `buttons` with the `alert` interface. @@ -318,9 +319,9 @@ export class Select implements ComponentInterface { await overlay.present(); - // focus selected option for popovers - if (this.interface === 'popover') { - const indexOfSelected = this.childOpts.map((o) => o.value).indexOf(this.value); + // focus selected option for popovers and modals + if (this.interface === 'popover' || this.interface === 'modal') { + const indexOfSelected = this.childOpts.findIndex((o) => o.value === this.value); if (indexOfSelected > -1) { const selectedItem = overlay.querySelector( @@ -328,8 +329,6 @@ export class Select implements ComponentInterface { ); if (selectedItem) { - focusVisibleElement(selectedItem); - /** * Browsers such as Firefox do not * correctly delegate focus when manually @@ -341,10 +340,17 @@ export class Select implements ComponentInterface { * we only need to worry about those two components * when focusing. */ - const interactiveEl = selectedItem.querySelector('ion-radio, ion-checkbox'); + const interactiveEl = selectedItem.querySelector('ion-radio, ion-checkbox') as + | HTMLIonRadioElement + | HTMLIonCheckboxElement + | null; if (interactiveEl) { - interactiveEl.focus(); + // Needs to be called before `focusVisibleElement` to prevent issue with focus event bubbling + // and removing `ion-focused` style + interactiveEl.setFocus(); } + + focusVisibleElement(selectedItem); } } else { /** @@ -352,14 +358,18 @@ export class Select implements ComponentInterface { */ const firstEnabledOption = overlay.querySelector( 'ion-radio:not(.radio-disabled), ion-checkbox:not(.checkbox-disabled)' - ); - if (firstEnabledOption) { - focusVisibleElement(firstEnabledOption.closest('ion-item')!); + ) as HTMLIonRadioElement | HTMLIonCheckboxElement | null; + if (firstEnabledOption) { /** * Focus the option for the same reason as we do above. + * + * Needs to be called before `focusVisibleElement` to prevent issue with focus event bubbling + * and removing `ion-focused` style */ - firstEnabledOption.focus(); + firstEnabledOption.setFocus(); + + focusVisibleElement(firstEnabledOption.closest('ion-item')!); } } } @@ -389,6 +399,9 @@ export class Select implements ComponentInterface { if (selectInterface === 'popover') { return this.openPopover(ev!); } + if (selectInterface === 'modal') { + return this.openModal(); + } return this.openAlert(); } @@ -406,7 +419,13 @@ export class Select implements ComponentInterface { case 'popover': const popover = overlay.querySelector('ion-select-popover'); if (popover) { - popover.options = this.createPopoverOptions(childOpts, value); + popover.options = this.createOverlaySelectOptions(childOpts, value); + } + break; + case 'modal': + const modal = overlay.querySelector('ion-select-modal'); + if (modal) { + modal.options = this.createOverlaySelectOptions(childOpts, value); } break; case 'alert': @@ -475,7 +494,7 @@ export class Select implements ComponentInterface { return alertInputs; } - private createPopoverOptions(data: HTMLIonSelectOptionElement[], selectValue: any): SelectPopoverOption[] { + private createOverlaySelectOptions(data: HTMLIonSelectOptionElement[], selectValue: any): SelectPopoverOption[] { const popoverOptions = data.map((option) => { const value = getOptionValue(option); @@ -553,7 +572,7 @@ export class Select implements ComponentInterface { message: interfaceOptions.message, multiple, value, - options: this.createPopoverOptions(this.childOpts, value), + options: this.createOverlaySelectOptions(this.childOpts, value), }, }; @@ -647,6 +666,40 @@ export class Select implements ComponentInterface { return alertController.create(alertOpts); } + private openModal() { + const { multiple, value, interfaceOptions } = this; + const mode = getIonMode(this); + + const modalOpts: ModalOptions = { + ...interfaceOptions, + mode, + + cssClass: ['select-modal', interfaceOptions.cssClass], + component: 'ion-select-modal', + componentProps: { + header: interfaceOptions.header, + multiple, + value, + options: this.createOverlaySelectOptions(this.childOpts, value), + }, + }; + + /** + * Workaround for Stencil to autodefine + * ion-select-modal and ion-modal when + * using Custom Elements build. + */ + // eslint-disable-next-line + if (false) { + // eslint-disable-next-line + // @ts-ignore + document.createElement('ion-select-modal'); + document.createElement('ion-modal'); + } + + return modalController.create(modalOpts); + } + /** * Close the select interface. */ diff --git a/core/src/components/select/test/basic/index.html b/core/src/components/select/test/basic/index.html index 421dd4505fe..c958d08f909 100644 --- a/core/src/components/select/test/basic/index.html +++ b/core/src/components/select/test/basic/index.html @@ -51,6 +51,14 @@ Pears + + + + Apples + Oranges + Pears + + @@ -76,6 +84,15 @@ Honey Badger + + + + Bird + Cat + Dog + Honey Badger + + @@ -124,6 +141,16 @@ Onions + + + + Pepperoni + Bacon + Extra Cheese + Mushrooms + Onions + + @@ -152,6 +179,14 @@ message: '$1.50 charge for every topping', }; customActionSheetSelect.interfaceOptions = customActionSheetOptions; + + var customModalSelect = document.getElementById('customModalSelect'); + var customModalSheetOptions = { + header: 'Pizza Toppings', + breakpoints: [0.5], + initialBreakpoint: 0.5, + }; + customModalSelect.interfaceOptions = customModalSheetOptions; diff --git a/core/src/components/select/test/basic/select.e2e.ts b/core/src/components/select/test/basic/select.e2e.ts index d44c8ca7619..35747728495 100644 --- a/core/src/components/select/test/basic/select.e2e.ts +++ b/core/src/components/select/test/basic/select.e2e.ts @@ -58,6 +58,24 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => { await expect(popover).toBeVisible(); }); }); + + test.describe('select: modal', () => { + test('it should open a modal select', async ({ page }) => { + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + + await page.click('#customModalSelect'); + + await ionModalDidPresent.next(); + + const modal = page.locator('ion-modal'); + + // select has no value, so first option should be focused by default + const modalOption1 = modal.locator('.select-interface-option:first-of-type ion-radio'); + await expect(modalOption1).toBeFocused(); + + await expect(modal).toBeVisible(); + }); + }); }); }); diff --git a/core/src/utils/menu-controller/index.ts b/core/src/utils/menu-controller/index.ts index aa09e8a825d..dca6c5bc8ce 100644 --- a/core/src/utils/menu-controller/index.ts +++ b/core/src/utils/menu-controller/index.ts @@ -174,7 +174,7 @@ const createMenuController = (): MenuControllerI => { } }; - const _setOpen = async (menu: MenuI, shouldOpen: boolean, animated: boolean): Promise => { + const _setOpen = async (menu: MenuI, shouldOpen: boolean, animated: boolean, role: string): Promise => { if (isAnimatingSync()) { return false; } @@ -184,7 +184,7 @@ const createMenuController = (): MenuControllerI => { await openedMenu.setOpen(false, false); } } - return menu._setOpen(shouldOpen, animated); + return menu._setOpen(shouldOpen, animated, role); }; const _createAnimation = (type: string, menuCmp: MenuI) => { diff --git a/core/src/utils/overlays-interface.ts b/core/src/utils/overlays-interface.ts index 4a37fca71ab..63f70baac8b 100644 --- a/core/src/utils/overlays-interface.ts +++ b/core/src/utils/overlays-interface.ts @@ -46,4 +46,8 @@ export interface HTMLIonOverlayElement extends HTMLStencilElement { present: () => Promise; } -export type OverlaySelect = HTMLIonActionSheetElement | HTMLIonAlertElement | HTMLIonPopoverElement; +export type OverlaySelect = + | HTMLIonActionSheetElement + | HTMLIonAlertElement + | HTMLIonPopoverElement + | HTMLIonModalElement; diff --git a/lerna.json b/lerna.json index 6b73eec8d48..9573d91b5e6 100644 --- a/lerna.json +++ b/lerna.json @@ -3,5 +3,5 @@ "core", "packages/*" ], - "version": "8.3.4" + "version": "8.4.0" } \ No newline at end of file diff --git a/packages/angular-server/CHANGELOG.md b/packages/angular-server/CHANGELOG.md index 8e18462ddf7..f76d843625a 100644 --- a/packages/angular-server/CHANGELOG.md +++ b/packages/angular-server/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [8.4.0](https://github.com/ionic-team/ionic-framework/compare/v8.3.4...v8.4.0) (2024-11-04) + +**Note:** Version bump only for package @ionic/angular-server + + + + + ## [8.3.4](https://github.com/ionic-team/ionic-framework/compare/v8.3.3...v8.3.4) (2024-10-30) **Note:** Version bump only for package @ionic/angular-server diff --git a/packages/angular-server/package-lock.json b/packages/angular-server/package-lock.json index 3a9a06112a6..d076c1944ef 100644 --- a/packages/angular-server/package-lock.json +++ b/packages/angular-server/package-lock.json @@ -1,15 +1,15 @@ { "name": "@ionic/angular-server", - "version": "8.3.4", + "version": "8.4.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@ionic/angular-server", - "version": "8.3.4", + "version": "8.4.0", "license": "MIT", "dependencies": { - "@ionic/core": "^8.3.4" + "@ionic/core": "^8.4.0" }, "devDependencies": { "@angular-eslint/eslint-plugin": "^16.0.0", @@ -1031,9 +1031,9 @@ "dev": true }, "node_modules/@ionic/core": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.3.4.tgz", - "integrity": "sha512-MkgaQ9+oQwj3AK/i25MkLgKUUH3/nvNjd4YlmUyFjPG7l2IBHsrLlSmuFGCyniB+doI9Hynu3T9CZP7NS4RjRQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.4.0.tgz", + "integrity": "sha512-mZ2Ni9QByFGWBNr5W/F/nyPV+cXLhK+6W5BRziy7QPX6YIS57KH8FpY+CjE7BEcpE78anyY49bZt3eOWcES02g==", "dependencies": { "@stencil/core": "4.20.0", "ionicons": "^7.2.2", @@ -7188,9 +7188,9 @@ "dev": true }, "@ionic/core": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.3.4.tgz", - "integrity": "sha512-MkgaQ9+oQwj3AK/i25MkLgKUUH3/nvNjd4YlmUyFjPG7l2IBHsrLlSmuFGCyniB+doI9Hynu3T9CZP7NS4RjRQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.4.0.tgz", + "integrity": "sha512-mZ2Ni9QByFGWBNr5W/F/nyPV+cXLhK+6W5BRziy7QPX6YIS57KH8FpY+CjE7BEcpE78anyY49bZt3eOWcES02g==", "requires": { "@stencil/core": "4.20.0", "ionicons": "^7.2.2", diff --git a/packages/angular-server/package.json b/packages/angular-server/package.json index ceb075f4a7f..2ab94afe04b 100644 --- a/packages/angular-server/package.json +++ b/packages/angular-server/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/angular-server", - "version": "8.3.4", + "version": "8.4.0", "description": "Angular SSR Module for Ionic", "keywords": [ "ionic", @@ -62,6 +62,6 @@ }, "prettier": "@ionic/prettier-config", "dependencies": { - "@ionic/core": "^8.3.4" + "@ionic/core": "^8.4.0" } } diff --git a/packages/angular/CHANGELOG.md b/packages/angular/CHANGELOG.md index 5899927ba34..623f30b8ae2 100644 --- a/packages/angular/CHANGELOG.md +++ b/packages/angular/CHANGELOG.md @@ -3,6 +3,19 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [8.4.0](https://github.com/ionic-team/ionic-framework/compare/v8.3.4...v8.4.0) (2024-11-04) + + +### Features + +* **menu:** pass role to ionWillClose and ionDidClose events ([#29954](https://github.com/ionic-team/ionic-framework/issues/29954)) ([ee2fa19](https://github.com/ionic-team/ionic-framework/commit/ee2fa19a1e9f09d492c7c08340d95ba6a56ebb2b)) +* **segment-view:** adds support for new `ion-segment-view` component ([#29969](https://github.com/ionic-team/ionic-framework/issues/29969)) ([89508fb](https://github.com/ionic-team/ionic-framework/commit/89508fb89172900b1d11cc3fc18883f57a7fbab6)) +* **select:** add `modal` as interface ([#29972](https://github.com/ionic-team/ionic-framework/issues/29972)) ([3628ea8](https://github.com/ionic-team/ionic-framework/commit/3628ea875a66a717783de5e0a4df440872339040)) + + + + + ## [8.3.4](https://github.com/ionic-team/ionic-framework/compare/v8.3.3...v8.3.4) (2024-10-30) diff --git a/packages/angular/package-lock.json b/packages/angular/package-lock.json index aaf40af261f..99ae2425d51 100644 --- a/packages/angular/package-lock.json +++ b/packages/angular/package-lock.json @@ -1,15 +1,15 @@ { "name": "@ionic/angular", - "version": "8.3.4", + "version": "8.4.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@ionic/angular", - "version": "8.3.4", + "version": "8.4.0", "license": "MIT", "dependencies": { - "@ionic/core": "^8.3.4", + "@ionic/core": "^8.4.0", "ionicons": "^7.0.0", "jsonc-parser": "^3.0.0", "tslib": "^2.3.0" @@ -1398,9 +1398,9 @@ "dev": true }, "node_modules/@ionic/core": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.3.4.tgz", - "integrity": "sha512-MkgaQ9+oQwj3AK/i25MkLgKUUH3/nvNjd4YlmUyFjPG7l2IBHsrLlSmuFGCyniB+doI9Hynu3T9CZP7NS4RjRQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.4.0.tgz", + "integrity": "sha512-mZ2Ni9QByFGWBNr5W/F/nyPV+cXLhK+6W5BRziy7QPX6YIS57KH8FpY+CjE7BEcpE78anyY49bZt3eOWcES02g==", "dependencies": { "@stencil/core": "4.20.0", "ionicons": "^7.2.2", @@ -9820,9 +9820,9 @@ "dev": true }, "@ionic/core": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.3.4.tgz", - "integrity": "sha512-MkgaQ9+oQwj3AK/i25MkLgKUUH3/nvNjd4YlmUyFjPG7l2IBHsrLlSmuFGCyniB+doI9Hynu3T9CZP7NS4RjRQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.4.0.tgz", + "integrity": "sha512-mZ2Ni9QByFGWBNr5W/F/nyPV+cXLhK+6W5BRziy7QPX6YIS57KH8FpY+CjE7BEcpE78anyY49bZt3eOWcES02g==", "requires": { "@stencil/core": "4.20.0", "ionicons": "^7.2.2", diff --git a/packages/angular/package.json b/packages/angular/package.json index cf3edb13741..e717fcb1fd7 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/angular", - "version": "8.3.4", + "version": "8.4.0", "description": "Angular specific wrappers for @ionic/core", "keywords": [ "ionic", @@ -47,7 +47,7 @@ } }, "dependencies": { - "@ionic/core": "^8.3.4", + "@ionic/core": "^8.4.0", "ionicons": "^7.0.0", "jsonc-parser": "^3.0.0", "tslib": "^2.3.0" diff --git a/packages/angular/src/directives/proxies-list.ts b/packages/angular/src/directives/proxies-list.ts index 1874d0bfe2d..b61d09669ca 100644 --- a/packages/angular/src/directives/proxies-list.ts +++ b/packages/angular/src/directives/proxies-list.ts @@ -69,7 +69,10 @@ export const DIRECTIVES = [ d.IonSearchbar, d.IonSegment, d.IonSegmentButton, + d.IonSegmentContent, + d.IonSegmentView, d.IonSelect, + d.IonSelectModal, d.IonSelectOption, d.IonSkeletonText, d.IonSpinner, diff --git a/packages/angular/src/directives/proxies.ts b/packages/angular/src/directives/proxies.ts index 0f10c6bb5b5..675c37bd1c1 100644 --- a/packages/angular/src/directives/proxies.ts +++ b/packages/angular/src/directives/proxies.ts @@ -1334,6 +1334,8 @@ export class IonMenu { } +import type { MenuCloseEventDetail as IIonMenuMenuCloseEventDetail } from '@ionic/core'; + export declare interface IonMenu extends Components.IonMenu { /** * Emitted when the menu is about to be opened. @@ -1342,7 +1344,7 @@ export declare interface IonMenu extends Components.IonMenu { /** * Emitted when the menu is about to be closed. */ - ionWillClose: EventEmitter>; + ionWillClose: EventEmitter>; /** * Emitted when the menu is open. */ @@ -1350,7 +1352,7 @@ export declare interface IonMenu extends Components.IonMenu { /** * Emitted when the menu is closed. */ - ionDidClose: EventEmitter>; + ionDidClose: EventEmitter>; } @@ -1985,14 +1987,14 @@ This event will not emit when programmatically setting the `value` property. @ProxyCmp({ - inputs: ['disabled', 'layout', 'mode', 'type', 'value'] + inputs: ['contentId', 'disabled', 'layout', 'mode', 'type', 'value'] }) @Component({ selector: 'ion-segment-button', changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['disabled', 'layout', 'mode', 'type', 'value'], + inputs: ['contentId', 'disabled', 'layout', 'mode', 'type', 'value'], }) export class IonSegmentButton { protected el: HTMLElement; @@ -2006,6 +2008,57 @@ export class IonSegmentButton { export declare interface IonSegmentButton extends Components.IonSegmentButton {} +@ProxyCmp({ +}) +@Component({ + selector: 'ion-segment-content', + changeDetection: ChangeDetectionStrategy.OnPush, + template: '', + // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property + inputs: [], +}) +export class IonSegmentContent { + protected el: HTMLElement; + constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { + c.detach(); + this.el = r.nativeElement; + } +} + + +export declare interface IonSegmentContent extends Components.IonSegmentContent {} + + +@ProxyCmp({ + inputs: ['disabled'] +}) +@Component({ + selector: 'ion-segment-view', + changeDetection: ChangeDetectionStrategy.OnPush, + template: '', + // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property + inputs: ['disabled'], +}) +export class IonSegmentView { + protected el: HTMLElement; + constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { + c.detach(); + this.el = r.nativeElement; + proxyOutputs(this, this.el, ['ionSegmentViewScroll']); + } +} + + +import type { SegmentViewScrollEvent as IIonSegmentViewSegmentViewScrollEvent } from '@ionic/core'; + +export declare interface IonSegmentView extends Components.IonSegmentView { + /** + * Emitted when the segment view is scrolled. + */ + ionSegmentViewScroll: EventEmitter>; +} + + @ProxyCmp({ inputs: ['cancelText', 'color', 'compareWith', 'disabled', 'expandedIcon', 'fill', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'selectedText', 'shape', 'toggleIcon', 'value'], methods: ['open'] @@ -2055,6 +2108,28 @@ This event will not emit when programmatically setting the `value` property. } +@ProxyCmp({ + inputs: ['header', 'multiple', 'options'] +}) +@Component({ + selector: 'ion-select-modal', + changeDetection: ChangeDetectionStrategy.OnPush, + template: '', + // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property + inputs: ['header', 'multiple', 'options'], +}) +export class IonSelectModal { + protected el: HTMLElement; + constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { + c.detach(); + this.el = r.nativeElement; + } +} + + +export declare interface IonSelectModal extends Components.IonSelectModal {} + + @ProxyCmp({ inputs: ['disabled', 'value'] }) diff --git a/packages/angular/standalone/src/directives/proxies.ts b/packages/angular/standalone/src/directives/proxies.ts index bc90bec4d03..a30385ead5c 100644 --- a/packages/angular/standalone/src/directives/proxies.ts +++ b/packages/angular/standalone/src/directives/proxies.ts @@ -65,6 +65,9 @@ import { defineCustomElement as defineIonReorderGroup } from '@ionic/core/compon import { defineCustomElement as defineIonRippleEffect } from '@ionic/core/components/ion-ripple-effect.js'; import { defineCustomElement as defineIonRow } from '@ionic/core/components/ion-row.js'; import { defineCustomElement as defineIonSegmentButton } from '@ionic/core/components/ion-segment-button.js'; +import { defineCustomElement as defineIonSegmentContent } from '@ionic/core/components/ion-segment-content.js'; +import { defineCustomElement as defineIonSegmentView } from '@ionic/core/components/ion-segment-view.js'; +import { defineCustomElement as defineIonSelectModal } from '@ionic/core/components/ion-select-modal.js'; import { defineCustomElement as defineIonSelectOption } from '@ionic/core/components/ion-select-option.js'; import { defineCustomElement as defineIonSkeletonText } from '@ionic/core/components/ion-skeleton-text.js'; import { defineCustomElement as defineIonSpinner } from '@ionic/core/components/ion-spinner.js'; @@ -1319,6 +1322,8 @@ export class IonMenu { } +import type { MenuCloseEventDetail as IIonMenuMenuCloseEventDetail } from '@ionic/core/components'; + export declare interface IonMenu extends Components.IonMenu { /** * Emitted when the menu is about to be opened. @@ -1327,7 +1332,7 @@ export declare interface IonMenu extends Components.IonMenu { /** * Emitted when the menu is about to be closed. */ - ionWillClose: EventEmitter>; + ionWillClose: EventEmitter>; /** * Emitted when the menu is open. */ @@ -1335,7 +1340,7 @@ export declare interface IonMenu extends Components.IonMenu { /** * Emitted when the menu is closed. */ - ionDidClose: EventEmitter>; + ionDidClose: EventEmitter>; } @@ -1817,14 +1822,14 @@ export declare interface IonRow extends Components.IonRow {} @ProxyCmp({ defineCustomElementFn: defineIonSegmentButton, - inputs: ['disabled', 'layout', 'mode', 'type', 'value'] + inputs: ['contentId', 'disabled', 'layout', 'mode', 'type', 'value'] }) @Component({ selector: 'ion-segment-button', changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['disabled', 'layout', 'mode', 'type', 'value'], + inputs: ['contentId', 'disabled', 'layout', 'mode', 'type', 'value'], standalone: true }) export class IonSegmentButton { @@ -1839,6 +1844,85 @@ export class IonSegmentButton { export declare interface IonSegmentButton extends Components.IonSegmentButton {} +@ProxyCmp({ + defineCustomElementFn: defineIonSegmentContent +}) +@Component({ + selector: 'ion-segment-content', + changeDetection: ChangeDetectionStrategy.OnPush, + template: '', + // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property + inputs: [], + standalone: true +}) +export class IonSegmentContent { + protected el: HTMLElement; + constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { + c.detach(); + this.el = r.nativeElement; + } +} + + +export declare interface IonSegmentContent extends Components.IonSegmentContent {} + + +@ProxyCmp({ + defineCustomElementFn: defineIonSegmentView, + inputs: ['disabled'] +}) +@Component({ + selector: 'ion-segment-view', + changeDetection: ChangeDetectionStrategy.OnPush, + template: '', + // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property + inputs: ['disabled'], + standalone: true +}) +export class IonSegmentView { + protected el: HTMLElement; + constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { + c.detach(); + this.el = r.nativeElement; + proxyOutputs(this, this.el, ['ionSegmentViewScroll']); + } +} + + +import type { SegmentViewScrollEvent as IIonSegmentViewSegmentViewScrollEvent } from '@ionic/core/components'; + +export declare interface IonSegmentView extends Components.IonSegmentView { + /** + * Emitted when the segment view is scrolled. + */ + ionSegmentViewScroll: EventEmitter>; +} + + +@ProxyCmp({ + defineCustomElementFn: defineIonSelectModal, + inputs: ['header', 'multiple', 'options'] +}) +@Component({ + selector: 'ion-select-modal', + changeDetection: ChangeDetectionStrategy.OnPush, + template: '', + // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property + inputs: ['header', 'multiple', 'options'], + standalone: true +}) +export class IonSelectModal { + protected el: HTMLElement; + constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { + c.detach(); + this.el = r.nativeElement; + } +} + + +export declare interface IonSelectModal extends Components.IonSelectModal {} + + @ProxyCmp({ defineCustomElementFn: defineIonSelectOption, inputs: ['disabled', 'value'] diff --git a/packages/docs/CHANGELOG.md b/packages/docs/CHANGELOG.md index 01d59dbdfaa..b4fa7afd0f6 100644 --- a/packages/docs/CHANGELOG.md +++ b/packages/docs/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [8.4.0](https://github.com/ionic-team/ionic-framework/compare/v8.3.4...v8.4.0) (2024-11-04) + +**Note:** Version bump only for package @ionic/docs + + + + + ## [8.3.4](https://github.com/ionic-team/ionic-framework/compare/v8.3.3...v8.3.4) (2024-10-30) **Note:** Version bump only for package @ionic/docs diff --git a/packages/docs/package-lock.json b/packages/docs/package-lock.json index b2ec1102513..cecd55ac062 100644 --- a/packages/docs/package-lock.json +++ b/packages/docs/package-lock.json @@ -1,12 +1,12 @@ { "name": "@ionic/docs", - "version": "8.3.4", + "version": "8.4.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@ionic/docs", - "version": "8.3.4", + "version": "8.4.0", "license": "MIT" } } diff --git a/packages/docs/package.json b/packages/docs/package.json index 9402ac8627d..ecfb015d6e8 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/docs", - "version": "8.3.4", + "version": "8.4.0", "description": "Pre-packaged API documentation for the Ionic docs.", "main": "core.json", "types": "core.d.ts", diff --git a/packages/react-router/CHANGELOG.md b/packages/react-router/CHANGELOG.md index 182220662ab..d4fbfb89f7e 100644 --- a/packages/react-router/CHANGELOG.md +++ b/packages/react-router/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [8.4.0](https://github.com/ionic-team/ionic-framework/compare/v8.3.4...v8.4.0) (2024-11-04) + +**Note:** Version bump only for package @ionic/react-router + + + + + ## [8.3.4](https://github.com/ionic-team/ionic-framework/compare/v8.3.3...v8.3.4) (2024-10-30) **Note:** Version bump only for package @ionic/react-router diff --git a/packages/react-router/package-lock.json b/packages/react-router/package-lock.json index 597890f036c..eec354f3d51 100644 --- a/packages/react-router/package-lock.json +++ b/packages/react-router/package-lock.json @@ -1,15 +1,15 @@ { "name": "@ionic/react-router", - "version": "8.3.4", + "version": "8.4.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@ionic/react-router", - "version": "8.3.4", + "version": "8.4.0", "license": "MIT", "dependencies": { - "@ionic/react": "^8.3.4", + "@ionic/react": "^8.4.0", "tslib": "*" }, "devDependencies": { @@ -238,9 +238,9 @@ "dev": true }, "node_modules/@ionic/core": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.3.4.tgz", - "integrity": "sha512-MkgaQ9+oQwj3AK/i25MkLgKUUH3/nvNjd4YlmUyFjPG7l2IBHsrLlSmuFGCyniB+doI9Hynu3T9CZP7NS4RjRQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.4.0.tgz", + "integrity": "sha512-mZ2Ni9QByFGWBNr5W/F/nyPV+cXLhK+6W5BRziy7QPX6YIS57KH8FpY+CjE7BEcpE78anyY49bZt3eOWcES02g==", "dependencies": { "@stencil/core": "4.20.0", "ionicons": "^7.2.2", @@ -414,11 +414,11 @@ } }, "node_modules/@ionic/react": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/@ionic/react/-/react-8.3.4.tgz", - "integrity": "sha512-CIoTHg/1nJJN11IjmsUqeQB1nIP4SxQyo2nBH+MhzeVCMv8Tj00Y4rU/9RYzKfRI7Zfsi9MOwVrwpGhPWi4KWA==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@ionic/react/-/react-8.4.0.tgz", + "integrity": "sha512-wCtixCwf673Qnes1uGxmRoyUP4FnGtEyUVwtkcfj9IBrPUbw641Ws8J4jRjQ2rOO1WkWkSCeHKnd+KYCqyulZg==", "dependencies": { - "@ionic/core": "8.3.4", + "@ionic/core": "8.4.0", "ionicons": "^7.0.0", "tslib": "*" }, @@ -4057,9 +4057,9 @@ "dev": true }, "@ionic/core": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.3.4.tgz", - "integrity": "sha512-MkgaQ9+oQwj3AK/i25MkLgKUUH3/nvNjd4YlmUyFjPG7l2IBHsrLlSmuFGCyniB+doI9Hynu3T9CZP7NS4RjRQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.4.0.tgz", + "integrity": "sha512-mZ2Ni9QByFGWBNr5W/F/nyPV+cXLhK+6W5BRziy7QPX6YIS57KH8FpY+CjE7BEcpE78anyY49bZt3eOWcES02g==", "requires": { "@stencil/core": "4.20.0", "ionicons": "^7.2.2", @@ -4163,11 +4163,11 @@ "requires": {} }, "@ionic/react": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/@ionic/react/-/react-8.3.4.tgz", - "integrity": "sha512-CIoTHg/1nJJN11IjmsUqeQB1nIP4SxQyo2nBH+MhzeVCMv8Tj00Y4rU/9RYzKfRI7Zfsi9MOwVrwpGhPWi4KWA==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@ionic/react/-/react-8.4.0.tgz", + "integrity": "sha512-wCtixCwf673Qnes1uGxmRoyUP4FnGtEyUVwtkcfj9IBrPUbw641Ws8J4jRjQ2rOO1WkWkSCeHKnd+KYCqyulZg==", "requires": { - "@ionic/core": "8.3.4", + "@ionic/core": "8.4.0", "ionicons": "^7.0.0", "tslib": "*" } diff --git a/packages/react-router/package.json b/packages/react-router/package.json index 79225e3bfb1..cfcd1a4e453 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/react-router", - "version": "8.3.4", + "version": "8.4.0", "description": "React Router wrapper for @ionic/react", "keywords": [ "ionic", @@ -36,7 +36,7 @@ "dist/" ], "dependencies": { - "@ionic/react": "^8.3.4", + "@ionic/react": "^8.4.0", "tslib": "*" }, "peerDependencies": { diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md index f9f691caab6..bcb4d07e5c1 100644 --- a/packages/react/CHANGELOG.md +++ b/packages/react/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [8.4.0](https://github.com/ionic-team/ionic-framework/compare/v8.3.4...v8.4.0) (2024-11-04) + + +### Features + +* **segment-view:** adds support for new `ion-segment-view` component ([#29969](https://github.com/ionic-team/ionic-framework/issues/29969)) ([89508fb](https://github.com/ionic-team/ionic-framework/commit/89508fb89172900b1d11cc3fc18883f57a7fbab6)) +* **select:** add `modal` as interface ([#29972](https://github.com/ionic-team/ionic-framework/issues/29972)) ([3628ea8](https://github.com/ionic-team/ionic-framework/commit/3628ea875a66a717783de5e0a4df440872339040)) + + + + + ## [8.3.4](https://github.com/ionic-team/ionic-framework/compare/v8.3.3...v8.3.4) (2024-10-30) **Note:** Version bump only for package @ionic/react diff --git a/packages/react/package-lock.json b/packages/react/package-lock.json index 232c3805ca5..05689bce961 100644 --- a/packages/react/package-lock.json +++ b/packages/react/package-lock.json @@ -1,15 +1,15 @@ { "name": "@ionic/react", - "version": "8.3.4", + "version": "8.4.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@ionic/react", - "version": "8.3.4", + "version": "8.4.0", "license": "MIT", "dependencies": { - "@ionic/core": "^8.3.4", + "@ionic/core": "^8.4.0", "ionicons": "^7.0.0", "tslib": "*" }, @@ -736,9 +736,9 @@ "dev": true }, "node_modules/@ionic/core": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.3.4.tgz", - "integrity": "sha512-MkgaQ9+oQwj3AK/i25MkLgKUUH3/nvNjd4YlmUyFjPG7l2IBHsrLlSmuFGCyniB+doI9Hynu3T9CZP7NS4RjRQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.4.0.tgz", + "integrity": "sha512-mZ2Ni9QByFGWBNr5W/F/nyPV+cXLhK+6W5BRziy7QPX6YIS57KH8FpY+CjE7BEcpE78anyY49bZt3eOWcES02g==", "dependencies": { "@stencil/core": "4.20.0", "ionicons": "^7.2.2", @@ -12315,9 +12315,9 @@ "dev": true }, "@ionic/core": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.3.4.tgz", - "integrity": "sha512-MkgaQ9+oQwj3AK/i25MkLgKUUH3/nvNjd4YlmUyFjPG7l2IBHsrLlSmuFGCyniB+doI9Hynu3T9CZP7NS4RjRQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.4.0.tgz", + "integrity": "sha512-mZ2Ni9QByFGWBNr5W/F/nyPV+cXLhK+6W5BRziy7QPX6YIS57KH8FpY+CjE7BEcpE78anyY49bZt3eOWcES02g==", "requires": { "@stencil/core": "4.20.0", "ionicons": "^7.2.2", diff --git a/packages/react/package.json b/packages/react/package.json index 59b96e911f9..42b40b1483f 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/react", - "version": "8.3.4", + "version": "8.4.0", "description": "React specific wrapper for @ionic/core", "keywords": [ "ionic", @@ -39,7 +39,7 @@ "css/" ], "dependencies": { - "@ionic/core": "^8.3.4", + "@ionic/core": "^8.4.0", "ionicons": "^7.0.0", "tslib": "*" }, diff --git a/packages/react/src/components/proxies.ts b/packages/react/src/components/proxies.ts index 05800f38773..a9f1416ee22 100644 --- a/packages/react/src/components/proxies.ts +++ b/packages/react/src/components/proxies.ts @@ -61,7 +61,10 @@ import { defineCustomElement as defineIonRow } from '@ionic/core/components/ion- import { defineCustomElement as defineIonSearchbar } from '@ionic/core/components/ion-searchbar.js'; import { defineCustomElement as defineIonSegment } from '@ionic/core/components/ion-segment.js'; import { defineCustomElement as defineIonSegmentButton } from '@ionic/core/components/ion-segment-button.js'; +import { defineCustomElement as defineIonSegmentContent } from '@ionic/core/components/ion-segment-content.js'; +import { defineCustomElement as defineIonSegmentView } from '@ionic/core/components/ion-segment-view.js'; import { defineCustomElement as defineIonSelect } from '@ionic/core/components/ion-select.js'; +import { defineCustomElement as defineIonSelectModal } from '@ionic/core/components/ion-select-modal.js'; import { defineCustomElement as defineIonSelectOption } from '@ionic/core/components/ion-select-option.js'; import { defineCustomElement as defineIonSkeletonText } from '@ionic/core/components/ion-skeleton-text.js'; import { defineCustomElement as defineIonSpinner } from '@ionic/core/components/ion-spinner.js'; @@ -130,7 +133,10 @@ export const IonRow = /*@__PURE__*/createReactComponent('ion-searchbar', undefined, undefined, defineIonSearchbar); export const IonSegment = /*@__PURE__*/createReactComponent('ion-segment', undefined, undefined, defineIonSegment); export const IonSegmentButton = /*@__PURE__*/createReactComponent('ion-segment-button', undefined, undefined, defineIonSegmentButton); +export const IonSegmentContent = /*@__PURE__*/createReactComponent('ion-segment-content', undefined, undefined, defineIonSegmentContent); +export const IonSegmentView = /*@__PURE__*/createReactComponent('ion-segment-view', undefined, undefined, defineIonSegmentView); export const IonSelect = /*@__PURE__*/createReactComponent('ion-select', undefined, undefined, defineIonSelect); +export const IonSelectModal = /*@__PURE__*/createReactComponent('ion-select-modal', undefined, undefined, defineIonSelectModal); export const IonSelectOption = /*@__PURE__*/createReactComponent('ion-select-option', undefined, undefined, defineIonSelectOption); export const IonSkeletonText = /*@__PURE__*/createReactComponent('ion-skeleton-text', undefined, undefined, defineIonSkeletonText); export const IonSpinner = /*@__PURE__*/createReactComponent('ion-spinner', undefined, undefined, defineIonSpinner); diff --git a/packages/vue-router/CHANGELOG.md b/packages/vue-router/CHANGELOG.md index b8e456b3ce3..a761ceda1e5 100644 --- a/packages/vue-router/CHANGELOG.md +++ b/packages/vue-router/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [8.4.0](https://github.com/ionic-team/ionic-framework/compare/v8.3.4...v8.4.0) (2024-11-04) + +**Note:** Version bump only for package @ionic/vue-router + + + + + ## [8.3.4](https://github.com/ionic-team/ionic-framework/compare/v8.3.3...v8.3.4) (2024-10-30) diff --git a/packages/vue-router/package-lock.json b/packages/vue-router/package-lock.json index 6e3138dc0d7..de31e284492 100644 --- a/packages/vue-router/package-lock.json +++ b/packages/vue-router/package-lock.json @@ -1,15 +1,15 @@ { "name": "@ionic/vue-router", - "version": "8.3.4", + "version": "8.4.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@ionic/vue-router", - "version": "8.3.4", + "version": "8.4.0", "license": "MIT", "dependencies": { - "@ionic/vue": "^8.3.4" + "@ionic/vue": "^8.4.0" }, "devDependencies": { "@ionic/eslint-config": "^0.3.0", @@ -661,9 +661,9 @@ "dev": true }, "node_modules/@ionic/core": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.3.4.tgz", - "integrity": "sha512-MkgaQ9+oQwj3AK/i25MkLgKUUH3/nvNjd4YlmUyFjPG7l2IBHsrLlSmuFGCyniB+doI9Hynu3T9CZP7NS4RjRQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.4.0.tgz", + "integrity": "sha512-mZ2Ni9QByFGWBNr5W/F/nyPV+cXLhK+6W5BRziy7QPX6YIS57KH8FpY+CjE7BEcpE78anyY49bZt3eOWcES02g==", "dependencies": { "@stencil/core": "4.20.0", "ionicons": "^7.2.2", @@ -852,11 +852,11 @@ } }, "node_modules/@ionic/vue": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/@ionic/vue/-/vue-8.3.4.tgz", - "integrity": "sha512-s7P5mTd078CbPK2dpIxxWGNirQacG3sXhJulJ1L0J6+6VI+HHHLKy4ueTRgPl5GLDmjGgdMCBuYu1n4K2nNXFg==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@ionic/vue/-/vue-8.4.0.tgz", + "integrity": "sha512-mtSerl9oC21d6xv1q+QuGm61IzJbqpkWbt0lQryXZ3kK1/aVVOnHAN5bX8tPPUN2ALA7CyTWXaCvhxceRV/paA==", "dependencies": { - "@ionic/core": "8.3.4", + "@ionic/core": "8.4.0", "ionicons": "^7.0.0" } }, @@ -7878,9 +7878,9 @@ "dev": true }, "@ionic/core": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.3.4.tgz", - "integrity": "sha512-MkgaQ9+oQwj3AK/i25MkLgKUUH3/nvNjd4YlmUyFjPG7l2IBHsrLlSmuFGCyniB+doI9Hynu3T9CZP7NS4RjRQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.4.0.tgz", + "integrity": "sha512-mZ2Ni9QByFGWBNr5W/F/nyPV+cXLhK+6W5BRziy7QPX6YIS57KH8FpY+CjE7BEcpE78anyY49bZt3eOWcES02g==", "requires": { "@stencil/core": "4.20.0", "ionicons": "^7.2.2", @@ -7993,11 +7993,11 @@ "requires": {} }, "@ionic/vue": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/@ionic/vue/-/vue-8.3.4.tgz", - "integrity": "sha512-s7P5mTd078CbPK2dpIxxWGNirQacG3sXhJulJ1L0J6+6VI+HHHLKy4ueTRgPl5GLDmjGgdMCBuYu1n4K2nNXFg==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@ionic/vue/-/vue-8.4.0.tgz", + "integrity": "sha512-mtSerl9oC21d6xv1q+QuGm61IzJbqpkWbt0lQryXZ3kK1/aVVOnHAN5bX8tPPUN2ALA7CyTWXaCvhxceRV/paA==", "requires": { - "@ionic/core": "8.3.4", + "@ionic/core": "8.4.0", "ionicons": "^7.0.0" } }, diff --git a/packages/vue-router/package.json b/packages/vue-router/package.json index f6a69910661..7cb544beab4 100644 --- a/packages/vue-router/package.json +++ b/packages/vue-router/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/vue-router", - "version": "8.3.4", + "version": "8.4.0", "description": "Vue Router integration for @ionic/vue", "scripts": { "test.spec": "jest", @@ -44,7 +44,7 @@ }, "homepage": "https://github.com/ionic-team/ionic-framework#readme", "dependencies": { - "@ionic/vue": "^8.3.4" + "@ionic/vue": "^8.4.0" }, "devDependencies": { "@ionic/eslint-config": "^0.3.0", diff --git a/packages/vue/CHANGELOG.md b/packages/vue/CHANGELOG.md index 59e84626d95..f7db26db10e 100644 --- a/packages/vue/CHANGELOG.md +++ b/packages/vue/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [8.4.0](https://github.com/ionic-team/ionic-framework/compare/v8.3.4...v8.4.0) (2024-11-04) + + +### Features + +* **segment-view:** adds support for new `ion-segment-view` component ([#29969](https://github.com/ionic-team/ionic-framework/issues/29969)) ([89508fb](https://github.com/ionic-team/ionic-framework/commit/89508fb89172900b1d11cc3fc18883f57a7fbab6)) +* **select:** add `modal` as interface ([#29972](https://github.com/ionic-team/ionic-framework/issues/29972)) ([3628ea8](https://github.com/ionic-team/ionic-framework/commit/3628ea875a66a717783de5e0a4df440872339040)) + + + + + ## [8.3.4](https://github.com/ionic-team/ionic-framework/compare/v8.3.3...v8.3.4) (2024-10-30) **Note:** Version bump only for package @ionic/vue diff --git a/packages/vue/package-lock.json b/packages/vue/package-lock.json index 2adae2a2acb..5733d9b5a62 100644 --- a/packages/vue/package-lock.json +++ b/packages/vue/package-lock.json @@ -1,15 +1,15 @@ { "name": "@ionic/vue", - "version": "8.3.4", + "version": "8.4.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@ionic/vue", - "version": "8.3.4", + "version": "8.4.0", "license": "MIT", "dependencies": { - "@ionic/core": "^8.3.4", + "@ionic/core": "^8.4.0", "ionicons": "^7.0.0" }, "devDependencies": { @@ -208,9 +208,9 @@ "dev": true }, "node_modules/@ionic/core": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.3.4.tgz", - "integrity": "sha512-MkgaQ9+oQwj3AK/i25MkLgKUUH3/nvNjd4YlmUyFjPG7l2IBHsrLlSmuFGCyniB+doI9Hynu3T9CZP7NS4RjRQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.4.0.tgz", + "integrity": "sha512-mZ2Ni9QByFGWBNr5W/F/nyPV+cXLhK+6W5BRziy7QPX6YIS57KH8FpY+CjE7BEcpE78anyY49bZt3eOWcES02g==", "dependencies": { "@stencil/core": "4.20.0", "ionicons": "^7.2.2", @@ -3970,9 +3970,9 @@ "dev": true }, "@ionic/core": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.3.4.tgz", - "integrity": "sha512-MkgaQ9+oQwj3AK/i25MkLgKUUH3/nvNjd4YlmUyFjPG7l2IBHsrLlSmuFGCyniB+doI9Hynu3T9CZP7NS4RjRQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.4.0.tgz", + "integrity": "sha512-mZ2Ni9QByFGWBNr5W/F/nyPV+cXLhK+6W5BRziy7QPX6YIS57KH8FpY+CjE7BEcpE78anyY49bZt3eOWcES02g==", "requires": { "@stencil/core": "4.20.0", "ionicons": "^7.2.2", diff --git a/packages/vue/package.json b/packages/vue/package.json index 86f48a1a0aa..e3920d69dfa 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/vue", - "version": "8.3.4", + "version": "8.4.0", "description": "Vue specific wrapper for @ionic/core", "scripts": { "eslint": "eslint src", @@ -66,7 +66,7 @@ "vue-router": "^4.0.16" }, "dependencies": { - "@ionic/core": "^8.3.4", + "@ionic/core": "^8.4.0", "ionicons": "^7.0.0" }, "vetur": { diff --git a/packages/vue/src/proxies.ts b/packages/vue/src/proxies.ts index 347673a7461..7dd5812ebbc 100644 --- a/packages/vue/src/proxies.ts +++ b/packages/vue/src/proxies.ts @@ -67,7 +67,10 @@ import { defineCustomElement as defineIonRow } from '@ionic/core/components/ion- import { defineCustomElement as defineIonSearchbar } from '@ionic/core/components/ion-searchbar.js'; import { defineCustomElement as defineIonSegment } from '@ionic/core/components/ion-segment.js'; import { defineCustomElement as defineIonSegmentButton } from '@ionic/core/components/ion-segment-button.js'; +import { defineCustomElement as defineIonSegmentContent } from '@ionic/core/components/ion-segment-content.js'; +import { defineCustomElement as defineIonSegmentView } from '@ionic/core/components/ion-segment-view.js'; import { defineCustomElement as defineIonSelect } from '@ionic/core/components/ion-select.js'; +import { defineCustomElement as defineIonSelectModal } from '@ionic/core/components/ion-select-modal.js'; import { defineCustomElement as defineIonSelectOption } from '@ionic/core/components/ion-select-option.js'; import { defineCustomElement as defineIonSkeletonText } from '@ionic/core/components/ion-skeleton-text.js'; import { defineCustomElement as defineIonSpinner } from '@ionic/core/components/ion-spinner.js'; @@ -744,6 +747,7 @@ export const IonSegment = /*@__PURE__*/ defineContainer('ion-segment-button', defineIonSegmentButton, [ + 'contentId', 'disabled', 'layout', 'type', @@ -752,6 +756,15 @@ export const IonSegmentButton = /*@__PURE__*/ defineContainer('ion-segment-content', defineIonSegmentContent); + + +export const IonSegmentView = /*@__PURE__*/ defineContainer('ion-segment-view', defineIonSegmentView, [ + 'disabled', + 'ionSegmentViewScroll' +]); + + export const IonSelect = /*@__PURE__*/ defineContainer('ion-select', defineIonSelect, [ 'cancelText', 'color', @@ -782,6 +795,13 @@ export const IonSelect = /*@__PURE__*/ defineContainer('ion-select-modal', defineIonSelectModal, [ + 'header', + 'multiple', + 'options' +]); + + export const IonSelectOption = /*@__PURE__*/ defineContainer('ion-select-option', defineIonSelectOption, [ 'disabled', 'value'