From a6ea277bcdfc252223030bfe57e44c4bcd4a9638 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Louren=C3=A7o?= Date: Tue, 28 Apr 2026 17:08:46 +0100 Subject: [PATCH 1/7] feat(item-sliding): added specific animations for ionic --- .../item-option/item-option.ionic.scss | 7 - .../components/item-sliding/item-sliding.tsx | 268 +++++++++++++++++- 2 files changed, 256 insertions(+), 19 deletions(-) diff --git a/core/src/components/item-option/item-option.ionic.scss b/core/src/components/item-option/item-option.ionic.scss index 20506623d29..7cccea4ad19 100644 --- a/core/src/components/item-option/item-option.ionic.scss +++ b/core/src/components/item-option/item-option.ionic.scss @@ -99,10 +99,3 @@ background: globals.current-color(base); color: globals.current-color(contrast); } - -// Item Expandable Animation -// -------------------------------------------------- - -:host(.item-option-expandable) { - transition-timing-function: globals.$ion-transition-curve-expressive; -} diff --git a/core/src/components/item-sliding/item-sliding.tsx b/core/src/components/item-sliding/item-sliding.tsx index 1d70cbc12d4..c7146f4a948 100644 --- a/core/src/components/item-sliding/item-sliding.tsx +++ b/core/src/components/item-sliding/item-sliding.tsx @@ -11,6 +11,19 @@ import type { Side } from '../menu/menu-interface'; const SWIPE_MARGIN = 30; const ELASTIC_FACTOR = 0.55; +const IONIC_SNAP_OPEN_RATIO = 0.4; +const IONIC_EXPAND_TRIGGER = 80; +const IONIC_VELOCITY_THRESHOLD = 0.4; +const IONIC_ACTION_BASE_WIDTH = 64; +const IONIC_OPEN_TRANSITION = '250ms cubic-bezier(0.25, 1, 0.5, 1)'; +const IONIC_SNAPBACK_TRANSITION = '300ms cubic-bezier(0.34, 1.4, 0.64, 1)'; +const IONIC_CONFIRM_EASE_IN = '150ms ease-in'; +const IONIC_CONFIRM_SNAPBACK = '480ms cubic-bezier(0.34, 1.4, 0.64, 1)'; +const IONIC_CONFIRM_PAUSE = 900; +const IONIC_EXPAND_RESISTANCE_FACTOR = 0.95; + +/** Expandable, non-disabled option (matches item-option expandable class). */ +const EXPANDABLE_OPTION_SELECTOR = 'ion-item-option.item-option-expandable:not(.item-option-disabled)'; const enum ItemSide { None = 0, @@ -38,7 +51,11 @@ let openSlidingItem: HTMLIonItemSlidingElement | undefined; */ @Component({ tag: 'ion-item-sliding', - styleUrl: 'item-sliding.scss', + styleUrls: { + ios: 'item-sliding.scss', + md: 'item-sliding.scss', + ionic: 'item-sliding.scss', + }, }) export class ItemSliding implements ComponentInterface { private item: HTMLIonItemElement | null = null; @@ -56,6 +73,12 @@ export class ItemSliding implements ComponentInterface { private contentEl: HTMLElement | null = null; private initialContentScrollY = true; private mutationObserver?: MutationObserver; + private leftExpandableBaseWidth = IONIC_ACTION_BASE_WIDTH; + private rightExpandableBaseWidth = IONIC_ACTION_BASE_WIDTH; + + private isIonicTheme(): boolean { + return getIonTheme(this) === 'ionic'; + } @Element() el!: HTMLIonItemSlidingElement; @@ -79,7 +102,6 @@ export class ItemSliding implements ComponentInterface { async connectedCallback() { const { el } = this; - this.item = el.querySelector('ion-item'); this.contentEl = findClosestIonContent(el); @@ -332,8 +354,8 @@ export class ItemSliding implements ComponentInterface { } /** - * Animate the item through a full swipe sequence: off-screen → trigger action → return. - * This is used when an expandable option is swiped beyond the threshold. + * Native (ios/md) full swipe: off-screen → fire swipe → return. + * Ionic theme uses `animateIonicFullSwipe` instead (see `onEndIonic`). */ private async animateFullSwipe(direction: 'start' | 'end') { const abortController = new AbortController(); @@ -395,6 +417,157 @@ export class ItemSliding implements ComponentInterface { } } + private queryExpandableOption(options?: HTMLIonItemOptionsElement): HTMLIonItemOptionElement | undefined { + return options?.querySelector(EXPANDABLE_OPTION_SELECTOR) ?? undefined; + } + + private getExpandableOption(direction: 'start' | 'end'): HTMLIonItemOptionElement | undefined { + return this.queryExpandableOption(direction === 'end' ? this.rightOptions : this.leftOptions); + } + + private getOpenDirectionFromAmount(openAmount: number): 'start' | 'end' | undefined { + if (openAmount > 0) { + return 'end'; + } + if (openAmount < 0) { + return 'start'; + } + return undefined; + } + + private getOptionsWidthForDirection(direction: 'start' | 'end'): number { + return direction === 'end' ? this.optsWidthRightSide : this.optsWidthLeftSide; + } + + private getExpandableBaseWidth(direction: 'start' | 'end'): number { + return direction === 'end' ? this.rightExpandableBaseWidth : this.leftExpandableBaseWidth; + } + + private setIonicExpandableWidth(direction: 'start' | 'end', width: number, animate: boolean, easing?: string) { + const expandableOption = this.getExpandableOption(direction); + if (!expandableOption) { + return; + } + + const style = expandableOption.style; + style.transition = animate ? `width ${easing ?? IONIC_OPEN_TRANSITION}` : 'none'; + const baseWidth = this.getExpandableBaseWidth(direction); + style.width = `${Math.max(baseWidth, width)}px`; + } + + private resetIonicExpandableOptions() { + [this.leftOptions, this.rightOptions].forEach((options) => { + const expandableOption = this.queryExpandableOption(options); + if (!expandableOption) { + return; + } + expandableOption.style.transition = ''; + expandableOption.style.width = ''; + }); + } + + private updateIonicExpandableFromOpenAmount(openAmount: number, isFinal: boolean, previousOpenAmount: number) { + if ((this.state & SlidingState.AnimatingFullSwipe) !== 0) { + return; + } + + const direction = this.getOpenDirectionFromAmount(openAmount); + if (direction === undefined) { + const previousDirection = this.getOpenDirectionFromAmount(previousOpenAmount); + if (previousDirection === undefined) { + this.resetIonicExpandableOptions(); + return; + } + + this.setIonicExpandableWidth( + previousDirection, + this.getExpandableBaseWidth(previousDirection), + isFinal, + IONIC_SNAPBACK_TRANSITION + ); + return; + } + + const baseWidth = this.getExpandableBaseWidth(direction); + const optionsWidth = this.getOptionsWidthForDirection(direction); + const extraWidth = Math.max(0, Math.abs(openAmount) - optionsWidth); + const resistedExtraWidth = isFinal ? extraWidth : extraWidth * IONIC_EXPAND_RESISTANCE_FACTOR; + const targetWidth = baseWidth + resistedExtraWidth; + const easing = openAmount === 0 ? IONIC_SNAPBACK_TRANSITION : IONIC_OPEN_TRANSITION; + + this.setIonicExpandableWidth(direction, targetWidth, isFinal, easing); + } + + private async animateIonicFullSwipe(direction: 'start' | 'end') { + const abortController = new AbortController(); + this.animationAbortController = abortController; + const { signal } = abortController; + const expandableOption = this.getExpandableOption(direction); + const options = direction === 'end' ? this.rightOptions : this.leftOptions; + + if (this.gesture) { + this.gesture.enable(false); + } + + try { + this.state = + direction === 'end' + ? SlidingState.End | SlidingState.AnimatingFullSwipe + : SlidingState.Start | SlidingState.AnimatingFullSwipe; + + if (!this.item) { + return; + } + + const itemWidth = this.el.offsetWidth || window.innerWidth; + const baseWidth = this.getExpandableBaseWidth(direction); + const expandableTargetWidth = Math.max(baseWidth, itemWidth - 16); + const offScreenPosition = direction === 'end' ? itemWidth : -itemWidth; + + if (expandableOption) { + expandableOption.style.transition = `width ${IONIC_CONFIRM_EASE_IN}`; + expandableOption.style.width = `${expandableTargetWidth}px`; + + } + + this.item.style.transition = `transform ${IONIC_CONFIRM_EASE_IN}`; + this.item.style.transform = `translate3d(${-offScreenPosition}px, 0, 0)`; + await this.delay(150, signal); + + options?.fireSwipeEvent(); + await this.delay(IONIC_CONFIRM_PAUSE, signal); + + if (expandableOption) { + expandableOption.style.transition = `width ${IONIC_CONFIRM_SNAPBACK}`; + expandableOption.style.width = `${baseWidth}px`; + } + + this.item.style.transition = `transform ${IONIC_CONFIRM_SNAPBACK}`; + this.item.style.transform = 'translate3d(0, 0, 0)'; + await this.delay(480, signal); + } catch { + // Animation was aborted. finally handles cleanup. + } finally { + this.animationAbortController = undefined; + + if (this.item) { + this.item.style.transition = ''; + this.item.style.transform = ''; + } + this.resetIonicExpandableOptions(); + this.openAmount = 0; + this.state = SlidingState.Disabled; + + if (openSlidingItem === this.el) { + openSlidingItem = undefined; + } + + if (this.gesture) { + this.gesture.enable(!this.disabled); + } + } + } + private async updateOptions() { const options = this.el.querySelectorAll('ion-item-options'); @@ -512,11 +685,22 @@ export class ItemSliding implements ComponentInterface { } private onEnd(gesture: GestureDetail) { + this.restoreContentScrollAfterSlide(); + if (this.isIonicTheme()) { + this.onEndIonic(gesture); + } else { + this.onEndNative(gesture); + } + } + + private restoreContentScrollAfterSlide() { const { contentEl, initialContentScrollY } = this; if (contentEl) { resetContentScrollY(contentEl, initialContentScrollY); } + } + private onEndNative(gesture: GestureDetail) { // Check for full swipe conditions with expandable options const rawSwipeDistance = Math.abs(gesture.deltaX); const direction = gesture.deltaX < 0 ? 'end' : 'start'; @@ -561,11 +745,65 @@ export class ItemSliding implements ComponentInterface { } } + private onEndIonic(gesture: GestureDetail) { + const velocity = gesture.velocityX; + const velocityX = velocity * 1000; + const activeDirection = this.getOpenDirectionFromAmount(this.openAmount); + if (activeDirection === undefined) { + this.setOpenAmount(0, true); + return; + } + + const optionsWidth = this.getOptionsWidthForDirection(activeDirection); + const extraWidth = Math.max(0, Math.abs(this.openAmount) - optionsWidth); + const hasExpandable = this.hasExpandableOptions(activeDirection === 'end' ? this.rightOptions : this.leftOptions); + const wasRevealed = Math.abs(this.initialOpenAmount) >= optionsWidth; + + if ( + hasExpandable && + (extraWidth >= IONIC_EXPAND_TRIGGER || (wasRevealed && velocityX < Math.abs(IONIC_VELOCITY_THRESHOLD * 1000))) + ) { + this.animateIonicFullSwipe(activeDirection).catch(() => { + if (this.gesture) { + this.gesture.enable(!this.disabled); + } + }); + return; + } + + const closeDirection = + activeDirection === 'end' + ? velocityX > IONIC_VELOCITY_THRESHOLD * 1000 + : velocityX < -IONIC_VELOCITY_THRESHOLD * 1000; + if (closeDirection) { + this.setOpenAmount(0, true); + return; + } + + const openThreshold = optionsWidth * IONIC_SNAP_OPEN_RATIO; + const shouldSnapOpen = Math.abs(this.openAmount) > openThreshold; + const restingPoint = shouldSnapOpen + ? activeDirection === 'end' + ? this.optsWidthRightSide + : -this.optsWidthLeftSide + : 0; + + this.setOpenAmount(restingPoint, true); + } + private calculateOptsWidth() { this.optsWidthRightSide = 0; if (this.rightOptions) { this.rightOptions.style.display = 'flex'; this.optsWidthRightSide = this.rightOptions.offsetWidth; + const rightExpandable = this.queryExpandableOption(this.rightOptions); + if (rightExpandable) { + rightExpandable.style.width = ''; + this.rightExpandableBaseWidth = Math.max( + IONIC_ACTION_BASE_WIDTH, + rightExpandable.getBoundingClientRect().width + ); + } this.rightOptions.style.display = ''; } @@ -573,6 +811,11 @@ export class ItemSliding implements ComponentInterface { if (this.leftOptions) { this.leftOptions.style.display = 'flex'; this.optsWidthLeftSide = this.leftOptions.offsetWidth; + const leftExpandable = this.queryExpandableOption(this.leftOptions); + if (leftExpandable) { + leftExpandable.style.width = ''; + this.leftExpandableBaseWidth = Math.max(IONIC_ACTION_BASE_WIDTH, leftExpandable.getBoundingClientRect().width); + } this.leftOptions.style.display = ''; } @@ -591,22 +834,23 @@ export class ItemSliding implements ComponentInterface { const { el } = this; const style = this.item.style; + const previousOpenAmount = this.openAmount; this.openAmount = openAmount; + if (this.isIonicTheme()) { + this.updateIonicExpandableFromOpenAmount(openAmount, isFinal, previousOpenAmount); + } + if (isFinal) { style.transition = ''; } if (openAmount > 0) { - this.state = - openAmount >= this.optsWidthRightSide + SWIPE_MARGIN - ? SlidingState.End | SlidingState.SwipeEnd - : SlidingState.End; + const fullSwipe = !this.isIonicTheme() && openAmount >= this.optsWidthRightSide + SWIPE_MARGIN; + this.state = fullSwipe ? SlidingState.End | SlidingState.SwipeEnd : SlidingState.End; } else if (openAmount < 0) { - this.state = - openAmount <= -this.optsWidthLeftSide - SWIPE_MARGIN - ? SlidingState.Start | SlidingState.SwipeStart - : SlidingState.Start; + const fullSwipe = !this.isIonicTheme() && openAmount <= -this.optsWidthLeftSide - SWIPE_MARGIN; + this.state = fullSwipe ? SlidingState.Start | SlidingState.SwipeStart : SlidingState.Start; } else { /** * The sliding options should not be From 9675242ff8f0097aa4e6d3cc04cfbd44faa3e76f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Louren=C3=A7o?= Date: Wed, 29 Apr 2026 11:41:46 +0100 Subject: [PATCH 2/7] feat(item-sliding): added specific swipe animations for ionic --- .../item-option/item-option.ionic.scss | 4 + ...-sliding.scss => item-sliding.common.scss} | 11 --- .../item-sliding/item-sliding.ionic.scss | 11 +++ .../item-sliding/item-sliding.ios.scss | 13 +++ .../item-sliding/item-sliding.md.scss | 13 +++ .../components/item-sliding/item-sliding.tsx | 94 ++++++++++++------- 6 files changed, 101 insertions(+), 45 deletions(-) rename core/src/components/item-sliding/{item-sliding.scss => item-sliding.common.scss} (81%) create mode 100644 core/src/components/item-sliding/item-sliding.ionic.scss create mode 100644 core/src/components/item-sliding/item-sliding.ios.scss create mode 100644 core/src/components/item-sliding/item-sliding.md.scss diff --git a/core/src/components/item-option/item-option.ionic.scss b/core/src/components/item-option/item-option.ionic.scss index 7cccea4ad19..35ff2e51e25 100644 --- a/core/src/components/item-option/item-option.ionic.scss +++ b/core/src/components/item-option/item-option.ionic.scss @@ -99,3 +99,7 @@ background: globals.current-color(base); color: globals.current-color(contrast); } + +:host(.item-option-expand-threshold) { + filter: brightness(0.92); +} \ No newline at end of file diff --git a/core/src/components/item-sliding/item-sliding.scss b/core/src/components/item-sliding/item-sliding.common.scss similarity index 81% rename from core/src/components/item-sliding/item-sliding.scss rename to core/src/components/item-sliding/item-sliding.common.scss index 8a342f57960..c8d62657f55 100644 --- a/core/src/components/item-sliding/item-sliding.scss +++ b/core/src/components/item-sliding/item-sliding.common.scss @@ -18,17 +18,6 @@ ion-item-sliding .item { user-select: none; } -.item-sliding-active-slide .item { - position: relative; - - transition: transform 500ms cubic-bezier(0.36, 0.66, 0.04, 1); - - opacity: 1; - z-index: $z-index-item-options + 1; - pointer-events: none; - will-change: transform; -} - .item-sliding-closing ion-item-options { pointer-events: none; } diff --git a/core/src/components/item-sliding/item-sliding.ionic.scss b/core/src/components/item-sliding/item-sliding.ionic.scss new file mode 100644 index 00000000000..7eb39403e7a --- /dev/null +++ b/core/src/components/item-sliding/item-sliding.ionic.scss @@ -0,0 +1,11 @@ +@use "./item-sliding.common"; +@import "../../themes/native/native.globals"; + +.item-sliding-active-slide .item { + position: relative; + + opacity: 1; + z-index: $z-index-item-options + 1; + pointer-events: none; + will-change: transform; +} \ No newline at end of file diff --git a/core/src/components/item-sliding/item-sliding.ios.scss b/core/src/components/item-sliding/item-sliding.ios.scss new file mode 100644 index 00000000000..4e4671244c1 --- /dev/null +++ b/core/src/components/item-sliding/item-sliding.ios.scss @@ -0,0 +1,13 @@ +@use "./item-sliding.common"; +@import "../../themes/native/native.globals"; + +.item-sliding-active-slide .item { + position: relative; + + transition: transform 500ms cubic-bezier(0.36, 0.66, 0.04, 1); + + opacity: 1; + z-index: $z-index-item-options + 1; + pointer-events: none; + will-change: transform; +} \ No newline at end of file diff --git a/core/src/components/item-sliding/item-sliding.md.scss b/core/src/components/item-sliding/item-sliding.md.scss new file mode 100644 index 00000000000..77b692579b1 --- /dev/null +++ b/core/src/components/item-sliding/item-sliding.md.scss @@ -0,0 +1,13 @@ +@use "./item-sliding.common"; +@import "../../themes/native/native.globals"; + +.item-sliding-active-slide .item { + position: relative; + + transition: transform 500ms cubic-bezier(0.36, 0.66, 0.04, 1); + + opacity: 1; + z-index: $z-index-item-options + 1; + pointer-events: none; + will-change: transform; + } \ No newline at end of file diff --git a/core/src/components/item-sliding/item-sliding.tsx b/core/src/components/item-sliding/item-sliding.tsx index c7146f4a948..4f11609c9f0 100644 --- a/core/src/components/item-sliding/item-sliding.tsx +++ b/core/src/components/item-sliding/item-sliding.tsx @@ -11,6 +11,7 @@ import type { Side } from '../menu/menu-interface'; const SWIPE_MARGIN = 30; const ELASTIC_FACTOR = 0.55; +const IONIC_ELASTIC_FACTOR = 0.15; const IONIC_SNAP_OPEN_RATIO = 0.4; const IONIC_EXPAND_TRIGGER = 80; const IONIC_VELOCITY_THRESHOLD = 0.4; @@ -19,11 +20,12 @@ const IONIC_OPEN_TRANSITION = '250ms cubic-bezier(0.25, 1, 0.5, 1)'; const IONIC_SNAPBACK_TRANSITION = '300ms cubic-bezier(0.34, 1.4, 0.64, 1)'; const IONIC_CONFIRM_EASE_IN = '150ms ease-in'; const IONIC_CONFIRM_SNAPBACK = '480ms cubic-bezier(0.34, 1.4, 0.64, 1)'; -const IONIC_CONFIRM_PAUSE = 900; +const IONIC_CONFIRM_PAUSE = 300; const IONIC_EXPAND_RESISTANCE_FACTOR = 0.95; /** Expandable, non-disabled option (matches item-option expandable class). */ const EXPANDABLE_OPTION_SELECTOR = 'ion-item-option.item-option-expandable:not(.item-option-disabled)'; +const ITEM_OPTION_EXPAND_THRESHOLD_CLASS = 'item-option-expand-threshold'; const enum ItemSide { None = 0, @@ -52,9 +54,9 @@ let openSlidingItem: HTMLIonItemSlidingElement | undefined; @Component({ tag: 'ion-item-sliding', styleUrls: { - ios: 'item-sliding.scss', - md: 'item-sliding.scss', - ionic: 'item-sliding.scss', + ios: 'item-sliding.ios.scss', + md: 'item-sliding.md.scss', + ionic: 'item-sliding.ionic.scss', }, }) export class ItemSliding implements ComponentInterface { @@ -457,6 +459,12 @@ export class ItemSliding implements ComponentInterface { private resetIonicExpandableOptions() { [this.leftOptions, this.rightOptions].forEach((options) => { + if (!options) { + return; + } + options.querySelectorAll(EXPANDABLE_OPTION_SELECTOR).forEach((node) => { + node.classList.remove(ITEM_OPTION_EXPAND_THRESHOLD_CLASS); + }); const expandableOption = this.queryExpandableOption(options); if (!expandableOption) { return; @@ -479,6 +487,10 @@ export class ItemSliding implements ComponentInterface { return; } + this.queryExpandableOption(previousDirection === 'end' ? this.rightOptions : this.leftOptions)?.classList.remove( + ITEM_OPTION_EXPAND_THRESHOLD_CLASS + ); + this.setIonicExpandableWidth( previousDirection, this.getExpandableBaseWidth(previousDirection), @@ -495,6 +507,15 @@ export class ItemSliding implements ComponentInterface { const targetWidth = baseWidth + resistedExtraWidth; const easing = openAmount === 0 ? IONIC_SNAPBACK_TRANSITION : IONIC_OPEN_TRANSITION; + const expandableOption = this.getExpandableOption(direction); + if (expandableOption) { + if (!isFinal && extraWidth >= IONIC_EXPAND_TRIGGER) { + expandableOption.classList.add(ITEM_OPTION_EXPAND_THRESHOLD_CLASS); + } else { + expandableOption.classList.remove(ITEM_OPTION_EXPAND_THRESHOLD_CLASS); + } + } + this.setIonicExpandableWidth(direction, targetWidth, isFinal, easing); } @@ -672,13 +693,23 @@ export class ItemSliding implements ComponentInterface { break; } - let optsWidth; - if (openAmount > this.optsWidthRightSide) { - optsWidth = this.optsWidthRightSide; - openAmount = optsWidth + (openAmount - optsWidth) * ELASTIC_FACTOR; - } else if (openAmount < -this.optsWidthLeftSide) { - optsWidth = -this.optsWidthLeftSide; - openAmount = optsWidth + (openAmount - optsWidth) * ELASTIC_FACTOR; + if (this.isIonicTheme()) { + if (openAmount > this.optsWidthRightSide) { + const overDrag = openAmount - this.optsWidthRightSide; + openAmount = this.optsWidthRightSide + overDrag * IONIC_ELASTIC_FACTOR; + } else if (openAmount < -this.optsWidthLeftSide) { + const overDrag = openAmount + this.optsWidthLeftSide; + openAmount = -this.optsWidthLeftSide + overDrag * IONIC_ELASTIC_FACTOR; + } + } else { + let optsWidth: number; + if (openAmount > this.optsWidthRightSide) { + optsWidth = this.optsWidthRightSide; + openAmount = optsWidth + (openAmount - optsWidth) * ELASTIC_FACTOR; + } else if (openAmount < -this.optsWidthLeftSide) { + optsWidth = -this.optsWidthLeftSide; + openAmount = optsWidth + (openAmount - optsWidth) * ELASTIC_FACTOR; + } } this.setOpenAmount(openAmount, false); @@ -759,9 +790,16 @@ export class ItemSliding implements ComponentInterface { const hasExpandable = this.hasExpandableOptions(activeDirection === 'end' ? this.rightOptions : this.leftOptions); const wasRevealed = Math.abs(this.initialOpenAmount) >= optionsWidth; + + const closeDirection = + activeDirection === 'end' + ? velocityX > IONIC_VELOCITY_THRESHOLD * 1000 + : velocityX < -IONIC_VELOCITY_THRESHOLD * 1000; + if ( + !closeDirection && hasExpandable && - (extraWidth >= IONIC_EXPAND_TRIGGER || (wasRevealed && velocityX < Math.abs(IONIC_VELOCITY_THRESHOLD * 1000))) + (extraWidth >= IONIC_EXPAND_TRIGGER || (extraWidth > 0 && (wasRevealed && velocityX < Math.abs(IONIC_VELOCITY_THRESHOLD * 1000)))) ) { this.animateIonicFullSwipe(activeDirection).catch(() => { if (this.gesture) { @@ -771,10 +809,7 @@ export class ItemSliding implements ComponentInterface { return; } - const closeDirection = - activeDirection === 'end' - ? velocityX > IONIC_VELOCITY_THRESHOLD * 1000 - : velocityX < -IONIC_VELOCITY_THRESHOLD * 1000; + if (closeDirection) { this.setOpenAmount(0, true); return; @@ -830,21 +865,23 @@ export class ItemSliding implements ComponentInterface { if (!this.item) { return; } - + const { el } = this; - const style = this.item.style; const previousOpenAmount = this.openAmount; this.openAmount = openAmount; - + if (this.isIonicTheme()) { this.updateIonicExpandableFromOpenAmount(openAmount, isFinal, previousOpenAmount); } - if (isFinal) { + if (this.isIonicTheme() && isFinal) { + const closing = Math.abs(openAmount) < Math.abs(previousOpenAmount); + style.transition = `transform ${closing ? IONIC_SNAPBACK_TRANSITION : IONIC_OPEN_TRANSITION}`; + } else if (isFinal) { style.transition = ''; } - + if (openAmount > 0) { const fullSwipe = !this.isIonicTheme() && openAmount >= this.optsWidthRightSide + SWIPE_MARGIN; this.state = fullSwipe ? SlidingState.End | SlidingState.SwipeEnd : SlidingState.End; @@ -852,19 +889,7 @@ export class ItemSliding implements ComponentInterface { const fullSwipe = !this.isIonicTheme() && openAmount <= -this.optsWidthLeftSide - SWIPE_MARGIN; this.state = fullSwipe ? SlidingState.Start | SlidingState.SwipeStart : SlidingState.Start; } else { - /** - * The sliding options should not be - * clickable while the item is closing. - */ el.classList.add('item-sliding-closing'); - - /** - * Item sliding cannot be interrupted - * while closing the item. If it did, - * it would allow the item to get into an - * inconsistent state where multiple - * items are then open at the same time. - */ if (this.gesture) { this.gesture.enable(false); } @@ -876,11 +901,12 @@ export class ItemSliding implements ComponentInterface { } el.classList.remove('item-sliding-closing'); }, 600); - + openSlidingItem = undefined; style.transform = ''; return; } + style.transform = `translate3d(${-openAmount}px,0,0)`; this.ionDrag.emit({ amount: openAmount, From 4ede42f04d0d6609db3c2b56c1a7c70766f2eb23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Louren=C3=A7o?= Date: Wed, 29 Apr 2026 12:54:01 +0100 Subject: [PATCH 3/7] feat(item-sliding): added specific classes for swipe animations for ionic --- .../item-sliding/item-sliding.common.scss | 10 +++ .../item-sliding/item-sliding.ionic.scss | 29 ++++++++ .../components/item-sliding/item-sliding.tsx | 69 +++++++++++-------- 3 files changed, 81 insertions(+), 27 deletions(-) diff --git a/core/src/components/item-sliding/item-sliding.common.scss b/core/src/components/item-sliding/item-sliding.common.scss index c8d62657f55..2a2f21753fa 100644 --- a/core/src/components/item-sliding/item-sliding.common.scss +++ b/core/src/components/item-sliding/item-sliding.common.scss @@ -18,6 +18,16 @@ ion-item-sliding .item { user-select: none; } +// During drag on native (ios/md), disable transition — matches former inline `transition: none` +.item-sliding-active-slide.item-sliding-dragging .item { + transition: none; +} + +// Native full-swipe animation (250ms ease-out; replaces inline styles on `ion-item`) +.item-sliding-active-slide.item-sliding-full-swipe-transition .item { + transition: transform 250ms ease-out; +} + .item-sliding-closing ion-item-options { pointer-events: none; } diff --git a/core/src/components/item-sliding/item-sliding.ionic.scss b/core/src/components/item-sliding/item-sliding.ionic.scss index 7eb39403e7a..36843c17d37 100644 --- a/core/src/components/item-sliding/item-sliding.ionic.scss +++ b/core/src/components/item-sliding/item-sliding.ionic.scss @@ -1,6 +1,35 @@ @use "./item-sliding.common"; +@use "../../themes/ionic/ionic.globals.scss" as globals; @import "../../themes/native/native.globals"; + + +// Transition utility classes +.item-sliding-transition-open .item { + transition: transform 250ms cubic-bezier(0.25, 1, 0.5, 1); +} + +.item-sliding-transition-snapback .item { + transition: transform globals.$ion-transition-time-300 globals.$ion-transition-curve-bounce; +} + +// Ionic full-swipe confirm sequence (replaces inline `transition` on item / expandable width) +.item-sliding-ionic-confirm-item-in .item { + transition: transform globals.$ion-transition-time-150 globals.$ion-transition-curve-base; +} + +.item-sliding-ionic-confirm-item-back .item { + transition: transform globals.$ion-transition-time-500 globals.$ion-transition-curve-bounce; +} + +ion-item-option.item-sliding-expandable-width-in { + transition: width globals.$ion-transition-time-150 globals.$ion-transition-curve-base; +} + +ion-item-option.item-sliding-expandable-width-back { + transition: width globals.$ion-transition-time-500 globals.$ion-transition-curve-bounce; +} + .item-sliding-active-slide .item { position: relative; diff --git a/core/src/components/item-sliding/item-sliding.tsx b/core/src/components/item-sliding/item-sliding.tsx index 4f11609c9f0..1d544686ee6 100644 --- a/core/src/components/item-sliding/item-sliding.tsx +++ b/core/src/components/item-sliding/item-sliding.tsx @@ -16,11 +16,8 @@ const IONIC_SNAP_OPEN_RATIO = 0.4; const IONIC_EXPAND_TRIGGER = 80; const IONIC_VELOCITY_THRESHOLD = 0.4; const IONIC_ACTION_BASE_WIDTH = 64; -const IONIC_OPEN_TRANSITION = '250ms cubic-bezier(0.25, 1, 0.5, 1)'; -const IONIC_SNAPBACK_TRANSITION = '300ms cubic-bezier(0.34, 1.4, 0.64, 1)'; -const IONIC_CONFIRM_EASE_IN = '150ms ease-in'; -const IONIC_CONFIRM_SNAPBACK = '480ms cubic-bezier(0.34, 1.4, 0.64, 1)'; const IONIC_CONFIRM_PAUSE = 300; +const FULL_SWIPE_TRANSITION_MS = 250; const IONIC_EXPAND_RESISTANCE_FACTOR = 0.95; /** Expandable, non-disabled option (matches item-option expandable class). */ @@ -331,7 +328,7 @@ export class ItemSliding implements ComponentInterface { return resolve(); } - this.item.style.transition = `transform ${duration}ms ease-out`; + this.el.classList.add('item-sliding-full-swipe-transition'); this.item.style.transform = `translate3d(${-position}px, 0, 0)`; const id = setTimeout(resolve, duration); @@ -384,7 +381,7 @@ export class ItemSliding implements ComponentInterface { // Animate off-screen while maintaining the expanded state const offScreenDistance = direction === 'end' ? window.innerWidth : -window.innerWidth; - await this.animateToPosition(offScreenDistance, 250, signal); + await this.animateToPosition(offScreenDistance, FULL_SWIPE_TRANSITION_MS, signal); // Trigger action if (options) { @@ -395,15 +392,15 @@ export class ItemSliding implements ComponentInterface { await this.delay(300, signal); // Return to closed state - await this.animateToPosition(0, 250, signal); + await this.animateToPosition(0, FULL_SWIPE_TRANSITION_MS, signal); } catch { // Animation was aborted (e.g. component disconnected). finally handles cleanup. } finally { this.animationAbortController = undefined; // Reset state + this.el.classList.remove('item-sliding-full-swipe-transition'); if (this.item) { - this.item.style.transition = ''; this.item.style.transform = ''; } this.openAmount = 0; @@ -445,14 +442,20 @@ export class ItemSliding implements ComponentInterface { return direction === 'end' ? this.rightExpandableBaseWidth : this.leftExpandableBaseWidth; } - private setIonicExpandableWidth(direction: 'start' | 'end', width: number, animate: boolean, easing?: string) { + private setIonicExpandableWidth(direction: 'start' | 'end', width: number, opening: boolean) { const expandableOption = this.getExpandableOption(direction); if (!expandableOption) { return; } const style = expandableOption.style; - style.transition = animate ? `width ${easing ?? IONIC_OPEN_TRANSITION}` : 'none'; + if (opening) { + expandableOption.classList.remove('item-sliding-expandable-snapback'); + expandableOption.classList.add('item-sliding-expandable-open'); + } else { + expandableOption.classList.remove('item-sliding-expandable-open'); + expandableOption.classList.add('item-sliding-expandable-snapback'); + } const baseWidth = this.getExpandableBaseWidth(direction); style.width = `${Math.max(baseWidth, width)}px`; } @@ -469,8 +472,13 @@ export class ItemSliding implements ComponentInterface { if (!expandableOption) { return; } - expandableOption.style.transition = ''; expandableOption.style.width = ''; + expandableOption.classList.remove( + 'item-sliding-expandable-open', + 'item-sliding-expandable-snapback', + 'item-sliding-expandable-width-in', + 'item-sliding-expandable-width-back' + ); }); } @@ -489,13 +497,12 @@ export class ItemSliding implements ComponentInterface { this.queryExpandableOption(previousDirection === 'end' ? this.rightOptions : this.leftOptions)?.classList.remove( ITEM_OPTION_EXPAND_THRESHOLD_CLASS - ); + ); this.setIonicExpandableWidth( previousDirection, this.getExpandableBaseWidth(previousDirection), - isFinal, - IONIC_SNAPBACK_TRANSITION + false ); return; } @@ -505,7 +512,6 @@ export class ItemSliding implements ComponentInterface { const extraWidth = Math.max(0, Math.abs(openAmount) - optionsWidth); const resistedExtraWidth = isFinal ? extraWidth : extraWidth * IONIC_EXPAND_RESISTANCE_FACTOR; const targetWidth = baseWidth + resistedExtraWidth; - const easing = openAmount === 0 ? IONIC_SNAPBACK_TRANSITION : IONIC_OPEN_TRANSITION; const expandableOption = this.getExpandableOption(direction); if (expandableOption) { @@ -516,7 +522,7 @@ export class ItemSliding implements ComponentInterface { } } - this.setIonicExpandableWidth(direction, targetWidth, isFinal, easing); + this.setIonicExpandableWidth(direction, targetWidth, true); } private async animateIonicFullSwipe(direction: 'start' | 'end') { @@ -546,12 +552,13 @@ export class ItemSliding implements ComponentInterface { const offScreenPosition = direction === 'end' ? itemWidth : -itemWidth; if (expandableOption) { - expandableOption.style.transition = `width ${IONIC_CONFIRM_EASE_IN}`; + expandableOption.classList.remove('item-sliding-expandable-width-back'); + expandableOption.classList.add('item-sliding-expandable-width-in'); expandableOption.style.width = `${expandableTargetWidth}px`; - } - this.item.style.transition = `transform ${IONIC_CONFIRM_EASE_IN}`; + this.el.classList.remove('item-sliding-ionic-confirm-item-back'); + this.el.classList.add('item-sliding-ionic-confirm-item-in'); this.item.style.transform = `translate3d(${-offScreenPosition}px, 0, 0)`; await this.delay(150, signal); @@ -559,11 +566,13 @@ export class ItemSliding implements ComponentInterface { await this.delay(IONIC_CONFIRM_PAUSE, signal); if (expandableOption) { - expandableOption.style.transition = `width ${IONIC_CONFIRM_SNAPBACK}`; + expandableOption.classList.remove('item-sliding-expandable-width-in'); + expandableOption.classList.add('item-sliding-expandable-width-back'); expandableOption.style.width = `${baseWidth}px`; } - this.item.style.transition = `transform ${IONIC_CONFIRM_SNAPBACK}`; + this.el.classList.remove('item-sliding-ionic-confirm-item-in'); + this.el.classList.add('item-sliding-ionic-confirm-item-back'); this.item.style.transform = 'translate3d(0, 0, 0)'; await this.delay(480, signal); } catch { @@ -571,8 +580,8 @@ export class ItemSliding implements ComponentInterface { } finally { this.animationAbortController = undefined; + this.el.classList.remove('item-sliding-ionic-confirm-item-in', 'item-sliding-ionic-confirm-item-back'); if (this.item) { - this.item.style.transition = ''; this.item.style.transform = ''; } this.resetIonicExpandableOptions(); @@ -667,7 +676,11 @@ export class ItemSliding implements ComponentInterface { } this.initialOpenAmount = this.openAmount; if (this.item) { - this.item.style.transition = 'none'; + if (this.isIonicTheme()) { + this.el.classList.remove('item-sliding-transition-open', 'item-sliding-transition-snapback'); + } else { + this.el.classList.add('item-sliding-dragging'); + } } } @@ -716,6 +729,7 @@ export class ItemSliding implements ComponentInterface { } private onEnd(gesture: GestureDetail) { + this.el.classList.remove('item-sliding-dragging'); this.restoreContentScrollAfterSlide(); if (this.isIonicTheme()) { this.onEndIonic(gesture); @@ -809,7 +823,6 @@ export class ItemSliding implements ComponentInterface { return; } - if (closeDirection) { this.setOpenAmount(0, true); return; @@ -877,9 +890,11 @@ export class ItemSliding implements ComponentInterface { if (this.isIonicTheme() && isFinal) { const closing = Math.abs(openAmount) < Math.abs(previousOpenAmount); - style.transition = `transform ${closing ? IONIC_SNAPBACK_TRANSITION : IONIC_OPEN_TRANSITION}`; - } else if (isFinal) { - style.transition = ''; + if (closing) { + this.el.classList.add('item-sliding-transition-snapback'); + } else { + this.el.classList.add('item-sliding-transition-open'); + } } if (openAmount > 0) { From c0338abf19787ea0b2b1c4b4de39b4420b90b224 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Louren=C3=A7o?= Date: Wed, 29 Apr 2026 16:59:08 +0100 Subject: [PATCH 4/7] feat(item-sliding): corrected ionic thresholds --- core/src/components/item-sliding/item-sliding.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/core/src/components/item-sliding/item-sliding.tsx b/core/src/components/item-sliding/item-sliding.tsx index 1d544686ee6..08c164c4918 100644 --- a/core/src/components/item-sliding/item-sliding.tsx +++ b/core/src/components/item-sliding/item-sliding.tsx @@ -13,8 +13,8 @@ const SWIPE_MARGIN = 30; const ELASTIC_FACTOR = 0.55; const IONIC_ELASTIC_FACTOR = 0.15; const IONIC_SNAP_OPEN_RATIO = 0.4; -const IONIC_EXPAND_TRIGGER = 80; -const IONIC_VELOCITY_THRESHOLD = 0.4; +const IONIC_EXPAND_TRIGGER = 40; +const IONIC_VELOCITY_THRESHOLD = 400; const IONIC_ACTION_BASE_WIDTH = 64; const IONIC_CONFIRM_PAUSE = 300; const FULL_SWIPE_TRANSITION_MS = 250; @@ -806,14 +806,12 @@ export class ItemSliding implements ComponentInterface { const closeDirection = - activeDirection === 'end' - ? velocityX > IONIC_VELOCITY_THRESHOLD * 1000 - : velocityX < -IONIC_VELOCITY_THRESHOLD * 1000; + activeDirection === 'end' ? velocityX > IONIC_VELOCITY_THRESHOLD : velocityX < -IONIC_VELOCITY_THRESHOLD; if ( !closeDirection && hasExpandable && - (extraWidth >= IONIC_EXPAND_TRIGGER || (extraWidth > 0 && (wasRevealed && velocityX < Math.abs(IONIC_VELOCITY_THRESHOLD * 1000)))) + (extraWidth >= IONIC_EXPAND_TRIGGER || (extraWidth > 0 && (wasRevealed && Math.abs(velocityX) > IONIC_VELOCITY_THRESHOLD))) ) { this.animateIonicFullSwipe(activeDirection).catch(() => { if (this.gesture) { From a247ac5867d6fb64a1dddd75f2f0b43bdca28a61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Louren=C3=A7o?= Date: Thu, 30 Apr 2026 11:38:28 +0100 Subject: [PATCH 5/7] feat(item-sliding): added velocity validation for open options --- .../item-option/item-option.ionic.scss | 2 +- .../item-sliding/item-sliding.ionic.scss | 4 +- .../item-sliding/item-sliding.ios.scss | 2 +- .../item-sliding/item-sliding.md.scss | 18 +- .../components/item-sliding/item-sliding.tsx | 50 +++--- .../test/full-swipe/item-sliding.e2e.ts | 165 +++++++++++++++++- 6 files changed, 196 insertions(+), 45 deletions(-) diff --git a/core/src/components/item-option/item-option.ionic.scss b/core/src/components/item-option/item-option.ionic.scss index 35ff2e51e25..6ea452073db 100644 --- a/core/src/components/item-option/item-option.ionic.scss +++ b/core/src/components/item-option/item-option.ionic.scss @@ -102,4 +102,4 @@ :host(.item-option-expand-threshold) { filter: brightness(0.92); -} \ No newline at end of file +} diff --git a/core/src/components/item-sliding/item-sliding.ionic.scss b/core/src/components/item-sliding/item-sliding.ionic.scss index 36843c17d37..d3501b8ccd3 100644 --- a/core/src/components/item-sliding/item-sliding.ionic.scss +++ b/core/src/components/item-sliding/item-sliding.ionic.scss @@ -2,8 +2,6 @@ @use "../../themes/ionic/ionic.globals.scss" as globals; @import "../../themes/native/native.globals"; - - // Transition utility classes .item-sliding-transition-open .item { transition: transform 250ms cubic-bezier(0.25, 1, 0.5, 1); @@ -37,4 +35,4 @@ ion-item-option.item-sliding-expandable-width-back { z-index: $z-index-item-options + 1; pointer-events: none; will-change: transform; -} \ No newline at end of file +} diff --git a/core/src/components/item-sliding/item-sliding.ios.scss b/core/src/components/item-sliding/item-sliding.ios.scss index 4e4671244c1..3e1d00a2196 100644 --- a/core/src/components/item-sliding/item-sliding.ios.scss +++ b/core/src/components/item-sliding/item-sliding.ios.scss @@ -10,4 +10,4 @@ z-index: $z-index-item-options + 1; pointer-events: none; will-change: transform; -} \ No newline at end of file +} diff --git a/core/src/components/item-sliding/item-sliding.md.scss b/core/src/components/item-sliding/item-sliding.md.scss index 77b692579b1..3e1d00a2196 100644 --- a/core/src/components/item-sliding/item-sliding.md.scss +++ b/core/src/components/item-sliding/item-sliding.md.scss @@ -2,12 +2,12 @@ @import "../../themes/native/native.globals"; .item-sliding-active-slide .item { - position: relative; - - transition: transform 500ms cubic-bezier(0.36, 0.66, 0.04, 1); - - opacity: 1; - z-index: $z-index-item-options + 1; - pointer-events: none; - will-change: transform; - } \ No newline at end of file + position: relative; + + transition: transform 500ms cubic-bezier(0.36, 0.66, 0.04, 1); + + opacity: 1; + z-index: $z-index-item-options + 1; + pointer-events: none; + will-change: transform; +} diff --git a/core/src/components/item-sliding/item-sliding.tsx b/core/src/components/item-sliding/item-sliding.tsx index 08c164c4918..d1ebced0dad 100644 --- a/core/src/components/item-sliding/item-sliding.tsx +++ b/core/src/components/item-sliding/item-sliding.tsx @@ -14,7 +14,8 @@ const ELASTIC_FACTOR = 0.55; const IONIC_ELASTIC_FACTOR = 0.15; const IONIC_SNAP_OPEN_RATIO = 0.4; const IONIC_EXPAND_TRIGGER = 40; -const IONIC_VELOCITY_THRESHOLD = 400; +const IONIC_FULL_SWIPE_VELOCITY_THRESHOLD = 400; +const IONIC_OPEN_VELOCITY_THRESHOLD = 200; const IONIC_ACTION_BASE_WIDTH = 64; const IONIC_CONFIRM_PAUSE = 300; const FULL_SWIPE_TRANSITION_MS = 250; @@ -499,11 +500,7 @@ export class ItemSliding implements ComponentInterface { ITEM_OPTION_EXPAND_THRESHOLD_CLASS ); - this.setIonicExpandableWidth( - previousDirection, - this.getExpandableBaseWidth(previousDirection), - false - ); + this.setIonicExpandableWidth(previousDirection, this.getExpandableBaseWidth(previousDirection), false); return; } @@ -802,16 +799,20 @@ export class ItemSliding implements ComponentInterface { const optionsWidth = this.getOptionsWidthForDirection(activeDirection); const extraWidth = Math.max(0, Math.abs(this.openAmount) - optionsWidth); const hasExpandable = this.hasExpandableOptions(activeDirection === 'end' ? this.rightOptions : this.leftOptions); - const wasRevealed = Math.abs(this.initialOpenAmount) >= optionsWidth; - const closeDirection = - activeDirection === 'end' ? velocityX > IONIC_VELOCITY_THRESHOLD : velocityX < -IONIC_VELOCITY_THRESHOLD; + activeDirection === 'end' + ? velocityX > IONIC_FULL_SWIPE_VELOCITY_THRESHOLD + : velocityX < -IONIC_FULL_SWIPE_VELOCITY_THRESHOLD; + + if (closeDirection) { + this.setOpenAmount(0, true); + return; + } if ( - !closeDirection && hasExpandable && - (extraWidth >= IONIC_EXPAND_TRIGGER || (extraWidth > 0 && (wasRevealed && Math.abs(velocityX) > IONIC_VELOCITY_THRESHOLD))) + (extraWidth >= IONIC_EXPAND_TRIGGER || Math.abs(velocityX) > IONIC_FULL_SWIPE_VELOCITY_THRESHOLD) ) { this.animateIonicFullSwipe(activeDirection).catch(() => { if (this.gesture) { @@ -821,18 +822,15 @@ export class ItemSliding implements ComponentInterface { return; } - if (closeDirection) { - this.setOpenAmount(0, true); - return; - } + const flickOpen = + activeDirection === 'end' + ? velocityX < -IONIC_OPEN_VELOCITY_THRESHOLD + : velocityX > IONIC_OPEN_VELOCITY_THRESHOLD; + const fullOpen = activeDirection === 'end' ? this.optsWidthRightSide : -this.optsWidthLeftSide; const openThreshold = optionsWidth * IONIC_SNAP_OPEN_RATIO; - const shouldSnapOpen = Math.abs(this.openAmount) > openThreshold; - const restingPoint = shouldSnapOpen - ? activeDirection === 'end' - ? this.optsWidthRightSide - : -this.optsWidthLeftSide - : 0; + const shouldSnapOpen = flickOpen || Math.abs(this.openAmount) > openThreshold; + const restingPoint = shouldSnapOpen ? fullOpen : 0; this.setOpenAmount(restingPoint, true); } @@ -876,12 +874,12 @@ export class ItemSliding implements ComponentInterface { if (!this.item) { return; } - + const { el } = this; const style = this.item.style; const previousOpenAmount = this.openAmount; this.openAmount = openAmount; - + if (this.isIonicTheme()) { this.updateIonicExpandableFromOpenAmount(openAmount, isFinal, previousOpenAmount); } @@ -894,7 +892,7 @@ export class ItemSliding implements ComponentInterface { this.el.classList.add('item-sliding-transition-open'); } } - + if (openAmount > 0) { const fullSwipe = !this.isIonicTheme() && openAmount >= this.optsWidthRightSide + SWIPE_MARGIN; this.state = fullSwipe ? SlidingState.End | SlidingState.SwipeEnd : SlidingState.End; @@ -914,12 +912,12 @@ export class ItemSliding implements ComponentInterface { } el.classList.remove('item-sliding-closing'); }, 600); - + openSlidingItem = undefined; style.transform = ''; return; } - + style.transform = `translate3d(${-openAmount}px,0,0)`; this.ionDrag.emit({ amount: openAmount, diff --git a/core/src/components/item-sliding/test/full-swipe/item-sliding.e2e.ts b/core/src/components/item-sliding/test/full-swipe/item-sliding.e2e.ts index 1ba692bf5a3..e95744ae651 100644 --- a/core/src/components/item-sliding/test/full-swipe/item-sliding.e2e.ts +++ b/core/src/components/item-sliding/test/full-swipe/item-sliding.e2e.ts @@ -2,10 +2,6 @@ import { expect } from '@playwright/test'; import { configs, dragElementBy, test } from '@utils/test/playwright'; /** - * Full swipe animation behavior is mode-independent but - * child components (ion-item-options, ion-item-option) have - * mode-specific styling, so we test across all modes. - * * When an item has at least one expandable option and the user swipes * beyond the threshold (or with sufficient velocity), the item slides * off-screen, fires ionSwipe, and returns to its closed position. @@ -14,7 +10,7 @@ import { configs, dragElementBy, test } from '@utils/test/playwright'; // Full animation cycle duration (100ms expand + 250ms off-screen + 300ms delay + 250ms return) const FULL_ANIMATION_MS = 1100; -configs({ modes: ['ios', 'md', 'ionic-md'], directions: ['ltr', 'rtl'] }).forEach(({ title, config }) => { +configs({ modes: ['ios', 'md'], directions: ['ltr', 'rtl'] }).forEach(({ title, config }) => { test.describe(title('item-sliding: full swipe'), () => { test('should fire ionSwipe when expandable option is swiped fully (end side)', async ({ page }) => { await page.setContent( @@ -58,6 +54,8 @@ configs({ modes: ['ios', 'md', 'ionic-md'], directions: ['ltr', 'rtl'] }).forEac const ionSwipe = await page.spyOnEvent('ionSwipe'); const item = page.locator('ion-item-sliding'); + await expect(item).toBeVisible(); + await page.waitForChanges(); const dragByX = config.direction === 'rtl' ? -190 : 190; await dragElementBy(item, page, dragByX); @@ -82,6 +80,8 @@ configs({ modes: ['ios', 'md', 'ionic-md'], directions: ['ltr', 'rtl'] }).forEac ); const item = page.locator('ion-item-sliding'); + await expect(item).toBeVisible(); + await page.waitForChanges(); const dragByX = config.direction === 'rtl' ? 190 : -190; await dragElementBy(item, page, dragByX); @@ -108,6 +108,8 @@ configs({ modes: ['ios', 'md', 'ionic-md'], directions: ['ltr', 'rtl'] }).forEac const ionSwipe = await page.spyOnEvent('ionSwipe'); const item = page.locator('ion-item-sliding'); + await expect(item).toBeVisible(); + await page.waitForChanges(); const dragByX = config.direction === 'rtl' ? 180 : -180; await dragElementBy(item, page, dragByX); @@ -138,6 +140,8 @@ configs({ modes: ['ios', 'md', 'ionic-md'], directions: ['ltr', 'rtl'] }).forEac const ionSwipe = await page.spyOnEvent('ionSwipe'); const item = page.locator('ion-item-sliding'); + await expect(item).toBeVisible(); + await page.waitForChanges(); const dragByX = config.direction === 'rtl' ? 190 : -190; await dragElementBy(item, page, dragByX); @@ -148,6 +152,155 @@ configs({ modes: ['ios', 'md', 'ionic-md'], directions: ['ltr', 'rtl'] }).forEac }); }); +/** + * Test for Ionic theme that has a different full swipe animation behavior. + */ +configs({ modes: ['ionic-md'], directions: ['ltr', 'rtl'] }).forEach(({ title, config }) => { + test.describe(title('item-sliding: full swipe'), () => { + test('should fire ionSwipe when expandable option is swiped fully (end side)', async ({ page }) => { + await page.setContent( + ` + + + Expandable End (Swipe Left) + + + Delete + + + `, + config + ); + + const ionSwipe = await page.spyOnEvent('ionSwipe'); + const item = page.locator('ion-item-sliding'); + await expect(item).toBeVisible(); + await page.waitForChanges(); + const box = (await item.boundingBox())!; + const y = box.y + box.height / 2; + + // 1) Peek open (distance-based; moderate steps is fine) + const peek = config.direction === 'rtl' ? 120 : -120; + await dragElementBy(item, page, peek); + + // 2) Fast flick in the same direction as “full swipe” + const startX = config.direction === 'rtl' ? box.x + 40 : box.x + box.width - 40; + const endX = config.direction === 'rtl' ? box.x + box.width - 40 : box.x + 40; + await page.mouse.move(startX, y); + await page.mouse.down(); + await page.mouse.move(endX, y, { steps: 2 }); // try 1–3; lower = faster + await page.mouse.up(); + await ionSwipe.next(); + expect(ionSwipe).toHaveReceivedEventTimes(1); + }); + + test('should fire ionSwipe when expandable option is swiped fully (start side)', async ({ page }) => { + await page.setContent( + ` + + + Expandable Start (Swipe Right) + + + Archive + + + `, + config + ); + + const ionSwipe = await page.spyOnEvent('ionSwipe'); + const item = page.locator('ion-item-sliding'); + await expect(item).toBeVisible(); + await page.waitForChanges(); + const box = (await item.boundingBox())!; + const y = box.y + box.height / 2; + + // 1) Peek open (distance-based; moderate steps is fine) + const peek = config.direction === 'rtl' ? -120 : 120; + await dragElementBy(item, page, peek); + + // 2) Fast flick in the same direction as “full swipe” + const startX = config.direction === 'rtl' ? box.x + box.width - 40 : box.x + 40; + const endX = config.direction === 'rtl' ? box.x + 40 : box.x + box.width - 40; + await page.mouse.move(startX, y); + await page.mouse.down(); + await page.mouse.move(endX, y, { steps: 2 }); + await page.mouse.up(); + await ionSwipe.next(); + expect(ionSwipe).toHaveReceivedEventTimes(1); + }); + + test('should return to closed state after full swipe animation completes', async ({ page }) => { + await page.setContent( + ` + + + Expandable End (Swipe Left) + + + Delete + + + `, + config + ); + + const item = page.locator('ion-item-sliding'); + await expect(item).toBeVisible(); + await page.waitForChanges(); + const box = (await item.boundingBox())!; + const y = box.y + box.height / 2; + + // 1) Peek open (distance-based; moderate steps is fine) + const peek = config.direction === 'rtl' ? -120 : 120; + await dragElementBy(item, page, peek); + + // 2) Fast flick in the same direction as “full swipe” + const startX = config.direction === 'rtl' ? box.x + box.width - 40 : box.x + 40; + const endX = config.direction === 'rtl' ? box.x + 40 : box.x + box.width - 40; + await page.mouse.move(startX, y); + await page.mouse.down(); + await page.mouse.move(endX, y, { steps: 2 }); + await page.mouse.up(); + await page.waitForTimeout(FULL_ANIMATION_MS); + + const openAmount = await item.evaluate((el: HTMLIonItemSlidingElement) => el.getOpenAmount()); + expect(openAmount).toBe(0); + }); + + test('should NOT trigger full swipe animation for non-expandable options', async ({ page }) => { + await page.setContent( + ` + + + Non-Expandable (Should Show Options) + + + Edit + + + `, + config + ); + const ionSwipe = await page.spyOnEvent('ionSwipe'); + const item = page.locator('ion-item-sliding'); + await expect(item).toBeVisible(); + await page.waitForChanges(); + const dragByX = config.direction === 'rtl' ? 180 : -180; + + await dragElementBy(item, page, dragByX); + await page.waitForChanges(); + + expect(ionSwipe).toHaveReceivedEventTimes(0); + + const openAmount = await item.evaluate((el: HTMLIonItemSlidingElement) => el.getOpenAmount()); + + expect(Math.abs(openAmount)).toBeGreaterThan(0); + }); + }); +}); + /** * Velocity-based trigger: a fast short swipe should trigger the full animation * even if the raw distance alone wouldn't exceed the threshold. @@ -172,6 +325,8 @@ configs({ modes: ['md'], directions: ['ltr', 'rtl'] }).forEach(({ title, config const ionSwipe = await page.spyOnEvent('ionSwipe'); const item = page.locator('ion-item-sliding'); + await expect(item).toBeVisible(); + await page.waitForChanges(); const box = (await item.boundingBox())!; // Few steps = high velocity gesture From 7468b4cc28f9522365284408bd8dc953d4e111a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Louren=C3=A7o?= Date: Thu, 30 Apr 2026 12:45:48 +0100 Subject: [PATCH 6/7] feat(item-sliding): improve full swipe animation separation --- core/src/components/item-sliding/item-sliding.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/components/item-sliding/item-sliding.tsx b/core/src/components/item-sliding/item-sliding.tsx index d1ebced0dad..279e9c3c442 100644 --- a/core/src/components/item-sliding/item-sliding.tsx +++ b/core/src/components/item-sliding/item-sliding.tsx @@ -812,7 +812,8 @@ export class ItemSliding implements ComponentInterface { if ( hasExpandable && - (extraWidth >= IONIC_EXPAND_TRIGGER || Math.abs(velocityX) > IONIC_FULL_SWIPE_VELOCITY_THRESHOLD) + (extraWidth >= IONIC_EXPAND_TRIGGER || + (extraWidth > 0 && Math.abs(velocityX) > IONIC_FULL_SWIPE_VELOCITY_THRESHOLD)) ) { this.animateIonicFullSwipe(activeDirection).catch(() => { if (this.gesture) { From 6d983d20a8692c0e86cd1426d4b99f935acc9131 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Louren=C3=A7o?= Date: Tue, 5 May 2026 09:40:14 +0100 Subject: [PATCH 7/7] feat(item-sliding): updated token library and ionic styles --- core/package-lock.json | 48 +++++++++---------- core/package.json | 2 +- .../item-sliding/item-sliding.common.scss | 5 -- .../item-sliding/item-sliding.ionic.scss | 5 +- .../item-sliding/item-sliding.ios.scss | 5 ++ .../item-sliding/item-sliding.md.scss | 5 ++ core/src/foundations/ionic.vars.scss | 2 + 7 files changed, 37 insertions(+), 35 deletions(-) diff --git a/core/package-lock.json b/core/package-lock.json index b30d0b3a1bc..d6ff97a7e58 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -43,7 +43,7 @@ "fs-extra": "^9.0.1", "jest": "^29.7.0", "jest-cli": "^29.7.0", - "outsystems-design-tokens": "^1.3.8", + "outsystems-design-tokens": "^1.3.9", "playwright-core": "^1.58.2", "prettier": "^2.8.8", "rollup": "^2.26.4", @@ -97,6 +97,7 @@ "version": "7.16.12", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.16.7", "@babel/generator": "^7.16.8", @@ -718,6 +719,7 @@ "integrity": "sha512-S4ajn4G/fS3VJj8salxqH/3LO5PPWv1VxGKQ27OCajnDcLJjEg9VXwgMPnlypgkIOqCJ2fmQLtk8GT+BlI9/rw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -936,6 +938,7 @@ "version": "4.33.0", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "4.33.0", "@typescript-eslint/types": "4.33.0", @@ -2091,6 +2094,7 @@ "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.43.0.tgz", "integrity": "sha512-6Uj2Z3lzLuufYAE7asZ6NLKgSwsB9uxl84Eh34PASnUjfj32GkrP4DtKK7fNeh1WFGGyffsTDka3gwtl+4reUg==", "license": "MIT", + "peer": true, "bin": { "stencil": "bin/stencil" }, @@ -2520,6 +2524,7 @@ "version": "6.7.2", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.7.2", "@typescript-eslint/types": "6.7.2", @@ -2745,7 +2750,6 @@ "integrity": "sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/parser": "^7.28.5", "@vue/shared": "3.5.25", @@ -2760,7 +2764,6 @@ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=0.12" }, @@ -2773,8 +2776,7 @@ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@vue/compiler-dom": { "version": "3.5.25", @@ -2782,7 +2784,6 @@ "integrity": "sha512-4We0OAcMZsKgYoGlMjzYvaoErltdFI2/25wqanuTu+S4gismOTRTBPi4IASOjxWdzIwrYSjnqONfKvuqkXzE2Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-core": "3.5.25", "@vue/shared": "3.5.25" @@ -2794,7 +2795,6 @@ "integrity": "sha512-PUgKp2rn8fFsI++lF2sO7gwO2d9Yj57Utr5yEsDf3GNaQcowCLKL7sf+LvVFvtJDXUp/03+dC6f2+LCv5aK1ag==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/parser": "^7.28.5", "@vue/compiler-core": "3.5.25", @@ -2812,8 +2812,7 @@ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@vue/compiler-sfc/node_modules/postcss": { "version": "8.5.6", @@ -2835,7 +2834,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -2851,7 +2849,6 @@ "integrity": "sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.25", "@vue/shared": "3.5.25" @@ -2863,7 +2860,6 @@ "integrity": "sha512-5xfAypCQepv4Jog1U4zn8cZIcbKKFka3AgWHEFQeK65OW+Ys4XybP6z2kKgws4YB43KGpqp5D/K3go2UPPunLA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vue/shared": "3.5.25" } @@ -2874,7 +2870,6 @@ "integrity": "sha512-Z751v203YWwYzy460bzsYQISDfPjHTl+6Zzwo/a3CsAf+0ccEjQ8c+0CdX1WsumRTHeywvyUFtW6KvNukT/smA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vue/reactivity": "3.5.25", "@vue/shared": "3.5.25" @@ -2886,7 +2881,6 @@ "integrity": "sha512-a4WrkYFbb19i9pjkz38zJBg8wa/rboNERq3+hRRb0dHiJh13c+6kAbgqCPfMaJ2gg4weWD3APZswASOfmKwamA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vue/reactivity": "3.5.25", "@vue/runtime-core": "3.5.25", @@ -2900,7 +2894,6 @@ "integrity": "sha512-UJaXR54vMG61i8XNIzTSf2Q7MOqZHpp8+x3XLGtE3+fL+nQd+k7O5+X3D/uWrnQXOdMw5VPih+Uremcw+u1woQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-ssr": "3.5.25", "@vue/shared": "3.5.25" @@ -2914,8 +2907,7 @@ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.25.tgz", "integrity": "sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@zeit/schemas": { "version": "2.21.0", @@ -2950,6 +2942,7 @@ "version": "7.4.0", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4248,8 +4241,7 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/debug": { "version": "2.6.9", @@ -4618,6 +4610,7 @@ "version": "7.32.0", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "7.12.11", "@eslint/eslintrc": "^0.4.3", @@ -8095,7 +8088,6 @@ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } @@ -8461,7 +8453,6 @@ } ], "license": "MIT", - "peer": true, "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -8660,9 +8651,9 @@ } }, "node_modules/outsystems-design-tokens": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/outsystems-design-tokens/-/outsystems-design-tokens-1.3.8.tgz", - "integrity": "sha512-hT2r9Crd7rn1ewG9hzx7bUA5AjxEdzFsaxzUJ11DqBW86aHmc2ImsCUkaavMlADmZ1g/ZHxV/eO5Bmy3lWKpmA==", + "version": "1.3.9", + "resolved": "https://registry.npmjs.org/outsystems-design-tokens/-/outsystems-design-tokens-1.3.9.tgz", + "integrity": "sha512-iH0N7CF3Vb+7r2L8sMGisVkXgZeOG+aEJvLiuu8H4EnTAJKEkWm9V0vsd0ppxYk05g2UfWVmZeKKkdaIMW6V+Q==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -8968,6 +8959,7 @@ "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "playwright-core": "cli.js" }, @@ -8989,6 +8981,7 @@ "version": "7.0.35", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "chalk": "^2.4.2", "source-map": "^0.6.1", @@ -9094,6 +9087,7 @@ "version": "0.36.2", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "postcss": ">=5.0.0" } @@ -9144,6 +9138,7 @@ "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin-prettier.js" }, @@ -9527,6 +9522,7 @@ "version": "2.35.1", "dev": true, "license": "MIT", + "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -9847,7 +9843,6 @@ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "license": "BSD-3-Clause", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -10607,7 +10602,8 @@ }, "node_modules/tslib": { "version": "2.1.0", - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tsutils": { "version": "3.21.0", diff --git a/core/package.json b/core/package.json index 4f7d6a596cb..dfbef3e4a9b 100644 --- a/core/package.json +++ b/core/package.json @@ -68,7 +68,7 @@ "fs-extra": "^9.0.1", "jest": "^29.7.0", "jest-cli": "^29.7.0", - "outsystems-design-tokens": "^1.3.8", + "outsystems-design-tokens": "^1.3.9", "playwright-core": "^1.58.2", "prettier": "^2.8.8", "rollup": "^2.26.4", diff --git a/core/src/components/item-sliding/item-sliding.common.scss b/core/src/components/item-sliding/item-sliding.common.scss index 2a2f21753fa..b42bd628d74 100644 --- a/core/src/components/item-sliding/item-sliding.common.scss +++ b/core/src/components/item-sliding/item-sliding.common.scss @@ -23,11 +23,6 @@ ion-item-sliding .item { transition: none; } -// Native full-swipe animation (250ms ease-out; replaces inline styles on `ion-item`) -.item-sliding-active-slide.item-sliding-full-swipe-transition .item { - transition: transform 250ms ease-out; -} - .item-sliding-closing ion-item-options { pointer-events: none; } diff --git a/core/src/components/item-sliding/item-sliding.ionic.scss b/core/src/components/item-sliding/item-sliding.ionic.scss index d3501b8ccd3..7dfd1225244 100644 --- a/core/src/components/item-sliding/item-sliding.ionic.scss +++ b/core/src/components/item-sliding/item-sliding.ionic.scss @@ -1,10 +1,9 @@ @use "./item-sliding.common"; @use "../../themes/ionic/ionic.globals.scss" as globals; -@import "../../themes/native/native.globals"; // Transition utility classes .item-sliding-transition-open .item { - transition: transform 250ms cubic-bezier(0.25, 1, 0.5, 1); + transition: transform globals.$ion-transition-time-250 globals.$ion-transition-curve-smooth; } .item-sliding-transition-snapback .item { @@ -32,7 +31,7 @@ ion-item-option.item-sliding-expandable-width-back { position: relative; opacity: 1; - z-index: $z-index-item-options + 1; + z-index: globals.$ion-z-index-100; pointer-events: none; will-change: transform; } diff --git a/core/src/components/item-sliding/item-sliding.ios.scss b/core/src/components/item-sliding/item-sliding.ios.scss index 3e1d00a2196..f81537f4f05 100644 --- a/core/src/components/item-sliding/item-sliding.ios.scss +++ b/core/src/components/item-sliding/item-sliding.ios.scss @@ -11,3 +11,8 @@ pointer-events: none; will-change: transform; } + +// Native full-swipe animation (250ms ease-out; replaces inline styles on `ion-item`) +.item-sliding-active-slide.item-sliding-full-swipe-transition .item { + transition: transform 250ms ease-out; +} diff --git a/core/src/components/item-sliding/item-sliding.md.scss b/core/src/components/item-sliding/item-sliding.md.scss index 3e1d00a2196..f81537f4f05 100644 --- a/core/src/components/item-sliding/item-sliding.md.scss +++ b/core/src/components/item-sliding/item-sliding.md.scss @@ -11,3 +11,8 @@ pointer-events: none; will-change: transform; } + +// Native full-swipe animation (250ms ease-out; replaces inline styles on `ion-item`) +.item-sliding-active-slide.item-sliding-full-swipe-transition .item { + transition: transform 250ms ease-out; +} diff --git a/core/src/foundations/ionic.vars.scss b/core/src/foundations/ionic.vars.scss index 939262cbe27..c72011d8378 100644 --- a/core/src/foundations/ionic.vars.scss +++ b/core/src/foundations/ionic.vars.scss @@ -937,6 +937,7 @@ $ion-elevation-4: var(--token-elevation-4, 0px 3px 12px 0px rgba(0, 0, 0, 0.12), // Transition $ion-transition-curve-linear: var(--token-transition-curve-linear, linear); $ion-transition-curve-quick: var(--token-transition-curve-quick, cubic-bezier(0, 0, 0.2, 1)); +$ion-transition-curve-smooth: var(--token-transition-curve-smooth, cubic-bezier(0.25, 1, 0.5, 1)); $ion-transition-curve-spring: var(--token-transition-curve-spring, cubic-bezier(0.16, 1, 0.3, 1)); $ion-transition-curve-base: var(--token-transition-curve-base, cubic-bezier(0.4, 0, 1, 1)); $ion-transition-curve-expressive: var(--token-transition-curve-expressive, cubic-bezier(0.4, 0, 0.2, 1)); @@ -945,6 +946,7 @@ $ion-transition-time-0: var(--token-transition-time-0, 0ms); $ion-transition-time-100: var(--token-transition-time-100, 100ms); $ion-transition-time-150: var(--token-transition-time-150, 150ms); $ion-transition-time-200: var(--token-transition-time-200, 200ms); +$ion-transition-time-250: var(--token-transition-time-250, 250ms); $ion-transition-time-300: var(--token-transition-time-300, 300ms); $ion-transition-time-350: var(--token-transition-time-350, 350ms); $ion-transition-time-500: var(--token-transition-time-500, 500ms);