@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.
Files changed (40) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/components/BottomSheet/BottomSheet.stories.d.ts +23 -0
  3. package/dist/components/BottomSheet/BottomSheet.stories.js +139 -0
  4. package/dist/components/BottomSheet/BottomSheetHeader.d.ts +9 -0
  5. package/dist/components/BottomSheet/BottomSheetHeader.js +14 -0
  6. package/dist/components/BottomSheet/BottomSheetItem.d.ts +10 -0
  7. package/dist/components/BottomSheet/BottomSheetItem.js +18 -0
  8. package/dist/components/BottomSheet/BottomSheetTitle.d.ts +10 -0
  9. package/dist/components/BottomSheet/BottomSheetTitle.js +20 -0
  10. package/dist/components/BottomSheet/core/BottomSheetCore.d.ts +28 -0
  11. package/dist/components/BottomSheet/core/BottomSheetCore.js +118 -0
  12. package/dist/components/BottomSheet/core/helpers.d.ts +8 -0
  13. package/dist/components/BottomSheet/core/helpers.js +29 -0
  14. package/dist/components/BottomSheet/helpers.d.ts +14 -0
  15. package/dist/components/BottomSheet/helpers.js +95 -0
  16. package/dist/components/BottomSheet/index.d.ts +8 -0
  17. package/dist/components/BottomSheet/index.js +158 -0
  18. package/dist/components/BottomSheet/safeArea.d.ts +3 -0
  19. package/dist/components/BottomSheet/safeArea.js +10 -0
  20. package/dist/components/BottomSheet/styles.d.ts +25 -0
  21. package/dist/components/BottomSheet/styles.js +100 -0
  22. package/dist/components/BottomSheet/types.d.ts +77 -0
  23. package/dist/components/BottomSheet/types.js +1 -0
  24. package/dist/components/ContactPopover/AttendeeInfo.js +3 -3
  25. package/dist/components/ContactPopover/ContactPopoverCalendarAction.js +2 -2
  26. package/dist/components/ContactPopover/ContactPopoverChatAction.js +2 -2
  27. package/dist/components/ContactPopover/ContactPopoverEmailAction.js +2 -2
  28. package/dist/components/ContactPopover/ContactPopoverVideoAction.js +2 -2
  29. package/dist/components/SearchBar/DisableHighlight.js +1 -1
  30. package/dist/components/SearchBar/FocusHighlight.js +1 -1
  31. package/dist/components/SearchBar/SearchBarIconWrapper.js +1 -1
  32. package/dist/components/SearchBar/SearchBarRoot.js +4 -4
  33. package/dist/components/SpaceOverlay/OverlayFrame.js +17 -21
  34. package/dist/components/SpaceOverlay/SpaceOverlay.js +2 -2
  35. package/dist/index.d.ts +16 -1
  36. package/dist/index.js +8 -1
  37. package/dist/lib/overrides.js +21 -19
  38. package/package.json +4 -1
  39. package/dist/components/SpaceOverlay/index.d.ts +0 -8
  40. 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;