mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-18 03:00:58 +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;
|
detail: T;
|
||||||
target: HTMLIonSegmentElement;
|
target: HTMLIonSegmentElement;
|
||||||
}
|
}
|
||||||
|
export interface IonSegmentViewCustomEvent<T> extends CustomEvent<T> {
|
||||||
|
detail: T;
|
||||||
|
target: HTMLIonSegmentViewElement;
|
||||||
|
}
|
||||||
export interface IonSelectCustomEvent<T> extends CustomEvent<T> {
|
export interface IonSelectCustomEvent<T> extends CustomEvent<T> {
|
||||||
detail: T;
|
detail: T;
|
||||||
target: HTMLIonSelectElement;
|
target: HTMLIonSelectElement;
|
||||||
@ -4437,7 +4441,18 @@ declare global {
|
|||||||
prototype: HTMLIonSegmentContentElement;
|
prototype: HTMLIonSegmentContentElement;
|
||||||
new (): HTMLIonSegmentContentElement;
|
new (): HTMLIonSegmentContentElement;
|
||||||
};
|
};
|
||||||
|
interface HTMLIonSegmentViewElementEventMap {
|
||||||
|
"ionSegmentViewScroll": { scrollDirection: string; scrollDistance: number };
|
||||||
|
}
|
||||||
interface HTMLIonSegmentViewElement extends Components.IonSegmentView, HTMLStencilElement {
|
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: {
|
var HTMLIonSegmentViewElement: {
|
||||||
prototype: HTMLIonSegmentViewElement;
|
prototype: HTMLIonSegmentViewElement;
|
||||||
@ -7519,6 +7534,7 @@ declare namespace LocalJSX {
|
|||||||
* If `true`, the segment view cannot be interacted with.
|
* If `true`, the segment view cannot be interacted with.
|
||||||
*/
|
*/
|
||||||
"disabled"?: boolean;
|
"disabled"?: boolean;
|
||||||
|
"onIonSegmentViewScroll"?: (event: IonSegmentViewCustomEvent<{ scrollDirection: string; scrollDistance: number }>) => void;
|
||||||
}
|
}
|
||||||
interface IonSelect {
|
interface IonSelect {
|
||||||
/**
|
/**
|
||||||
|
@ -168,10 +168,7 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
|
|||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
part="indicator"
|
part="indicator"
|
||||||
class={{
|
class="segment-button-indicator segment-button-indicator-animated"
|
||||||
'segment-button-indicator': true,
|
|
||||||
'segment-button-indicator-animated': true,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div part="indicator-background" class="segment-button-indicator-background"></div>
|
<div part="indicator-background" class="segment-button-indicator-background"></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import type { ComponentInterface } from '@stencil/core';
|
import type { ComponentInterface, EventEmitter } from '@stencil/core';
|
||||||
import { Component, Element, Host, Listen, Method, Prop, h } from '@stencil/core';
|
import { Component, Element, Event, Host, Listen, Method, Prop, h } from '@stencil/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
tag: 'ion-segment-view',
|
tag: 'ion-segment-view',
|
||||||
@ -10,6 +10,8 @@ import { Component, Element, Host, Listen, Method, Prop, h } from '@stencil/core
|
|||||||
shadow: true,
|
shadow: true,
|
||||||
})
|
})
|
||||||
export class SegmentView implements ComponentInterface {
|
export class SegmentView implements ComponentInterface {
|
||||||
|
private previousScrollLeft = 0;
|
||||||
|
|
||||||
@Element() el!: HTMLElement;
|
@Element() el!: HTMLElement;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -17,9 +19,23 @@ export class SegmentView implements ComponentInterface {
|
|||||||
*/
|
*/
|
||||||
@Prop() disabled = false;
|
@Prop() disabled = false;
|
||||||
|
|
||||||
|
@Event() ionSegmentViewScroll!: EventEmitter<{ scrollDirection: string; scrollDistance: number }>;
|
||||||
|
|
||||||
@Listen('scroll')
|
@Listen('scroll')
|
||||||
handleScroll(ev: any) {
|
handleScroll(ev: Event) {
|
||||||
const { scrollLeft, offsetWidth } = ev.target;
|
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;
|
const atSnappingPoint = scrollLeft % offsetWidth === 0;
|
||||||
|
|
||||||
if (!atSnappingPoint) return;
|
if (!atSnappingPoint) return;
|
||||||
@ -57,7 +73,7 @@ export class SegmentView implements ComponentInterface {
|
|||||||
this.el.scrollTo({
|
this.el.scrollTo({
|
||||||
top: 0,
|
top: 0,
|
||||||
left: index * contentWidth,
|
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
|
// Value before the segment is dragged
|
||||||
private valueBeforeGesture?: SegmentValue;
|
private valueBeforeGesture?: SegmentValue;
|
||||||
|
|
||||||
|
private segmentViewEl?: HTMLIonSegmentViewElement | null = null;
|
||||||
|
|
||||||
@Element() el!: HTMLIonSegmentElement;
|
@Element() el!: HTMLIonSegmentElement;
|
||||||
|
|
||||||
@State() activated = false;
|
@State() activated = false;
|
||||||
@ -142,6 +144,12 @@ export class Segment implements ComponentInterface {
|
|||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
this.emitStyle();
|
this.emitStyle();
|
||||||
|
|
||||||
|
this.segmentViewEl = this.getSegmentView();
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
this.segmentViewEl = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillLoad() {
|
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
|
* Finds the related segment view and sets its current content
|
||||||
* based on the selected segment button. This method
|
* based on the selected segment button. This method
|
||||||
|
@ -2043,11 +2043,15 @@ export class IonSegmentView {
|
|||||||
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
|
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
|
||||||
c.detach();
|
c.detach();
|
||||||
this.el = r.nativeElement;
|
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({
|
@ProxyCmp({
|
||||||
|
@ -1882,11 +1882,15 @@ export class IonSegmentView {
|
|||||||
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
|
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
|
||||||
c.detach();
|
c.detach();
|
||||||
this.el = r.nativeElement;
|
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({
|
@ProxyCmp({
|
||||||
|
@ -761,7 +761,8 @@ export const IonSegmentContent = /*@__PURE__*/ defineContainer<JSX.IonSegmentCon
|
|||||||
|
|
||||||
|
|
||||||
export const IonSegmentView = /*@__PURE__*/ defineContainer<JSX.IonSegmentView>('ion-segment-view', defineIonSegmentView, [
|
export const IonSegmentView = /*@__PURE__*/ defineContainer<JSX.IonSegmentView>('ion-segment-view', defineIonSegmentView, [
|
||||||
'disabled'
|
'disabled',
|
||||||
|
'ionSegmentViewScroll'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user