@butternutbox/pawprint-native 1.6.2 → 2.0.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/.turbo/turbo-build.log +9 -9
- package/CHANGELOG.md +60 -0
- package/COMPONENT_GUIDELINES.md +57 -1
- package/dist/index.cjs +1459 -1380
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +81 -117
- package/dist/index.d.ts +81 -117
- package/dist/index.js +1206 -1125
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/__mocks__/react-native.tsx +33 -5
- package/src/__mocks__/rn-primitives/portal.tsx +71 -0
- package/src/components/atoms/Input/InputField.tsx +12 -2
- package/src/components/molecules/Drawer/Drawer.test.tsx +54 -2
- package/src/components/molecules/Drawer/DrawerBody.tsx +2 -17
- package/src/components/molecules/Drawer/DrawerContent.tsx +49 -2
- package/src/components/molecules/Drawer/DrawerOverlay.tsx +28 -16
- package/src/components/molecules/SelectField/AsyncSearch.test.tsx +373 -0
- package/src/components/molecules/SelectField/SelectDrawerField.stories.tsx +192 -0
- package/src/components/molecules/SelectField/SelectDrawerField.test.tsx +210 -0
- package/src/components/molecules/SelectField/SelectDrawerField.tsx +245 -0
- package/src/components/molecules/SelectField/SelectField.stories.tsx +221 -421
- package/src/components/molecules/SelectField/SelectField.test.tsx +214 -662
- package/src/components/molecules/SelectField/SelectField.tsx +269 -539
- package/src/components/molecules/SelectField/SelectFieldDropdown.tsx +90 -0
- package/src/components/molecules/SelectField/SelectFieldList.tsx +88 -0
- package/src/components/molecules/SelectField/SelectFieldOption.tsx +90 -0
- package/src/components/molecules/SelectField/core/index.ts +8 -0
- package/src/components/molecules/SelectField/core/listHeight.test.ts +22 -0
- package/src/components/molecules/SelectField/core/listHeight.ts +8 -0
- package/src/components/molecules/SelectField/core/options.ts +35 -0
- package/src/components/molecules/SelectField/core/useSelectOptions.ts +116 -0
- package/src/components/molecules/SelectField/index.ts +12 -9
- package/src/index.ts +0 -1
- package/src/test-utils.tsx +5 -1
- package/vitest.config.ts +7 -0
- package/src/components/molecules/SelectField/SelectFieldContent.tsx +0 -81
- package/src/components/molecules/SelectField/SelectFieldItem.tsx +0 -133
- package/src/components/molecules/SelectField/SelectFieldTrigger.tsx +0 -188
- package/src/components/molecules/SelectField/SelectFieldValue.tsx +0 -33
- package/src/components/molecules/SelectField/hooks/index.ts +0 -2
- package/src/components/molecules/SelectField/hooks/useSelectField.ts +0 -84
- package/src/hooks/index.ts +0 -1
- package/src/hooks/useDrawerKeyboardAvoidance.ts +0 -29
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { ScrollView, View, ViewProps } from "react-native"
|
|
3
|
+
import styled from "@emotion/native"
|
|
4
|
+
import { parseTokenValue } from "../../../utils"
|
|
5
|
+
|
|
6
|
+
export type SelectFieldPlacement = "below" | "above"
|
|
7
|
+
|
|
8
|
+
export type SelectFieldAnchorRect = {
|
|
9
|
+
x: number
|
|
10
|
+
y: number
|
|
11
|
+
width: number
|
|
12
|
+
height: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type SelectFieldDropdownOwnProps = {
|
|
16
|
+
anchor: SelectFieldAnchorRect
|
|
17
|
+
placement: SelectFieldPlacement
|
|
18
|
+
maxHeight: number
|
|
19
|
+
screenHeight: number
|
|
20
|
+
children?: React.ReactNode
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type SelectFieldDropdownProps = SelectFieldDropdownOwnProps &
|
|
24
|
+
Omit<ViewProps, keyof SelectFieldDropdownOwnProps>
|
|
25
|
+
|
|
26
|
+
const StyledDropdown = styled(View)<{
|
|
27
|
+
dropdownPlacement: SelectFieldPlacement
|
|
28
|
+
anchorRect: SelectFieldAnchorRect
|
|
29
|
+
screenHeight: number
|
|
30
|
+
}>(({ theme, dropdownPlacement, anchorRect, screenHeight }) => {
|
|
31
|
+
const { dropdown } = theme.tokens.components.dropdownList
|
|
32
|
+
const { spacing } = theme.tokens.components.inputs
|
|
33
|
+
const shadow = dropdown.list.dropshadow.default
|
|
34
|
+
const offset = parseTokenValue(spacing.gap)
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
position: "absolute",
|
|
38
|
+
left: anchorRect.x,
|
|
39
|
+
width: anchorRect.width,
|
|
40
|
+
...(dropdownPlacement === "below"
|
|
41
|
+
? { top: anchorRect.y + anchorRect.height + offset }
|
|
42
|
+
: { bottom: screenHeight - anchorRect.y + offset }),
|
|
43
|
+
minWidth: parseTokenValue(dropdown.list.size.minWidth),
|
|
44
|
+
borderRadius: parseTokenValue(dropdown.list.borderRadius.default),
|
|
45
|
+
shadowColor: shadow.color,
|
|
46
|
+
shadowOffset: {
|
|
47
|
+
width: parseTokenValue(shadow.offsetX),
|
|
48
|
+
height: parseTokenValue(shadow.offsetY)
|
|
49
|
+
},
|
|
50
|
+
shadowOpacity: 1,
|
|
51
|
+
shadowRadius: parseTokenValue(shadow.blur),
|
|
52
|
+
elevation: 8
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const StyledSurface = styled(View)(({ theme }) => {
|
|
57
|
+
const { dropdown } = theme.tokens.components.dropdownList
|
|
58
|
+
const { colour } = theme.tokens.components.inputs
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
backgroundColor: colour.field.background.default,
|
|
62
|
+
borderRadius: parseTokenValue(dropdown.list.borderRadius.default),
|
|
63
|
+
overflow: "hidden"
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
export const SelectFieldDropdown = React.forwardRef<
|
|
68
|
+
View,
|
|
69
|
+
SelectFieldDropdownProps
|
|
70
|
+
>(({ anchor, placement, maxHeight, screenHeight, children, ...rest }, ref) => (
|
|
71
|
+
<StyledDropdown
|
|
72
|
+
ref={ref}
|
|
73
|
+
dropdownPlacement={placement}
|
|
74
|
+
anchorRect={anchor}
|
|
75
|
+
screenHeight={screenHeight}
|
|
76
|
+
{...rest}
|
|
77
|
+
>
|
|
78
|
+
<StyledSurface style={{ maxHeight }}>
|
|
79
|
+
<ScrollView
|
|
80
|
+
keyboardShouldPersistTaps="always"
|
|
81
|
+
nestedScrollEnabled
|
|
82
|
+
showsVerticalScrollIndicator
|
|
83
|
+
>
|
|
84
|
+
{children}
|
|
85
|
+
</ScrollView>
|
|
86
|
+
</StyledSurface>
|
|
87
|
+
</StyledDropdown>
|
|
88
|
+
))
|
|
89
|
+
|
|
90
|
+
SelectFieldDropdown.displayName = "SelectField.Dropdown"
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { View } from "react-native"
|
|
3
|
+
import styled from "@emotion/native"
|
|
4
|
+
import { Spinner } from "../../atoms/Spinner"
|
|
5
|
+
import { Typography } from "../../atoms/Typography"
|
|
6
|
+
import { parseTokenValue } from "../../../utils"
|
|
7
|
+
import { SelectFieldOption } from "./SelectFieldOption"
|
|
8
|
+
import type { SelectFieldOptionData, SelectOptionsState } from "./core"
|
|
9
|
+
|
|
10
|
+
export type SelectFieldListProps = {
|
|
11
|
+
state: SelectOptionsState
|
|
12
|
+
selectedValue: string | null | undefined
|
|
13
|
+
onSelect: (option: SelectFieldOptionData) => void
|
|
14
|
+
noResultsText: string
|
|
15
|
+
errorText: string
|
|
16
|
+
searchHintText: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const StyledStatusRow = styled(View)(({ theme }) => {
|
|
20
|
+
const { dropdown } = theme.tokens.components.dropdownList
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
height: parseTokenValue(dropdown.listItem.size.height),
|
|
24
|
+
paddingHorizontal: parseTokenValue(
|
|
25
|
+
dropdown.listItem.spacing.horiztonalPadding
|
|
26
|
+
),
|
|
27
|
+
alignItems: "center",
|
|
28
|
+
justifyContent: "center"
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
export const SelectFieldList = ({
|
|
33
|
+
state,
|
|
34
|
+
selectedValue,
|
|
35
|
+
onSelect,
|
|
36
|
+
noResultsText,
|
|
37
|
+
errorText,
|
|
38
|
+
searchHintText
|
|
39
|
+
}: SelectFieldListProps) => {
|
|
40
|
+
if (state.needsMoreInput) {
|
|
41
|
+
return (
|
|
42
|
+
<StyledStatusRow>
|
|
43
|
+
<Typography size="sm">{searchHintText}</Typography>
|
|
44
|
+
</StyledStatusRow>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (state.isLoading) {
|
|
49
|
+
return (
|
|
50
|
+
<StyledStatusRow accessibilityLabel="Loading results">
|
|
51
|
+
<Spinner size="sm" />
|
|
52
|
+
</StyledStatusRow>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (state.hasError) {
|
|
57
|
+
return (
|
|
58
|
+
<StyledStatusRow>
|
|
59
|
+
<Typography size="sm">{errorText}</Typography>
|
|
60
|
+
</StyledStatusRow>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (state.options.length === 0) {
|
|
65
|
+
return (
|
|
66
|
+
<StyledStatusRow>
|
|
67
|
+
<Typography size="sm">{noResultsText}</Typography>
|
|
68
|
+
</StyledStatusRow>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<>
|
|
74
|
+
{state.options.map((option) => (
|
|
75
|
+
<SelectFieldOption
|
|
76
|
+
key={option.value}
|
|
77
|
+
label={option.label}
|
|
78
|
+
leadingIcon={option.leadingIcon}
|
|
79
|
+
disabled={option.disabled}
|
|
80
|
+
isSelected={option.value === selectedValue}
|
|
81
|
+
onPress={() => onSelect(option)}
|
|
82
|
+
/>
|
|
83
|
+
))}
|
|
84
|
+
</>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
SelectFieldList.displayName = "SelectField.List"
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { Pressable, PressableProps, View } from "react-native"
|
|
3
|
+
import styled from "@emotion/native"
|
|
4
|
+
import { useTheme } from "@emotion/react"
|
|
5
|
+
import { Typography } from "../../atoms/Typography"
|
|
6
|
+
import { parseTokenValue } from "../../../utils"
|
|
7
|
+
|
|
8
|
+
type SelectFieldOptionOwnProps = {
|
|
9
|
+
label: string
|
|
10
|
+
leadingIcon?: React.ReactNode
|
|
11
|
+
disabled?: boolean
|
|
12
|
+
isSelected?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type SelectFieldOptionProps = SelectFieldOptionOwnProps &
|
|
16
|
+
Omit<PressableProps, keyof SelectFieldOptionOwnProps>
|
|
17
|
+
|
|
18
|
+
const StyledOption = styled(Pressable)(({ theme }) => {
|
|
19
|
+
const { dropdown } = theme.tokens.components.dropdownList
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
flexDirection: "row",
|
|
23
|
+
alignItems: "center",
|
|
24
|
+
gap: parseTokenValue(dropdown.listItem.content.gap),
|
|
25
|
+
height: parseTokenValue(dropdown.listItem.size.height),
|
|
26
|
+
paddingHorizontal: parseTokenValue(
|
|
27
|
+
dropdown.listItem.spacing.horiztonalPadding
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const StyledIconSlot = styled(View)({
|
|
33
|
+
flexShrink: 0,
|
|
34
|
+
alignItems: "center",
|
|
35
|
+
justifyContent: "center"
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const StyledTextSlot = styled(View)({
|
|
39
|
+
flex: 1,
|
|
40
|
+
minWidth: 0
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
export const SelectFieldOption = React.forwardRef<View, SelectFieldOptionProps>(
|
|
44
|
+
(
|
|
45
|
+
{ label, leadingIcon, disabled = false, isSelected = false, ...rest },
|
|
46
|
+
ref
|
|
47
|
+
) => {
|
|
48
|
+
const theme = useTheme()
|
|
49
|
+
const { dropdown } = theme.tokens.components.dropdownList
|
|
50
|
+
const { colour, typography } = dropdown.listItem
|
|
51
|
+
|
|
52
|
+
const backgroundColor = disabled
|
|
53
|
+
? colour.background.disabled
|
|
54
|
+
: isSelected
|
|
55
|
+
? colour.background.hover
|
|
56
|
+
: colour.background.default
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<StyledOption
|
|
60
|
+
ref={ref}
|
|
61
|
+
disabled={disabled}
|
|
62
|
+
accessibilityRole="button"
|
|
63
|
+
accessibilityState={{ disabled, selected: isSelected }}
|
|
64
|
+
style={{ backgroundColor }}
|
|
65
|
+
{...rest}
|
|
66
|
+
>
|
|
67
|
+
{leadingIcon && <StyledIconSlot>{leadingIcon}</StyledIconSlot>}
|
|
68
|
+
<StyledTextSlot>
|
|
69
|
+
<Typography
|
|
70
|
+
token={
|
|
71
|
+
isSelected
|
|
72
|
+
? typography.placeholder.hover
|
|
73
|
+
: typography.placeholder.default
|
|
74
|
+
}
|
|
75
|
+
color={
|
|
76
|
+
disabled
|
|
77
|
+
? colour.text.placeholder.disabled
|
|
78
|
+
: colour.text.placeholder.default
|
|
79
|
+
}
|
|
80
|
+
numberOfLines={1}
|
|
81
|
+
>
|
|
82
|
+
{label}
|
|
83
|
+
</Typography>
|
|
84
|
+
</StyledTextSlot>
|
|
85
|
+
</StyledOption>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
SelectFieldOption.displayName = "SelectField.Option"
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { matchOptions, labelFor } from "./options"
|
|
2
|
+
export type { SelectFieldOptionData } from "./options"
|
|
3
|
+
export { useSelectOptions } from "./useSelectOptions"
|
|
4
|
+
export type {
|
|
5
|
+
SelectOptionsResolver,
|
|
6
|
+
SelectOptionsState
|
|
7
|
+
} from "./useSelectOptions"
|
|
8
|
+
export { drawerListMaxHeight } from "./listHeight"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import { drawerListMaxHeight } from "./listHeight"
|
|
3
|
+
|
|
4
|
+
describe("drawerListMaxHeight", () => {
|
|
5
|
+
it("takes half the window on a phone", () => {
|
|
6
|
+
expect(drawerListMaxHeight(812)).toBe(406)
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it("caps a tall screen rather than filling it", () => {
|
|
10
|
+
expect(drawerListMaxHeight(1180)).toBe(480)
|
|
11
|
+
expect(drawerListMaxHeight(2000)).toBe(480)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it("never exceeds the ceiling", () => {
|
|
15
|
+
expect(drawerListMaxHeight(10000)).toBeLessThanOrEqual(480)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it("prefers an explicit override", () => {
|
|
19
|
+
expect(drawerListMaxHeight(1180, 200)).toBe(200)
|
|
20
|
+
expect(drawerListMaxHeight(400, 900)).toBe(900)
|
|
21
|
+
})
|
|
22
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
|
|
3
|
+
export type SelectFieldOptionData = {
|
|
4
|
+
value: string
|
|
5
|
+
label: string
|
|
6
|
+
leadingIcon?: React.ReactNode
|
|
7
|
+
disabled?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const matchOptions = (
|
|
11
|
+
options: SelectFieldOptionData[],
|
|
12
|
+
query: string
|
|
13
|
+
): SelectFieldOptionData[] => {
|
|
14
|
+
const needle = query.trim().toLowerCase()
|
|
15
|
+
if (needle.length === 0) return options
|
|
16
|
+
|
|
17
|
+
const startsWith: SelectFieldOptionData[] = []
|
|
18
|
+
const contains: SelectFieldOptionData[] = []
|
|
19
|
+
|
|
20
|
+
options.forEach((option) => {
|
|
21
|
+
const haystack = option.label.toLowerCase()
|
|
22
|
+
if (haystack.startsWith(needle)) {
|
|
23
|
+
startsWith.push(option)
|
|
24
|
+
} else if (haystack.includes(needle)) {
|
|
25
|
+
contains.push(option)
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
return [...startsWith, ...contains]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const labelFor = (
|
|
33
|
+
options: SelectFieldOptionData[],
|
|
34
|
+
value: string | null | undefined
|
|
35
|
+
): string => options.find((option) => option.value === value)?.label ?? ""
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { matchOptions } from "./options"
|
|
3
|
+
import type { SelectFieldOptionData } from "./options"
|
|
4
|
+
|
|
5
|
+
export type SelectOptionsResolver = (
|
|
6
|
+
query: string
|
|
7
|
+
) => Promise<SelectFieldOptionData[]>
|
|
8
|
+
|
|
9
|
+
type UseSelectOptionsArgs = {
|
|
10
|
+
options: SelectFieldOptionData[]
|
|
11
|
+
query: string
|
|
12
|
+
onSearchQuery?: SelectOptionsResolver
|
|
13
|
+
debounceMs: number
|
|
14
|
+
minQueryLength: number
|
|
15
|
+
enabled: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type SelectOptionsState = {
|
|
19
|
+
options: SelectFieldOptionData[]
|
|
20
|
+
isLoading: boolean
|
|
21
|
+
hasError: boolean
|
|
22
|
+
needsMoreInput: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const useSelectOptions = ({
|
|
26
|
+
options,
|
|
27
|
+
query,
|
|
28
|
+
onSearchQuery,
|
|
29
|
+
debounceMs,
|
|
30
|
+
minQueryLength,
|
|
31
|
+
enabled
|
|
32
|
+
}: UseSelectOptionsArgs): SelectOptionsState => {
|
|
33
|
+
const isQueryMode = enabled && typeof onSearchQuery === "function"
|
|
34
|
+
const trimmedQuery = query.trim()
|
|
35
|
+
const meetsMinimum = trimmedQuery.length >= minQueryLength
|
|
36
|
+
const isQueryActive = isQueryMode && trimmedQuery.length > 0 && meetsMinimum
|
|
37
|
+
const needsMoreInput = isQueryMode && !meetsMinimum
|
|
38
|
+
|
|
39
|
+
const [results, setResults] = React.useState<SelectFieldOptionData[]>([])
|
|
40
|
+
const [isLoading, setIsLoading] = React.useState(false)
|
|
41
|
+
const [hasError, setHasError] = React.useState(false)
|
|
42
|
+
|
|
43
|
+
const resolverRef = React.useRef(onSearchQuery)
|
|
44
|
+
React.useEffect(() => {
|
|
45
|
+
resolverRef.current = onSearchQuery
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const requestIdRef = React.useRef(0)
|
|
49
|
+
|
|
50
|
+
React.useEffect(() => {
|
|
51
|
+
if (!isQueryMode) return
|
|
52
|
+
|
|
53
|
+
if (trimmedQuery.length === 0 || trimmedQuery.length < minQueryLength) {
|
|
54
|
+
requestIdRef.current += 1
|
|
55
|
+
setResults([])
|
|
56
|
+
setIsLoading(false)
|
|
57
|
+
setHasError(false)
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const requestId = requestIdRef.current + 1
|
|
62
|
+
requestIdRef.current = requestId
|
|
63
|
+
setIsLoading(true)
|
|
64
|
+
setHasError(false)
|
|
65
|
+
|
|
66
|
+
const timer = setTimeout(() => {
|
|
67
|
+
const resolve = resolverRef.current
|
|
68
|
+
if (!resolve) {
|
|
69
|
+
setIsLoading(false)
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
Promise.resolve(resolve(trimmedQuery))
|
|
74
|
+
.then((items) => {
|
|
75
|
+
if (requestIdRef.current !== requestId) return
|
|
76
|
+
setResults(items)
|
|
77
|
+
setIsLoading(false)
|
|
78
|
+
})
|
|
79
|
+
.catch(() => {
|
|
80
|
+
if (requestIdRef.current !== requestId) return
|
|
81
|
+
setResults([])
|
|
82
|
+
setHasError(true)
|
|
83
|
+
setIsLoading(false)
|
|
84
|
+
})
|
|
85
|
+
}, debounceMs)
|
|
86
|
+
|
|
87
|
+
return () => clearTimeout(timer)
|
|
88
|
+
}, [isQueryMode, trimmedQuery, debounceMs, minQueryLength])
|
|
89
|
+
|
|
90
|
+
const localOptions = React.useMemo(
|
|
91
|
+
() => (enabled ? matchOptions(options, query) : options),
|
|
92
|
+
[enabled, options, query]
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if (needsMoreInput) {
|
|
96
|
+
const hasDefaultList = trimmedQuery.length === 0 && options.length > 0
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
options: hasDefaultList ? options : [],
|
|
100
|
+
isLoading: false,
|
|
101
|
+
hasError: false,
|
|
102
|
+
needsMoreInput: !hasDefaultList
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!isQueryActive) {
|
|
107
|
+
return {
|
|
108
|
+
options: localOptions,
|
|
109
|
+
isLoading: false,
|
|
110
|
+
hasError: false,
|
|
111
|
+
needsMoreInput: false
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { options: results, isLoading, hasError, needsMoreInput: false }
|
|
116
|
+
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
export { SelectField } from "./SelectField"
|
|
2
|
-
export type { SelectFieldProps
|
|
3
|
-
export {
|
|
4
|
-
export type {
|
|
5
|
-
export {
|
|
6
|
-
export type {
|
|
7
|
-
export {
|
|
8
|
-
export {
|
|
9
|
-
|
|
10
|
-
|
|
2
|
+
export type { SelectFieldProps } from "./SelectField"
|
|
3
|
+
export { SelectDrawerField } from "./SelectDrawerField"
|
|
4
|
+
export type { SelectDrawerFieldProps } from "./SelectDrawerField"
|
|
5
|
+
export { SelectFieldOption } from "./SelectFieldOption"
|
|
6
|
+
export type { SelectFieldOptionProps } from "./SelectFieldOption"
|
|
7
|
+
export { SelectFieldDropdown } from "./SelectFieldDropdown"
|
|
8
|
+
export type {
|
|
9
|
+
SelectFieldDropdownProps,
|
|
10
|
+
SelectFieldPlacement
|
|
11
|
+
} from "./SelectFieldDropdown"
|
|
12
|
+
export { matchOptions, labelFor } from "./core"
|
|
13
|
+
export type { SelectFieldOptionData } from "./core"
|
package/src/index.ts
CHANGED
package/src/test-utils.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from "react"
|
|
2
2
|
import { render, type RenderOptions } from "@testing-library/react"
|
|
3
3
|
import { ThemeProvider } from "@emotion/react"
|
|
4
|
+
import { PortalHost } from "@rn-primitives/portal"
|
|
4
5
|
import { createPawprintTheme } from "./theme/utils"
|
|
5
6
|
|
|
6
7
|
const theme = createPawprintTheme()
|
|
@@ -11,7 +12,10 @@ export function renderWithTheme(
|
|
|
11
12
|
) {
|
|
12
13
|
return render(ui, {
|
|
13
14
|
wrapper: ({ children }) => (
|
|
14
|
-
<ThemeProvider theme={theme}>
|
|
15
|
+
<ThemeProvider theme={theme}>
|
|
16
|
+
{children}
|
|
17
|
+
<PortalHost />
|
|
18
|
+
</ThemeProvider>
|
|
15
19
|
),
|
|
16
20
|
...options
|
|
17
21
|
})
|
package/vitest.config.ts
CHANGED
|
@@ -66,6 +66,13 @@ export default defineConfig({
|
|
|
66
66
|
"src/__mocks__/rn-primitives/select.tsx"
|
|
67
67
|
)
|
|
68
68
|
},
|
|
69
|
+
{
|
|
70
|
+
find: /^@rn-primitives\/portal$/,
|
|
71
|
+
replacement: resolve(
|
|
72
|
+
__dirname,
|
|
73
|
+
"src/__mocks__/rn-primitives/portal.tsx"
|
|
74
|
+
)
|
|
75
|
+
},
|
|
69
76
|
{
|
|
70
77
|
find: /^@rn-primitives\/slot$/,
|
|
71
78
|
replacement: resolve(__dirname, "src/__mocks__/rn-primitives/slot.tsx")
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import React from "react"
|
|
2
|
-
import { View, ViewProps, ScrollView, StyleSheet } from "react-native"
|
|
3
|
-
import styled from "@emotion/native"
|
|
4
|
-
import { useTheme } from "@emotion/react"
|
|
5
|
-
import * as SelectPrimitive from "@rn-primitives/select"
|
|
6
|
-
import { Slot } from "@rn-primitives/slot"
|
|
7
|
-
|
|
8
|
-
type SelectFieldContentProps = {
|
|
9
|
-
children?: React.ReactNode
|
|
10
|
-
} & Omit<ViewProps, "children">
|
|
11
|
-
|
|
12
|
-
const parseTokenValue = (value: string): number => parseFloat(value)
|
|
13
|
-
|
|
14
|
-
const StyledContentShadow = styled(View)(({ theme }) => {
|
|
15
|
-
const { dropdown } = theme.tokens.components.dropdownList
|
|
16
|
-
const shadow = dropdown.list.dropshadow.default
|
|
17
|
-
|
|
18
|
-
return {
|
|
19
|
-
borderRadius: parseTokenValue(dropdown.list.borderRadius.default),
|
|
20
|
-
shadowColor: shadow.color,
|
|
21
|
-
shadowOffset: {
|
|
22
|
-
width: parseTokenValue(shadow.offsetX),
|
|
23
|
-
height: parseTokenValue(shadow.offsetY)
|
|
24
|
-
},
|
|
25
|
-
shadowOpacity: 1,
|
|
26
|
-
shadowRadius: parseTokenValue(shadow.blur),
|
|
27
|
-
elevation: 8
|
|
28
|
-
}
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
const StyledContentInner = styled(View)(({ theme }) => {
|
|
32
|
-
const { dropdown } = theme.tokens.components.dropdownList
|
|
33
|
-
const { colour } = theme.tokens.components.inputs
|
|
34
|
-
|
|
35
|
-
return {
|
|
36
|
-
backgroundColor: colour.field.background.default,
|
|
37
|
-
borderRadius: parseTokenValue(dropdown.list.borderRadius.default),
|
|
38
|
-
minWidth: parseTokenValue(dropdown.list.size.minWidth),
|
|
39
|
-
maxHeight: parseTokenValue(dropdown.list.size.maxHeight),
|
|
40
|
-
overflow: "hidden"
|
|
41
|
-
}
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
export const SelectFieldContent = React.forwardRef<
|
|
45
|
-
View,
|
|
46
|
-
SelectFieldContentProps
|
|
47
|
-
>(({ children, ...rest }, ref) => {
|
|
48
|
-
const theme = useTheme()
|
|
49
|
-
const { spacing } = theme.tokens.components.inputs
|
|
50
|
-
|
|
51
|
-
return (
|
|
52
|
-
<SelectPrimitive.Portal>
|
|
53
|
-
<SelectPrimitive.Overlay style={StyleSheet.absoluteFill} />
|
|
54
|
-
<SelectPrimitive.Content
|
|
55
|
-
side="bottom"
|
|
56
|
-
align="start"
|
|
57
|
-
sideOffset={parseTokenValue(spacing.gap)}
|
|
58
|
-
alignOffset={0}
|
|
59
|
-
asChild
|
|
60
|
-
>
|
|
61
|
-
<Slot ref={ref}>
|
|
62
|
-
<StyledContentShadow {...rest}>
|
|
63
|
-
<StyledContentInner>
|
|
64
|
-
<SelectPrimitive.Viewport>
|
|
65
|
-
<ScrollView
|
|
66
|
-
showsVerticalScrollIndicator
|
|
67
|
-
nestedScrollEnabled
|
|
68
|
-
keyboardShouldPersistTaps="handled"
|
|
69
|
-
>
|
|
70
|
-
{children}
|
|
71
|
-
</ScrollView>
|
|
72
|
-
</SelectPrimitive.Viewport>
|
|
73
|
-
</StyledContentInner>
|
|
74
|
-
</StyledContentShadow>
|
|
75
|
-
</Slot>
|
|
76
|
-
</SelectPrimitive.Content>
|
|
77
|
-
</SelectPrimitive.Portal>
|
|
78
|
-
)
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
SelectFieldContent.displayName = "SelectField.Content"
|