MM-62226 - set focus back to search button (#29669)
* MM-62226 - set focus back to search button * fix linter
Этот коммит содержится в:
@@ -14,7 +14,9 @@ import {getSearchTerms, getSearchType} from 'selectors/rhs';
|
|||||||
|
|
||||||
import Popover from 'components/widgets/popover';
|
import Popover from 'components/widgets/popover';
|
||||||
|
|
||||||
import Constants from 'utils/constants';
|
import a11yController from 'utils/a11y_controller_instance';
|
||||||
|
import type {A11yFocusEventDetail} from 'utils/constants';
|
||||||
|
import Constants, {A11yCustomEventTypes} from 'utils/constants';
|
||||||
import * as Keyboard from 'utils/keyboard';
|
import * as Keyboard from 'utils/keyboard';
|
||||||
import {isServerVersionGreaterThanOrEqualTo} from 'utils/server_version';
|
import {isServerVersionGreaterThanOrEqualTo} from 'utils/server_version';
|
||||||
import {isDesktopApp, getDesktopVersion, isMacApp} from 'utils/user_agent';
|
import {isDesktopApp, getDesktopVersion, isMacApp} from 'utils/user_agent';
|
||||||
@@ -102,6 +104,7 @@ const NewSearch = (): JSX.Element => {
|
|||||||
const [focused, setFocused] = useState<boolean>(false);
|
const [focused, setFocused] = useState<boolean>(false);
|
||||||
const [currentChannel, setCurrentChannel] = useState('');
|
const [currentChannel, setCurrentChannel] = useState('');
|
||||||
const searchBoxRef = useRef<HTMLDivElement | null>(null);
|
const searchBoxRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const searchButtonRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const isDesktop = isDesktopApp() && isServerVersionGreaterThanOrEqualTo(getDesktopVersion(), '4.7.0');
|
const isDesktop = isDesktopApp() && isServerVersionGreaterThanOrEqualTo(getDesktopVersion(), '4.7.0');
|
||||||
@@ -160,10 +163,24 @@ const NewSearch = (): JSX.Element => {
|
|||||||
const closeSearchBox = useCallback(() => {
|
const closeSearchBox = useCallback(() => {
|
||||||
setFocused(false);
|
setFocused(false);
|
||||||
setCurrentChannel('');
|
setCurrentChannel('');
|
||||||
|
if (searchButtonRef.current) {
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent<A11yFocusEventDetail>(A11yCustomEventTypes.FOCUS, {
|
||||||
|
detail: {
|
||||||
|
target: searchButtonRef.current,
|
||||||
|
keyboardOnly: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
a11yController.resetOriginElement();
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const openSearchBox = useCallback(() => {
|
const openSearchBox = useCallback(() => {
|
||||||
setFocused(true);
|
setFocused(true);
|
||||||
|
if (searchButtonRef.current) {
|
||||||
|
a11yController.storeOriginElement(searchButtonRef.current);
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const openSearchBoxOnKeyPress = useCallback(
|
const openSearchBoxOnKeyPress = useCallback(
|
||||||
@@ -220,6 +237,7 @@ const NewSearch = (): JSX.Element => {
|
|||||||
id='searchFormContainer'
|
id='searchFormContainer'
|
||||||
role='button'
|
role='button'
|
||||||
className='a11y__region'
|
className='a11y__region'
|
||||||
|
ref={searchButtonRef}
|
||||||
>
|
>
|
||||||
<i className='icon icon-magnify'/>
|
<i className='icon icon-magnify'/>
|
||||||
{(searchType === 'messages' || searchType === 'files') && (
|
{(searchType === 'messages' || searchType === 'files') && (
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import SidebarMobileRightMenu from 'components/sidebar_mobile_right_menu';
|
|||||||
|
|
||||||
import webSocketClient from 'client/web_websocket_client';
|
import webSocketClient from 'client/web_websocket_client';
|
||||||
import {initializePlugins} from 'plugins';
|
import {initializePlugins} from 'plugins';
|
||||||
import A11yController from 'utils/a11y_controller';
|
import 'utils/a11y_controller_instance';
|
||||||
import {PageLoadContext, SCHEDULED_POST_URL_SUFFIX} from 'utils/constants';
|
import {PageLoadContext, SCHEDULED_POST_URL_SUFFIX} from 'utils/constants';
|
||||||
import DesktopApp from 'utils/desktop_api';
|
import DesktopApp from 'utils/desktop_api';
|
||||||
import {EmojiIndicesByAlias} from 'utils/emoji';
|
import {EmojiIndicesByAlias} from 'utils/emoji';
|
||||||
@@ -92,8 +92,6 @@ interface State {
|
|||||||
export default class Root extends React.PureComponent<Props, State> {
|
export default class Root extends React.PureComponent<Props, State> {
|
||||||
// The constructor adds a bunch of event listeners,
|
// The constructor adds a bunch of event listeners,
|
||||||
// so we do need this.
|
// so we do need this.
|
||||||
private a11yController: A11yController;
|
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
@@ -107,8 +105,6 @@ export default class Root extends React.PureComponent<Props, State> {
|
|||||||
this.state = {
|
this.state = {
|
||||||
shouldMountAppRoutes: false,
|
shouldMountAppRoutes: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.a11yController = new A11yController();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setRudderConfig = () => {
|
setRudderConfig = () => {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import RhsThread from 'components/rhs_thread';
|
|||||||
import Search from 'components/search/index';
|
import Search from 'components/search/index';
|
||||||
|
|
||||||
import RhsPlugin from 'plugins/rhs_plugin';
|
import RhsPlugin from 'plugins/rhs_plugin';
|
||||||
|
import a11yController from 'utils/a11y_controller_instance';
|
||||||
import type {A11yFocusEventDetail} from 'utils/constants';
|
import type {A11yFocusEventDetail} from 'utils/constants';
|
||||||
import Constants, {A11yCustomEventTypes} from 'utils/constants';
|
import Constants, {A11yCustomEventTypes} from 'utils/constants';
|
||||||
import {cmdOrCtrlPressed, isKeyPressed} from 'utils/keyboard';
|
import {cmdOrCtrlPressed, isKeyPressed} from 'utils/keyboard';
|
||||||
@@ -170,19 +171,23 @@ export default class SidebarRight extends React.PureComponent<Props, State> {
|
|||||||
} else if (!this.props.isOpen && wasOpen) {
|
} else if (!this.props.isOpen && wasOpen) {
|
||||||
// RHS just was closed, restore focus to the previous element had it
|
// RHS just was closed, restore focus to the previous element had it
|
||||||
// this will have to change for upcoming work specially for search and probalby plugins
|
// this will have to change for upcoming work specially for search and probalby plugins
|
||||||
requestAnimationFrame(() => {
|
if (a11yController.originElement) {
|
||||||
if (this.previousActiveElement) {
|
a11yController.restoreOriginFocus();
|
||||||
document.dispatchEvent(
|
} else {
|
||||||
new CustomEvent<A11yFocusEventDetail>(A11yCustomEventTypes.FOCUS, {
|
requestAnimationFrame(() => {
|
||||||
detail: {
|
if (this.previousActiveElement) {
|
||||||
target: this.previousActiveElement,
|
document.dispatchEvent(
|
||||||
keyboardOnly: false,
|
new CustomEvent<A11yFocusEventDetail>(A11yCustomEventTypes.FOCUS, {
|
||||||
},
|
detail: {
|
||||||
}),
|
target: this.previousActiveElement,
|
||||||
);
|
keyboardOnly: false,
|
||||||
this.previousActiveElement = null;
|
},
|
||||||
}
|
}),
|
||||||
});
|
);
|
||||||
|
this.previousActiveElement = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export default class A11yController {
|
|||||||
lKeyIsPressed = false;
|
lKeyIsPressed = false;
|
||||||
escKeyIsPressed = false;
|
escKeyIsPressed = false;
|
||||||
windowIsFocused = true;
|
windowIsFocused = true;
|
||||||
|
originElement?: HTMLElement | null = null;
|
||||||
|
|
||||||
// used to reset navigation whenever navigation within a region occurs (section or element)
|
// used to reset navigation whenever navigation within a region occurs (section or element)
|
||||||
resetNavigation = false;
|
resetNavigation = false;
|
||||||
@@ -71,6 +72,27 @@ export default class A11yController {
|
|||||||
window.removeEventListener(EventTypes.BLUR, this.handleWindowBlur, listenerOptions);
|
window.removeEventListener(EventTypes.BLUR, this.handleWindowBlur, listenerOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores a reference to the provided DOM element as the "origin element".
|
||||||
|
* The origin element is the element that triggered or last caused a certain
|
||||||
|
* navigation or focus event. This allows restoring focus back to this element
|
||||||
|
* once certain UI components (like RHS, panels, or menus) are closed.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} element - The DOM element to store as the origin element.
|
||||||
|
*/
|
||||||
|
storeOriginElement(element: HTMLElement) {
|
||||||
|
this.originElement = element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the stored origin element reference back to null.
|
||||||
|
* This is typically called after focus has been restored to the origin element,
|
||||||
|
* indicating that the controller no longer needs to keep track of it.
|
||||||
|
*/
|
||||||
|
resetOriginElement() {
|
||||||
|
this.originElement = null;
|
||||||
|
}
|
||||||
|
|
||||||
// convenience getter/setters
|
// convenience getter/setters
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -370,6 +392,23 @@ export default class A11yController {
|
|||||||
this.resetNavigation = true;
|
this.resetNavigation = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
restoreOriginFocus() {
|
||||||
|
if (this.originElement && this.isElementValid(this.originElement)) {
|
||||||
|
// Dispatch a focus event to manually focus this element
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent(A11yCustomEventTypes.FOCUS, {
|
||||||
|
detail: {
|
||||||
|
target: this.originElement,
|
||||||
|
keyboardOnly: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
setTimeout(() => {
|
||||||
|
this.originElement = null;
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resets the a11y navigation controller, active region/section/element, clears focus and resets user interraction states
|
* Resets the a11y navigation controller, active region/section/element, clears focus and resets user interraction states
|
||||||
*/
|
*/
|
||||||
|
|||||||
7
webapp/channels/src/utils/a11y_controller_instance.ts
Обычный файл
7
webapp/channels/src/utils/a11y_controller_instance.ts
Обычный файл
@@ -0,0 +1,7 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import A11yController from 'utils/a11y_controller';
|
||||||
|
|
||||||
|
const a11yController = new A11yController();
|
||||||
|
export default a11yController;
|
||||||
Ссылка в новой задаче
Block a user