@fountain-ui/lab 2.0.0-beta.82 → 2.0.0-beta.83

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.
@@ -1 +1 @@
1
- {"version":3,"names":[],"sources":["BottomSheetProps.ts"],"sourcesContent":["import React from 'react';\nimport type { ComponentProps } from '@fountain-ui/core';\n\nexport default interface BottomSheetProps extends ComponentProps<{\n /**\n * Opacity for BackdropComponent\n * @default 0.5\n */\n backdropOpacity?: number;\n\n /**\n * BottomSheet children, usually the included sub-components.\n */\n children?: React.ReactNode;\n\n /**\n * Enable dynamic sizing for content size.\n * @default true\n */\n enableDynamicSizing?: boolean;\n\n /**\n * Area to be fixed on the top of the bottom sheet.\n */\n header?: React.ReactNode;\n\n /**\n * Top element for displaying additional information on the bottom sheet.\n */\n topElement?: React.ReactNode;\n\n /**\n * Snap index. You could also provide -1 to bottom sheet in closed state.\n */\n index: number;\n\n /**\n * Maximum height(normalized value) of dialog\n * ex. 30% => 0.3 / 90% => 0.9\n * @default 0.9\n */\n maxHeightNormalizedRatio?: number;\n\n /**\n * Callback fired when the index is changed.\n * Important! Use memoized value.\n */\n onChange?: (newIndex: number) => void;\n\n /**\n * Points for the bottom sheet to snap to, points should be sorted from bottom to top.\n * Important! Use memoized value.\n * Only number type or string type(~% format) can be used.\n * @default []\n */\n snapPoints?: Array<number | string>;\n\n /**\n * Disable default backgroundColor.\n * @default false\n */\n disableDefaultBackgroundColor?: boolean;\n\n /**\n * Disable default shadow.\n * @default false\n */\n disableDefaultShadow?: boolean;\n}> {}\n"],"mappings":""}
1
+ {"version":3,"names":[],"sources":["BottomSheetProps.ts"],"sourcesContent":["import React from 'react';\nimport type { ComponentProps } from '@fountain-ui/core';\n\nexport default interface BottomSheetProps extends ComponentProps<{\n /**\n * Opacity for BackdropComponent\n * @default 0.5\n */\n backdropOpacity?: number;\n\n /**\n * Border radius for bottom sheet\n */\n borderRadius?: number;\n\n /**\n * BottomSheet children, usually the included sub-components.\n */\n children?: React.ReactNode;\n\n /**\n * Enable dynamic sizing for content size.\n * @default true\n */\n enableDynamicSizing?: boolean;\n\n /**\n * Area to be fixed on the top of the bottom sheet.\n */\n header?: React.ReactNode;\n\n /**\n * If set to true, a border will be applied to the header area when the bottom sheet content is scrollable.\n * This visually separates the header from the scrollable content.\n */\n enableScrollableHeaderBorder?: boolean;\n\n /**\n * Area to be fixed on the bottom of the bottom sheet.\n */\n stickyBottomElement?: React.ReactNode;\n\n /**\n * Top element for displaying additional information on the bottom sheet.\n */\n topElement?: React.ReactNode;\n\n /**\n * Snap index. You could also provide -1 to bottom sheet in closed state.\n */\n index: number;\n\n /**\n * Maximum height(normalized value) of dialog\n * ex. 30% => 0.3 / 90% => 0.9\n * @default 0.9\n */\n maxHeightNormalizedRatio?: number;\n\n /**\n * Callback fired when the index is changed.\n * Important! Use memoized value.\n */\n onChange?: (newIndex: number) => void;\n\n /**\n * Points for the bottom sheet to snap to, points should be sorted from bottom to top.\n * Important! Use memoized value.\n * Only number type or string type(~% format) can be used.\n * @default []\n */\n snapPoints?: Array<number | string>;\n}> {}\n"],"mappings":""}
@@ -1,11 +1,21 @@
1
- import React from 'react';
2
- import { ScrollView } from 'react-native';
3
- import { Column, Modal, Paper, StyleSheet, css, useTheme } from '@fountain-ui/core';
1
+ import React, { useLayoutEffect, useMemo, useState } from 'react';
2
+ import { ScrollView, useWindowDimensions, View } from 'react-native';
3
+ import { Column, css, Modal, Paper, StyleSheet, useElevationStyle, useTheme } from '@fountain-ui/core';
4
4
  import AnimatedY from '../AnimatedY';
5
5
  import useDynamicSnapPoints from './useDynamicSnapPoints';
6
6
 
7
+ const createHeightLayoutHandler = setHeight => {
8
+ return event => {
9
+ const {
10
+ height
11
+ } = event.nativeEvent.layout;
12
+ setHeight(height);
13
+ };
14
+ };
15
+
7
16
  const useStyles = function () {
8
17
  const theme = useTheme();
18
+ const stickyBottomElementShadow = useElevationStyle(8);
9
19
  return {
10
20
  root: {
11
21
  justifyContent: 'flex-end',
@@ -28,25 +38,51 @@ const useStyles = function () {
28
38
  position: 'absolute',
29
39
  bottom: 0,
30
40
  width: '100%'
31
- }
41
+ },
42
+ headerContainer: {
43
+ backgroundColor: theme.palette.paper.default
44
+ },
45
+ headerBorder: {
46
+ borderBottomWidth: 0.5,
47
+ borderBottomColor: theme.palette.divider
48
+ },
49
+ stickyBottomElement: {
50
+ position: 'absolute',
51
+ bottom: 0,
52
+ left: 0,
53
+ right: 0,
54
+ backgroundColor: theme.palette.paper.default
55
+ },
56
+ stickyBottomElementShadow
32
57
  };
33
58
  };
34
59
 
35
60
  export default function BottomSheet(props) {
36
61
  const {
37
62
  backdropOpacity,
63
+ borderRadius,
38
64
  children,
39
65
  enableDynamicSizing = true,
40
66
  header,
67
+ stickyBottomElement,
41
68
  topElement,
42
69
  index,
43
70
  maxHeightNormalizedRatio = 0.9,
44
71
  onChange,
45
72
  snapPoints = [],
46
- disableDefaultBackgroundColor = false,
47
- disableDefaultShadow = false
73
+ enableScrollableHeaderBorder = false
48
74
  } = props;
49
75
  const styles = useStyles();
76
+ const {
77
+ height: windowHeight
78
+ } = useWindowDimensions();
79
+ const [topElementHeight, setTopElementHeight] = useState(0);
80
+ const [stickyBottomElementHeight, setStickyBottomElementHeight] = useState(0);
81
+ const [contentHeight, setContentHeight] = useState(0);
82
+ const [isScrollable, setIsScrollable] = useState(false);
83
+ const maxDynamicContentSize = Math.round(windowHeight * maxHeightNormalizedRatio) - topElementHeight;
84
+ const handleTopElementLayout = createHeightLayoutHandler(setTopElementHeight);
85
+ const handleStickyBottomElementLayout = createHeightLayoutHandler(setStickyBottomElementHeight);
50
86
 
51
87
  const handleClose = () => {
52
88
  if (onChange) {
@@ -64,9 +100,27 @@ export default function BottomSheet(props) {
64
100
  snapPoints
65
101
  });
66
102
  const translateY = highestSnapPoint - (convertedSnapPoints[index] ?? 0);
67
- const paperStyles = [styles.paper, {
68
- height: highestSnapPoint
103
+ const adjustedContentHeight = useMemo(() => {
104
+ const adjustedHighestSnapPoint = highestSnapPoint + stickyBottomElementHeight;
105
+ return Math.min(maxDynamicContentSize, adjustedHighestSnapPoint);
106
+ }, [highestSnapPoint, stickyBottomElementHeight]);
107
+ const contentStyles = [styles.paper, borderRadius ? {
108
+ borderTopLeftRadius: borderRadius,
109
+ borderTopRightRadius: borderRadius
110
+ } : {}, {
111
+ height: adjustedContentHeight,
112
+ maxHeight: maxDynamicContentSize
69
113
  }];
114
+ const headerStyles = [styles.headerContainer, isScrollable && enableScrollableHeaderBorder ? styles.headerBorder : {}];
115
+ const stickyBottomElementStyles = [styles.stickyBottomElement, isScrollable ? styles.stickyBottomElementShadow : {}];
116
+ useLayoutEffect(() => {
117
+ if (contentHeight === 0 || highestSnapPoint === 0) {
118
+ return;
119
+ }
120
+
121
+ const adjustedHighestSnapPoint = highestSnapPoint + stickyBottomElementHeight;
122
+ setIsScrollable(adjustedHighestSnapPoint > maxDynamicContentSize);
123
+ }, [contentHeight, highestSnapPoint, stickyBottomElementHeight, maxDynamicContentSize]);
70
124
  return /*#__PURE__*/React.createElement(Modal, {
71
125
  backdropOpacity: backdropOpacity,
72
126
  onClose: handleClose,
@@ -76,15 +130,26 @@ export default function BottomSheet(props) {
76
130
  style: styles.animated,
77
131
  translateY: translateY
78
132
  }, topElement ? /*#__PURE__*/React.createElement(Column, null, /*#__PURE__*/React.createElement(Column, {
133
+ onLayout: handleTopElementLayout,
79
134
  style: styles.topElementLocation
80
135
  }, topElement)) : null, /*#__PURE__*/React.createElement(Paper, {
81
- elevation: disableDefaultShadow ? 0 : 12,
82
- style: paperStyles,
83
- colorValue: disableDefaultBackgroundColor ? '#ffffff00' : undefined
136
+ elevation: 12,
137
+ style: contentStyles
84
138
  }, /*#__PURE__*/React.createElement(ScrollView, {
85
- onContentSizeChange: handleContentSizeChange,
86
- stickyHeaderIndices: header ? [0] : undefined
87
- }, header, children))));
139
+ onContentSizeChange: (contentWidth, contentHeight) => {
140
+ setContentHeight(contentHeight);
141
+ handleContentSizeChange(contentWidth, contentHeight);
142
+ },
143
+ stickyHeaderIndices: header ? [0] : undefined,
144
+ style: {
145
+ paddingBottom: stickyBottomElementHeight
146
+ }
147
+ }, header ? /*#__PURE__*/React.createElement(View, {
148
+ style: headerStyles
149
+ }, header) : null, children), stickyBottomElement ? /*#__PURE__*/React.createElement(Column, {
150
+ style: stickyBottomElementStyles,
151
+ onLayout: handleStickyBottomElementLayout
152
+ }, stickyBottomElement) : null)));
88
153
  }
89
154
  ;
90
155
  //# sourceMappingURL=BottomSheetWeb.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["React","ScrollView","Column","Modal","Paper","StyleSheet","css","useTheme","AnimatedY","useDynamicSnapPoints","useStyles","theme","root","justifyContent","zIndex","dialog","animated","alignSelf","maxWidth","width","paper","borderTopLeftRadius","shape","roundnessExtra","borderTopRightRadius","borderBottomLeftRadius","borderBottomRightRadius","flexGrow","overflow","topElementLocation","position","bottom","BottomSheet","props","backdropOpacity","children","enableDynamicSizing","header","topElement","index","maxHeightNormalizedRatio","onChange","snapPoints","disableDefaultBackgroundColor","disableDefaultShadow","styles","handleClose","convertedSnapPoints","handleContentSizeChange","highestSnapPoint","translateY","paperStyles","height","absoluteFill","undefined"],"sources":["BottomSheetWeb.tsx"],"sourcesContent":["import React from 'react';\nimport { ScrollView } from 'react-native';\nimport { Column, Modal, Paper, StyleSheet, css, useTheme } from '@fountain-ui/core';\nimport { NamedStylesStringUnion, UseStyles } from '@fountain-ui/styles';\nimport AnimatedY from '../AnimatedY';\nimport type BottomSheetProps from './BottomSheetProps';\nimport useDynamicSnapPoints from './useDynamicSnapPoints';\n\ntype BottomSheetStyles = NamedStylesStringUnion<'root' | 'animated' | 'paper' | 'topElementLocation'>;\n\nconst useStyles: UseStyles<BottomSheetStyles> = function (): BottomSheetStyles {\n const theme = useTheme();\n\n return {\n root: {\n justifyContent: 'flex-end',\n zIndex: theme.zIndex.dialog,\n },\n animated: {\n alignSelf: 'center',\n maxWidth: 720,\n width: '100%',\n },\n paper: {\n borderTopLeftRadius: theme.shape.roundnessExtra,\n borderTopRightRadius: theme.shape.roundnessExtra,\n borderBottomLeftRadius: 0,\n borderBottomRightRadius: 0,\n flexGrow: 1,\n overflow: 'hidden',\n },\n topElementLocation: {\n position: 'absolute',\n bottom: 0,\n width: '100%',\n },\n };\n};\n\nexport default function BottomSheet(props: BottomSheetProps) {\n const {\n backdropOpacity,\n children,\n enableDynamicSizing = true,\n header,\n topElement,\n index,\n maxHeightNormalizedRatio = 0.9,\n onChange,\n snapPoints = [],\n disableDefaultBackgroundColor = false,\n disableDefaultShadow = false,\n } = props;\n\n const styles = useStyles();\n\n const handleClose = () => {\n if (onChange) {\n onChange(-1);\n }\n };\n\n const {\n convertedSnapPoints,\n handleContentSizeChange,\n highestSnapPoint,\n } = useDynamicSnapPoints({\n enableDynamicSizing,\n maxHeightNormalizedRatio,\n snapPoints,\n });\n\n const translateY = highestSnapPoint - (convertedSnapPoints[index] ?? 0);\n\n const paperStyles = [\n styles.paper,\n { height: highestSnapPoint },\n ];\n\n return (\n <Modal\n backdropOpacity={backdropOpacity}\n onClose={handleClose}\n visible={index >= 0}\n style={css([StyleSheet.absoluteFill, styles.root])}\n >\n <AnimatedY\n style={styles.animated}\n translateY={translateY}\n >\n {topElement ? (\n <Column>\n <Column style={styles.topElementLocation}>\n {topElement}\n </Column>\n </Column>\n ) : null}\n\n <Paper\n elevation={disableDefaultShadow ? 0 : 12}\n style={paperStyles}\n colorValue={disableDefaultBackgroundColor ? '#ffffff00' : undefined}\n >\n <ScrollView\n onContentSizeChange={handleContentSizeChange}\n stickyHeaderIndices={header ? [0] : undefined}\n >\n {header}\n\n {children}\n </ScrollView>\n </Paper>\n </AnimatedY>\n </Modal>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"names":["React","useLayoutEffect","useMemo","useState","ScrollView","useWindowDimensions","View","Column","css","Modal","Paper","StyleSheet","useElevationStyle","useTheme","AnimatedY","useDynamicSnapPoints","createHeightLayoutHandler","setHeight","event","height","nativeEvent","layout","useStyles","theme","stickyBottomElementShadow","root","justifyContent","zIndex","dialog","animated","alignSelf","maxWidth","width","paper","borderTopLeftRadius","shape","roundnessExtra","borderTopRightRadius","borderBottomLeftRadius","borderBottomRightRadius","flexGrow","overflow","topElementLocation","position","bottom","headerContainer","backgroundColor","palette","default","headerBorder","borderBottomWidth","borderBottomColor","divider","stickyBottomElement","left","right","BottomSheet","props","backdropOpacity","borderRadius","children","enableDynamicSizing","header","topElement","index","maxHeightNormalizedRatio","onChange","snapPoints","enableScrollableHeaderBorder","styles","windowHeight","topElementHeight","setTopElementHeight","stickyBottomElementHeight","setStickyBottomElementHeight","contentHeight","setContentHeight","isScrollable","setIsScrollable","maxDynamicContentSize","Math","round","handleTopElementLayout","handleStickyBottomElementLayout","handleClose","convertedSnapPoints","handleContentSizeChange","highestSnapPoint","translateY","adjustedContentHeight","adjustedHighestSnapPoint","min","contentStyles","maxHeight","headerStyles","stickyBottomElementStyles","absoluteFill","contentWidth","undefined","paddingBottom"],"sources":["BottomSheetWeb.tsx"],"sourcesContent":["import React, { useLayoutEffect, useMemo, useState } from 'react';\nimport { LayoutChangeEvent, ScrollView, useWindowDimensions, View } from 'react-native';\nimport { Column, css, Modal, Paper, StyleSheet, useElevationStyle, useTheme } from '@fountain-ui/core';\nimport { NamedStylesStringUnion, UseStyles } from '@fountain-ui/styles';\nimport AnimatedY from '../AnimatedY';\nimport type BottomSheetProps from './BottomSheetProps';\nimport useDynamicSnapPoints from './useDynamicSnapPoints';\n\nconst createHeightLayoutHandler = (setHeight: React.Dispatch<React.SetStateAction<number>>) => {\n return (event: LayoutChangeEvent) => {\n const { height } = event.nativeEvent.layout;\n setHeight(height);\n };\n}\n\ntype BottomSheetStyles = NamedStylesStringUnion<'root' | 'animated' | 'paper' | 'topElementLocation' | 'headerContainer' | 'headerBorder' | 'stickyBottomElement' | 'stickyBottomElementShadow'>;\n\nconst useStyles: UseStyles<BottomSheetStyles> = function (): BottomSheetStyles {\n const theme = useTheme();\n const stickyBottomElementShadow = useElevationStyle(8);\n\n return {\n root: {\n justifyContent: 'flex-end',\n zIndex: theme.zIndex.dialog,\n },\n animated: {\n alignSelf: 'center',\n maxWidth: 720,\n width: '100%',\n },\n paper: {\n borderTopLeftRadius: theme.shape.roundnessExtra,\n borderTopRightRadius: theme.shape.roundnessExtra,\n borderBottomLeftRadius: 0,\n borderBottomRightRadius: 0,\n flexGrow: 1,\n overflow: 'hidden',\n },\n topElementLocation: {\n position: 'absolute',\n bottom: 0,\n width: '100%',\n },\n headerContainer: {\n backgroundColor: theme.palette.paper.default,\n },\n headerBorder: {\n borderBottomWidth: 0.5,\n borderBottomColor: theme.palette.divider,\n },\n stickyBottomElement: {\n position: 'absolute',\n bottom: 0,\n left: 0,\n right: 0,\n backgroundColor: theme.palette.paper.default,\n },\n stickyBottomElementShadow,\n };\n};\n\nexport default function BottomSheet(props: BottomSheetProps) {\n const {\n backdropOpacity,\n borderRadius,\n children,\n enableDynamicSizing = true,\n header,\n stickyBottomElement,\n topElement,\n index,\n maxHeightNormalizedRatio = 0.9,\n onChange,\n snapPoints = [],\n enableScrollableHeaderBorder = false,\n } = props;\n\n const styles = useStyles();\n\n const { height: windowHeight } = useWindowDimensions();\n\n const [topElementHeight, setTopElementHeight] = useState(0);\n const [stickyBottomElementHeight, setStickyBottomElementHeight] = useState(0);\n const [contentHeight, setContentHeight] = useState(0);\n const [isScrollable, setIsScrollable] = useState(false);\n\n const maxDynamicContentSize = Math.round(windowHeight * maxHeightNormalizedRatio) - topElementHeight;\n\n const handleTopElementLayout = createHeightLayoutHandler(setTopElementHeight);\n const handleStickyBottomElementLayout = createHeightLayoutHandler(setStickyBottomElementHeight);\n\n const handleClose = () => {\n if (onChange) {\n onChange(-1);\n }\n };\n\n const {\n convertedSnapPoints,\n handleContentSizeChange,\n highestSnapPoint,\n } = useDynamicSnapPoints({\n enableDynamicSizing,\n maxHeightNormalizedRatio,\n snapPoints,\n });\n\n const translateY = highestSnapPoint - (convertedSnapPoints[index] ?? 0);\n\n const adjustedContentHeight = useMemo(() => {\n const adjustedHighestSnapPoint = highestSnapPoint + stickyBottomElementHeight;\n\n return Math.min(maxDynamicContentSize, adjustedHighestSnapPoint);\n }, [highestSnapPoint, stickyBottomElementHeight]);\n\n const contentStyles = [\n styles.paper,\n borderRadius ? { borderTopLeftRadius: borderRadius, borderTopRightRadius: borderRadius } : {},\n { height: adjustedContentHeight, maxHeight: maxDynamicContentSize },\n ];\n\n const headerStyles = [\n styles.headerContainer,\n isScrollable && enableScrollableHeaderBorder ? styles.headerBorder : {},\n ];\n\n const stickyBottomElementStyles = [\n styles.stickyBottomElement,\n isScrollable ? styles.stickyBottomElementShadow : {},\n ];\n\n useLayoutEffect(() => {\n if (contentHeight === 0 || highestSnapPoint === 0) {\n return;\n }\n\n const adjustedHighestSnapPoint = highestSnapPoint + stickyBottomElementHeight;\n setIsScrollable(adjustedHighestSnapPoint > maxDynamicContentSize);\n }, [contentHeight, highestSnapPoint, stickyBottomElementHeight, maxDynamicContentSize]);\n\n return (\n <Modal\n backdropOpacity={backdropOpacity}\n onClose={handleClose}\n visible={index >= 0}\n style={css([StyleSheet.absoluteFill, styles.root])}\n >\n <AnimatedY\n style={styles.animated}\n translateY={translateY}\n >\n {topElement ? (\n <Column>\n <Column\n onLayout={handleTopElementLayout}\n style={styles.topElementLocation}\n >\n {topElement}\n </Column>\n </Column>\n ) : null}\n\n <Paper\n elevation={12}\n style={contentStyles}\n >\n <ScrollView\n onContentSizeChange={(contentWidth: number, contentHeight: number) => {\n setContentHeight(contentHeight);\n handleContentSizeChange(contentWidth, contentHeight);\n }}\n stickyHeaderIndices={header ? [0] : undefined}\n style={{ paddingBottom: stickyBottomElementHeight }}\n >\n {header ? (\n <View style={headerStyles}>\n {header}\n </View>\n ): null}\n\n {children}\n </ScrollView>\n\n {stickyBottomElement ? (\n <Column\n style={stickyBottomElementStyles}\n onLayout={handleStickyBottomElementLayout}\n >\n {stickyBottomElement}\n </Column>\n ): null}\n </Paper>\n </AnimatedY>\n </Modal>\n );\n};\n"],"mappings":"AAAA,OAAOA,KAAP,IAAgBC,eAAhB,EAAiCC,OAAjC,EAA0CC,QAA1C,QAA0D,OAA1D;AACA,SAA4BC,UAA5B,EAAwCC,mBAAxC,EAA6DC,IAA7D,QAAyE,cAAzE;AACA,SAASC,MAAT,EAAiBC,GAAjB,EAAsBC,KAAtB,EAA6BC,KAA7B,EAAoCC,UAApC,EAAgDC,iBAAhD,EAAmEC,QAAnE,QAAmF,mBAAnF;AAEA,OAAOC,SAAP,MAAsB,cAAtB;AAEA,OAAOC,oBAAP,MAAiC,wBAAjC;;AAEA,MAAMC,yBAAyB,GAAIC,SAAD,IAA6D;EAC3F,OAAQC,KAAD,IAA8B;IACjC,MAAM;MAAEC;IAAF,IAAaD,KAAK,CAACE,WAAN,CAAkBC,MAArC;IACAJ,SAAS,CAACE,MAAD,CAAT;EACH,CAHD;AAIH,CALD;;AASA,MAAMG,SAAuC,GAAG,YAA+B;EAC3E,MAAMC,KAAK,GAAGV,QAAQ,EAAtB;EACA,MAAMW,yBAAyB,GAAGZ,iBAAiB,CAAC,CAAD,CAAnD;EAEA,OAAO;IACHa,IAAI,EAAE;MACFC,cAAc,EAAE,UADd;MAEFC,MAAM,EAAEJ,KAAK,CAACI,MAAN,CAAaC;IAFnB,CADH;IAKHC,QAAQ,EAAE;MACNC,SAAS,EAAE,QADL;MAENC,QAAQ,EAAE,GAFJ;MAGNC,KAAK,EAAE;IAHD,CALP;IAUHC,KAAK,EAAE;MACHC,mBAAmB,EAAEX,KAAK,CAACY,KAAN,CAAYC,cAD9B;MAEHC,oBAAoB,EAAEd,KAAK,CAACY,KAAN,CAAYC,cAF/B;MAGHE,sBAAsB,EAAE,CAHrB;MAIHC,uBAAuB,EAAE,CAJtB;MAKHC,QAAQ,EAAE,CALP;MAMHC,QAAQ,EAAE;IANP,CAVJ;IAkBHC,kBAAkB,EAAE;MAChBC,QAAQ,EAAE,UADM;MAEhBC,MAAM,EAAE,CAFQ;MAGhBZ,KAAK,EAAE;IAHS,CAlBjB;IAuBHa,eAAe,EAAE;MACbC,eAAe,EAAEvB,KAAK,CAACwB,OAAN,CAAcd,KAAd,CAAoBe;IADxB,CAvBd;IA0BHC,YAAY,EAAE;MACVC,iBAAiB,EAAE,GADT;MAEVC,iBAAiB,EAAE5B,KAAK,CAACwB,OAAN,CAAcK;IAFvB,CA1BX;IA8BHC,mBAAmB,EAAE;MACjBV,QAAQ,EAAE,UADO;MAEjBC,MAAM,EAAE,CAFS;MAGjBU,IAAI,EAAE,CAHW;MAIjBC,KAAK,EAAE,CAJU;MAKjBT,eAAe,EAAEvB,KAAK,CAACwB,OAAN,CAAcd,KAAd,CAAoBe;IALpB,CA9BlB;IAqCHxB;EArCG,CAAP;AAuCH,CA3CD;;AA6CA,eAAe,SAASgC,WAAT,CAAqBC,KAArB,EAA8C;EACzD,MAAM;IACFC,eADE;IAEFC,YAFE;IAGFC,QAHE;IAIFC,mBAAmB,GAAG,IAJpB;IAKFC,MALE;IAMFT,mBANE;IAOFU,UAPE;IAQFC,KARE;IASFC,wBAAwB,GAAG,GATzB;IAUFC,QAVE;IAWFC,UAAU,GAAG,EAXX;IAYFC,4BAA4B,GAAG;EAZ7B,IAaFX,KAbJ;EAeA,MAAMY,MAAM,GAAG/C,SAAS,EAAxB;EAEA,MAAM;IAAEH,MAAM,EAAEmD;EAAV,IAA2BjE,mBAAmB,EAApD;EAEA,MAAM,CAACkE,gBAAD,EAAmBC,mBAAnB,IAA0CrE,QAAQ,CAAC,CAAD,CAAxD;EACA,MAAM,CAACsE,yBAAD,EAA4BC,4BAA5B,IAA4DvE,QAAQ,CAAC,CAAD,CAA1E;EACA,MAAM,CAACwE,aAAD,EAAgBC,gBAAhB,IAAoCzE,QAAQ,CAAC,CAAD,CAAlD;EACA,MAAM,CAAC0E,YAAD,EAAeC,eAAf,IAAkC3E,QAAQ,CAAC,KAAD,CAAhD;EAEA,MAAM4E,qBAAqB,GAAGC,IAAI,CAACC,KAAL,CAAWX,YAAY,GAAGL,wBAA1B,IAAsDM,gBAApF;EAEA,MAAMW,sBAAsB,GAAGlE,yBAAyB,CAACwD,mBAAD,CAAxD;EACA,MAAMW,+BAA+B,GAAGnE,yBAAyB,CAAC0D,4BAAD,CAAjE;;EAEA,MAAMU,WAAW,GAAG,MAAM;IACtB,IAAIlB,QAAJ,EAAc;MACVA,QAAQ,CAAC,CAAC,CAAF,CAAR;IACH;EACJ,CAJD;;EAMA,MAAM;IACFmB,mBADE;IAEFC,uBAFE;IAGFC;EAHE,IAIFxE,oBAAoB,CAAC;IACrB8C,mBADqB;IAErBI,wBAFqB;IAGrBE;EAHqB,CAAD,CAJxB;EAUA,MAAMqB,UAAU,GAAGD,gBAAgB,IAAIF,mBAAmB,CAACrB,KAAD,CAAnB,IAA8B,CAAlC,CAAnC;EAEA,MAAMyB,qBAAqB,GAAGvF,OAAO,CAAC,MAAM;IACxC,MAAMwF,wBAAwB,GAAGH,gBAAgB,GAAGd,yBAApD;IAEA,OAAOO,IAAI,CAACW,GAAL,CAASZ,qBAAT,EAAgCW,wBAAhC,CAAP;EACH,CAJoC,EAIlC,CAACH,gBAAD,EAAmBd,yBAAnB,CAJkC,CAArC;EAMA,MAAMmB,aAAa,GAAG,CAClBvB,MAAM,CAACpC,KADW,EAElB0B,YAAY,GAAG;IAAEzB,mBAAmB,EAAEyB,YAAvB;IAAqCtB,oBAAoB,EAAEsB;EAA3D,CAAH,GAA+E,EAFzE,EAGlB;IAAExC,MAAM,EAAEsE,qBAAV;IAAiCI,SAAS,EAAEd;EAA5C,CAHkB,CAAtB;EAMA,MAAMe,YAAY,GAAG,CACjBzB,MAAM,CAACxB,eADU,EAEjBgC,YAAY,IAAIT,4BAAhB,GAA+CC,MAAM,CAACpB,YAAtD,GAAqE,EAFpD,CAArB;EAKA,MAAM8C,yBAAyB,GAAG,CAC9B1B,MAAM,CAAChB,mBADuB,EAE9BwB,YAAY,GAAGR,MAAM,CAAC7C,yBAAV,GAAsC,EAFpB,CAAlC;EAKAvB,eAAe,CAAC,MAAM;IAClB,IAAI0E,aAAa,KAAK,CAAlB,IAAuBY,gBAAgB,KAAK,CAAhD,EAAmD;MAC/C;IACH;;IAED,MAAMG,wBAAwB,GAAGH,gBAAgB,GAAGd,yBAApD;IACAK,eAAe,CAACY,wBAAwB,GAAGX,qBAA5B,CAAf;EACH,CAPc,EAOZ,CAACJ,aAAD,EAAgBY,gBAAhB,EAAkCd,yBAAlC,EAA6DM,qBAA7D,CAPY,CAAf;EASA,oBACI,oBAAC,KAAD;IACI,eAAe,EAAErB,eADrB;IAEI,OAAO,EAAE0B,WAFb;IAGI,OAAO,EAAEpB,KAAK,IAAI,CAHtB;IAII,KAAK,EAAExD,GAAG,CAAC,CAACG,UAAU,CAACqF,YAAZ,EAA0B3B,MAAM,CAAC5C,IAAjC,CAAD;EAJd,gBAMI,oBAAC,SAAD;IACI,KAAK,EAAE4C,MAAM,CAACxC,QADlB;IAEI,UAAU,EAAE2D;EAFhB,GAIKzB,UAAU,gBACP,oBAAC,MAAD,qBACI,oBAAC,MAAD;IACI,QAAQ,EAAEmB,sBADd;IAEI,KAAK,EAAEb,MAAM,CAAC3B;EAFlB,GAIKqB,UAJL,CADJ,CADO,GASP,IAbR,eAeI,oBAAC,KAAD;IACI,SAAS,EAAE,EADf;IAEI,KAAK,EAAE6B;EAFX,gBAII,oBAAC,UAAD;IACI,mBAAmB,EAAE,CAACK,YAAD,EAAuBtB,aAAvB,KAAiD;MAClEC,gBAAgB,CAACD,aAAD,CAAhB;MACAW,uBAAuB,CAACW,YAAD,EAAetB,aAAf,CAAvB;IACH,CAJL;IAKI,mBAAmB,EAAEb,MAAM,GAAG,CAAC,CAAD,CAAH,GAASoC,SALxC;IAMI,KAAK,EAAE;MAAEC,aAAa,EAAE1B;IAAjB;EANX,GAQKX,MAAM,gBACH,oBAAC,IAAD;IAAM,KAAK,EAAEgC;EAAb,GACKhC,MADL,CADG,GAIJ,IAZP,EAcKF,QAdL,CAJJ,EAqBKP,mBAAmB,gBAChB,oBAAC,MAAD;IACI,KAAK,EAAE0C,yBADX;IAEI,QAAQ,EAAEZ;EAFd,GAIK9B,mBAJL,CADgB,GAOjB,IA5BP,CAfJ,CANJ,CADJ;AAuDH;AAAA"}
@@ -6,6 +6,10 @@ export default interface BottomSheetProps extends ComponentProps<{
6
6
  * @default 0.5
7
7
  */
8
8
  backdropOpacity?: number;
9
+ /**
10
+ * Border radius for bottom sheet
11
+ */
12
+ borderRadius?: number;
9
13
  /**
10
14
  * BottomSheet children, usually the included sub-components.
11
15
  */
@@ -19,6 +23,15 @@ export default interface BottomSheetProps extends ComponentProps<{
19
23
  * Area to be fixed on the top of the bottom sheet.
20
24
  */
21
25
  header?: React.ReactNode;
26
+ /**
27
+ * If set to true, a border will be applied to the header area when the bottom sheet content is scrollable.
28
+ * This visually separates the header from the scrollable content.
29
+ */
30
+ enableScrollableHeaderBorder?: boolean;
31
+ /**
32
+ * Area to be fixed on the bottom of the bottom sheet.
33
+ */
34
+ stickyBottomElement?: React.ReactNode;
22
35
  /**
23
36
  * Top element for displaying additional information on the bottom sheet.
24
37
  */
@@ -45,15 +58,5 @@ export default interface BottomSheetProps extends ComponentProps<{
45
58
  * @default []
46
59
  */
47
60
  snapPoints?: Array<number | string>;
48
- /**
49
- * Disable default backgroundColor.
50
- * @default false
51
- */
52
- disableDefaultBackgroundColor?: boolean;
53
- /**
54
- * Disable default shadow.
55
- * @default false
56
- */
57
- disableDefaultShadow?: boolean;
58
61
  }> {
59
62
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fountain-ui/lab",
3
- "version": "2.0.0-beta.82",
3
+ "version": "2.0.0-beta.83",
4
4
  "private": false,
5
5
  "author": "Fountain-UI Team",
6
6
  "description": "Incubator for Fountain-UI React components.",
@@ -70,5 +70,5 @@
70
70
  "publishConfig": {
71
71
  "access": "public"
72
72
  },
73
- "gitHead": "43b797c06d530dd485c786adcfcef99dc042de26"
73
+ "gitHead": "dfa417385224924740cfd4438d32a3fbff88cb9c"
74
74
  }
@@ -1,7 +1,5 @@
1
1
  import React from 'react';
2
- import { Animated, Platform, useWindowDimensions, View } from 'react-native';
3
- import { Column, useAnimatedValue, ExtendedStyle, isNotAndroid12 } from '@fountain-ui/core';
4
- import { useTheme } from '@fountain-ui/styles';
2
+ import { Animated, LayoutChangeEvent, Platform, useWindowDimensions, View, ViewStyle } from 'react-native';
5
3
  import {
6
4
  BottomSheetBackdrop,
7
5
  BottomSheetBackdropProps,
@@ -9,31 +7,48 @@ import {
9
7
  BottomSheetModalProvider,
10
8
  BottomSheetScrollView,
11
9
  } from '@gorhom/bottom-sheet';
10
+ import { Column, ExtendedStyle, isNotAndroid12, useAnimatedValue, useElevationStyle } from '@fountain-ui/core';
11
+ import { useTheme } from '@fountain-ui/styles';
12
12
  import type BottomSheetProps from './BottomSheetProps';
13
13
  import TransparentBackdrop from './TransparentBackdrop';
14
14
 
15
15
  const NoHandle = () => null;
16
16
 
17
+ const createHeightLayoutHandler = (setHeight: React.Dispatch<React.SetStateAction<number>>) => {
18
+ return (event: LayoutChangeEvent) => {
19
+ const { height } = event.nativeEvent.layout;
20
+ setHeight(height);
21
+ };
22
+ }
23
+
17
24
  export default function BottomSheet(props: BottomSheetProps) {
18
25
  const {
19
26
  backdropOpacity = 0.5,
27
+ borderRadius: borderRadiusProp,
20
28
  children,
21
29
  enableDynamicSizing = true,
22
30
  header,
31
+ stickyBottomElement,
23
32
  topElement,
24
33
  index,
25
34
  maxHeightNormalizedRatio = 0.9,
26
35
  onChange,
27
36
  snapPoints = [],
28
- disableDefaultBackgroundColor = false,
29
- disableDefaultShadow = false,
37
+ enableScrollableHeaderBorder = false,
30
38
  } = props;
31
39
 
32
40
  const indexRef = React.useRef<number>(-1);
33
41
  const bottomSheetRef = React.useRef<BottomSheetModal | null>(null);
34
42
 
35
43
  const { height: windowHeight } = useWindowDimensions();
36
- const maxDynamicContentSize = Math.round(windowHeight * maxHeightNormalizedRatio);
44
+ const [isScrollable, setIsScrollable] = React.useState(false);
45
+ const [topElementHeight, setTopElementHeight] = React.useState(0);
46
+ const [stickyBottomElementHeight, setStickyBottomElementHeight] = React.useState(0);
47
+
48
+ const maxDynamicContentSize = Math.round(windowHeight * maxHeightNormalizedRatio) - topElementHeight;
49
+
50
+ const handleTopElementLayout = createHeightLayoutHandler(setTopElementHeight);
51
+ const handleStickyBottomElementLayout = createHeightLayoutHandler(setStickyBottomElementHeight);
37
52
 
38
53
  const handleChange = React.useCallback((newIndex: number) => {
39
54
  indexRef.current = newIndex;
@@ -64,25 +79,27 @@ export default function BottomSheet(props: BottomSheetProps) {
64
79
 
65
80
  const theme = useTheme();
66
81
  const shadow = theme.shadow[12];
82
+ const borderRadius = borderRadiusProp ?? theme.shape.roundnessExtra;
67
83
  const modalStyle = {
68
84
  backgroundColor: '#ffffff00',
69
- borderTopLeftRadius: theme.shape.roundnessExtra,
70
- borderTopRightRadius: theme.shape.roundnessExtra,
71
- ...(disableDefaultShadow ? {} : Platform.select<object>({
85
+ borderTopLeftRadius: borderRadius,
86
+ borderTopRightRadius: borderRadius,
87
+ ...Platform.select<object>({
72
88
  android: shadow?.elevation,
73
89
  ios: shadow?.shadow,
74
90
  web: shadow?.boxShadow,
75
- })),
91
+ }),
76
92
  };
93
+ const backgroundColor = theme.palette.paper.default;
77
94
  const backgroundStyle = {
78
- backgroundColor: disableDefaultBackgroundColor ? '#ffffff00' : theme.palette.paper.default,
79
- borderTopLeftRadius: theme.shape.roundnessExtra,
80
- borderTopRightRadius: theme.shape.roundnessExtra,
95
+ backgroundColor,
96
+ borderTopLeftRadius: borderRadius,
97
+ borderTopRightRadius: borderRadius,
81
98
  };
82
- const contentWrapperStyle = {
99
+ const contentWrapperStyle: ViewStyle = {
83
100
  flex: 1,
84
- borderTopLeftRadius: theme.shape.roundnessExtra,
85
- borderTopRightRadius: theme.shape.roundnessExtra,
101
+ borderTopLeftRadius: borderRadius,
102
+ borderTopRightRadius: borderRadius,
86
103
  overflow: 'hidden',
87
104
  };
88
105
 
@@ -115,6 +132,33 @@ export default function BottomSheet(props: BottomSheetProps) {
115
132
  }).start();
116
133
  };
117
134
 
135
+ const handleContentSizeChange = (_: number, contentHeight: number) => {
136
+ setIsScrollable(contentHeight > maxDynamicContentSize);
137
+ };
138
+
139
+ const headerStyle = {
140
+ backgroundColor,
141
+ ...(isScrollable && enableScrollableHeaderBorder ? {
142
+ borderBottomWidth: 0.5,
143
+ borderBottomColor: theme.palette.divider,
144
+ }: {})
145
+ }
146
+
147
+ const childrenStyle = {
148
+ backgroundColor,
149
+ paddingBottom: stickyBottomElementHeight,
150
+ }
151
+
152
+ const stickyBottomElementShadow = useElevationStyle(8);
153
+
154
+ const stickyBottomElementStyle = {
155
+ backgroundColor: theme.palette.paper.default,
156
+ position: 'absolute',
157
+ width: '100%',
158
+ bottom: 0,
159
+ ...(isScrollable ? stickyBottomElementShadow : {}),
160
+ }
161
+
118
162
  return (
119
163
  <BottomSheetModalProvider>
120
164
  <BottomSheetModal
@@ -136,22 +180,38 @@ export default function BottomSheet(props: BottomSheetProps) {
136
180
  >
137
181
  {topElement ? (
138
182
  <Animated.View style={topElementAnimationStyle}>
139
- <Column style={topElementLocationStyle}>
183
+ <Column
184
+ onLayout={handleTopElementLayout}
185
+ style={topElementLocationStyle}
186
+ >
140
187
  {topElement}
141
188
  </Column>
142
189
  </Animated.View>
143
190
  ) : null}
144
191
 
145
- {/* @ts-ignore */}
146
192
  <View style={contentWrapperStyle}>
147
193
  <BottomSheetScrollView
148
194
  bounces={false}
149
195
  stickyHeaderIndices={header ? [0] : undefined}
196
+ onContentSizeChange={handleContentSizeChange}
150
197
  >
151
- {header}
198
+ <View style={headerStyle}>
199
+ {header}
200
+ </View>
152
201
 
153
- {children}
202
+ <View style={childrenStyle}>
203
+ {children}
204
+ </View>
154
205
  </BottomSheetScrollView>
206
+
207
+ {stickyBottomElement ? (
208
+ <View
209
+ style={stickyBottomElementStyle}
210
+ onLayout={handleStickyBottomElementLayout}
211
+ >
212
+ {stickyBottomElement}
213
+ </View>
214
+ ): null}
155
215
  </View>
156
216
  </BottomSheetModal>
157
217
  </BottomSheetModalProvider>
@@ -8,6 +8,11 @@ export default interface BottomSheetProps extends ComponentProps<{
8
8
  */
9
9
  backdropOpacity?: number;
10
10
 
11
+ /**
12
+ * Border radius for bottom sheet
13
+ */
14
+ borderRadius?: number;
15
+
11
16
  /**
12
17
  * BottomSheet children, usually the included sub-components.
13
18
  */
@@ -24,6 +29,17 @@ export default interface BottomSheetProps extends ComponentProps<{
24
29
  */
25
30
  header?: React.ReactNode;
26
31
 
32
+ /**
33
+ * If set to true, a border will be applied to the header area when the bottom sheet content is scrollable.
34
+ * This visually separates the header from the scrollable content.
35
+ */
36
+ enableScrollableHeaderBorder?: boolean;
37
+
38
+ /**
39
+ * Area to be fixed on the bottom of the bottom sheet.
40
+ */
41
+ stickyBottomElement?: React.ReactNode;
42
+
27
43
  /**
28
44
  * Top element for displaying additional information on the bottom sheet.
29
45
  */
@@ -54,16 +70,4 @@ export default interface BottomSheetProps extends ComponentProps<{
54
70
  * @default []
55
71
  */
56
72
  snapPoints?: Array<number | string>;
57
-
58
- /**
59
- * Disable default backgroundColor.
60
- * @default false
61
- */
62
- disableDefaultBackgroundColor?: boolean;
63
-
64
- /**
65
- * Disable default shadow.
66
- * @default false
67
- */
68
- disableDefaultShadow?: boolean;
69
73
  }> {}
@@ -1,15 +1,23 @@
1
- import React from 'react';
2
- import { ScrollView } from 'react-native';
3
- import { Column, Modal, Paper, StyleSheet, css, useTheme } from '@fountain-ui/core';
1
+ import React, { useLayoutEffect, useMemo, useState } from 'react';
2
+ import { LayoutChangeEvent, ScrollView, useWindowDimensions, View } from 'react-native';
3
+ import { Column, css, Modal, Paper, StyleSheet, useElevationStyle, useTheme } from '@fountain-ui/core';
4
4
  import { NamedStylesStringUnion, UseStyles } from '@fountain-ui/styles';
5
5
  import AnimatedY from '../AnimatedY';
6
6
  import type BottomSheetProps from './BottomSheetProps';
7
7
  import useDynamicSnapPoints from './useDynamicSnapPoints';
8
8
 
9
- type BottomSheetStyles = NamedStylesStringUnion<'root' | 'animated' | 'paper' | 'topElementLocation'>;
9
+ const createHeightLayoutHandler = (setHeight: React.Dispatch<React.SetStateAction<number>>) => {
10
+ return (event: LayoutChangeEvent) => {
11
+ const { height } = event.nativeEvent.layout;
12
+ setHeight(height);
13
+ };
14
+ }
15
+
16
+ type BottomSheetStyles = NamedStylesStringUnion<'root' | 'animated' | 'paper' | 'topElementLocation' | 'headerContainer' | 'headerBorder' | 'stickyBottomElement' | 'stickyBottomElementShadow'>;
10
17
 
11
18
  const useStyles: UseStyles<BottomSheetStyles> = function (): BottomSheetStyles {
12
19
  const theme = useTheme();
20
+ const stickyBottomElementShadow = useElevationStyle(8);
13
21
 
14
22
  return {
15
23
  root: {
@@ -34,26 +42,54 @@ const useStyles: UseStyles<BottomSheetStyles> = function (): BottomSheetStyles {
34
42
  bottom: 0,
35
43
  width: '100%',
36
44
  },
45
+ headerContainer: {
46
+ backgroundColor: theme.palette.paper.default,
47
+ },
48
+ headerBorder: {
49
+ borderBottomWidth: 0.5,
50
+ borderBottomColor: theme.palette.divider,
51
+ },
52
+ stickyBottomElement: {
53
+ position: 'absolute',
54
+ bottom: 0,
55
+ left: 0,
56
+ right: 0,
57
+ backgroundColor: theme.palette.paper.default,
58
+ },
59
+ stickyBottomElementShadow,
37
60
  };
38
61
  };
39
62
 
40
63
  export default function BottomSheet(props: BottomSheetProps) {
41
64
  const {
42
65
  backdropOpacity,
66
+ borderRadius,
43
67
  children,
44
68
  enableDynamicSizing = true,
45
69
  header,
70
+ stickyBottomElement,
46
71
  topElement,
47
72
  index,
48
73
  maxHeightNormalizedRatio = 0.9,
49
74
  onChange,
50
75
  snapPoints = [],
51
- disableDefaultBackgroundColor = false,
52
- disableDefaultShadow = false,
76
+ enableScrollableHeaderBorder = false,
53
77
  } = props;
54
78
 
55
79
  const styles = useStyles();
56
80
 
81
+ const { height: windowHeight } = useWindowDimensions();
82
+
83
+ const [topElementHeight, setTopElementHeight] = useState(0);
84
+ const [stickyBottomElementHeight, setStickyBottomElementHeight] = useState(0);
85
+ const [contentHeight, setContentHeight] = useState(0);
86
+ const [isScrollable, setIsScrollable] = useState(false);
87
+
88
+ const maxDynamicContentSize = Math.round(windowHeight * maxHeightNormalizedRatio) - topElementHeight;
89
+
90
+ const handleTopElementLayout = createHeightLayoutHandler(setTopElementHeight);
91
+ const handleStickyBottomElementLayout = createHeightLayoutHandler(setStickyBottomElementHeight);
92
+
57
93
  const handleClose = () => {
58
94
  if (onChange) {
59
95
  onChange(-1);
@@ -72,11 +108,37 @@ export default function BottomSheet(props: BottomSheetProps) {
72
108
 
73
109
  const translateY = highestSnapPoint - (convertedSnapPoints[index] ?? 0);
74
110
 
75
- const paperStyles = [
111
+ const adjustedContentHeight = useMemo(() => {
112
+ const adjustedHighestSnapPoint = highestSnapPoint + stickyBottomElementHeight;
113
+
114
+ return Math.min(maxDynamicContentSize, adjustedHighestSnapPoint);
115
+ }, [highestSnapPoint, stickyBottomElementHeight]);
116
+
117
+ const contentStyles = [
76
118
  styles.paper,
77
- { height: highestSnapPoint },
119
+ borderRadius ? { borderTopLeftRadius: borderRadius, borderTopRightRadius: borderRadius } : {},
120
+ { height: adjustedContentHeight, maxHeight: maxDynamicContentSize },
121
+ ];
122
+
123
+ const headerStyles = [
124
+ styles.headerContainer,
125
+ isScrollable && enableScrollableHeaderBorder ? styles.headerBorder : {},
126
+ ];
127
+
128
+ const stickyBottomElementStyles = [
129
+ styles.stickyBottomElement,
130
+ isScrollable ? styles.stickyBottomElementShadow : {},
78
131
  ];
79
132
 
133
+ useLayoutEffect(() => {
134
+ if (contentHeight === 0 || highestSnapPoint === 0) {
135
+ return;
136
+ }
137
+
138
+ const adjustedHighestSnapPoint = highestSnapPoint + stickyBottomElementHeight;
139
+ setIsScrollable(adjustedHighestSnapPoint > maxDynamicContentSize);
140
+ }, [contentHeight, highestSnapPoint, stickyBottomElementHeight, maxDynamicContentSize]);
141
+
80
142
  return (
81
143
  <Modal
82
144
  backdropOpacity={backdropOpacity}
@@ -90,25 +152,44 @@ export default function BottomSheet(props: BottomSheetProps) {
90
152
  >
91
153
  {topElement ? (
92
154
  <Column>
93
- <Column style={styles.topElementLocation}>
155
+ <Column
156
+ onLayout={handleTopElementLayout}
157
+ style={styles.topElementLocation}
158
+ >
94
159
  {topElement}
95
160
  </Column>
96
161
  </Column>
97
162
  ) : null}
98
163
 
99
164
  <Paper
100
- elevation={disableDefaultShadow ? 0 : 12}
101
- style={paperStyles}
102
- colorValue={disableDefaultBackgroundColor ? '#ffffff00' : undefined}
165
+ elevation={12}
166
+ style={contentStyles}
103
167
  >
104
168
  <ScrollView
105
- onContentSizeChange={handleContentSizeChange}
169
+ onContentSizeChange={(contentWidth: number, contentHeight: number) => {
170
+ setContentHeight(contentHeight);
171
+ handleContentSizeChange(contentWidth, contentHeight);
172
+ }}
106
173
  stickyHeaderIndices={header ? [0] : undefined}
174
+ style={{ paddingBottom: stickyBottomElementHeight }}
107
175
  >
108
- {header}
176
+ {header ? (
177
+ <View style={headerStyles}>
178
+ {header}
179
+ </View>
180
+ ): null}
109
181
 
110
182
  {children}
111
183
  </ScrollView>
184
+
185
+ {stickyBottomElement ? (
186
+ <Column
187
+ style={stickyBottomElementStyles}
188
+ onLayout={handleStickyBottomElementLayout}
189
+ >
190
+ {stickyBottomElement}
191
+ </Column>
192
+ ): null}
112
193
  </Paper>
113
194
  </AnimatedY>
114
195
  </Modal>