@startupjs-ui/dropdown 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/dropdown
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
+ * **dropdown:** refactor Dropdown component ([4305c16](https://github.com/startupjs/startupjs-ui/commit/4305c1606c9b26ae4746932b53dcb91034f713a5))
package/README.mdx ADDED
@@ -0,0 +1,91 @@
1
+ import { useState } from 'react'
2
+ import { View } from 'react-native'
3
+ import Dropdown, { _PropsJsonSchema as DropdownPropsJsonSchema } from './index'
4
+ import Icon from '@startupjs-ui/icon'
5
+ import { Sandbox } from '@startupjs-ui/docs'
6
+ import {
7
+ faEllipsisV,
8
+ faPencilAlt,
9
+ faTrashAlt
10
+ } from '@fortawesome/free-solid-svg-icons'
11
+
12
+ # Dropdown
13
+
14
+ Pop-up menus. Adaptable depending on the extension.
15
+
16
+ ```jsx
17
+ import { Dropdown } from 'startupjs-ui'
18
+ ```
19
+
20
+ ## Simple example
21
+
22
+ ```jsx example
23
+ const [sort, setSort] = useState('')
24
+
25
+ return (
26
+ <Dropdown value={sort} onChange={v => setSort(v)}>
27
+ <Dropdown.Caption placeholder="Sort by" />
28
+ <Dropdown.Item value="popular" label="Popular" />
29
+ <Dropdown.Item value="brand" label="Brand" />
30
+ <Dropdown.Item value="name" label="Name" />
31
+ </Dropdown>
32
+ )
33
+ ```
34
+
35
+ ## Custom caption
36
+
37
+ ```jsx example
38
+ const [sort, setSort] = useState('')
39
+
40
+ const captionStyle = {
41
+ width: 30,
42
+ height: 30,
43
+ backgroundColor: 'white',
44
+ borderRadius: 50,
45
+ justifyContent: 'center',
46
+ alignItems: 'center'
47
+ }
48
+
49
+ return (
50
+ <Dropdown value={sort} onChange={v => setSort(v)}>
51
+ <Dropdown.Caption>
52
+ <View style={captionStyle}>
53
+ <Icon icon={faEllipsisV} />
54
+ </View>
55
+ </Dropdown.Caption>
56
+ <Dropdown.Item value="popular" label="Popular" />
57
+ <Dropdown.Item value="brand" label="Brand" />
58
+ <Dropdown.Item value="name" label="Name" />
59
+ </Dropdown>
60
+ )
61
+ ```
62
+
63
+ ## Icons in items
64
+
65
+ ```jsx example
66
+ const [value, setValue] = useState('')
67
+
68
+ return (
69
+ <Dropdown value={value} onChange={v => setValue(v)}>
70
+ <Dropdown.Item icon={faPencilAlt} value="edit" label="Edit" />
71
+ <Dropdown.Item icon={faTrashAlt} value="delete" label="Delete" />
72
+ </Dropdown>
73
+ )
74
+ ```
75
+
76
+ ## Sandbox
77
+
78
+ <Sandbox
79
+ Component={Dropdown}
80
+ propsJsonSchema={DropdownPropsJsonSchema}
81
+ props={{
82
+ value: 'popular',
83
+ onChange: v => alert(`Selected: ${v}`),
84
+ children: [
85
+ <Dropdown.Caption placeholder="Sort by" />,
86
+ <Dropdown.Item value="popular" label="Popular" />,
87
+ <Dropdown.Item value="brand" label="Brand" />,
88
+ <Dropdown.Item value="name" label="Name" />
89
+ ]
90
+ }}
91
+ />
@@ -0,0 +1,17 @@
1
+ .select
2
+ background-color var(--color-bg-main-strong)
3
+ height 4u
4
+ border-radius .5u
5
+ border-style solid
6
+ border-width 1px
7
+ border-color var(--color-border-main)
8
+ padding 0 1u
9
+ align-items center
10
+ justify-content space-between
11
+ min-width 150px
12
+
13
+ .placeholder
14
+ color var(--color-text-placeholder)
15
+
16
+ .active
17
+ color var(--color-text-main)
@@ -0,0 +1,48 @@
1
+ import { type ReactNode } from 'react'
2
+ import { pug, observer } from 'startupjs'
3
+ import Div from '@startupjs-ui/div'
4
+ import Span from '@startupjs-ui/span'
5
+ import Icon from '@startupjs-ui/icon'
6
+ import Button from '@startupjs-ui/button'
7
+ import { themed } from '@startupjs-ui/core'
8
+ import { faAngleDown } from '@fortawesome/free-solid-svg-icons/faAngleDown'
9
+ import './index.cssx.styl'
10
+
11
+ export interface DropdownCaptionProps {
12
+ /** Caption content (used when `variant='custom'`) */
13
+ children?: ReactNode
14
+ /** Placeholder text shown when no active item */
15
+ placeholder?: string
16
+ /** Visual variant @default 'select' */
17
+ variant?: 'select' | 'button' | 'custom'
18
+ /** @private Active item label injected by Dropdown */
19
+ _activeLabel?: string
20
+ }
21
+
22
+ function DropdownCaption ({
23
+ children,
24
+ placeholder = 'Select a state...',
25
+ variant = 'select',
26
+ _activeLabel
27
+ }: DropdownCaptionProps): ReactNode {
28
+ if (variant === 'custom') return children
29
+
30
+ if (variant === 'button') {
31
+ return pug`
32
+ Button(
33
+ variant='flat'
34
+ color='primary'
35
+ pointerEvents='box-none'
36
+ )= placeholder
37
+ `
38
+ }
39
+
40
+ return pug`
41
+ Div.select(row)
42
+ Span.placeholder(styleName={ active: !!_activeLabel })
43
+ = _activeLabel || placeholder
44
+ Icon(icon=faAngleDown)
45
+ `
46
+ }
47
+
48
+ export default observer(themed('DropdownCaption', DropdownCaption))
@@ -0,0 +1,47 @@
1
+ .item
2
+ &.list
3
+ flex-direction row
4
+ justify-content space-between
5
+ border-bottom-width 1px
6
+ border-bottom-color #cccccc
7
+
8
+ &.buttons
9
+ justify-content center
10
+ align-items center
11
+ height 6u
12
+ border-bottom-width 1px
13
+ border-bottom-color #cccccc
14
+
15
+ &.popover
16
+ padding 12px
17
+
18
+ &.itemDown
19
+ border-bottom-width 0
20
+
21
+ .itemText
22
+ &.list
23
+ padding 2u
24
+
25
+ &.buttons
26
+ padding 2u
27
+
28
+ &.active
29
+ color var(--color-text-primary)
30
+
31
+ .iconActive
32
+ &.list
33
+ position absolute
34
+ top 17px
35
+ right 14px
36
+ width 2u
37
+ height 2u
38
+
39
+ &.buttons
40
+ position absolute
41
+ top 17px
42
+ left 14px
43
+ width 2u
44
+ height 2u
45
+
46
+ .selectMenu
47
+ background-color #eeeeee
@@ -0,0 +1,117 @@
1
+ import { type ReactNode } from 'react'
2
+ import { Text, View, TouchableOpacity, type StyleProp, type ViewStyle } from 'react-native'
3
+ import { pug, observer } from 'startupjs'
4
+ import Icon from '@startupjs-ui/icon'
5
+ import Menu from '@startupjs-ui/menu'
6
+ import Link from '@startupjs-ui/link'
7
+ import { themed } from '@startupjs-ui/core'
8
+ import { faCheck } from '@fortawesome/free-solid-svg-icons/faCheck'
9
+ import './index.cssx.styl'
10
+
11
+ export interface DropdownItemProps {
12
+ /** Custom styles applied to the wrapper */
13
+ style?: StyleProp<ViewStyle>
14
+ /** Navigation target (renders as Link when provided) */
15
+ to?: any
16
+ /** Item label */
17
+ label?: string
18
+ /** Item value reported to Dropdown.onChange */
19
+ value?: string | number
20
+ /** Optional icon displayed in Menu.Item (popover variant) */
21
+ icon?: any
22
+ /** Disable item interactions */
23
+ disabled?: boolean
24
+ /** Custom press handler (bypasses Dropdown.onChange) */
25
+ onPress?: () => void
26
+ /** Custom content when used as a pure/custom item */
27
+ children?: ReactNode
28
+ /** @private Active value injected by Dropdown */
29
+ _activeValue?: any
30
+ /** @private Selected index for keyboard navigation */
31
+ _selectIndexValue?: number
32
+ /** @private Variant injected by Dropdown */
33
+ _variant?: 'list' | 'buttons' | 'popover' | 'pure'
34
+ /** @private Style for active item injected by Dropdown */
35
+ _styleActiveItem?: StyleProp<ViewStyle>
36
+ /** @private Change handler injected by Dropdown */
37
+ _onChange?: (value: any) => void
38
+ /** @private Dismiss handler injected by Dropdown */
39
+ _onDismissDropdown?: () => void
40
+ /** @private Item index injected by Dropdown */
41
+ _index?: number
42
+ /** @private Items count injected by Dropdown */
43
+ _childrenLength?: number
44
+ }
45
+
46
+ function DropdownItem ({
47
+ style,
48
+ to,
49
+ label,
50
+ value,
51
+ icon,
52
+ disabled,
53
+ onPress,
54
+ children,
55
+ _activeValue,
56
+ _selectIndexValue,
57
+ _variant,
58
+ _styleActiveItem,
59
+ _onChange,
60
+ _onDismissDropdown,
61
+ _index,
62
+ _childrenLength
63
+ }: DropdownItemProps): ReactNode {
64
+ const isPure = _variant === 'pure'
65
+
66
+ const handlePress = () => {
67
+ if (disabled) return
68
+
69
+ if (onPress) {
70
+ onPress()
71
+ _onDismissDropdown && _onDismissDropdown()
72
+ } else {
73
+ _onChange && _onChange(value)
74
+ }
75
+ }
76
+
77
+ if (_variant === 'popover' && !isPure) {
78
+ return pug`
79
+ Menu.Item(
80
+ to=to
81
+ style=style
82
+ active=_activeValue === value
83
+ disabled=disabled
84
+ styleName={ selectMenu: _selectIndexValue === _index }
85
+ onPress=handlePress
86
+ icon=icon
87
+ )= label
88
+ `
89
+ }
90
+
91
+ const Wrapper: any = to ? Link : TouchableOpacity
92
+ return pug`
93
+ Wrapper(
94
+ to=to
95
+ style=style
96
+ onPress=handlePress
97
+ )
98
+ View.item(
99
+ style=(!isPure && _activeValue === value) ? _styleActiveItem : undefined
100
+ styleName=[!isPure && _variant, {
101
+ active: !isPure && (_activeValue === value),
102
+ itemUp: !isPure && (_index === 0),
103
+ itemDown: !isPure && (_index === (_childrenLength || 0) - 1),
104
+ selectMenu: _selectIndexValue === _index
105
+ }]
106
+ )
107
+ if isPure
108
+ = children
109
+ else
110
+ Text.itemText(styleName=[_variant, { active: _activeValue && _activeValue === value }])
111
+ = label
112
+ if _activeValue === value
113
+ Icon.iconActive(styleName=_variant icon=faCheck)
114
+ `
115
+ }
116
+
117
+ export default observer(themed('DropdownItem', DropdownItem))
@@ -0,0 +1 @@
1
+ export { default as useKeyboard } from './useKeyboard'
@@ -0,0 +1,76 @@
1
+ import { useState, useEffect } from 'react'
2
+ import { Platform } from 'react-native'
3
+
4
+ export default function useKeyboard ({
5
+ isShow,
6
+ renderContent,
7
+ value,
8
+ onChange,
9
+ onChangeShow
10
+ }: {
11
+ isShow: boolean
12
+ renderContent: { current: any[] }
13
+ value: any
14
+ onChange?: (value: any) => void
15
+ onChangeShow: (visible: boolean) => void
16
+ }): [number] {
17
+ const [selectIndexValue, setSelectIndexValue] = useState(-1)
18
+
19
+ useEffect(() => {
20
+ if (Platform.OS !== 'web') return
21
+
22
+ if (isShow) {
23
+ document.addEventListener('keydown', onKeyDown)
24
+ } else {
25
+ document.removeEventListener('keydown', onKeyDown)
26
+ setSelectIndexValue(-1)
27
+ }
28
+
29
+ return () => { document.removeEventListener('keydown', onKeyDown) }
30
+ // eslint-disable-next-line react-hooks/exhaustive-deps
31
+ }, [isShow, selectIndexValue])
32
+
33
+ function onKeyDown (e: KeyboardEvent) {
34
+ e.preventDefault()
35
+ e.stopPropagation()
36
+
37
+ let item: any
38
+ let index: number
39
+ const keyName = e.key
40
+
41
+ switch (keyName) {
42
+ case 'ArrowUp':
43
+ if (selectIndexValue === 0 || (selectIndexValue === -1 && !value)) return
44
+
45
+ index = selectIndexValue - 1
46
+ if (selectIndexValue === -1 && value) {
47
+ index = renderContent.current.findIndex(item => item.props.value === value)
48
+ index--
49
+ }
50
+
51
+ setSelectIndexValue(index)
52
+ break
53
+
54
+ case 'ArrowDown':
55
+ if (selectIndexValue === renderContent.current.length - 1) return
56
+
57
+ index = selectIndexValue + 1
58
+ if (selectIndexValue === -1 && value) {
59
+ index = renderContent.current.findIndex(item => item.props.value === value)
60
+ index++
61
+ }
62
+
63
+ setSelectIndexValue(index)
64
+ break
65
+
66
+ case 'Enter':
67
+ if (selectIndexValue === -1) return
68
+ item = renderContent.current.find((_, i) => i === selectIndexValue)
69
+ onChange && onChange(item.props.value)
70
+ onChangeShow(false)
71
+ break
72
+ }
73
+ }
74
+
75
+ return [selectIndexValue]
76
+ }
@@ -0,0 +1,49 @@
1
+ .dropdown
2
+ &.list
3
+ padding 2u
4
+ padding-bottom 1u
5
+
6
+ &.buttons
7
+ max-height 100%
8
+
9
+ .case
10
+ &.buttons
11
+ margin 1.5u 1.5u 2u
12
+ border-radius 1u
13
+ background-color var(--color-bg-main-strong)
14
+
15
+ .caption
16
+ align-self flex-start
17
+
18
+ .captionText
19
+ &.list
20
+ padding 1u 2u 2u
21
+ font-size 21px
22
+
23
+ +web()
24
+ fontFamily('normal', 600)
25
+
26
+ +native()
27
+ fontFamily('normal', 600)
28
+
29
+ .button
30
+ &.buttons
31
+ justify-content center
32
+ align-items center
33
+ margin -1u 1.5u 1.5u
34
+ padding 2u
35
+ border-radius 1u
36
+ background-color var(--color-bg-main-strong)
37
+
38
+ .popover
39
+ border-radius .5u
40
+ shadow(2)
41
+
42
+ .drawerReset
43
+ background-color transparent
44
+ height auto
45
+ box-shadow none
46
+ border-radius 0
47
+
48
+ :export
49
+ media: $UI.media
package/index.d.ts ADDED
@@ -0,0 +1,50 @@
1
+ /* eslint-disable */
2
+ // DO NOT MODIFY THIS FILE - IT IS AUTOMATICALLY GENERATED ON COMMITS.
3
+
4
+ import { type ReactNode, type RefObject } from 'react';
5
+ import { type StyleProp, type ViewStyle } from 'react-native';
6
+ export declare const _PropsJsonSchema: {};
7
+ export interface DropdownProps {
8
+ /** Ref to control dropdown programmatically */
9
+ ref?: RefObject<DropdownRef>;
10
+ /** Custom styles applied to the dropdown content container */
11
+ style?: StyleProp<ViewStyle>;
12
+ /** Custom styles applied to the caption wrapper */
13
+ captionStyle?: StyleProp<ViewStyle>;
14
+ /** Custom styles applied to the active item view */
15
+ activeItemStyle?: StyleProp<ViewStyle>;
16
+ /** Dropdown caption and items */
17
+ children?: ReactNode;
18
+ /** Currently selected value @default '' */
19
+ value?: string | number;
20
+ /** Popover position @default 'bottom' */
21
+ position?: 'top' | 'bottom' | 'left' | 'right';
22
+ /** Popover attachment @default 'start' */
23
+ attachment?: 'start' | 'center' | 'end';
24
+ /** Fallback placements order */
25
+ placements?: any;
26
+ /** Drawer items rendering variant @default 'buttons' */
27
+ drawerVariant?: 'list' | 'buttons' | 'pure';
28
+ /** Title shown in list drawer variant */
29
+ drawerListTitle?: string;
30
+ /** Cancel button label in buttons drawer variant @default 'Cancel' */
31
+ drawerCancelLabel?: string;
32
+ /** Disable caption press */
33
+ disabled?: boolean;
34
+ /** Enable drawer behavior on small screens @default true */
35
+ hasDrawer?: boolean;
36
+ /** Show swipe responder zone in drawer */
37
+ showDrawerResponder?: boolean;
38
+ /** Called when item is selected */
39
+ onChange?: (value: string | number | undefined) => void;
40
+ /** Called when dropdown is dismissed via overlay/cancel */
41
+ onDismiss?: () => void;
42
+ }
43
+ export interface DropdownRef {
44
+ /** Open dropdown programmatically */
45
+ open: () => void;
46
+ /** Close dropdown programmatically */
47
+ close: () => void;
48
+ }
49
+ declare const ObservedDropdown: any;
50
+ export default ObservedDropdown;
package/index.tsx ADDED
@@ -0,0 +1,313 @@
1
+ import React, { useState, useRef, useImperativeHandle, useEffect, type ReactNode, type RefObject } from 'react'
2
+ import {
3
+ Dimensions,
4
+ UIManager,
5
+ ScrollView,
6
+ StyleSheet,
7
+ Text,
8
+ TouchableOpacity,
9
+ View,
10
+ type StyleProp,
11
+ type ViewStyle
12
+ } from 'react-native'
13
+ import { pug, observer, $ } from 'startupjs'
14
+ import { themed } from '@startupjs-ui/core'
15
+ import Drawer from '@startupjs-ui/drawer'
16
+ import Popover, { type PopoverRef } from '@startupjs-ui/popover'
17
+ import DropdownCaption from './components/Caption'
18
+ import DropdownItem from './components/Item'
19
+ import { useKeyboard } from './helpers'
20
+ import STYLES from './index.cssx.styl'
21
+
22
+ export const _PropsJsonSchema = {/* DropdownProps */}
23
+
24
+ export interface DropdownProps {
25
+ /** Ref to control dropdown programmatically */
26
+ ref?: RefObject<DropdownRef>
27
+ /** Custom styles applied to the dropdown content container */
28
+ style?: StyleProp<ViewStyle>
29
+ /** Custom styles applied to the caption wrapper */
30
+ captionStyle?: StyleProp<ViewStyle>
31
+ /** Custom styles applied to the active item view */
32
+ activeItemStyle?: StyleProp<ViewStyle>
33
+ /** Dropdown caption and items */
34
+ children?: ReactNode
35
+ /** Currently selected value @default '' */
36
+ value?: string | number
37
+ /** Popover position @default 'bottom' */
38
+ position?: 'top' | 'bottom' | 'left' | 'right'
39
+ /** Popover attachment @default 'start' */
40
+ attachment?: 'start' | 'center' | 'end'
41
+ /** Fallback placements order */
42
+ placements?: any
43
+ /** Drawer items rendering variant @default 'buttons' */
44
+ drawerVariant?: 'list' | 'buttons' | 'pure'
45
+ /** Title shown in list drawer variant */
46
+ drawerListTitle?: string
47
+ /** Cancel button label in buttons drawer variant @default 'Cancel' */
48
+ drawerCancelLabel?: string
49
+ /** Disable caption press */
50
+ disabled?: boolean
51
+ /** Enable drawer behavior on small screens @default true */
52
+ hasDrawer?: boolean
53
+ /** Show swipe responder zone in drawer */
54
+ showDrawerResponder?: boolean
55
+ /** Called when item is selected */
56
+ onChange?: (value: string | number | undefined) => void
57
+ /** Called when dropdown is dismissed via overlay/cancel */
58
+ onDismiss?: () => void
59
+ }
60
+
61
+ export interface DropdownRef {
62
+ /** Open dropdown programmatically */
63
+ open: () => void
64
+ /** Close dropdown programmatically */
65
+ close: () => void
66
+ }
67
+
68
+ // TODO: key event change scroll
69
+ function Dropdown ({
70
+ style = [],
71
+ captionStyle,
72
+ activeItemStyle,
73
+ children,
74
+ value = '',
75
+ position = 'bottom',
76
+ attachment = 'start',
77
+ placements,
78
+ drawerVariant = 'buttons',
79
+ drawerListTitle = '',
80
+ drawerCancelLabel = 'Cancel',
81
+ disabled,
82
+ hasDrawer = true,
83
+ showDrawerResponder,
84
+ onChange,
85
+ onDismiss,
86
+ ref
87
+ }: DropdownProps): ReactNode {
88
+ const popoverRef = useRef<PopoverRef>(null)
89
+ const refScroll = useRef<any>(null)
90
+ const renderContent = useRef<any[]>([])
91
+ const closeReason = useRef<null | 'toggle' | 'select' | 'dismiss' | 'resize'>(null)
92
+
93
+ const $isShow = $(false)
94
+ const [activeInfo, setActiveInfo] = useState<any>(null)
95
+ const $layoutWidth = $(
96
+ Math.min(Dimensions.get('window').width, Dimensions.get('screen').width)
97
+ )
98
+
99
+ const [selectIndexValue] = useKeyboard({
100
+ value,
101
+ isShow: $isShow.get(),
102
+ renderContent,
103
+ onChange: (v: any) => {
104
+ closeReason.current = 'select'
105
+ onChange && onChange(v)
106
+ },
107
+ onChangeShow: v => { handleVisibleChange(v) }
108
+ })
109
+
110
+ const isPopover = !hasDrawer || ($layoutWidth.get() > STYLES.media.tablet)
111
+
112
+ function handleWidthChange () {
113
+ closeReason.current = 'resize'
114
+ popoverRef.current?.close?.()
115
+ $isShow.set(false)
116
+ $layoutWidth.set(Math.min(Dimensions.get('window').width, Dimensions.get('screen').width))
117
+ }
118
+
119
+ useEffect(() => {
120
+ const listener = Dimensions.addEventListener('change', handleWidthChange)
121
+
122
+ return () => {
123
+ $isShow.del()
124
+ listener?.remove?.()
125
+ }
126
+ // eslint-disable-next-line react-hooks/exhaustive-deps
127
+ }, [])
128
+
129
+ useImperativeHandle(ref, () => ({
130
+ open: () => {
131
+ handleVisibleChange(true)
132
+ },
133
+ close: () => {
134
+ handleVisibleChange(false, { reason: 'toggle' })
135
+ }
136
+ }))
137
+
138
+ function handleVisibleChange (nextVisible: boolean, meta: { reason?: typeof closeReason.current } = {}) {
139
+ if (typeof meta.reason !== 'undefined') closeReason.current = meta.reason
140
+
141
+ if (isPopover) {
142
+ if (nextVisible) {
143
+ closeReason.current = null
144
+ popoverRef.current?.open?.()
145
+ $isShow.set(true)
146
+ } else {
147
+ popoverRef.current?.close?.()
148
+ $isShow.set(false)
149
+ }
150
+ return
151
+ }
152
+
153
+ if (!nextVisible && closeReason.current === 'dismiss') onDismiss && onDismiss()
154
+ $isShow.set(nextVisible)
155
+ }
156
+
157
+ function onLayoutActive ({ nativeEvent }: any) {
158
+ setActiveInfo(nativeEvent.layout)
159
+ }
160
+
161
+ function onCancel () {
162
+ handleVisibleChange(false, { reason: 'dismiss' })
163
+ }
164
+
165
+ function onRequestOpen () {
166
+ const node = refScroll.current?.getScrollableNode
167
+ ? refScroll.current.getScrollableNode()
168
+ : refScroll.current
169
+
170
+ if (!node) return
171
+
172
+ UIManager.measure(node, (x, y, width, curHeight) => {
173
+ if (activeInfo && activeInfo.y >= (curHeight - activeInfo.height)) {
174
+ refScroll.current?.scrollTo?.({ y: activeInfo.y, animated: false })
175
+ }
176
+ })
177
+ }
178
+
179
+ let caption: ReactNode = null
180
+ let activeLabel = ''
181
+ renderContent.current = []
182
+
183
+ React.Children.toArray(children).forEach((child: any, index, arr) => {
184
+ if (child?.type === DropdownCaption) {
185
+ if (index !== 0) Error('Caption need use first child')
186
+ if (child.props.children) {
187
+ caption = React.cloneElement(child, { variant: 'custom' })
188
+ } else {
189
+ caption = child
190
+ }
191
+ return
192
+ }
193
+
194
+ const _child = React.cloneElement(child, {
195
+ _variant: child.props.children
196
+ ? 'pure'
197
+ : (isPopover ? 'popover' : drawerVariant),
198
+ _styleActiveItem: activeItemStyle,
199
+ _activeValue: value,
200
+ _selectIndexValue: selectIndexValue,
201
+ _index: caption ? (index - 1) : index,
202
+ _childrenLength: caption ? (arr.length - 1) : arr.length,
203
+ _onDismissDropdown: () => { handleVisibleChange(false) },
204
+ _onChange: (v: any) => {
205
+ closeReason.current = 'select'
206
+ onChange && onChange(v)
207
+ handleVisibleChange(false)
208
+ }
209
+ })
210
+
211
+ if (value === child.props.value) {
212
+ activeLabel = child.props.label
213
+ renderContent.current.push(pug`
214
+ View(
215
+ key=index
216
+ value=child.props.value
217
+ onLayout=onLayoutActive
218
+ )=_child
219
+ `)
220
+ } else {
221
+ renderContent.current.push(_child)
222
+ }
223
+ })
224
+
225
+ if (!caption) {
226
+ caption = <DropdownCaption _activeLabel={activeLabel} />
227
+ } else {
228
+ caption = React.cloneElement(caption as any, { _activeLabel: activeLabel })
229
+ }
230
+
231
+ const _popoverStyle = StyleSheet.flatten(style)
232
+ if ((caption as any).props?.variant === 'button' || (caption as any).props?.variant === 'custom') {
233
+ ;(_popoverStyle as any).minWidth = 160
234
+ }
235
+
236
+ const matchAnchorWidth = !(_popoverStyle as any)?.width && !(_popoverStyle as any)?.minWidth
237
+
238
+ if (isPopover) {
239
+ const renderPopoverContent = (): ReactNode => pug`
240
+ ScrollView(
241
+ ref=refScroll
242
+ showsVerticalScrollIndicator=false
243
+ )= renderContent.current
244
+ `
245
+
246
+ const handlePopoverCloseComplete = () => {
247
+ $isShow.set(false)
248
+ if (closeReason.current !== 'select' && closeReason.current !== 'toggle' && closeReason.current !== 'resize') {
249
+ onDismiss && onDismiss()
250
+ }
251
+ closeReason.current = null
252
+ }
253
+
254
+ return pug`
255
+ Popover(
256
+ ref=popoverRef
257
+ style=captionStyle
258
+ attachmentStyle=_popoverStyle
259
+ position=position
260
+ attachment=attachment
261
+ placements=placements
262
+ matchAnchorWidth=matchAnchorWidth
263
+ onOpenComplete=onRequestOpen
264
+ onCloseComplete=handlePopoverCloseComplete
265
+ renderContent=renderPopoverContent
266
+ )
267
+ TouchableOpacity(
268
+ disabled=disabled
269
+ onPress=() => handleVisibleChange(!$isShow.get(), { reason: !$isShow.get() ? null : 'toggle' })
270
+ )
271
+ = caption
272
+ `
273
+ }
274
+
275
+ return pug`
276
+ if caption
277
+ TouchableOpacity.caption(
278
+ disabled=disabled
279
+ onPress=() => handleVisibleChange(!$isShow.get())
280
+ )
281
+ = caption
282
+ Drawer(
283
+ visible=$isShow.get()
284
+ position='bottom'
285
+ style={ maxHeight: '100%' }
286
+ styleName={ drawerReset: drawerVariant === 'buttons' }
287
+ onDismiss=() => handleVisibleChange(false)
288
+ onRequestOpen=onRequestOpen
289
+ showResponder=showDrawerResponder
290
+ )
291
+ View.dropdown(styleName=drawerVariant)
292
+ if drawerVariant === 'list'
293
+ View.caption(styleName=drawerVariant)
294
+ Text.captionText(styleName=drawerVariant)= drawerListTitle
295
+ ScrollView.case(
296
+ ref=refScroll
297
+ showsVerticalScrollIndicator=false
298
+ style=_popoverStyle
299
+ styleName=drawerVariant
300
+ )= renderContent.current
301
+ if drawerVariant === 'buttons'
302
+ TouchableOpacity(onPress=onCancel)
303
+ View.button(styleName=drawerVariant)
304
+ Text= drawerCancelLabel
305
+ `
306
+ }
307
+
308
+ const ObservedDropdown: any = observer(themed('Dropdown', Dropdown))
309
+
310
+ ObservedDropdown.Caption = DropdownCaption
311
+ ObservedDropdown.Item = DropdownItem
312
+
313
+ export default ObservedDropdown
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@startupjs-ui/dropdown",
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/drawer": "^0.1.3",
15
+ "@startupjs-ui/icon": "^0.1.3",
16
+ "@startupjs-ui/link": "^0.1.3",
17
+ "@startupjs-ui/menu": "^0.1.3",
18
+ "@startupjs-ui/popover": "^0.1.3",
19
+ "@startupjs-ui/span": "^0.1.3"
20
+ },
21
+ "peerDependencies": {
22
+ "react": "*",
23
+ "react-native": "*",
24
+ "startupjs": "*"
25
+ },
26
+ "gitHead": "fd964ebc3892d3dd0a6c85438c0af619cc50c3f0"
27
+ }