@linagora/twake-mui 10.4.0 → 10.5.0
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.
- package/CHANGELOG.md +15 -0
- package/dist/components/BottomSheet/BottomSheet.stories.d.ts +23 -0
- package/dist/components/BottomSheet/BottomSheet.stories.js +139 -0
- package/dist/components/BottomSheet/BottomSheetHeader.d.ts +9 -0
- package/dist/components/BottomSheet/BottomSheetHeader.js +14 -0
- package/dist/components/BottomSheet/BottomSheetItem.d.ts +10 -0
- package/dist/components/BottomSheet/BottomSheetItem.js +18 -0
- package/dist/components/BottomSheet/BottomSheetTitle.d.ts +10 -0
- package/dist/components/BottomSheet/BottomSheetTitle.js +20 -0
- package/dist/components/BottomSheet/core/BottomSheetCore.d.ts +28 -0
- package/dist/components/BottomSheet/core/BottomSheetCore.js +118 -0
- package/dist/components/BottomSheet/core/helpers.d.ts +8 -0
- package/dist/components/BottomSheet/core/helpers.js +29 -0
- package/dist/components/BottomSheet/helpers.d.ts +14 -0
- package/dist/components/BottomSheet/helpers.js +95 -0
- package/dist/components/BottomSheet/index.d.ts +8 -0
- package/dist/components/BottomSheet/index.js +158 -0
- package/dist/components/BottomSheet/safeArea.d.ts +3 -0
- package/dist/components/BottomSheet/safeArea.js +10 -0
- package/dist/components/BottomSheet/styles.d.ts +25 -0
- package/dist/components/BottomSheet/styles.js +100 -0
- package/dist/components/BottomSheet/types.d.ts +77 -0
- package/dist/components/BottomSheet/types.js +1 -0
- package/dist/components/ContactPopover/AttendeeInfo.js +3 -3
- package/dist/components/ContactPopover/ContactPopoverCalendarAction.js +2 -2
- package/dist/components/ContactPopover/ContactPopoverChatAction.js +2 -2
- package/dist/components/ContactPopover/ContactPopoverEmailAction.js +2 -2
- package/dist/components/ContactPopover/ContactPopoverVideoAction.js +2 -2
- package/dist/components/SearchBar/DisableHighlight.js +1 -1
- package/dist/components/SearchBar/FocusHighlight.js +1 -1
- package/dist/components/SearchBar/SearchBarIconWrapper.js +1 -1
- package/dist/components/SearchBar/SearchBarRoot.js +4 -4
- package/dist/components/SpaceOverlay/OverlayFrame.js +17 -21
- package/dist/components/SpaceOverlay/SpaceOverlay.js +2 -2
- package/dist/index.d.ts +16 -1
- package/dist/index.js +8 -1
- package/dist/lib/overrides.js +21 -19
- package/package.json +4 -1
- package/dist/components/SpaceOverlay/index.d.ts +0 -8
- package/dist/components/SpaceOverlay/index.js +0 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
# @linagora/twake-mui [10.5.0](https://github.com/linagora/twake-ui/compare/@linagora/twake-mui@10.4.1...@linagora/twake-mui@10.5.0) (2026-10-07)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **twake-mui:** Migrate BottomSheet from cozy-ui ([5144650](https://github.com/linagora/twake-ui/commit/51446500bc88f45859bfc5e8278a88347d6f91f5))
|
|
7
|
+
* **twake-mui:** Migrate BottomSheet subcomponents from cozy-ui ([006d755](https://github.com/linagora/twake-ui/commit/006d7559d13d8740b28f1e6c456372956884e7fb))
|
|
8
|
+
|
|
9
|
+
## @linagora/twake-mui [10.4.1](https://github.com/linagora/twake-ui/compare/@linagora/twake-mui@10.4.0...@linagora/twake-mui@10.4.1) (2026-10-07)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
* **twake-mui:** Follow the color scheme in overrides and SearchBar ([#142](https://github.com/linagora/twake-ui/issues/142)) ([1c2efac](https://github.com/linagora/twake-ui/commit/1c2efacba0a3e140384775e9a4f8e76416409b7a))
|
|
15
|
+
|
|
1
16
|
# @linagora/twake-mui [10.4.0](https://github.com/linagora/twake-ui/compare/@linagora/twake-mui@10.3.0...@linagora/twake-mui@10.4.0) (2026-10-07)
|
|
2
17
|
|
|
3
18
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
interface ExampleArgs {
|
|
3
|
+
open: boolean;
|
|
4
|
+
backdrop: boolean;
|
|
5
|
+
closable: boolean;
|
|
6
|
+
skipAnimation: boolean;
|
|
7
|
+
disablePortal: boolean;
|
|
8
|
+
withFakeToolbar: boolean;
|
|
9
|
+
withHeader: boolean;
|
|
10
|
+
withTitle: boolean;
|
|
11
|
+
withListContent: boolean;
|
|
12
|
+
longContent: boolean;
|
|
13
|
+
isOpenMin: boolean;
|
|
14
|
+
hasMinHeightOffset: boolean;
|
|
15
|
+
mediumHeight?: number;
|
|
16
|
+
mediumHeightRatio?: number;
|
|
17
|
+
offset?: number;
|
|
18
|
+
}
|
|
19
|
+
declare const meta: Meta<ExampleArgs>;
|
|
20
|
+
export default meta;
|
|
21
|
+
type Story = StoryObj<typeof meta>;
|
|
22
|
+
export declare const Default: Story;
|
|
23
|
+
export declare const Screenshot: Story;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { File, FileTypeSheet, FileTypeSlide, FileTypeText, FileTypeVideo, Icon, Right } from '@linagora/twake-icons';
|
|
2
|
+
import { Button, Checkbox, Divider, List, ListItemIcon, Radio } from '@mui/material';
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import BottomSheetHeader from './BottomSheetHeader';
|
|
5
|
+
import BottomSheetItem from './BottomSheetItem';
|
|
6
|
+
import BottomSheetTitle from './BottomSheetTitle';
|
|
7
|
+
import BottomSheet from './index';
|
|
8
|
+
import ListItemButton from '../ListItemButton';
|
|
9
|
+
import ListItemText from '../ListItemText';
|
|
10
|
+
const shortContent = 'Ada Lovelace wrote the first algorithm intended to be carried out by a machine, the Analytical Engine imagined by Charles Babbage.';
|
|
11
|
+
const longContent = Array.from({ length: 12 }, (_, index) => `${index + 1}. ${shortContent} She saw that such a machine could handle symbols as well as numbers, and could one day compose music.`).join(' ');
|
|
12
|
+
const ListContent = ({ withTitle }) => (React.createElement(BottomSheetItem, { disableGutters: true },
|
|
13
|
+
withTitle && (React.createElement(React.Fragment, null,
|
|
14
|
+
React.createElement(BottomSheetTitle, { icon: FileTypeText, label: "Title" }),
|
|
15
|
+
React.createElement(Divider, null))),
|
|
16
|
+
React.createElement(List, null,
|
|
17
|
+
React.createElement(ListItemButton, null,
|
|
18
|
+
React.createElement(ListItemIcon, null,
|
|
19
|
+
React.createElement(Icon, { icon: File })),
|
|
20
|
+
React.createElement(ListItemText, { primary: "Item with icon", secondary: "and with secondary text" })),
|
|
21
|
+
React.createElement(Divider, { variant: "inset" }),
|
|
22
|
+
React.createElement(ListItemButton, null,
|
|
23
|
+
React.createElement(ListItemIcon, null,
|
|
24
|
+
React.createElement(Checkbox, null)),
|
|
25
|
+
React.createElement(ListItemText, { primary: "Item with checkbox" })),
|
|
26
|
+
React.createElement(Divider, { variant: "inset" }),
|
|
27
|
+
React.createElement(ListItemButton, null,
|
|
28
|
+
React.createElement(ListItemIcon, null,
|
|
29
|
+
React.createElement(Radio, null)),
|
|
30
|
+
React.createElement(ListItemText, { primary: "Item with radio" })),
|
|
31
|
+
React.createElement(Divider, { variant: "inset" }),
|
|
32
|
+
React.createElement(ListItemButton, null,
|
|
33
|
+
React.createElement(ListItemIcon, null,
|
|
34
|
+
React.createElement(Icon, { icon: File })),
|
|
35
|
+
React.createElement(ListItemText, { primary: "Item with secondary action" }),
|
|
36
|
+
React.createElement(Icon, { icon: Right }))),
|
|
37
|
+
React.createElement(Divider, null),
|
|
38
|
+
React.createElement(List, null,
|
|
39
|
+
React.createElement(ListItemButton, null,
|
|
40
|
+
React.createElement(ListItemIcon, null,
|
|
41
|
+
React.createElement(Icon, { icon: FileTypeText, size: 32 })),
|
|
42
|
+
React.createElement(ListItemText, { primary: "Files" })),
|
|
43
|
+
React.createElement(ListItemButton, null,
|
|
44
|
+
React.createElement(ListItemIcon, null,
|
|
45
|
+
React.createElement(Icon, { icon: FileTypeSheet, size: 32 })),
|
|
46
|
+
React.createElement(ListItemText, { primary: "Sheets" }))),
|
|
47
|
+
React.createElement(Divider, { variant: "inset" }),
|
|
48
|
+
React.createElement(List, null,
|
|
49
|
+
React.createElement(ListItemButton, null,
|
|
50
|
+
React.createElement(ListItemIcon, null,
|
|
51
|
+
React.createElement(Icon, { icon: FileTypeSlide, size: 32 })),
|
|
52
|
+
React.createElement(ListItemText, { primary: "Slides" })),
|
|
53
|
+
React.createElement(ListItemButton, null,
|
|
54
|
+
React.createElement(ListItemIcon, null,
|
|
55
|
+
React.createElement(Icon, { icon: FileTypeVideo, size: 32 })),
|
|
56
|
+
React.createElement(ListItemText, { primary: "Videos" })))));
|
|
57
|
+
const ExampleBottomSheet = args => {
|
|
58
|
+
const [isOpen, setIsOpen] = useState(args.open);
|
|
59
|
+
const [isSecondOpen, setIsSecondOpen] = useState(false);
|
|
60
|
+
const handleClose = () => setIsOpen(false);
|
|
61
|
+
const settings = args.mediumHeight === undefined && args.mediumHeightRatio === undefined
|
|
62
|
+
? {}
|
|
63
|
+
: {
|
|
64
|
+
mediumHeight: args.mediumHeight,
|
|
65
|
+
mediumHeightRatio: args.mediumHeightRatio
|
|
66
|
+
};
|
|
67
|
+
return (React.createElement(React.Fragment, null,
|
|
68
|
+
React.createElement(Button, { variant: "outlined", onClick: () => setIsOpen(true) }, "Open BottomSheet"),
|
|
69
|
+
isOpen && (React.createElement(BottomSheet, { toolbarProps: args.withFakeToolbar ? { height: 50 } : undefined, settings: {
|
|
70
|
+
...settings,
|
|
71
|
+
isOpenMin: args.isOpenMin,
|
|
72
|
+
hasMinHeightOffset: args.hasMinHeightOffset
|
|
73
|
+
}, backdrop: args.backdrop, skipAnimation: args.skipAnimation, offset: args.offset, onClose: args.closable ? handleClose : undefined, portalProps: { disablePortal: args.disablePortal } },
|
|
74
|
+
args.withHeader && (React.createElement(BottomSheetHeader, { sx: { px: 2, pb: 2, gap: 1 } },
|
|
75
|
+
React.createElement(Button, { variant: "outlined", fullWidth: true, onClick: () => setIsSecondOpen(true) }, "Open BottomSheet"),
|
|
76
|
+
React.createElement(Button, { variant: "outlined", fullWidth: true }, "Button 2"),
|
|
77
|
+
isSecondOpen && (React.createElement(BottomSheet, { backdrop: true, onClose: () => setIsSecondOpen(false) },
|
|
78
|
+
React.createElement(BottomSheetItem, null, shortContent))))),
|
|
79
|
+
args.withListContent ? (React.createElement(ListContent, { withTitle: args.withTitle })) : (React.createElement(React.Fragment, null,
|
|
80
|
+
args.withTitle && React.createElement(BottomSheetTitle, { label: "Title" }),
|
|
81
|
+
React.createElement(BottomSheetItem, null, args.longContent ? longContent : shortContent)))))));
|
|
82
|
+
};
|
|
83
|
+
const meta = {
|
|
84
|
+
title: 'BottomSheet',
|
|
85
|
+
tags: ['autodocs'],
|
|
86
|
+
parameters: {
|
|
87
|
+
docs: {
|
|
88
|
+
description: {
|
|
89
|
+
component: 'A `backdrop` requires the sheet to be closable: check `closable` whenever `backdrop` is checked.'
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
argTypes: {
|
|
94
|
+
open: { control: 'boolean' },
|
|
95
|
+
backdrop: { control: 'boolean' },
|
|
96
|
+
closable: { control: 'boolean' },
|
|
97
|
+
skipAnimation: { control: 'boolean' },
|
|
98
|
+
disablePortal: { control: 'boolean' },
|
|
99
|
+
withFakeToolbar: { control: 'boolean' },
|
|
100
|
+
withHeader: { control: 'boolean' },
|
|
101
|
+
withTitle: { control: 'boolean' },
|
|
102
|
+
withListContent: { control: 'boolean' },
|
|
103
|
+
longContent: { control: 'boolean' },
|
|
104
|
+
isOpenMin: { control: 'boolean' },
|
|
105
|
+
hasMinHeightOffset: { control: 'boolean' },
|
|
106
|
+
mediumHeight: { control: 'number' },
|
|
107
|
+
mediumHeightRatio: { control: { type: 'number', step: 0.05 } },
|
|
108
|
+
offset: { control: 'number' }
|
|
109
|
+
},
|
|
110
|
+
args: {
|
|
111
|
+
open: false,
|
|
112
|
+
backdrop: true,
|
|
113
|
+
closable: true,
|
|
114
|
+
skipAnimation: false,
|
|
115
|
+
disablePortal: false,
|
|
116
|
+
withFakeToolbar: false,
|
|
117
|
+
withHeader: true,
|
|
118
|
+
withTitle: false,
|
|
119
|
+
withListContent: false,
|
|
120
|
+
longContent: false,
|
|
121
|
+
isOpenMin: false,
|
|
122
|
+
hasMinHeightOffset: false
|
|
123
|
+
},
|
|
124
|
+
render: args => React.createElement(ExampleBottomSheet, { ...args })
|
|
125
|
+
};
|
|
126
|
+
export default meta;
|
|
127
|
+
export const Default = {};
|
|
128
|
+
// Visual Regression - the sheet open at its medium stop, with every subcomponent. Kept out of
|
|
129
|
+
// the docs page, where its open sheet would cover the whole page
|
|
130
|
+
export const Screenshot = {
|
|
131
|
+
tags: ['argos', '!autodocs'],
|
|
132
|
+
render: () => (React.createElement(BottomSheet, { backdrop: true, skipAnimation: true, settings: { mediumHeight: 450 }, onClose: () => undefined },
|
|
133
|
+
React.createElement(BottomSheetHeader, { sx: { px: 2, pb: 2, gap: 1 } },
|
|
134
|
+
React.createElement(Button, { variant: "outlined", fullWidth: true }, "Button 1"),
|
|
135
|
+
React.createElement(Button, { variant: "outlined", fullWidth: true }, "Button 2")),
|
|
136
|
+
React.createElement(BottomSheetTitle, { label: "Title" }),
|
|
137
|
+
React.createElement(BottomSheetItem, null, shortContent),
|
|
138
|
+
React.createElement(ListContent, { withTitle: true })))
|
|
139
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { BottomSheetItemProps } from './BottomSheetItem';
|
|
3
|
+
export type BottomSheetHeaderProps = Omit<BottomSheetItemProps, 'disableGutters' | 'disableElevation'>;
|
|
4
|
+
/**
|
|
5
|
+
* Content kept visible at the bottom of the sheet when it is at its lowest stop, such as action
|
|
6
|
+
* buttons. Its height and bottom padding are part of the sheet minimum height.
|
|
7
|
+
*/
|
|
8
|
+
declare const BottomSheetHeader: React.ForwardRefExoticComponent<BottomSheetHeaderProps & React.RefAttributes<HTMLDivElement>>;
|
|
9
|
+
export default BottomSheetHeader;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { styled } from '@mui/material/styles';
|
|
2
|
+
import React, { forwardRef } from 'react';
|
|
3
|
+
import BottomSheetItem from './BottomSheetItem';
|
|
4
|
+
const BottomSheetHeaderRoot = styled(BottomSheetItem)({
|
|
5
|
+
display: 'flex',
|
|
6
|
+
alignItems: 'center'
|
|
7
|
+
});
|
|
8
|
+
/**
|
|
9
|
+
* Content kept visible at the bottom of the sheet when it is at its lowest stop, such as action
|
|
10
|
+
* buttons. Its height and bottom padding are part of the sheet minimum height.
|
|
11
|
+
*/
|
|
12
|
+
const BottomSheetHeader = forwardRef((props, ref) => (React.createElement(BottomSheetHeaderRoot, { ref: ref, disableGutters: true, disableElevation: true, ...props })));
|
|
13
|
+
BottomSheetHeader.displayName = 'BottomSheetHeader';
|
|
14
|
+
export default BottomSheetHeader;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { PaperProps } from '@mui/material';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
export interface BottomSheetItemProps extends Omit<PaperProps, 'ref'> {
|
|
4
|
+
/** Removes the default padding */
|
|
5
|
+
disableGutters?: boolean;
|
|
6
|
+
/** Removes the default elevation */
|
|
7
|
+
disableElevation?: boolean;
|
|
8
|
+
}
|
|
9
|
+
declare const BottomSheetItem: React.ForwardRefExoticComponent<BottomSheetItemProps & React.RefAttributes<HTMLDivElement>>;
|
|
10
|
+
export default BottomSheetItem;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Paper } from '@mui/material';
|
|
2
|
+
import { styled } from '@mui/material/styles';
|
|
3
|
+
import React, { forwardRef } from 'react';
|
|
4
|
+
/** Flat extra shadow the theme defines past the 24 MUI elevations */
|
|
5
|
+
const ITEM_ELEVATION = 25;
|
|
6
|
+
const BottomSheetItemRoot = styled(Paper, {
|
|
7
|
+
shouldForwardProp: prop => prop !== 'disableGutters'
|
|
8
|
+
})(({ theme }) => ({
|
|
9
|
+
variants: [
|
|
10
|
+
{
|
|
11
|
+
props: { disableGutters: false },
|
|
12
|
+
style: { padding: theme.spacing(2) }
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}));
|
|
16
|
+
const BottomSheetItem = forwardRef(({ disableGutters = false, disableElevation = false, ...props }, ref) => (React.createElement(BottomSheetItemRoot, { ref: ref, elevation: disableElevation ? 0 : ITEM_ELEVATION, square: true, disableGutters: disableGutters, ...props })));
|
|
17
|
+
BottomSheetItem.displayName = 'BottomSheetItem';
|
|
18
|
+
export default BottomSheetItem;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { IconProps } from '@linagora/twake-icons';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
export interface BottomSheetTitleProps {
|
|
4
|
+
className?: string;
|
|
5
|
+
label?: string;
|
|
6
|
+
/** Shows the title as a list item with this icon */
|
|
7
|
+
icon?: IconProps['icon'];
|
|
8
|
+
}
|
|
9
|
+
declare const BottomSheetTitle: React.ForwardRefExoticComponent<BottomSheetTitleProps & React.RefAttributes<HTMLElement>>;
|
|
10
|
+
export default BottomSheetTitle;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Icon } from '@linagora/twake-icons';
|
|
2
|
+
import { List, ListItemIcon, Typography } from '@mui/material';
|
|
3
|
+
import React, { forwardRef } from 'react';
|
|
4
|
+
import BottomSheetItem from './BottomSheetItem';
|
|
5
|
+
import ListItem from '../ListItem';
|
|
6
|
+
import ListItemText from '../ListItemText';
|
|
7
|
+
const BottomSheetTitle = forwardRef(({ className, label, icon }, ref) => {
|
|
8
|
+
if (icon) {
|
|
9
|
+
return (React.createElement(BottomSheetItem, { disableGutters: true, disableElevation: true },
|
|
10
|
+
React.createElement(List, { ref: ref },
|
|
11
|
+
React.createElement(ListItem, null,
|
|
12
|
+
React.createElement(ListItemIcon, null,
|
|
13
|
+
React.createElement(Icon, { icon: icon, size: 32 })),
|
|
14
|
+
React.createElement(ListItemText, { primary: label, slotProps: { primary: { variant: 'h6', noWrap: true } } })))));
|
|
15
|
+
}
|
|
16
|
+
return (React.createElement(BottomSheetItem, { disableElevation: true },
|
|
17
|
+
React.createElement(Typography, { ref: ref, className: className, variant: "h6", align: "center" }, label)));
|
|
18
|
+
});
|
|
19
|
+
BottomSheetTitle.displayName = 'BottomSheetTitle';
|
|
20
|
+
export default BottomSheetTitle;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { SpringConfig } from '@react-spring/web';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
export type SnapPointSeekerMode = 'close' | 'next';
|
|
4
|
+
export interface BottomSheetCoreProps {
|
|
5
|
+
/** Height of the sheet when closed, in px */
|
|
6
|
+
defaultHeight?: number;
|
|
7
|
+
/** Intermediate heights the sheet stops at, in px */
|
|
8
|
+
peekHeights?: number[] | null;
|
|
9
|
+
/** Allows the sheet to go full screen, otherwise it tops out at its content height */
|
|
10
|
+
fullHeight?: boolean;
|
|
11
|
+
/** Index of the stop to move the sheet to, among `[defaultHeight, ...peekHeights, fullHeight]` */
|
|
12
|
+
currentIndex?: number;
|
|
13
|
+
/** Called with the index of the stop the user moved the sheet to */
|
|
14
|
+
onIndexChange?: (index: number) => void;
|
|
15
|
+
/** Moves the sheet off screen */
|
|
16
|
+
hidden?: boolean;
|
|
17
|
+
/** Distance above the highest stop, in px, at which dragging up is cancelled */
|
|
18
|
+
threshold?: number;
|
|
19
|
+
/** Prevents dragging the sheet below its lowest stop */
|
|
20
|
+
disabledClosing?: boolean;
|
|
21
|
+
/** On release, snaps to the closest stop or to the next one in the drag direction */
|
|
22
|
+
snapPointSeekerMode?: SnapPointSeekerMode;
|
|
23
|
+
springConfig?: SpringConfig;
|
|
24
|
+
skipAnimation?: boolean;
|
|
25
|
+
className?: string;
|
|
26
|
+
children?: React.ReactNode;
|
|
27
|
+
}
|
|
28
|
+
export declare function BottomSheetCore({ defaultHeight, peekHeights, fullHeight, currentIndex, onIndexChange, hidden, threshold, disabledClosing, snapPointSeekerMode, springConfig, skipAnimation, className, children }: BottomSheetCoreProps): React.JSX.Element;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Adapted from mui-bottom-sheet
|
|
3
|
+
* Copyright (c) 2020 Harley Alexander, MIT License
|
|
4
|
+
*/
|
|
5
|
+
import { animated, config, useSpring } from '@react-spring/web';
|
|
6
|
+
import { useDrag } from '@use-gesture/react';
|
|
7
|
+
import React, { useEffect, useRef } from 'react';
|
|
8
|
+
import useMeasure from 'react-use-measure';
|
|
9
|
+
import { computeClosestStop, computeNextStop, isNodeInside } from './helpers';
|
|
10
|
+
const HIDDEN_MARGIN = 30;
|
|
11
|
+
const HORIZONTAL_DRAG_RATIO = 0.8;
|
|
12
|
+
export function BottomSheetCore({ defaultHeight = 100, peekHeights = [], fullHeight = true, currentIndex, onIndexChange, hidden = false, threshold = 70, disabledClosing = false, snapPointSeekerMode = 'close', springConfig = config.stiff, skipAnimation = false, className, children }) {
|
|
13
|
+
const computeStopPosition = (height) => window.innerHeight - height;
|
|
14
|
+
const defaultPosition = computeStopPosition(defaultHeight);
|
|
15
|
+
const stops = [defaultPosition];
|
|
16
|
+
const [measureRef, { height }] = useMeasure();
|
|
17
|
+
if (height > window.innerHeight && fullHeight) {
|
|
18
|
+
stops.push(0);
|
|
19
|
+
}
|
|
20
|
+
if (height < window.innerHeight && height > defaultHeight) {
|
|
21
|
+
stops.push(computeStopPosition(height));
|
|
22
|
+
}
|
|
23
|
+
peekHeights?.forEach(peekHeight => {
|
|
24
|
+
const position = computeStopPosition(peekHeight);
|
|
25
|
+
if (peekHeight < height &&
|
|
26
|
+
peekHeight < window.innerHeight &&
|
|
27
|
+
!stops.includes(position)) {
|
|
28
|
+
stops.push(position);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
// Positions grow from top to bottom while peek heights grow from bottom to top, so a descending
|
|
32
|
+
// sort makes stop indexes match peek height indexes
|
|
33
|
+
stops.sort((a, b) => b - a);
|
|
34
|
+
const containerRef = useRef(null);
|
|
35
|
+
const lastDirectionYRef = useRef(0);
|
|
36
|
+
const [{ y }, api] = useSpring(() => ({ y: defaultPosition }));
|
|
37
|
+
const bind = useDrag(({ last, cancel, offset: [, offsetY], delta: [deltaX, deltaY], direction: [, directionY], event }) => {
|
|
38
|
+
event.stopPropagation();
|
|
39
|
+
if (!isNodeInside(event.target, containerRef.current))
|
|
40
|
+
return;
|
|
41
|
+
if (Math.abs(deltaX) >
|
|
42
|
+
HORIZONTAL_DRAG_RATIO * Math.hypot(deltaX, deltaY)) {
|
|
43
|
+
cancel();
|
|
44
|
+
}
|
|
45
|
+
if (containerRef.current?.scrollTop)
|
|
46
|
+
return;
|
|
47
|
+
if (offsetY < 0 || offsetY < Math.min(...stops) - threshold) {
|
|
48
|
+
cancel();
|
|
49
|
+
}
|
|
50
|
+
if (disabledClosing && offsetY > Math.max(...stops)) {
|
|
51
|
+
cancel();
|
|
52
|
+
}
|
|
53
|
+
if (!last) {
|
|
54
|
+
lastDirectionYRef.current = directionY;
|
|
55
|
+
void api.start({
|
|
56
|
+
y: offsetY,
|
|
57
|
+
config: { duration: 0 },
|
|
58
|
+
immediate: skipAnimation
|
|
59
|
+
});
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
// The release event resets the direction when the pointer stood still, so the direction
|
|
63
|
+
// of the last move decides which stop comes next
|
|
64
|
+
const newPosition = snapPointSeekerMode === 'next'
|
|
65
|
+
? computeNextStop(offsetY, lastDirectionYRef.current, stops)
|
|
66
|
+
: computeClosestStop(offsetY, stops);
|
|
67
|
+
void api.start({
|
|
68
|
+
y: newPosition,
|
|
69
|
+
config: springConfig,
|
|
70
|
+
immediate: skipAnimation
|
|
71
|
+
});
|
|
72
|
+
onIndexChange?.(stops.findIndex(stop => stop === newPosition));
|
|
73
|
+
}, {
|
|
74
|
+
// Once the content is scrolled, starting from that negative offset makes the sheet follow
|
|
75
|
+
// the pointer only after the content is back to its top
|
|
76
|
+
from: () => [
|
|
77
|
+
0,
|
|
78
|
+
containerRef.current?.scrollTop
|
|
79
|
+
? -containerRef.current.scrollTop + Math.min(...stops)
|
|
80
|
+
: y.goal
|
|
81
|
+
],
|
|
82
|
+
bounds: { top: 0 },
|
|
83
|
+
// Touch and mouse events let the content scroll natively, unlike pointer events
|
|
84
|
+
pointer: { touch: true, mouse: true },
|
|
85
|
+
// Arrow keys typed in the sheet content must not move the sheet
|
|
86
|
+
keys: false
|
|
87
|
+
});
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
void api.start({
|
|
90
|
+
y: hidden ? window.innerHeight + HIDDEN_MARGIN : defaultPosition,
|
|
91
|
+
config: config.gentle,
|
|
92
|
+
immediate: skipAnimation
|
|
93
|
+
});
|
|
94
|
+
}, [hidden, api, defaultPosition, skipAnimation]);
|
|
95
|
+
// Stops are recomputed on every render, so this check runs after each one
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (currentIndex !== undefined && y.goal !== stops[currentIndex]) {
|
|
98
|
+
void api.start({
|
|
99
|
+
y: stops[currentIndex],
|
|
100
|
+
config: springConfig,
|
|
101
|
+
immediate: skipAnimation
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
return (React.createElement(animated.div, { ...bind(), ref: containerRef, className: className, style: {
|
|
106
|
+
width: '100%',
|
|
107
|
+
minHeight: defaultHeight,
|
|
108
|
+
maxHeight: '100%',
|
|
109
|
+
position: 'fixed',
|
|
110
|
+
left: 0,
|
|
111
|
+
top: 0,
|
|
112
|
+
y,
|
|
113
|
+
display: y.to(positionY => positionY < window.innerHeight + HIDDEN_MARGIN ? 'block' : 'none'),
|
|
114
|
+
// Content only scrolls once the sheet reached its highest stop
|
|
115
|
+
overflowY: y.to(positionY => positionY > Math.min(...stops) ? 'hidden' : 'scroll')
|
|
116
|
+
} },
|
|
117
|
+
React.createElement("div", { ref: measureRef }, children)));
|
|
118
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function computeClosestStop(goal: number, stops: number[]): number;
|
|
2
|
+
/**
|
|
3
|
+
* Finds the stop following `goal` in the drag direction: a higher value for a positive direction,
|
|
4
|
+
* a lower or equal one otherwise. A found `0` stop (full screen) falls back to the last stop like
|
|
5
|
+
* a miss: `||` is kept over `??` to preserve that snapping.
|
|
6
|
+
*/
|
|
7
|
+
export declare function computeNextStop(goal: number, direction: number, stops: number[]): number;
|
|
8
|
+
export declare function isNodeInside(node: EventTarget | null, container: Node | null): boolean;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Snap point helpers adapted from mui-bottom-sheet
|
|
3
|
+
* Copyright (c) 2020 Harley Alexander, MIT License
|
|
4
|
+
*/
|
|
5
|
+
export function computeClosestStop(goal, stops) {
|
|
6
|
+
return stops.reduce((prev, stop) => Math.abs(stop - goal) < Math.abs(prev - goal) ? stop : prev);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Finds the stop following `goal` in the drag direction: a higher value for a positive direction,
|
|
10
|
+
* a lower or equal one otherwise. A found `0` stop (full screen) falls back to the last stop like
|
|
11
|
+
* a miss: `||` is kept over `??` to preserve that snapping.
|
|
12
|
+
*/
|
|
13
|
+
export function computeNextStop(goal, direction, stops) {
|
|
14
|
+
if (direction > 0) {
|
|
15
|
+
const ascStops = [...stops].sort((a, b) => a - b);
|
|
16
|
+
return ascStops.find(stop => stop >= goal) || ascStops[ascStops.length - 1];
|
|
17
|
+
}
|
|
18
|
+
const descStops = [...stops].sort((a, b) => b - a);
|
|
19
|
+
return descStops.find(stop => stop <= goal) || descStops[descStops.length - 1];
|
|
20
|
+
}
|
|
21
|
+
export function isNodeInside(node, container) {
|
|
22
|
+
let current = node instanceof Node ? node : null;
|
|
23
|
+
while (current?.parentNode) {
|
|
24
|
+
if (current === container)
|
|
25
|
+
return true;
|
|
26
|
+
current = current.parentNode;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { BottomPositionParams, BottomSheetToolbarProps, BottomSpacerParams, MediumHeightParams, MinHeightParams, MinimizeAndCloseParams, TopPositionParams } from './types';
|
|
3
|
+
export declare const ANIMATION_DURATION = 250;
|
|
4
|
+
export declare const computeToolbarHeight: (toolbarProps?: BottomSheetToolbarProps) => number;
|
|
5
|
+
export declare const computeMaxHeight: (toolbarProps?: BottomSheetToolbarProps) => number;
|
|
6
|
+
export declare const computeMediumHeight: ({ backdrop, maxHeight, mediumHeight, mediumHeightRatio, innerContentHeight, bottomSpacerHeight, offset }: MediumHeightParams) => number;
|
|
7
|
+
export declare const computeMinHeight: ({ isClosable, isOpenMin, headerRef, offset, actionButtonsHeight, actionButtonsBottomMargin }: MinHeightParams) => number;
|
|
8
|
+
/** Gives the header child the ref the sheet measures it with */
|
|
9
|
+
export declare const makeOverriddenChildren: (children: React.ReactNode, headerContentRef: React.Ref<HTMLDivElement>) => React.ReactNode;
|
|
10
|
+
export declare const setTopPosition: ({ snapIndex, peekHeights, isTopPosition, setIsTopPosition }: TopPositionParams) => void;
|
|
11
|
+
export declare const setBottomPosition: ({ snapIndex, isBottomPosition, setIsBottomPosition }: BottomPositionParams) => void;
|
|
12
|
+
export declare const minimizeAndClose: ({ backdrop, setCurrentIndex, setIsTopPosition, setIsBottomPosition, handleClose }: MinimizeAndCloseParams) => void;
|
|
13
|
+
export declare const computeBottomSpacer: ({ backdrop, maxHeight, innerContentHeight, toolbarProps, offset }: BottomSpacerParams) => number;
|
|
14
|
+
export declare const getCssValue: (element: Element | null, property: string) => number;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import React, { cloneElement, isValidElement } from 'react';
|
|
2
|
+
import BottomSheetHeader from './BottomSheetHeader';
|
|
3
|
+
import { getSafeAreaInset } from './safeArea';
|
|
4
|
+
export const ANIMATION_DURATION = 250;
|
|
5
|
+
export const computeToolbarHeight = (toolbarProps = {}) => {
|
|
6
|
+
const { ref, height } = toolbarProps;
|
|
7
|
+
let toolbarHeight = 1;
|
|
8
|
+
if (height) {
|
|
9
|
+
toolbarHeight = height;
|
|
10
|
+
}
|
|
11
|
+
else if (ref?.current) {
|
|
12
|
+
toolbarHeight = ref.current.offsetHeight;
|
|
13
|
+
}
|
|
14
|
+
return toolbarHeight + getSafeAreaInset('top');
|
|
15
|
+
};
|
|
16
|
+
export const computeMaxHeight = (toolbarProps) => {
|
|
17
|
+
return window.innerHeight - computeToolbarHeight(toolbarProps);
|
|
18
|
+
};
|
|
19
|
+
export const computeMediumHeight = ({ backdrop, maxHeight, mediumHeight, mediumHeightRatio, innerContentHeight, bottomSpacerHeight, offset }) => {
|
|
20
|
+
const mediumHeightOrWithRatio = mediumHeight || Math.round(maxHeight * mediumHeightRatio);
|
|
21
|
+
if (backdrop) {
|
|
22
|
+
if (mediumHeightOrWithRatio < innerContentHeight) {
|
|
23
|
+
return mediumHeightOrWithRatio < maxHeight
|
|
24
|
+
? mediumHeightOrWithRatio
|
|
25
|
+
: maxHeight;
|
|
26
|
+
}
|
|
27
|
+
return innerContentHeight > maxHeight
|
|
28
|
+
? maxHeight
|
|
29
|
+
: innerContentHeight + bottomSpacerHeight;
|
|
30
|
+
}
|
|
31
|
+
if (innerContentHeight < mediumHeightOrWithRatio) {
|
|
32
|
+
return innerContentHeight + offset;
|
|
33
|
+
}
|
|
34
|
+
return mediumHeightOrWithRatio > maxHeight
|
|
35
|
+
? maxHeight
|
|
36
|
+
: mediumHeightOrWithRatio;
|
|
37
|
+
};
|
|
38
|
+
export const computeMinHeight = ({ isClosable, isOpenMin, headerRef, offset = 0, actionButtonsHeight, actionButtonsBottomMargin }) => {
|
|
39
|
+
if (isClosable && !isOpenMin)
|
|
40
|
+
return 0;
|
|
41
|
+
return ((headerRef.current?.offsetHeight ?? 0) +
|
|
42
|
+
offset +
|
|
43
|
+
actionButtonsHeight +
|
|
44
|
+
actionButtonsBottomMargin +
|
|
45
|
+
getSafeAreaInset('bottom'));
|
|
46
|
+
};
|
|
47
|
+
/** Gives the header child the ref the sheet measures it with */
|
|
48
|
+
export const makeOverriddenChildren = (children, headerContentRef) => {
|
|
49
|
+
return React.Children.map(children, child => isValidElement(child) && child.type === BottomSheetHeader
|
|
50
|
+
? cloneElement(child, {
|
|
51
|
+
ref: headerContentRef
|
|
52
|
+
})
|
|
53
|
+
: child);
|
|
54
|
+
};
|
|
55
|
+
export const setTopPosition = ({ snapIndex, peekHeights, isTopPosition, setIsTopPosition }) => {
|
|
56
|
+
const maxHeightSnapIndex = peekHeights.length - 1;
|
|
57
|
+
if (snapIndex > maxHeightSnapIndex) {
|
|
58
|
+
setIsTopPosition(true);
|
|
59
|
+
}
|
|
60
|
+
if (snapIndex === maxHeightSnapIndex && !isTopPosition) {
|
|
61
|
+
setIsTopPosition(true);
|
|
62
|
+
}
|
|
63
|
+
if (snapIndex < maxHeightSnapIndex && isTopPosition) {
|
|
64
|
+
setIsTopPosition(false);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
export const setBottomPosition = ({ snapIndex, isBottomPosition, setIsBottomPosition }) => {
|
|
68
|
+
if (snapIndex === 0 && !isBottomPosition) {
|
|
69
|
+
setIsBottomPosition(true);
|
|
70
|
+
}
|
|
71
|
+
if (snapIndex !== 0 && isBottomPosition) {
|
|
72
|
+
setIsBottomPosition(false);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
export const minimizeAndClose = ({ backdrop, setCurrentIndex, setIsTopPosition, setIsBottomPosition, handleClose }) => {
|
|
76
|
+
if (backdrop) {
|
|
77
|
+
setCurrentIndex(0);
|
|
78
|
+
setIsTopPosition(false);
|
|
79
|
+
setIsBottomPosition(true);
|
|
80
|
+
setTimeout(handleClose, ANIMATION_DURATION);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
export const computeBottomSpacer = ({ backdrop, maxHeight, innerContentHeight, toolbarProps, offset }) => {
|
|
84
|
+
// Content taller than the available height
|
|
85
|
+
if (maxHeight - innerContentHeight <= 0) {
|
|
86
|
+
return offset + computeToolbarHeight(toolbarProps);
|
|
87
|
+
}
|
|
88
|
+
// Without backdrop, the sheet must be able to open up to the top of the window
|
|
89
|
+
return backdrop ? offset : maxHeight - innerContentHeight;
|
|
90
|
+
};
|
|
91
|
+
export const getCssValue = (element, property) => {
|
|
92
|
+
return element
|
|
93
|
+
? parseFloat(getComputedStyle(element).getPropertyValue(property))
|
|
94
|
+
: 0;
|
|
95
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { BottomSheetProps } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Panel coming up from the bottom of the screen, that the user can drag between a minimum, a
|
|
5
|
+
* medium and a full height. Rendered in a portal, like dialogs.
|
|
6
|
+
*/
|
|
7
|
+
declare const BottomSheet: ({ portalProps, ...props }: BottomSheetProps) => React.JSX.Element;
|
|
8
|
+
export default BottomSheet;
|