Skip to content

Commit 622045f

Browse files
authored
feat(segment, segment-view): remove percentage based indicator effects on scroll (#29968)
Issue number: resolves internal --------- <!-- Please do not submit updates to dependencies unless it fixes an issue. --> <!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. --> ## What is the current behavior? <!-- Please describe the current behavior that you are modifying. --> ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> - - - ## Does this introduce a breaking change? - [ ] Yes - [ ] No <!-- If this introduces a breaking change: 1. Describe the impact and migration path for existing applications below. 2. Update the BREAKING.md file with the breaking change. 3. Add "BREAKING CHANGE: [...]" to the commit description when merging. See https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer for more information. --> ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. -->
1 parent 475de8b commit 622045f

24 files changed

+258
-362
lines changed

core/api.txt

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1542,7 +1542,7 @@ ion-segment,css-prop,--background,md
15421542

15431543
ion-segment-button,shadow
15441544
ion-segment-button,prop,contentId,string | undefined,undefined,false,true
1545-
ion-segment-button,prop,disabled,boolean,false,false,false
1545+
ion-segment-button,prop,disabled,boolean,false,false,true
15461546
ion-segment-button,prop,layout,"icon-bottom" | "icon-end" | "icon-hide" | "icon-start" | "icon-top" | "label-hide" | undefined,'icon-top',false,false
15471547
ion-segment-button,prop,mode,"ios" | "md",undefined,false,false
15481548
ion-segment-button,prop,type,"button" | "reset" | "submit",'button',false,false
@@ -1608,13 +1608,11 @@ ion-segment-button,part,indicator-background
16081608
ion-segment-button,part,native
16091609

16101610
ion-segment-content,shadow
1611-
ion-segment-content,prop,disabled,boolean,false,false,false
16121611

16131612
ion-segment-view,shadow
16141613
ion-segment-view,prop,disabled,boolean,false,false,false
1615-
ion-segment-view,method,setContent,setContent(id: string, smoothScroll?: boolean) => Promise<void>
1616-
ion-segment-view,event,ionSegmentViewScroll,{ scrollDirection: string; scrollDistance: number; scrollDistancePercentage: number; },true
1617-
ion-segment-view,event,ionSegmentViewScrollEnd,{ activeContentId: string; },true
1614+
ion-segment-view,event,ionSegmentViewScroll,SegmentViewScrollEvent,true
1615+
ion-segment-view,event,ionSegmentViewScrollEnd,void,true
16181616
ion-segment-view,event,ionSegmentViewScrollStart,void,true
16191617

16201618
ion-select,shadow

core/src/components.d.ts

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { NavigationHookCallback } from "./components/route/route-interface";
3434
import { SearchbarChangeEventDetail, SearchbarInputEventDetail } from "./components/searchbar/searchbar-interface";
3535
import { SegmentChangeEventDetail, SegmentValue } from "./components/segment/segment-interface";
3636
import { SegmentButtonLayout } from "./components/segment-button/segment-button-interface";
37+
import { SegmentViewScrollEvent } from "./components/segment-view/segment-view-interface";
3738
import { SelectChangeEventDetail, SelectCompareFn, SelectInterface } from "./components/select/select-interface";
3839
import { SelectPopoverOption } from "./components/select-popover/select-popover-interface";
3940
import { TabBarChangedEventDetail, TabButtonClickEventDetail, TabButtonLayout } from "./components/tab-bar/tab-bar-interface";
@@ -69,6 +70,7 @@ export { NavigationHookCallback } from "./components/route/route-interface";
6970
export { SearchbarChangeEventDetail, SearchbarInputEventDetail } from "./components/searchbar/searchbar-interface";
7071
export { SegmentChangeEventDetail, SegmentValue } from "./components/segment/segment-interface";
7172
export { SegmentButtonLayout } from "./components/segment-button/segment-button-interface";
73+
export { SegmentViewScrollEvent } from "./components/segment-view/segment-view-interface";
7274
export { SelectChangeEventDetail, SelectCompareFn, SelectInterface } from "./components/select/select-interface";
7375
export { SelectPopoverOption } from "./components/select-popover/select-popover-interface";
7476
export { TabBarChangedEventDetail, TabButtonClickEventDetail, TabButtonLayout } from "./components/tab-bar/tab-bar-interface";
@@ -2717,18 +2719,13 @@ export namespace Components {
27172719
"value": SegmentValue;
27182720
}
27192721
interface IonSegmentContent {
2720-
/**
2721-
* If `true`, the segment content will not be displayed.
2722-
*/
2723-
"disabled": boolean;
27242722
}
27252723
interface IonSegmentView {
27262724
/**
27272725
* If `true`, the segment view cannot be interacted with.
27282726
*/
27292727
"disabled": boolean;
27302728
/**
2731-
* This method is used to programmatically set the displayed segment content in the segment view. Calling this method will update the `value` of the corresponding segment button.
27322729
* @param id : The id of the segment content to display.
27332730
* @param smoothScroll : Whether to animate the scroll transition.
27342731
*/
@@ -4442,12 +4439,8 @@ declare global {
44424439
new (): HTMLIonSegmentContentElement;
44434440
};
44444441
interface HTMLIonSegmentViewElementEventMap {
4445-
"ionSegmentViewScroll": {
4446-
scrollDirection: string;
4447-
scrollDistance: number;
4448-
scrollDistancePercentage: number;
4449-
};
4450-
"ionSegmentViewScrollEnd": { activeContentId: string };
4442+
"ionSegmentViewScroll": SegmentViewScrollEvent;
4443+
"ionSegmentViewScrollEnd": void;
44514444
"ionSegmentViewScrollStart": void;
44524445
}
44534446
interface HTMLIonSegmentViewElement extends Components.IonSegmentView, HTMLStencilElement {
@@ -7530,10 +7523,6 @@ declare namespace LocalJSX {
75307523
"value"?: SegmentValue;
75317524
}
75327525
interface IonSegmentContent {
7533-
/**
7534-
* If `true`, the segment content will not be displayed.
7535-
*/
7536-
"disabled"?: boolean;
75377526
}
75387527
interface IonSegmentView {
75397528
/**
@@ -7543,15 +7532,11 @@ declare namespace LocalJSX {
75437532
/**
75447533
* Emitted when the segment view is scrolled.
75457534
*/
7546-
"onIonSegmentViewScroll"?: (event: IonSegmentViewCustomEvent<{
7547-
scrollDirection: string;
7548-
scrollDistance: number;
7549-
scrollDistancePercentage: number;
7550-
}>) => void;
7535+
"onIonSegmentViewScroll"?: (event: IonSegmentViewCustomEvent<SegmentViewScrollEvent>) => void;
75517536
/**
75527537
* Emitted when the segment view scroll has ended.
75537538
*/
7554-
"onIonSegmentViewScrollEnd"?: (event: IonSegmentViewCustomEvent<{ activeContentId: string }>) => void;
7539+
"onIonSegmentViewScrollEnd"?: (event: IonSegmentViewCustomEvent<void>) => void;
75557540
"onIonSegmentViewScrollStart"?: (event: IonSegmentViewCustomEvent<void>) => void;
75567541
}
75577542
interface IonSelect {

core/src/components/segment-button/segment-button.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
4444
/**
4545
* If `true`, the user cannot interact with the segment button.
4646
*/
47-
@Prop({ mutable: true }) disabled = false;
47+
@Prop({ mutable: true, reflect: true }) disabled = false;
4848

4949
/**
5050
* Set the layout of the text and icon in the segment.
@@ -91,8 +91,11 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
9191
return;
9292
}
9393

94-
// Set the disabled state of the Segment Content based on the button's disabled state
95-
segmentContent.disabled = this.disabled;
94+
// Prevent buttons from being disabled when associated with segment content
95+
if (this.disabled) {
96+
console.warn(`Segment Button: Segment buttons cannot be disabled when associated with an <ion-segment-content>.`);
97+
this.disabled = false;
98+
}
9699
}
97100

98101
disconnectedCallback() {

core/src/components/segment-content/segment-content.ios.scss

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,3 @@
33

44
// iOS Segment Content
55
// --------------------------------------------------
6-
7-
:host(.segment-content-disabled) {
8-
opacity: $segment-button-ios-opacity-disabled;
9-
}

core/src/components/segment-content/segment-content.md.scss

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,3 @@
33

44
// Material Design Segment Content
55
// --------------------------------------------------
6-
7-
:host(.segment-content-disabled) {
8-
opacity: $segment-button-md-opacity-disabled;
9-
}

core/src/components/segment-content/segment-content.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
:host {
55
scroll-snap-align: center;
6+
scroll-snap-stop: always;
67

78
flex-shrink: 0;
89

core/src/components/segment-content/segment-content.tsx

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ComponentInterface } from '@stencil/core';
2-
import { Component, Host, Prop, h } from '@stencil/core';
2+
import { Component, Host, h } from '@stencil/core';
33

44
@Component({
55
tag: 'ion-segment-content',
@@ -10,20 +10,9 @@ import { Component, Host, Prop, h } from '@stencil/core';
1010
shadow: true,
1111
})
1212
export class SegmentContent implements ComponentInterface {
13-
/**
14-
* If `true`, the segment content will not be displayed.
15-
*/
16-
@Prop() disabled = false;
17-
1813
render() {
19-
const { disabled } = this;
20-
2114
return (
22-
<Host
23-
class={{
24-
'segment-content-disabled': disabled,
25-
}}
26-
>
15+
<Host>
2716
<slot></slot>
2817
</Host>
2918
);
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export interface SegmentViewScrollEvent {
2+
scrollRatio: number;
3+
isManualScroll: boolean;
4+
}

core/src/components/segment-view/segment-view.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,7 @@
2525
touch-action: none;
2626
overflow-x: hidden;
2727
}
28+
29+
:host(.segment-view-scroll-disabled) {
30+
pointer-events: none;
31+
}

core/src/components/segment-view/segment-view.tsx

Lines changed: 30 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import type { ComponentInterface, EventEmitter } from '@stencil/core';
2-
import { Component, Element, Event, Host, Listen, Method, Prop, h } from '@stencil/core';
2+
import { Component, Element, Event, Host, Listen, Method, Prop, State, h } from '@stencil/core';
3+
4+
import type { SegmentViewScrollEvent } from './segment-view-interface';
35

46
@Component({
57
tag: 'ion-segment-view',
@@ -10,8 +12,6 @@ import { Component, Element, Event, Host, Listen, Method, Prop, h } from '@stenc
1012
shadow: true,
1113
})
1214
export class SegmentView implements ComponentInterface {
13-
private initialScrollLeft?: number;
14-
private previousScrollLeft = 0;
1515
private scrollEndTimeout: ReturnType<typeof setTimeout> | null = null;
1616
private isTouching = false;
1717

@@ -22,65 +22,37 @@ export class SegmentView implements ComponentInterface {
2222
*/
2323
@Prop() disabled = false;
2424

25+
/**
26+
* @internal
27+
*
28+
* If `true`, the segment view is scrollable.
29+
* If `false`, pointer events will be disabled. This is to prevent issues with
30+
* quickly scrolling after interacting with a segment button.
31+
*/
32+
@State() isManualScroll?: boolean;
33+
2534
/**
2635
* Emitted when the segment view is scrolled.
2736
*/
28-
@Event() ionSegmentViewScroll!: EventEmitter<{
29-
scrollDirection: string;
30-
scrollDistance: number;
31-
scrollDistancePercentage: number;
32-
}>;
37+
@Event() ionSegmentViewScroll!: EventEmitter<SegmentViewScrollEvent>;
3338

3439
/**
3540
* Emitted when the segment view scroll has ended.
3641
*/
37-
@Event() ionSegmentViewScrollEnd!: EventEmitter<{ activeContentId: string }>;
42+
@Event() ionSegmentViewScrollEnd!: EventEmitter<void>;
3843

3944
@Event() ionSegmentViewScrollStart!: EventEmitter<void>;
4045

41-
private activeContentId = '';
42-
4346
@Listen('scroll')
4447
handleScroll(ev: Event) {
45-
const { initialScrollLeft, previousScrollLeft } = this;
46-
const { scrollLeft, offsetWidth } = ev.target as HTMLElement;
47-
48-
// Set initial scroll position if it's undefined
49-
this.initialScrollLeft = initialScrollLeft ?? scrollLeft;
50-
51-
// Determine the scroll direction based on the previous scroll position
52-
const scrollDirection = scrollLeft > previousScrollLeft ? 'right' : 'left';
53-
this.previousScrollLeft = scrollLeft;
48+
const { scrollLeft, scrollWidth, clientWidth } = ev.target as HTMLElement;
49+
const scrollRatio = scrollLeft / (scrollWidth - clientWidth);
5450

55-
// Calculate the distance scrolled based on the initial scroll position
56-
// and then transform it to a percentage of the segment view width
57-
const scrollDistance = scrollLeft - this.initialScrollLeft;
58-
const scrollDistancePercentage = Math.abs(scrollDistance) / offsetWidth;
59-
60-
// Emit the scroll direction and distance
6151
this.ionSegmentViewScroll.emit({
62-
scrollDirection,
63-
scrollDistance,
64-
scrollDistancePercentage,
52+
scrollRatio,
53+
isManualScroll: this.isManualScroll ?? true,
6554
});
6655

67-
// Check if the scroll is at a snapping point and return if not
68-
const atSnappingPoint = scrollLeft % offsetWidth === 0;
69-
if (!atSnappingPoint) return;
70-
71-
// Find the current segment content based on the scroll position
72-
const currentIndex = Math.round(scrollLeft / offsetWidth);
73-
74-
// Recursively search for the next enabled content in the scroll direction
75-
const segmentContent = this.getNextEnabledContent(currentIndex, scrollDirection);
76-
77-
// Exit if no valid segment content found
78-
if (!segmentContent) return;
79-
80-
// Update active content ID and scroll to the segment content
81-
this.activeContentId = segmentContent.id;
82-
this.setContent(segmentContent.id);
83-
8456
// Reset the timeout to check for scroll end
8557
this.resetScrollEndTimeout();
8658
}
@@ -118,7 +90,7 @@ export class SegmentView implements ComponentInterface {
11890
}
11991
this.scrollEndTimeout = setTimeout(() => {
12092
this.checkForScrollEnd();
121-
}, 150);
93+
}, 50);
12294
}
12395

12496
/**
@@ -127,20 +99,21 @@ export class SegmentView implements ComponentInterface {
12799
* reset the scroll position and emit the scroll end event.
128100
*/
129101
private checkForScrollEnd() {
130-
const activeContent = this.getSegmentContents().find(content => content.id === this.activeContentId);
131-
132102
// Only emit scroll end event if the active content is not disabled and
133103
// the user is not touching the segment view
134-
if (activeContent?.disabled === false && !this.isTouching) {
135-
this.ionSegmentViewScrollEnd.emit({ activeContentId: this.activeContentId });
136-
this.initialScrollLeft = undefined;
104+
if (!this.isTouching) {
105+
this.ionSegmentViewScrollEnd.emit();
106+
this.isManualScroll = undefined;
137107
}
138108
}
139109

140110
/**
111+
* @internal
112+
*
141113
* This method is used to programmatically set the displayed segment content
142114
* in the segment view. Calling this method will update the `value` of the
143115
* corresponding segment button.
116+
*
144117
* @param id: The id of the segment content to display.
145118
* @param smoothScroll: Whether to animate the scroll transition.
146119
*/
@@ -151,6 +124,9 @@ export class SegmentView implements ComponentInterface {
151124

152125
if (index === -1) return;
153126

127+
this.isManualScroll = false;
128+
this.resetScrollEndTimeout();
129+
154130
const contentWidth = this.el.offsetWidth;
155131
this.el.scrollTo({
156132
top: 0,
@@ -163,35 +139,14 @@ export class SegmentView implements ComponentInterface {
163139
return Array.from(this.el.querySelectorAll('ion-segment-content'));
164140
}
165141

166-
/**
167-
* Recursively find the next enabled segment content based on the scroll direction.
168-
* If no enabled content is found, it will return null.
169-
*/
170-
private getNextEnabledContent(index: number, direction: string): HTMLIonSegmentContentElement | null {
171-
const contents = this.getSegmentContents();
172-
173-
// Stop if we reach the beginning or end of the content array
174-
if (index < 0 || index >= contents.length) return null;
175-
176-
const segmentContent = contents[index];
177-
178-
// If the content is not disabled, return it
179-
if (!segmentContent.disabled) {
180-
return segmentContent;
181-
}
182-
183-
// Otherwise, keep searching in the same direction
184-
const nextIndex = direction === 'right' ? index + 1 : index - 1;
185-
return this.getNextEnabledContent(nextIndex, direction);
186-
}
187-
188142
render() {
189-
const { disabled } = this;
143+
const { disabled, isManualScroll } = this;
190144

191145
return (
192146
<Host
193147
class={{
194148
'segment-view-disabled': disabled,
149+
'segment-view-scroll-disabled': isManualScroll === false,
195150
}}
196151
>
197152
<slot></slot>

0 commit comments

Comments
 (0)