Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/steady-keyboard-state.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@react-native-community/hooks": patch
---

Initialize `useKeyboard` from the current keyboard state and always clear its height when the keyboard hides.
2 changes: 1 addition & 1 deletion babel.config.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module.exports = {
presets: ['module:metro-react-native-babel-preset'],
presets: ["module:@react-native/babel-preset"],
}
289 changes: 266 additions & 23 deletions bun.lock

Large diffs are not rendered by default.

19 changes: 14 additions & 5 deletions jest.setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ jest.mock("react-native/Libraries/Utilities/Platform", () => ({
}))

jest.mock("react-native", () => {
const RN = jest.requireActual("react-native/Libraries/ReactNative/oss/ReactNativeRenderer-prod")
const keyboardListeners = new Map()

return {
Platform: {
Expand All @@ -28,10 +28,19 @@ jest.mock("react-native", () => {
})),
},
Keyboard: {
addListener: jest.fn(() => ({
remove: jest.fn(),
})),
emit: jest.fn(),
addListener: jest.fn((event, listener) => {
const listeners = keyboardListeners.get(event) ?? new Set()
listeners.add(listener)
keyboardListeners.set(event, listeners)
return {
remove: jest.fn(() => listeners.delete(listener)),
}
}),
emit: jest.fn((event, payload) => {
keyboardListeners.get(event)?.forEach((listener) => listener(payload))
}),
isVisible: jest.fn().mockReturnValue(false),
metrics: jest.fn().mockReturnValue(undefined),
},
AccessibilityInfo: {
addEventListener: jest.fn(() => ({
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,19 @@
"devDependencies": {
"@changesets/cli": "^2.28.1",
"@jest/globals": "^29.7.0",
"@react-native/babel-preset": "0.78.0",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/react-native": "^13.0.1",
"@types/jest": "^29.5.14",
"@types/react": "18.3.18",
"eslint": "9.20.1",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-prettier": "^5.2.3",
"jest": "^29.7.0",
"prettier": "3.5.1",
"react": "18.3.1",
"react-native": "0.78.0",
"react-test-renderer": "^19.0.0",
"react-test-renderer": "18.3.1",
"ts-jest": "^29.2.5",
"typescript": "5.7.3",
"typescript-eslint": "^8.24.1"
Expand Down
43 changes: 43 additions & 0 deletions src/useKeyboard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import { Keyboard } from "react-native"

describe("useKeyboard", () => {
const mockCoords = { screenX: 0, screenY: 0, width: 0, height: 0 }

beforeEach(() => {
jest.mocked(Keyboard.isVisible).mockReturnValue(false)
jest.mocked(Keyboard.metrics).mockReturnValue(undefined)
})

const emitKeyboardEvent = ({
show = true,
startCoordinates = mockCoords,
Expand All @@ -15,6 +21,17 @@ describe("useKeyboard", () => {
}

describe("setKeyboardHeight: number", () => {
it("uses the current keyboard height when mounted while open", () => {
const metrics = { ...mockCoords, height: 123 }
jest.mocked(Keyboard.isVisible).mockReturnValue(true)
jest.mocked(Keyboard.metrics).mockReturnValue(metrics)

const { result } = renderHook(() => useKeyboard())

expect(result.current.keyboardHeight).toBe(metrics.height)
expect(result.current.coordinates).toEqual({ start: undefined, end: metrics })
})

it("keyboard height is zero by default", () => {
const { result } = renderHook(() => useKeyboard())

Expand Down Expand Up @@ -48,9 +65,35 @@ describe("useKeyboard", () => {

expect(result.current.keyboardHeight).toBe(0)
})

it("resets keyboard height when the hide event includes coordinates", () => {
const height = 123
const { result } = renderHook(() => useKeyboard())

act(() => {
emitKeyboardEvent({ show: true, endCoordinates: { ...mockCoords, height } })
})

act(() => {
Keyboard.emit("keyboardDidHide", {
startCoordinates: mockCoords,
endCoordinates: mockCoords,
})
})

expect(result.current.keyboardHeight).toBe(0)
})
})

describe("keyboardShown: boolean", () => {
it("uses the current visibility when mounted while open", () => {
jest.mocked(Keyboard.isVisible).mockReturnValue(true)

const { result } = renderHook(() => useKeyboard())

expect(result.current.keyboardShown).toBe(true)
})

it("keyboard closed by default", () => {
const { result } = renderHook(() => useKeyboard())

Expand Down
13 changes: 9 additions & 4 deletions src/useKeyboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,17 @@ const initialValue = {
}

export function useKeyboard() {
const [shown, setShown] = useState(false)
// These APIs are unavailable in older supported React Native versions.
const [initialMetrics] = useState(() => Keyboard.metrics?.())
const [shown, setShown] = useState(() => {
const isVisible = Keyboard.isVisible?.()
return isVisible ?? initialMetrics !== undefined
})
const [coordinates, setCoordinates] = useState<{
start: undefined | KeyboardMetrics
end: KeyboardMetrics
}>(initialValue)
const [keyboardHeight, setKeyboardHeight] = useState<number>(0)
}>(initialMetrics ? { start: undefined, end: initialMetrics } : initialValue)
const [keyboardHeight, setKeyboardHeight] = useState(initialMetrics?.height ?? 0)

const handleKeyboardWillShow: KeyboardEventListener = (e) => {
setCoordinates({ start: e.startCoordinates, end: e.endCoordinates })
Expand All @@ -33,11 +38,11 @@ export function useKeyboard() {
}
const handleKeyboardDidHide: KeyboardEventListener = (e) => {
setShown(false)
setKeyboardHeight(0)
if (e) {
setCoordinates({ start: e.startCoordinates, end: e.endCoordinates })
} else {
setCoordinates(initialValue)
setKeyboardHeight(0)
}
}

Expand Down
8 changes: 5 additions & 3 deletions src/useRefresh.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,24 @@ import { useRefresh } from "./useRefresh"
const DELAY_IN_MS = 300
jest.useFakeTimers()
describe("useRefresh", () => {
it("should invoke refresh and return correct refreshing state", () => {
it("should invoke refresh and return correct refreshing state", async () => {
const wait = () => {
return new Promise((resolve) => setTimeout(resolve, DELAY_IN_MS))
}

const { result } = renderHook(() => useRefresh(wait))

const spy = jest.spyOn(result.current, "onRefresh")
let refreshPromise: Promise<void> | undefined
act(() => {
result.current.onRefresh()
refreshPromise = result.current.onRefresh()
})

expect(result.current.isRefreshing).toBe(true)
expect(spy).toHaveBeenCalledTimes(1)
act(() => {
await act(async () => {
jest.advanceTimersByTime(DELAY_IN_MS)
await refreshPromise
})

expect(result.current.isRefreshing).toBe(false)
Expand Down