From 8b90d1060b41ff8331bba14936bbde7908c14edb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Musia=C5=82?= Date: Fri, 3 Jul 2026 15:32:49 +0200 Subject: [PATCH 1/6] Move Multi Gesture Icon out of icon primitives --- .../Attachments/AttachmentView/index.tsx | 7 +- src/components/Icon/index.tsx | 28 +------- src/components/Icon/primitives/types.ts | 2 +- .../index.tsx} | 65 ++++++++++++++++--- .../useCanvasSize.ts | 0 .../useIconCarouselPager.ts | 0 6 files changed, 64 insertions(+), 38 deletions(-) rename src/components/{Icon/primitives/MultiGestureIcon.tsx => MultiGestureIcon/index.tsx} (51%) rename src/components/{Icon/primitives => MultiGestureIcon}/useCanvasSize.ts (100%) rename src/components/{Icon/primitives => MultiGestureIcon}/useIconCarouselPager.ts (100%) diff --git a/src/components/Attachments/AttachmentView/index.tsx b/src/components/Attachments/AttachmentView/index.tsx index ab69b29dfa5b..be3b6d783be5 100644 --- a/src/components/Attachments/AttachmentView/index.tsx +++ b/src/components/Attachments/AttachmentView/index.tsx @@ -4,6 +4,7 @@ import Button from '@components/Button'; import DistanceEReceipt from '@components/DistanceEReceipt'; import EReceipt from '@components/EReceipt'; import Icon from '@components/Icon'; +import MultiGestureIcon from '@components/MultiGestureIcon'; import {useSession} from '@components/OnyxListItemProvider'; import PerDiemEReceipt from '@components/PerDiemEReceipt'; import ScrollView from '@components/ScrollView'; @@ -23,6 +24,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {add as addCachedPDFPaths} from '@libs/actions/CachedPDFPaths'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import {getFileResolution, isHighResolutionImage} from '@libs/fileDownload/FileUtils'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import {hasEReceipt, hasReceiptSource, isDistanceRequest, isManualDistanceRequest, isOdometerDistanceRequest, isPerDiemRequest} from '@libs/TransactionUtils'; @@ -201,14 +203,15 @@ function AttachmentView({ additionalStyles = [defaultWorkspaceAvatarColor]; } + const IconComponent = canUseTouchScreen() ? MultiGestureIcon : Icon; + return ( - ); } diff --git a/src/components/Icon/index.tsx b/src/components/Icon/index.tsx index 654478d46a8e..5f94135bbaa2 100644 --- a/src/components/Icon/index.tsx +++ b/src/components/Icon/index.tsx @@ -1,11 +1,8 @@ import useStyleUtils from '@hooks/useStyleUtils'; -import {canUseTouchScreen} from '@libs/DeviceCapabilities'; - import variables from '@styles/variables'; import type IconAsset from '@src/types/utils/IconAsset'; -import type {Dimensions} from '@src/types/utils/Layout'; import type {ImageContentFit} from 'expo-image'; import type {StyleProp, ViewStyle} from 'react-native'; @@ -16,7 +13,6 @@ import type {IconSize} from './primitives/types'; import BaseIcon from './primitives/BaseIcon'; import InlineIcon from './primitives/InlineIcon'; -import MultiGestureIcon from './primitives/MultiGestureIcon'; import resolveIconSize from './primitives/resolveIconSize'; type IconProps = { @@ -80,14 +76,11 @@ type IconProps = { /** Keeps icon sizing consistent when used inside buttons. */ isButtonIcon?: boolean; - /** Wraps the icon in a multi-gesture canvas on touch devices. */ - enableMultiGestureCanvas?: boolean; - /** When set, exposes the icon to assistive tech. Leave unset for decorative icons. */ accessibilityLabel?: string; }; -/** Renders an SVG icon with preset sizes, inline layout, and optional gesture support. */ +/** Renders an SVG icon with preset sizes and inline layout. */ function Icon({ src, width = variables.iconSizeNormal, @@ -109,7 +102,6 @@ function Icon({ testID = '', contentFit = 'cover', isButtonIcon = false, - enableMultiGestureCanvas = false, accessibilityLabel, }: IconProps) { const StyleUtils = useStyleUtils(); @@ -139,24 +131,6 @@ function Icon({ ); } - if (canUseTouchScreen() && enableMultiGestureCanvas) { - const contentSize: Dimensions = {width: iconWidth as number, height: iconHeight as number}; - return ( - - ); - } - return ( ; + + /** Test identifier for end-to-end tests. */ + testID?: string; + + /** How the SVG content should fit its container. */ + contentFit?: ImageContentFit; +}; + /** Renders an icon inside a multi-gesture canvas for pinch, pan, and swipe interactions. */ -function MultiGestureIcon({testID, additionalStyles, src, contentSize, iconWidth, iconHeight, fill, isHovered, isPressed, contentFit}: ContentSizedIconProps) { +function MultiGestureIcon({ + src, + width = variables.iconSizeNormal, + height = variables.iconSizeNormal, + fill = undefined, + additionalStyles = [], + hovered = false, + pressed = false, + testID = '', + contentFit = 'cover', +}: MultiGestureIconProps) { const {canvasSize, isCanvasLoading, updateCanvasSize} = useCanvasSize(); const {pagerRef, isScrollEnabled, onTap, onSwipeDown} = useIconCarouselPager(); + if (!src) { + return null; + } + return ( @@ -52,3 +100,4 @@ function MultiGestureIcon({testID, additionalStyles, src, contentSize, iconWidth } export default MultiGestureIcon; +export type {MultiGestureIconProps}; diff --git a/src/components/Icon/primitives/useCanvasSize.ts b/src/components/MultiGestureIcon/useCanvasSize.ts similarity index 100% rename from src/components/Icon/primitives/useCanvasSize.ts rename to src/components/MultiGestureIcon/useCanvasSize.ts diff --git a/src/components/Icon/primitives/useIconCarouselPager.ts b/src/components/MultiGestureIcon/useIconCarouselPager.ts similarity index 100% rename from src/components/Icon/primitives/useIconCarouselPager.ts rename to src/components/MultiGestureIcon/useIconCarouselPager.ts From fd39252739a53d467f0f7dfde3bc58f5284f813e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Musia=C5=82?= Date: Fri, 3 Jul 2026 15:44:54 +0200 Subject: [PATCH 2/6] fix knip --- src/components/MultiGestureIcon/index.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/MultiGestureIcon/index.tsx b/src/components/MultiGestureIcon/index.tsx index da882fd1d9a3..1421bb029f69 100644 --- a/src/components/MultiGestureIcon/index.tsx +++ b/src/components/MultiGestureIcon/index.tsx @@ -81,7 +81,7 @@ function MultiGestureIcon({ > Date: Wed, 8 Jul 2026 10:29:01 +0200 Subject: [PATCH 3/6] cleanup after merge --- .../Attachments/AttachmentView/index.tsx | 13 ++++- src/components/Icon/primitives/BaseIcon.tsx | 27 +++++++++- src/components/Icon/primitives/InlineIcon.tsx | 4 +- src/components/Icon/primitives/types.ts | 35 ++++++------ src/components/MultiGestureIcon/index.tsx | 54 +++---------------- 5 files changed, 65 insertions(+), 68 deletions(-) diff --git a/src/components/Attachments/AttachmentView/index.tsx b/src/components/Attachments/AttachmentView/index.tsx index 0b302e4334e3..27d8ba68c4b4 100644 --- a/src/components/Attachments/AttachmentView/index.tsx +++ b/src/components/Attachments/AttachmentView/index.tsx @@ -212,10 +212,19 @@ function AttachmentView({ additionalStyles = [defaultWorkspaceAvatarColor]; } - const IconComponent = canUseTouchScreen() ? MultiGestureIcon : Icon; + if (canUseTouchScreen()) { + return ( + + ); + } return ( - ; -/** Shared render props for icon primitive components. */ -type BaseIconProps = { - /** Test identifier for end-to-end tests. */ - testID: string; - +type CommonIconProps = { /** Additional styles applied to the icon wrapper. */ additionalStyles: StyleProp; /** Icon asset to render. */ src: IconAsset; + /** Fill color passed to the SVG. */ + fill?: string; +}; + +/** Shared props for primitives that also receive a measured or layout content size. */ +type ContentSizedIcon = { + /** Layout size for inline icons, or intrinsic content size for gesture canvases. */ + contentSize: Dimensions; +}; + +/** Shared render props for icon primitive components. */ +type BaseIconProps = CommonIconProps & { + /** Test identifier for end-to-end tests. */ + testID?: string; + + /** Icon asset to render. */ + src: IconAsset; + /** Rendered width of the SVG content. */ iconWidth?: number | `${number}%` | 'auto'; /** Rendered height of the SVG content. */ iconHeight?: number | `${number}%` | 'auto'; - /** Fill color passed to the SVG. */ - fill?: string; - /** Whether the icon is in a hovered state. */ isHovered: boolean; @@ -39,10 +50,4 @@ type BaseIconProps = { contentFit: ImageContentFit; }; -/** Shared props for primitives that also receive a measured or layout content size. */ -type ContentSizedIconProps = BaseIconProps & { - /** Layout size for inline icons. */ - contentSize: Dimensions; -}; - -export type {BaseIconProps, ContentSizedIconProps, IconSize}; +export type {ContentSizedIcon, CommonIconProps, IconSize, BaseIconProps}; diff --git a/src/components/MultiGestureIcon/index.tsx b/src/components/MultiGestureIcon/index.tsx index c7c74215fcbb..9aee9840b02b 100644 --- a/src/components/MultiGestureIcon/index.tsx +++ b/src/components/MultiGestureIcon/index.tsx @@ -1,50 +1,15 @@ +import type {CommonIconProps, ContentSizedIcon} from '@components/Icon/primitives/types'; import ImageSVG from '@components/ImageSVG'; import MultiGestureCanvas, {DEFAULT_ZOOM_RANGE} from '@components/MultiGestureCanvas'; -import variables from '@styles/variables'; - -import type IconAsset from '@src/types/utils/IconAsset'; - -import type {ImageContentFit} from 'expo-image'; -import type {StyleProp, ViewStyle} from 'react-native'; - import React from 'react'; import {StyleSheet, View} from 'react-native'; import useCanvasSize from './useCanvasSize'; import useIconCarouselPager from './useIconCarouselPager'; -type MultiGestureIconProps = { - /** The asset to render. */ - src: IconAsset | undefined; - - /** Custom width when no preset size is selected. */ - width?: number; - - /** Custom height when no preset size is selected. */ - height?: number; - - /** Fill color for the SVG. */ - fill?: string; - - /** Whether the icon is hovered. */ - hovered?: boolean; - - /** Whether the icon is pressed. */ - pressed?: boolean; - - /** Additional styles for the icon wrapper. */ - additionalStyles?: StyleProp; - - /** Test identifier for end-to-end tests. */ - testID?: string; - - /** How the SVG content should fit its container. */ - contentFit?: ImageContentFit; -}; - /** Renders an icon inside a multi-gesture canvas for pinch, pan, and swipe interactions. */ -function MultiGestureIcon({testID, additionalStyles, src, contentSize, iconWidth, iconHeight, fill, isHovered, isPressed, contentFit}: ContentSizedIconProps) { +function MultiGestureIcon({additionalStyles, src, contentSize, fill}: CommonIconProps & ContentSizedIcon) { const {canvasSize, updateCanvasSize} = useCanvasSize(); const {pagerRef, isScrollEnabled, onTap, onSwipeDown} = useIconCarouselPager(); @@ -61,7 +26,7 @@ function MultiGestureIcon({testID, additionalStyles, src, contentSize, iconWidth - + From 1273a86adb338ad612e037fb05c6aba1f5712456 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Musia=C5=82?= Date: Wed, 8 Jul 2026 11:45:59 +0200 Subject: [PATCH 4/6] cleanup base icon props --- src/components/Icon/primitives/BaseIcon.tsx | 27 ++------------------- src/components/MultiGestureIcon/index.tsx | 4 --- 2 files changed, 2 insertions(+), 29 deletions(-) diff --git a/src/components/Icon/primitives/BaseIcon.tsx b/src/components/Icon/primitives/BaseIcon.tsx index c721f2429993..dc4252bd397f 100644 --- a/src/components/Icon/primitives/BaseIcon.tsx +++ b/src/components/Icon/primitives/BaseIcon.tsx @@ -1,36 +1,13 @@ import ImageSVG from '@components/ImageSVG'; -import type {ImageContentFit} from 'expo-image'; - import React from 'react'; import {View} from 'react-native'; -import type {CommonIconProps} from './types'; - -type BaseIconComponentProps = CommonIconProps & { - /** Test identifier for end-to-end tests. */ - testID?: string; +import type {BaseIconProps} from './types'; +type BaseIconComponentProps = BaseIconProps & { /** When set, exposes the icon to assistive tech with this label. */ accessibilityLabel?: string; - - /** Rendered width of the SVG content. */ - iconWidth?: number | `${number}%` | 'auto'; - - /** Rendered height of the SVG content. */ - iconHeight?: number | `${number}%` | 'auto'; - - /** Fill color passed to the SVG. */ - fill?: string; - - /** Whether the icon is in a hovered state. */ - isHovered?: boolean; - - /** Whether the icon is in a pressed state. */ - isPressed?: boolean; - - /** How the SVG content should fit its container. */ - contentFit?: ImageContentFit; }; /** Renders a standard icon with optional accessibility support. */ diff --git a/src/components/MultiGestureIcon/index.tsx b/src/components/MultiGestureIcon/index.tsx index 9aee9840b02b..1b5d6c5b9c88 100644 --- a/src/components/MultiGestureIcon/index.tsx +++ b/src/components/MultiGestureIcon/index.tsx @@ -13,10 +13,6 @@ function MultiGestureIcon({additionalStyles, src, contentSize, fill}: CommonIcon const {canvasSize, updateCanvasSize} = useCanvasSize(); const {pagerRef, isScrollEnabled, onTap, onSwipeDown} = useIconCarouselPager(); - if (!src) { - return null; - } - return ( Date: Wed, 8 Jul 2026 12:39:36 +0200 Subject: [PATCH 5/6] extract useCanvasSize --- .../Attachments/AttachmentView/index.tsx | 2 +- .../MultiGestureIcon/index.tsx | 30 +++++++++--- .../MultiGestureIcon/useIconCarouselPager.ts | 0 src/components/Icon/primitives/InlineIcon.tsx | 12 ++++- src/components/Icon/primitives/types.ts | 18 ++------ src/components/Lightbox/index.tsx | 11 ++--- .../MultiGestureIcon/useCanvasSize.ts | 22 --------- src/hooks/useCanvasSize.ts | 46 +++++++++++++++++++ 8 files changed, 87 insertions(+), 54 deletions(-) rename src/components/{ => Attachments}/MultiGestureIcon/index.tsx (68%) rename src/components/{ => Attachments}/MultiGestureIcon/useIconCarouselPager.ts (100%) delete mode 100644 src/components/MultiGestureIcon/useCanvasSize.ts create mode 100644 src/hooks/useCanvasSize.ts diff --git a/src/components/Attachments/AttachmentView/index.tsx b/src/components/Attachments/AttachmentView/index.tsx index 27d8ba68c4b4..66374e4bdae9 100644 --- a/src/components/Attachments/AttachmentView/index.tsx +++ b/src/components/Attachments/AttachmentView/index.tsx @@ -1,10 +1,10 @@ import {useAttachmentCarouselPagerActions} from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; +import MultiGestureIcon from '@components/Attachments/MultiGestureIcon'; import type {Attachment, AttachmentSource} from '@components/Attachments/types'; import Button from '@components/Button'; import DistanceEReceipt from '@components/DistanceEReceipt'; import EReceipt from '@components/EReceipt'; import Icon from '@components/Icon'; -import MultiGestureIcon from '@components/MultiGestureIcon'; import {useSession} from '@components/OnyxListItemProvider'; import PerDiemEReceipt from '@components/PerDiemEReceipt'; import ScrollView from '@components/ScrollView'; diff --git a/src/components/MultiGestureIcon/index.tsx b/src/components/Attachments/MultiGestureIcon/index.tsx similarity index 68% rename from src/components/MultiGestureIcon/index.tsx rename to src/components/Attachments/MultiGestureIcon/index.tsx index 1b5d6c5b9c88..f8817a9f2e1b 100644 --- a/src/components/MultiGestureIcon/index.tsx +++ b/src/components/Attachments/MultiGestureIcon/index.tsx @@ -1,16 +1,35 @@ -import type {CommonIconProps, ContentSizedIcon} from '@components/Icon/primitives/types'; import ImageSVG from '@components/ImageSVG'; import MultiGestureCanvas, {DEFAULT_ZOOM_RANGE} from '@components/MultiGestureCanvas'; +import useCanvasSize from '@hooks/useCanvasSize'; + +import type IconAsset from '@src/types/utils/IconAsset'; +import type {Dimensions} from '@src/types/utils/Layout'; + +import type {StyleProp, ViewStyle} from 'react-native'; + import React from 'react'; import {StyleSheet, View} from 'react-native'; -import useCanvasSize from './useCanvasSize'; import useIconCarouselPager from './useIconCarouselPager'; +type MultiGestureIconProps = { + /** Additional styles applied to the icon wrapper. */ + additionalStyles: StyleProp; + + /** Icon asset to render. */ + src: IconAsset; + + /** Fill color passed to the SVG. */ + fill?: string; + + /** Intrinsic content size for gesture canvases. */ + contentSize: Dimensions; +}; + /** Renders an icon inside a multi-gesture canvas for pinch, pan, and swipe interactions. */ -function MultiGestureIcon({additionalStyles, src, contentSize, fill}: CommonIconProps & ContentSizedIcon) { - const {canvasSize, updateCanvasSize} = useCanvasSize(); +function MultiGestureIcon({additionalStyles, src, contentSize, fill}: MultiGestureIconProps) { + const {canvasSize, updateCanvasSize, isCanvasLoading} = useCanvasSize(); const {pagerRef, isScrollEnabled, onTap, onSwipeDown} = useIconCarouselPager(); return ( @@ -18,7 +37,7 @@ function MultiGestureIcon({additionalStyles, src, contentSize, fill}: CommonIcon style={StyleSheet.absoluteFill} onLayout={updateCanvasSize} > - {!!canvasSize && ( + {!isCanvasLoading && ( diff --git a/src/components/MultiGestureIcon/useIconCarouselPager.ts b/src/components/Attachments/MultiGestureIcon/useIconCarouselPager.ts similarity index 100% rename from src/components/MultiGestureIcon/useIconCarouselPager.ts rename to src/components/Attachments/MultiGestureIcon/useIconCarouselPager.ts diff --git a/src/components/Icon/primitives/InlineIcon.tsx b/src/components/Icon/primitives/InlineIcon.tsx index 178efc0c121d..acd608f2f556 100644 --- a/src/components/Icon/primitives/InlineIcon.tsx +++ b/src/components/Icon/primitives/InlineIcon.tsx @@ -4,13 +4,21 @@ import ImageSVG from '@components/ImageSVG'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; +import type {Dimensions} from '@src/types/utils/Layout'; + import React from 'react'; import {View} from 'react-native'; -import type {BaseIconProps, ContentSizedIcon} from './types'; +import type {BaseIconProps} from './types'; + +/** Shared props for primitives that also receive a measured or layout content size. */ +type ContentSizedIcon = { + /** Layout size for inline icons. */ + contentSize: Dimensions; +}; /** Renders an icon positioned inline within surrounding text. */ -function InlineIcon({testID, additionalStyles, src, contentSize, iconWidth, iconHeight, fill, isHovered = false, isPressed = false, contentFit}: BaseIconProps & ContentSizedIcon) { +function InlineIcon({testID, additionalStyles, src, contentSize, iconWidth, iconHeight, fill, isHovered, isPressed, contentFit}: BaseIconProps & ContentSizedIcon) { const StyleUtils = useStyleUtils(); const styles = useThemeStyles(); const iconStyles = [StyleUtils.getWidthAndHeightStyle(contentSize.width ?? 0, contentSize.height), IconWrapperStyles, styles.pAbsolute, additionalStyles]; diff --git a/src/components/Icon/primitives/types.ts b/src/components/Icon/primitives/types.ts index 4776fcb4b1f8..25520f28b117 100644 --- a/src/components/Icon/primitives/types.ts +++ b/src/components/Icon/primitives/types.ts @@ -1,6 +1,5 @@ import type CONST from '@src/CONST'; import type IconAsset from '@src/types/utils/IconAsset'; -import type {Dimensions} from '@src/types/utils/Layout'; import type {ImageContentFit} from 'expo-image'; import type {StyleProp, ViewStyle} from 'react-native'; @@ -9,7 +8,8 @@ import type {ValueOf} from 'type-fest'; /** Preset icon size values from `CONST.ICON_SIZE`. */ type IconSize = ValueOf; -type CommonIconProps = { +/** Shared render props for icon primitive components. */ +type BaseIconProps = { /** Additional styles applied to the icon wrapper. */ additionalStyles: StyleProp; @@ -18,22 +18,10 @@ type CommonIconProps = { /** Fill color passed to the SVG. */ fill?: string; -}; - -/** Shared props for primitives that also receive a measured or layout content size. */ -type ContentSizedIcon = { - /** Layout size for inline icons, or intrinsic content size for gesture canvases. */ - contentSize: Dimensions; -}; -/** Shared render props for icon primitive components. */ -type BaseIconProps = CommonIconProps & { /** Test identifier for end-to-end tests. */ testID?: string; - /** Icon asset to render. */ - src: IconAsset; - /** Rendered width of the SVG content. */ iconWidth?: number | `${number}%` | 'auto'; @@ -50,4 +38,4 @@ type BaseIconProps = CommonIconProps & { contentFit: ImageContentFit; }; -export type {ContentSizedIcon, CommonIconProps, IconSize, BaseIconProps}; +export type {IconSize, BaseIconProps}; diff --git a/src/components/Lightbox/index.tsx b/src/components/Lightbox/index.tsx index ccec28d91179..bc65d0153f99 100644 --- a/src/components/Lightbox/index.tsx +++ b/src/components/Lightbox/index.tsx @@ -8,6 +8,7 @@ import MultiGestureCanvas, {DEFAULT_ZOOM_RANGE} from '@components/MultiGestureCa import type {OnScaleChangedCallback, ZoomRange} from '@components/MultiGestureCanvas/types'; import {getCanvasFitScale} from '@components/MultiGestureCanvas/utils'; +import useCanvasSize from '@hooks/useCanvasSize'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -18,7 +19,7 @@ import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan import CONST from '@src/CONST'; import type {Dimensions} from '@src/types/utils/Layout'; -import type {LayoutChangeEvent, StyleProp, ViewStyle} from 'react-native'; +import type {StyleProp, ViewStyle} from 'react-native'; import React, {useState} from 'react'; import {PixelRatio, StyleSheet, View} from 'react-native'; @@ -122,13 +123,7 @@ function Lightbox({attachmentID, isAuthTokenRequired = false, uri, onScaleChange const hasSiblingCarouselItems = isUsedInCarousel && !isSingleCarouselItem; const isActive = page === activePage; - const [canvasSize, setCanvasSize] = useState(); - const isCanvasLoading = canvasSize === undefined; - const updateCanvasSize = ({ - nativeEvent: { - layout: {width, height}, - }, - }: LayoutChangeEvent) => setCanvasSize({width: PixelRatio.roundToNearestPixel(width), height: PixelRatio.roundToNearestPixel(height)}); + const {canvasSize, updateCanvasSize, isCanvasLoading} = useCanvasSize(); const [contentSize, setInternalContentSize] = useState(() => cachedImageDimensions.get(uri)); const setContentSize = (newDimensions: Dimensions | undefined) => { diff --git a/src/components/MultiGestureIcon/useCanvasSize.ts b/src/components/MultiGestureIcon/useCanvasSize.ts deleted file mode 100644 index fd41a64e9e56..000000000000 --- a/src/components/MultiGestureIcon/useCanvasSize.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type {Dimensions} from '@src/types/utils/Layout'; - -import type {LayoutChangeEvent} from 'react-native'; - -import {useState} from 'react'; -import {PixelRatio} from 'react-native'; - -/** Tracks the measured layout size of the multi-gesture icon canvas. */ -function useCanvasSize() { - const [canvasSize, setCanvasSize] = useState(); - const isCanvasLoading = canvasSize === undefined; - - const updateCanvasSize = ({ - nativeEvent: { - layout: {width: layoutWidth, height: layoutHeight}, - }, - }: LayoutChangeEvent) => setCanvasSize({width: PixelRatio.roundToNearestPixel(layoutWidth), height: PixelRatio.roundToNearestPixel(layoutHeight)}); - - return {canvasSize, isCanvasLoading, updateCanvasSize}; -} - -export default useCanvasSize; diff --git a/src/hooks/useCanvasSize.ts b/src/hooks/useCanvasSize.ts new file mode 100644 index 000000000000..cd314dda5e4a --- /dev/null +++ b/src/hooks/useCanvasSize.ts @@ -0,0 +1,46 @@ +import type {Dimensions} from '@src/types/utils/Layout'; + +import type {LayoutChangeEvent} from 'react-native'; + +import {useState} from 'react'; +import {PixelRatio} from 'react-native'; + +/** + * Discriminated on `isCanvasLoading` so that checking it narrows `canvasSize` + * from `Dimensions | undefined` to `Dimensions`. + */ +type CanvasSizeResult = + | { + /** Measured layout size of the canvas. */ + canvasSize: Dimensions; + + /** Whether the canvas is still waiting for its first layout measurement. */ + isCanvasLoading: false; + + /** onLayout handler that records the measured canvas size. */ + updateCanvasSize: (event: LayoutChangeEvent) => void; + } + | { + canvasSize: undefined; + isCanvasLoading: true; + updateCanvasSize: (event: LayoutChangeEvent) => void; + }; + +/** Tracks the measured layout size of a multi-gesture canvas. */ +function useCanvasSize(): CanvasSizeResult { + const [canvasSize, setCanvasSize] = useState(); + + const updateCanvasSize = ({ + nativeEvent: { + layout: {width: layoutWidth, height: layoutHeight}, + }, + }: LayoutChangeEvent) => setCanvasSize({width: PixelRatio.roundToNearestPixel(layoutWidth), height: PixelRatio.roundToNearestPixel(layoutHeight)}); + + if (canvasSize === undefined) { + return {canvasSize, isCanvasLoading: true, updateCanvasSize}; + } + + return {canvasSize, isCanvasLoading: false, updateCanvasSize}; +} + +export default useCanvasSize; From 9bbda1d9a9f107cd3146802a253af53380e655eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Musia=C5=82?= Date: Thu, 9 Jul 2026 09:43:11 +0200 Subject: [PATCH 6/6] update testID to be required --- src/components/Icon/primitives/types.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/Icon/primitives/types.ts b/src/components/Icon/primitives/types.ts index 25520f28b117..f6f5d6552087 100644 --- a/src/components/Icon/primitives/types.ts +++ b/src/components/Icon/primitives/types.ts @@ -10,24 +10,24 @@ type IconSize = ValueOf; /** Shared render props for icon primitive components. */ type BaseIconProps = { + /** Test identifier for end-to-end tests. */ + testID: string; + /** Additional styles applied to the icon wrapper. */ additionalStyles: StyleProp; /** Icon asset to render. */ src: IconAsset; - /** Fill color passed to the SVG. */ - fill?: string; - - /** Test identifier for end-to-end tests. */ - testID?: string; - /** Rendered width of the SVG content. */ iconWidth?: number | `${number}%` | 'auto'; /** Rendered height of the SVG content. */ iconHeight?: number | `${number}%` | 'auto'; + /** Fill color passed to the SVG. */ + fill?: string; + /** Whether the icon is in a hovered state. */ isHovered: boolean;