diff --git a/core/api.txt b/core/api.txt index 9c620e0c08..9e15366577 100644 --- a/core/api.txt +++ b/core/api.txt @@ -1606,6 +1606,10 @@ 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-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 diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 7c725cf2c8..33f2f13ad3 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -2712,6 +2712,10 @@ export namespace Components { */ "value": SegmentValue; } + interface IonSegmentContent { + } + interface IonSegmentView { + } interface IonSelect { /** * The text to display on the cancel button. @@ -4409,6 +4413,18 @@ declare global { prototype: HTMLIonSegmentButtonElement; new (): HTMLIonSegmentButtonElement; }; + interface HTMLIonSegmentContentElement extends Components.IonSegmentContent, HTMLStencilElement { + } + var HTMLIonSegmentContentElement: { + prototype: HTMLIonSegmentContentElement; + new (): HTMLIonSegmentContentElement; + }; + interface HTMLIonSegmentViewElement extends Components.IonSegmentView, HTMLStencilElement { + } + var HTMLIonSegmentViewElement: { + prototype: HTMLIonSegmentViewElement; + new (): HTMLIonSegmentViewElement; + }; interface HTMLIonSelectElementEventMap { "ionChange": SelectChangeEventDetail; "ionCancel": void; @@ -4718,6 +4734,8 @@ 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-option": HTMLIonSelectOptionElement; "ion-select-popover": HTMLIonSelectPopoverElement; @@ -7468,6 +7486,10 @@ declare namespace LocalJSX { */ "value"?: SegmentValue; } + interface IonSegmentContent { + } + interface IonSegmentView { + } interface IonSelect { /** * The text to display on the cancel button. @@ -8159,6 +8181,8 @@ 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-option": IonSelectOption; "ion-select-popover": IonSelectPopover; @@ -8258,6 +8282,8 @@ 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-option": LocalJSX.IonSelectOption & JSXBase.HTMLAttributes; "ion-select-popover": LocalJSX.IonSelectPopover & JSXBase.HTMLAttributes; diff --git a/core/src/components/segment-content/segment-content.scss b/core/src/components/segment-content/segment-content.scss new file mode 100644 index 0000000000..00ca64f304 --- /dev/null +++ b/core/src/components/segment-content/segment-content.scss @@ -0,0 +1,10 @@ +// Segment Content +// -------------------------------------------------- + +:host { + scroll-snap-align: center; + + flex-shrink: 0; + + width: 100%; +} diff --git a/core/src/components/segment-content/segment-content.tsx b/core/src/components/segment-content/segment-content.tsx new file mode 100644 index 0000000000..2b5fefebff --- /dev/null +++ b/core/src/components/segment-content/segment-content.tsx @@ -0,0 +1,17 @@ +import type { ComponentInterface } from '@stencil/core'; +import { Component, Host, h } from '@stencil/core'; + +@Component({ + tag: 'ion-segment-content', + styleUrl: 'segment-content.scss', + shadow: true, +}) +export class SegmentContent implements ComponentInterface { + render() { + return ( + + + + ); + } +} diff --git a/core/src/components/segment-view/segment-view.scss b/core/src/components/segment-view/segment-view.scss new file mode 100644 index 0000000000..eef3aa34c6 --- /dev/null +++ b/core/src/components/segment-view/segment-view.scss @@ -0,0 +1,20 @@ +// Segment View +// -------------------------------------------------- + +:host { + display: flex; + + overflow-x: scroll; + scroll-snap-type: x mandatory; + + /* Hide scrollbar in Firefox */ + scrollbar-width: none; + + /* Hide scrollbar in IE and Edge */ + -ms-overflow-style: none; +} + +/* Hide scrollbar in webkit */ +:host::-webkit-scrollbar { + display: none; +} diff --git a/core/src/components/segment-view/segment-view.tsx b/core/src/components/segment-view/segment-view.tsx new file mode 100644 index 0000000000..322efbc894 --- /dev/null +++ b/core/src/components/segment-view/segment-view.tsx @@ -0,0 +1,90 @@ +import type { ComponentInterface } from '@stencil/core'; +import { Component, Element, Host, Listen, h } from '@stencil/core'; +import { addEventListener, removeEventListener } from '@utils/helpers'; + +@Component({ + tag: 'ion-segment-view', + styleUrl: 'segment-view.scss', + shadow: true, +}) +export class SegmentView implements ComponentInterface { + private segmentEl: HTMLIonSegmentElement | null = null; + + @Element() el!: HTMLElement; + + @Listen('scroll') + segmentViewScroll(ev: any) { + const { segmentEl } = this; + + const atSnappingPoint = ev.target.scrollLeft % ev.target.offsetWidth === 0; + + if (atSnappingPoint) { + const index = Math.round(ev.target.scrollLeft / ev.target.offsetWidth); + const segmentButton = this.getSegmentButtonAtIndex(index); + + if (segmentEl) { + segmentEl.value = segmentButton.value; + } + } + } + + connectedCallback() { + const segmentEl = (this.segmentEl = document.querySelector(`ion-segment[view=${this.el.id}]`)); + if (segmentEl) { + addEventListener(segmentEl, 'ionChange', this.updateSection); + } + } + + disconnectedCallback() { + const segmentEl = this.segmentEl; + if (segmentEl) { + removeEventListener(segmentEl, 'ionChange', this.updateSection); + this.segmentEl = null; + } + } + + private updateSection = () => { + const { segmentEl } = this; + + if (segmentEl) { + const value = segmentEl.value; + const index = this.getSegmentButtonIndexWithValue(value); + this.setSection(index); + } + }; + + private setSection = (index: number) => { + const sectionWidth = this.el.offsetWidth; + this.el.scrollTo({ + top: 0, + left: index * sectionWidth, + behavior: 'smooth', + }); + }; + + private getSegmentButtons(): HTMLIonSegmentButtonElement[] { + const { segmentEl } = this; + + if (!segmentEl) { + return []; + } + + return Array.from(segmentEl.querySelectorAll('ion-segment-button')); + } + + private getSegmentButtonAtIndex(index: number) { + return this.getSegmentButtons()[index]; + } + + private getSegmentButtonIndexWithValue(value: any) { + return this.getSegmentButtons().findIndex((b) => b.value === value); + } + + render() { + return ( + + + + ); + } +} diff --git a/core/src/components/segment-view/test/basic/index.html b/core/src/components/segment-view/test/basic/index.html new file mode 100644 index 0000000000..7ead70ba96 --- /dev/null +++ b/core/src/components/segment-view/test/basic/index.html @@ -0,0 +1,55 @@ + + + + + Segment View - Basic + + + + + + + + + + + + + Segment View - Basic + + + + + + Paid + + + Free + + + Top + + + + + + + + + Paid + + + Free + + + Top + + + + + + + diff --git a/packages/angular/src/directives/proxies-list.ts b/packages/angular/src/directives/proxies-list.ts index 1874d0bfe2..69247d0142 100644 --- a/packages/angular/src/directives/proxies-list.ts +++ b/packages/angular/src/directives/proxies-list.ts @@ -69,6 +69,8 @@ export const DIRECTIVES = [ d.IonSearchbar, d.IonSegment, d.IonSegmentButton, + d.IonSegmentContent, + d.IonSegmentView, d.IonSelect, d.IonSelectOption, d.IonSkeletonText, diff --git a/packages/angular/src/directives/proxies.ts b/packages/angular/src/directives/proxies.ts index f448236a16..b68949142f 100644 --- a/packages/angular/src/directives/proxies.ts +++ b/packages/angular/src/directives/proxies.ts @@ -2005,6 +2005,48 @@ 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({ +}) +@Component({ + selector: 'ion-segment-view', + changeDetection: ChangeDetectionStrategy.OnPush, + template: '', + // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property + inputs: [], +}) +export class IonSegmentView { + protected el: HTMLElement; + constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { + c.detach(); + this.el = r.nativeElement; + } +} + + +export declare interface IonSegmentView extends Components.IonSegmentView {} + + @ProxyCmp({ inputs: ['cancelText', 'color', 'compareWith', 'disabled', 'expandedIcon', 'fill', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'selectedText', 'shape', 'toggleIcon', 'value'], methods: ['open'] diff --git a/packages/angular/standalone/src/directives/proxies.ts b/packages/angular/standalone/src/directives/proxies.ts index c84717dfd1..62c918ea8a 100644 --- a/packages/angular/standalone/src/directives/proxies.ts +++ b/packages/angular/standalone/src/directives/proxies.ts @@ -65,6 +65,8 @@ 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 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'; @@ -1838,6 +1840,52 @@ 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 +}) +@Component({ + selector: 'ion-segment-view', + changeDetection: ChangeDetectionStrategy.OnPush, + template: '', + // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property + inputs: [], + standalone: true +}) +export class IonSegmentView { + protected el: HTMLElement; + constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { + c.detach(); + this.el = r.nativeElement; + } +} + + +export declare interface IonSegmentView extends Components.IonSegmentView {} + + @ProxyCmp({ defineCustomElementFn: defineIonSelectOption, inputs: ['disabled', 'value'] diff --git a/packages/react/src/components/proxies.ts b/packages/react/src/components/proxies.ts index 05800f3877..e85e9f0ec1 100644 --- a/packages/react/src/components/proxies.ts +++ b/packages/react/src/components/proxies.ts @@ -61,6 +61,8 @@ 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 defineIonSelectOption } from '@ionic/core/components/ion-select-option.js'; import { defineCustomElement as defineIonSkeletonText } from '@ionic/core/components/ion-skeleton-text.js'; @@ -130,6 +132,8 @@ 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 IonSelectOption = /*@__PURE__*/createReactComponent('ion-select-option', undefined, undefined, defineIonSelectOption); export const IonSkeletonText = /*@__PURE__*/createReactComponent('ion-skeleton-text', undefined, undefined, defineIonSkeletonText); diff --git a/packages/vue/src/proxies.ts b/packages/vue/src/proxies.ts index 347673a746..4e2b1c624f 100644 --- a/packages/vue/src/proxies.ts +++ b/packages/vue/src/proxies.ts @@ -67,6 +67,8 @@ 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 defineIonSelectOption } from '@ionic/core/components/ion-select-option.js'; import { defineCustomElement as defineIonSkeletonText } from '@ionic/core/components/ion-skeleton-text.js'; @@ -752,6 +754,12 @@ export const IonSegmentButton = /*@__PURE__*/ defineContainer('ion-segment-content', defineIonSegmentContent); + + +export const IonSegmentView = /*@__PURE__*/ defineContainer('ion-segment-view', defineIonSegmentView); + + export const IonSelect = /*@__PURE__*/ defineContainer('ion-select', defineIonSelect, [ 'cancelText', 'color',