From 07ccf562d8902b2ca3122cb4240a49d77bccefa9 Mon Sep 17 00:00:00 2001 From: Susmita Bhowmik Date: Thu, 1 Oct 2026 14:50:12 -0600 Subject: [PATCH 1/8] fix(modal): derive sheet dismissal from drag distance rather than viewport position --- core/src/components/modal/gestures/sheet.ts | 91 +++++---------------- 1 file changed, 19 insertions(+), 72 deletions(-) diff --git a/core/src/components/modal/gestures/sheet.ts b/core/src/components/modal/gestures/sheet.ts index 42a739ff558..4e821400024 100644 --- a/core/src/components/modal/gestures/sheet.ts +++ b/core/src/components/modal/gestures/sheet.ts @@ -421,14 +421,14 @@ export const createSheetGesture = ( animation.progressStep(offset); const snapBreakpoint = usePhysicsBasedGesture - ? calculateVelocitySnapBreakpoint(detail.deltaY, detail.velocityY, detail.currentY) + ? calculateVelocitySnapBreakpoint(detail.deltaY, detail.velocityY) : calculatePositionSnapBreakpoint(detail.deltaY); const eventDetail: ModalDragEventDetail = { currentY: detail.currentY, deltaY: detail.deltaY, velocityY: detail.velocityY, - progress: calculateProgress(detail.currentY), + progress: calculateProgress(detail.deltaY), snapBreakpoint: snapBreakpoint, }; @@ -437,7 +437,7 @@ export const createSheetGesture = ( const onEnd = (detail: GestureDetail) => { const snapBreakpoint = usePhysicsBasedGesture - ? calculateVelocitySnapBreakpoint(detail.deltaY, detail.velocityY, detail.currentY) + ? calculateVelocitySnapBreakpoint(detail.deltaY, detail.velocityY) : calculatePositionSnapBreakpoint(detail.deltaY); /** @@ -454,7 +454,7 @@ export const createSheetGesture = ( currentY: detail.currentY, deltaY: detail.deltaY, velocityY: detail.velocityY, - progress: calculateProgress(detail.currentY), + progress: calculateProgress(detail.deltaY), snapBreakpoint, isDismissing, }; @@ -681,15 +681,9 @@ export const createSheetGesture = ( */ const calculatePositionSnapBreakpoint = (deltaY: number): number => { /** - * Calculates the real-time vertical position of the modal. - * We combine the wrapper's current bounding box position with the - * gesture's deltaY to account for the physical movement during the drag. + * How far open the sheet is once the drag is applied, as a 0 to 1 value. */ - const currentY = wrapperEl.getBoundingClientRect().top + deltaY; - /** - * Convert that pixel position back into a 0 to 1 progress value. - */ - const currentProgress = calculateProgress(currentY); + const currentProgress = calculateProgress(deltaY); /** * Find and return the defined breakpoint that is closest to the @@ -714,15 +708,14 @@ export const createSheetGesture = ( * * @param deltaY The change in Y position since gesture started * @param velocityY The velocity in pixels per millisecond - * @param currentY The current Y position of the gesture * @returns The snap breakpoint value */ - const calculateVelocitySnapBreakpoint = (deltaY: number, velocityY: number, currentY: number): number => { + const calculateVelocitySnapBreakpoint = (deltaY: number, velocityY: number): number => { // Convert velocity from px/ms to px/s for easier threshold comparison const velocityYPerSecond = velocityY * 1000; // Calculate current progress (0 = fully closed, 1 = fully expanded) - const currentProgress = calculateProgress(currentY); + const currentProgress = calculateProgress(deltaY); // Rule 1: Fast downward flick always dismisses if (velocityYPerSecond > 500) { @@ -762,70 +755,24 @@ export const createSheetGesture = ( * A value closer to 1 means the modal is closer to being opened, * while a value closer to 0 means the modal is closer to being closed. * - * @param currentY The current Y position of the gesture + * This is measured from the drag rather than the pointer position, against + * the sheet's own height. Measuring against the viewport would report a + * sheet that does not fill the screen (a customized `--height`) as partly + * closed while it is fully open. + * + * @param deltaY The change in Y position since the gesture started * @returns The progress of the sheet gesture */ - const calculateProgress = (currentY: number): number => { - const minBreakpoint = breakpoints[0]; - const maxBreakpoint = breakpoints[breakpoints.length - 1]; - - /** - * The lowest point the sheet can be dragged to aka the point at which - * the sheet is fully closed. - */ - const maxY = convertBreakpointToY(minBreakpoint); - /** - * The highest point the sheet can be dragged to aka the point at which - * the sheet is fully open. - */ - const minY = convertBreakpointToY(maxBreakpoint); - // The total distance between the fully open and fully closed positions. - const totalDistance = maxY - minY; - // The distance from the current position to the fully closed position. - const distanceFromBottom = maxY - currentY; + const calculateProgress = (deltaY: number): number => { /** - * The progress represents how far the sheet is from the bottom relative - * to the total distance. When the user starts swiping up, the progress - * should be close to 1, and when the user has swiped all the way down, - * the progress should be close to 0. + * The sheet starts the gesture open at currentBreakpoint, and dragging + * down by the full sheet height would close it completely. */ - const progress = distanceFromBottom / totalDistance; + const progress = currentBreakpoint - deltaY / height; // Round to the nearest thousandth to avoid returning very small decimal const roundedProgress = Math.round(progress * 1000) / 1000; - return Math.max(0, Math.min(1, roundedProgress)); - }; - - /** - * Converts a breakpoint value (0 to 1) into a pixel Y coordinate - * on the screen. - * - * @param breakpoint The breakpoint value (e.g., 0.5 for half-open) - * @returns The pixel Y coordinate on the screen - */ - const convertBreakpointToY = (breakpoint: number): number => { - const rect = baseEl.getBoundingClientRect(); - const modalHeight = rect.height; - // The bottom of the screen. - const viewportBottom = window.innerHeight; - /** - * The active height is how much of the modal is actually showing - * on the screen for this specific breakpoint. - */ - const activeHeight = modalHeight * breakpoint; - - /** - * To find the Y coordinate, start at the bottom of the screen - * and move up by the active height of the modal. - * - * A breakpoint of 1.0 means the active height is the full modal height - * (fully open). A breakpoint of 0.0 means the active height is 0 - * (fully closed). - * - * Since screen Y coordinates get smaller as you go up, we subtract the - * active height from the viewport bottom. - */ - return viewportBottom - activeHeight; + return clamp(0, roundedProgress, 1); }; const gesture = createGesture({ From 37741dcf0cc2728487c99e2051c6974e1b7f0000 Mon Sep 17 00:00:00 2001 From: Susmita Bhowmik Date: Fri, 2 Oct 2026 09:53:19 -0600 Subject: [PATCH 2/8] fix(modal): dismiss sheets without intermediate breakpoints on any downward drag --- core/src/components/modal/gestures/sheet.ts | 31 +++++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/core/src/components/modal/gestures/sheet.ts b/core/src/components/modal/gestures/sheet.ts index 4e821400024..acc93605579 100644 --- a/core/src/components/modal/gestures/sheet.ts +++ b/core/src/components/modal/gestures/sheet.ts @@ -420,9 +420,7 @@ export const createSheetGesture = ( offset = clamp(0.0001, processedStep, maxStep); animation.progressStep(offset); - const snapBreakpoint = usePhysicsBasedGesture - ? calculateVelocitySnapBreakpoint(detail.deltaY, detail.velocityY) - : calculatePositionSnapBreakpoint(detail.deltaY); + const snapBreakpoint = calculateSnapBreakpoint(detail.deltaY, detail.velocityY); const eventDetail: ModalDragEventDetail = { currentY: detail.currentY, @@ -436,9 +434,7 @@ export const createSheetGesture = ( }; const onEnd = (detail: GestureDetail) => { - const snapBreakpoint = usePhysicsBasedGesture - ? calculateVelocitySnapBreakpoint(detail.deltaY, detail.velocityY) - : calculatePositionSnapBreakpoint(detail.deltaY); + const snapBreakpoint = calculateSnapBreakpoint(detail.deltaY, detail.velocityY); /** * `snapBreakpoint === 0` is not enough on its own. `canDismiss: false` @@ -671,6 +667,29 @@ export const createSheetGesture = ( }); }; + /** + * Decides where the sheet should settle for the current drag. + * + * A sheet that only opens and closes has one place to go on a downward drag, + * so any drag the gesture recognises dismisses it. Sheets with breakpoints in + * between snap to the nearest one, since dragging is also how they resize. + * + * @param deltaY The change in Y position since the gesture started + * @param velocityY The velocity in pixels per millisecond + * @returns The snap breakpoint value + */ + const calculateSnapBreakpoint = (deltaY: number, velocityY: number): number => { + const hasIntermediateBreakpoints = breakpoints.length > 2; + + if (deltaY > 0 && minBreakpoint === 0 && !hasIntermediateBreakpoints) { + return 0; + } + + return usePhysicsBasedGesture + ? calculateVelocitySnapBreakpoint(deltaY, velocityY) + : calculatePositionSnapBreakpoint(deltaY); + }; + /** * Calculates the breakpoint based on the current deltaY. * This determines where the sheet should snap to when the user releases the From 8e799ee5e08195ac501704e2681ade1ff6097044 Mon Sep 17 00:00:00 2001 From: Susmita Bhowmik Date: Fri, 2 Oct 2026 10:33:55 -0600 Subject: [PATCH 3/8] fix(modal): respect canDismiss and upward flicks when dismissing sheets --- core/src/components/modal/gestures/sheet.ts | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/core/src/components/modal/gestures/sheet.ts b/core/src/components/modal/gestures/sheet.ts index acc93605579..e4fd4a043f2 100644 --- a/core/src/components/modal/gestures/sheet.ts +++ b/core/src/components/modal/gestures/sheet.ts @@ -671,17 +671,21 @@ export const createSheetGesture = ( * Decides where the sheet should settle for the current drag. * * A sheet that only opens and closes has one place to go on a downward drag, - * so any drag the gesture recognises dismisses it. Sheets with breakpoints in + * so any drag the gesture recognizes dismisses it. Sheets with breakpoints in * between snap to the nearest one, since dragging is also how they resize. * + * An upward flick still wins, and `canDismiss` keeps its own threshold so a + * stray drag cannot trigger the callback. + * * @param deltaY The change in Y position since the gesture started * @param velocityY The velocity in pixels per millisecond * @returns The snap breakpoint value */ const calculateSnapBreakpoint = (deltaY: number, velocityY: number): number => { const hasIntermediateBreakpoints = breakpoints.length > 2; + const isFlickingUp = velocityY * 1000 < -400; - if (deltaY > 0 && minBreakpoint === 0 && !hasIntermediateBreakpoints) { + if (deltaY > 0 && minBreakpoint === 0 && !hasIntermediateBreakpoints && !isFlickingUp && !canDismissBlocksGesture) { return 0; } @@ -699,9 +703,6 @@ export const createSheetGesture = ( * @returns The snap breakpoint value. */ const calculatePositionSnapBreakpoint = (deltaY: number): number => { - /** - * How far open the sheet is once the drag is applied, as a 0 to 1 value. - */ const currentProgress = calculateProgress(deltaY); /** @@ -774,19 +775,11 @@ export const createSheetGesture = ( * A value closer to 1 means the modal is closer to being opened, * while a value closer to 0 means the modal is closer to being closed. * - * This is measured from the drag rather than the pointer position, against - * the sheet's own height. Measuring against the viewport would report a - * sheet that does not fill the screen (a customized `--height`) as partly - * closed while it is fully open. - * * @param deltaY The change in Y position since the gesture started * @returns The progress of the sheet gesture */ const calculateProgress = (deltaY: number): number => { - /** - * The sheet starts the gesture open at currentBreakpoint, and dragging - * down by the full sheet height would close it completely. - */ + // The inverse of the step applied to the animation in onMove. const progress = currentBreakpoint - deltaY / height; // Round to the nearest thousandth to avoid returning very small decimal const roundedProgress = Math.round(progress * 1000) / 1000; From 05ef59aef7204c30cd89d5bc067c94ab7ff00ec2 Mon Sep 17 00:00:00 2001 From: Susmita Bhowmik Date: Fri, 2 Oct 2026 11:05:24 -0600 Subject: [PATCH 4/8] fix(modal): remove magic numbers --- core/src/components/modal/gestures/sheet.ts | 25 +++++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/core/src/components/modal/gestures/sheet.ts b/core/src/components/modal/gestures/sheet.ts index e4fd4a043f2..d964fa2638d 100644 --- a/core/src/components/modal/gestures/sheet.ts +++ b/core/src/components/modal/gestures/sheet.ts @@ -9,6 +9,16 @@ import { getBackdropValueForSheet } from '../utils'; import { calculateSpringStep, canSwipeOnContent, handleCanDismiss } from './utils'; +/** Gesture velocity is measured in pixels per millisecond. */ +const MILLISECONDS_PER_SECOND = 1000; + +/** Flick velocities that decide the snap outright, in pixels per second. */ +const DISMISS_VELOCITY = 500; +const KEEP_OPEN_VELOCITY = -400; + +/** How far below the current breakpoint a drag must end to dismiss the sheet. */ +const DISMISS_PROGRESS_RATIO = 0.4; + export interface MoveSheetToBreakpointOptions { /** * The breakpoint value to move the sheet to. @@ -682,10 +692,11 @@ export const createSheetGesture = ( * @returns The snap breakpoint value */ const calculateSnapBreakpoint = (deltaY: number, velocityY: number): number => { - const hasIntermediateBreakpoints = breakpoints.length > 2; - const isFlickingUp = velocityY * 1000 < -400; + const isDraggingDown = deltaY > 0; + const isFlickingUp = velocityY * MILLISECONDS_PER_SECOND < KEEP_OPEN_VELOCITY; + const canOnlyOpenOrClose = minBreakpoint === 0 && breakpoints.length === 2; - if (deltaY > 0 && minBreakpoint === 0 && !hasIntermediateBreakpoints && !isFlickingUp && !canDismissBlocksGesture) { + if (isDraggingDown && canOnlyOpenOrClose && !isFlickingUp && !canDismissBlocksGesture) { return 0; } @@ -732,18 +743,18 @@ export const createSheetGesture = ( */ const calculateVelocitySnapBreakpoint = (deltaY: number, velocityY: number): number => { // Convert velocity from px/ms to px/s for easier threshold comparison - const velocityYPerSecond = velocityY * 1000; + const velocityYPerSecond = velocityY * MILLISECONDS_PER_SECOND; // Calculate current progress (0 = fully closed, 1 = fully expanded) const currentProgress = calculateProgress(deltaY); // Rule 1: Fast downward flick always dismisses - if (velocityYPerSecond > 500) { + if (velocityYPerSecond > DISMISS_VELOCITY) { return minBreakpoint; } // Rule 2: Fast upward flick moves to next breakpoint above - if (velocityYPerSecond < -400) { + if (velocityYPerSecond < KEEP_OPEN_VELOCITY) { // Find next breakpoint above current position const nextBreakpoint = breakpoints.find((bp) => bp > currentProgress); // If no breakpoint above, stay at max breakpoint @@ -757,7 +768,7 @@ export const createSheetGesture = ( const percentageBelowSnap = distanceBelowSnap / currentBreakpoint; // If dragged more than 40% below and not flicking up, dismiss - if (percentageBelowSnap > 0.4 && velocityYPerSecond <= 400) { + if (percentageBelowSnap > DISMISS_PROGRESS_RATIO && velocityYPerSecond <= 400) { return 0; } } From d87a72395f71884240f18aece508988034b1fcc5 Mon Sep 17 00:00:00 2001 From: Susmita Bhowmik Date: Fri, 2 Oct 2026 11:44:19 -0600 Subject: [PATCH 5/8] fix(modal): update comments --- core/src/components/modal/gestures/sheet.ts | 67 ++++++++++++++------- 1 file changed, 46 insertions(+), 21 deletions(-) diff --git a/core/src/components/modal/gestures/sheet.ts b/core/src/components/modal/gestures/sheet.ts index d964fa2638d..19fb7fa7602 100644 --- a/core/src/components/modal/gestures/sheet.ts +++ b/core/src/components/modal/gestures/sheet.ts @@ -9,14 +9,21 @@ import { getBackdropValueForSheet } from '../utils'; import { calculateSpringStep, canSwipeOnContent, handleCanDismiss } from './utils'; -/** Gesture velocity is measured in pixels per millisecond. */ +/** Gesture velocity arrives in pixels per millisecond; the thresholds below are per second. */ const MILLISECONDS_PER_SECOND = 1000; -/** Flick velocities that decide the snap outright, in pixels per second. */ +/** + * Flick speeds fast enough to decide the outcome on their own, in pixels per + * second. Both are magnitudes. Y grows downwards, so the upward threshold is + * negated where it is compared. + */ const DISMISS_VELOCITY = 500; -const KEEP_OPEN_VELOCITY = -400; +const KEEP_OPEN_VELOCITY = 400; -/** How far below the current breakpoint a drag must end to dismiss the sheet. */ +/** + * How far below its starting breakpoint a slow drag has to end to dismiss the + * sheet. 0.4 means the user dragged 40% of the way down from where they started. + */ const DISMISS_PROGRESS_RATIO = 0.4; export interface MoveSheetToBreakpointOptions { @@ -95,6 +102,11 @@ export const createSheetGesture = ( const contentEl = baseEl.querySelector('ion-content'); // Cache the initial value so the gesture restores it instead of forcing scrolling on. const initialContentScrollY = contentEl?.scrollY ?? true; + /** + * The height of the sheet the user actually sees. This comes from the wrapper + * rather than the host element, which is stretched to the full viewport no + * matter how tall the sheet itself is. + */ const height = wrapperEl.clientHeight; let currentBreakpoint = initialBreakpoint; let offset = 0; @@ -678,14 +690,19 @@ export const createSheetGesture = ( }; /** - * Decides where the sheet should settle for the current drag. + * Decides which breakpoint the sheet should settle on for the current drag. * - * A sheet that only opens and closes has one place to go on a downward drag, - * so any drag the gesture recognizes dismisses it. Sheets with breakpoints in - * between snap to the nearest one, since dragging is also how they resize. + * A sheet whose only breakpoints are 0 and 1 has nowhere to go on a downward + * drag except closed, so any drag the gesture recognizes closes it. Sheets + * with breakpoints in between snap to the nearest one instead, because + * dragging is how the user resizes them. * - * An upward flick still wins, and `canDismiss` keeps its own threshold so a - * stray drag cannot trigger the callback. + * Two cases opt back out of that shortcut and use the normal thresholds: + * - An upward flick, so a drag that wanders downwards before flicking back up + * reopens the sheet rather than closing it. + * - `canDismiss`, which asks the application whether closing is allowed. Going + * through the usual thresholds means a few stray pixels cannot trigger that + * callback. * * @param deltaY The change in Y position since the gesture started * @param velocityY The velocity in pixels per millisecond @@ -693,7 +710,7 @@ export const createSheetGesture = ( */ const calculateSnapBreakpoint = (deltaY: number, velocityY: number): number => { const isDraggingDown = deltaY > 0; - const isFlickingUp = velocityY * MILLISECONDS_PER_SECOND < KEEP_OPEN_VELOCITY; + const isFlickingUp = velocityY * MILLISECONDS_PER_SECOND < -KEEP_OPEN_VELOCITY; const canOnlyOpenOrClose = minBreakpoint === 0 && breakpoints.length === 2; if (isDraggingDown && canOnlyOpenOrClose && !isFlickingUp && !canDismissBlocksGesture) { @@ -754,7 +771,7 @@ export const createSheetGesture = ( } // Rule 2: Fast upward flick moves to next breakpoint above - if (velocityYPerSecond < KEEP_OPEN_VELOCITY) { + if (velocityYPerSecond < -KEEP_OPEN_VELOCITY) { // Find next breakpoint above current position const nextBreakpoint = breakpoints.find((bp) => bp > currentProgress); // If no breakpoint above, stay at max breakpoint @@ -767,7 +784,12 @@ export const createSheetGesture = ( const distanceBelowSnap = currentBreakpoint - currentProgress; const percentageBelowSnap = distanceBelowSnap / currentBreakpoint; - // If dragged more than 40% below and not flicking up, dismiss + /** + * The velocity check reads as "not flicking up", but upward flicks are + * negative and already satisfy it. What it excludes in practice is a + * downward drag faster than 400 px/s but not fast enough to trip the + * dismissal above, which falls through to the nearest breakpoint instead. + */ if (percentageBelowSnap > DISMISS_PROGRESS_RATIO && velocityYPerSecond <= 400) { return 0; } @@ -778,19 +800,22 @@ export const createSheetGesture = ( }; /** - * Calculates the progress of the swipe gesture. - * - * The progress is a value between 0 and 1 that represents how far - * the swipe has progressed towards closing the modal. - * - * A value closer to 1 means the modal is closer to being opened, - * while a value closer to 0 means the modal is closer to being closed. + * Calculates how open the sheet is part way through a gesture, on the same + * 0 to 1 scale as the breakpoints: 1 is fully open and 0 is fully closed. * * @param deltaY The change in Y position since the gesture started * @returns The progress of the sheet gesture */ const calculateProgress = (deltaY: number): number => { - // The inverse of the step applied to the animation in onMove. + /** + * Start from how open the sheet was when the gesture began, then subtract + * the fraction of the sheet's height the user has dragged. Dragging down is + * positive, so it lowers the progress. + * + * This is the inverse of the step applied to the animation in onMove, which + * keeps the breakpoint the sheet snaps to in agreement with the position it + * is drawn at. + */ const progress = currentBreakpoint - deltaY / height; // Round to the nearest thousandth to avoid returning very small decimal const roundedProgress = Math.round(progress * 1000) / 1000; From 6bd81549100d659d933b64d0838766c2928b7ef3 Mon Sep 17 00:00:00 2001 From: Susmita Bhowmik Date: Fri, 2 Oct 2026 14:24:23 -0600 Subject: [PATCH 6/8] test(modal): add drag tests for sheets without intermediate breakpoints --- .../modal/test/can-dismiss/modal-sheet.e2e.ts | 14 ++++-- .../components/modal/test/sheet/index.html | 14 ++++++ .../components/modal/test/sheet/modal.e2e.ts | 48 +++++++++++++++++++ 3 files changed, 72 insertions(+), 4 deletions(-) diff --git a/core/src/components/modal/test/can-dismiss/modal-sheet.e2e.ts b/core/src/components/modal/test/can-dismiss/modal-sheet.e2e.ts index 865b5b5a8de..2be21e2c03b 100644 --- a/core/src/components/modal/test/can-dismiss/modal-sheet.e2e.ts +++ b/core/src/components/modal/test/can-dismiss/modal-sheet.e2e.ts @@ -112,11 +112,11 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => await ionHandlerDone.next(); - const modal = page.locator('ion-modal'); - expect(modal).not.toBe(null); + await expect(page.locator('ion-modal')).toBeVisible(); }); test('should not dismiss on swipe when not attempting to close', async ({ page }) => { const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + const ionDragEnd = await page.spyOnEvent('ionDragEnd'); await page.click('#sheet-can-dismiss-promise-true'); @@ -125,8 +125,14 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => const modalHeader = page.locator('#modal-header'); await dragElementBy(modalHeader, page, 0, 30); - const modal = page.locator('ion-modal'); - expect(modal).not.toBe(null); + /** + * A drag this small snaps the sheet back open, so canDismiss is never + * consulted and the sheet stays where it was. + */ + const dragEndEvent = await ionDragEnd.next(); + + expect(dragEndEvent.detail.isDismissing).toBe(false); + await expect(page.locator('ion-modal')).toBeVisible(); }); test('should hit the dismiss threshold when swiping', async ({ page }) => { const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); diff --git a/core/src/components/modal/test/sheet/index.html b/core/src/components/modal/test/sheet/index.html index 4a8d3f5b666..f78a4a93108 100644 --- a/core/src/components/modal/test/sheet/index.html +++ b/core/src/components/modal/test/sheet/index.html @@ -21,6 +21,14 @@ --height: 50%; } + /** + * Shorter than the midpoint of the viewport, which is where the sheet + * position and the drag distance disagree the most. + */ + .short-sheet { + --height: 40%; + } + .custom-handle::part(handle) { top: -16px; background: rgba(255, 255, 255, 0.53); @@ -131,6 +139,12 @@ +