diff --git a/core/src/components.d.ts b/core/src/components.d.ts index e7ce3f3d40..3cd732f5d7 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -3435,6 +3435,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; @@ -4437,7 +4441,18 @@ declare global { prototype: HTMLIonSegmentContentElement; new (): HTMLIonSegmentContentElement; }; + interface HTMLIonSegmentViewElementEventMap { + "ionSegmentViewScroll": { scrollDirection: string; scrollDistance: number }; + } 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; @@ -7519,6 +7534,7 @@ declare namespace LocalJSX { * If `true`, the segment view cannot be interacted with. */ "disabled"?: boolean; + "onIonSegmentViewScroll"?: (event: IonSegmentViewCustomEvent<{ scrollDirection: string; scrollDistance: number }>) => void; } interface IonSelect { /** diff --git a/core/src/components/segment-button/segment-button.tsx b/core/src/components/segment-button/segment-button.tsx index c1dc1d2914..d12dc4369c 100644 --- a/core/src/components/segment-button/segment-button.tsx +++ b/core/src/components/segment-button/segment-button.tsx @@ -168,10 +168,7 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
diff --git a/core/src/components/segment-view/segment-view.tsx b/core/src/components/segment-view/segment-view.tsx index 4ab4088560..7df8193d12 100644 --- a/core/src/components/segment-view/segment-view.tsx +++ b/core/src/components/segment-view/segment-view.tsx @@ -1,5 +1,5 @@ -import type { ComponentInterface } from '@stencil/core'; -import { Component, Element, Host, Listen, Method, Prop, h } from '@stencil/core'; +import type { ComponentInterface, EventEmitter } from '@stencil/core'; +import { Component, Element, Event, Host, Listen, Method, Prop, h } from '@stencil/core'; @Component({ tag: 'ion-segment-view', @@ -10,6 +10,8 @@ import { Component, Element, Host, Listen, Method, Prop, h } from '@stencil/core shadow: true, }) export class SegmentView implements ComponentInterface { + private previousScrollLeft = 0; + @Element() el!: HTMLElement; /** @@ -17,9 +19,23 @@ export class SegmentView implements ComponentInterface { */ @Prop() disabled = false; + @Event() ionSegmentViewScroll!: EventEmitter<{ scrollDirection: string; scrollDistance: number }>; + @Listen('scroll') - handleScroll(ev: any) { - const { scrollLeft, offsetWidth } = ev.target; + handleScroll(ev: Event) { + const { scrollLeft, offsetWidth } = ev.target as HTMLElement; + + const scrollDirection = scrollLeft > this.previousScrollLeft ? 'right' : 'left'; + this.previousScrollLeft = scrollLeft; + + const scrollDistance = scrollLeft; + + // Emit the scroll direction and distance + this.ionSegmentViewScroll.emit({ + scrollDirection, + scrollDistance + }); + const atSnappingPoint = scrollLeft % offsetWidth === 0; if (!atSnappingPoint) return; @@ -57,7 +73,7 @@ export class SegmentView implements ComponentInterface { this.el.scrollTo({ top: 0, left: index * contentWidth, - behavior: smoothScroll ? 'smooth' : 'auto', + behavior: smoothScroll ? 'smooth' : 'instant', }); } diff --git a/core/src/components/segment/segment.tsx b/core/src/components/segment/segment.tsx index 34ae04099c..416d840cdb 100644 --- a/core/src/components/segment/segment.tsx +++ b/core/src/components/segment/segment.tsx @@ -27,6 +27,8 @@ export class Segment implements ComponentInterface { // Value before the segment is dragged private valueBeforeGesture?: SegmentValue; + private segmentViewEl?: HTMLIonSegmentViewElement | null = null; + @Element() el!: HTMLIonSegmentElement; @State() activated = false; @@ -142,6 +144,12 @@ export class Segment implements ComponentInterface { connectedCallback() { this.emitStyle(); + + this.segmentViewEl = this.getSegmentView(); + } + + disconnectedCallback() { + this.segmentViewEl = null; } componentWillLoad() { @@ -323,6 +331,60 @@ 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 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 segment view + // containing the segment contents that matches this segment's buttons + 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 || this.value === undefined) return; + + const index = buttons.findIndex((button) => button.value === this.value); + const current = buttons[index]; + const indicatorEl = this.getIndicator(current); + + const { scrollDirection, scrollDistance } = ev.detail; + + // Transform the indicator element to match the scroll of the segment view. + if (indicatorEl) { + indicatorEl.style.transition = 'transform 0.3s ease-out'; + + // Get dimensions of the segment and the button + const segmentRect = segmentEl.getBoundingClientRect(); + const buttonRect = current.getBoundingClientRect(); + + // Calculate the potential transform value based on scroll direction + const transformValue = scrollDirection === 'left' ? -scrollDistance : scrollDistance; + + // Calculate the max allowed transformation (indicator should not move beyond the segment boundaries) + const maxTransform = segmentRect.width - buttonRect.width; + const minTransform = 0; + + // Clamp the transform value to ensure it doesn't go out of bounds + const clampedTransform = Math.max(minTransform, Math.min(transformValue, maxTransform)); + + // Apply the clamped transform value to the indicator element + indicatorEl.style.transform = `translate3d(${clampedTransform}px, 0, 0)`; + } + } + } + /** * Finds the related segment view and sets its current content * based on the selected segment button. This method diff --git a/packages/angular/src/directives/proxies.ts b/packages/angular/src/directives/proxies.ts index 9d393aee59..55861be172 100644 --- a/packages/angular/src/directives/proxies.ts +++ b/packages/angular/src/directives/proxies.ts @@ -2043,11 +2043,15 @@ export class IonSegmentView { constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { c.detach(); this.el = r.nativeElement; + proxyOutputs(this, this.el, ['ionSegmentViewScroll']); } } -export declare interface IonSegmentView extends Components.IonSegmentView {} +export declare interface IonSegmentView extends Components.IonSegmentView { + + ionSegmentViewScroll: EventEmitter>; +} @ProxyCmp({ diff --git a/packages/angular/standalone/src/directives/proxies.ts b/packages/angular/standalone/src/directives/proxies.ts index 27242c007b..d8a7b7d0ee 100644 --- a/packages/angular/standalone/src/directives/proxies.ts +++ b/packages/angular/standalone/src/directives/proxies.ts @@ -1882,11 +1882,15 @@ export class IonSegmentView { constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { c.detach(); this.el = r.nativeElement; + proxyOutputs(this, this.el, ['ionSegmentViewScroll']); } } -export declare interface IonSegmentView extends Components.IonSegmentView {} +export declare interface IonSegmentView extends Components.IonSegmentView { + + ionSegmentViewScroll: EventEmitter>; +} @ProxyCmp({ diff --git a/packages/vue/src/proxies.ts b/packages/vue/src/proxies.ts index b433be92fe..b90135296c 100644 --- a/packages/vue/src/proxies.ts +++ b/packages/vue/src/proxies.ts @@ -761,7 +761,8 @@ export const IonSegmentContent = /*@__PURE__*/ defineContainer('ion-segment-view', defineIonSegmentView, [ - 'disabled' + 'disabled', + 'ionSegmentViewScroll' ]);