@butternutbox/pawprint-native 1.6.1 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@butternutbox/pawprint-native",
3
- "version": "1.6.1",
3
+ "version": "1.6.2",
4
4
  "type": "module",
5
5
  "description": "ButternutBox Pawprint Design System - React Native Components",
6
6
  "main": "./dist/index.cjs",
@@ -1,8 +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"
5
- import { Keyboard, MEASURED_TRIGGER } from "../../../__mocks__/react-native"
4
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
5
+ import { MEASURED_TRIGGER } from "../../../__mocks__/react-native"
6
6
  import { renderWithTheme } from "../../../test-utils"
7
7
  import { SelectField } from "./SelectField"
8
8
 
@@ -698,7 +698,18 @@ describe("SelectField", () => {
698
698
  expect(item).toBeInTheDocument()
699
699
  })
700
700
  })
701
- describe("when the keyboard moves the trigger", () => {
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
+
702
713
  const renderSearchable = () =>
703
714
  renderWithTheme(
704
715
  <SelectField label="Breed" placeholder="Select a breed" searchable>
@@ -712,34 +723,53 @@ describe("SelectField", () => {
712
723
  .querySelector("[data-trigger-position]")
713
724
  ?.getAttribute("data-trigger-position")
714
725
 
715
- it("re-anchors the dropdown to the trigger when the keyboard shows", () => {
726
+ const advanceFrames = (count: number) =>
727
+ act(() => vi.advanceTimersByTime(count * 16))
728
+
729
+ it("anchors the dropdown to the field once it has opened", () => {
716
730
  const { container } = renderSearchable()
717
731
 
718
732
  fireEvent.click(screen.getAllByRole("button")[0])
719
- expect(triggerPositionOf(container)).toBeUndefined()
720
-
721
- act(() => Keyboard.emit("keyboardDidShow"))
733
+ advanceFrames(3)
722
734
 
723
735
  expect(triggerPositionOf(container)).toBe(
724
736
  JSON.stringify(MEASURED_TRIGGER)
725
737
  )
726
738
  })
727
739
 
728
- it("re-anchors the dropdown when the keyboard hides", () => {
740
+ it("follows the field to where it comes to rest", () => {
729
741
  const { container } = renderSearchable()
730
742
 
731
743
  fireEvent.click(screen.getAllByRole("button")[0])
732
- act(() => Keyboard.emit("keyboardDidHide"))
744
+ advanceFrames(3)
745
+ MEASURED_TRIGGER.pageY = 380
746
+ advanceFrames(3)
733
747
 
734
748
  expect(triggerPositionOf(container)).toBe(
735
- JSON.stringify(MEASURED_TRIGGER)
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 })
736
766
  )
737
767
  })
738
768
 
739
- it("does not re-anchor while the dropdown is closed", () => {
769
+ it("does not track the field while the dropdown is closed", () => {
740
770
  const { container } = renderSearchable()
741
771
 
742
- act(() => Keyboard.emit("keyboardDidShow"))
772
+ advanceFrames(3)
743
773
 
744
774
  expect(triggerPositionOf(container)).toBeUndefined()
745
775
  })
@@ -80,25 +80,49 @@ type SelectFieldAnchorProps = {
80
80
  enabled: boolean
81
81
  }
82
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
+
83
95
  function SelectFieldAnchor({ triggerRef, enabled }: SelectFieldAnchorProps) {
84
- const { setTriggerPosition } = SelectPrimitive.useRootContext()
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])
85
103
 
86
104
  React.useEffect(() => {
87
105
  if (!enabled) return
88
106
 
89
- const reanchor = () => {
107
+ let frame: number
108
+ let previous: TriggerPosition | null = null
109
+
110
+ const track = () => {
90
111
  triggerRef.current?.measure((_x, _y, width, height, pageX, pageY) => {
91
- setTriggerPosition({ 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
92
119
  })
120
+ frame = requestAnimationFrame(track)
93
121
  }
94
122
 
95
- const shown = Keyboard.addListener("keyboardDidShow", reanchor)
96
- const hidden = Keyboard.addListener("keyboardDidHide", reanchor)
123
+ frame = requestAnimationFrame(track)
97
124
 
98
- return () => {
99
- shown.remove()
100
- hidden.remove()
101
- }
125
+ return () => cancelAnimationFrame(frame)
102
126
  }, [enabled, setTriggerPosition, triggerRef])
103
127
 
104
128
  return null