Permissions popup context iso
This commit is contained in:
parent
f3715411c6
commit
7b5faa1cc1
49 changed files with 562 additions and 506 deletions
|
@ -2,7 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { usePrevious } from '../util/hooks';
|
||||
import { usePrevious } from './usePrevious';
|
||||
|
||||
type RemoteParticipant = {
|
||||
hasRemoteVideo: boolean;
|
||||
|
|
16
ts/hooks/useBoundActions.ts
Normal file
16
ts/hooks/useBoundActions.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
// Copyright 2020-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { ActionCreatorsMapObject, bindActionCreators } from 'redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useBoundActions = <T extends ActionCreatorsMapObject>(
|
||||
actions: T
|
||||
): T => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
return useMemo(() => {
|
||||
return bindActionCreators(actions, dispatch);
|
||||
}, [actions, dispatch]);
|
||||
};
|
26
ts/hooks/useEscapeHandling.ts
Normal file
26
ts/hooks/useEscapeHandling.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export function useEscapeHandling(handleEscape?: () => unknown): void {
|
||||
useEffect(() => {
|
||||
if (!handleEscape) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handler = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
handleEscape();
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handler);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handler);
|
||||
};
|
||||
}, [handleEscape]);
|
||||
}
|
56
ts/hooks/useHasWrapped.ts
Normal file
56
ts/hooks/useHasWrapped.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
// Copyright 2020-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { Ref, useEffect, useState } from 'react';
|
||||
import { first, last, noop } from 'lodash';
|
||||
|
||||
function getTop(element: Readonly<Element>): number {
|
||||
return element.getBoundingClientRect().top;
|
||||
}
|
||||
|
||||
function isWrapped(element: Readonly<null | HTMLElement>): boolean {
|
||||
if (!element) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { children } = element;
|
||||
const firstChild = first(children);
|
||||
const lastChild = last(children);
|
||||
|
||||
return Boolean(
|
||||
firstChild &&
|
||||
lastChild &&
|
||||
firstChild !== lastChild &&
|
||||
getTop(firstChild) !== getTop(lastChild)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A hook that returns a ref (to put on your element) and a boolean. The boolean will be
|
||||
* `true` if the element's children have different `top`s, and `false` otherwise.
|
||||
*/
|
||||
export function useHasWrapped<T extends HTMLElement>(): [Ref<T>, boolean] {
|
||||
const [element, setElement] = useState<null | T>(null);
|
||||
|
||||
const [hasWrapped, setHasWrapped] = useState(isWrapped(element));
|
||||
|
||||
useEffect(() => {
|
||||
if (!element) {
|
||||
return noop;
|
||||
}
|
||||
|
||||
// We can remove this `any` when we upgrade to TypeScript 4.2+, which adds
|
||||
// `ResizeObserver` type definitions.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const observer = new (window as any).ResizeObserver(() => {
|
||||
setHasWrapped(isWrapped(element));
|
||||
});
|
||||
observer.observe(element);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [element]);
|
||||
|
||||
return [setElement, hasWrapped];
|
||||
}
|
64
ts/hooks/useIntersectionObserver.ts
Normal file
64
ts/hooks/useIntersectionObserver.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
// Copyright 2020-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import * as log from '../logging/log';
|
||||
|
||||
/**
|
||||
* A light hook wrapper around `IntersectionObserver`.
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* function MyComponent() {
|
||||
* const [intersectionRef, intersectionEntry] = useIntersectionObserver();
|
||||
* const isVisible = intersectionEntry
|
||||
* ? intersectionEntry.isIntersecting
|
||||
* : true;
|
||||
*
|
||||
* return (
|
||||
* <div ref={intersectionRef}>
|
||||
* I am {isVisible ? 'on the screen' : 'invisible'}
|
||||
* </div>
|
||||
* );
|
||||
* }
|
||||
*/
|
||||
export function useIntersectionObserver(): [
|
||||
(el?: Element | null) => void,
|
||||
IntersectionObserverEntry | null
|
||||
] {
|
||||
const [
|
||||
intersectionObserverEntry,
|
||||
setIntersectionObserverEntry,
|
||||
] = useState<IntersectionObserverEntry | null>(null);
|
||||
|
||||
const unobserveRef = useRef<(() => unknown) | null>(null);
|
||||
|
||||
const setRef = useCallback((el?: Element | null) => {
|
||||
if (unobserveRef.current) {
|
||||
unobserveRef.current();
|
||||
unobserveRef.current = null;
|
||||
}
|
||||
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
if (entries.length !== 1) {
|
||||
log.error(
|
||||
'IntersectionObserverWrapper was observing the wrong number of elements'
|
||||
);
|
||||
return;
|
||||
}
|
||||
entries.forEach(entry => {
|
||||
setIntersectionObserverEntry(entry);
|
||||
});
|
||||
});
|
||||
|
||||
unobserveRef.current = observer.unobserve.bind(observer, el);
|
||||
|
||||
observer.observe(el);
|
||||
}, []);
|
||||
|
||||
return [setRef, intersectionObserverEntry];
|
||||
}
|
26
ts/hooks/usePageVisibility.ts
Normal file
26
ts/hooks/usePageVisibility.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
// Copyright 2020-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export function usePageVisibility(): boolean {
|
||||
const [result, setResult] = useState(!document.hidden);
|
||||
|
||||
useEffect(() => {
|
||||
const onVisibilityChange = () => {
|
||||
setResult(!document.hidden);
|
||||
};
|
||||
|
||||
document.addEventListener('visibilitychange', onVisibilityChange, false);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener(
|
||||
'visibilitychange',
|
||||
onVisibilityChange,
|
||||
false
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return result;
|
||||
}
|
11
ts/hooks/usePrevious.ts
Normal file
11
ts/hooks/usePrevious.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
// Copyright 2020-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { useRef } from 'react';
|
||||
|
||||
export function usePrevious<T>(initialValue: T, currentValue: T): T {
|
||||
const previousValueRef = useRef<T>(initialValue);
|
||||
const result = previousValueRef.current;
|
||||
previousValueRef.current = currentValue;
|
||||
return result;
|
||||
}
|
47
ts/hooks/useRestoreFocus.ts
Normal file
47
ts/hooks/useRestoreFocus.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
type CallbackType = (toFocus: HTMLElement | null | undefined) => void;
|
||||
|
||||
// Restore focus on teardown
|
||||
export const useRestoreFocus = (): Array<CallbackType> => {
|
||||
const toFocusRef = React.useRef<HTMLElement | null>(null);
|
||||
const lastFocusedRef = React.useRef<HTMLElement | null>(null);
|
||||
|
||||
// We need to use a callback here because refs aren't necessarily populated on first
|
||||
// render. For example, ModalHost makes a top-level parent div first, and then renders
|
||||
// into it. And the children you pass it don't have access to that root div.
|
||||
const setFocusRef = React.useCallback(
|
||||
(toFocus: HTMLElement | null | undefined) => {
|
||||
if (!toFocus) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We only want to do this once.
|
||||
if (toFocusRef.current) {
|
||||
return;
|
||||
}
|
||||
toFocusRef.current = toFocus;
|
||||
|
||||
// Remember last-focused element, focus this new target element.
|
||||
lastFocusedRef.current = document.activeElement as HTMLElement;
|
||||
toFocus.focus();
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
// On unmount, returned focus to element focused before we set the focus
|
||||
setTimeout(() => {
|
||||
if (lastFocusedRef.current && lastFocusedRef.current.focus) {
|
||||
lastFocusedRef.current.focus();
|
||||
}
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
return [setFocusRef];
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue