mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-17 18:54:11 +08:00
feat(segment): move indicator with scroll
This commit is contained in:
16
core/src/components.d.ts
vendored
16
core/src/components.d.ts
vendored
@ -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 {
|
||||
/**
|
||||
|
@ -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>
|
||||
|
@ -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',
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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({
|
||||
|
@ -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({
|
||||
|
@ -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'
|
||||
]);
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user