diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss
index 12d95b33a6..9afa963229 100644
--- a/stylesheets/_modules.scss
+++ b/stylesheets/_modules.scss
@@ -7584,11 +7584,29 @@ button.module-image__border-overlay:focus {
flex-direction: row;
width: 100%;
- &:hover {
- @include light-theme {
- background-color: $color-gray-15;
+ @include light-theme {
+ &:hover {
+ background-color: $color-gray-05;
}
- @include dark-theme {
+ }
+ @include dark-theme {
+ &:hover {
+ background-color: $color-gray-60;
+ }
+ }
+ @include keyboard-mode {
+ &:hover {
+ background-color: inherit;
+ }
+ &:focus {
+ background-color: $color-gray-05;
+ }
+ }
+ @include dark-keyboard-mode {
+ &:hover {
+ background-color: inherit;
+ }
+ &:focus {
background-color: $color-gray-60;
}
}
diff --git a/ts/components/AvatarPreview.tsx b/ts/components/AvatarPreview.tsx
index fcc609a9e2..2c316be759 100644
--- a/ts/components/AvatarPreview.tsx
+++ b/ts/components/AvatarPreview.tsx
@@ -134,7 +134,7 @@ export const AvatarPreview = ({
const isLoading = imageStatus === ImageStatus.Loading;
- const clickProps = onClick ? { role: 'button', onClick } : {};
+ const clickProps = onClick ? { role: 'button', onClick, tabIndex: 0 } : {};
const componentStyle = {
...style,
};
diff --git a/ts/components/ChatColorPicker.tsx b/ts/components/ChatColorPicker.tsx
index 672624eb76..0853732c54 100644
--- a/ts/components/ChatColorPicker.tsx
+++ b/ts/components/ChatColorPicker.tsx
@@ -18,6 +18,8 @@ import { SampleMessageBubbles } from './SampleMessageBubbles';
import { PanelRow } from './conversation/conversation-details/PanelRow';
import { getCustomColorStyle } from '../util/getCustomColorStyle';
+import { useDelayedRestoreFocus } from '../hooks/useRestoreFocus';
+
type CustomColorDataType = {
id?: string;
value?: CustomColorType;
@@ -84,6 +86,8 @@ export const ChatColorPicker = ({
CustomColorDataType | undefined
>(undefined);
+ const [focusRef] = useDelayedRestoreFocus();
+
const onSelectColor = (
conversationColor: ConversationColorType,
customColorData?: { id: string; value: CustomColorType }
@@ -172,7 +176,7 @@ export const ChatColorPicker = ({
/>
- {ConversationColors.map(color => (
+ {ConversationColors.map((color, i) => (
))}
{Object.keys(customColors).map(colorId => {
diff --git a/ts/components/Select.tsx b/ts/components/Select.tsx
index 29c967b225..527deec395 100644
--- a/ts/components/Select.tsx
+++ b/ts/components/Select.tsx
@@ -19,41 +19,40 @@ export type PropsType = Readonly<{
value?: string | number;
}>;
-export function Select({
- disabled,
- moduleClassName,
- name,
- onChange,
- options,
- value,
-}: PropsType): JSX.Element {
- const onSelectChange = (event: ChangeEvent
) => {
- onChange(event.target.value);
- };
+export const Select = React.forwardRef(
+ (
+ { disabled, moduleClassName, name, onChange, options, value }: PropsType,
+ ref: React.Ref
+ ): JSX.Element => {
+ const onSelectChange = (event: ChangeEvent) => {
+ onChange(event.target.value);
+ };
- return (
-
-
-
- );
-}
+ return (
+
+
+
+ );
+ }
+);
diff --git a/ts/components/conversation/conversation-details/GroupLinkManagement.stories.tsx b/ts/components/conversation/conversation-details/GroupLinkManagement.stories.tsx
index 14f5e5eca7..2168d73d3c 100644
--- a/ts/components/conversation/conversation-details/GroupLinkManagement.stories.tsx
+++ b/ts/components/conversation/conversation-details/GroupLinkManagement.stories.tsx
@@ -88,3 +88,9 @@ story.add('On (Non-admin)', () => {
return ;
});
+
+story.add('Off (Non-admin) - user cannot get here', () => {
+ const props = createProps(undefined, false);
+
+ return ;
+});
diff --git a/ts/components/conversation/conversation-details/GroupLinkManagement.tsx b/ts/components/conversation/conversation-details/GroupLinkManagement.tsx
index cb8b8fc30c..bfee5873c7 100644
--- a/ts/components/conversation/conversation-details/GroupLinkManagement.tsx
+++ b/ts/components/conversation/conversation-details/GroupLinkManagement.tsx
@@ -11,6 +11,8 @@ import { PanelRow } from './PanelRow';
import { PanelSection } from './PanelSection';
import { Select } from '../../Select';
+import { useDelayedRestoreFocus } from '../../../hooks/useRestoreFocus';
+
const AccessControlEnum = Proto.AccessControl.AccessRequired;
export type PropsType = {
@@ -36,6 +38,8 @@ export const GroupLinkManagement: React.ComponentType = ({
throw new Error('GroupLinkManagement rendered without a conversation');
}
+ const [focusRef] = useDelayedRestoreFocus();
+
const createEventHandler = (handleEvent: (x: boolean) => void) => {
return (value: string) => {
handleEvent(value === 'true');
@@ -72,6 +76,7 @@ export const GroupLinkManagement: React.ComponentType = ({
value: 'false',
},
]}
+ ref={focusRef}
value={String(Boolean(hasGroupLink))}
/>
) : null
@@ -90,6 +95,7 @@ export const GroupLinkManagement: React.ComponentType = ({
/>
}
label={i18n('GroupLinkManagement--share')}
+ ref={!isAdmin ? focusRef : undefined}
onClick={() => {
if (conversation.groupLink) {
copyGroupLink(conversation.groupLink);
diff --git a/ts/components/conversation/conversation-details/PanelRow.tsx b/ts/components/conversation/conversation-details/PanelRow.tsx
index f1cd725a10..99d084b297 100644
--- a/ts/components/conversation/conversation-details/PanelRow.tsx
+++ b/ts/components/conversation/conversation-details/PanelRow.tsx
@@ -19,43 +19,55 @@ export type Props = {
const bem = bemGenerator('ConversationDetails-panel-row');
-export const PanelRow: React.ComponentType = ({
- alwaysShowActions,
- className,
- disabled,
- icon,
- label,
- info,
- right,
- actions,
- onClick,
-}) => {
- const content = (
- <>
- {icon !== undefined ? {icon}
: null}
-
-
{label}
- {info !== undefined ?
{info}
: null}
-
- {right !== undefined ? {right}
: null}
- {actions !== undefined ? (
- {actions}
- ) : null}
- >
- );
-
- if (onClick) {
- return (
-
+export const PanelRow = React.forwardRef(
+ (
+ {
+ alwaysShowActions,
+ className,
+ disabled,
+ icon,
+ label,
+ info,
+ right,
+ actions,
+ onClick,
+ }: Props,
+ ref: React.Ref
+ ) => {
+ const content = (
+ <>
+ {icon !== undefined ? {icon}
: null}
+
+
{label}
+ {info !== undefined ? (
+
{info}
+ ) : null}
+
+ {right !== undefined ? (
+ {right}
+ ) : null}
+ {actions !== undefined ? (
+
+ {actions}
+
+ ) : null}
+ >
);
- }
- return {content}
;
-};
+ if (onClick) {
+ return (
+
+ );
+ }
+
+ return {content}
;
+ }
+);
diff --git a/ts/hooks/useRestoreFocus.ts b/ts/hooks/useRestoreFocus.ts
index 02f82db0c7..a5d92fe5ee 100644
--- a/ts/hooks/useRestoreFocus.ts
+++ b/ts/hooks/useRestoreFocus.ts
@@ -45,3 +45,51 @@ export const useRestoreFocus = (): Array => {
return [setFocusRef];
};
+
+// Panels are initially rendered outside the DOM, and then added to it. We need to
+// delay our attempts to set focus.
+// Just like the above hook, but with a debounce.
+export const useDelayedRestoreFocus = (): Array => {
+ const toFocusRef = React.useRef(null);
+ const lastFocusedRef = React.useRef(null);
+
+ const setFocusRef = React.useCallback(
+ (toFocus: HTMLElement | null | undefined) => {
+ function setFocus() {
+ 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();
+ }
+
+ const timeout = setTimeout(setFocus, 250);
+
+ return () => {
+ clearTimeout(timeout);
+ };
+ },
+ []
+ );
+
+ 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];
+};
diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json
index 8debd74577..4ddc8f756b 100644
--- a/ts/util/lint/exceptions.json
+++ b/ts/util/lint/exceptions.json
@@ -13022,6 +13022,20 @@
"reasonCategory": "usageTrusted",
"updated": "2021-09-17T17:37:46.279Z"
},
+ {
+ "rule": "React-useRef",
+ "path": "ts/hooks/useRestoreFocus.js",
+ "line": " const toFocusRef = React.useRef(null);",
+ "reasonCategory": "usageTrusted",
+ "updated": "2021-10-22T00:52:39.251Z"
+ },
+ {
+ "rule": "React-useRef",
+ "path": "ts/hooks/useRestoreFocus.js",
+ "line": " const lastFocusedRef = React.useRef(null);",
+ "reasonCategory": "usageTrusted",
+ "updated": "2021-10-22T00:52:39.251Z"
+ },
{
"rule": "React-useRef",
"path": "ts/hooks/useRestoreFocus.ts",
@@ -13036,6 +13050,20 @@
"reasonCategory": "usageTrusted",
"updated": "2021-07-30T16:57:33.618Z"
},
+ {
+ "rule": "React-useRef",
+ "path": "ts/hooks/useRestoreFocus.ts",
+ "line": " const toFocusRef = React.useRef(null);",
+ "reasonCategory": "usageTrusted",
+ "updated": "2021-10-22T00:52:39.251Z"
+ },
+ {
+ "rule": "React-useRef",
+ "path": "ts/hooks/useRestoreFocus.ts",
+ "line": " const lastFocusedRef = React.useRef(null);",
+ "reasonCategory": "usageTrusted",
+ "updated": "2021-10-22T00:52:39.251Z"
+ },
{
"rule": "jQuery-append(",
"path": "ts/logging/debuglogs.js",