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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### 🎉 New features

- Added a JS-only `lazy` prop — set to `false` to mount sheet content before presentation, allowing `auto` detents to measure settled content before the sheet opens. ([#792](https://github.com/lodev09/react-native-true-sheet/pull/792) by [@maxlapides](https://github.com/maxlapides))
- **iOS**: Smoother `onPositionChange`, and `onWillDismiss` now fires only when a drag-to-dismiss is committed. ([#744](https://github.com/lodev09/react-native-true-sheet/pull/744), [#756](https://github.com/lodev09/react-native-true-sheet/pull/756) by [@lodev09](https://github.com/lodev09))
- The `auto` detent now works with plugged scrollables — the sheet sizes to the scrollable's content height. ([#743](https://github.com/lodev09/react-native-true-sheet/pull/743) by [@lodev09](https://github.com/lodev09))
- New `headerOptions` prop with a `position` option — set to `'absolute'` to float the header over the content and exclude it from the `auto` detent height. ([#747](https://github.com/lodev09/react-native-true-sheet/pull/747) by [@lodev09](https://github.com/lodev09))
Expand Down
12 changes: 12 additions & 0 deletions docs/docs/reference/01-configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,18 @@ Used with `initialDetentIndex`.
| - | - | - | - | - |
| `boolean` | `true` | ✅ | ✅ | ✅ |

## `lazy`

Specify whether the sheet content should mount lazily, on first presentation. Set to `false` to mount the content before presentation without presenting the sheet. Use this when content is not ready on the first render, then call [`present()`](methods#present) after your readiness signal so [`auto`](types#sheetdetent) detents measure the settled content.

| Type | Default | 🍎 | 🤖 | 🌐 |
| - | - | - | - | - |
| `boolean` | `true` | ✅ | ✅ | |

:::note
Content that requires window attachment to measure, such as SwiftUI-hosted views, still only measures during presentation.
:::

## `grabber`

Shows a native grabber (or drag handle) on the sheet.
Expand Down
3 changes: 2 additions & 1 deletion jest.setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ jest.mock('./src/fabric/TrueSheetViewNativeComponent', () => {
return {
__esModule: true,
default: React.forwardRef((props, ref) => {
return React.createElement(View, { ...props, ref });
React.useImperativeHandle(ref, () => ({ _nativeTag: 1 }));
return React.createElement(View, props);
}),
};
});
Expand Down
19 changes: 14 additions & 5 deletions src/TrueSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,10 @@ export class TrueSheet

this.validateDetents();

// Lazy load by default, except when initialDetentIndex is set (for auto-presentation)
// Lazy load by default, except when auto-presenting or when lazy is disabled
const shouldRenderImmediately =
props.initialDetentIndex !== undefined && props.initialDetentIndex >= 0;
(props.initialDetentIndex !== undefined && props.initialDetentIndex >= 0) ||
props.lazy === false;

this.state = {
shouldRenderNativeView: shouldRenderImmediately,
Expand Down Expand Up @@ -350,9 +351,8 @@ export class TrueSheet
this.backHandlerSubscription?.remove();
this.backHandlerSubscription = null;

// Clean up native view after dismiss for lazy loading.
// Skip unmount if a present is in progress to avoid race condition.
if (!this.isPresenting) {
// Non-lazy content stays mounted; otherwise clean it up unless another presentation is active.
if (!this.isPresenting && this.props.lazy !== false) {
this.setState({ shouldRenderNativeView: false });
}

Expand Down Expand Up @@ -484,6 +484,14 @@ export class TrueSheet
this.registerInstance();
this.updateScrollableHandle();

if (
prevProps.lazy !== false &&
this.props.lazy === false &&
!this.state.shouldRenderNativeView
) {
this.setState({ shouldRenderNativeView: true });
}

// Validate when detents prop changes
if (prevProps.detents !== this.props.detents) {
this.validateDetents();
Expand Down Expand Up @@ -543,6 +551,7 @@ export class TrueSheet
insetAdjustment = 'automatic',
...rest
} = this.props;
delete rest.lazy;

// Trim to max 3 detents and clamp fractions
const resolvedDetents: number[] = detents.slice(0, 3).map((detent) => {
Expand Down
15 changes: 15 additions & 0 deletions src/TrueSheet.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,21 @@ export interface TrueSheetProps extends ViewProps {
*/
initialDetentAnimated?: boolean;

/**
* Specify whether the sheet content should mount lazily, on first presentation.
* Set to `false` to mount the content before presentation without presenting the sheet.
* Use this when content is not ready on the first render, then call `present()`
* after your readiness signal so auto detents measure the settled content.
*
* Content that requires window attachment to measure, such as SwiftUI-hosted views,
* still only measures during presentation.
*
* @platform android
* @platform ios
* @default true
*/
lazy?: boolean;

/**
* The detent index that the sheet should start to dim the background.
* This is ignored if `dimmed` is set to `false`.
Expand Down
65 changes: 65 additions & 0 deletions src/__tests__/TrueSheet.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
/* eslint-disable dot-notation -- bracket access reaches TrueSheet's private test hooks with full typing */
import { createRef } from 'react';
import { Text } from 'react-native';
import { render, act } from '@testing-library/react-native';
import { TrueSheet, TrueSheetPeek } from '../index';
import TrueSheetModule from '../specs/NativeTrueSheetModule';
import type {
DidDismissEvent,
WillFocusEvent,
Expand Down Expand Up @@ -117,6 +119,69 @@ describe('TrueSheet', () => {
expect(getByText('Eager Content')).toBeDefined();
});

it('should render native view content without presentation when lazy is disabled', () => {
const { getByText, getByTestId } = render(
<TrueSheet name="non-lazy-test" lazy={false} testID="non-lazy-host">
<Text>Non-Lazy Content</Text>
</TrueSheet>
);

expect(getByText('Non-Lazy Content')).toBeDefined();
expect(getByTestId('non-lazy-host').props.lazy).toBeUndefined();
});

it('should render native view content when lazy becomes disabled', () => {
const sheet = (
<TrueSheet name="deferred-non-lazy-test">
<Text>Deferred Non-Lazy Content</Text>
</TrueSheet>
);
const { queryByText, rerender } = render(sheet);

expect(queryByText('Deferred Non-Lazy Content')).toBeNull();

rerender(<TrueSheet {...sheet.props} lazy={false} />);

expect(queryByText('Deferred Non-Lazy Content')).not.toBeNull();

rerender(sheet);

expect(queryByText('Deferred Non-Lazy Content')).not.toBeNull();
});

it('should keep non-lazy native view content mounted after dismiss', () => {
const { getByTestId, queryByText } = render(
<TrueSheet name="non-lazy-dismiss-test" lazy={false} testID="non-lazy-dismiss-host">
<Text>Persistent Non-Lazy Content</Text>
</TrueSheet>
);

act(() => {
getByTestId('non-lazy-dismiss-host').props.onDidDismiss({
nativeEvent: null,
});
});

expect(queryByText('Persistent Non-Lazy Content')).not.toBeNull();
});

it('should present non-lazy content without waiting for another mount', async () => {
const sheetRef = createRef<TrueSheet>();
const onMountMock = jest.fn();
render(
<TrueSheet ref={sheetRef} name="non-lazy-present-test" lazy={false} onMount={onMountMock}>
<Text>Ready Non-Lazy Content</Text>
</TrueSheet>
);

await act(async () => {
await sheetRef.current?.present();
});

expect(onMountMock).not.toHaveBeenCalled();
expect(TrueSheetModule?.presentByRef).toHaveBeenCalledTimes(1);
});

it('should render native view content when present is called', async () => {
const onMountMock = jest.fn();
const { queryByText } = render(
Expand Down
Loading