Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions packages/design-system-react-native/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@ module.exports = merge(baseConfig, {
lines: 100,
statements: 100,
},
// useAnimatedScrollHandler wraps onScroll in a Reanimated worklet; Jest uses the
// reanimated mock, which does not execute that worklet body. Scroll logic is covered
// via updateScrollYFromEvent unit tests, but the hook line that forwards scrollEvent
// into updateScrollYFromEvent stays uncovered here—so statements/lines/functions sit
// below 100% while branches remain fully exercised.
'./src/components/HeaderStandardAnimated/useHeaderStandardAnimated.ts': {
branches: 100,
functions: 75,
lines: 87,
statements: 87,
},
},

// Add coverage ignore patterns
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,10 @@
import React, { useMemo } from 'react';

// External dependencies.
import { BoxAlignItems } from '../Box';
import { BoxColumn } from '../BoxColumn';
import type { ButtonIconProps } from '../ButtonIcon';
import { HeaderBase } from '../HeaderBase';
import { IconName } from '../Icon';
import { TextOrChildren } from '../temp-components/TextOrChildren';
import type { TextProps } from '../Text';
import { FontWeight, TextColor, TextVariant } from '../Text';
import { HeaderStandardCenterColumn } from '../temp-components/HeaderStandardCenterColumn';

// Internal dependencies.
import type { HeaderStandardProps } from './HeaderStandard.types';
Expand Down Expand Up @@ -68,38 +64,13 @@ export const HeaderStandard: React.FC<HeaderStandardProps> = ({
return children;
}
if (title) {
let subtitleTextProps: Omit<Partial<TextProps>, 'children'> | undefined;
if (subtitle && typeof subtitle === 'string') {
const { twClassName: subtitleTwClassName, ...subtitleTextRest } =
subtitleProps ?? {};
subtitleTextProps = {
variant: TextVariant.BodySm,
color: TextColor.TextAlternative,
...subtitleTextRest,
twClassName: ['-mt-0.5', subtitleTwClassName]
.filter(Boolean)
.join(' '),
};
}

return (
<BoxColumn
alignItems={BoxAlignItems.Center}
textProps={{
variant: TextVariant.BodyMd,
fontWeight: FontWeight.Bold,
...titleProps,
}}
bottomAccessory={
subtitle ? (
<TextOrChildren textProps={subtitleTextProps}>
{subtitle}
</TextOrChildren>
) : undefined
}
>
{title}
</BoxColumn>
<HeaderStandardCenterColumn
title={title}
titleProps={titleProps}
subtitle={subtitle}
subtitleProps={subtitleProps}
/>
);
}
return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,58 +1,31 @@
// Third party dependencies.
import React from 'react';

// External dependencies.
import type { ButtonIconProps } from '../ButtonIcon';
import type { HeaderBaseProps } from '../HeaderBase';
import type { TextProps } from '../Text';
import type { HeaderStandardCenterColumnFields } from '../temp-components/HeaderStandardCenterColumn';

/**
* HeaderStandard component props.
*/
export type HeaderStandardProps = HeaderBaseProps & {
/**
* Title to display in the header. Can be a string or a React node.
* Used as children if children prop is not provided.
* When string: rendered with TextVariant.BodyMd and FontWeight.Bold by default; titleProps apply.
* When node: rendered as-is; titleProps are not applied.
*/
title?: string | React.ReactNode;
/**
* Additional props to pass to the title Text component.
* Props are spread to the Text component and can override default values.
* Only applied when title is a string.
*/
titleProps?: Partial<TextProps>;
/**
* Subtitle to display below the title. Can be a string or a React node.
* When string: rendered with TextVariant.BodySm and TextColor.TextAlternative by default; subtitleProps apply.
* When node: rendered as-is; subtitleProps are not applied (add spacing on your root if needed, e.g. twClassName).
*/
subtitle?: string | React.ReactNode;
/**
* Additional props to pass to the subtitle Text component.
* Props are spread to the Text component and can override default values.
* Only applied when subtitle is a string.
*/
subtitleProps?: Partial<TextProps>;
/**
* Callback when the back button is pressed.
* If provided, a back button will be rendered as startButtonIconProps.
*/
onBack?: () => void;
/**
* Additional props to pass to the back ButtonIcon.
* If provided, a back button will be rendered as startButtonIconProps with these props spread.
*/
backButtonProps?: Omit<ButtonIconProps, 'iconName'>;
/**
* Callback when the close button is pressed.
* If provided, a close button will be added to endButtonIconProps.
*/
onClose?: () => void;
/**
* Additional props to pass to the close ButtonIcon.
* If provided, a close button will be added to endButtonIconProps with these props spread.
*/
closeButtonProps?: Omit<ButtonIconProps, 'iconName'>;
};
export type HeaderStandardProps = HeaderBaseProps &
HeaderStandardCenterColumnFields & {
/**
* Callback when the back button is pressed.
* If provided, a back button will be rendered as startButtonIconProps.
*/
onBack?: () => void;
/**
* Additional props to pass to the back ButtonIcon.
* If provided, a back button will be rendered as startButtonIconProps with these props spread.
*/
backButtonProps?: Omit<ButtonIconProps, 'iconName'>;
/**
* Callback when the close button is pressed.
* If provided, a close button will be added to endButtonIconProps.
*/
onClose?: () => void;
/**
* Additional props to pass to the close ButtonIcon.
* If provided, a close button will be added to endButtonIconProps with these props spread.
*/
closeButtonProps?: Omit<ButtonIconProps, 'iconName'>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import type { Meta, StoryObj } from '@storybook/react-native';
import type { ComponentType } from 'react';
import React from 'react';
import Animated from 'react-native-reanimated';

import { Box } from '../Box';
import { IconName } from '../Icon';
import { Text, TextColor, TextVariant } from '../Text';
import { TitleStandard } from '../TitleStandard';

import { HeaderStandardAnimated } from './HeaderStandardAnimated';
import type { HeaderStandardAnimatedProps } from './HeaderStandardAnimated.types';
import { useHeaderStandardAnimated } from './useHeaderStandardAnimated';

type ScrollStoryArgs = Omit<
HeaderStandardAnimatedProps,
'scrollY' | 'titleSectionHeight' | 'children'
>;

const meta: Meta<ScrollStoryArgs> = {
title: 'Components/HeaderStandardAnimated',
component:
HeaderStandardAnimated as unknown as ComponentType<ScrollStoryArgs>,
parameters: {
docs: {
description: {
component:
'Scroll-linked header: the center title animates with scroll position. Use `useHeaderStandardAnimated` for `scrollY`, `titleSectionHeight`, and `onScroll`, and attach `onScroll` to `Animated.ScrollView`. Use `HeaderStandard` when you do not need this behavior.',
},
},
},
argTypes: {
Comment thread
brianacnguyen marked this conversation as resolved.
title: { control: 'text' },
subtitle: { control: 'text' },
},
decorators: [
(Story) => (
<Box twClassName="w-full flex-1 bg-background-default">
<Story />
</Box>
),
],
};

export default meta;

type Story = StoryObj<ScrollStoryArgs>;

const SampleContent = ({ itemCount = 20 }: { itemCount?: number }) => (
<>
{Array.from({ length: itemCount }).map((_, index) => (
<Box key={index} twClassName="p-4 mb-2 bg-muted rounded-lg mx-4">
<Text variant={TextVariant.BodyMd}>Item {index + 1}</Text>
<Text variant={TextVariant.BodySm}>
This is sample content to demonstrate scrolling behavior.
</Text>
</Box>
))}
</>
);

function ScrollDemo(args: ScrollStoryArgs) {
const { scrollY, onScroll, setTitleSectionHeight, titleSectionHeightSv } =
useHeaderStandardAnimated();

return (
<Box twClassName="flex-1 bg-default">
<HeaderStandardAnimated
{...args}
scrollY={scrollY}
titleSectionHeight={titleSectionHeightSv}
/>
<Animated.ScrollView
onScroll={onScroll}
scrollEventThrottle={16}
showsVerticalScrollIndicator={false}
>
<Box
onLayout={(e) => setTitleSectionHeight(e.nativeEvent.layout.height)}
>
<TitleStandard
topAccessory={
<Text
variant={TextVariant.BodySm}
color={TextColor.TextAlternative}
>
Perps
</Text>
}
title="ETH-PERP"
twClassName="px-4 pt-1 pb-3"
/>
</Box>
<SampleContent />
</Animated.ScrollView>
</Box>
);
}

export const Default: Story = {
args: {
title: 'Market',
},
render: (args) => <ScrollDemo {...args} />,
};

export const Subtitle: Story = {
args: {
title: 'Market',
subtitle: 'Perpetual futures',
onBack: () => undefined,
},
render: (args) => <ScrollDemo {...args} />,
};

export const OnBack: Story = {
args: {
title: 'Settings',
onBack: () => undefined,
},
render: (args) => <ScrollDemo {...args} />,
};

export const OnClose: Story = {
args: {
title: 'Modal Title',
onClose: () => undefined,
},
render: (args) => <ScrollDemo {...args} />,
};

export const BackAndClose: Story = {
args: {
title: 'Settings',
onBack: () => undefined,
onClose: () => undefined,
},
render: (args) => <ScrollDemo {...args} />,
};

export const EndButtonIconProps: Story = {
args: {
title: 'Search',
onBack: () => undefined,
onClose: () => undefined,
endButtonIconProps: [
{
iconName: IconName.Search,
onPress: () => undefined,
},
],
},
render: (args) => <ScrollDemo {...args} />,
};
Comment thread
brianacnguyen marked this conversation as resolved.
Loading
Loading