Skip to content

Commit

Permalink
feat(segment, segment-view): remove percentage based indicator effect…
Browse files Browse the repository at this point in the history
…s 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. -->
  • Loading branch information
tanner-reits committed Oct 25, 2024
1 parent 9494d43 commit f74f154
Show file tree
Hide file tree
Showing 24 changed files with 258 additions and 362 deletions.
8 changes: 3 additions & 5 deletions core/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1543,7 +1543,7 @@ ion-segment,css-prop,--background,md

ion-segment-button,shadow
ion-segment-button,prop,contentId,string | undefined,undefined,false,true
ion-segment-button,prop,disabled,boolean,false,false,false
ion-segment-button,prop,disabled,boolean,false,false,true
ion-segment-button,prop,layout,"icon-bottom" | "icon-end" | "icon-hide" | "icon-start" | "icon-top" | "label-hide" | undefined,'icon-top',false,false
ion-segment-button,prop,mode,"ios" | "md",undefined,false,false
ion-segment-button,prop,type,"button" | "reset" | "submit",'button',false,false
Expand Down Expand Up @@ -1609,13 +1609,11 @@ ion-segment-button,part,indicator-background
ion-segment-button,part,native

ion-segment-content,shadow
ion-segment-content,prop,disabled,boolean,false,false,false

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

ion-select,shadow
Expand Down
27 changes: 6 additions & 21 deletions core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { NavigationHookCallback } from "./components/route/route-interface";
import { SearchbarChangeEventDetail, SearchbarInputEventDetail } from "./components/searchbar/searchbar-interface";
import { SegmentChangeEventDetail, SegmentValue } from "./components/segment/segment-interface";
import { SegmentButtonLayout } from "./components/segment-button/segment-button-interface";
import { SegmentViewScrollEvent } from "./components/segment-view/segment-view-interface";
import { SelectChangeEventDetail, SelectCompareFn, SelectInterface } from "./components/select/select-interface";
import { SelectPopoverOption } from "./components/select-popover/select-popover-interface";
import { TabBarChangedEventDetail, TabButtonClickEventDetail, TabButtonLayout } from "./components/tab-bar/tab-bar-interface";
Expand Down Expand Up @@ -69,6 +70,7 @@ export { NavigationHookCallback } from "./components/route/route-interface";
export { SearchbarChangeEventDetail, SearchbarInputEventDetail } from "./components/searchbar/searchbar-interface";
export { SegmentChangeEventDetail, SegmentValue } from "./components/segment/segment-interface";
export { SegmentButtonLayout } from "./components/segment-button/segment-button-interface";
export { SegmentViewScrollEvent } from "./components/segment-view/segment-view-interface";
export { SelectChangeEventDetail, SelectCompareFn, SelectInterface } from "./components/select/select-interface";
export { SelectPopoverOption } from "./components/select-popover/select-popover-interface";
export { TabBarChangedEventDetail, TabButtonClickEventDetail, TabButtonLayout } from "./components/tab-bar/tab-bar-interface";
Expand Down Expand Up @@ -2720,18 +2722,13 @@ export namespace Components {
"value": SegmentValue;
}
interface IonSegmentContent {
/**
* If `true`, the segment content will not be displayed.
*/
"disabled": boolean;
}
interface IonSegmentView {
/**
* If `true`, the segment view cannot be interacted with.
*/
"disabled": boolean;
/**
* 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.
* @param id : The id of the segment content to display.
* @param smoothScroll : Whether to animate the scroll transition.
*/
Expand Down Expand Up @@ -4445,12 +4442,8 @@ declare global {
new (): HTMLIonSegmentContentElement;
};
interface HTMLIonSegmentViewElementEventMap {
"ionSegmentViewScroll": {
scrollDirection: string;
scrollDistance: number;
scrollDistancePercentage: number;
};
"ionSegmentViewScrollEnd": { activeContentId: string };
"ionSegmentViewScroll": SegmentViewScrollEvent;
"ionSegmentViewScrollEnd": void;
"ionSegmentViewScrollStart": void;
}
interface HTMLIonSegmentViewElement extends Components.IonSegmentView, HTMLStencilElement {
Expand Down Expand Up @@ -7533,10 +7526,6 @@ declare namespace LocalJSX {
"value"?: SegmentValue;
}
interface IonSegmentContent {
/**
* If `true`, the segment content will not be displayed.
*/
"disabled"?: boolean;
}
interface IonSegmentView {
/**
Expand All @@ -7546,15 +7535,11 @@ declare namespace LocalJSX {
/**
* Emitted when the segment view is scrolled.
*/
"onIonSegmentViewScroll"?: (event: IonSegmentViewCustomEvent<{
scrollDirection: string;
scrollDistance: number;
scrollDistancePercentage: number;
}>) => void;
"onIonSegmentViewScroll"?: (event: IonSegmentViewCustomEvent<SegmentViewScrollEvent>) => void;
/**
* Emitted when the segment view scroll has ended.
*/
"onIonSegmentViewScrollEnd"?: (event: IonSegmentViewCustomEvent<{ activeContentId: string }>) => void;
"onIonSegmentViewScrollEnd"?: (event: IonSegmentViewCustomEvent<void>) => void;
"onIonSegmentViewScrollStart"?: (event: IonSegmentViewCustomEvent<void>) => void;
}
interface IonSelect {
Expand Down
9 changes: 6 additions & 3 deletions core/src/components/segment-button/segment-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
/**
* If `true`, the user cannot interact with the segment button.
*/
@Prop({ mutable: true }) disabled = false;
@Prop({ mutable: true, reflect: true }) disabled = false;

/**
* Set the layout of the text and icon in the segment.
Expand Down Expand Up @@ -91,8 +91,11 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
return;
}

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

disconnectedCallback() {
Expand Down
4 changes: 0 additions & 4 deletions core/src/components/segment-content/segment-content.ios.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,3 @@

// iOS Segment Content
// --------------------------------------------------

:host(.segment-content-disabled) {
opacity: $segment-button-ios-opacity-disabled;
}
4 changes: 0 additions & 4 deletions core/src/components/segment-content/segment-content.md.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,3 @@

// Material Design Segment Content
// --------------------------------------------------

:host(.segment-content-disabled) {
opacity: $segment-button-md-opacity-disabled;
}
1 change: 1 addition & 0 deletions core/src/components/segment-content/segment-content.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

:host {
scroll-snap-align: center;
scroll-snap-stop: always;

flex-shrink: 0;

Expand Down
15 changes: 2 additions & 13 deletions core/src/components/segment-content/segment-content.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ComponentInterface } from '@stencil/core';
import { Component, Host, Prop, h } from '@stencil/core';
import { Component, Host, h } from '@stencil/core';

@Component({
tag: 'ion-segment-content',
Expand All @@ -10,20 +10,9 @@ import { Component, Host, Prop, h } from '@stencil/core';
shadow: true,
})
export class SegmentContent implements ComponentInterface {
/**
* If `true`, the segment content will not be displayed.
*/
@Prop() disabled = false;

render() {
const { disabled } = this;

return (
<Host
class={{
'segment-content-disabled': disabled,
}}
>
<Host>
<slot></slot>
</Host>
);
Expand Down
4 changes: 4 additions & 0 deletions core/src/components/segment-view/segment-view-interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface SegmentViewScrollEvent {
scrollRatio: number;
isManualScroll: boolean;
}
4 changes: 4 additions & 0 deletions core/src/components/segment-view/segment-view.scss
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,7 @@
touch-action: none;
overflow-x: hidden;
}

:host(.segment-view-scroll-disabled) {
pointer-events: none;
}
105 changes: 30 additions & 75 deletions core/src/components/segment-view/segment-view.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { ComponentInterface, EventEmitter } from '@stencil/core';
import { Component, Element, Event, Host, Listen, Method, Prop, h } from '@stencil/core';
import { Component, Element, Event, Host, Listen, Method, Prop, State, h } from '@stencil/core';

import type { SegmentViewScrollEvent } from './segment-view-interface';

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

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

/**
* @internal
*
* If `true`, the segment view is scrollable.
* If `false`, pointer events will be disabled. This is to prevent issues with
* quickly scrolling after interacting with a segment button.
*/
@State() isManualScroll?: boolean;

/**
* Emitted when the segment view is scrolled.
*/
@Event() ionSegmentViewScroll!: EventEmitter<{
scrollDirection: string;
scrollDistance: number;
scrollDistancePercentage: number;
}>;
@Event() ionSegmentViewScroll!: EventEmitter<SegmentViewScrollEvent>;

/**
* Emitted when the segment view scroll has ended.
*/
@Event() ionSegmentViewScrollEnd!: EventEmitter<{ activeContentId: string }>;
@Event() ionSegmentViewScrollEnd!: EventEmitter<void>;

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

private activeContentId = '';

@Listen('scroll')
handleScroll(ev: Event) {
const { initialScrollLeft, previousScrollLeft } = this;
const { scrollLeft, offsetWidth } = ev.target as HTMLElement;

// Set initial scroll position if it's undefined
this.initialScrollLeft = initialScrollLeft ?? scrollLeft;

// Determine the scroll direction based on the previous scroll position
const scrollDirection = scrollLeft > previousScrollLeft ? 'right' : 'left';
this.previousScrollLeft = scrollLeft;
const { scrollLeft, scrollWidth, clientWidth } = ev.target as HTMLElement;
const scrollRatio = scrollLeft / (scrollWidth - clientWidth);

// Calculate the distance scrolled based on the initial scroll position
// and then transform it to a percentage of the segment view width
const scrollDistance = scrollLeft - this.initialScrollLeft;
const scrollDistancePercentage = Math.abs(scrollDistance) / offsetWidth;

// Emit the scroll direction and distance
this.ionSegmentViewScroll.emit({
scrollDirection,
scrollDistance,
scrollDistancePercentage,
scrollRatio,
isManualScroll: this.isManualScroll ?? true,
});

// Check if the scroll is at a snapping point and return if not
const atSnappingPoint = scrollLeft % offsetWidth === 0;
if (!atSnappingPoint) return;

// Find the current segment content based on the scroll position
const currentIndex = Math.round(scrollLeft / offsetWidth);

// Recursively search for the next enabled content in the scroll direction
const segmentContent = this.getNextEnabledContent(currentIndex, scrollDirection);

// Exit if no valid segment content found
if (!segmentContent) return;

// Update active content ID and scroll to the segment content
this.activeContentId = segmentContent.id;
this.setContent(segmentContent.id);

// Reset the timeout to check for scroll end
this.resetScrollEndTimeout();
}
Expand Down Expand Up @@ -118,7 +90,7 @@ export class SegmentView implements ComponentInterface {
}
this.scrollEndTimeout = setTimeout(() => {
this.checkForScrollEnd();
}, 150);
}, 50);
}

/**
Expand All @@ -127,20 +99,21 @@ 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);

// Only emit scroll end event if the active content is not disabled and
// the user is not touching the segment view
if (activeContent?.disabled === false && !this.isTouching) {
this.ionSegmentViewScrollEnd.emit({ activeContentId: this.activeContentId });
this.initialScrollLeft = undefined;
if (!this.isTouching) {
this.ionSegmentViewScrollEnd.emit();
this.isManualScroll = undefined;
}
}

/**
* @internal
*
* 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.
*
* @param id: The id of the segment content to display.
* @param smoothScroll: Whether to animate the scroll transition.
*/
Expand All @@ -151,6 +124,9 @@ export class SegmentView implements ComponentInterface {

if (index === -1) return;

this.isManualScroll = false;
this.resetScrollEndTimeout();

const contentWidth = this.el.offsetWidth;
this.el.scrollTo({
top: 0,
Expand All @@ -163,35 +139,14 @@ export class SegmentView implements ComponentInterface {
return Array.from(this.el.querySelectorAll('ion-segment-content'));
}

/**
* Recursively find the next enabled segment content based on the scroll direction.
* If no enabled content is found, it will return null.
*/
private getNextEnabledContent(index: number, direction: string): HTMLIonSegmentContentElement | null {
const contents = this.getSegmentContents();

// Stop if we reach the beginning or end of the content array
if (index < 0 || index >= contents.length) return null;

const segmentContent = contents[index];

// If the content is not disabled, return it
if (!segmentContent.disabled) {
return segmentContent;
}

// Otherwise, keep searching in the same direction
const nextIndex = direction === 'right' ? index + 1 : index - 1;
return this.getNextEnabledContent(nextIndex, direction);
}

render() {
const { disabled } = this;
const { disabled, isManualScroll } = this;

return (
<Host
class={{
'segment-view-disabled': disabled,
'segment-view-scroll-disabled': isManualScroll === false,
}}
>
<slot></slot>
Expand Down
Loading

0 comments on commit f74f154

Please sign in to comment.