lupine.components 1.1.45 → 1.1.47
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/package.json +1 -1
- package/src/component-pool/floating-icon-menu/floating-icon-menu.tsx +24 -7
- package/src/components/mobile-components/mobile-side-menu.tsx +37 -17
- package/src/demo/demo-frame-helper.tsx +1 -1
- package/src/demo/demo-registry.ts +1 -1
- package/src/demo/demo-render-page.tsx +8 -3
- package/src/demo/mock/demo-icons.ts +1 -0
- package/src/demo/mock/side-menu-mock.tsx +1 -5
- package/src/demo/mock/user-settings-mock.tsx +0 -2
- package/src/frames/index.ts +1 -0
- package/src/{components → frames}/slider-frame-demo.tsx +34 -10
- package/src/frames/slider-frame.tsx +26 -93
- package/src/frames/slider-helper.tsx +192 -0
package/package.json
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
import { CssProps, RefProps, bindGlobalStyle, getGlobalStylesId } from 'lupine.web';
|
|
2
2
|
import { IconMenuItemProps } from '../../components/mobile-components/icon-menu-item-props';
|
|
3
3
|
|
|
4
|
+
export const FloatingIconSize = {
|
|
5
|
+
SmallSmall: { w: 30, h: 30 },
|
|
6
|
+
Small: { w: 43, h: 50 },
|
|
7
|
+
Medium: { w: 56, h: 56 },
|
|
8
|
+
Large: { w: 69, h: 69 },
|
|
9
|
+
LargeLarge: { w: 85, h: 85 },
|
|
10
|
+
};
|
|
11
|
+
export type FloatingIconSizeProps = {
|
|
12
|
+
w: number;
|
|
13
|
+
h: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
4
16
|
export interface FloatingIconMenuProps {
|
|
17
|
+
size?: FloatingIconSizeProps;
|
|
5
18
|
mainIcon: IconMenuItemProps;
|
|
6
19
|
items: IconMenuItemProps[];
|
|
7
20
|
className?: string;
|
|
@@ -9,16 +22,20 @@ export interface FloatingIconMenuProps {
|
|
|
9
22
|
left?: string;
|
|
10
23
|
right?: string;
|
|
11
24
|
bottom?: string;
|
|
25
|
+
textColor?: string;
|
|
26
|
+
backgroundColor?: string;
|
|
27
|
+
zIndex?: string;
|
|
12
28
|
direction?: 'up' | 'down' | 'left' | 'right';
|
|
13
29
|
}
|
|
14
30
|
|
|
15
31
|
export const FloatingIconMenu = (props: FloatingIconMenuProps) => {
|
|
32
|
+
const size = props.size || FloatingIconSize.Medium;
|
|
16
33
|
const css: CssProps = {
|
|
17
34
|
position: 'fixed',
|
|
18
35
|
display: 'flex',
|
|
19
36
|
alignItems: 'center',
|
|
20
37
|
gap: '16px',
|
|
21
|
-
zIndex: '
|
|
38
|
+
zIndex: props.zIndex || 'var(--layer-header-footer)',
|
|
22
39
|
|
|
23
40
|
'&.dir-up': { flexDirection: 'column-reverse' },
|
|
24
41
|
'&.dir-up .&-children': { flexDirection: 'column-reverse' },
|
|
@@ -33,11 +50,11 @@ export const FloatingIconMenu = (props: FloatingIconMenuProps) => {
|
|
|
33
50
|
'&.dir-right .&-children': { flexDirection: 'row' },
|
|
34
51
|
|
|
35
52
|
'.&-main-btn': {
|
|
36
|
-
width:
|
|
37
|
-
height:
|
|
53
|
+
width: `${size.w}px`,
|
|
54
|
+
height: `${size.h}px`,
|
|
38
55
|
borderRadius: '50%',
|
|
39
|
-
backgroundColor: 'var(--primary-color)',
|
|
40
|
-
color: 'var(--primary-bg-color)',
|
|
56
|
+
backgroundColor: props.backgroundColor || 'var(--primary-color)',
|
|
57
|
+
color: props.textColor || 'var(--primary-bg-color)',
|
|
41
58
|
display: 'flex',
|
|
42
59
|
justifyContent: 'center',
|
|
43
60
|
alignItems: 'center',
|
|
@@ -65,8 +82,8 @@ export const FloatingIconMenu = (props: FloatingIconMenuProps) => {
|
|
|
65
82
|
},
|
|
66
83
|
|
|
67
84
|
'.&-child-item': {
|
|
68
|
-
width:
|
|
69
|
-
height:
|
|
85
|
+
width: `${size.w - 8}px`,
|
|
86
|
+
height: `${size.h - 8}px`,
|
|
70
87
|
borderRadius: '50%',
|
|
71
88
|
backgroundColor: 'var(--primary-bg-color)',
|
|
72
89
|
color: 'var(--primary-color)',
|
|
@@ -59,6 +59,7 @@ export class MobileSideMenuHelper {
|
|
|
59
59
|
let touchstartX = 0;
|
|
60
60
|
let direction = '';
|
|
61
61
|
let moveStart = false;
|
|
62
|
+
let pendingOpen = false;
|
|
62
63
|
let isOpen = false;
|
|
63
64
|
let menuWidth = 0;
|
|
64
65
|
|
|
@@ -71,6 +72,7 @@ export class MobileSideMenuHelper {
|
|
|
71
72
|
touchstartX = e.touches[0].clientX;
|
|
72
73
|
direction = '';
|
|
73
74
|
moveStart = false;
|
|
75
|
+
pendingOpen = false;
|
|
74
76
|
isOpen = maskDom.classList.contains('show');
|
|
75
77
|
|
|
76
78
|
const menuDom = maskDom.querySelector('.mobile-side-menu') as HTMLDivElement;
|
|
@@ -87,35 +89,53 @@ export class MobileSideMenuHelper {
|
|
|
87
89
|
}
|
|
88
90
|
} else {
|
|
89
91
|
if (touchstartX < 40) {
|
|
90
|
-
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
menuDom.style.transition = 'none'; // Disable transition for 1:1 finger tracking
|
|
95
|
-
menuDom.style.transform = `translateX(-100%)`; // Start fully hidden left
|
|
96
|
-
}
|
|
92
|
+
// Do not start the drawer immediately on touchstart. A tap on a left-edge
|
|
93
|
+
// header button should still receive its normal click/touchend event.
|
|
94
|
+
// The drawer gesture is confirmed later in touchmove after a horizontal drag.
|
|
95
|
+
pendingOpen = true;
|
|
97
96
|
}
|
|
98
97
|
}
|
|
99
98
|
});
|
|
100
99
|
|
|
101
100
|
document.addEventListener('touchmove', (e) => {
|
|
102
|
-
if (!moveStart) {
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
101
|
const maskDom = document.querySelector('.mobile-side-menu-mask') as HTMLDivElement;
|
|
107
102
|
if (!maskDom) return;
|
|
108
103
|
|
|
109
104
|
const currentX = e.touches[0].clientX;
|
|
105
|
+
const currentY = e.touches[0].clientY;
|
|
110
106
|
const deltaX = currentX - touchstartX;
|
|
107
|
+
const deltaY = currentY - touchstartY;
|
|
111
108
|
|
|
112
|
-
if (
|
|
113
|
-
if (
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
|
|
109
|
+
if (!moveStart) {
|
|
110
|
+
if (!pendingOpen) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Confirm the side-menu gesture only after the finger clearly drags
|
|
115
|
+
// right horizontally. Simple taps on left-edge buttons will fall through.
|
|
116
|
+
if (Math.abs(deltaX) < 8 && Math.abs(deltaY) < 8) {
|
|
117
117
|
return;
|
|
118
118
|
}
|
|
119
|
+
if (deltaX <= 0 || Math.abs(deltaX) <= Math.abs(deltaY)) {
|
|
120
|
+
pendingOpen = false;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
moveStart = true;
|
|
125
|
+
pendingOpen = false;
|
|
126
|
+
maskDom.classList.add('show');
|
|
127
|
+
|
|
128
|
+
const menuDom = maskDom.querySelector('.mobile-side-menu') as HTMLDivElement;
|
|
129
|
+
if (menuDom) {
|
|
130
|
+
menuDom.style.transition = 'none'; // Disable transition for 1:1 finger tracking
|
|
131
|
+
menuDom.style.transform = `translateX(-100%)`; // Start fully hidden left
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
|
|
137
|
+
if (direction === '') {
|
|
138
|
+
direction = 'X';
|
|
119
139
|
}
|
|
120
140
|
|
|
121
141
|
const menuDom = maskDom.querySelector('.mobile-side-menu') as HTMLDivElement;
|
|
@@ -144,7 +164,7 @@ export class MobileSideMenuHelper {
|
|
|
144
164
|
}
|
|
145
165
|
}
|
|
146
166
|
}
|
|
147
|
-
});
|
|
167
|
+
}, { passive: false });
|
|
148
168
|
|
|
149
169
|
document.addEventListener('touchend', (e) => {
|
|
150
170
|
if (!moveStart) return;
|
|
@@ -29,7 +29,7 @@ import { toggleButtonDemo } from '../components/toggle-button-demo';
|
|
|
29
29
|
import { messageBoxDemo } from '../components/message-box-demo';
|
|
30
30
|
import { loadingSpinDemo } from '../components/loading-spin-demo';
|
|
31
31
|
import { mobileSideMenuDemo } from '../components/mobile-components/mobile-side-menu-demo';
|
|
32
|
-
import { sliderFrameDemo } from '../
|
|
32
|
+
import { sliderFrameDemo } from '../frames/slider-frame-demo';
|
|
33
33
|
import { rangeDemo, gaugeDemo } from '../component-pool/range';
|
|
34
34
|
import { badgeDemo } from '../component-pool/badge';
|
|
35
35
|
import { timelineDemo } from '../component-pool/timeline';
|
|
@@ -27,7 +27,7 @@ import { messageBoxDemo } from '../components/message-box-demo';
|
|
|
27
27
|
import { loadingSpinDemo } from '../components/loading-spin-demo';
|
|
28
28
|
import { mobileSideMenuDemo } from '../components/mobile-components/mobile-side-menu-demo';
|
|
29
29
|
import { responsiveFrameDemo } from '../frames/responsive-frame-demo';
|
|
30
|
-
import { sliderFrameDemo } from '../
|
|
30
|
+
import { sliderFrameDemo } from '../frames/slider-frame-demo';
|
|
31
31
|
import { carouselDemo } from '../component-pool/carousel';
|
|
32
32
|
import { rangeDemo, gaugeDemo } from '../component-pool/range';
|
|
33
33
|
import { cascaderDemo } from '../component-pool/cascader';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { bindGlobalStyle, CssProps, encodeHtml, HtmlVar, isFrontEnd, PageProps } from 'lupine.components';
|
|
1
|
+
import { bindGlobalStyle, CssProps, encodeHtml, HtmlVar, isFrontEnd, MediaQueryMaxWidth, PageProps } from 'lupine.components';
|
|
2
2
|
import { demoRegistry } from './demo-registry';
|
|
3
3
|
import { demoIconsCss } from './mock/demo-icons';
|
|
4
4
|
|
|
@@ -65,12 +65,16 @@ export const DemoRenderPage = async (props: PageProps) => {
|
|
|
65
65
|
width: '100%',
|
|
66
66
|
height: '100%',
|
|
67
67
|
// Reset any body margins if they exist, though typically handled by global css
|
|
68
|
-
margin: 0,
|
|
68
|
+
margin: '0 auto',
|
|
69
69
|
boxSizing: 'border-box',
|
|
70
70
|
display: 'flex',
|
|
71
71
|
justifyContent: 'center',
|
|
72
72
|
alignItems: 'center', // Center components by default
|
|
73
73
|
overflow: 'auto',
|
|
74
|
+
transform: 'translateX(0)', // Traps position: fixed children inside this container's dimensions
|
|
75
|
+
maxWidth: MediaQueryMaxWidth.DesktopMax,
|
|
76
|
+
borderRight: '1px solid var(--primary-border-color)',
|
|
77
|
+
borderLeft: '1px solid var(--primary-border-color)',
|
|
74
78
|
'>fragment>div': {
|
|
75
79
|
width: '100%',
|
|
76
80
|
height: '100%',
|
|
@@ -80,7 +84,8 @@ export const DemoRenderPage = async (props: PageProps) => {
|
|
|
80
84
|
bindGlobalStyle('demo-icons', demoIconsCss, false, true);
|
|
81
85
|
|
|
82
86
|
return (
|
|
83
|
-
|
|
87
|
+
// responsive-frame is for containning slider-frame
|
|
88
|
+
<div css={css} class='demo-render-page responsive-frame'>
|
|
84
89
|
{dom.node}
|
|
85
90
|
</div>
|
|
86
91
|
);
|
|
@@ -39,6 +39,7 @@ export const demoIcons = {
|
|
|
39
39
|
'ma-pencil-outline': `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z'/%3E%3C/svg%3E`,
|
|
40
40
|
'ic-play': `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' width='24' height='24'%3E%3Cpath fill='currentColor' d='M20 0 L20 100 L90 50 Z'/%3E%3C/svg%3E`,
|
|
41
41
|
'ic-pause': `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' width='24' height='24'%3E%3Cpath fill='currentColor' d='M0 0 L0 100 L33.33 100 L33.33 0 Z M66.66 0 L66.66 100 L100 100 L100 0 Z'/%3E%3C/svg%3E`,
|
|
42
|
+
'ma-cog-outline': `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z'/%3E%3Cpath fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1Z'/%3E%3C/svg%3E`,
|
|
42
43
|
};
|
|
43
44
|
|
|
44
45
|
export const demoIconsCss: CssProps = {
|
|
@@ -17,11 +17,7 @@ const sideMenuMockCss: CssProps = {
|
|
|
17
17
|
borderBottom: '1px solid var(--primary-border-color)',
|
|
18
18
|
},
|
|
19
19
|
'.msm-header-icon': {
|
|
20
|
-
width: '32px',
|
|
21
|
-
height: '32px',
|
|
22
20
|
marginRight: '12px',
|
|
23
|
-
borderRadius: '4px',
|
|
24
|
-
backgroundColor: 'var(--primary-color)',
|
|
25
21
|
},
|
|
26
22
|
'.msm-header-title': {
|
|
27
23
|
fontSize: '18px',
|
|
@@ -90,7 +86,7 @@ export const SideMenuMock = ({
|
|
|
90
86
|
return (
|
|
91
87
|
<div ref={ref} style={{ display: 'flex', flexDirection: 'column', flex: 1, height: '100%' }}>
|
|
92
88
|
<div class='msm-header'>
|
|
93
|
-
<div class='msm-header-icon'></div>
|
|
89
|
+
<div class='msm-header-icon'><i class='ifc-icon ma-home-outline'></i></div>
|
|
94
90
|
<div class='msm-header-title'>{title}</div>
|
|
95
91
|
</div>
|
|
96
92
|
|
|
@@ -153,7 +153,6 @@ const NestedDemoMockContent = () => {
|
|
|
153
153
|
};
|
|
154
154
|
|
|
155
155
|
export const NestedDemoMock = (props: { sliderFrameHook: SliderFrameHookProps }) => {
|
|
156
|
-
props.sliderFrameHook.addClass!('desktop-slide-right');
|
|
157
156
|
|
|
158
157
|
return (
|
|
159
158
|
<HeaderWithBackFrame
|
|
@@ -297,7 +296,6 @@ const UserSettingsMockContent = () => {
|
|
|
297
296
|
};
|
|
298
297
|
|
|
299
298
|
export const UserSettingsMock = (props: { sliderFrameHook: SliderFrameHookProps }) => {
|
|
300
|
-
props.sliderFrameHook.addClass!('desktop-slide-left');
|
|
301
299
|
|
|
302
300
|
return (
|
|
303
301
|
<HeaderWithBackFrame
|
package/src/frames/index.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { DemoStory } from '../demo/demo-types';
|
|
2
|
-
import { SliderFrame, SliderFrameHookProps } from '
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { SliderFrame, SliderFrameHookProps } from './slider-frame';
|
|
3
|
+
import { SliderHelper, SliderHelperCloseProps } from './slider-helper';
|
|
4
|
+
import { HeaderWithBackFrame } from '../components/mobile-components/mobile-header-with-back';
|
|
5
|
+
import { Button, ButtonSize } from '../components/button';
|
|
5
6
|
|
|
6
7
|
// A dummy component to load inside the slider
|
|
7
8
|
const DummyInnerPage = ({ hook }: { hook: SliderFrameHookProps }) => {
|
|
@@ -38,13 +39,36 @@ export const sliderFrameDemo: DemoStory<any> = {
|
|
|
38
39
|
<p style={{ color: '#666', marginBottom: '20px' }}>
|
|
39
40
|
This demo shows how to use SliderFrame to slide in a new full-cover page over the current view.
|
|
40
41
|
</p>
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '10px' }}>
|
|
43
|
+
<Button
|
|
44
|
+
text='Open Slider'
|
|
45
|
+
size={ButtonSize.Medium}
|
|
46
|
+
onClick={() => {
|
|
47
|
+
sliderHook.load!(<DummyInnerPage hook={sliderHook} />);
|
|
48
|
+
}}
|
|
49
|
+
/>
|
|
50
|
+
<Button
|
|
51
|
+
text='Open Slider Helper'
|
|
52
|
+
size={ButtonSize.Medium}
|
|
53
|
+
onClick={async () => {
|
|
54
|
+
let close: SliderHelperCloseProps;
|
|
55
|
+
close = await SliderHelper.show({
|
|
56
|
+
direction: args.direction,
|
|
57
|
+
children: (
|
|
58
|
+
<HeaderWithBackFrame title='Slider Helper Inner Page' onBack={() => close()}>
|
|
59
|
+
<div style={{ padding: '20px', display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
|
60
|
+
<h3>This is the SliderHelper inner page content!</h3>
|
|
61
|
+
<p>It uses a static show function and shows a mask on larger screens.</p>
|
|
62
|
+
<div>
|
|
63
|
+
<Button text='Close SliderHelper via Button' size={ButtonSize.Medium} onClick={() => close()} />
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</HeaderWithBackFrame>
|
|
67
|
+
),
|
|
68
|
+
});
|
|
69
|
+
}}
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
48
72
|
|
|
49
73
|
{/* The SliderFrame is placed here but initially hidden */}
|
|
50
74
|
<SliderFrame hook={sliderHook} direction={args.direction} defaultContent='' />
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
</div>
|
|
13
13
|
);
|
|
14
14
|
*/
|
|
15
|
-
import { VNode,
|
|
15
|
+
import { VNode, stopPropagation } from 'lupine.components';
|
|
16
|
+
import { SliderHelper, SliderHelperCloseProps } from './slider-helper';
|
|
16
17
|
|
|
17
18
|
// addClass(SliderFramePosition) is used to show two SliderFrames for big screens,
|
|
18
19
|
// so when the second is showing, it needs to set this on the first one
|
|
@@ -30,107 +31,39 @@ export type SliderFrameProps = {
|
|
|
30
31
|
hook?: SliderFrameHookProps;
|
|
31
32
|
afterClose?: () => void | Promise<void>;
|
|
32
33
|
};
|
|
34
|
+
// deprecated
|
|
33
35
|
export const SliderFrame = (props: SliderFrameProps) => {
|
|
36
|
+
let closeSlider: SliderHelperCloseProps | undefined;
|
|
37
|
+
let opened = false;
|
|
38
|
+
let className = '';
|
|
39
|
+
|
|
34
40
|
if (props.hook) {
|
|
35
41
|
props.hook.load = (children) => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
opened = true;
|
|
43
|
+
SliderHelper.show({
|
|
44
|
+
direction: props.direction || 'right',
|
|
45
|
+
children,
|
|
46
|
+
className,
|
|
47
|
+
closeEvent: () => {
|
|
48
|
+
opened = false;
|
|
49
|
+
closeSlider = undefined;
|
|
50
|
+
props.afterClose?.();
|
|
51
|
+
},
|
|
52
|
+
}).then((close) => {
|
|
53
|
+
closeSlider = close;
|
|
54
|
+
});
|
|
41
55
|
};
|
|
42
56
|
props.hook.close = (event: Event) => {
|
|
43
57
|
stopPropagation(event);
|
|
44
|
-
|
|
45
|
-
setTimeout(async () => {
|
|
46
|
-
ref.current?.classList.add('d-none');
|
|
47
|
-
dom.value = '';
|
|
48
|
-
if (props.afterClose) {
|
|
49
|
-
await props.afterClose();
|
|
50
|
-
}
|
|
51
|
-
}, 400);
|
|
58
|
+
closeSlider?.();
|
|
52
59
|
};
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
// deprecated
|
|
61
|
+
props.hook.addClass = (newClassName) => {
|
|
62
|
+
className = [className, newClassName].join(' ').trim();
|
|
55
63
|
};
|
|
56
64
|
props.hook.isOpened = () => {
|
|
57
|
-
return
|
|
65
|
+
return opened;
|
|
58
66
|
};
|
|
59
67
|
}
|
|
60
|
-
|
|
61
|
-
const ref: RefProps = {
|
|
62
|
-
onLoad: async (el: Element) => {
|
|
63
|
-
// Keep fixed sliders out of padded/top-frame layout containers on iOS.
|
|
64
|
-
// iOS WebView can treat fixed children inside safe-area padded app frames inconsistently;
|
|
65
|
-
// mounting the slider at body level also makes z-index compare globally.
|
|
66
|
-
if (isFrontEnd()) {
|
|
67
|
-
const root = (window.parent as any).document.querySelector('.lupine-root') as HTMLElement;
|
|
68
|
-
if (root && el.parentElement !== root) {
|
|
69
|
-
root.appendChild(el);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
},
|
|
73
|
-
};
|
|
74
|
-
const css: CssProps = {
|
|
75
|
-
display: 'flex',
|
|
76
|
-
flexDirection: 'column',
|
|
77
|
-
position: 'fixed',
|
|
78
|
-
top: '0',
|
|
79
|
-
left: 'var(--auto-sidemenu-left-offset, 0px)',
|
|
80
|
-
right: '0',
|
|
81
|
-
bottom: '0',
|
|
82
|
-
zIndex: 'var(--layer-slider)',
|
|
83
|
-
transform: props.direction === 'bottom' ? 'translateY(100%)' : 'translateX(100%)',
|
|
84
|
-
transition: 'transform 0.4s ease-in-out',
|
|
85
|
-
backgroundColor: 'var(--primary-bg-color)',
|
|
86
|
-
// trick: to put two padding-top properties
|
|
87
|
-
'padding-top ': 'constant(safe-area-inset-top)',
|
|
88
|
-
'padding-top': 'env(safe-area-inset-top)',
|
|
89
|
-
'&.show': {
|
|
90
|
-
transform: props.direction === 'bottom' ? 'translateY(0)' : 'translateX(0)',
|
|
91
|
-
},
|
|
92
|
-
'& > *': {
|
|
93
|
-
'--auto-sidemenu-left-offset': '0px',
|
|
94
|
-
},
|
|
95
|
-
'& > fragment': {
|
|
96
|
-
height: '100%',
|
|
97
|
-
'--auto-sidemenu-left-offset': '0px',
|
|
98
|
-
},
|
|
99
|
-
'&.desktop-slide-left': {
|
|
100
|
-
[MediaQueryRange.TabletAbove]: {
|
|
101
|
-
'.header-back-content': {
|
|
102
|
-
width: '30%',
|
|
103
|
-
},
|
|
104
|
-
},
|
|
105
|
-
},
|
|
106
|
-
'&.desktop-slide-right': {
|
|
107
|
-
[MediaQueryRange.TabletAbove]: {
|
|
108
|
-
top: '59px', // Just below DesktopHeader
|
|
109
|
-
left: 'calc(max(var(--auto-sidemenu-left-offset, 0px), 30%))',
|
|
110
|
-
transform: 'translateX(0)',
|
|
111
|
-
// notice: here is connected with mobile-header-title-icon.tsx
|
|
112
|
-
'.mobile-header-title-icon-top': {
|
|
113
|
-
width: '100%',
|
|
114
|
-
boxShadow: 'unset',
|
|
115
|
-
},
|
|
116
|
-
'.header-back-content': {
|
|
117
|
-
width: '100%',
|
|
118
|
-
},
|
|
119
|
-
'.mhti-title': {
|
|
120
|
-
fontSize: '15px',
|
|
121
|
-
},
|
|
122
|
-
'.mhti-left, .mhti-right': {
|
|
123
|
-
display: 'none',
|
|
124
|
-
},
|
|
125
|
-
'&.d-none': {
|
|
126
|
-
display: 'unset !important',
|
|
127
|
-
},
|
|
128
|
-
},
|
|
129
|
-
},
|
|
130
|
-
};
|
|
131
|
-
return (
|
|
132
|
-
<div ref={ref} css={css} class='slider-frame d-none'>
|
|
133
|
-
{dom.node}
|
|
134
|
-
</div>
|
|
135
|
-
);
|
|
68
|
+
return <></>;
|
|
136
69
|
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { CssProps, RefProps, VNode, mountInnerComponent } from 'lupine.web';
|
|
2
|
+
import { backActionHelper, stopPropagation } from '../lib';
|
|
3
|
+
import { MediaQueryMaxWidth, MediaQueryRange } from '../styles';
|
|
4
|
+
|
|
5
|
+
export type SliderHelperDirection = 'right' | 'left' | 'bottom' | 'top';
|
|
6
|
+
export type SliderHelperCloseProps = () => void;
|
|
7
|
+
|
|
8
|
+
export type SliderHelperShowProps = {
|
|
9
|
+
children: VNode<any>;
|
|
10
|
+
direction?: SliderHelperDirection;
|
|
11
|
+
closeEvent?: () => void;
|
|
12
|
+
closeWhenClickOutside?: boolean;
|
|
13
|
+
zIndex?: string;
|
|
14
|
+
maxWidth?: string;
|
|
15
|
+
className?: string;
|
|
16
|
+
contentClassName?: string;
|
|
17
|
+
mountTarget?: HTMLElement;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export class SliderHelper {
|
|
21
|
+
static async show({
|
|
22
|
+
children,
|
|
23
|
+
direction = 'right',
|
|
24
|
+
closeEvent,
|
|
25
|
+
closeWhenClickOutside = true,
|
|
26
|
+
zIndex,
|
|
27
|
+
maxWidth = MediaQueryMaxWidth.MobileMax,
|
|
28
|
+
className = '',
|
|
29
|
+
contentClassName = '',
|
|
30
|
+
mountTarget,
|
|
31
|
+
}: SliderHelperShowProps): Promise<SliderHelperCloseProps> {
|
|
32
|
+
const base = document.createElement('div');
|
|
33
|
+
const isHorizontal = direction === 'left' || direction === 'right';
|
|
34
|
+
const isFromEnd = direction === 'right' || direction === 'bottom';
|
|
35
|
+
const closeClassName = isFromEnd ? 'close-to-end' : 'close-to-start';
|
|
36
|
+
|
|
37
|
+
let closed = false;
|
|
38
|
+
const handleClose = () => {
|
|
39
|
+
if (closed) return;
|
|
40
|
+
closed = true;
|
|
41
|
+
closeEvent?.();
|
|
42
|
+
ref.current?.classList.remove('show');
|
|
43
|
+
ref.current?.classList.add(closeClassName);
|
|
44
|
+
setTimeout(() => {
|
|
45
|
+
base.remove();
|
|
46
|
+
}, 400);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const onClickContainer = (event: MouseEvent) => {
|
|
50
|
+
if (closeWhenClickOutside !== false && event.target === ref.current) {
|
|
51
|
+
handleClose();
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const onClickContent = (event: MouseEvent) => {
|
|
56
|
+
stopPropagation(event);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const ref: RefProps = {
|
|
60
|
+
onLoad: async () => {
|
|
61
|
+
// Make sure the browser has painted the initial transform state before adding show,
|
|
62
|
+
// otherwise rapid close/open can skip the transition and display directly.
|
|
63
|
+
ref.current?.classList.remove('show', 'close-to-end', 'close-to-start');
|
|
64
|
+
ref.current?.getBoundingClientRect();
|
|
65
|
+
requestAnimationFrame(() => {
|
|
66
|
+
requestAnimationFrame(() => {
|
|
67
|
+
ref.current?.classList.add('show');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const startTransform = isHorizontal
|
|
74
|
+
? direction === 'right'
|
|
75
|
+
? 'translateX(100%)'
|
|
76
|
+
: 'translateX(-100%)'
|
|
77
|
+
: direction === 'bottom'
|
|
78
|
+
? 'translateY(100%)'
|
|
79
|
+
: 'translateY(-100%)';
|
|
80
|
+
const showTransform = 'translate(0, 0)';
|
|
81
|
+
const endTransform = isHorizontal
|
|
82
|
+
? direction === 'right'
|
|
83
|
+
? 'translateX(100%)'
|
|
84
|
+
: 'translateX(-100%)'
|
|
85
|
+
: direction === 'bottom'
|
|
86
|
+
? 'translateY(100%)'
|
|
87
|
+
: 'translateY(-100%)';
|
|
88
|
+
const oppositeTransform = isHorizontal
|
|
89
|
+
? direction === 'right'
|
|
90
|
+
? 'translateX(-100%)'
|
|
91
|
+
: 'translateX(100%)'
|
|
92
|
+
: direction === 'bottom'
|
|
93
|
+
? 'translateY(-100%)'
|
|
94
|
+
: 'translateY(100%)';
|
|
95
|
+
|
|
96
|
+
const contentCss: CssProps = isHorizontal
|
|
97
|
+
? {
|
|
98
|
+
top: '0',
|
|
99
|
+
bottom: '0',
|
|
100
|
+
width: '100%',
|
|
101
|
+
maxWidth,
|
|
102
|
+
[direction]: '0',
|
|
103
|
+
}
|
|
104
|
+
: {
|
|
105
|
+
top: direction === 'top' ? '0' : 'auto',
|
|
106
|
+
bottom: direction === 'bottom' ? '0' : 'auto',
|
|
107
|
+
left: 'auto',
|
|
108
|
+
right: '0',
|
|
109
|
+
width: '100%',
|
|
110
|
+
maxWidth,
|
|
111
|
+
height: '100%',
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const cssContainer: CssProps = {
|
|
115
|
+
display: 'flex',
|
|
116
|
+
position: 'fixed',
|
|
117
|
+
top: '0',
|
|
118
|
+
left: '0',
|
|
119
|
+
right: '0',
|
|
120
|
+
bottom: '0',
|
|
121
|
+
zIndex: zIndex || 'var(--layer-slider)',
|
|
122
|
+
overflow: 'hidden',
|
|
123
|
+
backgroundColor: '#00000000',
|
|
124
|
+
transition: 'background-color 0.4s ease-in-out',
|
|
125
|
+
pointerEvents: 'auto',
|
|
126
|
+
'&.show': {
|
|
127
|
+
[MediaQueryRange.MobileAbove]: {
|
|
128
|
+
backgroundColor: 'var(--cover-mask-bg-color)',
|
|
129
|
+
},
|
|
130
|
+
'.slider-helper-content': {
|
|
131
|
+
transform: showTransform,
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
'&.close-to-end': {
|
|
135
|
+
'.slider-helper-content': {
|
|
136
|
+
transform: endTransform,
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
'&.close-to-start': {
|
|
140
|
+
'.slider-helper-content': {
|
|
141
|
+
transform: oppositeTransform,
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
'.slider-helper-content': {
|
|
145
|
+
display: 'flex',
|
|
146
|
+
flexDirection: 'column',
|
|
147
|
+
position: 'fixed',
|
|
148
|
+
boxSizing: 'border-box',
|
|
149
|
+
overflowX: 'hidden',
|
|
150
|
+
overflowY: 'hidden',
|
|
151
|
+
scrollbarWidth: 'none',
|
|
152
|
+
color: 'var(--primary-color)',
|
|
153
|
+
backgroundColor: 'var(--primary-bg-color)',
|
|
154
|
+
boxShadow: 'var(--cover-box-shadow)',
|
|
155
|
+
transform: startTransform,
|
|
156
|
+
transition: 'transform 0.4s ease-in-out',
|
|
157
|
+
// trick: to put two padding-top properties
|
|
158
|
+
'padding-top ': 'constant(safe-area-inset-top)',
|
|
159
|
+
'padding-top': 'env(safe-area-inset-top)',
|
|
160
|
+
...contentCss,
|
|
161
|
+
'&::-webkit-scrollbar': {
|
|
162
|
+
display: 'none',
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const component = (
|
|
168
|
+
<div
|
|
169
|
+
css={cssContainer}
|
|
170
|
+
ref={ref}
|
|
171
|
+
class={['slider-helper-box', `from-${direction}`, className].join(' ').trim()}
|
|
172
|
+
onClick={onClickContainer}
|
|
173
|
+
>
|
|
174
|
+
<div
|
|
175
|
+
class={['slider-helper-content', contentClassName].join(' ').trim()}
|
|
176
|
+
onClick={onClickContent}
|
|
177
|
+
data-back-action={backActionHelper.genBackActionId()}
|
|
178
|
+
>
|
|
179
|
+
{children}
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
base.style.position = 'fixed';
|
|
185
|
+
base.style.inset = '0';
|
|
186
|
+
base.style.zIndex = zIndex || 'var(--layer-slider)';
|
|
187
|
+
const host = mountTarget || document.querySelector('.responsive-frame') || document.querySelector('.lupine-root') || document.body;
|
|
188
|
+
host.appendChild(base);
|
|
189
|
+
await mountInnerComponent(base, component);
|
|
190
|
+
return handleClose;
|
|
191
|
+
}
|
|
192
|
+
}
|