From 714e1616711ae2198f276aac1da0156967ddb8fc Mon Sep 17 00:00:00 2001 From: Jamie <113370520+jamiebuilds-signal@users.noreply.github.com> Date: Wed, 12 Nov 2025 09:31:52 -0800 Subject: [PATCH] Migrate react-contextmenu menus to axo menus Co-authored-by: Fedor Indutny --- .eslint/rules/file-suffix.js | 1 - ACKNOWLEDGMENTS.md | 24 - _locales/en/messages.json | 4 + package.json | 6 +- patches/react-contextmenu+2.14.0.patch | 186 ------- pnpm-lock.yaml | 23 - stylesheets/_modules.scss | 318 +---------- .../components/ConversationHeader.scss | 9 - ts/axo/AxoContextMenu.dom.tsx | 115 +++- ts/axo/AxoDropdownMenu.dom.tsx | 20 +- ts/axo/AxoSymbol.dom.tsx | 2 +- ts/axo/_internal/AxoBaseMenu.dom.tsx | 14 +- ts/axo/_internal/ariaRoles.dom.tsx | 185 +++++++ ts/components/ChatColorPicker.dom.tsx | 287 +++++----- .../EditHistoryMessagesModal.dom.tsx | 68 ++- ts/components/ModalHost.dom.tsx | 12 +- ts/components/StoryViewer.dom.tsx | 2 +- ts/components/StoryViewsNRepliesModal.dom.tsx | 235 ++++---- .../CallingNotification.dom.stories.tsx | 1 - .../conversation/CallingNotification.dom.tsx | 140 ++--- .../ConversationHeader.dom.stories.tsx | 54 ++ .../conversation/ConversationHeader.dom.tsx | 508 ++++++++++-------- ts/components/conversation/Message.dom.tsx | 61 ++- .../conversation/MessageContextMenu.dom.tsx | 318 +++-------- .../conversation/TimelineItem.dom.tsx | 1 - .../conversation/TimelineMessage.dom.tsx | 228 ++++---- ts/hooks/useKeyboardShortcuts.dom.tsx | 34 -- ts/test-mock/backups/backups_test.node.ts | 6 +- ts/test-mock/messaging/edit_test.node.ts | 3 +- .../expire_timer_version_test.node.ts | 24 +- .../pnp/accept_gv2_invite_test.node.ts | 7 +- ts/test-mock/pnp/send_gv2_invite_test.node.ts | 6 +- ts/test-mock/storage/archive_test.node.ts | 12 +- ts/test-mock/storage/conflict_test.node.ts | 28 +- ts/test-mock/storage/pin_unpin_test.node.ts | 24 +- ts/util/lint/exceptions.json | 92 ++-- ts/util/lint/linter.node.ts | 1 - 37 files changed, 1366 insertions(+), 1693 deletions(-) delete mode 100644 patches/react-contextmenu+2.14.0.patch create mode 100644 ts/axo/_internal/ariaRoles.dom.tsx diff --git a/.eslint/rules/file-suffix.js b/.eslint/rules/file-suffix.js index 6217847366..52d9da4f5b 100644 --- a/.eslint/rules/file-suffix.js +++ b/.eslint/rules/file-suffix.js @@ -157,7 +157,6 @@ const DOM_PACKAGES = new Set([ 'react-aria', 'react-aria-components', 'react-blurhash', - 'react-contextmenu', 'react-popper', 'react-virtualized', // Note that: react-dom/server is categorized separately diff --git a/ACKNOWLEDGMENTS.md b/ACKNOWLEDGMENTS.md index 3cf767d10f..bdafc7707c 100644 --- a/ACKNOWLEDGMENTS.md +++ b/ACKNOWLEDGMENTS.md @@ -5485,30 +5485,6 @@ Signal Desktop makes use of the following open source projects. License: MIT -## react-contextmenu - - The MIT License (MIT) - - Copyright (c) 2015 Vivek Kumar Bansal - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - ## react-dom MIT License diff --git a/_locales/en/messages.json b/_locales/en/messages.json index b0123bf822..f82b35bb49 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -9058,6 +9058,10 @@ "messageformat": "Edit history", "description": "Modal title for the edit history messages modal" }, + "icu:EditHistoryMessagesModal__Message__ContextMenu__CopyTimestamp": { + "messageformat": "Copy timestamp", + "description": "Context menu item to help debugging" + }, "icu:ResendMessageEdit__body": { "messageformat": "This edit could not be sent. Check your connection and try again", "description": "Modal body for the confirmation dialog shown to user when attempting to resend message edit" diff --git a/package.json b/package.json index c8dd82906d..a0fcfd85af 100644 --- a/package.json +++ b/package.json @@ -201,7 +201,6 @@ "react-aria": "3.35.1", "react-aria-components": "1.4.1", "react-blurhash": "0.3.0", - "react-contextmenu": "2.14.0", "react-dom": "18.3.1", "react-intl": "7.1.11", "react-popper": "2.3.0", @@ -377,9 +376,7 @@ "canvas": "-", "jsdom": "-", "thenify-all>thenify": "3.3.1", - "@electron/rebuild@3.7.2>@electron/node-gyp": "10.2.0-electron.2", - "react-contextmenu>react": "18.3.1", - "react-contextmenu>react-dom": "18.3.1" + "@electron/rebuild@3.7.2>@electron/node-gyp": "10.2.0-electron.2" }, "patchedDependencies": { "casual@1.6.2": "patches/casual+1.6.2.patch", @@ -390,7 +387,6 @@ "qrcode-generator@1.4.4": "patches/qrcode-generator+1.4.4.patch", "@types/node-fetch@2.6.12": "patches/@types+node-fetch+2.6.12.patch", "fabric@4.6.0": "patches/fabric+4.6.0.patch", - "react-contextmenu@2.14.0": "patches/react-contextmenu+2.14.0.patch", "@vitest/expect@2.0.5": "patches/@vitest+expect+2.0.5.patch", "got@11.8.5": "patches/got+11.8.5.patch", "growing-file@0.1.3": "patches/growing-file+0.1.3.patch", diff --git a/patches/react-contextmenu+2.14.0.patch b/patches/react-contextmenu+2.14.0.patch deleted file mode 100644 index f4893fce60..0000000000 --- a/patches/react-contextmenu+2.14.0.patch +++ /dev/null @@ -1,186 +0,0 @@ -diff --git a/modules/ContextMenu.js b/modules/ContextMenu.js -index 2f8821393f15a1ce85132385688f08cd6e390bf4..41e47ea740636f81bdd484a7093d0ea3169eb9d5 100644 ---- a/modules/ContextMenu.js -+++ b/modules/ContextMenu.js -@@ -81,6 +81,11 @@ var ContextMenu = function (_AbstractMenu) { - x = _e$detail$position.x, - y = _e$detail$position.y; - -+ if (x === undefined) { -+ var rect = e.detail.target.getBoundingClientRect(); -+ x = rect.x; -+ y = rect.y; -+ } - - _this.setState({ isVisible: true, x: x, y: y }); - _this.registerHandlers(); -@@ -226,6 +231,9 @@ var ContextMenu = function (_AbstractMenu) { - - var wrapper = window.requestAnimationFrame || setTimeout; - if (this.state.isVisible) { -+ if (!this.previousFocus) { -+ this.previousFocus = document.activeElement; -+ } - wrapper(function () { - var _state = _this2.state, - x = _state.x, -@@ -240,7 +248,10 @@ var ContextMenu = function (_AbstractMenu) { - _this2.menu.style.top = top + 'px'; - _this2.menu.style.left = left + 'px'; - _this2.menu.style.opacity = 1; -+ _this2.menu.style.visibility = 'visible'; - _this2.menu.style.pointerEvents = 'auto'; -+ -+ _this2.menu.focus(); - }); - }); - } else { -@@ -248,6 +259,16 @@ var ContextMenu = function (_AbstractMenu) { - if (!_this2.menu) return; - _this2.menu.style.opacity = 0; - _this2.menu.style.pointerEvents = 'none'; -+ -+ // Return to the previous focus state when dismissing the menu, unless the -+ // menu option focused another element. This is important for keyboard mode. -+ if (_this2.props.avoidFocusRestoreOnBlur) return; -+ -+ var isFocusWithinMenu = _this2.menu.contains(document.activeElement); -+ if (isFocusWithinMenu && _this2.previousFocus && _this2.previousFocus.focus) { -+ _this2.previousFocus.focus(); -+ _this2.previousFocus = null; -+ } - }); - } - } -diff --git a/modules/SubMenu.js b/modules/SubMenu.js -index ad1dc7043c13cbc30f1659d5b82696ed974c996a..c919be8d12329dd5bcf77d3660b85edb83714bb8 100644 ---- a/modules/SubMenu.js -+++ b/modules/SubMenu.js -@@ -129,6 +129,7 @@ var SubMenu = function (_AbstractMenu) { - - if (_this.props.disabled || _this.state.visible) return; - -+ if (_this.opentimer) clearTimeout(_this.opentimer); - _this.opentimer = setTimeout(function () { - return _this.setState({ - visible: true, -@@ -142,6 +143,7 @@ var SubMenu = function (_AbstractMenu) { - - if (!_this.state.visible) return; - -+ if (_this.closetimer) clearTimeout(_this.closetimer); - _this.closetimer = setTimeout(function () { - return _this.setState({ - visible: false, -@@ -170,6 +172,15 @@ var SubMenu = function (_AbstractMenu) { - } - }; - -+ _this.cleanup = function () { -+ _this.subMenu.removeEventListener('transitionend', _this.cleanup); -+ _this.subMenu.style.removeProperty('bottom'); -+ _this.subMenu.style.removeProperty('right'); -+ _this.subMenu.style.top = 0; -+ _this.subMenu.style.left = '100%'; -+ _this.unregisterHandlers(); -+ }; -+ - _this.state = (0, _objectAssign2.default)({}, _this.state, { - visible: false - }); -@@ -202,32 +213,28 @@ var SubMenu = function (_AbstractMenu) { - if (this.props.forceOpen || this.state.visible) { - var wrapper = window.requestAnimationFrame || setTimeout; - wrapper(function () { -- var styles = _this2.props.rtl ? _this2.getRTLMenuPosition() : _this2.getMenuPosition(); -- -- _this2.subMenu.style.removeProperty('top'); -- _this2.subMenu.style.removeProperty('bottom'); -- _this2.subMenu.style.removeProperty('left'); -- _this2.subMenu.style.removeProperty('right'); -- -- if ((0, _helpers.hasOwnProp)(styles, 'top')) _this2.subMenu.style.top = styles.top; -- if ((0, _helpers.hasOwnProp)(styles, 'left')) _this2.subMenu.style.left = styles.left; -- if ((0, _helpers.hasOwnProp)(styles, 'bottom')) _this2.subMenu.style.bottom = styles.bottom; -- if ((0, _helpers.hasOwnProp)(styles, 'right')) _this2.subMenu.style.right = styles.right; -- _this2.subMenu.classList.add(_helpers.cssClasses.menuVisible); -- -- _this2.registerHandlers(); -- _this2.setState({ selectedItem: null }); -+ if (_this2.props.forceOpen || _this2.state.visible) { -+ _this2.subMenu.removeEventListener('transitionend', _this2.cleanup); -+ var styles = _this2.props.rtl ? _this2.getRTLMenuPosition() : _this2.getMenuPosition(); -+ -+ _this2.subMenu.style.removeProperty('top'); -+ _this2.subMenu.style.removeProperty('bottom'); -+ _this2.subMenu.style.removeProperty('left'); -+ _this2.subMenu.style.removeProperty('right'); -+ -+ if ((0, _helpers.hasOwnProp)(styles, 'top')) _this2.subMenu.style.top = styles.top; -+ if ((0, _helpers.hasOwnProp)(styles, 'left')) _this2.subMenu.style.left = styles.left; -+ if ((0, _helpers.hasOwnProp)(styles, 'bottom')) _this2.subMenu.style.bottom = styles.bottom; -+ if ((0, _helpers.hasOwnProp)(styles, 'right')) _this2.subMenu.style.right = styles.right; -+ _this2.subMenu.classList.add(_helpers.cssClasses.menuVisible); -+ -+ _this2.registerHandlers(); -+ _this2.setState({ selectedItem: null }); -+ } - }); - } else { -- var cleanup = function cleanup() { -- _this2.subMenu.removeEventListener('transitionend', cleanup); -- _this2.subMenu.style.removeProperty('bottom'); -- _this2.subMenu.style.removeProperty('right'); -- _this2.subMenu.style.top = 0; -- _this2.subMenu.style.left = '100%'; -- _this2.unregisterHandlers(); -- }; -- this.subMenu.addEventListener('transitionend', cleanup); -+ this.subMenu.removeEventListener('transitionend', this.cleanup); -+ this.subMenu.addEventListener('transitionend', this.cleanup); - this.subMenu.classList.remove(_helpers.cssClasses.menuVisible); - } - } -diff --git a/src/index.d.ts b/src/index.d.ts -index 753ce9081490fd90b4354e6e73937dc85957689c..e8a8b0815a7ca61ef9bc488299cda08e181267bc 100644 ---- a/src/index.d.ts -+++ b/src/index.d.ts -@@ -14,6 +14,8 @@ declare module "react-contextmenu" { - preventHideOnResize?: boolean, - preventHideOnScroll?: boolean, - style?: React.CSSProperties, -+ avoidFocusRestoreOnBlur?: boolean; -+ children?: React.ReactNode; - } - - export interface ContextMenuTriggerProps { -@@ -25,6 +27,7 @@ declare module "react-contextmenu" { - renderTag?: React.ElementType, - mouseButton?: number, - disableIfShiftIsPressed?: boolean, -+ children?: React.ReactNode, - } - - export interface MenuItemProps { -@@ -35,6 +38,7 @@ declare module "react-contextmenu" { - divider?: boolean, - preventClose?: boolean, - onClick?: {(event: React.TouchEvent | React.MouseEvent, data: Object, target: HTMLElement): void} | Function, -+ children?: React.ReactNode, - } - - export interface SubMenuProps { -@@ -45,11 +49,13 @@ declare module "react-contextmenu" { - rtl?: boolean, - preventCloseOnClick?: boolean, - onClick?: {(event: React.TouchEvent | React.MouseEvent, data: Object, target: HTMLElement): void} | Function, -+ children?: React.ReactNode, - } - - export interface ConnectMenuProps { - id: string; - trigger: any; -+ children?: React.ReactNode; - } - - export const ContextMenu: React.ComponentClass; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7886956fb6..bc25947a36 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,8 +11,6 @@ overrides: jsdom: '-' thenify-all>thenify: 3.3.1 '@electron/rebuild@3.7.2>@electron/node-gyp': 10.2.0-electron.2 - react-contextmenu>react: 18.3.1 - react-contextmenu>react-dom: 18.3.1 patchedDependencies: '@types/express@4.17.21': @@ -63,9 +61,6 @@ patchedDependencies: qrcode-generator@1.4.4: hash: 1f10c592d849ed4cfc9f81301196d39857b79240997ef5772138218cb3717e80 path: patches/qrcode-generator+1.4.4.patch - react-contextmenu@2.14.0: - hash: 0a61a588d4e16ca308a33d4765e00ccade23abac650b981439a128bd5be785d7 - path: patches/react-contextmenu+2.14.0.patch websocket@1.0.34: hash: b8d361a6a73e44000bb51102dea0d841c22d2bb455dd6c54de566d0e0bd86355 path: patches/websocket+1.0.34.patch @@ -335,9 +330,6 @@ importers: react-blurhash: specifier: 0.3.0 version: 0.3.0(blurhash@2.0.5)(react@18.3.1) - react-contextmenu: - specifier: 2.14.0 - version: 2.14.0(patch_hash=0a61a588d4e16ca308a33d4765e00ccade23abac650b981439a128bd5be785d7)(prop-types@15.8.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-dom: specifier: 18.3.1 version: 18.3.1(react@18.3.1) @@ -9201,13 +9193,6 @@ packages: blurhash: ^2.0.3 react: '>=15' - react-contextmenu@2.14.0: - resolution: {integrity: sha512-ktqMOuad6sCFNJs/ltEwppN8F0YeXmqoZfwycgtZR/MxOXMYx1xgYC44SzWH259HdGyshk1/7sXGuIRwj9hzbw==} - peerDependencies: - prop-types: ^15.0.0 - react: 18.3.1 - react-dom: 18.3.1 - react-devtools-core@6.0.1: resolution: {integrity: sha512-II3iSJhnR5nAscYDa9FCgPLq8mO5aEx/EKKtdXYTDnvdFEa3K7gs3jn1SKRXwQf9maOmIilmjnnx7Qy+3annPA==} @@ -21233,14 +21218,6 @@ snapshots: blurhash: 2.0.5 react: 18.3.1 - react-contextmenu@2.14.0(patch_hash=0a61a588d4e16ca308a33d4765e00ccade23abac650b981439a128bd5be785d7)(prop-types@15.8.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): - dependencies: - classnames: 2.5.1 - object-assign: 4.1.1 - prop-types: 15.8.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-devtools-core@6.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10): dependencies: shell-quote: 1.8.2 diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 7b123d6cc4..f8a40ce57d 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -160,7 +160,8 @@ opacity: 0; } -.module-message:hover .module-message__buttons { +.module-message:hover .module-message__buttons, +.module-message__buttons:has([data-state='open']) { opacity: 1; } @@ -6537,169 +6538,6 @@ button.module-calling-participants-list__contact { margin-bottom: 3px; } -/* Third-party module: react-contextmenu*/ - -.react-contextmenu { - @include mixins.popper-shadow(); - - & { - outline: none; - border-radius: 4px; - min-width: 220px; - padding-block: 6px; - padding-inline: 0; - opacity: 0; - user-select: none; - visibility: hidden; - } - - // style a menu with only one option - &:not(:has(:nth-child(2))) { - padding-block: 0; - .react-contextmenu-item { - padding-block: 9px; - border-radius: 4px; - } - } - & { - @include mixins.light-theme { - background-color: variables.$color-white; - } - @include mixins.dark-theme { - background-color: variables.$color-gray-75; - } - } -} - -.react-contextmenu--visible { - opacity: 1; - visibility: visible; - z-index: variables.$z-index-context-menu; - -webkit-app-region: no-drag; -} - -.react-contextmenu-item { - outline: none; - cursor: pointer; - white-space: nowrap; - display: flex; - width: 100%; - align-items: center; - - @include mixins.font-body-2; - - padding-block: 7px; - - padding-inline: 12px; - - @include mixins.light-theme { - color: variables.$color-gray-90; - } - @include mixins.dark-theme { - color: variables.$color-gray-02; - } - - &--divider { - height: 1px; - margin-block: 6px; - margin-inline: 0; - padding: 0; - - @include mixins.light-theme { - background-color: variables.$color-gray-15; - } - @include mixins.dark-theme { - background-color: variables.$color-gray-60; - } - } -} - -.react-contextmenu-item--checked:before { - content: '✓'; - display: inline-block; - position: absolute; - inset-inline-end: 7px; - - @include mixins.light-theme { - color: variables.$color-gray-90; - } - @include mixins.dark-theme { - color: variables.$color-gray-02; - } -} - -.react-contextmenu-item.react-contextmenu-submenu { - padding: 0; -} - -.react-contextmenu-item.react-contextmenu-submenu > .react-contextmenu-item { - padding-inline-end: 36px; -} - -.react-contextmenu-item.react-contextmenu-submenu - > .react-contextmenu-item:after { - content: ' '; - display: inline-block; - height: 18px; - position: absolute; - inset-inline-end: 7px; - width: 12px; - - @include mixins.light-theme { - @include mixins.color-svg( - '../images/icons/v3/chevron/chevron-right.svg', - variables.$color-gray-75 - ); - & { - color: variables.$color-gray-90; - } - } - @include mixins.dark-theme { - @include mixins.color-svg( - '../images/icons/v3/chevron/chevron-right.svg', - variables.$color-gray-15 - ); - & { - color: variables.$color-gray-02; - } - } -} - -.react-contextmenu-item.react-contextmenu-item--active, -.react-contextmenu-item.react-contextmenu-item--selected { - color: variables.$color-black; - @include mixins.light-theme { - background-color: variables.$color-gray-15; - } - @include mixins.dark-theme { - background-color: variables.$color-gray-60; - color: variables.$color-white; - } -} - -.react-contextmenu-item--disabled.react-contextmenu-item--selected { - background-color: inherit; - cursor: inherit; -} - -.react-contextmenu-item.react-contextmenu-item--active.react-contextmenu-item--checked:before, -.react-contextmenu-item.react-contextmenu-item--selected.react-contextmenu-item--checked:before { - color: variables.$color-black; - @include mixins.dark-theme { - color: variables.$color-white; - } -} - -.react-contextmenu-item.react-contextmenu-submenu - > .react-contextmenu-item.react-contextmenu-item--active:after, -.react-contextmenu-item.react-contextmenu-submenu - > .react-contextmenu-item.react-contextmenu-item--selected:after { - color: variables.$color-black; - @include mixins.dark-theme { - color: variables.$color-white; - } -} - // To limit messages with things forcing them wider, like long attachment names .module-message__container { max-width: 100%; @@ -6731,158 +6569,6 @@ button.module-calling-participants-list__contact { vertical-align: middle; } - &__download::before { - @include mixins.light-theme { - @include mixins.color-svg( - '../images/icons/v3/save/save-compact.svg', - variables.$color-black - ); - } - - @include mixins.dark-theme { - @include mixins.color-svg( - '../images/icons/v3/save/save-compact.svg', - variables.$color-gray-15 - ); - } - } - - &__reply::before { - @include mixins.light-theme { - @include mixins.color-svg( - '../images/icons/v3/reply/reply-compact.svg', - variables.$color-black - ); - } - - @include mixins.dark-theme { - @include mixins.color-svg( - '../images/icons/v3/reply/reply-compact.svg', - variables.$color-gray-15 - ); - } - } - - &__react::before { - @include mixins.light-theme { - @include mixins.color-svg( - '../images/icons/v3/heart/heart-plus-compact.svg', - variables.$color-black - ); - } - - @include mixins.dark-theme { - @include mixins.color-svg( - '../images/icons/v3/heart/heart-plus-compact.svg', - variables.$color-gray-15 - ); - } - } - - &__end-poll::before { - @include mixins.color-svg( - '../images/icons/v3/stop/stop-circle.svg', - light-dark(variables.$color-black, variables.$color-gray-15) - ); - } - - &__more-info::before { - @include mixins.light-theme { - @include mixins.color-svg( - '../images/icons/v3/info/info-compact.svg', - variables.$color-black - ); - } - - @include mixins.dark-theme { - @include mixins.color-svg( - '../images/icons/v3/info/info-compact.svg', - variables.$color-gray-15 - ); - } - } - - &__select::before { - @include mixins.light-theme { - @include mixins.color-svg( - '../images/icons/v3/check/check-circle-compact.svg', - variables.$color-black - ); - } - - @include mixins.dark-theme { - @include mixins.color-svg( - '../images/icons/v3/check/check-circle-compact.svg', - variables.$color-gray-15 - ); - } - } - - &__retry-send::before { - @include mixins.light-theme { - @include mixins.color-svg( - '../images/icons/v3/send/send.svg', - variables.$color-black - ); - } - - @include mixins.dark-theme { - @include mixins.color-svg( - '../images/icons/v3/send/send.svg', - variables.$color-gray-15 - ); - } - } - - &__forward-message::before { - @include mixins.light-theme { - @include mixins.color-svg( - '../images/icons/v3/forward/forward-compact.svg', - variables.$color-black - ); - } - - @include mixins.dark-theme { - @include mixins.color-svg( - '../images/icons/v3/forward/forward-compact.svg', - variables.$color-gray-15 - ); - } - } - - &__edit-message::before { - @include mixins.light-theme { - @include mixins.color-svg( - '../images/icons/v3/edit/edit-compact.svg', - variables.$color-black - ); - } - - @include mixins.dark-theme { - @include mixins.color-svg( - '../images/icons/v3/edit/edit-compact.svg', - variables.$color-gray-15 - ); - } - } - - &__delete-message::before, - &__delete-message-for-everyone::before { - @include mixins.light-theme { - @include mixins.color-svg( - '../images/icons/v3/trash/trash-compact.svg', - variables.$color-black - ); - } - - @include mixins.dark-theme { - @include mixins.color-svg( - '../images/icons/v3/trash/trash-compact.svg', - variables.$color-gray-15 - ); - } - } - &__copy-timestamp::before { @include mixins.light-theme { @include mixins.color-svg( diff --git a/stylesheets/components/ConversationHeader.scss b/stylesheets/components/ConversationHeader.scss index c675348300..51977fbbd9 100644 --- a/stylesheets/components/ConversationHeader.scss +++ b/stylesheets/components/ConversationHeader.scss @@ -329,13 +329,4 @@ } } } - - &__disappearing-timer__item { - padding-inline-start: 25px; - - &--active { - padding-inline-start: 0px; - @include icon-element('../images/icons/v3/check/check-compact.svg', 12px); - } - } } diff --git a/ts/axo/AxoContextMenu.dom.tsx b/ts/axo/AxoContextMenu.dom.tsx index f1e9305667..51a161d8a5 100644 --- a/ts/axo/AxoContextMenu.dom.tsx +++ b/ts/axo/AxoContextMenu.dom.tsx @@ -1,11 +1,17 @@ // Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import React, { memo } from 'react'; +import React, { memo, useCallback, useEffect, useRef, useState } from 'react'; import { ContextMenu } from 'radix-ui'; -import type { FC } from 'react'; +import type { + FC, + KeyboardEvent, + KeyboardEventHandler, + MouseEvent as ReactMouseEvent, +} from 'react'; import { AxoSymbol } from './AxoSymbol.dom.js'; import { AxoBaseMenu } from './_internal/AxoBaseMenu.dom.js'; import { tw } from './tw.dom.js'; +import { assert } from './_internal/assert.dom.js'; const Namespace = 'AxoContextMenu'; @@ -57,7 +63,11 @@ export namespace AxoContextMenu { export type RootProps = AxoBaseMenu.MenuRootProps; export const Root: FC = memo(props => { - return {props.children}; + return ( + + {props.children} + + ); }); Root.displayName = `${Namespace}.Root`; @@ -67,14 +77,111 @@ export namespace AxoContextMenu { * ----------------------------------- */ + type TriggerElementGetter = (event: KeyboardEvent) => Element; + + // eslint-disable-next-line no-inner-declarations + function useContextMenuTriggerKeyboardEventHandler( + getTriggerElement: TriggerElementGetter + ) { + const getTriggerElementRef = + useRef(getTriggerElement); + + useEffect(() => { + getTriggerElementRef.current = getTriggerElement; + }, [getTriggerElement]); + + return useCallback( + (event: KeyboardEvent) => { + const isMacOS = window.platform === 'darwin'; + + if ( + (isMacOS ? event.metaKey : !event.metaKey) && + (isMacOS ? !event.ctrlKey : event.ctrlKey) && + (isMacOS ? !event.shiftKey : event.shiftKey) && + !event.altKey && + (isMacOS ? event.key === 'F12' : event.key === 'F10') + ) { + event.preventDefault(); + event.stopPropagation(); + + const trigger = getTriggerElement(event); + + const clientRect = trigger.getBoundingClientRect(); + + trigger.dispatchEvent( + new MouseEvent('contextmenu', { + bubbles: true, + cancelable: true, + clientX: clientRect.left, + clientY: clientRect.bottom, + }) + ); + } + }, + [getTriggerElement] + ); + } + export type TriggerProps = AxoBaseMenu.MenuTriggerProps; export const Trigger: FC = memo(props => { - return {props.children}; + const [disableCurrentEvent, setDisableCurrentEvent] = useState(false); + + const handleContextMenuCapture = useCallback( + (event: ReactMouseEvent) => { + const { target, currentTarget } = event; + if ( + target instanceof HTMLElement && + target.closest('a[href], [role=link]') != null + ) { + setDisableCurrentEvent(true); + } + + const selection = window.getSelection(); + if ( + selection != null && + !selection.isCollapsed && + selection.containsNode(currentTarget, true) + ) { + setDisableCurrentEvent(true); + } + }, + [] + ); + + const handleContextMenu = useCallback(() => { + setDisableCurrentEvent(false); + }, []); + + const handleKeyDown = useContextMenuTriggerKeyboardEventHandler(event => { + return event.currentTarget; + }); + + return ( + + {props.children} + + ); }); Trigger.displayName = `${Namespace}.Trigger`; + export function useAxoContextMenuOutsideKeyboardTrigger(): KeyboardEventHandler { + return useContextMenuTriggerKeyboardEventHandler(event => { + return assert( + event.currentTarget.querySelector('[data-axo-context-menu-trigger]'), + `Couldn't find <${Namespace}.Trigger> element, did you forget to pass all html props through?` + ); + }); + } + /** * Component: * ----------------------------------- diff --git a/ts/axo/AxoDropdownMenu.dom.tsx b/ts/axo/AxoDropdownMenu.dom.tsx index 7da6ce758d..78efc3227c 100644 --- a/ts/axo/AxoDropdownMenu.dom.tsx +++ b/ts/axo/AxoDropdownMenu.dom.tsx @@ -3,7 +3,7 @@ import React, { memo, useEffect, useId, useRef } from 'react'; import { DropdownMenu } from 'radix-ui'; import type { FC, ReactNode } from 'react'; -import { getRole, computeAccessibleName } from 'dom-accessibility-api'; +import { computeAccessibleName } from 'dom-accessibility-api'; import { AxoSymbol } from './AxoSymbol.dom.js'; import { AxoBaseMenu } from './_internal/AxoBaseMenu.dom.js'; import { tw } from './tw.dom.js'; @@ -13,6 +13,10 @@ import { useCreateAriaLabellingContext, } from './_internal/AriaLabellingContext.dom.js'; import { assert } from './_internal/assert.dom.js'; +import { + getElementAriaRole, + isAriaWidgetRole, +} from './_internal/ariaRoles.dom.js'; const Namespace = 'AxoDropdownMenu'; @@ -107,8 +111,8 @@ export namespace AxoDropdownMenu { `${triggerDisplayName} child must forward ref` ); assert( - getRole(ref.current) === 'button', - `${triggerDisplayName} child must be a
{conversationId ? ( @@ -291,49 +285,32 @@ function CustomColorBubble({ onEdit, onChoose, }: CustomColorBubblePropsType): JSX.Element { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const menuRef = useRef(null); const [confirmDeleteCount, setConfirmDeleteCount] = useState< number | undefined >(undefined); - const handleClick = (ev: KeyboardEvent | MouseEvent) => { - if (!isSelected) { - onChoose(); - return; - } - - if (menuRef && menuRef.current) { - menuRef.current.handleContextClick(ev); - } - }; - - const bubble = ( -
{ - if (ev.key === 'Enter') { - handleClick(ev); - } - }} - role="option" - tabIndex={0} - style={{ - ...getCustomColorStyle(color), - }} - /> + const handleClick = useCallback( + (event: MouseEvent) => { + if (!isSelected) { + onChoose(); + event.currentTarget.focus(); + } + }, + [isSelected, onChoose] ); + const handleDelete = useCallback(() => { + const conversations = getConversationsWithCustomColor(colorId); + if (!conversations.length) { + onDelete(); + } else { + setConfirmDeleteCount(conversations.length); + } + }, [getConversationsWithCustomColor, colorId, onDelete]); + return ( <> - {confirmDeleteCount ? ( + {confirmDeleteCount != null && ( - ) : null} - {isSelected ? ( - - {bubble} - - ) : ( - bubble )} - - { - event.stopPropagation(); - event.preventDefault(); - - onEdit(); - }} - > - {i18n('icu:ChatColorPicker__context--edit')} - - { - event.stopPropagation(); - event.preventDefault(); - - onDupe(); - }} - > - {i18n('icu:ChatColorPicker__context--duplicate')} - - { - event.stopPropagation(); - event.preventDefault(); - - const conversations = getConversationsWithCustomColor(colorId); - if (!conversations.length) { - onDelete(); - } else { - setConfirmDeleteCount(conversations.length); - } - }} - > - {i18n('icu:ChatColorPicker__context--delete')} - - + +
); } diff --git a/ts/hooks/useKeyboardShortcuts.dom.tsx b/ts/hooks/useKeyboardShortcuts.dom.tsx index 55f177e59e..b426930629 100644 --- a/ts/hooks/useKeyboardShortcuts.dom.tsx +++ b/ts/hooks/useKeyboardShortcuts.dom.tsx @@ -8,7 +8,6 @@ import * as KeyboardLayout from '../services/keyboardLayout.dom.js'; import { getHasPanelOpen } from '../state/selectors/conversations.dom.js'; import { isInFullScreenCall } from '../state/selectors/calling.std.js'; import { isShowingAnyModal } from '../state/selectors/globalModals.std.js'; -import type { ContextMenuTriggerType } from '../components/conversation/MessageContextMenu.dom.js'; const { get } = lodash; @@ -295,39 +294,6 @@ export function useToggleReactionPicker( ); } -export function useOpenContextMenu( - openContextMenu: ContextMenuTriggerType['handleContextClick'] | undefined -): KeyboardShortcutHandlerType { - const hasOverlay = useHasAnyOverlay(); - - return useCallback( - ev => { - if (hasOverlay) { - return false; - } - - const { shiftKey } = ev; - const key = KeyboardLayout.lookup(ev); - - const isMacOS = get(window, 'platform') === 'darwin'; - - if ( - (!isMacOS && shiftKey && key === 'F10') || - (isMacOS && isCmdOrCtrl(ev) && key === 'F12') - ) { - ev.preventDefault(); - ev.stopPropagation(); - - openContextMenu?.(new MouseEvent('click')); - return true; - } - - return false; - }, - [hasOverlay, openContextMenu] - ); -} - export function useEditLastMessageSent( maybeEditMessage: () => boolean ): KeyboardShortcutHandlerType { diff --git a/ts/test-mock/backups/backups_test.node.ts b/ts/test-mock/backups/backups_test.node.ts index f1881fec87..2e9fb9cd77 100644 --- a/ts/test-mock/backups/backups_test.node.ts +++ b/ts/test-mock/backups/backups_test.node.ts @@ -173,12 +173,10 @@ describe('backups', function (this: Mocha.Suite) { debug('setting bubble color'); const conversationStack = window.locator('.Inbox__conversation-stack'); await conversationStack - .locator('button.module-ConversationHeader__button--more') + .getByRole('button', { name: 'More Info' }) .click(); - await window - .locator('.react-contextmenu-item >> "Chat settings"') - .click(); + await window.getByRole('menuitem', { name: 'Chat settings' }).click(); await conversationStack .locator('.ConversationDetails__chat-color') diff --git a/ts/test-mock/messaging/edit_test.node.ts b/ts/test-mock/messaging/edit_test.node.ts index 335ad9feae..0e5c1b22c8 100644 --- a/ts/test-mock/messaging/edit_test.node.ts +++ b/ts/test-mock/messaging/edit_test.node.ts @@ -110,8 +110,9 @@ describe('editing', function (this: Mocha.Suite) { ) { await page .getByTestId(`${timestamp}`) - .locator('.module-message__buttons__menu') + .getByRole('button', { name: 'More actions' }) .click(); + await page.getByRole('menuitem', { name: 'Edit' }).click(); const input = await waitForEnabledComposer(page); await typeIntoInput(input, additionalText, previousText); diff --git a/ts/test-mock/messaging/expire_timer_version_test.node.ts b/ts/test-mock/messaging/expire_timer_version_test.node.ts index a0d83078ea..5d16cd4520 100644 --- a/ts/test-mock/messaging/expire_timer_version_test.node.ts +++ b/ts/test-mock/messaging/expire_timer_version_test.node.ts @@ -279,19 +279,13 @@ describe('messaging/expireTimerVersion', function (this: Mocha.Suite) { const conversationStack = window.locator('.Inbox__conversation-stack'); debug('setting timer to 1 week'); - await conversationStack - .locator('button.module-ConversationHeader__button--more') - .click(); + await conversationStack.getByRole('button', { name: 'More Info' }).click(); await window - .locator('.react-contextmenu-item >> "Disappearing messages"') + .getByRole('menuitem', { name: 'Disappearing messages' }) .click(); - await window - .locator( - '.module-ConversationHeader__disappearing-timer__item >> "1 week"' - ) - .click(); + await window.getByRole('menuitemradio', { name: '1 week' }).click(); debug('Getting first expiration update'); { @@ -305,19 +299,13 @@ describe('messaging/expireTimerVersion', function (this: Mocha.Suite) { } debug('setting timer to 4 weeks'); - await conversationStack - .locator('button.module-ConversationHeader__button--more') - .click(); + await conversationStack.getByRole('button', { name: 'More Info' }).click(); await window - .locator('.react-contextmenu-item >> "Disappearing messages"') + .getByRole('menuitem', { name: 'Disappearing messages' }) .click(); - await window - .locator( - '.module-ConversationHeader__disappearing-timer__item >> "4 weeks"' - ) - .click(); + await window.getByRole('menuitemradio', { name: '4 weeks' }).click(); debug('Getting second expiration update'); { diff --git a/ts/test-mock/pnp/accept_gv2_invite_test.node.ts b/ts/test-mock/pnp/accept_gv2_invite_test.node.ts index 1c7e8eefb2..769c97091c 100644 --- a/ts/test-mock/pnp/accept_gv2_invite_test.node.ts +++ b/ts/test-mock/pnp/accept_gv2_invite_test.node.ts @@ -143,11 +143,8 @@ describe('pnp/accept gv2 invite', function (this: Mocha.Suite) { .locator('.module-message-request-actions button >> "Accept"') .waitFor({ state: 'hidden' }); - await window - .locator('button.module-ConversationHeader__button--more') - .click(); - - await window.locator('.react-contextmenu-item >> "Group settings"').click(); + await window.getByRole('button', { name: 'More Info' }).click(); + await window.getByRole('menuitem', { name: 'Group settings' }).click(); debug( 'Checking that we see all members of group, including (previously) unknown contact' diff --git a/ts/test-mock/pnp/send_gv2_invite_test.node.ts b/ts/test-mock/pnp/send_gv2_invite_test.node.ts index 24c8955cee..f31f8e2f5f 100644 --- a/ts/test-mock/pnp/send_gv2_invite_test.node.ts +++ b/ts/test-mock/pnp/send_gv2_invite_test.node.ts @@ -159,11 +159,9 @@ describe('pnp/send gv2 invite', function (this: Mocha.Suite) { debug('opening group settings'); - await conversationStack - .locator('button.module-ConversationHeader__button--more') - .click(); + await conversationStack.getByRole('button', { name: 'More Info' }).click(); - await window.locator('.react-contextmenu-item >> "Group settings"').click(); + await window.getByRole('menuitem', { name: 'Group settings' }).click(); debug('editing group title'); { diff --git a/ts/test-mock/storage/archive_test.node.ts b/ts/test-mock/storage/archive_test.node.ts index 7422c99d25..2e55fd306d 100644 --- a/ts/test-mock/storage/archive_test.node.ts +++ b/ts/test-mock/storage/archive_test.node.ts @@ -92,14 +92,14 @@ describe('storage service', function (this: Mocha.Suite) { .locator(`[data-testid="${firstContact.device.aci}"]`) .click(); - const moreButton = conversationStack.locator( - 'button.module-ConversationHeader__button--more' - ); + const moreButton = conversationStack.getByRole('button', { + name: 'More Info', + }); await moreButton.click(); - const archiveButton = window.locator( - '.react-contextmenu-item >> "Archive"' - ); + const archiveButton = window.getByRole('menuitem', { + name: 'Archive', + }); await archiveButton.click(); const newState = await phone.waitForStorageState({ diff --git a/ts/test-mock/storage/conflict_test.node.ts b/ts/test-mock/storage/conflict_test.node.ts index 4f58c6438d..8ccfe9a476 100644 --- a/ts/test-mock/storage/conflict_test.node.ts +++ b/ts/test-mock/storage/conflict_test.node.ts @@ -61,10 +61,10 @@ describe('storage service', function (this: Mocha.Suite) { await leftPane.locator(`[data-testid="${testid}"]`).click(); await conversationStack - .locator('button.module-ConversationHeader__button--more') + .getByRole('button', { name: 'More Info' }) .click(); - await window.locator('.react-contextmenu-item >> "Archive"').click(); + await window.getByRole('menuitem', { name: 'Archive' }).click(); const newState = await phone.waitForStorageState({ after: state, @@ -102,10 +102,10 @@ describe('storage service', function (this: Mocha.Suite) { await leftPane.locator(`[data-testid="${testid}"]`).click(); await conversationStack - .locator('button.module-ConversationHeader__button--more') + .getByRole('button', { name: 'More Info' }) .click(); - await window.locator('.react-contextmenu-item >> "Unarchive"').click(); + await window.getByRole('menuitem', { name: 'Unarchive' }).click(); await app.waitForManifestVersion(archivedVersion); @@ -114,10 +114,10 @@ describe('storage service', function (this: Mocha.Suite) { // Conversation should be still open await conversationStack - .locator('button.module-ConversationHeader__button--more') + .getByRole('button', { name: 'More Info' }) .click(); - await window.locator('.react-contextmenu-item >> "Unarchive"').waitFor(); + await window.getByRole('menuitem', { name: 'Unarchive' }).waitFor(); debug('Verifying the final manifest version'); const finalState = await phone.expectStorageState('final state'); @@ -148,10 +148,10 @@ describe('storage service', function (this: Mocha.Suite) { await leftPane.locator(`[data-testid="${second.device.aci}"]`).click(); await conversationStack - .locator('button.module-ConversationHeader__button--more') + .getByRole('button', { name: 'More Info' }) .click(); - await window.locator('.react-contextmenu-item >> "Pin chat"').click(); + await window.getByRole('menuitem', { name: 'Pin chat' }).click(); const newState = await phone.waitForStorageState({ after: state, @@ -172,20 +172,16 @@ describe('storage service', function (this: Mocha.Suite) { } debug('unpinning second contact'); - await conversationStack - .locator('button.module-ConversationHeader__button--more') - .click(); + await conversationStack.getByRole('button', { name: 'More Info' }).click(); - await window.locator('.react-contextmenu-item >> "Unpin chat"').click(); + await window.getByRole('menuitem', { name: 'Unpin chat' }).click(); await app.waitForManifestVersion(archivedVersion); debug('verifying that second contact is still unpinned'); - await conversationStack - .locator('button.module-ConversationHeader__button--more') - .click(); + await conversationStack.getByRole('button', { name: 'More Info' }).click(); - await window.locator('.react-contextmenu-item >> "Unpin chat"').waitFor(); + await window.getByRole('menuitem', { name: 'Unpin chat' }).waitFor(); debug('Verifying the final manifest version'); const finalState = await phone.expectStorageState('final state'); diff --git a/ts/test-mock/storage/pin_unpin_test.node.ts b/ts/test-mock/storage/pin_unpin_test.node.ts index dfedb6546e..53bb97b24b 100644 --- a/ts/test-mock/storage/pin_unpin_test.node.ts +++ b/ts/test-mock/storage/pin_unpin_test.node.ts @@ -65,14 +65,14 @@ describe('storage service', function (this: Mocha.Suite) { const convo = leftPane.getByTestId(group.id); await convo.click(); - const moreButton = conversationStack.locator( - 'button.module-ConversationHeader__button--more' - ); + const moreButton = conversationStack.getByRole('button', { + name: 'More Info', + }); await moreButton.click(); - const pinButton = window.locator( - '.react-contextmenu-item >> "Pin chat"' - ); + const pinButton = window.getByRole('menuitem', { + name: 'Pin chat', + }); await pinButton.click(); const newState = await phone.waitForStorageState({ @@ -113,14 +113,14 @@ describe('storage service', function (this: Mocha.Suite) { ); await convo.click(); - const moreButton = conversationStack.locator( - 'button.module-ConversationHeader__button--more' - ); + const moreButton = conversationStack.getByRole('button', { + name: 'More Info', + }); await moreButton.click(); - const pinButton = window.locator( - '.react-contextmenu-item >> "Pin chat"' - ); + const pinButton = window.getByRole('menuitem', { + name: 'Pin chat', + }); await pinButton.click(); if (isLast) { diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index 930f0ad39c..84ab0bfb15 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -825,6 +825,13 @@ "reasonCategory": "usageTrusted", "updated": "2025-08-28T23:36:44.974Z" }, + { + "rule": "React-useRef", + "path": "ts/axo/AxoContextMenu.dom.tsx", + "line": " useRef(getTriggerElement);", + "reasonCategory": "usageTrusted", + "updated": "2025-11-11T22:17:13.219Z" + }, { "rule": "React-useRef", "path": "ts/axo/AxoDropdownMenu.dom.tsx", @@ -1148,13 +1155,6 @@ "reasonCategory": "usageTrusted", "updated": "2021-07-30T16:57:33.618Z" }, - { - "rule": "React-useRef", - "path": "ts/components/ChatColorPicker.dom.tsx", - "line": " const menuRef = useRef(null);", - "reasonCategory": "usageTrusted", - "updated": "2021-07-30T16:57:33.618Z" - }, { "rule": "React-useRef", "path": "ts/components/CompositionArea.dom.tsx", @@ -1169,6 +1169,14 @@ "reasonCategory": "usageTrusted", "updated": "2021-09-23T00:07:11.885Z" }, + { + "rule": "React-useRef", + "path": "ts/components/CompositionArea.dom.tsx", + "line": " const photoVideoInputRef = useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2025-10-27T16:28:11.852Z", + "reasonDetail": "Ref for photo/video file input element" + }, { "rule": "React-useRef", "path": "ts/components/CompositionInput.dom.tsx", @@ -1492,6 +1500,22 @@ "updated": "2022-09-14T16:20:15.384Z", "reasonDetail": "Holds a reference to a container element to prevent outside clicks" }, + { + "rule": "React-useRef", + "path": "ts/components/PollCreateModal.dom.tsx", + "line": " const optionRefsMap = useRef>(", + "reasonCategory": "usageTrusted", + "updated": "2025-11-02T17:27:24.705Z", + "reasonDetail": "Map of refs for poll option inputs to manage focus" + }, + { + "rule": "React-useRef", + "path": "ts/components/PollCreateModal.dom.tsx", + "line": " const questionInputRef = useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2025-11-02T17:27:24.705Z", + "reasonDetail": "Ref for question input to manage focus on validation errors" + }, { "rule": "React-useRef", "path": "ts/components/Preferences.dom.tsx", @@ -1735,20 +1759,6 @@ "updated": "2025-05-28T00:57:39.376Z", "reasonDetail": "Holding on to a close function" }, - { - "rule": "React-useRef", - "path": "ts/components/conversation/CallingNotification.dom.tsx", - "line": " const menuTriggerRef = React.useRef(null);", - "reasonCategory": "usageTrusted", - "updated": "2023-12-08T20:28:57.595Z" - }, - { - "rule": "React-useRef", - "path": "ts/components/conversation/ConversationHeader.dom.tsx", - "line": " const menuTriggerRef = useRef(null);", - "reasonCategory": "usageTrusted", - "updated": "2024-03-15T18:29:48.327Z" - }, { "rule": "React-useRef", "path": "ts/components/conversation/ConversationHeader.dom.tsx", @@ -1931,6 +1941,14 @@ "updated": "2024-09-03T00:45:23.978Z", "reasonDetail": "Because we need the current tab value outside the callback" }, + { + "rule": "React-useRef", + "path": "ts/components/conversation/poll-message/PollMessageContents.dom.tsx", + "line": " const pendingCheckTimer = useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2025-11-06T20:28:00.760Z", + "reasonDetail": "Ref for timer" + }, { "rule": "React-useRef", "path": "ts/components/fun/FunGif.dom.tsx", @@ -2334,37 +2352,5 @@ "line": " message.innerHTML = window.SignalContext.i18n('icu:optimizingApplication');", "reasonCategory": "usageTrusted", "updated": "2021-09-17T21:02:59.414Z" - }, - { - "rule": "React-useRef", - "path": "ts/components/CompositionArea.dom.tsx", - "line": " const photoVideoInputRef = useRef(null);", - "reasonCategory": "usageTrusted", - "updated": "2025-10-27T16:28:11.852Z", - "reasonDetail": "Ref for photo/video file input element" - }, - { - "rule": "React-useRef", - "path": "ts/components/PollCreateModal.dom.tsx", - "line": " const questionInputRef = useRef(null);", - "reasonCategory": "usageTrusted", - "updated": "2025-11-02T17:27:24.705Z", - "reasonDetail": "Ref for question input to manage focus on validation errors" - }, - { - "rule": "React-useRef", - "path": "ts/components/PollCreateModal.dom.tsx", - "line": " const optionRefsMap = useRef>(", - "reasonCategory": "usageTrusted", - "updated": "2025-11-02T17:27:24.705Z", - "reasonDetail": "Map of refs for poll option inputs to manage focus" - }, - { - "rule": "React-useRef", - "path": "ts/components/conversation/poll-message/PollMessageContents.dom.tsx", - "line": " const pendingCheckTimer = useRef(null);", - "reasonCategory": "usageTrusted", - "updated": "2025-11-06T20:28:00.760Z", - "reasonDetail": "Ref for timer" } ] diff --git a/ts/util/lint/linter.node.ts b/ts/util/lint/linter.node.ts index 3a4c1ba301..da16af10d4 100644 --- a/ts/util/lint/linter.node.ts +++ b/ts/util/lint/linter.node.ts @@ -94,7 +94,6 @@ const excludedFilesRegexp = RegExp( '^node_modules/lodash/.+', '^node_modules/react/.+', '^node_modules/react-aria-components/.+', - '^node_modules/react-contextmenu/.+', '^node_modules/react-dom/.+', '^node_modules/react-icon-base/.+', '^node_modules/react-input-autosize/.+',