diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 9e6042b40a..af8139f3db 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -2698,6 +2698,7 @@ export namespace Components { * If `true`, the user cannot interact with the segment button. */ "disabled": boolean; + "hasIndicator": boolean; /** * Set the layout of the text and icon in the segment. */ @@ -7512,6 +7513,7 @@ declare namespace LocalJSX { * If `true`, the user cannot interact with the segment button. */ "disabled"?: boolean; + "hasIndicator"?: boolean; /** * Set the layout of the text and icon in the segment. */ diff --git a/core/src/components/segment-button/segment-button.tsx b/core/src/components/segment-button/segment-button.tsx index 004c853a7e..c4e8cc739f 100644 --- a/core/src/components/segment-button/segment-button.tsx +++ b/core/src/components/segment-button/segment-button.tsx @@ -65,6 +65,8 @@ export class SegmentButton implements ComponentInterface, ButtonInterface { this.updateState(); } + @Prop() hasIndicator = true; + connectedCallback() { const segmentEl = (this.segmentEl = this.el.closest('ion-segment')); if (segmentEl) { @@ -187,9 +189,11 @@ export class SegmentButton implements ComponentInterface, ButtonInterface { {mode === 'md' && } -
-
-
+ {this.hasIndicator && ( +
+
+
+ )} ); } diff --git a/core/src/components/segment-content/segment-content.scss b/core/src/components/segment-content/segment-content.scss index 00ca64f304..464402b41f 100644 --- a/core/src/components/segment-content/segment-content.scss +++ b/core/src/components/segment-content/segment-content.scss @@ -3,6 +3,7 @@ :host { scroll-snap-align: center; + scroll-snap-stop: always; flex-shrink: 0; diff --git a/core/src/components/segment-view/segment-view.scss b/core/src/components/segment-view/segment-view.scss index e9eacc5a24..caeeafb21e 100644 --- a/core/src/components/segment-view/segment-view.scss +++ b/core/src/components/segment-view/segment-view.scss @@ -9,6 +9,8 @@ overflow-x: scroll; scroll-snap-type: x mandatory; + scroll-behavior: smooth; + /* Hide scrollbar in Firefox */ scrollbar-width: none; diff --git a/core/src/components/segment-view/segment-view.tsx b/core/src/components/segment-view/segment-view.tsx index 892c6e1f28..491876073e 100644 --- a/core/src/components/segment-view/segment-view.tsx +++ b/core/src/components/segment-view/segment-view.tsx @@ -127,7 +127,7 @@ export class SegmentView implements ComponentInterface { * reset the scroll position and emit the scroll end event. */ private checkForScrollEnd() { - const activeContent = this.getSegmentContents().find(content => content.id === this.activeContentId); + const activeContent = this.getSegmentContents().find((content) => content.id === this.activeContentId); // Only emit scroll end event if the active content is not disabled and // the user is not touching the segment view diff --git a/core/src/components/segment/segment.scss b/core/src/components/segment/segment.scss index f4e8ca9cfe..571b9c775c 100644 --- a/core/src/components/segment/segment.scss +++ b/core/src/components/segment/segment.scss @@ -31,8 +31,19 @@ contain: paint; user-select: none; -} + .segment-indicator { + @include transform-origin(left); + + position: absolute; + height: 100%; + + div { + background: red; + height: 100%; + } + } +} // Segment: Scrollable // -------------------------------------------------- diff --git a/core/src/components/segment/segment.tsx b/core/src/components/segment/segment.tsx index 671e33b0d3..243ebe0840 100644 --- a/core/src/components/segment/segment.tsx +++ b/core/src/components/segment/segment.tsx @@ -156,6 +156,10 @@ export class Segment implements ComponentInterface { this.emitStyle(); this.segmentViewEl = this.getSegmentView(); + + if (this.segmentViewEl) { + this.getButtons().forEach((ref) => (ref.hasIndicator = false)); + } } disconnectedCallback() { @@ -202,6 +206,28 @@ export class Segment implements ComponentInterface { // Update segment view based on the initial value, // but do not animate the scroll this.updateSegmentView(false); + + // TODO: this isn't always consistent, button width can sometimes be 0 + if (this.segmentViewEl) { + const buttons = this.getButtons(); + const activeButtonIndex = buttons.findIndex((ref) => ref.value === this.value); + if (activeButtonIndex >= 0) { + const activeButtonStyles = buttons[activeButtonIndex].getBoundingClientRect(); + const indicator = this.el.shadowRoot!.querySelector('.segment-indicator') as HTMLDivElement | null; + if (indicator) { + const startingX = buttons + .slice(0, activeButtonIndex) + .reduce((acc, ref) => acc + ref.getBoundingClientRect().width, 0); + + indicator.style.width = `${activeButtonStyles.width}px`; + indicator.style.left = `${startingX}px`; + + // setTimeout(() => { + // indicator.style.transition = 'left 0.3s linear, width 0.3s linear'; + // }); + } + } + } } onStart(detail: GestureDetail) { @@ -364,62 +390,36 @@ export class Segment implements ComponentInterface { // 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(); + const currentIndex = buttons.findIndex((button) => button.value === this.value); + const currentButton = buttons[currentIndex]; + const indicator = this.el.shadowRoot!.querySelector('.segment-indicator') as HTMLDivElement | null; // 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); - this.scrolledIndicator = indicatorEl; + if (!buttons.length || this.value === undefined || !indicator) return; const { scrollDistancePercentage, scrollDistance } = ev.detail; - if (indicatorEl && !isNaN(scrollDistancePercentage)) { - indicatorEl.style.transition = 'transform 0.3s ease-out'; + const nextIndex = scrollDistance > 0 ? currentIndex + 1 : currentIndex - 1; + if (nextIndex >= 0 && nextIndex < buttons.length) { + const nextButton = buttons[nextIndex]; + const nextButtonWidth = nextButton.getBoundingClientRect().width; - // Calculate the amount the indicator should move based on the scroll percentage - // and the width of the current button - const scrollAmount = scrollDistancePercentage * current.getBoundingClientRect().width; - const transformValue = scrollDistance < 0 ? -scrollAmount : scrollAmount; + // Scale the width based on the width of the next button + const diff = nextButtonWidth - currentButton.getBoundingClientRect().width; + const width = currentButton.getBoundingClientRect().width + diff * scrollDistancePercentage; + indicator.style.width = `${width}px`; - // Calculate total width of buttons to the left of the current button - const totalButtonWidthBefore = buttons - .slice(0, index) - .reduce((acc, button) => acc + button.getBoundingClientRect().width, 0); - - // Calculate total width of buttons to the right of the current button - const totalButtonWidthAfter = buttons - .slice(index + 1) - .reduce((acc, button) => acc + button.getBoundingClientRect().width, 0); - - // Set minTransform and maxTransform - const minTransform = -totalButtonWidthBefore; - const maxTransform = totalButtonWidthAfter; - - // 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 - const transform = `translate3d(${clampedTransform}px, 0, 0)`; - indicatorEl.style.setProperty('transform', transform); - - // Scroll the buttons if the indicator is out of view - const indicatorX = indicatorEl.getBoundingClientRect().x; - const buttonWidth = current.getBoundingClientRect().width; - if (scrollDistance < 0 && indicatorX < 0) { - this.el.scrollBy({ - top: 0, - left: indicatorX, - behavior: 'instant', - }); - } else if (scrollDistance > 0 && indicatorX + buttonWidth > this.el.offsetWidth) { - this.el.scrollBy({ - top: 0, - left: indicatorX + buttonWidth - this.el.offsetWidth, - behavior: 'instant', - }); - } + // Translate the indicator based on the scroll distance + const distanceToNextButton = buttons + .slice(0, nextIndex) + .reduce((acc, ref) => acc + ref.getBoundingClientRect().width, 0); + const distanceToCurrentButton = buttons + .slice(0, currentIndex) + .reduce((acc, ref) => acc + ref.getBoundingClientRect().width, 0); + indicator.style.left = + scrollDistance > 0 + ? `${distanceToCurrentButton + currentButton.getBoundingClientRect().width * scrollDistancePercentage}px` + : `${distanceToNextButton + nextButtonWidth - nextButtonWidth * scrollDistancePercentage}px`; } } } @@ -442,25 +442,9 @@ export class Segment implements ComponentInterface { const segmentEl = this.el; if (ev.composedPath().includes(segmentViewEl) || dispatchedFrom?.contains(segmentEl)) { - if (this.scrolledIndicator) { - const computedStyle = window.getComputedStyle(this.scrolledIndicator); - const isTransitioning = computedStyle.transitionDuration !== '0s'; - - if (isTransitioning) { - // Add a transitionend listener if the indicator is transitioning - this.waitForTransitionEnd(this.scrolledIndicator, () => { - this.updateValueAfterTransition(ev.detail.activeContentId); - }); - } else { - // Immediately update the value if there's no transition - this.updateValueAfterTransition(ev.detail.activeContentId); - } - } else { - // Immediately update the value if there's no indicator - this.updateValueAfterTransition(ev.detail.activeContentId); - } - this.isScrolling = false; + + this.value = ev.detail.activeContentId; } } @@ -771,6 +755,11 @@ export class Segment implements ComponentInterface { 'segment-scrollable': this.scrollable, })} > + {this.segmentViewEl && ( +
+
+
+ )} ); diff --git a/packages/angular/src/directives/proxies.ts b/packages/angular/src/directives/proxies.ts index 6e8e6303b1..aa28a5b56c 100644 --- a/packages/angular/src/directives/proxies.ts +++ b/packages/angular/src/directives/proxies.ts @@ -1984,14 +1984,14 @@ This event will not emit when programmatically setting the `value` property. @ProxyCmp({ - inputs: ['contentId', 'disabled', 'layout', 'mode', 'type', 'value'] + inputs: ['contentId', 'disabled', 'hasIndicator', 'layout', 'mode', 'type', 'value'] }) @Component({ selector: 'ion-segment-button', changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['contentId', 'disabled', 'layout', 'mode', 'type', 'value'], + inputs: ['contentId', 'disabled', 'hasIndicator', 'layout', 'mode', 'type', 'value'], }) export class IonSegmentButton { protected el: HTMLElement; diff --git a/packages/angular/standalone/src/directives/proxies.ts b/packages/angular/standalone/src/directives/proxies.ts index 35be08d93e..003e6f8950 100644 --- a/packages/angular/standalone/src/directives/proxies.ts +++ b/packages/angular/standalone/src/directives/proxies.ts @@ -1818,14 +1818,14 @@ export declare interface IonRow extends Components.IonRow {} @ProxyCmp({ defineCustomElementFn: defineIonSegmentButton, - inputs: ['contentId', 'disabled', 'layout', 'mode', 'type', 'value'] + inputs: ['contentId', 'disabled', 'hasIndicator', 'layout', 'mode', 'type', 'value'] }) @Component({ selector: 'ion-segment-button', changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['contentId', 'disabled', 'layout', 'mode', 'type', 'value'], + inputs: ['contentId', 'disabled', 'hasIndicator', 'layout', 'mode', 'type', 'value'], standalone: true }) export class IonSegmentButton { diff --git a/packages/vue/src/proxies.ts b/packages/vue/src/proxies.ts index d72ec0378c..f64ee29627 100644 --- a/packages/vue/src/proxies.ts +++ b/packages/vue/src/proxies.ts @@ -750,7 +750,8 @@ export const IonSegmentButton = /*@__PURE__*/ defineContainer