@startupjs-ui/modal 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ # Change Log
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
+
6
+ ## [0.1.3](https://github.com/startupjs/startupjs-ui/compare/v0.1.2...v0.1.3) (2025-12-29)
7
+
8
+ **Note:** Version bump only for package @startupjs-ui/modal
9
+
10
+
11
+
12
+
13
+
14
+ ## [0.1.2](https://github.com/startupjs/startupjs-ui/compare/v0.1.1...v0.1.2) (2025-12-29)
15
+
16
+
17
+ ### Features
18
+
19
+ * add mdx and docs packages. Refactor docs to get rid of any @startupjs/ui usage and use startupjs-ui instead ([703c926](https://github.com/startupjs/startupjs-ui/commit/703c92636efb0421ffd11783f692fc892b74018f))
20
+ * **modal:** refactor Modal component ([e5d2901](https://github.com/startupjs/startupjs-ui/commit/e5d29013594b1898df339acfd4450450e8625bb9))
@@ -0,0 +1,7 @@
1
+ $gutter = $UI.gutters.m
2
+
3
+ .root
4
+ padding $gutter
5
+
6
+ .action
7
+ margin-left 1u
@@ -0,0 +1,56 @@
1
+ import React, { type ReactNode } from 'react'
2
+ import { type StyleProp, type ViewStyle } from 'react-native'
3
+ import { pug, observer } from 'startupjs'
4
+ import { themed } from '@startupjs-ui/core'
5
+ import Div from '@startupjs-ui/div'
6
+ import Button from '@startupjs-ui/button'
7
+ import './index.cssx.styl'
8
+
9
+ export const DEFAULT_CANCEL_LABEL = 'Cancel'
10
+ export const DEFAULT_CONFIRM_LABEL = 'Confirm'
11
+
12
+ export const _PropsJsonSchema = {/* ModalActionsProps */}
13
+
14
+ export interface ModalActionsProps {
15
+ /** Custom styles applied to the actions container */
16
+ style?: StyleProp<ViewStyle>
17
+ /** Custom actions content */
18
+ children?: ReactNode
19
+ /** Text for cancel button @default 'Cancel' */
20
+ cancelLabel?: string
21
+ /** Text for confirm button @default 'Confirm' */
22
+ confirmLabel?: string
23
+ /** Cancel button handler */
24
+ onCancel?: (event: any) => void | Promise<void>
25
+ /** Confirm button handler */
26
+ onConfirm?: (event: any) => void | Promise<void>
27
+ }
28
+
29
+ function ModalActions ({
30
+ style,
31
+ children,
32
+ cancelLabel = DEFAULT_CANCEL_LABEL,
33
+ confirmLabel = DEFAULT_CONFIRM_LABEL,
34
+ onCancel,
35
+ onConfirm
36
+ }: ModalActionsProps): ReactNode {
37
+ return pug`
38
+ Div.root(row style=style align='right')
39
+ if children
40
+ = children
41
+ else
42
+ if onCancel
43
+ Button.action(
44
+ color='primary'
45
+ onPress=onCancel
46
+ )= cancelLabel
47
+ if onConfirm
48
+ Button.action(
49
+ color='primary'
50
+ variant='flat'
51
+ onPress=onConfirm
52
+ )= confirmLabel
53
+ `
54
+ }
55
+
56
+ export default observer(themed('ModalActions', ModalActions))
@@ -0,0 +1,2 @@
1
+ .root
2
+ padding 2u
@@ -0,0 +1,45 @@
1
+ import React, { type ReactNode, type ComponentType } from 'react'
2
+ import { type StyleProp, type ViewStyle } from 'react-native'
3
+ import { pug, observer } from 'startupjs'
4
+ import { themed } from '@startupjs-ui/core'
5
+ import Span from '@startupjs-ui/span'
6
+ import ScrollView from '@startupjs-ui/scroll-view'
7
+ import './index.cssx.styl'
8
+
9
+ export const _PropsJsonSchema = {/* ModalContentProps */}
10
+
11
+ export interface ModalContentProps {
12
+ /** Custom styles applied to the content */
13
+ style?: StyleProp<ViewStyle>
14
+ /** Content rendered inside modal */
15
+ children?: ReactNode
16
+ /** Component used for wrapping content @default ScrollView */
17
+ ContentComponent?: ComponentType<any>
18
+ }
19
+
20
+ function ModalContent ({
21
+ style,
22
+ children,
23
+ ContentComponent,
24
+ ...props
25
+ }: ModalContentProps): ReactNode {
26
+ const content = React.Children.map(children, (child): ReactNode => {
27
+ if (typeof child === 'string') {
28
+ return pug`
29
+ Span= child
30
+ `
31
+ }
32
+ return child
33
+ })
34
+
35
+ if (!ContentComponent) ContentComponent = ScrollView
36
+
37
+ return pug`
38
+ ContentComponent.root(
39
+ style=style
40
+ ...props
41
+ )= content
42
+ `
43
+ }
44
+
45
+ export default observer(themed('ModalContent', ModalContent))
@@ -0,0 +1,20 @@
1
+ $gutter = $UI.gutters.m
2
+
3
+ .root
4
+ padding $gutter
5
+
6
+ &.between
7
+ justify-content space-between
8
+
9
+ &.right
10
+ justify-content flex-end
11
+
12
+ .title
13
+ font(h5)
14
+ flex 1
15
+
16
+ .close
17
+ margin-left 2u
18
+
19
+ .icon
20
+ color: var(--color-text-description)
@@ -0,0 +1,49 @@
1
+ import React, { type ReactNode } from 'react'
2
+ import { type StyleProp, type ViewStyle } from 'react-native'
3
+ import { pug, observer } from 'startupjs'
4
+ import { themed } from '@startupjs-ui/core'
5
+ import Div from '@startupjs-ui/div'
6
+ import Icon from '@startupjs-ui/icon'
7
+ import Span from '@startupjs-ui/span'
8
+ import { faTimes } from '@fortawesome/free-solid-svg-icons/faTimes'
9
+ import './index.cssx.styl'
10
+
11
+ export const _PropsJsonSchema = {/* ModalHeaderProps */}
12
+
13
+ export interface ModalHeaderProps {
14
+ /** Custom styles applied to the header container */
15
+ style?: StyleProp<ViewStyle>
16
+ /** Header content */
17
+ children?: ReactNode
18
+ /** Handler for closing cross @private */
19
+ onCrossPress?: (event: any) => void
20
+ /** Icon rendered inside close button @default faTimes */
21
+ closeIcon?: object
22
+ /** Style applied to the close icon */
23
+ iconStyle?: StyleProp<ViewStyle>
24
+ }
25
+
26
+ function ModalHeader ({
27
+ style,
28
+ children,
29
+ onCrossPress, // @private
30
+ closeIcon = faTimes,
31
+ iconStyle
32
+ }: ModalHeaderProps): ReactNode {
33
+ return pug`
34
+ Div.root(row style=style styleName=children ? 'between' : 'right' vAlign='center')
35
+ if typeof children === 'string'
36
+ Span.title(numberOfLines=1)= children
37
+ else
38
+ = children
39
+ if onCrossPress
40
+ Div.close(onPress=onCrossPress)
41
+ Icon.icon(
42
+ style=iconStyle
43
+ icon=closeIcon
44
+ size='xl'
45
+ )
46
+ `
47
+ }
48
+
49
+ export default observer(themed('ModalHeader', ModalHeader))
package/README.mdx ADDED
@@ -0,0 +1,204 @@
1
+ import { useState, useRef } from 'react'
2
+ import { $ } from 'startupjs'
3
+ import { Sandbox } from '@startupjs-ui/docs'
4
+ import Modal, {
5
+ _PropsJsonSchema as ModalPropsJsonSchema
6
+ } from './index'
7
+ import { _PropsJsonSchema as ModalHeaderPropsJsonSchema } from './ModalHeader'
8
+ import { _PropsJsonSchema as ModalContentPropsJsonSchema } from './ModalContent'
9
+ import { _PropsJsonSchema as ModalActionsPropsJsonSchema } from './ModalActions'
10
+ import Button from '@startupjs-ui/button'
11
+ import Span from '@startupjs-ui/span'
12
+ import { Table, Thead, Tbody, Tr, Th, Td } from '@startupjs-ui/table'
13
+ import './index.mdx.cssx.styl'
14
+
15
+ # Modal
16
+
17
+ Inherits [React Native Modal](https://reactnative.dev/docs/modal).
18
+
19
+ Modal can be used when the user needs to inform about critical information, require decisions or interact a complex sub-application without navigating to a new page or interrupting the workflow.
20
+
21
+ ```js
22
+ import { Modal } from 'startupjs-ui'
23
+ ```
24
+
25
+ ## Simple example
26
+
27
+ ```jsx example
28
+ const [visible, setVisible] = useState(false)
29
+
30
+ return (
31
+ <>
32
+ <Modal
33
+ visible={visible}
34
+ title='Text in modal'
35
+ onRequestClose={setVisible}
36
+ >
37
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit.
38
+ </Modal>
39
+ <Button onPress={() => setVisible(true)}>
40
+ Open modal
41
+ </Button>
42
+ </>
43
+ )
44
+ ```
45
+
46
+ ## Managing visibility
47
+
48
+ There are three options for managing visiblity of a modal:
49
+
50
+ 1. By passing the scoped model to the `$visible` property from the state of which visibility is controlled.
51
+
52
+ ```jsx example
53
+ const $visible = $()
54
+
55
+ return (
56
+ <React.Fragment>
57
+ <Button onPress={() => $visible.set(true)}>
58
+ Open
59
+ </Button>
60
+ <Modal
61
+ title='Example'
62
+ $visible={$visible}
63
+ >
64
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit.
65
+ </Modal>
66
+ </React.Fragment>
67
+ )
68
+ ```
69
+
70
+ 2. By passing the `visible` property that determines whether modal is visible.
71
+
72
+ ```jsx example
73
+ const [visible, setVisible] = useState(false)
74
+
75
+ return (
76
+ <React.Fragment>
77
+ <Button onPress={() => setVisible(true)}>
78
+ Open
79
+ </Button>
80
+ <Modal
81
+ title='Example'
82
+ visible={visible}
83
+ onRequestClose={() => setVisible(false)}
84
+ >
85
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit.
86
+ </Modal>
87
+ </React.Fragment>
88
+ )
89
+ ```
90
+
91
+ 3. By passing `ref`, which will receive the `open()` and `close()` methods to control visibility.
92
+
93
+ ```jsx example
94
+ const modalRef = useRef()
95
+
96
+ return (
97
+ <React.Fragment>
98
+ <Button onPress={() => modalRef.current.open()}>
99
+ Open
100
+ </Button>
101
+ <Modal
102
+ title='Example'
103
+ ref={modalRef}
104
+ >
105
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit.
106
+ </Modal>
107
+ </React.Fragment>
108
+ )
109
+ ```
110
+
111
+ ## Fullscreen modal
112
+
113
+ By default the modal shows like window in center of the page. To make it fullscreen, you need pass the string `fullscreen` to the `variant` property.
114
+
115
+ ```jsx example
116
+ const $visible = $(false)
117
+
118
+ return (
119
+ <React.Fragment>
120
+ <Button onPress={() => $visible.set(true)}>
121
+ Open fullscreen modal
122
+ </Button>
123
+ <Modal
124
+ variant='fullscreen'
125
+ title='Fullscreen example'
126
+ $visible={$visible}
127
+ onCancel={() => console.log('onCancel')}
128
+ onConfirm={() => console.log('onConfirm')}
129
+ >
130
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit.
131
+ </Modal>
132
+ </React.Fragment>
133
+ )
134
+ ```
135
+
136
+ ## Advanced usage
137
+
138
+ Modal consists of three parts - `Header`, `Content` and `Actions`. These parts can be used to add custom markup, the `Header` is used instead of `title`, the `Content` is used instead of children and the `Actions` is used instead of handlers `onCancel`, `onConfirm`. They can be used separately.
139
+
140
+ ```jsx example
141
+ const $visible = $(false)
142
+
143
+ return (
144
+ <React.Fragment>
145
+ <Button onPress={() => $visible.set(true)}>
146
+ Open advanced modal
147
+ </Button>
148
+ <Modal $visible={$visible}>
149
+ <Modal.Header>Advanced modal</Modal.Header>
150
+ <Modal.Content>
151
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit.
152
+ </Modal.Content>
153
+ <Modal.Actions>
154
+ <Button
155
+ shape='circle'
156
+ onPress={() => $visible.set(false)}
157
+ >
158
+ Custom close
159
+ </Button>
160
+ <Button
161
+ pushed
162
+ shape='circle'
163
+ onPress={() => $visible.set(false)}
164
+ >
165
+ Custom confirm
166
+ </Button>
167
+ </Modal.Actions>
168
+ </Modal>
169
+ </React.Fragment>
170
+ )
171
+ ```
172
+
173
+ ## Sandbox
174
+
175
+ ### Modal
176
+
177
+ <Sandbox
178
+ Component={Modal}
179
+ propsJsonSchema={ModalPropsJsonSchema}
180
+ props={{ /* $visible: $.session.Props.Modal.visible */ }}
181
+ $props={$.session.Props.Modal}
182
+ {...{ _comment: "HACK to make alive sandbox when user click on 'visible' checkbox" }}
183
+ />
184
+
185
+ ### Modal.Header
186
+
187
+ <Sandbox
188
+ Component={Modal.Header}
189
+ propsJsonSchema={ModalHeaderPropsJsonSchema}
190
+ />
191
+
192
+ ### Modal.Content
193
+
194
+ <Sandbox
195
+ Component={Modal.Content}
196
+ propsJsonSchema={ModalContentPropsJsonSchema}
197
+ />
198
+
199
+ ### Modal.Actions
200
+
201
+ <Sandbox
202
+ Component={Modal.Actions}
203
+ propsJsonSchema={ModalActionsPropsJsonSchema}
204
+ />
@@ -0,0 +1,34 @@
1
+ $modalBg = var(--color-bg-main-strong)
2
+ $gutter = $UI.gutters.m
3
+
4
+ .root
5
+ height 100%
6
+
7
+ &.window
8
+ padding 0 $gutter
9
+
10
+ .overlay
11
+ position absolute
12
+ top 0
13
+ right 0
14
+ left 0
15
+ bottom 0
16
+ background-color var(--Modal-overlayBg)
17
+
18
+ +web()
19
+ cursor pointer
20
+
21
+ .modal
22
+ flex-shrink 1
23
+ background-color $modalBg
24
+
25
+ &.window
26
+ margin auto
27
+ max-height 90%
28
+ max-width 776px
29
+ min-width 280px
30
+ border-radius 4px
31
+ shadow(4)
32
+
33
+ &.fullscreen
34
+ height 100%
package/index.d.ts ADDED
@@ -0,0 +1,63 @@
1
+ /* eslint-disable */
2
+ // DO NOT MODIFY THIS FILE - IT IS AUTOMATICALLY GENERATED ON COMMITS.
3
+
4
+ import { type ReactNode, type ComponentType, type RefObject } from 'react';
5
+ import { type StyleProp, type ViewStyle } from 'react-native';
6
+ type SupportedOrientation = 'portrait' | 'portrait-upside-down' | 'landscape' | 'landscape-left' | 'landscape-right';
7
+ export declare const _PropsJsonSchema: {};
8
+ export interface ModalProps {
9
+ /** Custom styles applied to the root view */
10
+ style?: StyleProp<ViewStyle>;
11
+ /** Custom styles applied to the modal content container */
12
+ modalStyle?: StyleProp<ViewStyle>;
13
+ /** Content rendered inside the modal */
14
+ children?: ReactNode;
15
+ /** Layout variant @default 'window' */
16
+ variant?: 'window' | 'fullscreen';
17
+ /** Controlled visibility flag */
18
+ visible?: boolean;
19
+ /** Model binding for two-way visibility control */
20
+ $visible?: any;
21
+ /** Imperative ref to open/close modal when not controlled */
22
+ ref?: RefObject<any>;
23
+ /** Header title text */
24
+ title?: string;
25
+ /** Label for cancel action @default 'Cancel' */
26
+ cancelLabel?: string;
27
+ /** Label for confirm action @default 'Confirm' */
28
+ confirmLabel?: string;
29
+ /** Show a cross in the header @default true */
30
+ showCross?: boolean;
31
+ /** Makes the backdrop clickable @default true */
32
+ enableBackdropPress?: boolean;
33
+ /** Component used as modal container @default SafeAreaView */
34
+ ModalElement?: ComponentType<any>;
35
+ /** Modal animation type @default 'fade' */
36
+ animationType?: 'slide' | 'fade' | 'none';
37
+ /** Render modal with transparent background @default true */
38
+ transparent?: boolean;
39
+ /** Control status bar translucency on Android */
40
+ statusBarTranslucent?: boolean;
41
+ /** Allowed screen orientations */
42
+ supportedOrientations?: SupportedOrientation[];
43
+ /** Callback fired after modal becomes visible */
44
+ onShow?: () => void;
45
+ /** Called when user clicks on the cross */
46
+ onCrossPress?: (event: any) => void | Promise<void>;
47
+ /** Show only the one `OK` button that uses this handler */
48
+ onCancel?: (event: any) => void | Promise<void>;
49
+ /** Show two buttons: `OK` uses this handler and `Cancel` uses the `onCancel` handler */
50
+ onConfirm?: (event: any) => void | Promise<void>;
51
+ /** Called when the user clicks on the backdrop (requires enableBackdropPress to be true) */
52
+ onBackdropPress?: (event: any) => void | Promise<void>;
53
+ /** Called when the orientation changes while the modal is displayed */
54
+ onOrientationChange?: (event: any) => void;
55
+ /** Called when user requests to close the modal (hardware back, menu button, Esc, etc.). Also fired after cross/backdrop/cancel/confirm unless handlers call event.preventDefault() */
56
+ onRequestClose?: (value?: boolean) => void;
57
+ /** Called once the modal has been dismissed */
58
+ onDismiss?: () => void;
59
+ /** DEPRECATED: use onRequestClose instead */
60
+ onChange?: (value?: boolean) => void;
61
+ }
62
+ declare const ObservedModal: any;
63
+ export default ObservedModal;
@@ -0,0 +1,2 @@
1
+ .table
2
+ background-color white
package/index.tsx ADDED
@@ -0,0 +1,198 @@
1
+ import React, {
2
+ useCallback,
3
+ useImperativeHandle,
4
+ type ReactNode,
5
+ type ComponentType,
6
+ type RefObject
7
+ } from 'react'
8
+ import { SafeAreaView, Modal as RNModal, type StyleProp, type ViewStyle } from 'react-native'
9
+ import { pug, observer, $ } from 'startupjs'
10
+ import { themed } from '@startupjs-ui/core'
11
+ import Portal from '@startupjs-ui/portal'
12
+ import Layout from './layout'
13
+ import ModalHeader from './ModalHeader'
14
+ import ModalContent from './ModalContent'
15
+ import ModalActions, { DEFAULT_CANCEL_LABEL, DEFAULT_CONFIRM_LABEL } from './ModalActions'
16
+
17
+ type SupportedOrientation =
18
+ | 'portrait'
19
+ | 'portrait-upside-down'
20
+ | 'landscape'
21
+ | 'landscape-left'
22
+ | 'landscape-right'
23
+
24
+ const SUPPORTED_ORIENTATIONS: SupportedOrientation[] = [
25
+ 'portrait',
26
+ 'portrait-upside-down',
27
+ 'landscape',
28
+ 'landscape-left',
29
+ 'landscape-right'
30
+ ]
31
+
32
+ export const _PropsJsonSchema = {/* ModalProps */}
33
+
34
+ export interface ModalProps {
35
+ /** Custom styles applied to the root view */
36
+ style?: StyleProp<ViewStyle>
37
+ /** Custom styles applied to the modal content container */
38
+ modalStyle?: StyleProp<ViewStyle>
39
+ /** Content rendered inside the modal */
40
+ children?: ReactNode
41
+ /** Layout variant @default 'window' */
42
+ variant?: 'window' | 'fullscreen'
43
+ /** Controlled visibility flag */
44
+ visible?: boolean
45
+ /** Model binding for two-way visibility control */
46
+ $visible?: any
47
+ /** Imperative ref to open/close modal when not controlled */
48
+ ref?: RefObject<any>
49
+ /** Header title text */
50
+ title?: string
51
+ /** Label for cancel action @default 'Cancel' */
52
+ cancelLabel?: string
53
+ /** Label for confirm action @default 'Confirm' */
54
+ confirmLabel?: string
55
+ /** Show a cross in the header @default true */
56
+ showCross?: boolean
57
+ /** Makes the backdrop clickable @default true */
58
+ enableBackdropPress?: boolean
59
+ /** Component used as modal container @default SafeAreaView */
60
+ ModalElement?: ComponentType<any>
61
+ /** Modal animation type @default 'fade' */
62
+ animationType?: 'slide' | 'fade' | 'none'
63
+ /** Render modal with transparent background @default true */
64
+ transparent?: boolean
65
+ /** Control status bar translucency on Android */
66
+ statusBarTranslucent?: boolean
67
+ /** Allowed screen orientations */
68
+ supportedOrientations?: SupportedOrientation[]
69
+ /** Callback fired after modal becomes visible */
70
+ onShow?: () => void
71
+ /** Called when user clicks on the cross */
72
+ onCrossPress?: (event: any) => void | Promise<void>
73
+ /** Show only the one `OK` button that uses this handler */
74
+ onCancel?: (event: any) => void | Promise<void>
75
+ /** Show two buttons: `OK` uses this handler and `Cancel` uses the `onCancel` handler */
76
+ onConfirm?: (event: any) => void | Promise<void>
77
+ /** Called when the user clicks on the backdrop (requires enableBackdropPress to be true) */
78
+ onBackdropPress?: (event: any) => void | Promise<void>
79
+ /** Called when the orientation changes while the modal is displayed */
80
+ onOrientationChange?: (event: any) => void
81
+ /** Called when user requests to close the modal (hardware back, menu button, Esc, etc.). Also fired after cross/backdrop/cancel/confirm unless handlers call event.preventDefault() */
82
+ onRequestClose?: (value?: boolean) => void
83
+ /** Called once the modal has been dismissed */
84
+ onDismiss?: () => void
85
+ /** DEPRECATED: use onRequestClose instead */
86
+ onChange?: (value?: boolean) => void
87
+ }
88
+
89
+ function ModalRoot ({
90
+ style,
91
+ modalStyle,
92
+ children,
93
+ variant = 'window',
94
+ visible,
95
+ $visible,
96
+ ref,
97
+ title,
98
+ cancelLabel = DEFAULT_CANCEL_LABEL,
99
+ confirmLabel = DEFAULT_CONFIRM_LABEL,
100
+ showCross = true,
101
+ enableBackdropPress = true,
102
+ ModalElement = SafeAreaView,
103
+ animationType = 'fade',
104
+ transparent = true,
105
+ supportedOrientations = SUPPORTED_ORIENTATIONS,
106
+ statusBarTranslucent,
107
+ onChange, // DEPRECATED
108
+ onRequestClose,
109
+ onDismiss,
110
+ onShow,
111
+ onCrossPress,
112
+ onBackdropPress,
113
+ onCancel,
114
+ onConfirm,
115
+ onOrientationChange,
116
+ ...props
117
+ }: ModalProps): ReactNode {
118
+ if (onChange) {
119
+ console.warn('[@startupjs/ui] Modal: onChange is DEPRECATED, use onRequestClose instead.')
120
+ }
121
+
122
+ const _$visible = $() // used for uncontrolled mode
123
+ if (visible == null) {
124
+ if ($visible) {
125
+ visible = $visible.get()
126
+ } else if (_$visible.get()) {
127
+ visible = _$visible.get()
128
+ }
129
+ }
130
+
131
+ // WORKAROUND
132
+ // convert 'visible' to boolean
133
+ // because modal window appears for undefined value on web
134
+ visible = !!visible
135
+
136
+ const _onRequestClose = useCallback(() => {
137
+ onRequestClose?.()
138
+ if ($visible) {
139
+ $visible.del()
140
+ } else {
141
+ _$visible.del()
142
+ }
143
+ }, [onRequestClose, $visible, _$visible])
144
+
145
+ useImperativeHandle(ref, () => ({
146
+ open: () => {
147
+ if ($visible) {
148
+ $visible.set(true)
149
+ } else {
150
+ _$visible.set(true)
151
+ }
152
+ },
153
+ close: () => {
154
+ _onRequestClose()
155
+ }
156
+ }), [_onRequestClose, $visible, _$visible])
157
+
158
+ return pug`
159
+ RNModal(
160
+ visible=visible
161
+ transparent=transparent
162
+ supportedOrientations=supportedOrientations
163
+ animationType=animationType
164
+ statusBarTranslucent=statusBarTranslucent
165
+ onRequestClose=_onRequestClose
166
+ onOrientationChange=onOrientationChange
167
+ onShow=onShow
168
+ onDismiss=onDismiss
169
+ ...props
170
+ )
171
+ Portal.Provider
172
+ if visible
173
+ Layout(
174
+ style=style
175
+ modalStyle=modalStyle
176
+ variant=variant
177
+ title=title
178
+ cancelLabel=cancelLabel
179
+ confirmLabel=confirmLabel
180
+ showCross=showCross
181
+ enableBackdropPress=enableBackdropPress
182
+ ModalElement=ModalElement
183
+ onRequestClose=_onRequestClose
184
+ onCrossPress=onCrossPress
185
+ onBackdropPress=onBackdropPress
186
+ onCancel=onCancel
187
+ onConfirm=onConfirm
188
+ )= children
189
+ `
190
+ }
191
+
192
+ const ObservedModal = observer(themed('Modal', ModalRoot)) as any
193
+
194
+ ObservedModal.Header = ModalHeader
195
+ ObservedModal.Content = ModalContent
196
+ ObservedModal.Actions = ModalActions
197
+
198
+ export default ObservedModal
package/layout.tsx ADDED
@@ -0,0 +1,204 @@
1
+ import React, { type ReactNode, type ComponentType } from 'react'
2
+ import { View, TouchableOpacity, type StyleProp, type ViewStyle } from 'react-native'
3
+ import { pug, observer } from 'startupjs'
4
+ import { themed } from '@startupjs-ui/core'
5
+ import ModalHeader from './ModalHeader'
6
+ import ModalContent from './ModalContent'
7
+ import ModalActions, { DEFAULT_CANCEL_LABEL, DEFAULT_CONFIRM_LABEL } from './ModalActions'
8
+ import './index.cssx.styl'
9
+
10
+ export interface ModalLayoutProps {
11
+ /** Custom styles applied to the root view */
12
+ style?: StyleProp<ViewStyle>
13
+ /** Custom styles applied to the modal surface */
14
+ modalStyle?: StyleProp<ViewStyle>
15
+ /** Children rendered inside modal sections */
16
+ children?: ReactNode
17
+ /** Layout variant @default 'window' */
18
+ variant?: 'window' | 'fullscreen'
19
+ /** Title rendered when no custom header provided */
20
+ title?: string
21
+ /** DEPRECATED: use cancelLabel instead */
22
+ dismissLabel?: string
23
+ /** Cancel action label @default 'Cancel' */
24
+ cancelLabel?: string
25
+ /** Confirm action label @default 'Confirm' */
26
+ confirmLabel?: string
27
+ /** Component used to wrap modal content */
28
+ ModalElement?: ComponentType<any>
29
+ /** Show cross button in header */
30
+ showCross?: boolean
31
+ /** Enable closing modal by tapping backdrop */
32
+ enableBackdropPress?: boolean
33
+ /** Request close handler */
34
+ onRequestClose?: () => void
35
+ /** Cross press handler */
36
+ onCrossPress?: (event: any) => void | Promise<void>
37
+ /** Backdrop press handler */
38
+ onBackdropPress?: (event: any) => void | Promise<void>
39
+ /** Cancel action handler */
40
+ onCancel?: (event: any) => void | Promise<void>
41
+ /** Confirm action handler */
42
+ onConfirm?: (event: any) => void | Promise<void>
43
+ }
44
+
45
+ function Modal ({
46
+ style,
47
+ modalStyle,
48
+ children,
49
+ variant,
50
+ title,
51
+ dismissLabel,
52
+ cancelLabel = DEFAULT_CANCEL_LABEL,
53
+ confirmLabel = DEFAULT_CONFIRM_LABEL,
54
+ ModalElement,
55
+ showCross,
56
+ enableBackdropPress,
57
+ onRequestClose,
58
+ onCrossPress,
59
+ onBackdropPress,
60
+ onCancel,
61
+ onConfirm
62
+ }: ModalLayoutProps): ReactNode {
63
+ // DEPRECATED
64
+ if (dismissLabel) {
65
+ console.warn(
66
+ '[@startupjs/ui] Modal: dismissLabel is DEPRECATED, use cancelLabel instead'
67
+ )
68
+ cancelLabel = dismissLabel
69
+ }
70
+
71
+ // Deconstruct template variables
72
+ let header: ReactNode | undefined
73
+ let actions: ReactNode | undefined
74
+ let content: ReactNode | undefined
75
+ const contentChildren: ReactNode[] = []
76
+
77
+ React.Children.forEach(children, child => {
78
+ if (!child) return
79
+
80
+ switch ((child as any).type) {
81
+ case ModalHeader:
82
+ if (header) throw Error('[ui -> Modal] You must specify a single <Modal.Header>')
83
+ header = child
84
+ break
85
+ case ModalActions:
86
+ if (actions) throw Error('[ui -> Modal] You must specify a single <Modal.Actions>')
87
+ actions = child
88
+ break
89
+ case ModalContent:
90
+ if (content) throw Error('[ui -> Modal] You must specify a single <Modal.Content>')
91
+ content = child
92
+ break
93
+ default:
94
+ contentChildren.push(child)
95
+ }
96
+ })
97
+
98
+ if (content && contentChildren.length > 0) {
99
+ throw Error('[ui -> Modal] React elements found directly within <Modal>. ' +
100
+ 'If <Modal.Content> is specified, you have to put all your content inside it')
101
+ }
102
+
103
+ let _onConfirm
104
+ let _onCancel
105
+ const isWindowLayout = variant === 'window'
106
+ const hasActions = !!onCancel || !!onConfirm
107
+ const hasHeader = !!title || !!showCross
108
+
109
+ const _onCrossPress = async (event: any) => {
110
+ event.persist() // TODO: remove in react 17
111
+ const promise: any = onCrossPress?.(event)
112
+ if (promise && typeof promise.then === 'function') await promise
113
+ if (event.defaultPrevented) return
114
+ if (onRequestClose) onRequestClose()
115
+ }
116
+
117
+ const _onBackdropPress = async (event: any) => {
118
+ event.persist() // TODO: remove in react 17
119
+ const promise: any = onBackdropPress?.(event)
120
+ if (promise && typeof promise.then === 'function') await promise
121
+ if (event.defaultPrevented) return
122
+ if (onRequestClose) onRequestClose()
123
+ }
124
+
125
+ if (onConfirm) {
126
+ _onConfirm = async (event: any) => {
127
+ event.persist() // TODO: remove in react 17
128
+ const promise: any = onConfirm(event)
129
+ if (promise && typeof promise.then === 'function') await promise
130
+ if (event.defaultPrevented) return
131
+ if (onRequestClose) onRequestClose()
132
+ }
133
+ }
134
+
135
+ if (hasActions) {
136
+ _onCancel = async (event: any) => {
137
+ event.persist() // TODO: remove in react 17
138
+ const promise: any = onCancel?.(event)
139
+ if (promise && typeof promise.then === 'function') await promise
140
+ if (event.defaultPrevented) return
141
+ if (onRequestClose) onRequestClose()
142
+ }
143
+ }
144
+
145
+ if (!onConfirm && cancelLabel === DEFAULT_CANCEL_LABEL) {
146
+ cancelLabel = 'OK'
147
+ }
148
+
149
+ // Handle <Modal.Header>
150
+ const headerProps = {
151
+ onCrossPress: showCross ? _onCrossPress : undefined
152
+ }
153
+
154
+ header = header
155
+ ? React.cloneElement(header as any, { ...headerProps, ...(header as any).props })
156
+ : hasHeader
157
+ ? React.createElement(ModalHeader, headerProps, title)
158
+ : null
159
+
160
+ // Handle <Modal.Actions>
161
+ const actionsProps = {
162
+ cancelLabel,
163
+ confirmLabel,
164
+ onCancel: _onCancel,
165
+ onConfirm: _onConfirm
166
+ }
167
+
168
+ actions = actions
169
+ ? React.cloneElement(actions as any, { ...actionsProps, ...(actions as any).props })
170
+ : hasActions
171
+ ? React.createElement(ModalActions, actionsProps)
172
+ : null
173
+
174
+ // Handle <Modal.Content>
175
+ const contentStyle: StyleProp<ViewStyle> = {}
176
+
177
+ if (header) contentStyle.paddingTop = 0
178
+ if (actions) contentStyle.paddingBottom = 0
179
+
180
+ const contentProps = { variant, style: contentStyle }
181
+
182
+ // content part should always present
183
+ content = content
184
+ ? React.cloneElement(content as any, { ...contentProps, ...(content as any).props })
185
+ : React.createElement(ModalContent, contentProps, contentChildren)
186
+
187
+ return pug`
188
+ View.root(style=style styleName=[variant])
189
+ if isWindowLayout
190
+ TouchableOpacity.overlay(
191
+ activeOpacity=1
192
+ onPress=enableBackdropPress ? _onBackdropPress : undefined
193
+ )
194
+ ModalElement.modal(
195
+ style=modalStyle
196
+ styleName=[variant]
197
+ )
198
+ = header
199
+ = content
200
+ = actions
201
+ `
202
+ }
203
+
204
+ export default observer(themed('Modal', Modal))
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@startupjs-ui/modal",
3
+ "version": "0.1.3",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "main": "index.tsx",
8
+ "types": "index.d.ts",
9
+ "type": "module",
10
+ "dependencies": {
11
+ "@startupjs-ui/button": "^0.1.3",
12
+ "@startupjs-ui/core": "^0.1.3",
13
+ "@startupjs-ui/div": "^0.1.3",
14
+ "@startupjs-ui/icon": "^0.1.3",
15
+ "@startupjs-ui/portal": "^0.1.3",
16
+ "@startupjs-ui/scroll-view": "^0.1.3",
17
+ "@startupjs-ui/span": "^0.1.3"
18
+ },
19
+ "peerDependencies": {
20
+ "react": "*",
21
+ "react-native": "*",
22
+ "startupjs": "*"
23
+ },
24
+ "gitHead": "fd964ebc3892d3dd0a6c85438c0af619cc50c3f0"
25
+ }