summaryrefslogtreecommitdiffhomepage
path: root/desktop
diff options
context:
space:
mode:
authorOliver <oliver@mohlin.dev>2025-09-19 11:53:28 +0200
committerTobias Järvelöv <tobias.jarvelov@mullvad.net>2025-09-22 12:35:44 +0200
commit8f0195f85571e63ea16e845c673f9a110decb4f9 (patch)
treeb447337177b02ac2e1f3fa2d83bcc0b149140d40 /desktop
parent88ae787bb246735f307f3c09049615d464b066de (diff)
downloadmullvadvpn-8f0195f85571e63ea16e845c673f9a110decb4f9.tar.xz
mullvadvpn-8f0195f85571e63ea16e845c673f9a110decb4f9.zip
Add InitialFocus component
Diffstat (limited to 'desktop')
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/initial-focus/InitialFocus.tsx18
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/initial-focus/index.ts1
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/hooks/index.ts3
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/hooks/useFocusReference.ts12
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/hooks/useInitialFocus.ts23
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/hooks/useIsDefaultActiveElementAfterMount.ts23
6 files changed, 80 insertions, 0 deletions
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/initial-focus/InitialFocus.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/initial-focus/InitialFocus.tsx
new file mode 100644
index 0000000000..731892d0c2
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/initial-focus/InitialFocus.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+
+import { useInitialFocus } from '../../hooks';
+
+type AnyElement = React.ElementType;
+
+export type InitialFocusProps<E extends AnyElement> = {
+ children: React.ReactElement<React.ComponentPropsWithRef<E>>;
+} & Omit<React.ComponentPropsWithoutRef<E>, 'children'>;
+
+export function InitialFocus<E extends AnyElement>({ children, ...props }: InitialFocusProps<E>) {
+ const { ref } = useInitialFocus();
+ return React.cloneElement(children, {
+ ref,
+ tabIndex: -1,
+ ...props,
+ } as React.ComponentPropsWithRef<E>);
+}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/initial-focus/index.ts b/desktop/packages/mullvad-vpn/src/renderer/components/initial-focus/index.ts
new file mode 100644
index 0000000000..0dfd0f3458
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/initial-focus/index.ts
@@ -0,0 +1 @@
+export * from './InitialFocus';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/hooks/index.ts b/desktop/packages/mullvad-vpn/src/renderer/hooks/index.ts
index c28f580da0..a152881c63 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/hooks/index.ts
+++ b/desktop/packages/mullvad-vpn/src/renderer/hooks/index.ts
@@ -4,7 +4,10 @@ export * from './useHasAppUpgradeError';
export * from './useHasAppUpgradeEvent';
export * from './useHasAppUpgradeInitiated';
export * from './useHasAppUpgradeVerifiedInstallerPath';
+export * from './useIsDefaultActiveElementAfterMount';
export * from './useIsPlatformLinux';
export * from './useMeasure';
export * from './useScrollToAnchor';
export * from './useScrollToListItem';
+export * from './useInitialFocus';
+export * from './useFocusReference';
diff --git a/desktop/packages/mullvad-vpn/src/renderer/hooks/useFocusReference.ts b/desktop/packages/mullvad-vpn/src/renderer/hooks/useFocusReference.ts
new file mode 100644
index 0000000000..d49264630f
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/hooks/useFocusReference.ts
@@ -0,0 +1,12 @@
+import React from 'react';
+
+export const useFocusReference = <T extends HTMLElement>(
+ ref?: React.RefObject<T | null>,
+ focus?: boolean,
+) => {
+ React.useEffect(() => {
+ if (focus) {
+ ref?.current?.focus({ preventScroll: true });
+ }
+ }, [ref, focus]);
+};
diff --git a/desktop/packages/mullvad-vpn/src/renderer/hooks/useInitialFocus.ts b/desktop/packages/mullvad-vpn/src/renderer/hooks/useInitialFocus.ts
new file mode 100644
index 0000000000..72e8355489
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/hooks/useInitialFocus.ts
@@ -0,0 +1,23 @@
+import React from 'react';
+
+import { useFocusReference } from './useFocusReference';
+import { useIsDefaultActiveElementAfterMount } from './useIsDefaultActiveElementAfterMount';
+
+export const useInitialFocus = <T extends HTMLElement = HTMLDivElement>(): {
+ ref?: React.RefObject<T | null>;
+} => {
+ const ref = React.useRef<T>(null);
+
+ const isDefaultFocus = useIsDefaultActiveElementAfterMount();
+ const shouldFocus = isDefaultFocus === true;
+
+ useFocusReference(ref, shouldFocus);
+
+ if (!isDefaultFocus)
+ return {
+ ref: undefined,
+ };
+ return {
+ ref,
+ };
+};
diff --git a/desktop/packages/mullvad-vpn/src/renderer/hooks/useIsDefaultActiveElementAfterMount.ts b/desktop/packages/mullvad-vpn/src/renderer/hooks/useIsDefaultActiveElementAfterMount.ts
new file mode 100644
index 0000000000..f79669e766
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/hooks/useIsDefaultActiveElementAfterMount.ts
@@ -0,0 +1,23 @@
+import React from 'react';
+
+export const useIsDefaultActiveElementAfterMount = () => {
+ const [isDefaultActiveElementAfterMount, setIsDefaultActiveElementAfterMount] = React.useState<
+ boolean | undefined
+ >(undefined);
+
+ React.useEffect(() => {
+ if (typeof document !== 'undefined') {
+ const isBodyOrDocumentElement =
+ document.activeElement === document.body ||
+ document.activeElement === document.documentElement;
+
+ setIsDefaultActiveElementAfterMount(isBodyOrDocumentElement);
+ }
+
+ return () => {
+ setIsDefaultActiveElementAfterMount(undefined);
+ };
+ }, []);
+
+ return isDefaultActiveElementAfterMount;
+};