diff options
| author | Oliver <oliver@mohlin.dev> | 2025-09-19 11:53:28 +0200 |
|---|---|---|
| committer | Tobias Järvelöv <tobias.jarvelov@mullvad.net> | 2025-09-22 12:35:44 +0200 |
| commit | 8f0195f85571e63ea16e845c673f9a110decb4f9 (patch) | |
| tree | b447337177b02ac2e1f3fa2d83bcc0b149140d40 /desktop | |
| parent | 88ae787bb246735f307f3c09049615d464b066de (diff) | |
| download | mullvadvpn-8f0195f85571e63ea16e845c673f9a110decb4f9.tar.xz mullvadvpn-8f0195f85571e63ea16e845c673f9a110decb4f9.zip | |
Add InitialFocus component
Diffstat (limited to 'desktop')
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; +}; |
