@neko-os/ui 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/abstractions/KeyboardDismissView.js +3 -0
- package/dist/abstractions/KeyboardDismissView.native.js +9 -0
- package/dist/abstractions/TouchableOpacity.native.js +8 -2
- package/dist/components/actions/ClearLink.js +6 -0
- package/dist/components/actions/FloatingMenu.js +1 -1
- package/dist/components/calendar/CalendarNav.js +6 -6
- package/dist/components/carousel/Carousel.js +48 -0
- package/dist/components/carousel/CarouselArrows.js +40 -0
- package/dist/components/carousel/CarouselArrows.native.js +40 -0
- package/dist/components/carousel/CarouselDots.js +32 -0
- package/dist/components/carousel/CarouselDots.native.js +36 -0
- package/dist/components/carousel/CarouselHandler.js +86 -0
- package/dist/components/carousel/CarouselSlider.js +124 -0
- package/dist/components/carousel/CarouselSlider.native.js +110 -0
- package/dist/components/carousel/InfiniteCarousel.js +50 -0
- package/dist/components/carousel/index.js +6 -0
- package/dist/components/form/Form.js +5 -3
- package/dist/components/index.js +3 -1
- package/dist/components/inputs/DateInput.js +7 -4
- package/dist/components/inputs/InputWrapper.js +1 -2
- package/dist/components/inputs/LinkInput.js +1 -1
- package/dist/components/inputs/Picker.js +1 -1
- package/dist/components/inputs/TextInput.js +7 -6
- package/dist/components/inputs/datePicker/DayPicker.js +65 -23
- package/dist/components/inputs/datePicker/MonthPicker.js +51 -27
- package/dist/components/inputs/datePicker/QuarterPicker.js +52 -28
- package/dist/components/inputs/datePicker/WeekPicker.js +59 -24
- package/dist/components/inputs/datePicker/YearPicker.js +59 -35
- package/dist/components/keyboard/KeyboardDismissButton.js +3 -0
- package/dist/components/keyboard/KeyboardDismissButton.native.js +38 -0
- package/dist/components/keyboard/index.js +1 -0
- package/dist/components/modals/bottomDrawer/native/BottomDrawer.js +28 -7
- package/dist/components/presentation/LabelValue.js +1 -1
- package/dist/components/presentation/Result.js +11 -3
- package/dist/components/structure/KeyboardAvoidingView.js +9 -2
- package/dist/components/theme/ThemePicker.js +7 -12
- package/dist/components/theme/ThemeThumb.js +1 -1
- package/dist/theme/ThemeHandler.js +31 -3
- package/package.json +1 -1
- package/src/abstractions/KeyboardDismissView.js +3 -0
- package/src/abstractions/KeyboardDismissView.native.js +9 -0
- package/src/abstractions/TouchableOpacity.native.js +8 -2
- package/src/components/actions/ClearLink.js +6 -0
- package/src/components/actions/FloatingMenu.js +1 -1
- package/src/components/calendar/CalendarNav.js +6 -6
- package/src/components/carousel/Carousel.js +48 -0
- package/src/components/carousel/CarouselArrows.js +40 -0
- package/src/components/carousel/CarouselArrows.native.js +40 -0
- package/src/components/carousel/CarouselDots.js +32 -0
- package/src/components/carousel/CarouselDots.native.js +36 -0
- package/src/components/carousel/CarouselHandler.js +86 -0
- package/src/components/carousel/CarouselSlider.js +124 -0
- package/src/components/carousel/CarouselSlider.native.js +110 -0
- package/src/components/carousel/InfiniteCarousel.js +50 -0
- package/src/components/carousel/index.js +6 -0
- package/src/components/form/Form.js +2 -0
- package/src/components/index.js +2 -0
- package/src/components/inputs/DateInput.js +4 -1
- package/src/components/inputs/InputWrapper.js +1 -2
- package/src/components/inputs/LinkInput.js +1 -1
- package/src/components/inputs/Picker.js +1 -1
- package/src/components/inputs/TextInput.js +5 -4
- package/src/components/inputs/datePicker/DayPicker.js +63 -21
- package/src/components/inputs/datePicker/MonthPicker.js +50 -26
- package/src/components/inputs/datePicker/QuarterPicker.js +50 -26
- package/src/components/inputs/datePicker/WeekPicker.js +57 -22
- package/src/components/inputs/datePicker/YearPicker.js +58 -34
- package/src/components/keyboard/KeyboardDismissButton.js +3 -0
- package/src/components/keyboard/KeyboardDismissButton.native.js +38 -0
- package/src/components/keyboard/index.js +1 -0
- package/src/components/modals/bottomDrawer/native/BottomDrawer.js +27 -6
- package/src/components/presentation/LabelValue.js +1 -1
- package/src/components/presentation/Result.js +10 -2
- package/src/components/structure/KeyboardAvoidingView.js +9 -2
- package/src/components/theme/ThemePicker.js +8 -13
- package/src/components/theme/ThemeThumb.js +1 -1
- package/src/theme/ThemeHandler.js +31 -3
|
@@ -1,3 +1,9 @@
|
|
|
1
|
-
import { TouchableOpacity as RNTouchableOpacity } from 'react-native'
|
|
1
|
+
import { Linking, TouchableOpacity as RNTouchableOpacity } from 'react-native'
|
|
2
2
|
|
|
3
|
-
export
|
|
3
|
+
export function AbsTouchableOpacity({ href, target, link, onPress, ...props }) {
|
|
4
|
+
const handlePress = href
|
|
5
|
+
? (e) => { onPress?.(e); Linking.openURL(href) }
|
|
6
|
+
: onPress
|
|
7
|
+
|
|
8
|
+
return <RNTouchableOpacity onPress={handlePress} {...props} />
|
|
9
|
+
}
|
|
@@ -14,7 +14,7 @@ export function FloatingMenu({ fixed, onChange, items, activeIndex, size = 'md',
|
|
|
14
14
|
|
|
15
15
|
return (
|
|
16
16
|
<View absolute={!fixed} fixed={fixed} left="md" right="md" centerH bottom={Math.max(insets.bottom / 2, 16)}>
|
|
17
|
-
<WrapperView height={height} shadow round row paddingH="sm" bg={bg} {...props}>
|
|
17
|
+
<WrapperView height={height} shadow round row paddingH="sm" bg={bg} border="overlayDivider" {...props}>
|
|
18
18
|
{items.map((item, index) => {
|
|
19
19
|
const isActive = index === activeIndex
|
|
20
20
|
|
|
@@ -20,7 +20,7 @@ export function CalendarNav({ value, onChange, level, ...props }) {
|
|
|
20
20
|
|
|
21
21
|
return (
|
|
22
22
|
<View className="neko-calendar-nav" row centerV gap="xxs" height={30} {...props}>
|
|
23
|
-
<Link onPress={prevDecade} aria-label="Previous decade" padding="
|
|
23
|
+
<Link onPress={prevDecade} aria-label="Previous decade" padding="sm" paddingL={0}>
|
|
24
24
|
<Icon name="arrow-left-double-line" />
|
|
25
25
|
</Link>
|
|
26
26
|
|
|
@@ -28,7 +28,7 @@ export function CalendarNav({ value, onChange, level, ...props }) {
|
|
|
28
28
|
{year}-{year + 9}
|
|
29
29
|
</Text>
|
|
30
30
|
|
|
31
|
-
<Link onPress={nextDecade} aria-label="Next decade" padding="
|
|
31
|
+
<Link onPress={nextDecade} aria-label="Next decade" padding="sm" paddingR={0}>
|
|
32
32
|
<Icon name="arrow-right-double-line" />
|
|
33
33
|
</Link>
|
|
34
34
|
</View>
|
|
@@ -37,12 +37,12 @@ export function CalendarNav({ value, onChange, level, ...props }) {
|
|
|
37
37
|
|
|
38
38
|
return (
|
|
39
39
|
<View className="neko-calendar-nav" row centerV gap="xxs" height={30} {...props}>
|
|
40
|
-
<Link onPress={prevYear} aria-label="Previous year" padding="
|
|
40
|
+
<Link onPress={prevYear} aria-label="Previous year" padding="sm" paddingR={0}>
|
|
41
41
|
<Icon name="arrow-left-double-line" />
|
|
42
42
|
</Link>
|
|
43
43
|
|
|
44
44
|
{showMonth && (
|
|
45
|
-
<Link onPress={prevMonth} aria-label="Previous month" padding="
|
|
45
|
+
<Link onPress={prevMonth} aria-label="Previous month" padding="sm">
|
|
46
46
|
<Icon name="arrow-left-s-line" />
|
|
47
47
|
</Link>
|
|
48
48
|
)}
|
|
@@ -54,12 +54,12 @@ export function CalendarNav({ value, onChange, level, ...props }) {
|
|
|
54
54
|
</View>
|
|
55
55
|
|
|
56
56
|
{showMonth && (
|
|
57
|
-
<Link onPress={nextMonth} aria-label="Next month" padding="
|
|
57
|
+
<Link onPress={nextMonth} aria-label="Next month" padding="sm">
|
|
58
58
|
<Icon name="arrow-right-s-line" />
|
|
59
59
|
</Link>
|
|
60
60
|
)}
|
|
61
61
|
|
|
62
|
-
<Link onPress={nextYear} aria-label="Next year" padding="
|
|
62
|
+
<Link onPress={nextYear} aria-label="Next year" padding="sm" paddingL={0}>
|
|
63
63
|
<Icon name="arrow-right-double-line" />
|
|
64
64
|
</Link>
|
|
65
65
|
</View>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { View } from '../structure/View'
|
|
2
|
+
import { CarouselArrows } from './CarouselArrows'
|
|
3
|
+
import { CarouselDots } from './CarouselDots'
|
|
4
|
+
import { CarouselHandler } from './CarouselHandler'
|
|
5
|
+
import { CarouselSlider } from './CarouselSlider'
|
|
6
|
+
|
|
7
|
+
export function Carousel({
|
|
8
|
+
items,
|
|
9
|
+
activeIndex,
|
|
10
|
+
activeKey,
|
|
11
|
+
onChange,
|
|
12
|
+
afterChange,
|
|
13
|
+
autoplay,
|
|
14
|
+
autoplaySpeed,
|
|
15
|
+
draggable,
|
|
16
|
+
loop,
|
|
17
|
+
showDots,
|
|
18
|
+
showArrows,
|
|
19
|
+
floatingDots,
|
|
20
|
+
dotsProps,
|
|
21
|
+
arrowsProps,
|
|
22
|
+
children,
|
|
23
|
+
...rootProps
|
|
24
|
+
}) {
|
|
25
|
+
return (
|
|
26
|
+
<CarouselHandler
|
|
27
|
+
items={items}
|
|
28
|
+
activeIndex={activeIndex}
|
|
29
|
+
activeKey={activeKey}
|
|
30
|
+
onChange={onChange}
|
|
31
|
+
afterChange={afterChange}
|
|
32
|
+
autoplay={autoplay}
|
|
33
|
+
autoplaySpeed={autoplaySpeed}
|
|
34
|
+
draggable={draggable}
|
|
35
|
+
loop={loop}
|
|
36
|
+
>
|
|
37
|
+
<View {...rootProps}>
|
|
38
|
+
<View relative>
|
|
39
|
+
<CarouselSlider />
|
|
40
|
+
{showArrows && <CarouselArrows {...arrowsProps} />}
|
|
41
|
+
{showDots && floatingDots && <CarouselDots absolute bottom="xs" left={0} right={0} {...dotsProps} />}
|
|
42
|
+
</View>
|
|
43
|
+
{showDots && !floatingDots && <CarouselDots {...dotsProps} />}
|
|
44
|
+
{children}
|
|
45
|
+
</View>
|
|
46
|
+
</CarouselHandler>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import { Icon } from '../presentation/Icon'
|
|
4
|
+
import { Link } from '../actions/Link'
|
|
5
|
+
import { View } from '../structure/View'
|
|
6
|
+
import { useCarousel } from './CarouselHandler'
|
|
7
|
+
|
|
8
|
+
export function CarouselArrows({ iconSize = 'md', ...props }) {
|
|
9
|
+
const { goToNext, goToPrev, activeIndex, itemsCount, loop } = useCarousel()
|
|
10
|
+
|
|
11
|
+
const showPrev = loop || activeIndex > 0
|
|
12
|
+
const showNext = loop || activeIndex < itemsCount - 1
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<View
|
|
16
|
+
absoluteFill
|
|
17
|
+
row
|
|
18
|
+
centerV
|
|
19
|
+
paddingH="xs"
|
|
20
|
+
justify="space-between"
|
|
21
|
+
style={{ pointerEvents: 'none' }}
|
|
22
|
+
{...props}
|
|
23
|
+
>
|
|
24
|
+
{showPrev ? (
|
|
25
|
+
<Link onPress={goToPrev} bg="overlayBG_op80" round padding="xs" shadow style={{ pointerEvents: 'auto' }}>
|
|
26
|
+
<Icon name="arrow-left-s-line" text3 size={iconSize} />
|
|
27
|
+
</Link>
|
|
28
|
+
) : (
|
|
29
|
+
<View />
|
|
30
|
+
)}
|
|
31
|
+
{showNext ? (
|
|
32
|
+
<Link onPress={goToNext} bg="overlayBG_op80" round padding="xs" shadow style={{ pointerEvents: 'auto' }}>
|
|
33
|
+
<Icon name="arrow-right-s-line" text3 size={iconSize} />
|
|
34
|
+
</Link>
|
|
35
|
+
) : (
|
|
36
|
+
<View />
|
|
37
|
+
)}
|
|
38
|
+
</View>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import { Icon } from '../presentation/Icon'
|
|
4
|
+
import { Link } from '../actions/Link'
|
|
5
|
+
import { View } from '../structure/View'
|
|
6
|
+
import { useCarousel } from './CarouselHandler'
|
|
7
|
+
|
|
8
|
+
export function CarouselArrows({ iconSize = 'md', ...props }) {
|
|
9
|
+
const { goToNext, goToPrev, activeIndex, itemsCount, loop } = useCarousel()
|
|
10
|
+
|
|
11
|
+
const showPrev = loop || activeIndex > 0
|
|
12
|
+
const showNext = loop || activeIndex < itemsCount - 1
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<View
|
|
16
|
+
absoluteFill
|
|
17
|
+
row
|
|
18
|
+
centerV
|
|
19
|
+
paddingH="xs"
|
|
20
|
+
justify="space-between"
|
|
21
|
+
pointerEvents="box-none"
|
|
22
|
+
{...props}
|
|
23
|
+
>
|
|
24
|
+
{showPrev ? (
|
|
25
|
+
<Link onPress={goToPrev} bg="overlayBG_op80" round padding="xs" shadow>
|
|
26
|
+
<Icon name="arrow-left-s-line" text3 size={iconSize} />
|
|
27
|
+
</Link>
|
|
28
|
+
) : (
|
|
29
|
+
<View />
|
|
30
|
+
)}
|
|
31
|
+
{showNext ? (
|
|
32
|
+
<Link onPress={goToNext} bg="overlayBG_op80" round padding="xs" shadow>
|
|
33
|
+
<Icon name="arrow-right-s-line" text3 size={iconSize} />
|
|
34
|
+
</Link>
|
|
35
|
+
) : (
|
|
36
|
+
<View />
|
|
37
|
+
)}
|
|
38
|
+
</View>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import { Pressable } from '../actions/Pressable'
|
|
4
|
+
import { View } from '../structure/View'
|
|
5
|
+
import { useCarousel } from './CarouselHandler'
|
|
6
|
+
|
|
7
|
+
function Dot({ active, onPress }) {
|
|
8
|
+
return (
|
|
9
|
+
<Pressable
|
|
10
|
+
onPress={onPress}
|
|
11
|
+
width={active ? 20 : 8}
|
|
12
|
+
height={8}
|
|
13
|
+
round
|
|
14
|
+
bg={active ? 'primary' : 'text4_op30'}
|
|
15
|
+
style={{ transition: 'all 200ms ease-in-out' }}
|
|
16
|
+
/>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function CarouselDots(props) {
|
|
21
|
+
const { items, activeIndex, goTo } = useCarousel()
|
|
22
|
+
|
|
23
|
+
if (!items?.length) return null
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<View row center gap="xs" paddingV="sm" {...props}>
|
|
27
|
+
{items.map((item, index) => (
|
|
28
|
+
<Dot key={item.key} active={index === activeIndex} onPress={() => goTo(index)} />
|
|
29
|
+
))}
|
|
30
|
+
</View>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
|
|
3
|
+
|
|
4
|
+
import { Pressable } from '../actions/Pressable'
|
|
5
|
+
import { View } from '../structure/View'
|
|
6
|
+
import { useColors } from '../../theme/ThemeHandler'
|
|
7
|
+
import { useCarousel } from './CarouselHandler'
|
|
8
|
+
|
|
9
|
+
function Dot({ active, onPress }) {
|
|
10
|
+
const colors = useColors()
|
|
11
|
+
|
|
12
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
13
|
+
width: withTiming(active ? 20 : 8, { duration: 200 }),
|
|
14
|
+
backgroundColor: withTiming(active ? colors.primary : colors.text4_op30, { duration: 200 }),
|
|
15
|
+
}), [active])
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Pressable onPress={onPress}>
|
|
19
|
+
<Animated.View style={[{ height: 8, borderRadius: 4 }, animatedStyle]} />
|
|
20
|
+
</Pressable>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function CarouselDots(props) {
|
|
25
|
+
const { items, activeIndex, goTo } = useCarousel()
|
|
26
|
+
|
|
27
|
+
if (!items?.length) return null
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<View row center gap="xs" paddingV="sm" {...props}>
|
|
31
|
+
{items.map((item, index) => (
|
|
32
|
+
<Dot key={item.key} active={index === activeIndex} onPress={() => goTo(index)} />
|
|
33
|
+
))}
|
|
34
|
+
</View>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
const CarouselContext = React.createContext(null)
|
|
4
|
+
export const useCarousel = () => React.useContext(CarouselContext) || {}
|
|
5
|
+
|
|
6
|
+
export function CarouselHandler({
|
|
7
|
+
children,
|
|
8
|
+
items,
|
|
9
|
+
activeIndex: controlledIndex,
|
|
10
|
+
activeKey,
|
|
11
|
+
onChange,
|
|
12
|
+
afterChange,
|
|
13
|
+
autoplay,
|
|
14
|
+
autoplaySpeed = 3000,
|
|
15
|
+
draggable,
|
|
16
|
+
loop,
|
|
17
|
+
}) {
|
|
18
|
+
const itemsCount = items?.length || 0
|
|
19
|
+
const isControlled = controlledIndex !== undefined || activeKey !== undefined
|
|
20
|
+
|
|
21
|
+
const [internalIndex, setInternalIndex] = React.useState(() => {
|
|
22
|
+
if (controlledIndex !== undefined) return controlledIndex
|
|
23
|
+
if (activeKey !== undefined) return Math.max(0, items?.findIndex((i) => i.key === activeKey) || 0)
|
|
24
|
+
return 0
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const resolvedIndex = isControlled
|
|
28
|
+
? activeKey !== undefined
|
|
29
|
+
? Math.max(0, items?.findIndex((i) => i.key === activeKey) || 0)
|
|
30
|
+
: controlledIndex
|
|
31
|
+
: internalIndex
|
|
32
|
+
|
|
33
|
+
const activeItem = React.useMemo(() => items?.[resolvedIndex], [items, resolvedIndex])
|
|
34
|
+
|
|
35
|
+
const pausedRef = React.useRef(false)
|
|
36
|
+
|
|
37
|
+
const goTo = React.useCallback(
|
|
38
|
+
(index) => {
|
|
39
|
+
let next = index
|
|
40
|
+
if (loop) next = ((next % itemsCount) + itemsCount) % itemsCount
|
|
41
|
+
else next = Math.max(0, Math.min(next, itemsCount - 1))
|
|
42
|
+
|
|
43
|
+
if (!isControlled) setInternalIndex(next)
|
|
44
|
+
onChange?.(items?.[next]?.key, next)
|
|
45
|
+
},
|
|
46
|
+
[loop, itemsCount, isControlled, onChange, items]
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
const goToNext = React.useCallback(() => goTo(resolvedIndex + 1), [goTo, resolvedIndex])
|
|
50
|
+
const goToPrev = React.useCallback(() => goTo(resolvedIndex - 1), [goTo, resolvedIndex])
|
|
51
|
+
|
|
52
|
+
const pauseAutoplay = React.useCallback(() => {
|
|
53
|
+
pausedRef.current = true
|
|
54
|
+
}, [])
|
|
55
|
+
|
|
56
|
+
const resumeAutoplay = React.useCallback(() => {
|
|
57
|
+
pausedRef.current = false
|
|
58
|
+
}, [])
|
|
59
|
+
|
|
60
|
+
React.useEffect(() => {
|
|
61
|
+
if (!autoplay || itemsCount <= 1) return
|
|
62
|
+
|
|
63
|
+
const interval = setInterval(() => {
|
|
64
|
+
if (!pausedRef.current) goToNext()
|
|
65
|
+
}, autoplaySpeed)
|
|
66
|
+
|
|
67
|
+
return () => clearInterval(interval)
|
|
68
|
+
}, [autoplay, autoplaySpeed, itemsCount, resolvedIndex, goToNext])
|
|
69
|
+
|
|
70
|
+
const value = {
|
|
71
|
+
items,
|
|
72
|
+
activeIndex: resolvedIndex,
|
|
73
|
+
activeItem,
|
|
74
|
+
itemsCount,
|
|
75
|
+
goTo,
|
|
76
|
+
goToNext,
|
|
77
|
+
goToPrev,
|
|
78
|
+
afterChange,
|
|
79
|
+
draggable,
|
|
80
|
+
loop,
|
|
81
|
+
pauseAutoplay,
|
|
82
|
+
resumeAutoplay,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return <CarouselContext.Provider value={value}>{children}</CarouselContext.Provider>
|
|
86
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import { View } from '../structure/View'
|
|
4
|
+
import { useCarousel } from './CarouselHandler'
|
|
5
|
+
|
|
6
|
+
function SlideContent({ item }) {
|
|
7
|
+
const Content = React.useMemo(
|
|
8
|
+
() => item.render || item.renderContent || item.Content,
|
|
9
|
+
[item.render, item.renderContent, item.Content]
|
|
10
|
+
)
|
|
11
|
+
return Content ? <Content /> : null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function CarouselSlider() {
|
|
15
|
+
const { items, activeIndex, itemsCount, draggable, loop, afterChange, goToNext, goToPrev, pauseAutoplay, resumeAutoplay } =
|
|
16
|
+
useCarousel()
|
|
17
|
+
|
|
18
|
+
const containerRef = React.useRef(null)
|
|
19
|
+
const [isDragging, setIsDragging] = React.useState(false)
|
|
20
|
+
const [dragOffset, setDragOffset] = React.useState(0)
|
|
21
|
+
const startXRef = React.useRef(0)
|
|
22
|
+
const startTimeRef = React.useRef(0)
|
|
23
|
+
const prevItemsRef = React.useRef(items)
|
|
24
|
+
const skipTransitionRef = React.useRef(false)
|
|
25
|
+
|
|
26
|
+
if (items !== prevItemsRef.current) {
|
|
27
|
+
skipTransitionRef.current = true
|
|
28
|
+
prevItemsRef.current = items
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
React.useEffect(() => {
|
|
32
|
+
if (skipTransitionRef.current) {
|
|
33
|
+
const id = requestAnimationFrame(() => {
|
|
34
|
+
skipTransitionRef.current = false
|
|
35
|
+
})
|
|
36
|
+
return () => cancelAnimationFrame(id)
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
if (!items?.length) return null
|
|
41
|
+
|
|
42
|
+
const baseTranslate = -(activeIndex * 100) / itemsCount
|
|
43
|
+
const dragPercent =
|
|
44
|
+
isDragging && containerRef.current ? (dragOffset / containerRef.current.offsetWidth) * (100 / itemsCount) : 0
|
|
45
|
+
const transformX = baseTranslate + dragPercent
|
|
46
|
+
|
|
47
|
+
const dragStateRef = React.useRef({ isDragging: false, dragOffset: 0 })
|
|
48
|
+
|
|
49
|
+
const handlePointerDown = (e) => {
|
|
50
|
+
if (!draggable) return
|
|
51
|
+
startXRef.current = e.clientX
|
|
52
|
+
startTimeRef.current = Date.now()
|
|
53
|
+
dragStateRef.current = { isDragging: true, dragOffset: 0 }
|
|
54
|
+
setIsDragging(true)
|
|
55
|
+
setDragOffset(0)
|
|
56
|
+
pauseAutoplay()
|
|
57
|
+
|
|
58
|
+
const onMove = (ev) => {
|
|
59
|
+
const raw = ev.clientX - startXRef.current
|
|
60
|
+
let offset = raw
|
|
61
|
+
if (!loop) {
|
|
62
|
+
const containerWidth = containerRef.current?.offsetWidth || 1
|
|
63
|
+
const atStart = activeIndex === 0 && raw > 0
|
|
64
|
+
const atEnd = activeIndex === itemsCount - 1 && raw < 0
|
|
65
|
+
if (atStart || atEnd) offset = raw * 0.3
|
|
66
|
+
}
|
|
67
|
+
dragStateRef.current.dragOffset = offset
|
|
68
|
+
setDragOffset(offset)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const onUp = () => {
|
|
72
|
+
window.removeEventListener('pointermove', onMove)
|
|
73
|
+
window.removeEventListener('pointerup', onUp)
|
|
74
|
+
window.removeEventListener('pointercancel', onUp)
|
|
75
|
+
|
|
76
|
+
const offset = dragStateRef.current.dragOffset
|
|
77
|
+
const containerWidth = containerRef.current?.offsetWidth || 1
|
|
78
|
+
const threshold = containerWidth * 0.25
|
|
79
|
+
const elapsed = Date.now() - startTimeRef.current
|
|
80
|
+
const velocity = Math.abs(offset) / (elapsed || 1)
|
|
81
|
+
|
|
82
|
+
if (offset < -threshold || (offset < 0 && velocity > 0.5)) {
|
|
83
|
+
goToNext()
|
|
84
|
+
} else if (offset > threshold || (offset > 0 && velocity > 0.5)) {
|
|
85
|
+
goToPrev()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
dragStateRef.current = { isDragging: false, dragOffset: 0 }
|
|
89
|
+
setIsDragging(false)
|
|
90
|
+
setDragOffset(0)
|
|
91
|
+
resumeAutoplay()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
window.addEventListener('pointermove', onMove)
|
|
95
|
+
window.addEventListener('pointerup', onUp)
|
|
96
|
+
window.addEventListener('pointercancel', onUp)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<View hiddenOverflow fullW ref={containerRef}>
|
|
101
|
+
<View
|
|
102
|
+
row
|
|
103
|
+
style={{
|
|
104
|
+
width: `${itemsCount * 100}%`,
|
|
105
|
+
transform: `translateX(${transformX}%)`,
|
|
106
|
+
transition: isDragging || skipTransitionRef.current ? 'none' : 'transform 300ms ease-in-out',
|
|
107
|
+
touchAction: 'pan-y',
|
|
108
|
+
cursor: draggable ? (isDragging ? 'grabbing' : 'grab') : undefined,
|
|
109
|
+
userSelect: 'none',
|
|
110
|
+
}}
|
|
111
|
+
onTransitionEnd={afterChange ? (e) => {
|
|
112
|
+
if (e.propertyName === 'transform') afterChange(items?.[activeIndex]?.key, activeIndex)
|
|
113
|
+
} : undefined}
|
|
114
|
+
onPointerDown={draggable ? handlePointerDown : undefined}
|
|
115
|
+
>
|
|
116
|
+
{items.map((item) => (
|
|
117
|
+
<View key={item.key} style={{ width: `${100 / itemsCount}%` }}>
|
|
118
|
+
<SlideContent item={item} />
|
|
119
|
+
</View>
|
|
120
|
+
))}
|
|
121
|
+
</View>
|
|
122
|
+
</View>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
|
|
3
|
+
import Animated, { useSharedValue, useAnimatedStyle, withTiming, runOnJS } from 'react-native-reanimated'
|
|
4
|
+
|
|
5
|
+
import { View } from '../structure/View'
|
|
6
|
+
import { useCarousel } from './CarouselHandler'
|
|
7
|
+
|
|
8
|
+
function SlideContent({ item }) {
|
|
9
|
+
const Content = React.useMemo(
|
|
10
|
+
() => item.render || item.renderContent || item.Content,
|
|
11
|
+
[item.render, item.renderContent, item.Content]
|
|
12
|
+
)
|
|
13
|
+
return Content ? <Content /> : null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function clampIndex(index, count, loop) {
|
|
17
|
+
'worklet'
|
|
18
|
+
if (loop) return ((index % count) + count) % count
|
|
19
|
+
return Math.max(0, Math.min(index, count - 1))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function CarouselSlider() {
|
|
23
|
+
const { items, activeIndex, itemsCount, draggable, goTo, afterChange, pauseAutoplay, resumeAutoplay, loop } =
|
|
24
|
+
useCarousel()
|
|
25
|
+
|
|
26
|
+
const [slideWidth, setSlideWidth] = React.useState(0)
|
|
27
|
+
const translateX = useSharedValue(0)
|
|
28
|
+
const gestureStartX = useSharedValue(0)
|
|
29
|
+
const prevItemsRef = React.useRef(items)
|
|
30
|
+
|
|
31
|
+
React.useEffect(() => {
|
|
32
|
+
if (slideWidth > 0) {
|
|
33
|
+
if (prevItemsRef.current !== items) {
|
|
34
|
+
prevItemsRef.current = items
|
|
35
|
+
translateX.value = -activeIndex * slideWidth
|
|
36
|
+
} else {
|
|
37
|
+
translateX.value = withTiming(-activeIndex * slideWidth, { duration: 300 }, (finished) => {
|
|
38
|
+
if (finished && afterChange) runOnJS(afterChange)(items?.[activeIndex]?.key, activeIndex)
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}, [activeIndex, slideWidth, items])
|
|
43
|
+
|
|
44
|
+
const onLayout = React.useCallback((e) => {
|
|
45
|
+
setSlideWidth(e.nativeEvent.layout.width)
|
|
46
|
+
}, [])
|
|
47
|
+
|
|
48
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
49
|
+
transform: [{ translateX: translateX.value }],
|
|
50
|
+
}))
|
|
51
|
+
|
|
52
|
+
if (!items?.length) return null
|
|
53
|
+
|
|
54
|
+
const minTranslate = -(itemsCount - 1) * slideWidth
|
|
55
|
+
const maxTranslate = 0
|
|
56
|
+
|
|
57
|
+
const panGesture = Gesture.Pan()
|
|
58
|
+
.activeOffsetX([-10, 10])
|
|
59
|
+
.failOffsetY([-10, 10])
|
|
60
|
+
.onStart(() => {
|
|
61
|
+
gestureStartX.value = translateX.value
|
|
62
|
+
runOnJS(pauseAutoplay)()
|
|
63
|
+
})
|
|
64
|
+
.onUpdate((e) => {
|
|
65
|
+
const raw = gestureStartX.value + e.translationX
|
|
66
|
+
if (loop) {
|
|
67
|
+
translateX.value = raw
|
|
68
|
+
} else {
|
|
69
|
+
// Rubber band resistance at edges
|
|
70
|
+
if (raw > maxTranslate) {
|
|
71
|
+
translateX.value = maxTranslate + (raw - maxTranslate) * 0.3
|
|
72
|
+
} else if (raw < minTranslate) {
|
|
73
|
+
translateX.value = minTranslate + (raw - minTranslate) * 0.3
|
|
74
|
+
} else {
|
|
75
|
+
translateX.value = raw
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
.onEnd((e) => {
|
|
80
|
+
const threshold = slideWidth * 0.25
|
|
81
|
+
let targetIndex = activeIndex
|
|
82
|
+
|
|
83
|
+
if (e.translationX < -threshold || e.velocityX < -500) {
|
|
84
|
+
targetIndex = activeIndex + 1
|
|
85
|
+
} else if (e.translationX > threshold || e.velocityX > 500) {
|
|
86
|
+
targetIndex = activeIndex - 1
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const clamped = clampIndex(targetIndex, itemsCount, loop)
|
|
90
|
+
translateX.value = withTiming(-clamped * slideWidth, { duration: 300 })
|
|
91
|
+
runOnJS(goTo)(targetIndex)
|
|
92
|
+
runOnJS(resumeAutoplay)()
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const track = (
|
|
96
|
+
<Animated.View style={[{ flexDirection: 'row', width: slideWidth * itemsCount }, animatedStyle]}>
|
|
97
|
+
{items.map((item) => (
|
|
98
|
+
<View key={item.key} style={{ width: slideWidth }}>
|
|
99
|
+
<SlideContent item={item} />
|
|
100
|
+
</View>
|
|
101
|
+
))}
|
|
102
|
+
</Animated.View>
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<View hiddenOverflow fullW onLayout={onLayout}>
|
|
107
|
+
{draggable ? <GestureDetector gesture={panGesture}>{track}</GestureDetector> : track}
|
|
108
|
+
</View>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import { Carousel } from './Carousel'
|
|
4
|
+
|
|
5
|
+
function buildItems(value, min, max, renderSlideRef) {
|
|
6
|
+
const items = []
|
|
7
|
+
if (min === undefined || value - 1 >= min) {
|
|
8
|
+
items.push({ key: value - 1, render: () => renderSlideRef.current(value - 1) })
|
|
9
|
+
}
|
|
10
|
+
items.push({ key: value, render: () => renderSlideRef.current(value) })
|
|
11
|
+
if (max === undefined || value + 1 <= max) {
|
|
12
|
+
items.push({ key: value + 1, render: () => renderSlideRef.current(value + 1) })
|
|
13
|
+
}
|
|
14
|
+
return items
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function InfiniteCarousel({ value, onChange, renderSlide, min, max, ...carouselProps }) {
|
|
18
|
+
const renderSlideRef = React.useRef(renderSlide)
|
|
19
|
+
renderSlideRef.current = renderSlide
|
|
20
|
+
|
|
21
|
+
const [items, setItems] = React.useState(() => buildItems(value, min, max, renderSlideRef))
|
|
22
|
+
const [activeKey, setActiveKey] = React.useState(value)
|
|
23
|
+
|
|
24
|
+
React.useEffect(() => {
|
|
25
|
+
setItems(buildItems(value, min, max, renderSlideRef))
|
|
26
|
+
setActiveKey(value)
|
|
27
|
+
}, [value, min, max])
|
|
28
|
+
|
|
29
|
+
const handleChange = React.useCallback((key) => {
|
|
30
|
+
setActiveKey(key)
|
|
31
|
+
}, [])
|
|
32
|
+
|
|
33
|
+
const handleAfterChange = React.useCallback(
|
|
34
|
+
(key) => {
|
|
35
|
+
if (key !== value) onChange?.(key)
|
|
36
|
+
},
|
|
37
|
+
[value, onChange]
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<Carousel
|
|
42
|
+
items={items}
|
|
43
|
+
activeKey={activeKey}
|
|
44
|
+
onChange={handleChange}
|
|
45
|
+
afterChange={handleAfterChange}
|
|
46
|
+
draggable
|
|
47
|
+
{...carouselProps}
|
|
48
|
+
/>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
|
|
3
3
|
import { FormWrapperComponent } from './FormWrapperComponent'
|
|
4
|
+
import { KeyboardDismissButton } from '../keyboard/KeyboardDismissButton'
|
|
4
5
|
import { LoadingView } from '../state/LoadingView'
|
|
5
6
|
import { useNewForm } from './useNewForm'
|
|
6
7
|
|
|
@@ -24,6 +25,7 @@ export function Form({ form, onSubmit, onValuesChange, initialValues, children,
|
|
|
24
25
|
<LoadingView active={loading} noWrapper>
|
|
25
26
|
<FormWrapperComponent form={form} onSubmit={onSubmit} gap="md" {...props}>
|
|
26
27
|
{children}
|
|
28
|
+
<KeyboardDismissButton />
|
|
27
29
|
</FormWrapperComponent>
|
|
28
30
|
</LoadingView>
|
|
29
31
|
</FormContext.Provider>
|