feat: component's updates from Discover page (#3869)
* feat: component's updates from Discover page * docs: update component stories * chore: add react-native-view-shot
@@ -410,6 +410,8 @@ PODS:
|
||||
- react-native-tcp-socket (6.0.6):
|
||||
- CocoaAsyncSocket
|
||||
- React-Core
|
||||
- react-native-view-shot (3.1.2):
|
||||
- React
|
||||
- react-native-webview (11.24.0):
|
||||
- React-Core
|
||||
- React-NativeModulesApple (0.72.5):
|
||||
@@ -654,6 +656,7 @@ DEPENDENCIES:
|
||||
- "react-native-slider (from `../../../node_modules/@react-native-community/slider`)"
|
||||
- react-native-tab-page-view (from `../../../node_modules/react-native-tab-page-view`)
|
||||
- react-native-tcp-socket (from `../../../node_modules/react-native-tcp-socket`)
|
||||
- react-native-view-shot (from `../../../node_modules/react-native-view-shot`)
|
||||
- react-native-webview (from `../../../node_modules/react-native-webview`)
|
||||
- React-NativeModulesApple (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
|
||||
- React-perflogger (from `../../../node_modules/react-native/ReactCommon/reactperflogger`)
|
||||
@@ -815,6 +818,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../../../node_modules/react-native-tab-page-view"
|
||||
react-native-tcp-socket:
|
||||
:path: "../../../node_modules/react-native-tcp-socket"
|
||||
react-native-view-shot:
|
||||
:path: "../../../node_modules/react-native-view-shot"
|
||||
react-native-webview:
|
||||
:path: "../../../node_modules/react-native-webview"
|
||||
React-NativeModulesApple:
|
||||
@@ -944,6 +949,7 @@ SPEC CHECKSUMS:
|
||||
react-native-slider: 1cdd6ba29675df21f30544253bf7351d3c2d68c4
|
||||
react-native-tab-page-view: 35bba61d5d8036fdf23976439d613a43ef1f041c
|
||||
react-native-tcp-socket: e724380c910c2e704816ec817ed28f1342246ff7
|
||||
react-native-view-shot: 4475fde003fe8a210053d1f98fb9e06c1d834e1c
|
||||
react-native-webview: 6408bc328e65042d240d0ebd62baa76ffd23b1aa
|
||||
React-NativeModulesApple: 797bc6078d566eef3fb3f74127e6e1d2e945a15f
|
||||
React-perflogger: cd8886513f68e1c135a1e79d20575c6489641597
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
"react-native-svg": "^13.9.0",
|
||||
"react-native-tab-page-view": "https://github.com/OneKeyHQ/react-native-tab-page-view#90ea54bfb01021c4e54e27cb9fc72a3702df0aa3",
|
||||
"react-native-tcp-socket": "^6.0.6",
|
||||
"react-native-view-shot": "3.1.2",
|
||||
"react-native-webview": "https://github.com/OneKeyHQ/react-native-webview.git#5036d59cba4f5609c32b7eb5dec560e738ce7115",
|
||||
"react-native-zip-archive": "^6.0.9",
|
||||
"readable-stream": "^3.6.0",
|
||||
|
||||
@@ -53,15 +53,14 @@ function ActionListItem({
|
||||
opacity={disabled ? 0.5 : 1}
|
||||
disabled={disabled}
|
||||
{...(!disabled && {
|
||||
mb: 2,
|
||||
hoverStyle: { bg: '$bgHover' },
|
||||
pressStyle: { bg: '$bgActive' },
|
||||
focusable: true,
|
||||
focusStyle: {
|
||||
outlineColor: '$focusRing',
|
||||
outlineStyle: 'solid',
|
||||
outlineWidth: 2,
|
||||
},
|
||||
// focusable: true,
|
||||
// focusStyle: {
|
||||
// outlineColor: '$focusRing',
|
||||
// outlineStyle: 'solid',
|
||||
// outlineWidth: 2,
|
||||
// },
|
||||
})}
|
||||
onPress={handlePress}
|
||||
>
|
||||
@@ -86,7 +85,7 @@ function ActionListItem({
|
||||
);
|
||||
}
|
||||
|
||||
interface IActionListSection {
|
||||
export interface IActionListSection {
|
||||
title?: string;
|
||||
items: IActionListItemProps[];
|
||||
}
|
||||
|
||||
@@ -347,6 +347,7 @@ const icons = {
|
||||
CommandOutline: () => import("./react/outline/Command"),
|
||||
CommandKeyOutline: () => import("./react/outline/CommandKey"),
|
||||
CompassOutline: () => import("./react/outline/Compass"),
|
||||
CompassCircleOutline: () => import("./react/outline/CompassCircle"),
|
||||
CompassSquareOutline: () => import("./react/outline/CompassSquare"),
|
||||
ComputerOutline: () => import("./react/outline/Computer"),
|
||||
ConsoleOutline: () => import("./react/outline/Console"),
|
||||
@@ -854,6 +855,7 @@ const icons = {
|
||||
VolumeUpOutline: () => import("./react/outline/VolumeUp"),
|
||||
WalletOutline: () => import("./react/outline/Wallet"),
|
||||
WalletCardOutline: () => import("./react/outline/WalletCard"),
|
||||
WalletCryptoOutline: () => import("./react/outline/WalletCrypto"),
|
||||
WatchRoundOutline: () => import("./react/outline/WatchRound"),
|
||||
WatchSquareOutline: () => import("./react/outline/WatchSquare"),
|
||||
WcPaperToiletOutline: () => import("./react/outline/WcPaperToilet"),
|
||||
@@ -1122,6 +1124,7 @@ const icons = {
|
||||
CommandSolid: () => import("./react/solid/Command"),
|
||||
CommandKeySolid: () => import("./react/solid/CommandKey"),
|
||||
CompassSolid: () => import("./react/solid/Compass"),
|
||||
CompassCircleSolid: () => import("./react/solid/CompassCircle"),
|
||||
CompassSquareSolid: () => import("./react/solid/CompassSquare"),
|
||||
ComputerSolid: () => import("./react/solid/Computer"),
|
||||
ConsoleSolid: () => import("./react/solid/Console"),
|
||||
@@ -1620,6 +1623,7 @@ const icons = {
|
||||
VolumeUpSolid: () => import("./react/solid/VolumeUp"),
|
||||
WalletSolid: () => import("./react/solid/Wallet"),
|
||||
WalletCardSolid: () => import("./react/solid/WalletCard"),
|
||||
WalletCryptoSolid: () => import("./react/solid/WalletCrypto"),
|
||||
WatchRoundSolid: () => import("./react/solid/WatchRound"),
|
||||
WatchSquareSolid: () => import("./react/solid/WatchSquare"),
|
||||
WcPaperToiletSolid: () => import("./react/solid/WcPaperToilet"),
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { IKeyOfIcons } from './Icons';
|
||||
import { GetProps, styled, withStaticProperties } from 'tamagui';
|
||||
import type { Svg, SvgProps } from 'react-native-svg';
|
||||
import { TextStyle } from 'react-native';
|
||||
import { View } from '../View';
|
||||
|
||||
export type IIconContainerProps = Omit<SvgProps, 'color' | 'style'> & {
|
||||
name?: IKeyOfIcons;
|
||||
@@ -68,10 +69,13 @@ const IconContainer = forwardRef(({ name, style }: IIconContainerProps, _) => {
|
||||
color={componentColor}
|
||||
/>
|
||||
) : (
|
||||
<Suspense fallback={null}>
|
||||
<Suspense fallback={<View style={{
|
||||
width: componentWidth,
|
||||
height: componentHeight,
|
||||
}} />}>
|
||||
<IconLoader
|
||||
width={componentWidth}
|
||||
height={componentHeight}
|
||||
width={componentWidth}
|
||||
height={componentHeight}
|
||||
style={style}
|
||||
color={componentColor}
|
||||
name={name}
|
||||
|
||||
12
packages/components/src/Icon/react/outline/CompassCircle.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import Svg, { SvgProps, Path } from 'react-native-svg';
|
||||
const SvgCompassCircle = (props: SvgProps) => (
|
||||
<Svg fill="none" viewBox="0 0 24 24" accessibilityRole="image" {...props}>
|
||||
<Path
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
d="M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm12.075-2.075-3.26.89-.89 3.26 3.26-.89.89-3.26Zm.318-2.16a1.5 1.5 0 0 1 1.841 1.842l-1.119 4.105a2 2 0 0 1-1.403 1.403l-4.105 1.12a1.5 1.5 0 0 1-1.842-1.842l1.12-4.105a2 2 0 0 1 1.403-1.403l4.105-1.12Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</Svg>
|
||||
);
|
||||
export default SvgCompassCircle;
|
||||
@@ -3,13 +3,13 @@ const SvgWallet = (props: SvgProps) => (
|
||||
<Svg fill="none" viewBox="0 0 24 24" accessibilityRole="image" {...props}>
|
||||
<Path
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
d="M3 6.5A3.5 3.5 0 0 1 6.5 3h8.75c.966 0 1.75.784 1.75 1.75V8h1a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3h-5a1 1 0 1 1 0-2h5a1 1 0 0 0 1-1v-7a1 1 0 0 0-1-1H6.5c-.537 0-1.045-.12-1.5-.337v.837a1 1 0 1 1-2 0v-4Zm2 0A1.5 1.5 0 0 0 6.5 8H15V5H6.5A1.5 1.5 0 0 0 5 6.5Zm-.008 6.68a2 2 0 0 1 2.016 0l2 1.167A2 2 0 0 1 10 16.074v2.352a2 2 0 0 1-.992 1.727l-2 1.167a2 2 0 0 1-2.016 0l-2-1.167A2 2 0 0 1 2 18.426v-2.352a2 2 0 0 1 .992-1.727l2-1.167ZM8 16.074l-2-1.166-2 1.166v2.352l2 1.166 2-1.166v-2.352Z"
|
||||
clipRule="evenodd"
|
||||
d="M15.5 15.75a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Z"
|
||||
/>
|
||||
<Path
|
||||
fill="currentColor"
|
||||
d="M17 14.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Z"
|
||||
fillRule="evenodd"
|
||||
d="M3 6.5A3.5 3.5 0 0 1 6.5 3h8.088A2.412 2.412 0 0 1 17 5.412V8h1a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3H7a4 4 0 0 1-4-4V6.5Zm2 3.163V17a2 2 0 0 0 2 2h11a1 1 0 0 0 1-1v-7a1 1 0 0 0-1-1H6.5c-.537 0-1.045-.12-1.5-.337ZM5 6.5A1.5 1.5 0 0 0 6.5 8H15V5.412A.412.412 0 0 0 14.588 5H6.5A1.5 1.5 0 0 0 5 6.5Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</Svg>
|
||||
);
|
||||
|
||||
13
packages/components/src/Icon/react/outline/WalletCrypto.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import Svg, { SvgProps, Path } from 'react-native-svg';
|
||||
const SvgWalletCrypto = (props: SvgProps) => (
|
||||
<Svg fill="none" viewBox="0 0 24 24" accessibilityRole="image" {...props}>
|
||||
<Path
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
d="M3 6.5A3.5 3.5 0 0 1 6.5 3h8.75c.966 0 1.75.784 1.75 1.75V8h1a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3h-5a1 1 0 1 1 0-2h5a1 1 0 0 0 1-1v-7a1 1 0 0 0-1-1H6.5c-.537 0-1.045-.12-1.5-.337v.837a1 1 0 1 1-2 0v-4Zm2 0A1.5 1.5 0 0 0 6.5 8H15V5H6.5A1.5 1.5 0 0 0 5 6.5Zm-.008 6.68a2 2 0 0 1 2.016 0l2 1.167A2 2 0 0 1 10 16.074v2.352a2 2 0 0 1-.992 1.727l-2 1.167a2 2 0 0 1-2.016 0l-2-1.167A2 2 0 0 1 2 18.426v-2.352a2 2 0 0 1 .992-1.727l2-1.167ZM8 16.074l-2-1.166-2 1.166v2.352l2 1.166 2-1.166v-2.352Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
<Path fill="#000" d="M17 14.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Z" />
|
||||
</Svg>
|
||||
);
|
||||
export default SvgWalletCrypto;
|
||||
@@ -245,6 +245,7 @@ export { default as Color } from './Color';
|
||||
export { default as ColumnWide } from './ColumnWide';
|
||||
export { default as CommandKey } from './CommandKey';
|
||||
export { default as Command } from './Command';
|
||||
export { default as CompassCircle } from './CompassCircle';
|
||||
export { default as CompassSquare } from './CompassSquare';
|
||||
export { default as Compass } from './Compass';
|
||||
export { default as Computer } from './Computer';
|
||||
@@ -741,6 +742,7 @@ export { default as VolumeOffMute } from './VolumeOffMute';
|
||||
export { default as VolumeOff } from './VolumeOff';
|
||||
export { default as VolumeUp } from './VolumeUp';
|
||||
export { default as WalletCard } from './WalletCard';
|
||||
export { default as WalletCrypto } from './WalletCrypto';
|
||||
export { default as Wallet } from './Wallet';
|
||||
export { default as WatchRound } from './WatchRound';
|
||||
export { default as WatchSquare } from './WatchSquare';
|
||||
|
||||
12
packages/components/src/Icon/react/solid/CompassCircle.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import Svg, { SvgProps, Path } from 'react-native-svg';
|
||||
const SvgCompassCircle = (props: SvgProps) => (
|
||||
<Svg fill="none" viewBox="0 0 24 24" accessibilityRole="image" {...props}>
|
||||
<Path
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
d="M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm12.524-3.753a1 1 0 0 1 1.228 1.228l-1.12 4.105a1.5 1.5 0 0 1-1.052 1.052l-4.105 1.12a1 1 0 0 1-1.228-1.228l1.12-4.105a1.5 1.5 0 0 1 1.052-1.052l4.105-1.12Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</Svg>
|
||||
);
|
||||
export default SvgCompassCircle;
|
||||
@@ -4,13 +4,7 @@ const SvgWallet = (props: SvgProps) => (
|
||||
<Path
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
d="M5.496 12.886a1 1 0 0 1 1.008 0l3 1.75A1 1 0 0 1 10 15.5V19a1 1 0 0 1-.496.864l-3 1.75a1 1 0 0 1-1.008 0l-3-1.75A1 1 0 0 1 2 19v-3.5a1 1 0 0 1 .496-.864l3-1.75ZM4 16.074v2.352l2 1.166 2-1.166v-2.352l-2-1.166-2 1.166Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
<Path
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
d="M3 6.5v5.527l1.488-.868a3 3 0 0 1 3.024 0l3 1.75A3 3 0 0 1 12 15.5V21h6a3 3 0 0 0 3-3v-7a3 3 0 0 0-3-3h-1V5.412A2.412 2.412 0 0 0 14.588 3H6.5A3.5 3.5 0 0 0 3 6.5ZM6.5 8a1.5 1.5 0 1 1 0-3h8.088c.228 0 .412.184.412.412V8H6.5Zm9 8a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"
|
||||
d="M6.5 3A3.5 3.5 0 0 0 3 6.5V17a4 4 0 0 0 4 4h11a3 3 0 0 0 3-3v-7a3 3 0 0 0-3-3h-1V5.412A2.412 2.412 0 0 0 14.588 3H6.5ZM15 8V5.412A.412.412 0 0 0 14.588 5H6.5a1.5 1.5 0 1 0 0 3H15Zm.5 7.75a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</Svg>
|
||||
|
||||
18
packages/components/src/Icon/react/solid/WalletCrypto.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import Svg, { SvgProps, Path } from 'react-native-svg';
|
||||
const SvgWalletCrypto = (props: SvgProps) => (
|
||||
<Svg fill="none" viewBox="0 0 24 24" accessibilityRole="image" {...props}>
|
||||
<Path
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
d="M5.496 12.886a1 1 0 0 1 1.008 0l3 1.75A1 1 0 0 1 10 15.5V19a1 1 0 0 1-.496.864l-3 1.75a1 1 0 0 1-1.008 0l-3-1.75A1 1 0 0 1 2 19v-3.5a1 1 0 0 1 .496-.864l3-1.75ZM4 16.074v2.352l2 1.166 2-1.166v-2.352l-2-1.166-2 1.166Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
<Path
|
||||
fill="#000"
|
||||
fillRule="evenodd"
|
||||
d="M3 6.5v5.527l1.488-.868a3 3 0 0 1 3.024 0l3 1.75A3 3 0 0 1 12 15.5V21h6a3 3 0 0 0 3-3v-7a3 3 0 0 0-3-3h-1V5.412A2.412 2.412 0 0 0 14.588 3H6.5A3.5 3.5 0 0 0 3 6.5ZM6.5 8a1.5 1.5 0 1 1 0-3h8.088c.228 0 .412.184.412.412V8H6.5Zm9 8a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</Svg>
|
||||
);
|
||||
export default SvgWalletCrypto;
|
||||
@@ -245,6 +245,7 @@ export { default as Color } from './Color';
|
||||
export { default as ColumnWide } from './ColumnWide';
|
||||
export { default as CommandKey } from './CommandKey';
|
||||
export { default as Command } from './Command';
|
||||
export { default as CompassCircle } from './CompassCircle';
|
||||
export { default as CompassSquare } from './CompassSquare';
|
||||
export { default as Compass } from './Compass';
|
||||
export { default as Computer } from './Computer';
|
||||
@@ -741,6 +742,7 @@ export { default as VolumeOffMute } from './VolumeOffMute';
|
||||
export { default as VolumeOff } from './VolumeOff';
|
||||
export { default as VolumeUp } from './VolumeUp';
|
||||
export { default as WalletCard } from './WalletCard';
|
||||
export { default as WalletCrypto } from './WalletCrypto';
|
||||
export { default as Wallet } from './Wallet';
|
||||
export { default as WatchRound } from './WatchRound';
|
||||
export { default as WatchSquare } from './WatchSquare';
|
||||
|
||||
@@ -65,6 +65,7 @@ export const IconButton = (props: IIconButtonProps) => {
|
||||
{...(variant === 'tertiary' && {
|
||||
m: negativeMargin,
|
||||
})}
|
||||
{...(size === 'small' && { hitSlop: 8 })}
|
||||
{...sharedFrameStyles}
|
||||
{...rest}
|
||||
>
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Text } from '../Text';
|
||||
import { getSharedInputStyles } from './sharedStyles';
|
||||
|
||||
import type { IKeyOfIcons } from '../Icon';
|
||||
import type { GetProps } from 'tamagui';
|
||||
import type { ColorTokens, GetProps } from 'tamagui';
|
||||
|
||||
type ITMInputProps = GetProps<typeof TMInput>;
|
||||
|
||||
@@ -21,6 +21,7 @@ export type IInputProps = {
|
||||
error?: boolean;
|
||||
addOns?: {
|
||||
iconName?: IKeyOfIcons;
|
||||
iconColor?: ColorTokens;
|
||||
label?: string;
|
||||
onPress?: () => void;
|
||||
loading?: boolean;
|
||||
@@ -75,6 +76,7 @@ function BaseInput(
|
||||
} = SIZE_MAPPINGS[size];
|
||||
|
||||
const sharedStyles = getSharedInputStyles({ disabled, editable, error });
|
||||
|
||||
return (
|
||||
<Group
|
||||
orientation="horizontal"
|
||||
@@ -140,52 +142,65 @@ function BaseInput(
|
||||
borderColor={sharedStyles.borderColor}
|
||||
bg={sharedStyles.backgroundColor}
|
||||
disabled={disabled}
|
||||
disablePassBorderRadius="start"
|
||||
>
|
||||
{addOns.map(({ iconName, label, onPress, loading }) => (
|
||||
<Group.Item>
|
||||
<XStack
|
||||
onPress={onPress}
|
||||
key={`${iconName || ''}-${label || ''}`}
|
||||
alignItems="center"
|
||||
px={size === 'large' ? '$2.5' : '$2'}
|
||||
{...(onPress &&
|
||||
!disabled && {
|
||||
hoverStyle: {
|
||||
bg: '$bgHover',
|
||||
},
|
||||
pressStyle: {
|
||||
bg: '$bgActive',
|
||||
},
|
||||
})}
|
||||
focusable={!(disabled || loading)}
|
||||
focusStyle={sharedStyles.focusStyle}
|
||||
>
|
||||
{loading ? (
|
||||
<YStack {...(size !== 'small' && { p: '$0.5' })}>
|
||||
<Spinner size="small" />
|
||||
</YStack>
|
||||
) : (
|
||||
iconName && (
|
||||
<Icon
|
||||
name={iconName}
|
||||
color={disabled ? '$iconDisabled' : '$icon'}
|
||||
size={size === 'small' ? '$5' : '$6'}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{label && (
|
||||
<Text
|
||||
userSelect="none"
|
||||
variant={size === 'small' ? '$bodyMd' : '$bodyLg'}
|
||||
ml={iconName ? '$2' : '$0'}
|
||||
color={sharedStyles.color}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
)}
|
||||
</XStack>
|
||||
</Group.Item>
|
||||
))}
|
||||
{addOns.map(({ iconName, iconColor, label, onPress, loading }) => {
|
||||
const getIconColor = () => {
|
||||
if (disabled) {
|
||||
return '$iconDisabled';
|
||||
}
|
||||
if (iconColor) {
|
||||
return iconColor;
|
||||
}
|
||||
return '$iconSubdued';
|
||||
};
|
||||
|
||||
return (
|
||||
<Group.Item>
|
||||
<XStack
|
||||
onPress={onPress}
|
||||
key={`${iconName || ''}-${label || ''}`}
|
||||
alignItems="center"
|
||||
px={size === 'large' ? '$2.5' : '$2'}
|
||||
{...(onPress &&
|
||||
!disabled && {
|
||||
hoverStyle: {
|
||||
bg: '$bgHover',
|
||||
},
|
||||
pressStyle: {
|
||||
bg: '$bgActive',
|
||||
},
|
||||
})}
|
||||
focusable={!(disabled || loading)}
|
||||
focusStyle={sharedStyles.focusStyle}
|
||||
>
|
||||
{loading ? (
|
||||
<YStack {...(size !== 'small' && { p: '$0.5' })}>
|
||||
<Spinner size="small" />
|
||||
</YStack>
|
||||
) : (
|
||||
iconName && (
|
||||
<Icon
|
||||
name={iconName}
|
||||
color={getIconColor()}
|
||||
size={size === 'small' ? '$5' : '$6'}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{label && (
|
||||
<Text
|
||||
userSelect="none"
|
||||
variant={size === 'small' ? '$bodyMd' : '$bodyLg'}
|
||||
ml={iconName ? '$2' : '$0'}
|
||||
color={disabled ? '$textDisabled' : '$textSubdued'}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
)}
|
||||
</XStack>
|
||||
</Group.Item>
|
||||
);
|
||||
})}
|
||||
</Group>
|
||||
</Group.Item>
|
||||
)}
|
||||
|
||||
@@ -211,6 +211,7 @@ function ListItem(props: IListItemProps) {
|
||||
focusable: true,
|
||||
focusStyle: {
|
||||
outlineWidth: 2,
|
||||
outlineOffset: -2,
|
||||
outlineStyle: 'solid',
|
||||
outlineColor: '$focusRing',
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@ import { IconButton } from '../../IconButton';
|
||||
import type { IIconButtonProps } from '../../IconButton';
|
||||
|
||||
function HeaderIconButton(props: IIconButtonProps) {
|
||||
return <IconButton variant="tertiary" {...props} />;
|
||||
return <IconButton variant="tertiary" focusStyle={undefined} {...props} />;
|
||||
}
|
||||
|
||||
export default HeaderIconButton;
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
|
||||
type IHeaderSearchBarProps = {
|
||||
height?: string;
|
||||
autoFocus?: boolean;
|
||||
/**
|
||||
* A callback that gets called when search bar has lost focus
|
||||
*/
|
||||
@@ -38,6 +39,7 @@ type IHeaderSearchBarProps = {
|
||||
};
|
||||
|
||||
function HeaderSearchBar({
|
||||
autoFocus,
|
||||
onBlur,
|
||||
onFocus,
|
||||
onChangeText,
|
||||
@@ -93,6 +95,7 @@ function HeaderSearchBar({
|
||||
{...(media.gtMd && {
|
||||
size: 'small',
|
||||
})}
|
||||
autoFocus={autoFocus}
|
||||
onBlur={onBlurCallback}
|
||||
onFocus={onFocusCallback}
|
||||
onChangeText={handleChangeCallback}
|
||||
|
||||
@@ -133,6 +133,7 @@ function HeaderView({
|
||||
</Stack>
|
||||
{!!headerSearchBarOptions && (
|
||||
<HeaderSearchBar
|
||||
autoFocus={headerSearchBarOptions?.autoFocus}
|
||||
placeholder={headerSearchBarOptions?.placeholder}
|
||||
onChangeText={headerSearchBarOptions?.onChangeText}
|
||||
onBlur={headerSearchBarOptions?.onBlur}
|
||||
|
||||
@@ -41,15 +41,21 @@ export function RootModalNavigator<RouteName extends string>({
|
||||
);
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
// iOS Pad Modal not is full screen
|
||||
const modalStyle =
|
||||
PlatformEnv.isNative && !PlatformEnv.isNativeIOSPad
|
||||
? {
|
||||
paddingBottom: insets.bottom,
|
||||
bg: '$bgApp',
|
||||
}
|
||||
: {};
|
||||
|
||||
return (
|
||||
<Stack
|
||||
flex={1}
|
||||
paddingBottom={
|
||||
// iOS Pad Modal not is full screen
|
||||
PlatformEnv.isNative && !PlatformEnv.isNativeIOSPad ? insets.bottom : 0
|
||||
}
|
||||
paddingLeft={insets.left}
|
||||
paddingRight={insets.right}
|
||||
{...modalStyle}
|
||||
>
|
||||
<ThemeProvider value={TransparentModalTheme}>
|
||||
<ModalStack.Navigator screenOptions={screenOptions}>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ComponentType } from 'react';
|
||||
|
||||
import type { IActionListSection } from '../../ActionList';
|
||||
import type { IKeyOfIcons } from '../../Icon';
|
||||
import type { ILocaleIds } from '../../locale';
|
||||
import type { RouteProp } from '@react-navigation/core/lib/typescript/src/types';
|
||||
@@ -27,6 +28,7 @@ export interface ITabNavigatorConfig<RouteName extends string> {
|
||||
freezeOnBlur?: boolean;
|
||||
disable?: boolean;
|
||||
tabBarStyle?: Animated.WithAnimatedValue<StyleProp<ViewStyle>>;
|
||||
actionList?: IActionListSection[];
|
||||
}
|
||||
|
||||
export interface ICommonNavigatorConfig<
|
||||
|
||||
@@ -84,6 +84,9 @@ export interface ISearchBarProps {
|
||||
*/
|
||||
shouldShowHintSearchIcon?: boolean;
|
||||
hideNavigationBar?: boolean;
|
||||
hideWhenScrolling?: boolean;
|
||||
autoFocus?: boolean;
|
||||
cancelButtonText?: string;
|
||||
}
|
||||
|
||||
export type IStackHeaderProps = {
|
||||
|
||||
@@ -14,8 +14,9 @@ import { Portal } from '../../../Portal';
|
||||
import useProviderSideBarValue from '../../../Provider/hooks/useProviderSideBarValue';
|
||||
import { YStack } from '../../../Stack';
|
||||
|
||||
import { TabItem } from './TabItem';
|
||||
import { DesktopTabItem } from './DesktopTabItem';
|
||||
|
||||
import type { IActionListSection } from '../../../ActionList';
|
||||
import type { IKeyOfIcons } from '../../../Icon';
|
||||
import type { ITabNavigatorExtraConfig } from '../../Navigator/types';
|
||||
import type {
|
||||
@@ -33,7 +34,9 @@ function TabItemView({
|
||||
isActive: boolean;
|
||||
route: NavigationState['routes'][0];
|
||||
onPress: () => void;
|
||||
options: BottomTabNavigationOptions;
|
||||
options: BottomTabNavigationOptions & {
|
||||
actionList?: IActionListSection[];
|
||||
};
|
||||
isCollapse?: boolean;
|
||||
}) {
|
||||
useMemo(() => {
|
||||
@@ -46,7 +49,7 @@ function TabItemView({
|
||||
}, [options]);
|
||||
const contentMemo = useMemo(
|
||||
() => (
|
||||
<TabItem
|
||||
<DesktopTabItem
|
||||
onPress={onPress}
|
||||
aria-current={isActive ? 'page' : undefined}
|
||||
selected={isActive}
|
||||
@@ -54,6 +57,7 @@ function TabItemView({
|
||||
// @ts-expect-error
|
||||
icon={options?.tabBarIcon?.(isActive) as IKeyOfIcons}
|
||||
label={(options.tabBarLabel ?? route.name) as string}
|
||||
actionList={options.actionList}
|
||||
/>
|
||||
),
|
||||
[isActive, onPress, options, route.name],
|
||||
@@ -104,6 +108,8 @@ export function DesktopLeftSideBar({
|
||||
if (route.name === extraConfig?.name) {
|
||||
return (
|
||||
<YStack
|
||||
flex={1}
|
||||
key={route.key}
|
||||
onPress={() => {
|
||||
// Avoid re-rendering by checking if it's the current route.
|
||||
if (state.routeNames[state.index] !== extraConfig?.name) {
|
||||
@@ -169,10 +175,15 @@ export function DesktopLeftSideBar({
|
||||
/>
|
||||
)}
|
||||
<YStack
|
||||
testID="Desktop-AppSideBar-Content-Container"
|
||||
flex={1}
|
||||
pt={platformEnv.isDesktopMac ? undefined : '$3'}
|
||||
px="$3"
|
||||
pb="$3"
|
||||
testID="Desktop-AppSideBar-Content-Container"
|
||||
// Need to replaced by HeaderHeightContext
|
||||
pt={platformEnv.isDesktopMac ? undefined : '$3'}
|
||||
$platform-web={{
|
||||
h: platformEnv.isDesktopMac ? 'calc(100vh - 64px)' : '100vh',
|
||||
}}
|
||||
>
|
||||
{tabs}
|
||||
</YStack>
|
||||
|
||||
104
packages/components/src/Navigation/Tab/TabBar/DesktopTabItem.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { ActionList, type IActionListSection } from '../../../ActionList';
|
||||
import { Avatar } from '../../../Avatar';
|
||||
import { Icon } from '../../../Icon';
|
||||
import { IconButton } from '../../../IconButton';
|
||||
import { Stack, XStack } from '../../../Stack';
|
||||
import { Text } from '../../../Text';
|
||||
|
||||
import type { IKeyOfIcons } from '../../../Icon';
|
||||
import type { Animated, StyleProp, ViewStyle } from 'react-native';
|
||||
import type { AvatarImage, GetProps } from 'tamagui';
|
||||
|
||||
export interface IDesktopTabItemProps {
|
||||
icon?: IKeyOfIcons;
|
||||
avatarSrc?: GetProps<typeof AvatarImage>['src'];
|
||||
label?: string;
|
||||
selected?: boolean;
|
||||
tabBarStyle?: Animated.WithAnimatedValue<StyleProp<ViewStyle>>;
|
||||
actionList?: IActionListSection[];
|
||||
onActionListOpenChange?: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
export function DesktopTabItem(
|
||||
props: IDesktopTabItemProps & GetProps<typeof Stack>,
|
||||
) {
|
||||
const {
|
||||
icon,
|
||||
label,
|
||||
selected,
|
||||
tabBarStyle,
|
||||
actionList,
|
||||
avatarSrc,
|
||||
onActionListOpenChange,
|
||||
...rest
|
||||
} = props;
|
||||
return (
|
||||
<XStack
|
||||
alignItems="center"
|
||||
py="$1.5"
|
||||
$gtMd={{
|
||||
flexDirection: 'row',
|
||||
px: '$2',
|
||||
bg: selected ? '$bgActive' : undefined,
|
||||
borderRadius: '$2',
|
||||
}}
|
||||
style={tabBarStyle as ViewStyle}
|
||||
{...rest}
|
||||
>
|
||||
{icon && (
|
||||
<Icon
|
||||
flexShrink={0}
|
||||
name={icon}
|
||||
color={selected ? '$iconActive' : '$iconSubdued'}
|
||||
size="$5"
|
||||
/>
|
||||
)}
|
||||
{avatarSrc && (
|
||||
<Avatar borderRadius="$1" size="$4.5" m="$px">
|
||||
<Avatar.Image src={avatarSrc} />
|
||||
<Avatar.Fallback>
|
||||
<Icon
|
||||
size="$4.5"
|
||||
name="GlobusOutline"
|
||||
color={selected ? '$iconActive' : '$iconSubdued'}
|
||||
/>
|
||||
</Avatar.Fallback>
|
||||
</Avatar>
|
||||
)}
|
||||
{label && (
|
||||
<Text
|
||||
flex={1}
|
||||
numberOfLines={1}
|
||||
ml="$2"
|
||||
color="$text"
|
||||
variant="$bodyMd"
|
||||
userSelect="none"
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
)}
|
||||
{actionList && (
|
||||
<ActionList
|
||||
title="Action List"
|
||||
placement="right-start"
|
||||
onOpenChange={onActionListOpenChange}
|
||||
renderTrigger={
|
||||
selected && (
|
||||
<Stack>
|
||||
<IconButton
|
||||
size="small"
|
||||
icon="DotHorOutline"
|
||||
variant="tertiary"
|
||||
focusStyle={undefined}
|
||||
p="$0.5"
|
||||
m={-3}
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
sections={actionList}
|
||||
/>
|
||||
)}
|
||||
</XStack>
|
||||
);
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { useIsKeyboardShown } from '../../../hooks';
|
||||
import useSafeAreaInsets from '../../../Provider/hooks/useSafeAreaInsets';
|
||||
import { Stack } from '../../../Stack';
|
||||
|
||||
import { TabItem } from './TabItem';
|
||||
import { MobileTabItem } from './MobileTabItem';
|
||||
|
||||
import type { IKeyOfIcons } from '../../../Icon';
|
||||
import type { BottomTabBarProps } from '@react-navigation/bottom-tabs/src/types';
|
||||
@@ -51,7 +51,7 @@ export default function MobileBottomTabBar({
|
||||
};
|
||||
|
||||
const renderItemContent = (renderActive: boolean) => (
|
||||
<TabItem
|
||||
<MobileTabItem
|
||||
testID="Mobile-AppTabBar-TabItem-Icon"
|
||||
// @ts-expect-error
|
||||
icon={options?.tabBarIcon?.(renderActive) as IKeyOfIcons}
|
||||
|
||||
@@ -1,44 +1,31 @@
|
||||
import { type GetProps } from 'tamagui';
|
||||
|
||||
import { Icon } from '../../../Icon';
|
||||
import { Stack } from '../../../Stack';
|
||||
import { YStack } from '../../../Stack';
|
||||
import { Text } from '../../../Text';
|
||||
|
||||
import type { IKeyOfIcons } from '../../../Icon';
|
||||
import type { Animated, StyleProp, ViewStyle } from 'react-native';
|
||||
|
||||
interface ITabItemProps {
|
||||
interface IMobileTabItemProps {
|
||||
icon?: IKeyOfIcons;
|
||||
label?: string;
|
||||
selected?: boolean;
|
||||
tabBarStyle?: Animated.WithAnimatedValue<StyleProp<ViewStyle>>;
|
||||
}
|
||||
|
||||
export function TabItem(props: ITabItemProps & GetProps<typeof Stack>) {
|
||||
export function MobileTabItem(
|
||||
props: IMobileTabItemProps & GetProps<typeof YStack>,
|
||||
) {
|
||||
const { icon, label, selected, tabBarStyle, ...rest } = props;
|
||||
|
||||
return (
|
||||
<Stack
|
||||
alignItems="center"
|
||||
py="$1.5"
|
||||
$gtMd={{
|
||||
flexDirection: 'row',
|
||||
px: '$2',
|
||||
bg: selected ? '$bgActive' : undefined,
|
||||
borderRadius: '$2',
|
||||
}}
|
||||
style={tabBarStyle as ViewStyle}
|
||||
{...rest}
|
||||
>
|
||||
<YStack alignItems="center" py="$1.5" {...rest}>
|
||||
{icon && (
|
||||
<Icon
|
||||
flexShrink={0}
|
||||
name={icon}
|
||||
color={selected ? '$iconActive' : '$iconSubdued'}
|
||||
size="$7"
|
||||
$gtMd={{
|
||||
size: '$5',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{label && (
|
||||
@@ -47,17 +34,11 @@ export function TabItem(props: ITabItemProps & GetProps<typeof Stack>) {
|
||||
mt="$0.5"
|
||||
variant="$headingXxs"
|
||||
color={selected ? '$text' : '$textSubdued'}
|
||||
$gtMd={{
|
||||
mt: '$0',
|
||||
ml: '$2',
|
||||
color: '$text',
|
||||
variant: '$bodyMd',
|
||||
}}
|
||||
userSelect="none"
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</YStack>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useLayoutEffect } from 'react';
|
||||
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
|
||||
import { navigationRef } from '../Navigation/Navigator/NavigationContainer';
|
||||
import { Stack } from '../Stack';
|
||||
|
||||
import type { IStackNavigationOptions } from '../Navigation';
|
||||
|
||||
@@ -10,18 +10,9 @@ export type IPageHeaderProps = IStackNavigationOptions;
|
||||
|
||||
export function PageHeader(props: IPageHeaderProps) {
|
||||
const navigation = useNavigation();
|
||||
const ref = useRef<IPageHeaderProps | undefined>();
|
||||
useEffect(() => {
|
||||
if (!ref.current) {
|
||||
ref.current = navigationRef.current?.getCurrentOptions();
|
||||
}
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions(props);
|
||||
return () => {
|
||||
if (ref.current) {
|
||||
navigation.setOptions(ref.current);
|
||||
}
|
||||
};
|
||||
}, [navigation, props]);
|
||||
|
||||
return null;
|
||||
return <Stack />;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { ProgressProps as TMProgressProps } from 'tamagui';
|
||||
type IProgressProps = TMProgressProps;
|
||||
|
||||
export const Progress = ({ size, ...props }: IProgressProps) => (
|
||||
<TMProgress value={50} backgroundColor="$neutral5" {...props} h="$1">
|
||||
<TMProgress backgroundColor="$neutral5" h="$0.5" {...props}>
|
||||
<TMProgress.Indicator
|
||||
animation="quick"
|
||||
backgroundColor="$bgPrimary"
|
||||
|
||||
158
packages/components/src/TabView/Header.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import { forwardRef, useCallback, useMemo } from 'react';
|
||||
import type { ForwardedRef } from 'react';
|
||||
|
||||
import { useProps, useStyle } from '@tamagui/core';
|
||||
import { PageHeaderView } from 'react-native-tab-page-view';
|
||||
|
||||
import { useThemeValue } from '../Provider/hooks/useThemeValue';
|
||||
|
||||
import type { StackStyleProps, TextStyleProps } from '@tamagui/web/types/types';
|
||||
import type { GetProps } from 'tamagui';
|
||||
|
||||
export type IHeaderProps = Omit<
|
||||
GetProps<typeof PageHeaderView>,
|
||||
| 'style'
|
||||
| 'itemContainerStyle'
|
||||
| 'itemTitleStyle'
|
||||
| 'itemTitleNormalStyle'
|
||||
| 'itemTitleSelectedStyle'
|
||||
| 'cursorStyle'
|
||||
> &
|
||||
StackStyleProps & {
|
||||
style?: StackStyleProps;
|
||||
contentContainerStyle?: StackStyleProps;
|
||||
scrollContainerStyle?: StackStyleProps;
|
||||
containerStyle?: StackStyleProps;
|
||||
itemContainerStyle?: StackStyleProps;
|
||||
itemTitleStyle?: TextStyleProps;
|
||||
itemTitleNormalStyle?: TextStyleProps & { color: string };
|
||||
itemTitleSelectedStyle?: TextStyleProps & { color: string };
|
||||
cursorStyle?: StackStyleProps;
|
||||
};
|
||||
|
||||
const HeaderComponent = (
|
||||
{
|
||||
style,
|
||||
titleFromItem = (item: { title: string }) => item.title,
|
||||
contentContainerStyle = {},
|
||||
scrollContainerStyle = {},
|
||||
containerStyle = {},
|
||||
itemContainerStyle = { px: '$3', mr: '$2' },
|
||||
itemTitleStyle = { fontSize: 16 },
|
||||
itemTitleNormalStyle = { color: '$textSubdued' },
|
||||
itemTitleSelectedStyle = { color: '$text' },
|
||||
cursorStyle = {
|
||||
left: '$3',
|
||||
right: '$3',
|
||||
h: '$0.5',
|
||||
bg: '$text',
|
||||
},
|
||||
...props
|
||||
}: IHeaderProps,
|
||||
ref: ForwardedRef<PageHeaderView>,
|
||||
) => {
|
||||
const normalColor = itemTitleNormalStyle.color;
|
||||
const selectedColor = itemTitleSelectedStyle.color;
|
||||
const [rawNormalColor, rawSelectedColor] = useThemeValue(
|
||||
// @ts-expect-error
|
||||
[normalColor, selectedColor],
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
const data = useMemo(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
() => props.data,
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[props.data, rawNormalColor, rawSelectedColor],
|
||||
);
|
||||
const reloadWebPxNumber = useCallback((value: any) => {
|
||||
if (typeof value === 'string') {
|
||||
return Number(value?.replace(/px/, ''));
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return value;
|
||||
}, []);
|
||||
|
||||
const rawProps = useProps(props, {
|
||||
resolveValues: 'value',
|
||||
});
|
||||
const rawStyle = useStyle(
|
||||
{ ...{ h: '$14', bg: '$bgApp' }, ...style } as Record<string, unknown>,
|
||||
{
|
||||
resolveValues: 'value',
|
||||
},
|
||||
);
|
||||
const rawContentContainerStyle = useStyle(
|
||||
contentContainerStyle as Record<string, unknown>,
|
||||
{
|
||||
resolveValues: 'value',
|
||||
},
|
||||
);
|
||||
const rawScrollContainerStyle = useStyle(
|
||||
scrollContainerStyle as Record<string, unknown>,
|
||||
{
|
||||
resolveValues: 'value',
|
||||
},
|
||||
);
|
||||
const rawContainerStyle = useStyle(
|
||||
containerStyle as Record<string, unknown>,
|
||||
{
|
||||
resolveValues: 'value',
|
||||
},
|
||||
);
|
||||
const rawItemContainerStyle = {
|
||||
...useStyle(itemContainerStyle as Record<string, unknown>, {
|
||||
resolveValues: 'value',
|
||||
}),
|
||||
...{ flex: itemContainerStyle.flex },
|
||||
};
|
||||
const rawItemTitleStyle = {
|
||||
...useStyle(itemTitleStyle as Record<string, unknown>, {
|
||||
resolveValues: 'value',
|
||||
}),
|
||||
...{ fontSize: itemTitleStyle.fontSize },
|
||||
};
|
||||
const rawItemTitleNormalStyle = {
|
||||
...useStyle(itemTitleNormalStyle as Record<string, unknown>, {
|
||||
resolveValues: 'value',
|
||||
}),
|
||||
...{
|
||||
color: rawNormalColor,
|
||||
fontSize: itemTitleNormalStyle.fontSize ?? itemTitleStyle.fontSize,
|
||||
},
|
||||
};
|
||||
const rawItemTitleSelectedStyle = {
|
||||
...useStyle(itemTitleSelectedStyle as Record<string, unknown>, {
|
||||
resolveValues: 'value',
|
||||
}),
|
||||
...{
|
||||
color: rawSelectedColor,
|
||||
fontSize: itemTitleSelectedStyle.fontSize ?? itemTitleStyle.fontSize,
|
||||
},
|
||||
};
|
||||
const rawCursorStyle = useStyle(cursorStyle as Record<string, unknown>, {
|
||||
resolveValues: 'value',
|
||||
});
|
||||
rawCursorStyle.left = reloadWebPxNumber(rawCursorStyle?.left);
|
||||
rawCursorStyle.right = reloadWebPxNumber(rawCursorStyle?.right);
|
||||
rawCursorStyle.width = reloadWebPxNumber(rawCursorStyle?.width);
|
||||
return (
|
||||
<PageHeaderView
|
||||
ref={ref}
|
||||
titleFromItem={titleFromItem}
|
||||
style={rawStyle as any}
|
||||
contentContainerStyle={rawContentContainerStyle}
|
||||
scrollContainerStyle={rawScrollContainerStyle}
|
||||
containerStyle={rawContainerStyle}
|
||||
itemContainerStyle={rawItemContainerStyle}
|
||||
itemTitleStyle={rawItemTitleStyle}
|
||||
itemTitleNormalStyle={rawItemTitleNormalStyle}
|
||||
itemTitleSelectedStyle={rawItemTitleSelectedStyle}
|
||||
cursorStyle={rawCursorStyle}
|
||||
data={data}
|
||||
{...rawProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Header = forwardRef(HeaderComponent);
|
||||
@@ -1,24 +1,195 @@
|
||||
import {
|
||||
PageContentView,
|
||||
PageHeaderView,
|
||||
PageManager,
|
||||
SelectedLabel,
|
||||
} from 'react-native-tab-page-view';
|
||||
import { forwardRef, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import type { ReactElement } from 'react';
|
||||
|
||||
PageHeaderView.defaultProps = {
|
||||
...PageHeaderView.defaultProps,
|
||||
titleFromItem: (item: any) => (item as { title: string }).title,
|
||||
itemContainerStyle: { paddingHorizontal: 12, marginRight: 8 },
|
||||
itemTitleStyle: { fontSize: 15 },
|
||||
itemTitleNormalStyle: { color: '#333' },
|
||||
itemTitleSelectedStyle: { color: 'orange', fontSize: 17 },
|
||||
cursorStyle: {
|
||||
width: null,
|
||||
// width: 30,
|
||||
// borderRadius: 1,
|
||||
height: 2,
|
||||
backgroundColor: 'orange',
|
||||
},
|
||||
import { PageContentView, PageManager } from 'react-native-tab-page-view';
|
||||
import { withStaticProperties } from 'tamagui';
|
||||
|
||||
import platformEnv from '@onekeyhq/shared/src/platformEnv';
|
||||
|
||||
import { ScrollView } from '../ScrollView';
|
||||
import { Stack } from '../Stack';
|
||||
|
||||
import { Header } from './Header';
|
||||
|
||||
import type { IHeaderProps } from './Header';
|
||||
import type { IScrollViewProps, IScrollViewRef } from '../ScrollView';
|
||||
|
||||
type IPageType = ({
|
||||
onContentSizeChange,
|
||||
}: {
|
||||
onContentSizeChange: (width: number, height: number) => void;
|
||||
}) => ReactElement | null;
|
||||
|
||||
export interface ITabProps extends IScrollViewProps {
|
||||
data: { title: string; page: IPageType }[];
|
||||
initialScrollIndex?: number;
|
||||
ListHeaderComponent?: ReactElement;
|
||||
headerProps?: IHeaderProps;
|
||||
}
|
||||
|
||||
const TabComponent = ({
|
||||
data,
|
||||
initialScrollIndex,
|
||||
ListHeaderComponent,
|
||||
headerProps,
|
||||
...props
|
||||
}: ITabProps) => {
|
||||
const [contentHeight, setContentHeight] = useState<number | undefined>(1);
|
||||
const scrollViewRef = useRef<IScrollViewRef | null>(null);
|
||||
const pageContainerRef = useRef<any | null>(null);
|
||||
const stickyConfig = useMemo(
|
||||
() => ({
|
||||
lastIndex: -1,
|
||||
scrollViewHeight: 0,
|
||||
headerViewY: 0,
|
||||
headerViewHeight: 0,
|
||||
data: data.map(() => ({
|
||||
contentHeight: 0,
|
||||
contentOffsetY: 0,
|
||||
})),
|
||||
}),
|
||||
[data],
|
||||
);
|
||||
const reloadContentHeight = useCallback(
|
||||
(index: number) => {
|
||||
if (stickyConfig.scrollViewHeight * stickyConfig.headerViewHeight <= 0) {
|
||||
return;
|
||||
}
|
||||
const minHeight =
|
||||
stickyConfig.scrollViewHeight - stickyConfig.headerViewHeight;
|
||||
const height = Math.max(
|
||||
stickyConfig.data[index].contentHeight,
|
||||
minHeight,
|
||||
);
|
||||
if (platformEnv.isNative) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||
pageContainerRef?.current?.setNativeProps?.({
|
||||
height,
|
||||
});
|
||||
} else {
|
||||
if (height === contentHeight) {
|
||||
return;
|
||||
}
|
||||
setContentHeight(height);
|
||||
}
|
||||
},
|
||||
[stickyConfig, pageContainerRef, contentHeight],
|
||||
);
|
||||
const pageManagerProps = useMemo(
|
||||
() => ({
|
||||
data,
|
||||
initialScrollIndex,
|
||||
onSelectedPageIndex: (index: number) => {
|
||||
if (index >= stickyConfig.data.length) {
|
||||
return;
|
||||
}
|
||||
reloadContentHeight(index);
|
||||
const { contentOffsetY } = stickyConfig.data[index];
|
||||
const lastContentOffsetY =
|
||||
stickyConfig.data?.[stickyConfig.lastIndex]?.contentOffsetY ?? 0;
|
||||
if (
|
||||
Math.round(lastContentOffsetY) < Math.round(stickyConfig.headerViewY)
|
||||
) {
|
||||
stickyConfig.data[index].contentOffsetY = lastContentOffsetY;
|
||||
} else if (
|
||||
Math.round(contentOffsetY) <= Math.round(stickyConfig.headerViewY)
|
||||
) {
|
||||
stickyConfig.data[index].contentOffsetY = stickyConfig.headerViewY;
|
||||
}
|
||||
// Need to wait for contentHeight to be updated on android and web
|
||||
setTimeout(() => {
|
||||
if (platformEnv.isNative) {
|
||||
scrollViewRef?.current?.setNativeProps({
|
||||
contentOffset: { y: stickyConfig.data[index].contentOffsetY },
|
||||
});
|
||||
} else {
|
||||
scrollViewRef?.current?.scrollTo({
|
||||
y: stickyConfig.data[index].contentOffsetY,
|
||||
animated: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
stickyConfig.lastIndex = index;
|
||||
},
|
||||
}),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[stickyConfig, data, initialScrollIndex],
|
||||
);
|
||||
const pageManager = useMemo(
|
||||
() => new PageManager(pageManagerProps),
|
||||
[pageManagerProps],
|
||||
);
|
||||
const renderContentItem = useCallback(
|
||||
({
|
||||
item,
|
||||
index,
|
||||
}: {
|
||||
item: {
|
||||
page: IPageType;
|
||||
};
|
||||
index: number;
|
||||
}) => (
|
||||
<item.page
|
||||
onContentSizeChange={(_: number, height: number) => {
|
||||
stickyConfig.data[index].contentHeight = height;
|
||||
if (index === pageManager.pageIndex) {
|
||||
reloadContentHeight(index);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
),
|
||||
[stickyConfig.data, pageManager, reloadContentHeight],
|
||||
);
|
||||
const Content = pageManager.renderContentView;
|
||||
return (
|
||||
<ScrollView
|
||||
ref={scrollViewRef}
|
||||
onLayout={(event) => {
|
||||
stickyConfig.scrollViewHeight = event.nativeEvent.layout.height;
|
||||
}}
|
||||
stickyHeaderIndices={[1]}
|
||||
scrollEventThrottle={16}
|
||||
onScroll={(event) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
stickyConfig.data[pageManager.pageIndex].contentOffsetY = (
|
||||
event as any
|
||||
).nativeEvent.contentOffset.y;
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{ListHeaderComponent}
|
||||
<Header
|
||||
ref={pageManager.headerView}
|
||||
{...pageManagerProps}
|
||||
{...headerProps}
|
||||
onLayout={(event) => {
|
||||
stickyConfig.headerViewHeight = event.nativeEvent.layout.height;
|
||||
}}
|
||||
onSelectedPageIndex={(pageIndex: number) => {
|
||||
pageManager?.contentView?.current?.scrollPageIndex(pageIndex);
|
||||
}}
|
||||
/>
|
||||
<Stack
|
||||
ref={pageContainerRef}
|
||||
onLayout={(event) => {
|
||||
stickyConfig.headerViewY =
|
||||
event.nativeEvent.layout.y - stickyConfig.headerViewHeight;
|
||||
}}
|
||||
h={contentHeight}
|
||||
>
|
||||
<Content
|
||||
windowSize={5}
|
||||
scrollEnabled={platformEnv.isNative}
|
||||
shouldSelectedPageAnimation={platformEnv.isNative}
|
||||
renderItem={renderContentItem}
|
||||
/>
|
||||
</Stack>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
export { PageHeaderView, PageContentView, PageManager, SelectedLabel };
|
||||
export const Tab = withStaticProperties(forwardRef(TabComponent), {
|
||||
Header,
|
||||
Manager: PageManager,
|
||||
Content: PageContentView,
|
||||
});
|
||||
|
||||
55
packages/components/src/UnorderedList/index.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { Children, cloneElement, isValidElement } from 'react';
|
||||
|
||||
import { Icon } from '../Icon';
|
||||
import { Stack, XStack } from '../Stack';
|
||||
import { Text } from '../Text';
|
||||
|
||||
import type { IIconProps } from '../Icon';
|
||||
import type { StackProps, XStackProps } from 'tamagui';
|
||||
|
||||
interface IUnOrderedListItemProps extends XStackProps {
|
||||
icon?: IIconProps['name'];
|
||||
iconProps?: IIconProps;
|
||||
}
|
||||
|
||||
export function UnOrderedListItem({
|
||||
children,
|
||||
icon,
|
||||
iconProps,
|
||||
...rest
|
||||
}: IUnOrderedListItemProps) {
|
||||
return (
|
||||
<XStack tag="li" role="listitem" {...rest}>
|
||||
<XStack w="$5" h="$6" justifyContent="center" alignItems="center">
|
||||
{icon ? (
|
||||
<Icon name={icon} {...iconProps} />
|
||||
) : (
|
||||
<XStack w="$1.5" h="$1.5" borderRadius="$full" bg="$textSubdued" />
|
||||
)}
|
||||
</XStack>
|
||||
<Text pl="$2" tag="p" variant="$bodyLg">
|
||||
{children}
|
||||
</Text>
|
||||
</XStack>
|
||||
);
|
||||
}
|
||||
|
||||
export function UnOrderedList({ children, ...rest }: StackProps) {
|
||||
let isFirstItem = true;
|
||||
|
||||
const enhanceChildren = Children.map(children, (child) => {
|
||||
if (isValidElement<IUnOrderedListItemProps>(child) && isFirstItem) {
|
||||
isFirstItem = false;
|
||||
return child;
|
||||
}
|
||||
return cloneElement(child, { pt: '$1' });
|
||||
});
|
||||
|
||||
return (
|
||||
<Stack p="$0" m="$0" tag="ul" role="list" {...rest}>
|
||||
{enhanceChildren}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
UnOrderedList.Item = UnOrderedListItem;
|
||||
@@ -23,6 +23,7 @@ export * from './Select';
|
||||
export * from './Dialog';
|
||||
export * from './ActionList';
|
||||
export * from './Slider';
|
||||
export * from './UnorderedList';
|
||||
export * from './Popover';
|
||||
export * from './Shortcut';
|
||||
export * from './Progress';
|
||||
|
||||
5
packages/components/svg/outline/compass-circle.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" fill-rule="evenodd"
|
||||
d="M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm12.075-2.075-3.26.89-.89 3.26 3.26-.89.89-3.26Zm.318-2.16a1.5 1.5 0 0 1 1.841 1.842l-1.119 4.105a2 2 0 0 1-1.403 1.403l-4.105 1.12a1.5 1.5 0 0 1-1.842-1.842l1.12-4.105a2 2 0 0 1 1.403-1.403l4.105-1.12Z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 495 B |
6
packages/components/svg/outline/wallet-crypto.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" fill-rule="evenodd"
|
||||
d="M3 6.5A3.5 3.5 0 0 1 6.5 3h8.75c.966 0 1.75.784 1.75 1.75V8h1a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3h-5a1 1 0 1 1 0-2h5a1 1 0 0 0 1-1v-7a1 1 0 0 0-1-1H6.5c-.537 0-1.045-.12-1.5-.337v.837a1 1 0 1 1-2 0v-4Zm2 0A1.5 1.5 0 0 0 6.5 8H15V5H6.5A1.5 1.5 0 0 0 5 6.5Zm-.008 6.68a2 2 0 0 1 2.016 0l2 1.167A2 2 0 0 1 10 16.074v2.352a2 2 0 0 1-.992 1.727l-2 1.167a2 2 0 0 1-2.016 0l-2-1.167A2 2 0 0 1 2 18.426v-2.352a2 2 0 0 1 .992-1.727l2-1.167ZM8 16.074l-2-1.166-2 1.166v2.352l2 1.166 2-1.166v-2.352Z"
|
||||
clip-rule="evenodd" />
|
||||
<path fill="#000" d="M17 14.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 740 B |
@@ -1,6 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M15.5 15.75a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Z" />
|
||||
<path fill="currentColor" fill-rule="evenodd"
|
||||
d="M3 6.5A3.5 3.5 0 0 1 6.5 3h8.75c.966 0 1.75.784 1.75 1.75V8h1a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3h-5a1 1 0 1 1 0-2h5a1 1 0 0 0 1-1v-7a1 1 0 0 0-1-1H6.5c-.537 0-1.045-.12-1.5-.337v.837a1 1 0 1 1-2 0v-4Zm2 0A1.5 1.5 0 0 0 6.5 8H15V5H6.5A1.5 1.5 0 0 0 5 6.5Zm-.008 6.68a2 2 0 0 1 2.016 0l2 1.167A2 2 0 0 1 10 16.074v2.352a2 2 0 0 1-.992 1.727l-2 1.167a2 2 0 0 1-2.016 0l-2-1.167A2 2 0 0 1 2 18.426v-2.352a2 2 0 0 1 .992-1.727l2-1.167ZM8 16.074l-2-1.166-2 1.166v2.352l2 1.166 2-1.166v-2.352Z"
|
||||
d="M3 6.5A3.5 3.5 0 0 1 6.5 3h8.088A2.412 2.412 0 0 1 17 5.412V8h1a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3H7a4 4 0 0 1-4-4V6.5Zm2 3.163V17a2 2 0 0 0 2 2h11a1 1 0 0 0 1-1v-7a1 1 0 0 0-1-1H6.5c-.537 0-1.045-.12-1.5-.337ZM5 6.5A1.5 1.5 0 0 0 6.5 8H15V5.412A.412.412 0 0 0 14.588 5H6.5A1.5 1.5 0 0 0 5 6.5Z"
|
||||
clip-rule="evenodd" />
|
||||
<path fill="currentColor" d="M17 14.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Z" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 748 B After Width: | Height: | Size: 567 B |
5
packages/components/svg/solid/compass-circle.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" fill-rule="evenodd"
|
||||
d="M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm12.524-3.753a1 1 0 0 1 1.228 1.228l-1.12 4.105a1.5 1.5 0 0 1-1.052 1.052l-4.105 1.12a1 1 0 0 1-1.228-1.228l1.12-4.105a1.5 1.5 0 0 1 1.052-1.052l4.105-1.12Z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 413 B |
8
packages/components/svg/solid/wallet-crypto.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" fill-rule="evenodd"
|
||||
d="M5.496 12.886a1 1 0 0 1 1.008 0l3 1.75A1 1 0 0 1 10 15.5V19a1 1 0 0 1-.496.864l-3 1.75a1 1 0 0 1-1.008 0l-3-1.75A1 1 0 0 1 2 19v-3.5a1 1 0 0 1 .496-.864l3-1.75ZM4 16.074v2.352l2 1.166 2-1.166v-2.352l-2-1.166-2 1.166Z"
|
||||
clip-rule="evenodd" />
|
||||
<path fill="#000" fill-rule="evenodd"
|
||||
d="M3 6.5v5.527l1.488-.868a3 3 0 0 1 3.024 0l3 1.75A3 3 0 0 1 12 15.5V21h6a3 3 0 0 0 3-3v-7a3 3 0 0 0-3-3h-1V5.412A2.412 2.412 0 0 0 14.588 3H6.5A3.5 3.5 0 0 0 3 6.5ZM6.5 8a1.5 1.5 0 1 1 0-3h8.088c.228 0 .412.184.412.412V8H6.5Zm9 8a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 743 B |
@@ -1,8 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" fill-rule="evenodd"
|
||||
d="M5.496 12.886a1 1 0 0 1 1.008 0l3 1.75A1 1 0 0 1 10 15.5V19a1 1 0 0 1-.496.864l-3 1.75a1 1 0 0 1-1.008 0l-3-1.75A1 1 0 0 1 2 19v-3.5a1 1 0 0 1 .496-.864l3-1.75ZM4 16.074v2.352l2 1.166 2-1.166v-2.352l-2-1.166-2 1.166Z"
|
||||
clip-rule="evenodd" />
|
||||
<path fill="currentColor" fill-rule="evenodd"
|
||||
d="M3 6.5v5.527l1.488-.868a3 3 0 0 1 3.024 0l3 1.75A3 3 0 0 1 12 15.5V21h6a3 3 0 0 0 3-3v-7a3 3 0 0 0-3-3h-1V5.412A2.412 2.412 0 0 0 14.588 3H6.5A3.5 3.5 0 0 0 3 6.5ZM6.5 8a1.5 1.5 0 1 1 0-3h8.088c.228 0 .412.184.412.412V8H6.5Zm9 8a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"
|
||||
d="M6.5 3A3.5 3.5 0 0 0 3 6.5V17a4 4 0 0 0 4 4h11a3 3 0 0 0 3-3v-7a3 3 0 0 0-3-3h-1V5.412A2.412 2.412 0 0 0 14.588 3H6.5ZM15 8V5.412A.412.412 0 0 0 14.588 5H6.5a1.5 1.5 0 1 0 0 3H15Zm.5 7.75a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 751 B After Width: | Height: | Size: 418 B |
@@ -441,7 +441,7 @@ const mergedTokens = createTokens({
|
||||
28: 112,
|
||||
32: 128,
|
||||
'-px': -1,
|
||||
'-0.5': 2,
|
||||
'-0.5': -2,
|
||||
'-1': -4,
|
||||
'-1.5': -6,
|
||||
'-2': -8,
|
||||
|
||||
@@ -85,7 +85,6 @@ function useAppNavigation<
|
||||
headerSearchBarOptions: {
|
||||
// always show search bar on iOS
|
||||
hideNavigationBar: false,
|
||||
// @ts-expect-error
|
||||
hideWhenScrolling: false,
|
||||
cancelButtonText: searchCancelText,
|
||||
textColor: searchTextColor,
|
||||
|
||||
@@ -34,7 +34,6 @@ const DemoRootHomeOptions = () => {
|
||||
),
|
||||
headerSearchBarOptions: {
|
||||
hideNavigationBar: true,
|
||||
// @ts-expect-error
|
||||
hideWhenScrolling: true,
|
||||
placeholder: intl.formatMessage({
|
||||
id: 'content__search_dapps_or_type_url',
|
||||
|
||||
@@ -82,7 +82,6 @@ function useDemoAppNavigation<
|
||||
newHeaderSearchBarOptions = {
|
||||
headerSearchBarOptions: {
|
||||
hideNavigationBar: false,
|
||||
// @ts-expect-error
|
||||
hideWhenScrolling: false,
|
||||
cancelButtonText: searchCancelText,
|
||||
textColor: searchTextColor,
|
||||
|
||||
@@ -1,34 +1,10 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import {
|
||||
ListView,
|
||||
RefreshControl,
|
||||
ScrollView,
|
||||
Stack,
|
||||
Text,
|
||||
} from '@onekeyhq/components';
|
||||
import { PageHeaderView, PageManager } from '@onekeyhq/components/src/TabView';
|
||||
import { ListView, Stack, Text } from '@onekeyhq/components';
|
||||
import { Tab } from '@onekeyhq/components/src/TabView';
|
||||
|
||||
import { Layout } from './utils/Layout';
|
||||
|
||||
const HeaderPropsList = {
|
||||
data: [
|
||||
{ title: '标签1' },
|
||||
{ title: '标签2' },
|
||||
{ title: '标签标签3' },
|
||||
{ title: '标签4' },
|
||||
],
|
||||
style: {
|
||||
height: 50,
|
||||
backgroundColor: 'white',
|
||||
borderBottomColor: '#F5F5F5',
|
||||
borderBottomWidth: 1,
|
||||
},
|
||||
onSelectedPageIndex: (index: number) => {
|
||||
console.log('选中', index);
|
||||
},
|
||||
};
|
||||
|
||||
const FirstRoute = ({
|
||||
onContentSizeChange,
|
||||
}: {
|
||||
@@ -66,85 +42,26 @@ const SecondRoute = ({
|
||||
);
|
||||
|
||||
const TabViewScrollStickyDemo = () => {
|
||||
const onRefresh = () => {};
|
||||
const [contentHeight, setContentHeight] = useState<number | undefined>(1);
|
||||
const data = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: '标签1',
|
||||
backgroundColor: 'skyblue',
|
||||
contentHeight: undefined,
|
||||
page: FirstRoute,
|
||||
},
|
||||
{
|
||||
title: '标签2',
|
||||
backgroundColor: 'coral',
|
||||
contentHeight: undefined,
|
||||
page: SecondRoute,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
const pageManager = useMemo(
|
||||
() =>
|
||||
new PageManager({
|
||||
data,
|
||||
initialScrollIndex: 1,
|
||||
onSelectedPageIndex: (index: number) => {
|
||||
setContentHeight(data[index]?.contentHeight);
|
||||
},
|
||||
}),
|
||||
[data],
|
||||
);
|
||||
const Header = pageManager.renderHeaderView;
|
||||
const Content = pageManager.renderContentView;
|
||||
return (
|
||||
<ScrollView
|
||||
style={{ height: 600, backgroundColor: 'black' }}
|
||||
nestedScrollEnabled
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={false} onRefresh={onRefresh} />
|
||||
}
|
||||
stickyHeaderIndices={[1]}
|
||||
>
|
||||
<Stack style={{ height: 100 }} />
|
||||
<Header
|
||||
// HTPageHeaderView.defaultProps in TabView
|
||||
style={HeaderPropsList.style}
|
||||
itemContainerStyle={{ flex: 1 }}
|
||||
/>
|
||||
<Stack style={{ height: contentHeight }}>
|
||||
<Content
|
||||
renderItem={({
|
||||
item,
|
||||
index,
|
||||
}: {
|
||||
item: {
|
||||
backgroundColor: string;
|
||||
contentHeight: number | undefined;
|
||||
page: any;
|
||||
};
|
||||
index: number;
|
||||
}) => (
|
||||
<Stack
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: item.backgroundColor,
|
||||
}}
|
||||
>
|
||||
<item.page
|
||||
onContentSizeChange={(width: number, height: number) => {
|
||||
item.contentHeight = height;
|
||||
if (index === pageManager.pageIndex) {
|
||||
setContentHeight(height);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
</ScrollView>
|
||||
<Tab
|
||||
data={data}
|
||||
initialScrollIndex={1}
|
||||
// style={{ width: 400, height: 600, backgroundColor: 'black' }}
|
||||
h={600}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -156,20 +73,42 @@ const TabViewGallery = () => (
|
||||
elements={[
|
||||
{
|
||||
title: 'Header 单独使用',
|
||||
element: <PageHeaderView {...HeaderPropsList} />,
|
||||
element: (
|
||||
<Tab.Header
|
||||
data={[
|
||||
{ title: '标签1' },
|
||||
{ title: '标签2' },
|
||||
{ title: '标签标签3' },
|
||||
{ title: '标签4' },
|
||||
]}
|
||||
onSelectedPageIndex={(index: number) => {
|
||||
console.log('选中', index);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Header 自定义1',
|
||||
element: (
|
||||
<PageHeaderView
|
||||
{...HeaderPropsList}
|
||||
<Tab.Header
|
||||
data={[
|
||||
{ title: '标签1' },
|
||||
{ title: '标签2' },
|
||||
{ title: '标签标签3' },
|
||||
{ title: '标签4' },
|
||||
]}
|
||||
itemContainerStyle={{ flex: 1 }}
|
||||
itemTitleNormalStyle={{ color: '$text', fontSize: 13 }}
|
||||
itemTitleSelectedStyle={{ color: '$textInverse', fontSize: 15 }}
|
||||
cursorStyle={{
|
||||
width: 88,
|
||||
height: 48,
|
||||
borderRadius: 48 / 2.0,
|
||||
top: 0.5,
|
||||
backgroundColor: 'black',
|
||||
height: 44,
|
||||
borderRadius: 44 / 2.0,
|
||||
top: 5,
|
||||
bg: '$bgInfoStrong',
|
||||
}}
|
||||
onSelectedPageIndex={(index: number) => {
|
||||
console.log('选中', index);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { memo, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
|
||||
import { useIntl } from 'react-intl';
|
||||
import { RefreshControl, useWindowDimensions } from 'react-native';
|
||||
|
||||
import type { IScrollViewRef } from '@onekeyhq/components';
|
||||
import { ListView, Page, ScrollView, Stack, Text } from '@onekeyhq/components';
|
||||
import { getTokens } from '@onekeyhq/components/src/hooks';
|
||||
import { useThemeValue } from '@onekeyhq/components/src/Provider/hooks/useThemeValue';
|
||||
import { PageManager } from '@onekeyhq/components/src/TabView';
|
||||
import platformEnv from '@onekeyhq/shared/src/platformEnv';
|
||||
import { Tab } from '@onekeyhq/components/src/TabView';
|
||||
|
||||
import HeaderView from './HeaderView';
|
||||
|
||||
@@ -81,12 +78,6 @@ const ListRoute = ({
|
||||
/>
|
||||
);
|
||||
|
||||
// const renderScene = SceneMap({
|
||||
// [EHomePageTabsEnum.Demo1]: FirstRoute,
|
||||
// [EHomePageTabsEnum.Demo2]: SecondRoute,
|
||||
// [EHomePageTabsEnum.Demo3]: OtherRoute,
|
||||
// });
|
||||
|
||||
function HomePage() {
|
||||
const screenWidth = useWindowDimensions().width;
|
||||
const sideBarWidth = getTokens().size.sideBarWidth.val;
|
||||
@@ -96,163 +87,41 @@ function HomePage() {
|
||||
// tabsViewRef?.current?.setRefreshing(true);
|
||||
}, []);
|
||||
|
||||
const [contentHeight, setContentHeight] = useState<number | undefined>(1);
|
||||
const scrollView = useRef<IScrollViewRef | null>(null);
|
||||
const container = useRef<any | null>(null);
|
||||
const config = useMemo(
|
||||
() => ({
|
||||
lastIndex: -1,
|
||||
scrollViewHeight: 0,
|
||||
headerLayoutY: 0,
|
||||
headerViewHeight: 0,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const [bgAppColor, textColor, textSubduedColor] = useThemeValue(
|
||||
['bgApp', 'text', 'textSubdued'],
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
const data = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: 'Label',
|
||||
backgroundColor: 'skyblue',
|
||||
contentHeight: undefined,
|
||||
contentOffsetY: 0,
|
||||
page: memo(FirstRoute, () => true),
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
id: 'action__default_chain',
|
||||
}),
|
||||
backgroundColor: 'coral',
|
||||
contentHeight: undefined,
|
||||
contentOffsetY: 0,
|
||||
page: memo(SecondRoute, () => true),
|
||||
},
|
||||
{
|
||||
title: 'Label',
|
||||
backgroundColor: 'turquoise',
|
||||
contentHeight: undefined,
|
||||
contentOffsetY: 0,
|
||||
page: memo(ListRoute, () => true),
|
||||
},
|
||||
{
|
||||
title: 'Label',
|
||||
backgroundColor: 'pink',
|
||||
contentHeight: undefined,
|
||||
contentOffsetY: 0,
|
||||
page: memo(OtherRoute, () => true),
|
||||
},
|
||||
],
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[intl, bgAppColor, textColor, textSubduedColor],
|
||||
[intl],
|
||||
);
|
||||
|
||||
const reloadContentHeight = useCallback(
|
||||
(index: number) => {
|
||||
if (config.scrollViewHeight * config.headerViewHeight <= 0) {
|
||||
return;
|
||||
}
|
||||
const minHeight = config.scrollViewHeight - config.headerViewHeight;
|
||||
const height = Math.max(data[index].contentHeight ?? 0, minHeight);
|
||||
if (height === contentHeight) {
|
||||
return;
|
||||
}
|
||||
if (platformEnv.isNative) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||
container?.current?.setNativeProps?.({
|
||||
height,
|
||||
});
|
||||
} else {
|
||||
setContentHeight(height);
|
||||
}
|
||||
},
|
||||
[data, config, container, contentHeight],
|
||||
);
|
||||
|
||||
const pageManager = useMemo(
|
||||
() =>
|
||||
new PageManager({
|
||||
data,
|
||||
initialScrollIndex: 3,
|
||||
onSelectedPageIndex: (index: number) => {
|
||||
reloadContentHeight(index);
|
||||
const { contentOffsetY } = data[index];
|
||||
const lastContentOffsetY =
|
||||
data?.[config.lastIndex]?.contentOffsetY ?? 0;
|
||||
|
||||
if (
|
||||
Math.round(lastContentOffsetY) < Math.round(config.headerLayoutY)
|
||||
) {
|
||||
data[index].contentOffsetY = lastContentOffsetY;
|
||||
} else if (
|
||||
Math.round(contentOffsetY) <= Math.round(config.headerLayoutY)
|
||||
) {
|
||||
data[index].contentOffsetY = config.headerLayoutY;
|
||||
}
|
||||
|
||||
// Need to wait for contentHeight to be updated
|
||||
setTimeout(() => {
|
||||
if (platformEnv.isNative) {
|
||||
scrollView?.current?.setNativeProps({
|
||||
contentOffset: { y: data[index].contentOffsetY },
|
||||
});
|
||||
} else {
|
||||
scrollView?.current?.scrollTo({
|
||||
y: data[index].contentOffsetY,
|
||||
animated: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
config.lastIndex = index;
|
||||
},
|
||||
}),
|
||||
[data, config, scrollView, reloadContentHeight],
|
||||
);
|
||||
const Header = pageManager.renderHeaderView;
|
||||
const Content = pageManager.renderContentView;
|
||||
|
||||
const renderHeaderView = useCallback(() => <HeaderView />, []);
|
||||
|
||||
const renderContentItem = useCallback(
|
||||
({
|
||||
item,
|
||||
index,
|
||||
}: {
|
||||
item: {
|
||||
backgroundColor: string;
|
||||
contentHeight: number | undefined;
|
||||
page: any;
|
||||
};
|
||||
index: number;
|
||||
}) => (
|
||||
<Stack
|
||||
style={{
|
||||
flex: 1,
|
||||
// backgroundColor: item.backgroundColor,
|
||||
}}
|
||||
>
|
||||
<item.page
|
||||
onContentSizeChange={(_: number, height: number) => {
|
||||
item.contentHeight = height;
|
||||
if (index === pageManager.pageIndex) {
|
||||
reloadContentHeight(index);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
),
|
||||
[pageManager, reloadContentHeight],
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
<Page>
|
||||
<Page.Body alignItems="center">
|
||||
<ScrollView
|
||||
<Tab
|
||||
// @ts-expect-error
|
||||
data={data}
|
||||
ListHeaderComponent={<>{renderHeaderView()}</>}
|
||||
initialScrollIndex={3}
|
||||
$md={{
|
||||
width: '100%',
|
||||
}}
|
||||
@@ -262,76 +131,12 @@ function HomePage() {
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={false} onRefresh={onRefresh} />
|
||||
}
|
||||
stickyHeaderIndices={[1]}
|
||||
ref={scrollView}
|
||||
showsVerticalScrollIndicator={false}
|
||||
scrollEventThrottle={16}
|
||||
onLayout={(event) => {
|
||||
config.scrollViewHeight = event.nativeEvent.layout.height;
|
||||
}}
|
||||
onScroll={(event) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
data[pageManager.pageIndex].contentOffsetY = (
|
||||
event as any
|
||||
).nativeEvent.contentOffset.y;
|
||||
}}
|
||||
>
|
||||
{renderHeaderView()}
|
||||
<Header
|
||||
// HTPageHeaderView.defaultProps in TabView
|
||||
style={{
|
||||
height: 54,
|
||||
backgroundColor: bgAppColor,
|
||||
}}
|
||||
onLayout={(event: any) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
config.headerViewHeight = event.nativeEvent.layout.height;
|
||||
}}
|
||||
itemTitleStyle={{ fontSize: 16 }}
|
||||
itemTitleNormalStyle={{ color: textSubduedColor }}
|
||||
itemTitleSelectedStyle={{ color: textColor, fontSize: 16 }}
|
||||
cursorStyle={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
height: 2,
|
||||
backgroundColor: textColor,
|
||||
}}
|
||||
/>
|
||||
<Stack
|
||||
ref={container}
|
||||
onLayout={(event) => {
|
||||
config.headerLayoutY =
|
||||
event.nativeEvent.layout.y - config.headerViewHeight;
|
||||
}}
|
||||
style={{ height: contentHeight }}
|
||||
>
|
||||
<Content
|
||||
windowSize={5}
|
||||
scrollEnabled={platformEnv.isNative}
|
||||
shouldSelectedPageAnimation={platformEnv.isNative}
|
||||
renderItem={renderContentItem}
|
||||
/>
|
||||
</Stack>
|
||||
</ScrollView>
|
||||
/>
|
||||
</Page.Body>
|
||||
</Page>
|
||||
),
|
||||
[
|
||||
screenWidth,
|
||||
sideBarWidth,
|
||||
bgAppColor,
|
||||
textColor,
|
||||
textSubduedColor,
|
||||
contentHeight,
|
||||
Header,
|
||||
Content,
|
||||
onRefresh,
|
||||
renderHeaderView,
|
||||
renderContentItem,
|
||||
data,
|
||||
config,
|
||||
pageManager,
|
||||
],
|
||||
[screenWidth, sideBarWidth, onRefresh, renderHeaderView, data],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
11
yarn.lock
@@ -5181,6 +5181,7 @@ __metadata:
|
||||
react-native-svg: ^13.9.0
|
||||
react-native-tab-page-view: "https://github.com/OneKeyHQ/react-native-tab-page-view#90ea54bfb01021c4e54e27cb9fc72a3702df0aa3"
|
||||
react-native-tcp-socket: ^6.0.6
|
||||
react-native-view-shot: 3.1.2
|
||||
react-native-webview: "https://github.com/OneKeyHQ/react-native-webview.git#5036d59cba4f5609c32b7eb5dec560e738ce7115"
|
||||
react-native-zip-archive: ^6.0.9
|
||||
readable-stream: ^3.6.0
|
||||
@@ -24230,6 +24231,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-native-view-shot@npm:3.1.2":
|
||||
version: 3.1.2
|
||||
resolution: "react-native-view-shot@npm:3.1.2"
|
||||
peerDependencies:
|
||||
react: "*"
|
||||
react-native: "*"
|
||||
checksum: 8f5e148830bd27887736310805c4f385c6a2df0f53827fb727fbe7f5cdb35dae951b90139cee752a14133bc8b32795434d4f9dbf33f9d9b56f007b99caa661f0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-native-web-internals@npm:1.71.0":
|
||||
version: 1.71.0
|
||||
resolution: "react-native-web-internals@npm:1.71.0"
|
||||
|
||||