@ledgerhq/lumen-ui-rnative 0.1.38 → 0.1.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/module/lib/Components/AmountDisplay/AmountDisplay.js +28 -11
  2. package/dist/module/lib/Components/AmountDisplay/AmountDisplay.js.map +1 -1
  3. package/dist/module/lib/Components/AmountDisplay/AmountDisplay.test.js +71 -0
  4. package/dist/module/lib/Components/AmountDisplay/AmountDisplay.test.js.map +1 -1
  5. package/dist/module/lib/Components/AmountInput/AmountInput.js +9 -4
  6. package/dist/module/lib/Components/AmountInput/AmountInput.js.map +1 -1
  7. package/dist/module/lib/Components/AmountInput/AmountInput.test.js +152 -0
  8. package/dist/module/lib/Components/AmountInput/AmountInput.test.js.map +1 -0
  9. package/dist/module/lib/Components/Avatar/Avatar.figma.js +5 -0
  10. package/dist/module/lib/Components/Avatar/Avatar.figma.js.map +1 -1
  11. package/dist/module/lib/Components/Avatar/Avatar.js +9 -2
  12. package/dist/module/lib/Components/Avatar/Avatar.js.map +1 -1
  13. package/dist/module/lib/Components/Avatar/Avatar.mdx +9 -0
  14. package/dist/module/lib/Components/Avatar/Avatar.stories.js +47 -0
  15. package/dist/module/lib/Components/Avatar/Avatar.stories.js.map +1 -1
  16. package/dist/module/lib/Components/Avatar/Avatar.test.js +23 -1
  17. package/dist/module/lib/Components/Avatar/Avatar.test.js.map +1 -1
  18. package/dist/module/lib/Components/BottomSheet/BottomSheet.js +3 -1
  19. package/dist/module/lib/Components/BottomSheet/BottomSheet.js.map +1 -1
  20. package/dist/module/lib/Components/BottomSheet/BottomSheet.stories.js +1 -0
  21. package/dist/module/lib/Components/BottomSheet/BottomSheet.stories.js.map +1 -1
  22. package/dist/module/lib/Components/BottomSheet/BottomSheet.test.js +33 -1
  23. package/dist/module/lib/Components/BottomSheet/BottomSheet.test.js.map +1 -1
  24. package/dist/module/lib/Components/BottomSheet/BottomSheetHeader.js +7 -3
  25. package/dist/module/lib/Components/BottomSheet/BottomSheetHeader.js.map +1 -1
  26. package/dist/module/lib/Components/Switch/Switch.js +144 -8
  27. package/dist/module/lib/Components/Switch/Switch.js.map +1 -1
  28. package/dist/typescript/src/lib/Components/AmountDisplay/AmountDisplay.d.ts.map +1 -1
  29. package/dist/typescript/src/lib/Components/AmountInput/AmountInput.d.ts.map +1 -1
  30. package/dist/typescript/src/lib/Components/Avatar/Avatar.d.ts +1 -1
  31. package/dist/typescript/src/lib/Components/Avatar/Avatar.d.ts.map +1 -1
  32. package/dist/typescript/src/lib/Components/Avatar/types.d.ts +6 -0
  33. package/dist/typescript/src/lib/Components/Avatar/types.d.ts.map +1 -1
  34. package/dist/typescript/src/lib/Components/BottomSheet/BottomSheet.d.ts +2 -2
  35. package/dist/typescript/src/lib/Components/BottomSheet/BottomSheet.d.ts.map +1 -1
  36. package/dist/typescript/src/lib/Components/BottomSheet/BottomSheetHeader.d.ts.map +1 -1
  37. package/dist/typescript/src/lib/Components/BottomSheet/types.d.ts +9 -0
  38. package/dist/typescript/src/lib/Components/BottomSheet/types.d.ts.map +1 -1
  39. package/dist/typescript/src/lib/Components/Switch/Switch.d.ts +1 -1
  40. package/dist/typescript/src/lib/Components/Switch/Switch.d.ts.map +1 -1
  41. package/dist/typescript/src/lib/Components/Switch/types.d.ts +2 -1
  42. package/dist/typescript/src/lib/Components/Switch/types.d.ts.map +1 -1
  43. package/package.json +1 -1
  44. package/src/lib/Components/AmountDisplay/AmountDisplay.test.tsx +92 -0
  45. package/src/lib/Components/AmountDisplay/AmountDisplay.tsx +37 -15
  46. package/src/lib/Components/AmountInput/AmountInput.test.tsx +166 -0
  47. package/src/lib/Components/AmountInput/AmountInput.tsx +6 -1
  48. package/src/lib/Components/Avatar/Avatar.figma.tsx +5 -0
  49. package/src/lib/Components/Avatar/Avatar.mdx +9 -0
  50. package/src/lib/Components/Avatar/Avatar.stories.tsx +41 -0
  51. package/src/lib/Components/Avatar/Avatar.test.tsx +31 -1
  52. package/src/lib/Components/Avatar/Avatar.tsx +17 -4
  53. package/src/lib/Components/Avatar/types.ts +6 -0
  54. package/src/lib/Components/BottomSheet/BottomSheet.stories.tsx +1 -0
  55. package/src/lib/Components/BottomSheet/BottomSheet.test.tsx +32 -1
  56. package/src/lib/Components/BottomSheet/BottomSheet.tsx +10 -4
  57. package/src/lib/Components/BottomSheet/BottomSheetHeader.tsx +10 -6
  58. package/src/lib/Components/BottomSheet/types.ts +9 -0
  59. package/src/lib/Components/Switch/Switch.tsx +132 -11
  60. package/src/lib/Components/Switch/types.ts +3 -1
  61. package/dist/module/lib/Components/Switch/BaseSwitch.js +0 -221
  62. package/dist/module/lib/Components/Switch/BaseSwitch.js.map +0 -1
  63. package/dist/typescript/src/lib/Components/Switch/BaseSwitch.d.ts +0 -13
  64. package/dist/typescript/src/lib/Components/Switch/BaseSwitch.d.ts.map +0 -1
  65. package/src/lib/Components/Switch/BaseSwitch.tsx +0 -249
@@ -0,0 +1,166 @@
1
+ import { describe, expect, it, jest } from '@jest/globals';
2
+ import { ledgerLiveThemes } from '@ledgerhq/lumen-design-core';
3
+ import { fireEvent, render, screen } from '@testing-library/react-native';
4
+ import { ThemeProvider } from '../ThemeProvider/ThemeProvider';
5
+ import { AmountInput } from './AmountInput';
6
+
7
+ const hidden = { includeHiddenElements: true } as const;
8
+
9
+ const renderWithProvider = (component: React.ReactElement) =>
10
+ render(
11
+ <ThemeProvider themes={ledgerLiveThemes} colorScheme='dark' locale='en'>
12
+ {component}
13
+ </ThemeProvider>,
14
+ );
15
+
16
+ describe('AmountInput', () => {
17
+ describe('Rendering', () => {
18
+ it('renders with an empty value', () => {
19
+ renderWithProvider(
20
+ <AmountInput value='' onChangeText={jest.fn()} testID='input' />,
21
+ );
22
+ expect(screen.getByTestId('input')).toBeTruthy();
23
+ });
24
+
25
+ it('renders the currency text', () => {
26
+ renderWithProvider(
27
+ <AmountInput value='' onChangeText={jest.fn()} currencyText='USD' />,
28
+ );
29
+ expect(screen.getByText('USD', hidden)).toBeTruthy();
30
+ });
31
+
32
+ it('displays 0 when value is empty', () => {
33
+ renderWithProvider(<AmountInput value='' onChangeText={jest.fn()} />);
34
+ expect(screen.getByText('0', hidden)).toBeTruthy();
35
+ });
36
+ });
37
+
38
+ describe('Interactions', () => {
39
+ it('calls onChangeText with formatted text on change', () => {
40
+ const onChangeText = jest.fn();
41
+ renderWithProvider(
42
+ <AmountInput
43
+ value=''
44
+ onChangeText={onChangeText}
45
+ testID='input'
46
+ thousandsSeparator={false}
47
+ />,
48
+ );
49
+ fireEvent.changeText(screen.getByTestId('input'), '123.45');
50
+ expect(onChangeText).toHaveBeenCalledWith('123.45');
51
+ });
52
+
53
+ it('formats with thousands separator on user input', () => {
54
+ const onChangeText = jest.fn();
55
+ renderWithProvider(
56
+ <AmountInput value='' onChangeText={onChangeText} testID='input' />,
57
+ );
58
+ fireEvent.changeText(screen.getByTestId('input'), '1000');
59
+ expect(onChangeText).toHaveBeenCalledWith('1 000');
60
+ });
61
+
62
+ it('strips decimal input when allowDecimals is false', () => {
63
+ const onChangeText = jest.fn();
64
+ renderWithProvider(
65
+ <AmountInput
66
+ value=''
67
+ onChangeText={onChangeText}
68
+ allowDecimals={false}
69
+ thousandsSeparator={false}
70
+ testID='input'
71
+ />,
72
+ );
73
+ fireEvent.changeText(screen.getByTestId('input'), '12.34');
74
+ expect(onChangeText).toHaveBeenCalledWith('1234');
75
+ });
76
+
77
+ it('truncates decimal part with maxDecimalLength on user input', () => {
78
+ const onChangeText = jest.fn();
79
+ renderWithProvider(
80
+ <AmountInput
81
+ value=''
82
+ onChangeText={onChangeText}
83
+ maxDecimalLength={2}
84
+ thousandsSeparator={false}
85
+ testID='input'
86
+ />,
87
+ );
88
+ fireEvent.changeText(screen.getByTestId('input'), '1.2345');
89
+ expect(onChangeText).toHaveBeenCalledWith('1.23');
90
+ });
91
+ });
92
+
93
+ describe('programmatic value formatting', () => {
94
+ it('formats a raw unformatted string on initial render', () => {
95
+ renderWithProvider(
96
+ <AmountInput value='2433.123456789' onChangeText={jest.fn()} />,
97
+ );
98
+ expect(screen.getByText('2 433.123456789', hidden)).toBeTruthy();
99
+ });
100
+
101
+ it('formats a number value on initial render', () => {
102
+ renderWithProvider(
103
+ <AmountInput value={2433.123456789} onChangeText={jest.fn()} />,
104
+ );
105
+ expect(screen.getByText('2 433.123456789', hidden)).toBeTruthy();
106
+ });
107
+
108
+ it('adds thousands grouping for a large number value', () => {
109
+ renderWithProvider(
110
+ <AmountInput value={1000000} onChangeText={jest.fn()} />,
111
+ );
112
+ expect(screen.getByText('1 000 000', hidden)).toBeTruthy();
113
+ });
114
+
115
+ it('applies maxDecimalLength to a programmatically set value', () => {
116
+ renderWithProvider(
117
+ <AmountInput
118
+ value='2433.123456789'
119
+ maxDecimalLength={2}
120
+ onChangeText={jest.fn()}
121
+ />,
122
+ );
123
+ expect(screen.getByText('2 433.12', hidden)).toBeTruthy();
124
+ });
125
+
126
+ it('applies maxIntegerLength to a programmatically set value', () => {
127
+ renderWithProvider(
128
+ <AmountInput
129
+ value='1234567890'
130
+ maxIntegerLength={3}
131
+ allowDecimals={false}
132
+ thousandsSeparator={false}
133
+ onChangeText={jest.fn()}
134
+ />,
135
+ );
136
+ expect(screen.getByText('123', hidden)).toBeTruthy();
137
+ });
138
+
139
+ it('omits thousands separator when thousandsSeparator is false', () => {
140
+ renderWithProvider(
141
+ <AmountInput
142
+ value='2433.12'
143
+ thousandsSeparator={false}
144
+ onChangeText={jest.fn()}
145
+ />,
146
+ );
147
+ expect(screen.getByText('2433.12', hidden)).toBeTruthy();
148
+ });
149
+
150
+ it('formats consistently with what typing the same digits produces', () => {
151
+ const onChangeText = jest.fn();
152
+ renderWithProvider(
153
+ <AmountInput
154
+ value='2433.123456789'
155
+ onChangeText={onChangeText}
156
+ testID='input'
157
+ />,
158
+ );
159
+
160
+ expect(screen.getByText('2 433.123456789', hidden)).toBeTruthy();
161
+
162
+ fireEvent.changeText(screen.getByTestId('input'), '2433.123456789');
163
+ expect(onChangeText).toHaveBeenCalledWith('2 433.123456789');
164
+ });
165
+ });
166
+ });
@@ -46,7 +46,12 @@ export const AmountInput = ({
46
46
  ...props
47
47
  }: AmountInputProps) => {
48
48
  const inputRef = useRef<TextInput>(null);
49
- const inputValue = String(value);
49
+ const inputValue = textFormatter(String(value), {
50
+ allowDecimals,
51
+ thousandsSeparator,
52
+ maxIntegerLength,
53
+ maxDecimalLength,
54
+ });
50
55
  const [isFocused, setIsFocused] = useState(false);
51
56
  const disabled = useDisabledContext({
52
57
  consumerName: 'AmountInput',
@@ -7,6 +7,10 @@ figma.connect(
7
7
  {
8
8
  imports: ["import { Avatar } from '@ledgerhq/lumen-ui-rnative'"],
9
9
  props: {
10
+ appearance: figma.enum('appearance', {
11
+ gray: 'gray',
12
+ transparent: 'transparent',
13
+ }),
10
14
  size: figma.enum('size', {
11
15
  sm: 'sm',
12
16
  md: 'md',
@@ -21,6 +25,7 @@ figma.connect(
21
25
  example: (props) => (
22
26
  <Avatar
23
27
  src='https://example-image.com'
28
+ appearance={props.appearance}
24
29
  size={props.size}
25
30
  alt="John Doe's Avatar"
26
31
  showNotification={props.showNotification}
@@ -40,6 +40,15 @@ Avatars come in four different sizes:
40
40
 
41
41
  <Canvas of={AvatarStories.SizeShowcase} />
42
42
 
43
+ ### Appearance
44
+
45
+ The `appearance` prop controls the background color of the avatar container.
46
+
47
+ - **`gray`**: Uses a muted gray background
48
+ - **`transparent`**: Uses a semi-transparent muted background — default
49
+
50
+ <Canvas of={AvatarStories.AppearanceShowcase} />
51
+
43
52
  ### States
44
53
 
45
54
  The Avatar component handles two primary states automatically:
@@ -90,6 +90,47 @@ export const SizeShowcase: Story = {
90
90
  ),
91
91
  };
92
92
 
93
+ const AppearanceShowcaseRender = () => {
94
+ return (
95
+ <Box lx={{ gap: 's8' }}>
96
+ <Box lx={{ flexDirection: 'row', gap: 's16', padding: 's8' }}>
97
+ <Avatar
98
+ alt='gray fallback'
99
+ size='md'
100
+ appearance='gray'
101
+ showNotification={false}
102
+ />
103
+ <Avatar
104
+ alt='transparent fallback'
105
+ size='md'
106
+ appearance='transparent'
107
+ showNotification={false}
108
+ />
109
+ </Box>
110
+ <Box lx={{ flexDirection: 'row', gap: 's16', padding: 's8' }}>
111
+ <Avatar
112
+ src={exampleSrc}
113
+ alt='gray with image'
114
+ size='md'
115
+ appearance='gray'
116
+ showNotification={false}
117
+ />
118
+ <Avatar
119
+ src={exampleSrc}
120
+ alt='transparent with image'
121
+ size='md'
122
+ appearance='transparent'
123
+ showNotification={false}
124
+ />
125
+ </Box>
126
+ </Box>
127
+ );
128
+ };
129
+
130
+ export const AppearanceShowcase: Story = {
131
+ render: () => <AppearanceShowcaseRender />,
132
+ };
133
+
93
134
  export const FallbackShowcase: Story = {
94
135
  args: {
95
136
  src: 'https://brokenLink.random',
@@ -4,7 +4,7 @@ import { render, waitFor } from '@testing-library/react-native';
4
4
  import { ThemeProvider } from '../ThemeProvider/ThemeProvider';
5
5
  import { Avatar } from './Avatar';
6
6
 
7
- const { sizes } = ledgerLiveThemes.dark;
7
+ const { sizes, colors } = ledgerLiveThemes.dark;
8
8
 
9
9
  const TestWrapper = ({ children }: { children: React.ReactNode }) => (
10
10
  <ThemeProvider themes={ledgerLiveThemes} colorScheme='dark' locale='en'>
@@ -33,6 +33,36 @@ describe('Avatar Component', () => {
33
33
  getByLabelText('user profile');
34
34
  });
35
35
 
36
+ it('should render with transparent appearance by default', () => {
37
+ const { getByTestId } = render(
38
+ <TestWrapper>
39
+ <Avatar testID='avatar-id' />
40
+ </TestWrapper>,
41
+ );
42
+
43
+ expect(getByTestId('avatar-id').props.style.backgroundColor).toBe(
44
+ colors.bg.mutedTransparent,
45
+ );
46
+ });
47
+
48
+ it.each<['transparent' | 'gray', string]>([
49
+ ['transparent', colors.bg.mutedTransparent],
50
+ ['gray', colors.bg.muted],
51
+ ])(
52
+ 'should render with %s appearance when specified',
53
+ (appearance, expectedColor) => {
54
+ const { getByTestId } = render(
55
+ <TestWrapper>
56
+ <Avatar testID='avatar-id' appearance={appearance} />
57
+ </TestWrapper>,
58
+ );
59
+
60
+ expect(getByTestId('avatar-id').props.style.backgroundColor).toBe(
61
+ expectedColor,
62
+ );
63
+ },
64
+ );
65
+
36
66
  it('should render with different sizes', () => {
37
67
  const { getByTestId, rerender } = render(
38
68
  <TestWrapper>
@@ -7,6 +7,7 @@ import { DotIndicator } from '../DotIndicator';
7
7
  import { Box } from '../Utility';
8
8
  import type { AvatarProps } from './types';
9
9
 
10
+ type Appearance = NonNullable<AvatarProps['appearance']>;
10
11
  type Size = NonNullable<AvatarProps['size']>;
11
12
 
12
13
  const fallbackSizes = {
@@ -23,9 +24,20 @@ const dotSizeMap: Partial<
23
24
  md: 'xl',
24
25
  };
25
26
 
26
- const useStyles = ({ size }: { size: Size }) => {
27
+ const useStyles = ({
28
+ appearance,
29
+ size,
30
+ }: {
31
+ appearance: Appearance;
32
+ size: Size;
33
+ }) => {
27
34
  return useStyleSheet(
28
35
  (t) => {
36
+ const backgroundColors: Record<Appearance, string> = {
37
+ gray: t.colors.bg.muted,
38
+ transparent: t.colors.bg.mutedTransparent,
39
+ };
40
+
29
41
  const sizeMap = {
30
42
  sm: { size: t.sizes.s40, padding: t.spacings.s4 },
31
43
  md: { size: t.sizes.s48, padding: t.spacings.s4 },
@@ -39,7 +51,7 @@ const useStyles = ({ size }: { size: Size }) => {
39
51
  width: sizeMap[size].size,
40
52
  height: sizeMap[size].size,
41
53
  borderRadius: 9999,
42
- backgroundColor: t.colors.bg.muted,
54
+ backgroundColor: backgroundColors[appearance],
43
55
  alignItems: 'center',
44
56
  justifyContent: 'center',
45
57
  padding: sizeMap[size].padding,
@@ -52,7 +64,7 @@ const useStyles = ({ size }: { size: Size }) => {
52
64
  },
53
65
  };
54
66
  },
55
- [size],
67
+ [appearance, size],
56
68
  );
57
69
  };
58
70
 
@@ -77,6 +89,7 @@ export const Avatar = ({
77
89
  style,
78
90
  src,
79
91
  alt = 'avatar',
92
+ appearance = 'transparent',
80
93
  size = 'md',
81
94
  showNotification: showNotificationProp = false,
82
95
  testID,
@@ -86,7 +99,7 @@ export const Avatar = ({
86
99
  const { t } = useCommonTranslation();
87
100
  const [error, setError] = useState<boolean>(false);
88
101
  const shouldFallback = !src || error;
89
- const styles = useStyles({ size });
102
+ const styles = useStyles({ appearance, size });
90
103
 
91
104
  const resolvedAlt = alt || t('components.avatar.defaultAlt');
92
105
 
@@ -11,6 +11,12 @@ export type AvatarProps = {
11
11
  * @optional
12
12
  */
13
13
  alt?: string;
14
+ /**
15
+ * The visual appearance of the avatar background: `gray` or `transparent`.
16
+ * @optional
17
+ * @default transparent
18
+ */
19
+ appearance?: 'gray' | 'transparent';
14
20
  /**
15
21
  * The size variant of the avatar.
16
22
  * @optional
@@ -105,6 +105,7 @@ export const Base: Story = {
105
105
  hideCloseButton: false,
106
106
  onBack: undefined,
107
107
  onClose: undefined,
108
+ onHeaderClosePressed: undefined,
108
109
  enableHandlePanningGesture: true,
109
110
  enablePanDownToClose: true,
110
111
  enableBlurKeyboardOnGesture: true,
@@ -1,10 +1,11 @@
1
1
  import { describe, it, expect, jest } from '@jest/globals';
2
2
  import { ledgerLiveThemes } from '@ledgerhq/lumen-design-core';
3
3
  import type { RenderOptions } from '@testing-library/react-native';
4
- import { render } from '@testing-library/react-native';
4
+ import { fireEvent, render } from '@testing-library/react-native';
5
5
  import { Text, View } from 'react-native';
6
6
  import { ThemeProvider } from '../ThemeProvider/ThemeProvider';
7
7
  import { BottomSheet as BottomSheetComponent } from './BottomSheet';
8
+ import { BottomSheetHeader } from './BottomSheetHeader';
8
9
  // Mock react-native-gesture-handler which is used by @gorhom/bottom-sheet
9
10
  jest.mock('react-native-gesture-handler', () => ({}));
10
11
 
@@ -339,6 +340,36 @@ describe('BottomSheet', () => {
339
340
  expect(onDismiss).not.toHaveBeenCalled();
340
341
  });
341
342
 
343
+ it('calls onHeaderClosePressed when the header close button is pressed', () => {
344
+ const { BottomSheet } = require('./BottomSheet');
345
+ const onHeaderClosePressed = jest.fn();
346
+ const { getByTestId } = renderWithTheme(
347
+ <BottomSheet
348
+ onHeaderClosePressed={onHeaderClosePressed}
349
+ testID='bottom-sheet'
350
+ >
351
+ <BottomSheetHeader title='Title' />
352
+ </BottomSheet>,
353
+ );
354
+
355
+ fireEvent.press(getByTestId('bottom-sheet-header-close-button'));
356
+
357
+ expect(onHeaderClosePressed).toHaveBeenCalledTimes(1);
358
+ });
359
+
360
+ it('does not require onHeaderClosePressed to close from the header', () => {
361
+ const { BottomSheet } = require('./BottomSheet');
362
+ const { getByTestId } = renderWithTheme(
363
+ <BottomSheet testID='bottom-sheet'>
364
+ <BottomSheetHeader title='Title' />
365
+ </BottomSheet>,
366
+ );
367
+
368
+ expect(() =>
369
+ fireEvent.press(getByTestId('bottom-sheet-header-close-button')),
370
+ ).not.toThrow();
371
+ });
372
+
342
373
  it('accepts multiple callbacks simultaneously', () => {
343
374
  const { BottomSheet } = require('./BottomSheet');
344
375
  const onOpen = jest.fn();
@@ -57,14 +57,18 @@ const useStyles = ({
57
57
  };
58
58
 
59
59
  const [BottomSheetProvider, useBottomSheetContext] =
60
- createSafeContext<Pick<BottomSheetProps, 'onBack' | 'hideCloseButton'>>(
61
- 'BottomSheet',
62
- );
60
+ createSafeContext<
61
+ Pick<
62
+ BottomSheetProps,
63
+ 'onBack' | 'hideCloseButton' | 'onHeaderClosePressed'
64
+ >
65
+ >('BottomSheet');
63
66
 
64
67
  export const BottomSheet = ({
65
68
  onOpen,
66
69
  onClose,
67
70
  onDismiss,
71
+ onHeaderClosePressed,
68
72
  onBack,
69
73
  onAnimate,
70
74
  children,
@@ -203,7 +207,9 @@ export const BottomSheet = ({
203
207
  handleComponent={hideHandle ? HiddenHandle : CustomHandle}
204
208
  backdropComponent={hideBackdrop ? undefined : renderBackdrop}
205
209
  >
206
- <BottomSheetProvider value={{ onBack, hideCloseButton }}>
210
+ <BottomSheetProvider
211
+ value={{ onBack, hideCloseButton, onHeaderClosePressed }}
212
+ >
207
213
  {children}
208
214
  </BottomSheetProvider>
209
215
  </GorhomBottomSheetModal>
@@ -97,14 +97,18 @@ export const BottomSheetHeader = ({
97
97
  }: BottomSheetHeaderProps) => {
98
98
  const { t } = useCommonTranslation();
99
99
  const { close } = useBottomSheet();
100
- const { onBack, hideCloseButton } = useBottomSheetContext({
101
- consumerName: 'BottomSheetHeader',
102
- contextRequired: true,
103
- });
100
+ const { onBack, hideCloseButton, onHeaderClosePressed } =
101
+ useBottomSheetContext({
102
+ consumerName: 'BottomSheetHeader',
103
+ contextRequired: true,
104
+ });
104
105
 
105
106
  const handleClose = useCallback(() => {
107
+ if (onHeaderClosePressed) {
108
+ onHeaderClosePressed();
109
+ }
106
110
  close();
107
- }, [close]);
111
+ }, [close, onHeaderClosePressed]);
108
112
 
109
113
  const hasTitleSection = Boolean(title || description);
110
114
  const hasIcons = Boolean(onBack || !hideCloseButton);
@@ -115,7 +119,7 @@ export const BottomSheetHeader = ({
115
119
  hidden: !hasIcons && density !== 'compact',
116
120
  });
117
121
 
118
- if (!title && !description && !onBack && hideCloseButton) {
122
+ if (!hasTitleSection && !onBack && hideCloseButton) {
119
123
  return null;
120
124
  }
121
125
 
@@ -94,6 +94,15 @@ export type BottomSheetProps = PropsWithChildren & {
94
94
  * @default undefined
95
95
  */
96
96
  onClose?: () => void;
97
+ /**
98
+ * Callback function to handle when the close button in the header is pressed.
99
+ * This is distinct from {@link onClose} and {@link onDismiss}—those will always
100
+ * also be called after this event if the close button results in a full sheet dismissal.
101
+ * Use this to react specifically to header close intent (e.g., tracking, custom UI),
102
+ * but do cleanup/unmount logic in {@link onClose} or {@link onDismiss}.
103
+ * @default undefined
104
+ */
105
+ onHeaderClosePressed?: () => void;
97
106
  /**
98
107
  * Callback function to handle the open event.
99
108
  * @default undefined