@butternutbox/pawprint-native 1.6.0 → 1.6.2
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 +6 -6
- package/CHANGELOG.md +34 -0
- package/dist/index.cjs +38 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__mocks__/react-native.tsx +46 -5
- package/src/__mocks__/rn-primitives/select.tsx +22 -3
- package/src/components/molecules/SelectField/SelectField.test.tsx +78 -1
- package/src/components/molecules/SelectField/SelectField.tsx +59 -0
package/package.json
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
import React from "react"
|
|
2
2
|
|
|
3
|
+
export const MEASURED_TRIGGER = {
|
|
4
|
+
width: 320,
|
|
5
|
+
height: 52,
|
|
6
|
+
pageX: 40,
|
|
7
|
+
pageY: 700
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const attachMeasure =
|
|
11
|
+
(ref: React.Ref<HTMLDivElement>) => (node: HTMLDivElement | null) => {
|
|
12
|
+
if (node) {
|
|
13
|
+
;(node as unknown as { measure: unknown }).measure = (
|
|
14
|
+
callback: (
|
|
15
|
+
x: number,
|
|
16
|
+
y: number,
|
|
17
|
+
width: number,
|
|
18
|
+
height: number,
|
|
19
|
+
pageX: number,
|
|
20
|
+
pageY: number
|
|
21
|
+
) => void
|
|
22
|
+
) =>
|
|
23
|
+
callback(
|
|
24
|
+
0,
|
|
25
|
+
0,
|
|
26
|
+
MEASURED_TRIGGER.width,
|
|
27
|
+
MEASURED_TRIGGER.height,
|
|
28
|
+
MEASURED_TRIGGER.pageX,
|
|
29
|
+
MEASURED_TRIGGER.pageY
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
if (typeof ref === "function") ref(node)
|
|
33
|
+
else if (ref) (ref as React.RefObject<HTMLDivElement | null>).current = node
|
|
34
|
+
}
|
|
35
|
+
|
|
3
36
|
// ─── View ────────────────────────────────────────────────────────────────
|
|
4
37
|
export const View = React.forwardRef<HTMLDivElement, any>(
|
|
5
38
|
(
|
|
@@ -45,7 +78,7 @@ export const View = React.forwardRef<HTMLDivElement, any>(
|
|
|
45
78
|
domProps["aria-valuemax"] = String(accessibilityValue.max)
|
|
46
79
|
}
|
|
47
80
|
return (
|
|
48
|
-
<div ref={ref} style={style} {...domProps}>
|
|
81
|
+
<div ref={attachMeasure(ref)} style={style} {...domProps}>
|
|
49
82
|
{children}
|
|
50
83
|
</div>
|
|
51
84
|
)
|
|
@@ -335,11 +368,19 @@ export const Platform = {
|
|
|
335
368
|
select: (obj: any) => obj.ios ?? obj.default
|
|
336
369
|
}
|
|
337
370
|
|
|
371
|
+
const keyboardHandlers = new Map<string, Set<() => void>>()
|
|
372
|
+
|
|
338
373
|
export const Keyboard = {
|
|
339
|
-
addListener: (
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
374
|
+
addListener: (event: string, handler: () => void) => {
|
|
375
|
+
const handlers = keyboardHandlers.get(event) ?? new Set()
|
|
376
|
+
handlers.add(handler)
|
|
377
|
+
keyboardHandlers.set(event, handlers)
|
|
378
|
+
return { remove: () => handlers.delete(handler) }
|
|
379
|
+
},
|
|
380
|
+
dismiss: () => {},
|
|
381
|
+
emit: (event: string) => {
|
|
382
|
+
keyboardHandlers.get(event)?.forEach((handler) => handler())
|
|
383
|
+
}
|
|
343
384
|
}
|
|
344
385
|
|
|
345
386
|
// ─── Type exports (re-exported as empty for TS compat) ───────────────────
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import React from "react"
|
|
2
2
|
|
|
3
|
+
type TriggerPosition = {
|
|
4
|
+
width: number
|
|
5
|
+
height: number
|
|
6
|
+
pageX: number
|
|
7
|
+
pageY: number
|
|
8
|
+
}
|
|
9
|
+
|
|
3
10
|
const SelectCtx = React.createContext<{
|
|
4
11
|
open: boolean
|
|
5
12
|
setOpen: (open: boolean) => void
|
|
6
|
-
|
|
13
|
+
setTriggerPosition: (position: TriggerPosition | null) => void
|
|
14
|
+
}>({ open: false, setOpen: () => {}, setTriggerPosition: () => {} })
|
|
15
|
+
|
|
16
|
+
export const useRootContext = () => React.useContext(SelectCtx)
|
|
7
17
|
|
|
8
18
|
export const Root = React.forwardRef<any, any>(
|
|
9
19
|
(
|
|
@@ -19,6 +29,8 @@ export const Root = React.forwardRef<any, any>(
|
|
|
19
29
|
ref
|
|
20
30
|
) => {
|
|
21
31
|
const [open, setOpenState] = React.useState(false)
|
|
32
|
+
const [triggerPosition, setTriggerPosition] =
|
|
33
|
+
React.useState<TriggerPosition | null>(null)
|
|
22
34
|
const [internalValue, setInternalValue] = React.useState(
|
|
23
35
|
value ?? defaultValue ?? null
|
|
24
36
|
)
|
|
@@ -37,8 +49,15 @@ export const Root = React.forwardRef<any, any>(
|
|
|
37
49
|
}
|
|
38
50
|
|
|
39
51
|
return (
|
|
40
|
-
<SelectCtx.Provider value={{ open, setOpen }}>
|
|
41
|
-
<div
|
|
52
|
+
<SelectCtx.Provider value={{ open, setOpen, setTriggerPosition }}>
|
|
53
|
+
<div
|
|
54
|
+
ref={ref}
|
|
55
|
+
data-disabled={disabled}
|
|
56
|
+
data-trigger-position={
|
|
57
|
+
triggerPosition ? JSON.stringify(triggerPosition) : undefined
|
|
58
|
+
}
|
|
59
|
+
{...rest}
|
|
60
|
+
>
|
|
42
61
|
{typeof children === "function"
|
|
43
62
|
? children({ value: currentValue, onChange: handleChange })
|
|
44
63
|
: children}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import React from "react"
|
|
2
2
|
import { View } from "react-native"
|
|
3
3
|
import { screen, fireEvent, act, waitFor } from "@testing-library/react"
|
|
4
|
-
import { describe, it, expect, vi } from "vitest"
|
|
4
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
|
|
5
|
+
import { MEASURED_TRIGGER } from "../../../__mocks__/react-native"
|
|
5
6
|
import { renderWithTheme } from "../../../test-utils"
|
|
6
7
|
import { SelectField } from "./SelectField"
|
|
7
8
|
|
|
@@ -697,4 +698,80 @@ describe("SelectField", () => {
|
|
|
697
698
|
expect(item).toBeInTheDocument()
|
|
698
699
|
})
|
|
699
700
|
})
|
|
701
|
+
describe("when the field moves while the dropdown is open", () => {
|
|
702
|
+
const restingPageY = MEASURED_TRIGGER.pageY
|
|
703
|
+
|
|
704
|
+
beforeEach(() => {
|
|
705
|
+
vi.useFakeTimers()
|
|
706
|
+
})
|
|
707
|
+
|
|
708
|
+
afterEach(() => {
|
|
709
|
+
MEASURED_TRIGGER.pageY = restingPageY
|
|
710
|
+
vi.useRealTimers()
|
|
711
|
+
})
|
|
712
|
+
|
|
713
|
+
const renderSearchable = () =>
|
|
714
|
+
renderWithTheme(
|
|
715
|
+
<SelectField label="Breed" placeholder="Select a breed" searchable>
|
|
716
|
+
<SelectField.Item value="lab">Labrador</SelectField.Item>
|
|
717
|
+
<SelectField.Item value="pug">Pug</SelectField.Item>
|
|
718
|
+
</SelectField>
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
const triggerPositionOf = (container: HTMLElement) =>
|
|
722
|
+
container
|
|
723
|
+
.querySelector("[data-trigger-position]")
|
|
724
|
+
?.getAttribute("data-trigger-position")
|
|
725
|
+
|
|
726
|
+
const advanceFrames = (count: number) =>
|
|
727
|
+
act(() => vi.advanceTimersByTime(count * 16))
|
|
728
|
+
|
|
729
|
+
it("anchors the dropdown to the field once it has opened", () => {
|
|
730
|
+
const { container } = renderSearchable()
|
|
731
|
+
|
|
732
|
+
fireEvent.click(screen.getAllByRole("button")[0])
|
|
733
|
+
advanceFrames(3)
|
|
734
|
+
|
|
735
|
+
expect(triggerPositionOf(container)).toBe(
|
|
736
|
+
JSON.stringify(MEASURED_TRIGGER)
|
|
737
|
+
)
|
|
738
|
+
})
|
|
739
|
+
|
|
740
|
+
it("follows the field to where it comes to rest", () => {
|
|
741
|
+
const { container } = renderSearchable()
|
|
742
|
+
|
|
743
|
+
fireEvent.click(screen.getAllByRole("button")[0])
|
|
744
|
+
advanceFrames(3)
|
|
745
|
+
MEASURED_TRIGGER.pageY = 380
|
|
746
|
+
advanceFrames(3)
|
|
747
|
+
|
|
748
|
+
expect(triggerPositionOf(container)).toBe(
|
|
749
|
+
JSON.stringify({ ...MEASURED_TRIGGER, pageY: 380 })
|
|
750
|
+
)
|
|
751
|
+
})
|
|
752
|
+
|
|
753
|
+
it("holds the dropdown in place while the field is still moving", () => {
|
|
754
|
+
const { container } = renderSearchable()
|
|
755
|
+
|
|
756
|
+
fireEvent.click(screen.getAllByRole("button")[0])
|
|
757
|
+
advanceFrames(3)
|
|
758
|
+
|
|
759
|
+
for (const pageY of [650, 600, 550]) {
|
|
760
|
+
MEASURED_TRIGGER.pageY = pageY
|
|
761
|
+
advanceFrames(1)
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
expect(triggerPositionOf(container)).toBe(
|
|
765
|
+
JSON.stringify({ ...MEASURED_TRIGGER, pageY: restingPageY })
|
|
766
|
+
)
|
|
767
|
+
})
|
|
768
|
+
|
|
769
|
+
it("does not track the field while the dropdown is closed", () => {
|
|
770
|
+
const { container } = renderSearchable()
|
|
771
|
+
|
|
772
|
+
advanceFrames(3)
|
|
773
|
+
|
|
774
|
+
expect(triggerPositionOf(container)).toBeUndefined()
|
|
775
|
+
})
|
|
776
|
+
})
|
|
700
777
|
})
|
|
@@ -75,6 +75,59 @@ type SelectFieldOwnProps<Value = Option | string> = {
|
|
|
75
75
|
export type SelectFieldProps<Value = Option> = SelectFieldOwnProps<Value> &
|
|
76
76
|
Omit<ViewProps, keyof SelectFieldOwnProps<Value>>
|
|
77
77
|
|
|
78
|
+
type SelectFieldAnchorProps = {
|
|
79
|
+
triggerRef: React.RefObject<View | null>
|
|
80
|
+
enabled: boolean
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
type TriggerPosition = NonNullable<
|
|
84
|
+
ReturnType<typeof SelectPrimitive.useRootContext>["triggerPosition"]
|
|
85
|
+
>
|
|
86
|
+
|
|
87
|
+
const isSamePosition = (a: TriggerPosition | null, b: TriggerPosition | null) =>
|
|
88
|
+
a !== null &&
|
|
89
|
+
b !== null &&
|
|
90
|
+
a.width === b.width &&
|
|
91
|
+
a.height === b.height &&
|
|
92
|
+
a.pageX === b.pageX &&
|
|
93
|
+
a.pageY === b.pageY
|
|
94
|
+
|
|
95
|
+
function SelectFieldAnchor({ triggerRef, enabled }: SelectFieldAnchorProps) {
|
|
96
|
+
const { triggerPosition, setTriggerPosition } =
|
|
97
|
+
SelectPrimitive.useRootContext()
|
|
98
|
+
const anchoredRef = React.useRef<TriggerPosition | null>(null)
|
|
99
|
+
|
|
100
|
+
React.useEffect(() => {
|
|
101
|
+
anchoredRef.current = triggerPosition ?? null
|
|
102
|
+
}, [triggerPosition])
|
|
103
|
+
|
|
104
|
+
React.useEffect(() => {
|
|
105
|
+
if (!enabled) return
|
|
106
|
+
|
|
107
|
+
let frame: number
|
|
108
|
+
let previous: TriggerPosition | null = null
|
|
109
|
+
|
|
110
|
+
const track = () => {
|
|
111
|
+
triggerRef.current?.measure((_x, _y, width, height, pageX, pageY) => {
|
|
112
|
+
const measured = { width, height, pageX, pageY }
|
|
113
|
+
const settled = isSamePosition(measured, previous)
|
|
114
|
+
if (settled && !isSamePosition(measured, anchoredRef.current)) {
|
|
115
|
+
anchoredRef.current = measured
|
|
116
|
+
setTriggerPosition(measured)
|
|
117
|
+
}
|
|
118
|
+
previous = measured
|
|
119
|
+
})
|
|
120
|
+
frame = requestAnimationFrame(track)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
frame = requestAnimationFrame(track)
|
|
124
|
+
|
|
125
|
+
return () => cancelAnimationFrame(frame)
|
|
126
|
+
}, [enabled, setTriggerPosition, triggerRef])
|
|
127
|
+
|
|
128
|
+
return null
|
|
129
|
+
}
|
|
130
|
+
|
|
78
131
|
/**
|
|
79
132
|
* Select field component for choosing a value from a dropdown list.
|
|
80
133
|
* Built on @rn-primitives/select with design system styling.
|
|
@@ -216,6 +269,7 @@ const SelectFieldRoot = React.forwardRef<View, SelectFieldProps>(
|
|
|
216
269
|
const [searchQuery, setSearchQuery] = React.useState("")
|
|
217
270
|
const [clearKey, setClearKey] = React.useState(0)
|
|
218
271
|
const searchInputRef = React.useRef<TextInput>(null)
|
|
272
|
+
const triggerViewRef = React.useRef<View>(null)
|
|
219
273
|
|
|
220
274
|
const isQueryMode = searchable && typeof onSearchQuery === "function"
|
|
221
275
|
|
|
@@ -447,6 +501,7 @@ const SelectFieldRoot = React.forwardRef<View, SelectFieldProps>(
|
|
|
447
501
|
</InputLabel>
|
|
448
502
|
)}
|
|
449
503
|
<SelectFieldTrigger
|
|
504
|
+
ref={triggerViewRef}
|
|
450
505
|
state={state}
|
|
451
506
|
leadingIcon={leadingIcon}
|
|
452
507
|
isOpen={isOpen}
|
|
@@ -476,6 +531,10 @@ const SelectFieldRoot = React.forwardRef<View, SelectFieldProps>(
|
|
|
476
531
|
)}
|
|
477
532
|
{error && state === "error" && <InputError>{error}</InputError>}
|
|
478
533
|
</StyledRoot>
|
|
534
|
+
<SelectFieldAnchor
|
|
535
|
+
triggerRef={triggerViewRef}
|
|
536
|
+
enabled={searchable && isOpen}
|
|
537
|
+
/>
|
|
479
538
|
{showContent && hasContentToShow && (
|
|
480
539
|
<SelectFieldContent>
|
|
481
540
|
{isSearching ? (
|