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