From 1ba61d3cddec4c6386d3669d99295dcb86e092b9 Mon Sep 17 00:00:00 2001 From: Justin Haut Date: Wed, 10 Sep 2025 13:58:43 -0500 Subject: [PATCH 1/2] Fix Android dark mode detection for KeyboardToolbar --- src/components/KeyboardToolbar/Arrow.tsx | 4 +-- src/components/KeyboardToolbar/Button.tsx | 5 +-- src/components/KeyboardToolbar/index.tsx | 4 +-- src/hooks/index.ts | 1 + src/hooks/useKeyboardToolbarTheme/index.ts | 40 ++++++++++++++++++++++ 5 files changed, 48 insertions(+), 6 deletions(-) create mode 100644 src/hooks/useKeyboardToolbarTheme/index.ts diff --git a/src/components/KeyboardToolbar/Arrow.tsx b/src/components/KeyboardToolbar/Arrow.tsx index 5a1290d99a..a33dd09c10 100644 --- a/src/components/KeyboardToolbar/Arrow.tsx +++ b/src/components/KeyboardToolbar/Arrow.tsx @@ -1,7 +1,7 @@ import React, { useMemo } from "react"; import { Animated, StyleSheet, View } from "react-native"; -import { useKeyboardState } from "../../hooks"; +import { useKeyboardToolbarTheme } from "../../hooks"; import type { KeyboardToolbarTheme } from "./types"; import type { ViewStyle } from "react-native"; @@ -13,7 +13,7 @@ type ArrowProps = { }; const ArrowComponent: React.FC = ({ type, disabled, theme }) => { - const colorScheme = useKeyboardState((state) => state.appearance); + const colorScheme = useKeyboardToolbarTheme(); const color = useMemo( () => ({ diff --git a/src/components/KeyboardToolbar/Button.tsx b/src/components/KeyboardToolbar/Button.tsx index d0a90d906d..6caacee414 100644 --- a/src/components/KeyboardToolbar/Button.tsx +++ b/src/components/KeyboardToolbar/Button.tsx @@ -6,7 +6,7 @@ import { View, } from "react-native"; -import { useKeyboardState } from "../../hooks"; +import { useKeyboardToolbarTheme } from "../../hooks"; import type { KeyboardToolbarTheme } from "./types"; import type { PropsWithChildren } from "react"; @@ -64,7 +64,8 @@ const ButtonAndroid = ({ style, theme, }: PropsWithChildren) => { - const colorScheme = useKeyboardState((state) => state.appearance); + const colorScheme = useKeyboardToolbarTheme(); + const accessibilityState = useMemo(() => ({ disabled }), [disabled]); const ripple = useMemo( () => diff --git a/src/components/KeyboardToolbar/index.tsx b/src/components/KeyboardToolbar/index.tsx index 741b230ef6..a3f993ad5f 100644 --- a/src/components/KeyboardToolbar/index.tsx +++ b/src/components/KeyboardToolbar/index.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useMemo, useState } from "react"; import { StyleSheet, Text, View } from "react-native"; import { FocusedInputEvents } from "../../bindings"; -import { useKeyboardState } from "../../hooks"; +import { useKeyboardToolbarTheme } from "../../hooks"; import { KeyboardController } from "../../module"; import KeyboardStickyView from "../KeyboardStickyView"; @@ -106,7 +106,7 @@ const KeyboardToolbar: React.FC = (props) => { insets, ...rest } = props; - const colorScheme = useKeyboardState((state) => state.appearance); + const colorScheme = useKeyboardToolbarTheme(); const [inputs, setInputs] = useState({ current: 0, count: 0, diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 3dc66e5856..e787a10753 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -237,3 +237,4 @@ export function useFocusedInputHandler( export * from "./useWindowDimensions"; export * from "./useKeyboardState"; +export * from "./useKeyboardToolbarTheme"; diff --git a/src/hooks/useKeyboardToolbarTheme/index.ts b/src/hooks/useKeyboardToolbarTheme/index.ts new file mode 100644 index 0000000000..7a3b7e8d05 --- /dev/null +++ b/src/hooks/useKeyboardToolbarTheme/index.ts @@ -0,0 +1,40 @@ +import { useEffect, useState } from "react"; +import { Appearance, type ColorSchemeName } from "react-native"; + +import { useKeyboardState } from "../useKeyboardState"; + +type ColorScheme = "light" | "dark"; + +/** + * Hook that provides the correct theme for the keyboard toolbar. + * Falls back to system appearance for Android devices where keyboard + * appearance detection may be unreliable. + * + * @returns The color scheme to use for the toolbar ("light" or "dark") + */ +export const useKeyboardToolbarTheme = (): ColorScheme => { + const keyboardColorScheme = useKeyboardState((state) => state.appearance); + const [systemColorScheme, setSystemColorScheme] = useState( + Appearance.getColorScheme(), + ); + + useEffect(() => { + const subscription = Appearance.addChangeListener(({ colorScheme }) => { + setSystemColorScheme(colorScheme); + }); + + return () => subscription.remove(); + }, []); + + // Use keyboard appearance if it's explicitly dark, + // or if keyboard is light but system is dark (Android < API 29 fallback) + if (keyboardColorScheme === "dark") { + return "dark"; + } + + if (keyboardColorScheme === "light" && systemColorScheme === "dark") { + return "dark"; + } + + return "light"; +}; From 3916c4da513b3f580227c5c304855a723e2f8553 Mon Sep 17 00:00:00 2001 From: Justin Haut Date: Fri, 12 Sep 2025 07:56:13 -0500 Subject: [PATCH 2/2] fix(android): improve KeyboardToolbar dark mode detection - Use Configuration.UI_MODE_NIGHT_MASK for reliable system dark mode detection - Remove JavaScript fallback approach for cleaner native-only solution - Fix issue where toolbar showed light theme when system was in dark mode Tested on Galaxy S22 (API 35) - now correctly detects dark mode --- .../extensions/Context.kt | 8 +--- src/components/KeyboardToolbar/Arrow.tsx | 4 +- src/components/KeyboardToolbar/Button.tsx | 4 +- src/components/KeyboardToolbar/index.tsx | 4 +- src/hooks/index.ts | 1 - src/hooks/useKeyboardToolbarTheme/index.ts | 40 ------------------- 6 files changed, 8 insertions(+), 53 deletions(-) delete mode 100644 src/hooks/useKeyboardToolbarTheme/index.ts diff --git a/android/src/main/java/com/reactnativekeyboardcontroller/extensions/Context.kt b/android/src/main/java/com/reactnativekeyboardcontroller/extensions/Context.kt index 0a651ec50c..8dede30a3b 100644 --- a/android/src/main/java/com/reactnativekeyboardcontroller/extensions/Context.kt +++ b/android/src/main/java/com/reactnativekeyboardcontroller/extensions/Context.kt @@ -3,6 +3,7 @@ package com.reactnativekeyboardcontroller.extensions import android.annotation.SuppressLint import android.content.ComponentName import android.content.Context +import android.content.res.Configuration import android.graphics.Point import android.os.Build import android.provider.Settings @@ -40,12 +41,7 @@ fun Context.getDisplaySize(): Point { } fun Context.isSystemDarkMode(): Boolean = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - (getSystemService(Context.UI_MODE_SERVICE) as? android.app.UiModeManager) - ?.nightMode == android.app.UiModeManager.MODE_NIGHT_YES - } else { - false - } + resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES fun Context.currentImePackage(): String? { val id = diff --git a/src/components/KeyboardToolbar/Arrow.tsx b/src/components/KeyboardToolbar/Arrow.tsx index a33dd09c10..5a1290d99a 100644 --- a/src/components/KeyboardToolbar/Arrow.tsx +++ b/src/components/KeyboardToolbar/Arrow.tsx @@ -1,7 +1,7 @@ import React, { useMemo } from "react"; import { Animated, StyleSheet, View } from "react-native"; -import { useKeyboardToolbarTheme } from "../../hooks"; +import { useKeyboardState } from "../../hooks"; import type { KeyboardToolbarTheme } from "./types"; import type { ViewStyle } from "react-native"; @@ -13,7 +13,7 @@ type ArrowProps = { }; const ArrowComponent: React.FC = ({ type, disabled, theme }) => { - const colorScheme = useKeyboardToolbarTheme(); + const colorScheme = useKeyboardState((state) => state.appearance); const color = useMemo( () => ({ diff --git a/src/components/KeyboardToolbar/Button.tsx b/src/components/KeyboardToolbar/Button.tsx index 6caacee414..a4ca60e596 100644 --- a/src/components/KeyboardToolbar/Button.tsx +++ b/src/components/KeyboardToolbar/Button.tsx @@ -6,7 +6,7 @@ import { View, } from "react-native"; -import { useKeyboardToolbarTheme } from "../../hooks"; +import { useKeyboardState } from "../../hooks"; import type { KeyboardToolbarTheme } from "./types"; import type { PropsWithChildren } from "react"; @@ -64,7 +64,7 @@ const ButtonAndroid = ({ style, theme, }: PropsWithChildren) => { - const colorScheme = useKeyboardToolbarTheme(); + const colorScheme = useKeyboardState((state) => state.appearance); const accessibilityState = useMemo(() => ({ disabled }), [disabled]); const ripple = useMemo( diff --git a/src/components/KeyboardToolbar/index.tsx b/src/components/KeyboardToolbar/index.tsx index a3f993ad5f..741b230ef6 100644 --- a/src/components/KeyboardToolbar/index.tsx +++ b/src/components/KeyboardToolbar/index.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useMemo, useState } from "react"; import { StyleSheet, Text, View } from "react-native"; import { FocusedInputEvents } from "../../bindings"; -import { useKeyboardToolbarTheme } from "../../hooks"; +import { useKeyboardState } from "../../hooks"; import { KeyboardController } from "../../module"; import KeyboardStickyView from "../KeyboardStickyView"; @@ -106,7 +106,7 @@ const KeyboardToolbar: React.FC = (props) => { insets, ...rest } = props; - const colorScheme = useKeyboardToolbarTheme(); + const colorScheme = useKeyboardState((state) => state.appearance); const [inputs, setInputs] = useState({ current: 0, count: 0, diff --git a/src/hooks/index.ts b/src/hooks/index.ts index e787a10753..3dc66e5856 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -237,4 +237,3 @@ export function useFocusedInputHandler( export * from "./useWindowDimensions"; export * from "./useKeyboardState"; -export * from "./useKeyboardToolbarTheme"; diff --git a/src/hooks/useKeyboardToolbarTheme/index.ts b/src/hooks/useKeyboardToolbarTheme/index.ts deleted file mode 100644 index 7a3b7e8d05..0000000000 --- a/src/hooks/useKeyboardToolbarTheme/index.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { useEffect, useState } from "react"; -import { Appearance, type ColorSchemeName } from "react-native"; - -import { useKeyboardState } from "../useKeyboardState"; - -type ColorScheme = "light" | "dark"; - -/** - * Hook that provides the correct theme for the keyboard toolbar. - * Falls back to system appearance for Android devices where keyboard - * appearance detection may be unreliable. - * - * @returns The color scheme to use for the toolbar ("light" or "dark") - */ -export const useKeyboardToolbarTheme = (): ColorScheme => { - const keyboardColorScheme = useKeyboardState((state) => state.appearance); - const [systemColorScheme, setSystemColorScheme] = useState( - Appearance.getColorScheme(), - ); - - useEffect(() => { - const subscription = Appearance.addChangeListener(({ colorScheme }) => { - setSystemColorScheme(colorScheme); - }); - - return () => subscription.remove(); - }, []); - - // Use keyboard appearance if it's explicitly dark, - // or if keyboard is light but system is dark (Android < API 29 fallback) - if (keyboardColorScheme === "dark") { - return "dark"; - } - - if (keyboardColorScheme === "light" && systemColorScheme === "dark") { - return "dark"; - } - - return "light"; -};