Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Sheet): new component #840

Merged
merged 29 commits into from
Aug 24, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
e5d5039
WEB-1492 feat(BottomSheet): new component
atabel Aug 3, 2023
858f52b
Merge branch 'master' into atoledano-sheet
atabel Aug 3, 2023
7b36788
forwardRef and refactor ActionsBottomSheet
atabel Aug 4, 2023
d9995c3
desktop support
atabel Aug 4, 2023
99f4ca2
fix title divider logic
atabel Aug 4, 2023
81ff7b8
improve stories
atabel Aug 8, 2023
8b7f3d2
BottomSheetRoot
atabel Aug 10, 2023
046301b
tests
atabel Aug 11, 2023
03d6546
missing tests
atabel Aug 11, 2023
ca2b10e
Merge branch 'master' into atoledano-sheet
atabel Aug 11, 2023
45496e9
Merge branch 'master' into atoledano-sheet
atabel Aug 11, 2023
ee13795
Merge branch 'master' into atoledano-sheet
atabel Aug 21, 2023
b58d9f9
rename bottom sheet to just sheet
atabel Aug 21, 2023
b627dbd
fixes from CR
atabel Aug 21, 2023
b67663f
rename tests
atabel Aug 21, 2023
4cbd652
hover effect for close X
atabel Aug 21, 2023
bf064cb
fixes from cr
atabel Aug 21, 2023
853312f
snippets
atabel Aug 21, 2023
52fc6a3
add subtitle and description to info snippet
atabel Aug 21, 2023
fb9a4a8
fix bridge type
atabel Aug 22, 2023
515bd46
fix text
atabel Aug 22, 2023
00e2495
fix scrollbars in desktop]
atabel Aug 22, 2023
509ac2c
move specific sheet native methods from bridge to mistica
atabel Aug 22, 2023
1aad7a0
keep top padding in SheetBody when no title
atabel Aug 22, 2023
60cf078
link to doc in sheet story
atabel Aug 22, 2023
1c842d9
snippets
atabel Aug 23, 2023
87f8616
remove divider in desktop
atabel Aug 23, 2023
be5e558
Merge branch 'master' into atoledano-sheet
atabel Aug 23, 2023
355ae57
update snapshots
atabel Aug 24, 2023
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
135 changes: 135 additions & 0 deletions doc/bottomSheet.md
atabel marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# BottomSheet

Mística provides a bottom sheet component that can be used to display a modal-like content from over the main
content of the screen.

## Basic usage

You can show any content you want inside the bottom sheet by passing it as a child of the component.

```jsx
import {BottomSheet} from 'mistica';

const MyComponent = () => {
const [showSheet, setShowSheet] = useState(false);
return (
<>
<ButtonPrimary onPress={() => setShowSheet(true)}>show bottom sheet</ButtonPrimary>
{showSheet && (
<BottomSheet onClose={() => setShowSheet(false)}>
<Placeholder />
</BottomSheet>
)}
</>
);
};
```

The sheet will close when the user does the swipe down gesture or when the background overlay is touched. The
`onClose` callback is called when the closing animation finishes, that's the right place to unmount the sheet
as shown in the example above.

You can also close the sheet programmatically using the render prop:

```jsx
import {BottomSheet} from 'mistica';

const MyComponent = () => {
const [showSheet, setShowSheet] = useState(false);
return (
<>
<ButtonPrimary onPress={() => setShowSheet(true)}>show bottom sheet</ButtonPrimary>
{showSheet && (
<BottomSheet onClose={() => setShowSheet(false)}>
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note that onClose is called when the closing animation finishes, so the idea is not unmounting the component until the transition finishes, that's why I set showSheet to false in onClose handler instead of ButtonPrimary onPress handler

{({closeModal, modalTitleId}) => (
<>
<Title1 id={modalTitleId}>My sheet</Title1>
<Placeholder />
<ButtonPrimary onPress={closeModal}>Close</ButtonPrimary>
</>
)}
</BottomSheet>
)}
</>
);
};
```

## Sheet with predefined content

Mística predefines some common bottom sheet patterns for you to use: `RadioListBottomSheet`,
`ActionsListBottomSheet`, `InfoBottomSheet` and `ActionsBottomSheet`. You can see examples in the storybook.

## `showBottomSheet` imperative api
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this equivalent to the confirm(), alert() methods we have for dialogs


Instead of using React components, there is an alternative way to show a bottom sheet: using the
`showBottomSheet` function. For this to work, you need to render a `<BottomSheetRoot/>` somewhere in your app,
typically where you render the mistica `<ThemeContextProvider/>`, but it could be anywhere.

```jsx
import {BottomSheetRoot} from '@telefonica/mistica';

export const App = () => {
return (
<>
<BottomSheetRoot />
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've decided to not include this automatically inside ThemeContextProvider to allow webs using mistica to optin to use this. The advantage is that we don't bloat webs with all sheet components JS code if they are not going to use it. Also, it isn't needed to use BottomSheetRoot if you don't wan't to use the imperative api

<RestOfYourApp />
</>
);
};
```

Then you can call `showBottomSheet` from anywhere:

```jsx
import {BottomSheet} from 'mistica';
atabel marked this conversation as resolved.
Show resolved Hide resolved

const MyComponent = () => {
return (
<ButtonPrimary
onPress={() =>
showBottomSheet({
type: 'RADIO_LIST',
props: {
title: 'Select an fruit',
items: [
{id: '1', title: 'Apple'},
{id: '2', title: 'Banana'},
{id: '3', title: 'Orange'},
],
},
}).then((result) => {
// The promise is resolved when the sheet is closed
console.log(result);
})
}
>
show bottom sheet
</ButtonPrimary>
);
};
```

### native implementation
atabel marked this conversation as resolved.
Show resolved Hide resolved

If you are using mistica inside Novum app, you can configure `showBottomSheet` to use the native sheet
implementation with the webview bridge.

```jsx
import {BottomSheetRoot} from '@telefonica/mistica';
import * as webviewBridge from '@tef-novum/webview-bridge';

const nativeImplementation = createNativeSheetImplementationFromWebviewBridge(webviewBridge);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with this approach, mistica doesn't need to depend on webview bridge lib, because the native implementation is injected, not imported. I think we could follow a similar approach for alert() confirm() or other components using the bridge. That would allow us to remove the "@tef-novum/webview-bridge" dependency from mistica package.json

Copy link
Member

@pladaria pladaria Aug 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally we should be able to inject the bridge function "as is" to the Root component.

Something like:

import {openBottomSheet} from '@tef-novum/webview-bridge`;

<BottomSheetRoot nativeImplementation={openBottomSheet} />

and the nativeImplementation prop would be typed as typeof openBottomSheet;

This way everything is tied, types are verified at compile time and we won't need a createNativeSheetImplementationFromWebviewBridge.

Perhaps the bridge dependency could be listed as optionalDependency. I think it would be a problem to fully decouple both implementations.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm... I see your point, but that would mean I'd need to re-implement the specific sheet methods in mistica (bottomSheetSingleSelector, bottomSheetActionSelector, bottomSheetInfo, bottomSheetActions)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, I've moved these specific sheet methods from the bridge lib to mistica and removed them from webview-bridge (Telefonica/webview-bridge#115)


export const App = () => {
return (
<>
<BottomSheetRoot nativeImplementation={nativeImplementation} />
<RestOfYourApp />
</>
);
};
```

Then when you call `showBottomSheet`, if the code is running inside a webview, it will use the native
implementation instead of the web one.
2 changes: 2 additions & 0 deletions playroom/frame-component.tsx
atabel marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
skinVars,
VIVO_NEW_SKIN,
TELEFONICA_SKIN,
BottomSheetRoot,
} from '../src';

import type {ThemeConfig} from '../src';
Expand Down Expand Up @@ -61,6 +62,7 @@ const FrameComponent = ({children, theme}: Props): React.ReactNode => (
<ThemeOverriderContextProvider>
{(overridenTheme) => (
<ThemeContextProvider theme={overridenTheme ?? theme}>
<BottomSheetRoot />
<OverscrollColorProvider>
<App skinName={(overridenTheme ?? theme).skin.name}>{children}</App>
</OverscrollColorProvider>
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
83 changes: 83 additions & 0 deletions src/__screenshot_tests__/bottom-sheet-screenshot-test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import {openStoryPage, screen} from '../test-utils';

const TESTABLE_DEVICES = ['MOBILE_IOS', 'DESKTOP'] as const;

test.each(TESTABLE_DEVICES)('BottomSheet in %s', async (device) => {
const page = await openStoryPage({
id: 'components-modals-bottomsheet--default',
device,
});

const button = await screen.findByRole('button', {name: 'Open'});
await button.click();

await screen.findByRole('dialog');

const image = await page.screenshot();

expect(image).toMatchImageSnapshot();
});

test.each(TESTABLE_DEVICES)('ActionsListBottomSheet in %s', async (device) => {
const page = await openStoryPage({
id: 'components-modals-bottomsheet--actions-list',
device,
});

const button = await screen.findByRole('button', {name: 'Open'});
await button.click();

await screen.findByRole('dialog');

const image = await page.screenshot();

expect(image).toMatchImageSnapshot();
});

test.each(TESTABLE_DEVICES)('RadioListBottomSheet in %s', async (device) => {
const page = await openStoryPage({
id: 'components-modals-bottomsheet--radio-list',
device,
});

const button = await screen.findByRole('button', {name: 'Open'});
await button.click();

await screen.findByRole('dialog');

const image = await page.screenshot();

expect(image).toMatchImageSnapshot();
});

test.each(TESTABLE_DEVICES)('InfoBottomSheet in %s', async (device) => {
const page = await openStoryPage({
id: 'components-modals-bottomsheet--info',
device,
});

const button = await screen.findByRole('button', {name: 'Open'});
await button.click();

await screen.findByRole('dialog');

const image = await page.screenshot();

expect(image).toMatchImageSnapshot();
});

test.each(TESTABLE_DEVICES)('ActionsBottomSheet in %s', async (device) => {
const page = await openStoryPage({
id: 'components-modals-bottomsheet--actions',
device,
});

const button = await screen.findByRole('button', {name: 'Open'});
await button.click();

await screen.findByRole('dialog');

const image = await page.screenshot();

expect(image).toMatchImageSnapshot();
});
Loading
Loading