feat(segment): move indicator with scroll

This commit is contained in:
Brandy Carney
2024-09-24 19:22:46 -04:00
parent 798e725712
commit d811221750
7 changed files with 112 additions and 12 deletions

View File

@ -3435,6 +3435,10 @@ export interface IonSegmentCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLIonSegmentElement;
}
export interface IonSegmentViewCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLIonSegmentViewElement;
}
export interface IonSelectCustomEvent<T> extends CustomEvent<T> {
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<K extends keyof HTMLIonSegmentViewElementEventMap>(type: K, listener: (this: HTMLIonSegmentViewElement, ev: IonSegmentViewCustomEvent<HTMLIonSegmentViewElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof HTMLIonSegmentViewElementEventMap>(type: K, listener: (this: HTMLIonSegmentViewElement, ev: IonSegmentViewCustomEvent<HTMLIonSegmentViewElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof HTMLElementEventMap>(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 {
/**

View File

@ -168,10 +168,7 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
</button>
<div
part="indicator"
class={{
'segment-button-indicator': true,
'segment-button-indicator-animated': true,
}}
class="segment-button-indicator segment-button-indicator-animated"
>
<div part="indicator-background" class="segment-button-indicator-background"></div>
</div>

View File

@ -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',
});
}

View File

@ -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

View File

@ -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<CustomEvent<{ scrollDirection: string; scrollDistance: number }>>;
}
@ProxyCmp({

View File

@ -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<CustomEvent<{ scrollDirection: string; scrollDistance: number }>>;
}
@ProxyCmp({

View File

@ -761,7 +761,8 @@ export const IonSegmentContent = /*@__PURE__*/ defineContainer<JSX.IonSegmentCon
export const IonSegmentView = /*@__PURE__*/ defineContainer<JSX.IonSegmentView>('ion-segment-view', defineIonSegmentView, [
'disabled'
'disabled',
'ionSegmentViewScroll'
]);