@butternutbox/pawprint-native 0.21.0 → 0.23.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 +18 -0
- package/dist/index.cjs +90 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.js +90 -21
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/atoms/Button/Button.stories.tsx +22 -2
- package/src/components/atoms/Button/Button.tsx +6 -5
- package/src/components/atoms/IconButton/IconButton.tsx +1 -1
- package/src/components/atoms/Tag/Tag.stories.tsx +11 -1
- package/src/components/atoms/Tag/Tag.tsx +12 -2
- package/src/components/molecules/Checkbox/Checkbox.stories.tsx +4 -0
- package/src/components/molecules/Checkbox/Checkbox.tsx +5 -2
- package/src/components/molecules/Drawer/DrawerBody.tsx +1 -1
- package/src/components/molecules/SelectField/SelectField.stories.tsx +53 -0
- package/src/components/molecules/SelectField/SelectField.test.tsx +257 -1
- package/src/components/molecules/SelectField/SelectField.tsx +193 -46
- package/src/components/molecules/SelectField/index.ts +1 -1
- package/src/components/molecules/Slider/Slider.tsx +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@butternutbox/pawprint-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "ButternutBox Pawprint Design System - React Native Components",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"test": "vitest run"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@butternutbox/pawprint-tokens": "^0.
|
|
25
|
+
"@butternutbox/pawprint-tokens": "^0.5.0",
|
|
26
26
|
"@emotion/native": "^11.11.0",
|
|
27
27
|
"@emotion/react": "^11.14.0",
|
|
28
28
|
"@rn-primitives/avatar": "^1.4.0",
|
|
@@ -11,6 +11,22 @@ import {
|
|
|
11
11
|
KeyboardDoubleArrowDown
|
|
12
12
|
} from "@butternutbox/pawprint-icons/core"
|
|
13
13
|
|
|
14
|
+
const iconElements: Record<string, React.ReactNode> = {
|
|
15
|
+
None: undefined,
|
|
16
|
+
KeyboardDoubleArrowDown: (
|
|
17
|
+
<Icon icon={KeyboardDoubleArrowDown} colour="primary" />
|
|
18
|
+
),
|
|
19
|
+
IosShare: <Icon icon={IosShare} colour="primary" />,
|
|
20
|
+
Whatsapp: <Icon icon={Whatsapp} colour="primary" />,
|
|
21
|
+
Check: <Icon icon={Check} colour="primary" />
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const iconArgType = {
|
|
25
|
+
control: "select" as const,
|
|
26
|
+
options: Object.keys(iconElements),
|
|
27
|
+
mapping: iconElements
|
|
28
|
+
}
|
|
29
|
+
|
|
14
30
|
export default {
|
|
15
31
|
title: "Atoms/Button",
|
|
16
32
|
component: Button,
|
|
@@ -45,7 +61,9 @@ export default {
|
|
|
45
61
|
children: {
|
|
46
62
|
control: { type: "text" },
|
|
47
63
|
description: "Button label text"
|
|
48
|
-
}
|
|
64
|
+
},
|
|
65
|
+
startIcon: iconArgType,
|
|
66
|
+
endIcon: iconArgType
|
|
49
67
|
}
|
|
50
68
|
}
|
|
51
69
|
|
|
@@ -57,7 +75,9 @@ Playground.args = {
|
|
|
57
75
|
colour: "primary",
|
|
58
76
|
loading: false,
|
|
59
77
|
fullWidth: false,
|
|
60
|
-
disabled: false
|
|
78
|
+
disabled: false,
|
|
79
|
+
startIcon: undefined,
|
|
80
|
+
endIcon: undefined
|
|
61
81
|
}
|
|
62
82
|
|
|
63
83
|
export const AllVariants = () => (
|
|
@@ -39,7 +39,7 @@ const IconWrapper = styled(View)({
|
|
|
39
39
|
})
|
|
40
40
|
|
|
41
41
|
const StyledButton = styled(Pressable)<{
|
|
42
|
-
|
|
42
|
+
buttonMinHeight: number
|
|
43
43
|
buttonMinWidth: number
|
|
44
44
|
buttonPaddingHorizontal: number
|
|
45
45
|
buttonPaddingVertical: number
|
|
@@ -52,7 +52,7 @@ const StyledButton = styled(Pressable)<{
|
|
|
52
52
|
buttonFullWidth: boolean
|
|
53
53
|
}>(
|
|
54
54
|
({
|
|
55
|
-
|
|
55
|
+
buttonMinHeight,
|
|
56
56
|
buttonMinWidth,
|
|
57
57
|
buttonPaddingHorizontal,
|
|
58
58
|
buttonPaddingVertical,
|
|
@@ -68,7 +68,7 @@ const StyledButton = styled(Pressable)<{
|
|
|
68
68
|
alignItems: "center",
|
|
69
69
|
justifyContent: "center",
|
|
70
70
|
position: "relative",
|
|
71
|
-
|
|
71
|
+
minHeight: buttonMinHeight,
|
|
72
72
|
minWidth: buttonMinWidth,
|
|
73
73
|
paddingHorizontal: buttonPaddingHorizontal,
|
|
74
74
|
paddingVertical: buttonPaddingVertical,
|
|
@@ -87,7 +87,7 @@ const StyledTextWrapper = styled(View)<{
|
|
|
87
87
|
textOpacity: number
|
|
88
88
|
}>(({ textOpacity }) => ({
|
|
89
89
|
opacity: textOpacity,
|
|
90
|
-
|
|
90
|
+
flex: 1
|
|
91
91
|
}))
|
|
92
92
|
|
|
93
93
|
const StyledSpinnerWrapper = styled(View)({
|
|
@@ -207,7 +207,7 @@ const Button = React.forwardRef<View, ButtonProps>(
|
|
|
207
207
|
accessibilityState={{ disabled: isDisabled, busy: loading }}
|
|
208
208
|
onPressIn={() => setPressed(true)}
|
|
209
209
|
onPressOut={() => setPressed(false)}
|
|
210
|
-
|
|
210
|
+
buttonMinHeight={parseTokenValue(sizeTokens.minHeight)}
|
|
211
211
|
buttonMinWidth={parseTokenValue(sizeTokens.minWidth)}
|
|
212
212
|
buttonPaddingHorizontal={parseTokenValue(
|
|
213
213
|
spacingTokens.horizontalPadding
|
|
@@ -238,6 +238,7 @@ const Button = React.forwardRef<View, ButtonProps>(
|
|
|
238
238
|
letterSpacing: typography.letterSpacing
|
|
239
239
|
}}
|
|
240
240
|
color={variantStyles.textColor as never}
|
|
241
|
+
align="center"
|
|
241
242
|
>
|
|
242
243
|
{children}
|
|
243
244
|
</Typography>
|
|
@@ -112,7 +112,7 @@ const IconButton = React.forwardRef<View, IconButtonProps>(
|
|
|
112
112
|
const isDisabled = disabled || loading
|
|
113
113
|
const buttons = theme.tokens.components.buttons
|
|
114
114
|
const sizeTokens = buttons.size[size]
|
|
115
|
-
const dimension = parseTokenValue(sizeTokens.
|
|
115
|
+
const dimension = parseTokenValue(sizeTokens.minHeight)
|
|
116
116
|
const iconSizing = theme.tokens.components.icons.sizing.icons.core
|
|
117
117
|
const iconDimension = parseTokenValue(iconSizing[sizeToIconSizeToken[size]])
|
|
118
118
|
|
|
@@ -2,6 +2,7 @@ import React from "react"
|
|
|
2
2
|
import { View, StyleSheet, Text } from "react-native"
|
|
3
3
|
import { Tag } from "./Tag"
|
|
4
4
|
import type { TagProps } from "./Tag"
|
|
5
|
+
import { CheckCircle } from "@butternutbox/pawprint-icons/core"
|
|
5
6
|
|
|
6
7
|
export default {
|
|
7
8
|
title: "Atoms/Tag",
|
|
@@ -28,6 +29,14 @@ export default {
|
|
|
28
29
|
children: {
|
|
29
30
|
control: { type: "text" },
|
|
30
31
|
description: "Tag label text"
|
|
32
|
+
},
|
|
33
|
+
icon: {
|
|
34
|
+
control: "boolean",
|
|
35
|
+
mapping: {
|
|
36
|
+
true: CheckCircle,
|
|
37
|
+
false: undefined
|
|
38
|
+
},
|
|
39
|
+
description: "Show/hide leading icon"
|
|
31
40
|
}
|
|
32
41
|
}
|
|
33
42
|
}
|
|
@@ -36,7 +45,8 @@ export const Playground = (args: TagProps) => <Tag {...args} />
|
|
|
36
45
|
Playground.args = {
|
|
37
46
|
children: "Tag",
|
|
38
47
|
variant: "primary",
|
|
39
|
-
size: "medium"
|
|
48
|
+
size: "medium",
|
|
49
|
+
icon: undefined
|
|
40
50
|
}
|
|
41
51
|
|
|
42
52
|
const tagVariants = [
|
|
@@ -32,6 +32,7 @@ const StyledTag = styled(View)<{
|
|
|
32
32
|
tagSize: TagSize
|
|
33
33
|
}>(({ theme, tagVariant, tagSize }) => {
|
|
34
34
|
const { sizing, spacing, colour, badge } = theme.tokens.components.tags
|
|
35
|
+
const { spacing: semanticSpacing } = theme.tokens.semantics.dimensions
|
|
35
36
|
|
|
36
37
|
const backgroundColorMap = {
|
|
37
38
|
primary: colour.primary.background,
|
|
@@ -43,13 +44,22 @@ const StyledTag = styled(View)<{
|
|
|
43
44
|
error: colour.primary.error
|
|
44
45
|
} as const
|
|
45
46
|
|
|
47
|
+
const verticalPaddingMap = {
|
|
48
|
+
small: semanticSpacing["2xs"],
|
|
49
|
+
medium: semanticSpacing["3xs"],
|
|
50
|
+
large: semanticSpacing["2xs"]
|
|
51
|
+
} as const
|
|
52
|
+
|
|
46
53
|
return {
|
|
47
54
|
flexDirection: "row",
|
|
48
55
|
alignItems: "center",
|
|
49
56
|
justifyContent: "center",
|
|
50
|
-
|
|
57
|
+
minHeight: parseTokenValue(sizing[tagSize].minHeight),
|
|
51
58
|
minWidth: parseTokenValue(sizing[tagSize].minWidth),
|
|
52
|
-
|
|
59
|
+
paddingLeft: parseTokenValue(spacing.horizontalPadding) * 2,
|
|
60
|
+
paddingRight: parseTokenValue(spacing.horizontalPadding) * 2,
|
|
61
|
+
paddingTop: parseTokenValue(verticalPaddingMap[tagSize]),
|
|
62
|
+
paddingBottom: parseTokenValue(verticalPaddingMap[tagSize]),
|
|
53
63
|
gap: parseTokenValue(spacing[tagSize].gap),
|
|
54
64
|
borderRadius: parseTokenValue(badge.borderRadius.default),
|
|
55
65
|
backgroundColor: backgroundColorMap[tagVariant]
|
|
@@ -99,6 +99,10 @@ export const Tile = () => (
|
|
|
99
99
|
</Typography>
|
|
100
100
|
<CheckboxGroup orientation="horizontal">
|
|
101
101
|
<Checkbox variant="tile" label="Chicken" />
|
|
102
|
+
<Checkbox
|
|
103
|
+
variant="tile"
|
|
104
|
+
label="A really long piece of text that needs multiple lines"
|
|
105
|
+
/>
|
|
102
106
|
<Checkbox variant="tile" label="Beef" />
|
|
103
107
|
<Checkbox variant="tile" label="Lamb" />
|
|
104
108
|
</CheckboxGroup>
|
|
@@ -114,11 +114,13 @@ const StyledControl = styled(View)<{
|
|
|
114
114
|
const StyledContent = styled(View)<{
|
|
115
115
|
contentGap: number
|
|
116
116
|
contentFlex: number
|
|
117
|
-
|
|
117
|
+
contentFitContent?: boolean
|
|
118
|
+
}>(({ contentGap, contentFlex, contentFitContent }) => ({
|
|
118
119
|
flexDirection: "column",
|
|
119
120
|
gap: contentGap,
|
|
120
121
|
flex: contentFlex,
|
|
121
|
-
minWidth: 0
|
|
122
|
+
minWidth: 0,
|
|
123
|
+
...(contentFitContent ? { maxWidth: "85%" } : {})
|
|
122
124
|
}))
|
|
123
125
|
|
|
124
126
|
const StyledIllustration = styled(View)<{
|
|
@@ -262,6 +264,7 @@ export const Checkbox = React.forwardRef<View, CheckboxProps>(
|
|
|
262
264
|
<StyledContent
|
|
263
265
|
contentGap={parseTokenValue(checkbox.spacing.content.gap)}
|
|
264
266
|
contentFlex={isTile && fitContent ? 0 : 1}
|
|
267
|
+
contentFitContent={isTile && fitContent}
|
|
265
268
|
>
|
|
266
269
|
{label && (
|
|
267
270
|
<Typography
|
|
@@ -79,7 +79,7 @@ export const DrawerBody = React.forwardRef<ScrollView, DrawerBodyProps>(
|
|
|
79
79
|
const paddingRight =
|
|
80
80
|
headerContext === null
|
|
81
81
|
? parseTokenValue(spacing.close.right.md) +
|
|
82
|
-
parseTokenValue(buttons.size.sm.
|
|
82
|
+
parseTokenValue(buttons.size.sm.minHeight)
|
|
83
83
|
: horizontalPadding
|
|
84
84
|
|
|
85
85
|
// With no footer, the body is the last element in the panel, so it must
|
|
@@ -409,6 +409,59 @@ export const SearchableDefaultPlaceholder = () => (
|
|
|
409
409
|
</StoryWrapper>
|
|
410
410
|
)
|
|
411
411
|
|
|
412
|
+
const ALL_BREEDS = [
|
|
413
|
+
{ value: "labrador", label: "Labrador Retriever" },
|
|
414
|
+
{ value: "german-shepherd", label: "German Shepherd" },
|
|
415
|
+
{ value: "golden-retriever", label: "Golden Retriever" },
|
|
416
|
+
{ value: "bulldog", label: "Bulldog" },
|
|
417
|
+
{ value: "poodle", label: "Poodle" },
|
|
418
|
+
{ value: "beagle", label: "Beagle" },
|
|
419
|
+
{ value: "rottweiler", label: "Rottweiler" },
|
|
420
|
+
{ value: "dachshund", label: "Dachshund" },
|
|
421
|
+
{ value: "yorkshire-terrier", label: "Yorkshire Terrier" },
|
|
422
|
+
{ value: "boxer", label: "Boxer" }
|
|
423
|
+
]
|
|
424
|
+
|
|
425
|
+
export const SearchableAsyncQuery = () => {
|
|
426
|
+
const searchBreeds = (query: string) =>
|
|
427
|
+
new Promise<typeof ALL_BREEDS>((resolve, reject) => {
|
|
428
|
+
setTimeout(() => {
|
|
429
|
+
// Type "fail" to see the error state.
|
|
430
|
+
if (query.toLowerCase().includes("fail")) {
|
|
431
|
+
reject(new Error("Simulated request failure"))
|
|
432
|
+
return
|
|
433
|
+
}
|
|
434
|
+
resolve(
|
|
435
|
+
ALL_BREEDS.filter((breed) =>
|
|
436
|
+
breed.label.toLowerCase().includes(query.toLowerCase())
|
|
437
|
+
)
|
|
438
|
+
)
|
|
439
|
+
}, 600)
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
return (
|
|
443
|
+
<StoryWrapper>
|
|
444
|
+
<View style={styles.column}>
|
|
445
|
+
<View style={styles.section}>
|
|
446
|
+
<Typography size="sm" weight="semiBold" color="tertiary">
|
|
447
|
+
Async Query (spinner, no-results, error)
|
|
448
|
+
</Typography>
|
|
449
|
+
<SelectField
|
|
450
|
+
label="Breed"
|
|
451
|
+
placeholder="Select a breed"
|
|
452
|
+
description="Type to search — 'fail' triggers the error state"
|
|
453
|
+
searchable
|
|
454
|
+
searchPlaceholder="Search breeds..."
|
|
455
|
+
noResultsText="No breeds found"
|
|
456
|
+
errorText="Couldn't load breeds"
|
|
457
|
+
onSearchQuery={searchBreeds}
|
|
458
|
+
/>
|
|
459
|
+
</View>
|
|
460
|
+
</View>
|
|
461
|
+
</StoryWrapper>
|
|
462
|
+
)
|
|
463
|
+
}
|
|
464
|
+
|
|
412
465
|
const styles = StyleSheet.create({
|
|
413
466
|
container: {
|
|
414
467
|
width: 320
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from "react"
|
|
2
2
|
import { View } from "react-native"
|
|
3
|
-
import { screen, fireEvent, act } from "@testing-library/react"
|
|
3
|
+
import { screen, fireEvent, act, waitFor } from "@testing-library/react"
|
|
4
4
|
import { describe, it, expect, vi } from "vitest"
|
|
5
5
|
import { renderWithTheme } from "../../../test-utils"
|
|
6
6
|
import { SelectField } from "./SelectField"
|
|
@@ -377,6 +377,262 @@ describe("SelectField", () => {
|
|
|
377
377
|
})
|
|
378
378
|
})
|
|
379
379
|
|
|
380
|
+
describe("when searchable with an async query", () => {
|
|
381
|
+
type Result = { value: string; label: string }
|
|
382
|
+
|
|
383
|
+
const openAndType = (query: string) => {
|
|
384
|
+
fireEvent.click(screen.getAllByRole("button")[0])
|
|
385
|
+
const input = screen.getByPlaceholderText("Search...")
|
|
386
|
+
fireEvent.change(input, { target: { value: query } })
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
it("shows children as the default list before the user types", async () => {
|
|
390
|
+
const onSearchQuery = vi.fn(async (): Promise<Result[]> => [])
|
|
391
|
+
|
|
392
|
+
renderWithTheme(
|
|
393
|
+
<SelectField
|
|
394
|
+
label="Breed"
|
|
395
|
+
placeholder="Select a breed"
|
|
396
|
+
searchable
|
|
397
|
+
debounceMs={0}
|
|
398
|
+
onSearchQuery={onSearchQuery}
|
|
399
|
+
>
|
|
400
|
+
<SelectField.Item value="popular">Most popular</SelectField.Item>
|
|
401
|
+
</SelectField>
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
fireEvent.click(screen.getAllByRole("button")[0])
|
|
405
|
+
|
|
406
|
+
expect(await screen.findByText("Most popular")).toBeInTheDocument()
|
|
407
|
+
expect(onSearchQuery).not.toHaveBeenCalled()
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
it("does not open an empty dropdown in query mode before the user types", () => {
|
|
411
|
+
const onSearchQuery = vi.fn(async (): Promise<Result[]> => [])
|
|
412
|
+
|
|
413
|
+
renderWithTheme(
|
|
414
|
+
<SelectField
|
|
415
|
+
label="Breed"
|
|
416
|
+
placeholder="Select a breed"
|
|
417
|
+
searchable
|
|
418
|
+
debounceMs={0}
|
|
419
|
+
onSearchQuery={onSearchQuery}
|
|
420
|
+
/>
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
vi.useFakeTimers()
|
|
424
|
+
fireEvent.click(screen.getAllByRole("button")[0])
|
|
425
|
+
act(() => vi.advanceTimersByTime(500))
|
|
426
|
+
vi.useRealTimers()
|
|
427
|
+
|
|
428
|
+
// Trigger is open (search input shown) but no content container is
|
|
429
|
+
// rendered: an empty dropdown crashes the real Select.Content on web.
|
|
430
|
+
expect(screen.getByPlaceholderText("Search...")).toBeInTheDocument()
|
|
431
|
+
expect(screen.queryByTestId("select-overlay")).not.toBeInTheDocument()
|
|
432
|
+
expect(onSearchQuery).not.toHaveBeenCalled()
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
it("calls onSearchQuery with the typed query and renders the results", async () => {
|
|
436
|
+
const onSearchQuery = vi.fn(
|
|
437
|
+
async (): Promise<Result[]> => [
|
|
438
|
+
{ value: "labrador", label: "Labrador Retriever" }
|
|
439
|
+
]
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
renderWithTheme(
|
|
443
|
+
<SelectField
|
|
444
|
+
label="Breed"
|
|
445
|
+
placeholder="Select a breed"
|
|
446
|
+
searchable
|
|
447
|
+
debounceMs={0}
|
|
448
|
+
onSearchQuery={onSearchQuery}
|
|
449
|
+
/>
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
openAndType("lab")
|
|
453
|
+
|
|
454
|
+
expect(await screen.findByText("Labrador Retriever")).toBeInTheDocument()
|
|
455
|
+
expect(onSearchQuery).toHaveBeenCalledWith("lab")
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
it("shows a loading spinner while the query is in flight", async () => {
|
|
459
|
+
let resolveQuery: (results: Result[]) => void = () => {}
|
|
460
|
+
const onSearchQuery = vi.fn(
|
|
461
|
+
() =>
|
|
462
|
+
new Promise<Result[]>((resolve) => {
|
|
463
|
+
resolveQuery = resolve
|
|
464
|
+
})
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
renderWithTheme(
|
|
468
|
+
<SelectField
|
|
469
|
+
label="Breed"
|
|
470
|
+
placeholder="Select a breed"
|
|
471
|
+
searchable
|
|
472
|
+
debounceMs={0}
|
|
473
|
+
onSearchQuery={onSearchQuery}
|
|
474
|
+
/>
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
openAndType("lab")
|
|
478
|
+
|
|
479
|
+
expect(await screen.findByRole("progressbar")).toBeInTheDocument()
|
|
480
|
+
|
|
481
|
+
await waitFor(() => expect(onSearchQuery).toHaveBeenCalled())
|
|
482
|
+
act(() => resolveQuery([{ value: "labrador", label: "Labrador" }]))
|
|
483
|
+
|
|
484
|
+
expect(await screen.findByText("Labrador")).toBeInTheDocument()
|
|
485
|
+
expect(screen.queryByRole("progressbar")).not.toBeInTheDocument()
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
it("shows errorText when the query rejects", async () => {
|
|
489
|
+
const onSearchQuery = vi.fn(async (): Promise<Result[]> => {
|
|
490
|
+
throw new Error("boom")
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
renderWithTheme(
|
|
494
|
+
<SelectField
|
|
495
|
+
label="Breed"
|
|
496
|
+
placeholder="Select a breed"
|
|
497
|
+
searchable
|
|
498
|
+
debounceMs={0}
|
|
499
|
+
errorText="Couldn't load breeds"
|
|
500
|
+
onSearchQuery={onSearchQuery}
|
|
501
|
+
/>
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
openAndType("lab")
|
|
505
|
+
|
|
506
|
+
expect(
|
|
507
|
+
await screen.findByText("Couldn't load breeds")
|
|
508
|
+
).toBeInTheDocument()
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
it("shows noResultsText when the query resolves empty", async () => {
|
|
512
|
+
const onSearchQuery = vi.fn(async (): Promise<Result[]> => [])
|
|
513
|
+
|
|
514
|
+
renderWithTheme(
|
|
515
|
+
<SelectField
|
|
516
|
+
label="Breed"
|
|
517
|
+
placeholder="Select a breed"
|
|
518
|
+
searchable
|
|
519
|
+
debounceMs={0}
|
|
520
|
+
noResultsText="No breeds found"
|
|
521
|
+
onSearchQuery={onSearchQuery}
|
|
522
|
+
/>
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
openAndType("xyz")
|
|
526
|
+
|
|
527
|
+
expect(await screen.findByText("No breeds found")).toBeInTheDocument()
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
it("renders results instead of locally filtering children in query mode", async () => {
|
|
531
|
+
const onSearchQuery = vi.fn(
|
|
532
|
+
async (): Promise<Result[]> => [
|
|
533
|
+
{ value: "labrador", label: "Labrador Retriever" }
|
|
534
|
+
]
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
renderWithTheme(
|
|
538
|
+
<SelectField
|
|
539
|
+
label="Breed"
|
|
540
|
+
placeholder="Select a breed"
|
|
541
|
+
searchable
|
|
542
|
+
debounceMs={0}
|
|
543
|
+
onSearchQuery={onSearchQuery}
|
|
544
|
+
>
|
|
545
|
+
<SelectField.Item value="uk">United Kingdom</SelectField.Item>
|
|
546
|
+
</SelectField>
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
openAndType("lab")
|
|
550
|
+
|
|
551
|
+
expect(await screen.findByText("Labrador Retriever")).toBeInTheDocument()
|
|
552
|
+
expect(screen.queryByText("United Kingdom")).not.toBeInTheDocument()
|
|
553
|
+
})
|
|
554
|
+
|
|
555
|
+
it("debounces the query, firing once after the last keystroke", () => {
|
|
556
|
+
const onSearchQuery = vi.fn(
|
|
557
|
+
async (): Promise<Result[]> => [
|
|
558
|
+
{ value: "labrador", label: "Labrador Retriever" }
|
|
559
|
+
]
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
renderWithTheme(
|
|
563
|
+
<SelectField
|
|
564
|
+
label="Breed"
|
|
565
|
+
placeholder="Select a breed"
|
|
566
|
+
searchable
|
|
567
|
+
debounceMs={300}
|
|
568
|
+
onSearchQuery={onSearchQuery}
|
|
569
|
+
/>
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
vi.useFakeTimers()
|
|
573
|
+
fireEvent.click(screen.getAllByRole("button")[0])
|
|
574
|
+
const input = screen.getByPlaceholderText("Search...")
|
|
575
|
+
|
|
576
|
+
fireEvent.change(input, { target: { value: "l" } })
|
|
577
|
+
act(() => vi.advanceTimersByTime(200))
|
|
578
|
+
fireEvent.change(input, { target: { value: "la" } })
|
|
579
|
+
act(() => vi.advanceTimersByTime(200))
|
|
580
|
+
fireEvent.change(input, { target: { value: "lab" } })
|
|
581
|
+
|
|
582
|
+
// Each keystroke restarted the timer, so nothing has fired yet even
|
|
583
|
+
// though 400ms of typing has elapsed.
|
|
584
|
+
expect(onSearchQuery).not.toHaveBeenCalled()
|
|
585
|
+
|
|
586
|
+
act(() => vi.advanceTimersByTime(300))
|
|
587
|
+
vi.useRealTimers()
|
|
588
|
+
|
|
589
|
+
expect(onSearchQuery).toHaveBeenCalledTimes(1)
|
|
590
|
+
expect(onSearchQuery).toHaveBeenCalledWith("lab")
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
it("ignores a stale response superseded by a later keystroke", async () => {
|
|
594
|
+
const resolvers: Array<(results: Result[]) => void> = []
|
|
595
|
+
const onSearchQuery = vi.fn(
|
|
596
|
+
() =>
|
|
597
|
+
new Promise<Result[]>((resolve) => {
|
|
598
|
+
resolvers.push(resolve)
|
|
599
|
+
})
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
renderWithTheme(
|
|
603
|
+
<SelectField
|
|
604
|
+
label="Breed"
|
|
605
|
+
placeholder="Select a breed"
|
|
606
|
+
searchable
|
|
607
|
+
debounceMs={0}
|
|
608
|
+
onSearchQuery={onSearchQuery}
|
|
609
|
+
/>
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
fireEvent.click(screen.getAllByRole("button")[0])
|
|
613
|
+
const input = screen.getByPlaceholderText("Search...")
|
|
614
|
+
|
|
615
|
+
fireEvent.change(input, { target: { value: "lab" } })
|
|
616
|
+
await waitFor(() => expect(resolvers).toHaveLength(1))
|
|
617
|
+
|
|
618
|
+
fireEvent.change(input, { target: { value: "labr" } })
|
|
619
|
+
await waitFor(() => expect(resolvers).toHaveLength(2))
|
|
620
|
+
|
|
621
|
+
// The newer query lands first, then the superseded one resolves late.
|
|
622
|
+
await act(async () => {
|
|
623
|
+
resolvers[1]([{ value: "labrador", label: "Labrador Retriever" }])
|
|
624
|
+
})
|
|
625
|
+
expect(screen.getByText("Labrador Retriever")).toBeInTheDocument()
|
|
626
|
+
|
|
627
|
+
await act(async () => {
|
|
628
|
+
resolvers[0]([{ value: "stale", label: "Stale Result" }])
|
|
629
|
+
})
|
|
630
|
+
|
|
631
|
+
expect(screen.queryByText("Stale Result")).not.toBeInTheDocument()
|
|
632
|
+
expect(screen.getByText("Labrador Retriever")).toBeInTheDocument()
|
|
633
|
+
})
|
|
634
|
+
})
|
|
635
|
+
|
|
380
636
|
describe("with items", () => {
|
|
381
637
|
const openDropdown = () => fireEvent.click(screen.getAllByRole("button")[0])
|
|
382
638
|
|