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.
|
* If `true`, the user cannot interact with the segment button.
|
||||||
*/
|
*/
|
||||||
"disabled": boolean;
|
"disabled": boolean;
|
||||||
|
"hasIndicator": boolean;
|
||||||
/**
|
/**
|
||||||
* Set the layout of the text and icon in the segment.
|
* 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.
|
* If `true`, the user cannot interact with the segment button.
|
||||||
*/
|
*/
|
||||||
"disabled"?: boolean;
|
"disabled"?: boolean;
|
||||||
|
"hasIndicator"?: boolean;
|
||||||
/**
|
/**
|
||||||
* Set the layout of the text and icon in the segment.
|
* Set the layout of the text and icon in the segment.
|
||||||
*/
|
*/
|
||||||
|
@ -65,6 +65,8 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
|
|||||||
this.updateState();
|
this.updateState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Prop() hasIndicator = true;
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
const segmentEl = (this.segmentEl = this.el.closest('ion-segment'));
|
const segmentEl = (this.segmentEl = this.el.closest('ion-segment'));
|
||||||
if (segmentEl) {
|
if (segmentEl) {
|
||||||
@ -187,9 +189,11 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
|
|||||||
</span>
|
</span>
|
||||||
{mode === 'md' && <ion-ripple-effect></ion-ripple-effect>}
|
{mode === 'md' && <ion-ripple-effect></ion-ripple-effect>}
|
||||||
</button>
|
</button>
|
||||||
|
{this.hasIndicator && (
|
||||||
<div part="indicator" class="segment-button-indicator segment-button-indicator-animated">
|
<div part="indicator" class="segment-button-indicator segment-button-indicator-animated">
|
||||||
<div part="indicator-background" class="segment-button-indicator-background"></div>
|
<div part="indicator-background" class="segment-button-indicator-background"></div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</Host>
|
</Host>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
:host {
|
:host {
|
||||||
scroll-snap-align: center;
|
scroll-snap-align: center;
|
||||||
|
scroll-snap-stop: always;
|
||||||
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
@ -9,6 +9,8 @@
|
|||||||
overflow-x: scroll;
|
overflow-x: scroll;
|
||||||
scroll-snap-type: x mandatory;
|
scroll-snap-type: x mandatory;
|
||||||
|
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
|
||||||
/* Hide scrollbar in Firefox */
|
/* Hide scrollbar in Firefox */
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
|
|
||||||
|
@ -127,7 +127,7 @@ export class SegmentView implements ComponentInterface {
|
|||||||
* reset the scroll position and emit the scroll end event.
|
* reset the scroll position and emit the scroll end event.
|
||||||
*/
|
*/
|
||||||
private checkForScrollEnd() {
|
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
|
// Only emit scroll end event if the active content is not disabled and
|
||||||
// the user is not touching the segment view
|
// the user is not touching the segment view
|
||||||
|
@ -31,8 +31,19 @@
|
|||||||
contain: paint;
|
contain: paint;
|
||||||
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
|
||||||
|
|
||||||
|
.segment-indicator {
|
||||||
|
@include transform-origin(left);
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
div {
|
||||||
|
background: red;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Segment: Scrollable
|
// Segment: Scrollable
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
|
@ -156,6 +156,10 @@ export class Segment implements ComponentInterface {
|
|||||||
this.emitStyle();
|
this.emitStyle();
|
||||||
|
|
||||||
this.segmentViewEl = this.getSegmentView();
|
this.segmentViewEl = this.getSegmentView();
|
||||||
|
|
||||||
|
if (this.segmentViewEl) {
|
||||||
|
this.getButtons().forEach((ref) => (ref.hasIndicator = false));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
@ -202,6 +206,28 @@ export class Segment implements ComponentInterface {
|
|||||||
// Update segment view based on the initial value,
|
// Update segment view based on the initial value,
|
||||||
// but do not animate the scroll
|
// but do not animate the scroll
|
||||||
this.updateSegmentView(false);
|
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) {
|
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
|
// Only update the indicator if the event was dispatched from the correct segment view
|
||||||
if (ev.composedPath().includes(segmentViewEl) || dispatchedFrom?.contains(segmentEl)) {
|
if (ev.composedPath().includes(segmentViewEl) || dispatchedFrom?.contains(segmentEl)) {
|
||||||
const buttons = this.getButtons();
|
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 no buttons are found or there is no value set then do nothing
|
||||||
if (!buttons.length || this.value === undefined) return;
|
if (!buttons.length || this.value === undefined || !indicator) return;
|
||||||
|
|
||||||
const index = buttons.findIndex((button) => button.value === this.value);
|
|
||||||
const current = buttons[index];
|
|
||||||
const indicatorEl = this.getIndicator(current);
|
|
||||||
this.scrolledIndicator = indicatorEl;
|
|
||||||
|
|
||||||
const { scrollDistancePercentage, scrollDistance } = ev.detail;
|
const { scrollDistancePercentage, scrollDistance } = ev.detail;
|
||||||
|
|
||||||
if (indicatorEl && !isNaN(scrollDistancePercentage)) {
|
const nextIndex = scrollDistance > 0 ? currentIndex + 1 : currentIndex - 1;
|
||||||
indicatorEl.style.transition = 'transform 0.3s ease-out';
|
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
|
// Scale the width based on the width of the next button
|
||||||
// and the width of the current button
|
const diff = nextButtonWidth - currentButton.getBoundingClientRect().width;
|
||||||
const scrollAmount = scrollDistancePercentage * current.getBoundingClientRect().width;
|
const width = currentButton.getBoundingClientRect().width + diff * scrollDistancePercentage;
|
||||||
const transformValue = scrollDistance < 0 ? -scrollAmount : scrollAmount;
|
indicator.style.width = `${width}px`;
|
||||||
|
|
||||||
// Calculate total width of buttons to the left of the current button
|
// Translate the indicator based on the scroll distance
|
||||||
const totalButtonWidthBefore = buttons
|
const distanceToNextButton = buttons
|
||||||
.slice(0, index)
|
.slice(0, nextIndex)
|
||||||
.reduce((acc, button) => acc + button.getBoundingClientRect().width, 0);
|
.reduce((acc, ref) => acc + ref.getBoundingClientRect().width, 0);
|
||||||
|
const distanceToCurrentButton = buttons
|
||||||
// Calculate total width of buttons to the right of the current button
|
.slice(0, currentIndex)
|
||||||
const totalButtonWidthAfter = buttons
|
.reduce((acc, ref) => acc + ref.getBoundingClientRect().width, 0);
|
||||||
.slice(index + 1)
|
indicator.style.left =
|
||||||
.reduce((acc, button) => acc + button.getBoundingClientRect().width, 0);
|
scrollDistance > 0
|
||||||
|
? `${distanceToCurrentButton + currentButton.getBoundingClientRect().width * scrollDistancePercentage}px`
|
||||||
// Set minTransform and maxTransform
|
: `${distanceToNextButton + nextButtonWidth - nextButtonWidth * scrollDistancePercentage}px`;
|
||||||
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',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -442,25 +442,9 @@ export class Segment implements ComponentInterface {
|
|||||||
const segmentEl = this.el;
|
const segmentEl = this.el;
|
||||||
|
|
||||||
if (ev.composedPath().includes(segmentViewEl) || dispatchedFrom?.contains(segmentEl)) {
|
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.isScrolling = false;
|
||||||
|
|
||||||
|
this.value = ev.detail.activeContentId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -771,6 +755,11 @@ export class Segment implements ComponentInterface {
|
|||||||
'segment-scrollable': this.scrollable,
|
'segment-scrollable': this.scrollable,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
{this.segmentViewEl && (
|
||||||
|
<div part="indicator" class="segment-indicator">
|
||||||
|
<div part="indicator-background"></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<slot onSlotchange={this.onSlottedItemsChange}></slot>
|
<slot onSlotchange={this.onSlottedItemsChange}></slot>
|
||||||
</Host>
|
</Host>
|
||||||
);
|
);
|
||||||
|
@ -1984,14 +1984,14 @@ This event will not emit when programmatically setting the `value` property.
|
|||||||
|
|
||||||
|
|
||||||
@ProxyCmp({
|
@ProxyCmp({
|
||||||
inputs: ['contentId', 'disabled', 'layout', 'mode', 'type', 'value']
|
inputs: ['contentId', 'disabled', 'hasIndicator', 'layout', 'mode', 'type', 'value']
|
||||||
})
|
})
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ion-segment-button',
|
selector: 'ion-segment-button',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
template: '<ng-content></ng-content>',
|
template: '<ng-content></ng-content>',
|
||||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
// 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 {
|
export class IonSegmentButton {
|
||||||
protected el: HTMLElement;
|
protected el: HTMLElement;
|
||||||
|
@ -1818,14 +1818,14 @@ export declare interface IonRow extends Components.IonRow {}
|
|||||||
|
|
||||||
@ProxyCmp({
|
@ProxyCmp({
|
||||||
defineCustomElementFn: defineIonSegmentButton,
|
defineCustomElementFn: defineIonSegmentButton,
|
||||||
inputs: ['contentId', 'disabled', 'layout', 'mode', 'type', 'value']
|
inputs: ['contentId', 'disabled', 'hasIndicator', 'layout', 'mode', 'type', 'value']
|
||||||
})
|
})
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ion-segment-button',
|
selector: 'ion-segment-button',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
template: '<ng-content></ng-content>',
|
template: '<ng-content></ng-content>',
|
||||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
// 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
|
standalone: true
|
||||||
})
|
})
|
||||||
export class IonSegmentButton {
|
export class IonSegmentButton {
|
||||||
|
@ -750,7 +750,8 @@ export const IonSegmentButton = /*@__PURE__*/ defineContainer<JSX.IonSegmentButt
|
|||||||
'disabled',
|
'disabled',
|
||||||
'layout',
|
'layout',
|
||||||
'type',
|
'type',
|
||||||
'value'
|
'value',
|
||||||
|
'hasIndicator'
|
||||||
],
|
],
|
||||||
'value', 'ion-change');
|
'value', 'ion-change');
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user