mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-18 19:21:34 +08:00
support translation & width scaling
This commit is contained in:
2
core/src/components.d.ts
vendored
2
core/src/components.d.ts
vendored
@ -2698,6 +2698,7 @@ export namespace Components {
|
||||
* If `true`, the user cannot interact with the segment button.
|
||||
*/
|
||||
"disabled": boolean;
|
||||
"hasIndicator": boolean;
|
||||
/**
|
||||
* Set the layout of the text and icon in the segment.
|
||||
*/
|
||||
@ -7512,6 +7513,7 @@ declare namespace LocalJSX {
|
||||
* If `true`, the user cannot interact with the segment button.
|
||||
*/
|
||||
"disabled"?: boolean;
|
||||
"hasIndicator"?: boolean;
|
||||
/**
|
||||
* Set the layout of the text and icon in the segment.
|
||||
*/
|
||||
|
@ -65,6 +65,8 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
|
||||
this.updateState();
|
||||
}
|
||||
|
||||
@Prop() hasIndicator = true;
|
||||
|
||||
connectedCallback() {
|
||||
const segmentEl = (this.segmentEl = this.el.closest('ion-segment'));
|
||||
if (segmentEl) {
|
||||
@ -187,9 +189,11 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
|
||||
</span>
|
||||
{mode === 'md' && <ion-ripple-effect></ion-ripple-effect>}
|
||||
</button>
|
||||
{this.hasIndicator && (
|
||||
<div part="indicator" class="segment-button-indicator segment-button-indicator-animated">
|
||||
<div part="indicator-background" class="segment-button-indicator-background"></div>
|
||||
</div>
|
||||
)}
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
:host {
|
||||
scroll-snap-align: center;
|
||||
scroll-snap-stop: always;
|
||||
|
||||
flex-shrink: 0;
|
||||
|
||||
|
@ -9,6 +9,8 @@
|
||||
overflow-x: scroll;
|
||||
scroll-snap-type: x mandatory;
|
||||
|
||||
scroll-behavior: smooth;
|
||||
|
||||
/* Hide scrollbar in Firefox */
|
||||
scrollbar-width: none;
|
||||
|
||||
|
@ -127,7 +127,7 @@ 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);
|
||||
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
|
||||
|
@ -31,8 +31,19 @@
|
||||
contain: paint;
|
||||
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.segment-indicator {
|
||||
@include transform-origin(left);
|
||||
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
|
||||
div {
|
||||
background: red;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Segment: Scrollable
|
||||
// --------------------------------------------------
|
||||
|
@ -156,6 +156,10 @@ export class Segment implements ComponentInterface {
|
||||
this.emitStyle();
|
||||
|
||||
this.segmentViewEl = this.getSegmentView();
|
||||
|
||||
if (this.segmentViewEl) {
|
||||
this.getButtons().forEach((ref) => (ref.hasIndicator = false));
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
@ -202,6 +206,28 @@ export class Segment implements ComponentInterface {
|
||||
// Update segment view based on the initial value,
|
||||
// but do not animate the scroll
|
||||
this.updateSegmentView(false);
|
||||
|
||||
// TODO: this isn't always consistent, button width can sometimes be 0
|
||||
if (this.segmentViewEl) {
|
||||
const buttons = this.getButtons();
|
||||
const activeButtonIndex = buttons.findIndex((ref) => ref.value === this.value);
|
||||
if (activeButtonIndex >= 0) {
|
||||
const activeButtonStyles = buttons[activeButtonIndex].getBoundingClientRect();
|
||||
const indicator = this.el.shadowRoot!.querySelector('.segment-indicator') as HTMLDivElement | null;
|
||||
if (indicator) {
|
||||
const startingX = buttons
|
||||
.slice(0, activeButtonIndex)
|
||||
.reduce((acc, ref) => acc + ref.getBoundingClientRect().width, 0);
|
||||
|
||||
indicator.style.width = `${activeButtonStyles.width}px`;
|
||||
indicator.style.left = `${startingX}px`;
|
||||
|
||||
// setTimeout(() => {
|
||||
// indicator.style.transition = 'left 0.3s linear, width 0.3s linear';
|
||||
// });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onStart(detail: GestureDetail) {
|
||||
@ -364,62 +390,36 @@ export class Segment implements ComponentInterface {
|
||||
// Only update the indicator if the event was dispatched from the correct segment view
|
||||
if (ev.composedPath().includes(segmentViewEl) || dispatchedFrom?.contains(segmentEl)) {
|
||||
const buttons = this.getButtons();
|
||||
const currentIndex = buttons.findIndex((button) => button.value === this.value);
|
||||
const currentButton = buttons[currentIndex];
|
||||
const indicator = this.el.shadowRoot!.querySelector('.segment-indicator') as HTMLDivElement | null;
|
||||
|
||||
// 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);
|
||||
this.scrolledIndicator = indicatorEl;
|
||||
if (!buttons.length || this.value === undefined || !indicator) return;
|
||||
|
||||
const { scrollDistancePercentage, scrollDistance } = ev.detail;
|
||||
|
||||
if (indicatorEl && !isNaN(scrollDistancePercentage)) {
|
||||
indicatorEl.style.transition = 'transform 0.3s ease-out';
|
||||
const nextIndex = scrollDistance > 0 ? currentIndex + 1 : currentIndex - 1;
|
||||
if (nextIndex >= 0 && nextIndex < buttons.length) {
|
||||
const nextButton = buttons[nextIndex];
|
||||
const nextButtonWidth = nextButton.getBoundingClientRect().width;
|
||||
|
||||
// Calculate the amount the indicator should move based on the scroll percentage
|
||||
// and the width of the current button
|
||||
const scrollAmount = scrollDistancePercentage * current.getBoundingClientRect().width;
|
||||
const transformValue = scrollDistance < 0 ? -scrollAmount : scrollAmount;
|
||||
// Scale the width based on the width of the next button
|
||||
const diff = nextButtonWidth - currentButton.getBoundingClientRect().width;
|
||||
const width = currentButton.getBoundingClientRect().width + diff * scrollDistancePercentage;
|
||||
indicator.style.width = `${width}px`;
|
||||
|
||||
// Calculate total width of buttons to the left of the current button
|
||||
const totalButtonWidthBefore = buttons
|
||||
.slice(0, index)
|
||||
.reduce((acc, button) => acc + button.getBoundingClientRect().width, 0);
|
||||
|
||||
// Calculate total width of buttons to the right of the current button
|
||||
const totalButtonWidthAfter = buttons
|
||||
.slice(index + 1)
|
||||
.reduce((acc, button) => acc + button.getBoundingClientRect().width, 0);
|
||||
|
||||
// Set minTransform and maxTransform
|
||||
const minTransform = -totalButtonWidthBefore;
|
||||
const maxTransform = totalButtonWidthAfter;
|
||||
|
||||
// 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
|
||||
const transform = `translate3d(${clampedTransform}px, 0, 0)`;
|
||||
indicatorEl.style.setProperty('transform', transform);
|
||||
|
||||
// Scroll the buttons if the indicator is out of view
|
||||
const indicatorX = indicatorEl.getBoundingClientRect().x;
|
||||
const buttonWidth = current.getBoundingClientRect().width;
|
||||
if (scrollDistance < 0 && indicatorX < 0) {
|
||||
this.el.scrollBy({
|
||||
top: 0,
|
||||
left: indicatorX,
|
||||
behavior: 'instant',
|
||||
});
|
||||
} else if (scrollDistance > 0 && indicatorX + buttonWidth > this.el.offsetWidth) {
|
||||
this.el.scrollBy({
|
||||
top: 0,
|
||||
left: indicatorX + buttonWidth - this.el.offsetWidth,
|
||||
behavior: 'instant',
|
||||
});
|
||||
}
|
||||
// Translate the indicator based on the scroll distance
|
||||
const distanceToNextButton = buttons
|
||||
.slice(0, nextIndex)
|
||||
.reduce((acc, ref) => acc + ref.getBoundingClientRect().width, 0);
|
||||
const distanceToCurrentButton = buttons
|
||||
.slice(0, currentIndex)
|
||||
.reduce((acc, ref) => acc + ref.getBoundingClientRect().width, 0);
|
||||
indicator.style.left =
|
||||
scrollDistance > 0
|
||||
? `${distanceToCurrentButton + currentButton.getBoundingClientRect().width * scrollDistancePercentage}px`
|
||||
: `${distanceToNextButton + nextButtonWidth - nextButtonWidth * scrollDistancePercentage}px`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -442,25 +442,9 @@ export class Segment implements ComponentInterface {
|
||||
const segmentEl = this.el;
|
||||
|
||||
if (ev.composedPath().includes(segmentViewEl) || dispatchedFrom?.contains(segmentEl)) {
|
||||
if (this.scrolledIndicator) {
|
||||
const computedStyle = window.getComputedStyle(this.scrolledIndicator);
|
||||
const isTransitioning = computedStyle.transitionDuration !== '0s';
|
||||
|
||||
if (isTransitioning) {
|
||||
// Add a transitionend listener if the indicator is transitioning
|
||||
this.waitForTransitionEnd(this.scrolledIndicator, () => {
|
||||
this.updateValueAfterTransition(ev.detail.activeContentId);
|
||||
});
|
||||
} else {
|
||||
// Immediately update the value if there's no transition
|
||||
this.updateValueAfterTransition(ev.detail.activeContentId);
|
||||
}
|
||||
} else {
|
||||
// Immediately update the value if there's no indicator
|
||||
this.updateValueAfterTransition(ev.detail.activeContentId);
|
||||
}
|
||||
|
||||
this.isScrolling = false;
|
||||
|
||||
this.value = ev.detail.activeContentId;
|
||||
}
|
||||
}
|
||||
|
||||
@ -771,6 +755,11 @@ export class Segment implements ComponentInterface {
|
||||
'segment-scrollable': this.scrollable,
|
||||
})}
|
||||
>
|
||||
{this.segmentViewEl && (
|
||||
<div part="indicator" class="segment-indicator">
|
||||
<div part="indicator-background"></div>
|
||||
</div>
|
||||
)}
|
||||
<slot onSlotchange={this.onSlottedItemsChange}></slot>
|
||||
</Host>
|
||||
);
|
||||
|
@ -1984,14 +1984,14 @@ This event will not emit when programmatically setting the `value` property.
|
||||
|
||||
|
||||
@ProxyCmp({
|
||||
inputs: ['contentId', 'disabled', 'layout', 'mode', 'type', 'value']
|
||||
inputs: ['contentId', 'disabled', 'hasIndicator', 'layout', 'mode', 'type', 'value']
|
||||
})
|
||||
@Component({
|
||||
selector: 'ion-segment-button',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: '<ng-content></ng-content>',
|
||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
||||
inputs: ['contentId', 'disabled', 'layout', 'mode', 'type', 'value'],
|
||||
inputs: ['contentId', 'disabled', 'hasIndicator', 'layout', 'mode', 'type', 'value'],
|
||||
})
|
||||
export class IonSegmentButton {
|
||||
protected el: HTMLElement;
|
||||
|
@ -1818,14 +1818,14 @@ export declare interface IonRow extends Components.IonRow {}
|
||||
|
||||
@ProxyCmp({
|
||||
defineCustomElementFn: defineIonSegmentButton,
|
||||
inputs: ['contentId', 'disabled', 'layout', 'mode', 'type', 'value']
|
||||
inputs: ['contentId', 'disabled', 'hasIndicator', 'layout', 'mode', 'type', 'value']
|
||||
})
|
||||
@Component({
|
||||
selector: 'ion-segment-button',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: '<ng-content></ng-content>',
|
||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
||||
inputs: ['contentId', 'disabled', 'layout', 'mode', 'type', 'value'],
|
||||
inputs: ['contentId', 'disabled', 'hasIndicator', 'layout', 'mode', 'type', 'value'],
|
||||
standalone: true
|
||||
})
|
||||
export class IonSegmentButton {
|
||||
|
@ -750,7 +750,8 @@ export const IonSegmentButton = /*@__PURE__*/ defineContainer<JSX.IonSegmentButt
|
||||
'disabled',
|
||||
'layout',
|
||||
'type',
|
||||
'value'
|
||||
'value',
|
||||
'hasIndicator'
|
||||
],
|
||||
'value', 'ion-change');
|
||||
|
||||
|
Reference in New Issue
Block a user