Fix FunLightbox and FunTooltip

This commit is contained in:
Jamie Kyle 2025-06-03 06:29:51 -07:00 committed by GitHub
commit 06ff9fa09e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 891 additions and 211 deletions

View file

@ -197,6 +197,30 @@ Signal Desktop makes use of the following open source projects.
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 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. CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
## @radix-ui/react-tooltip
MIT License
Copyright (c) 2022 WorkOS
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-aria/focus ## @react-aria/focus
Apache License Apache License

View file

@ -114,6 +114,7 @@
"@indutny/simple-windows-notifications": "2.0.16", "@indutny/simple-windows-notifications": "2.0.16",
"@indutny/sneequals": "4.0.0", "@indutny/sneequals": "4.0.0",
"@popperjs/core": "2.11.8", "@popperjs/core": "2.11.8",
"@radix-ui/react-tooltip": "1.2.7",
"@react-aria/focus": "3.19.1", "@react-aria/focus": "3.19.1",
"@react-aria/interactions": "3.23.0", "@react-aria/interactions": "3.23.0",
"@react-aria/utils": "3.25.3", "@react-aria/utils": "3.25.3",

420
pnpm-lock.yaml generated
View file

@ -110,6 +110,9 @@ importers:
'@popperjs/core': '@popperjs/core':
specifier: 2.11.8 specifier: 2.11.8
version: 2.11.8 version: 2.11.8
'@radix-ui/react-tooltip':
specifier: 1.2.7
version: 1.2.7(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@react-aria/focus': '@react-aria/focus':
specifier: 3.19.1 specifier: 3.19.1
version: 3.19.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 3.19.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -1333,6 +1336,21 @@ packages:
resolution: {integrity: sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==} resolution: {integrity: sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
'@floating-ui/core@1.7.0':
resolution: {integrity: sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==}
'@floating-ui/dom@1.7.0':
resolution: {integrity: sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg==}
'@floating-ui/react-dom@2.1.2':
resolution: {integrity: sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
'@floating-ui/utils@0.2.9':
resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==}
'@formatjs/ecma402-abstract@1.11.4': '@formatjs/ecma402-abstract@1.11.4':
resolution: {integrity: sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==} resolution: {integrity: sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==}
@ -1918,6 +1936,215 @@ packages:
'@protobufjs/utf8@1.1.0': '@protobufjs/utf8@1.1.0':
resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==}
'@radix-ui/primitive@1.1.2':
resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==}
'@radix-ui/react-arrow@1.1.7':
resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-compose-refs@1.1.2':
resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-context@1.1.2':
resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-dismissable-layer@1.1.10':
resolution: {integrity: sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-id@1.1.1':
resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-popper@1.2.7':
resolution: {integrity: sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-portal@1.1.9':
resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-presence@1.1.4':
resolution: {integrity: sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-primitive@2.1.3':
resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-slot@1.2.3':
resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-tooltip@1.2.7':
resolution: {integrity: sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-use-callback-ref@1.1.1':
resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-controllable-state@1.2.2':
resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-effect-event@0.0.2':
resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-escape-keydown@1.1.1':
resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-layout-effect@1.1.1':
resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-rect@1.1.1':
resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-size@1.1.1':
resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-visually-hidden@1.2.3':
resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/rect@1.1.1':
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
'@react-aria/accordion@3.0.0-alpha.35': '@react-aria/accordion@3.0.0-alpha.35':
resolution: {integrity: sha512-eZcsHJDVDNIZ2XUmJynHScRv1YAF/+fj5T0zoGdyEPImIIxJLROupQ75uwarAI5btGSR2TFeqYRmRXJrVuxgoA==} resolution: {integrity: sha512-eZcsHJDVDNIZ2XUmJynHScRv1YAF/+fj5T0zoGdyEPImIIxJLROupQ75uwarAI5btGSR2TFeqYRmRXJrVuxgoA==}
peerDependencies: peerDependencies:
@ -10171,6 +10398,23 @@ snapshots:
'@eslint/js@8.56.0': {} '@eslint/js@8.56.0': {}
'@floating-ui/core@1.7.0':
dependencies:
'@floating-ui/utils': 0.2.9
'@floating-ui/dom@1.7.0':
dependencies:
'@floating-ui/core': 1.7.0
'@floating-ui/utils': 0.2.9
'@floating-ui/react-dom@2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@floating-ui/dom': 1.7.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
'@floating-ui/utils@0.2.9': {}
'@formatjs/ecma402-abstract@1.11.4': '@formatjs/ecma402-abstract@1.11.4':
dependencies: dependencies:
'@formatjs/intl-localematcher': 0.2.25 '@formatjs/intl-localematcher': 0.2.25
@ -10946,6 +11190,182 @@ snapshots:
'@protobufjs/utf8@1.1.0': {} '@protobufjs/utf8@1.1.0': {}
'@radix-ui/primitive@1.1.2': {}
'@radix-ui/react-arrow@1.1.7(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.20
'@types/react-dom': 18.3.6(@types/react@18.3.20)
'@radix-ui/react-compose-refs@1.1.2(@types/react@18.3.20)(react@18.3.1)':
dependencies:
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.20
'@radix-ui/react-context@1.1.2(@types/react@18.3.20)(react@18.3.1)':
dependencies:
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.20
'@radix-ui/react-dismissable-layer@1.1.10(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.2
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.20)(react@18.3.1)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.20)(react@18.3.1)
'@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@18.3.20)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.20
'@types/react-dom': 18.3.6(@types/react@18.3.20)
'@radix-ui/react-id@1.1.1(@types/react@18.3.20)(react@18.3.1)':
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.20)(react@18.3.1)
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.20
'@radix-ui/react-popper@1.2.7(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@floating-ui/react-dom': 2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-arrow': 1.1.7(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.20)(react@18.3.1)
'@radix-ui/react-context': 1.1.2(@types/react@18.3.20)(react@18.3.1)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.20)(react@18.3.1)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.20)(react@18.3.1)
'@radix-ui/react-use-rect': 1.1.1(@types/react@18.3.20)(react@18.3.1)
'@radix-ui/react-use-size': 1.1.1(@types/react@18.3.20)(react@18.3.1)
'@radix-ui/rect': 1.1.1
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.20
'@types/react-dom': 18.3.6(@types/react@18.3.20)
'@radix-ui/react-portal@1.1.9(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.20)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.20
'@types/react-dom': 18.3.6(@types/react@18.3.20)
'@radix-ui/react-presence@1.1.4(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.20)(react@18.3.1)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.20)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.20
'@types/react-dom': 18.3.6(@types/react@18.3.20)
'@radix-ui/react-primitive@2.1.3(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/react-slot': 1.2.3(@types/react@18.3.20)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.20
'@types/react-dom': 18.3.6(@types/react@18.3.20)
'@radix-ui/react-slot@1.2.3(@types/react@18.3.20)(react@18.3.1)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.20)(react@18.3.1)
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.20
'@radix-ui/react-tooltip@1.2.7(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.2
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.20)(react@18.3.1)
'@radix-ui/react-context': 1.1.2(@types/react@18.3.20)(react@18.3.1)
'@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-id': 1.1.1(@types/react@18.3.20)(react@18.3.1)
'@radix-ui/react-popper': 1.2.7(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-slot': 1.2.3(@types/react@18.3.20)(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.20)(react@18.3.1)
'@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.20
'@types/react-dom': 18.3.6(@types/react@18.3.20)
'@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.20)(react@18.3.1)':
dependencies:
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.20
'@radix-ui/react-use-controllable-state@1.2.2(@types/react@18.3.20)(react@18.3.1)':
dependencies:
'@radix-ui/react-use-effect-event': 0.0.2(@types/react@18.3.20)(react@18.3.1)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.20)(react@18.3.1)
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.20
'@radix-ui/react-use-effect-event@0.0.2(@types/react@18.3.20)(react@18.3.1)':
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.20)(react@18.3.1)
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.20
'@radix-ui/react-use-escape-keydown@1.1.1(@types/react@18.3.20)(react@18.3.1)':
dependencies:
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.20)(react@18.3.1)
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.20
'@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.20)(react@18.3.1)':
dependencies:
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.20
'@radix-ui/react-use-rect@1.1.1(@types/react@18.3.20)(react@18.3.1)':
dependencies:
'@radix-ui/rect': 1.1.1
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.20
'@radix-ui/react-use-size@1.1.1(@types/react@18.3.20)(react@18.3.1)':
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.20)(react@18.3.1)
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.20
'@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.20
'@types/react-dom': 18.3.6(@types/react@18.3.20)
'@radix-ui/rect@1.1.1': {}
'@react-aria/accordion@3.0.0-alpha.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': '@react-aria/accordion@3.0.0-alpha.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies: dependencies:
'@react-aria/button': 3.11.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@react-aria/button': 3.11.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)

View file

@ -136,6 +136,7 @@ $image-radius: $button-radius - $image-margin;
} }
.FunSubNav__ListBoxItem { .FunSubNav__ListBoxItem {
position: relative;
flex: 1; flex: 1;
padding: 1px; padding: 1px;
cursor: pointer; cursor: pointer;
@ -154,6 +155,13 @@ $image-radius: $button-radius - $image-margin;
} }
} }
.FunSubNav__ListBoxItem__TooltipTarget {
display: block;
position: absolute;
inset: 0;
z-index: 2;
}
.FunSubNav__ListBoxItem__Button { .FunSubNav__ListBoxItem__Button {
position: relative; position: relative;
display: flex; display: flex;

View file

@ -6,6 +6,8 @@
@use './FunConstants.scss'; @use './FunConstants.scss';
.FunTooltip { .FunTooltip {
position: relative;
z-index: 100000;
max-width: calc(32ch + (8px * 2)); max-width: calc(32ch + (8px * 2));
padding-block: 4px; padding-block: 4px;
padding-inline: 8px; padding-inline: 8px;

View file

@ -1,12 +1,13 @@
// Copyright 2025 Signal Messenger, LLC // Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import type { ForwardedRef, ReactNode } from 'react'; import type {
import React, { forwardRef, useEffect, useRef } from 'react'; ForwardedRef,
import { type PressEvent, useLongPress } from 'react-aria'; ReactNode,
import type { LongPressEvent } from '@react-types/shared'; DOMAttributes,
import { Button } from 'react-aria-components'; PointerEvent,
import { mergeRefs } from '@react-aria/utils'; } from 'react';
import { PressResponder } from '@react-aria/interactions'; import React, { forwardRef, useCallback, useEffect, useMemo } from 'react';
import { mergeProps } from '@react-aria/utils';
import { strictAssert } from '../../../util/assert'; import { strictAssert } from '../../../util/assert';
/** /**
@ -28,49 +29,120 @@ export type FunItemButtonProps = Readonly<
{ {
'aria-label': string; 'aria-label': string;
excludeFromTabOrder: boolean; excludeFromTabOrder: boolean;
onPress: (event: PressEvent) => void; onClick: (event: PointerEvent) => void;
onContextMenu?: (event: MouseEvent) => void; onContextMenu?: (event: PointerEvent) => void;
children: ReactNode; children: ReactNode;
} & FunItemButtonLongPressProps } & FunItemButtonLongPressProps
>; >;
export const FunItemButton = forwardRef(function FunItemButton( export const FunItemButton = forwardRef(function FunItemButton(
props: FunItemButtonProps, props: FunItemButtonProps,
outerRef: ForwardedRef<HTMLButtonElement> ref: ForwardedRef<HTMLButtonElement>
): JSX.Element { ): JSX.Element {
const { onContextMenu } = props; const {
const innerRef = useRef<HTMLButtonElement>(null); 'aria-label': ariaLabel,
excludeFromTabOrder,
onClick,
onContextMenu,
children,
longPressAccessibilityDescription,
onLongPress,
...rest
} = props;
const { longPressProps } = useLongPress({ const longPressProps = useLongPress(onLongPress ?? null);
isDisabled: props.onLongPress == null,
accessibilityDescription: props.longPressAccessibilityDescription,
onLongPress: props.onLongPress,
});
useEffect(() => { const handleClick = useCallback(
strictAssert(innerRef.current, 'Missing ref element'); (event: PointerEvent) => {
const element = innerRef.current; if (!event.defaultPrevented) {
if (onContextMenu == null) { onClick(event);
return () => null; }
} },
element.addEventListener('contextmenu', onContextMenu); [onClick]
return () => { );
element.removeEventListener('contextmenu', onContextMenu);
};
}, [onContextMenu]);
return ( return (
<PressResponder {...longPressProps}> // eslint-disable-next-line jsx-a11y/role-supports-aria-props
<Button <button
ref={mergeRefs(innerRef, outerRef)} ref={ref}
type="button" type="button"
className="FunItem__Button" className="FunItem__Button"
aria-label={props['aria-label']} aria-label={ariaLabel}
excludeFromTabOrder={props.excludeFromTabOrder} aria-description={longPressAccessibilityDescription}
onPress={props.onPress} tabIndex={excludeFromTabOrder ? -1 : undefined}
> {...mergeProps(
{props.children} longPressProps,
</Button> {
</PressResponder> onClick: handleClick,
onContextMenu,
},
rest
)}
>
{children}
</button>
); );
}); });
type LongPressEvent = Readonly<{
pointerType: PointerEvent['pointerType'];
}>;
function useLongPress(
onLongPress: ((event: LongPressEvent) => void) | null
): DOMAttributes<Element> {
const { cleanup, props } = useMemo(() => {
if (onLongPress == null) {
return { props: {} };
}
let timer: ReturnType<typeof setTimeout>;
let isLongPressed = false;
let lastLongPress: number | null = null;
function reset() {
clearTimeout(timer);
isLongPressed = false;
}
function handleCancel(event: PointerEvent) {
if (isLongPressed) {
lastLongPress = event.timeStamp;
}
reset();
}
function handleStart(event: PointerEvent) {
const press: LongPressEvent = { pointerType: event.pointerType };
reset();
timer = setTimeout(() => {
isLongPressed = true;
strictAssert(onLongPress != null, 'Missing callback');
onLongPress(press);
}, 500);
}
function handleClick(event: PointerEvent) {
if (event.timeStamp === lastLongPress) {
event.preventDefault();
}
}
return {
cleanup: reset,
props: {
onPointerDown: handleStart,
onPointerUp: handleCancel,
onPointerCancel: handleCancel,
onPointerLeave: handleCancel,
onClick: handleClick,
} satisfies DOMAttributes<Element>,
};
}, [onLongPress]);
useEffect(() => {
return cleanup;
}, [cleanup]);
return props;
}

View file

@ -5,6 +5,7 @@ import React, { useCallback } from 'react';
import type { Placement } from 'react-aria'; import type { Placement } from 'react-aria';
import { Dialog, Popover } from 'react-aria-components'; import { Dialog, Popover } from 'react-aria-components';
import classNames from 'classnames'; import classNames from 'classnames';
import * as Tooltip from '@radix-ui/react-tooltip';
import { ThemeType } from '../../../types/Util'; import { ThemeType } from '../../../types/Util';
export type FunPopoverProps = Readonly<{ export type FunPopoverProps = Readonly<{
@ -16,8 +17,14 @@ export type FunPopoverProps = Readonly<{
export function FunPopover(props: FunPopoverProps): JSX.Element { export function FunPopover(props: FunPopoverProps): JSX.Element {
const shouldCloseOnInteractOutside = useCallback( const shouldCloseOnInteractOutside = useCallback(
(element: Element): boolean => { (element: Element): boolean => {
// Don't close when quill steals focus const match = element.closest(
const match = element.closest('.module-composition-input__input'); [
// Don't close when quill steals focus
'.module-composition-input__input',
// Don't close when clicking tooltip
'.FunTooltip',
].join(', ')
);
if (match != null) { if (match != null) {
return false; return false;
} }
@ -27,16 +34,18 @@ export function FunPopover(props: FunPopoverProps): JSX.Element {
); );
return ( return (
<Popover <Tooltip.Provider>
data-fun-overlay <Popover
className={classNames('FunPopover', { data-fun-overlay
'light-theme': props.theme === ThemeType.light, className={classNames('FunPopover', {
'dark-theme': props.theme === ThemeType.dark, 'light-theme': props.theme === ThemeType.light,
})} 'dark-theme': props.theme === ThemeType.dark,
placement={props.placement} })}
shouldCloseOnInteractOutside={shouldCloseOnInteractOutside} placement={props.placement}
> shouldCloseOnInteractOutside={shouldCloseOnInteractOutside}
<Dialog className="FunPopover__Dialog">{props.children}</Dialog> >
</Popover> <Dialog className="FunPopover__Dialog">{props.children}</Dialog>
</Popover>
</Tooltip.Provider>
); );
} }

View file

@ -3,7 +3,7 @@
import classNames from 'classnames'; import classNames from 'classnames';
import type { Transition } from 'framer-motion'; import type { Transition } from 'framer-motion';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import type { ReactNode } from 'react'; import type { ReactNode, Ref } from 'react';
import React, { import React, {
createContext, createContext,
useCallback, useCallback,
@ -13,6 +13,7 @@ import React, {
useRef, useRef,
useState, useState,
useId, useId,
forwardRef,
} from 'react'; } from 'react';
import type { Selection } from 'react-aria-components'; import type { Selection } from 'react-aria-components';
import { ListBox, ListBoxItem } from 'react-aria-components'; import { ListBox, ListBoxItem } from 'react-aria-components';
@ -25,6 +26,7 @@ import * as log from '../../../logging/log';
import * as Errors from '../../../types/errors'; import * as Errors from '../../../types/errors';
import { strictAssert } from '../../../util/assert'; import { strictAssert } from '../../../util/assert';
import { FunImage } from './FunImage'; import { FunImage } from './FunImage';
import { FunTooltip } from './FunTooltip';
/** /**
* Sub Nav * Sub Nav
@ -246,11 +248,30 @@ function FunSubNavListBoxItemButton(props: {
); );
} }
const FunSubNavListBoxItemTooltipTarget = forwardRef(
function FunSubNavListBoxItemTooltipTarget(props, ref: Ref<HTMLSpanElement>) {
return (
<span
ref={ref}
{...props}
className="FunSubNav__ListBoxItem__TooltipTarget"
/>
);
}
);
export function FunSubNavListBoxItem( export function FunSubNavListBoxItem(
props: FunSubNavListBoxItemProps props: FunSubNavListBoxItemProps
): JSX.Element { ): JSX.Element {
const context = useContext(FunSubNavListBoxContext); const context = useContext(FunSubNavListBoxContext);
strictAssert(context, 'Must be wrapped with <FunSubNavListBox>'); strictAssert(context, 'Must be wrapped with <FunSubNavListBox>');
const [tooltipOpen, setTooltipOpen] = useState(false);
const handleTooltipOpenChange = useCallback((open: boolean) => {
setTooltipOpen(open);
}, []);
return ( return (
<ListBoxItem <ListBoxItem
id={props.id} id={props.id}
@ -260,22 +281,35 @@ export function FunSubNavListBoxItem(
> >
{({ isSelected, isFocusVisible }) => { {({ isSelected, isFocusVisible }) => {
return ( return (
<FunSubNavListBoxItemButton isSelected={isSelected}> <>
<span className="FunSubNav__ListBoxItem__ButtonIcon"> <FunTooltip
{props.children} open={tooltipOpen || (isSelected && isFocusVisible)}
</span> onOpenChange={handleTooltipOpenChange}
{isSelected && ( side="top"
<motion.div content={props.label}
className="FunSubNav__ListBoxItem__ButtonIndicator" collisionBoundarySelector=".FunPanel"
layoutId={`FunSubNav__ListBoxItem__ButtonIndicator--${context.id}`} collisionPadding={6}
layoutDependency={context.selected} disableHoverableContent
transition={FunSubNavListBoxItemTransition} >
/> <FunSubNavListBoxItemTooltipTarget />
)} </FunTooltip>
{!isSelected && isFocusVisible && ( <FunSubNavListBoxItemButton isSelected={isSelected}>
<div className="FunSubNav__ListBoxItem__ButtonIndicator" /> <span className="FunSubNav__ListBoxItem__ButtonIcon">
)} {props.children}
</FunSubNavListBoxItemButton> </span>
{isSelected && (
<motion.div
className="FunSubNav__ListBoxItem__ButtonIndicator"
layoutId={`FunSubNav__ListBoxItem__ButtonIndicator--${context.id}`}
layoutDependency={context.selected}
transition={FunSubNavListBoxItemTransition}
/>
)}
{!isSelected && isFocusVisible && (
<div className="FunSubNav__ListBoxItem__ButtonIndicator" />
)}
</FunSubNavListBoxItemButton>
</>
); );
}} }}
</ListBoxItem> </ListBoxItem>

View file

@ -1,19 +1,57 @@
// Copyright 2025 Signal Messenger, LLC // Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React, { type ReactNode } from 'react'; import React, { useRef, useState, type ReactNode } from 'react';
import type { Placement } from 'react-aria'; import * as Tooltip from '@radix-ui/react-tooltip';
import { Tooltip } from 'react-aria-components'; import { useLayoutEffect } from '@react-aria/utils';
import { strictAssert } from '../../../util/assert';
export type FunTooltipProps = Readonly<{ export type FunTooltipProps = Readonly<{
placement?: Placement; open?: boolean;
onOpenChange?: (open: boolean) => void;
disableHoverableContent?: boolean;
side?: Tooltip.TooltipContentProps['side'];
align?: Tooltip.TooltipContentProps['align'];
collisionBoundarySelector?: string;
collisionPadding?: number;
content: ReactNode;
children: ReactNode; children: ReactNode;
}>; }>;
export function FunTooltip(props: FunTooltipProps): JSX.Element { export function FunTooltip(props: FunTooltipProps): JSX.Element {
const ref = useRef<HTMLButtonElement>(null);
const [collisionBoundary, setCollisionBoundary] = useState<Element | null>(
null
);
useLayoutEffect(() => {
if (props.collisionBoundarySelector == null) {
return;
}
strictAssert(ref.current, 'missing ref');
const trigger = ref.current;
setCollisionBoundary(trigger.closest(props.collisionBoundarySelector));
}, [props.collisionBoundarySelector]);
return ( return (
<Tooltip className="FunTooltip" placement={props.placement}> <Tooltip.Root
{props.children} open={props.open}
</Tooltip> onOpenChange={props.onOpenChange}
disableHoverableContent={props.disableHoverableContent}
>
<Tooltip.Trigger ref={ref} asChild>
{props.children}
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content
side={props.side}
align={props.align}
className="FunTooltip"
collisionBoundary={collisionBoundary}
collisionPadding={props.collisionPadding}
>
<span className="FunTooltip__Text">{props.content}</span>
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
); );
} }

View file

@ -1,16 +1,16 @@
// Copyright 2025 Signal Messenger, LLC // Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React, { memo, useCallback, useMemo, useRef, useState } from 'react'; import React, { memo, useCallback, useMemo, useRef, useState } from 'react';
import type { MouseEvent, PointerEvent } from 'react';
import { import {
Dialog, Dialog,
DialogTrigger, DialogTrigger,
Heading, Heading,
OverlayArrow, OverlayArrow,
Popover, Popover,
TooltipTrigger,
} from 'react-aria-components'; } from 'react-aria-components';
import type { PressEvent } from 'react-aria';
import { VisuallyHidden } from 'react-aria'; import { VisuallyHidden } from 'react-aria';
import * as Tooltip from '@radix-ui/react-tooltip';
import type { LocalizerType } from '../../../types/I18N'; import type { LocalizerType } from '../../../types/I18N';
import { strictAssert } from '../../../util/assert'; import { strictAssert } from '../../../util/assert';
import { missingCaseError } from '../../../util/missingCaseError'; import { missingCaseError } from '../../../util/missingCaseError';
@ -388,95 +388,99 @@ export function FunPanelEmojis({
</FunPanelFooter> </FunPanelFooter>
)} )}
<FunPanelBody> <FunPanelBody>
<FunScroller <Tooltip.Provider skipDelayDuration={0}>
ref={scrollerRef} <FunScroller
sectionGap={EMOJI_GRID_SECTION_GAP} ref={scrollerRef}
onScrollSectionChange={handleScrollSectionChange} sectionGap={EMOJI_GRID_SECTION_GAP}
> onScrollSectionChange={handleScrollSectionChange}
{layout.sections.length === 0 && ( >
<FunResults aria-busy={false}> {layout.sections.length === 0 && (
<FunResultsHeader> <FunResults aria-busy={false}>
{i18n('icu:FunPanelEmojis__SearchResults__EmptyHeading')}{' '} <FunResultsHeader>
<FunStaticEmoji {i18n('icu:FunPanelEmojis__SearchResults__EmptyHeading')}{' '}
size={16} <FunStaticEmoji
role="presentation" size={16}
emoji={emojiVariantConstant('\u{1F641}')} role="presentation"
/> emoji={emojiVariantConstant('\u{1F641}')}
</FunResultsHeader> />
</FunResults> </FunResultsHeader>
)} </FunResults>
{layout.sections.length > 0 && ( )}
<FunKeyboard {layout.sections.length > 0 && (
scrollerRef={scrollerRef} <FunKeyboard
keyboard={keyboard} scrollerRef={scrollerRef}
onStateChange={handleKeyboardStateChange} keyboard={keyboard}
> onStateChange={handleKeyboardStateChange}
<FunGridContainer
totalSize={layout.totalHeight}
columnCount={EMOJI_GRID_COLUMNS}
cellWidth={EMOJI_GRID_CELL_WIDTH}
cellHeight={EMOJI_GRID_CELL_HEIGHT}
> >
{layout.sections.map(section => { <FunGridContainer
return ( totalSize={layout.totalHeight}
<FunGridScrollerSection columnCount={EMOJI_GRID_COLUMNS}
key={section.key} cellWidth={EMOJI_GRID_CELL_WIDTH}
id={section.id} cellHeight={EMOJI_GRID_CELL_HEIGHT}
sectionOffset={section.sectionOffset} >
sectionSize={section.sectionSize} {layout.sections.map(section => {
> return (
<FunGridHeader <FunGridScrollerSection
id={section.header.key} key={section.key}
headerOffset={section.header.headerOffset} id={section.id}
headerSize={section.header.headerSize} sectionOffset={section.sectionOffset}
sectionSize={section.sectionSize}
> >
<FunGridHeaderText> <FunGridHeader
{getTitleForSection( id={section.header.key}
i18n, headerOffset={section.header.headerOffset}
section.id as FunEmojisSection headerSize={section.header.headerSize}
)} >
</FunGridHeaderText> <FunGridHeaderText>
{section.id === {getTitleForSection(
EmojiPickerCategory.SmileysAndPeople && ( i18n,
<SectionSkinToneHeaderPopover section.id as FunEmojisSection
i18n={i18n} )}
open={skinTonePopoverOpen} </FunGridHeaderText>
onOpenChange={handleSkinTonePopoverOpenChange} {section.id ===
onSelectSkinTone={fun.onEmojiSkinToneDefaultChange} EmojiPickerCategory.SmileysAndPeople && (
/> <SectionSkinToneHeaderPopover
)}
</FunGridHeader>
<FunGridRowGroup
aria-labelledby={section.header.key}
colCount={section.colCount}
rowCount={section.rowCount}
rowGroupOffset={section.rowGroup.rowGroupOffset}
rowGroupSize={section.rowGroup.rowGroupSize}
>
{section.rowGroup.rows.map(row => {
return (
<Row
key={row.key}
i18n={i18n} i18n={i18n}
rowIndex={row.rowIndex} open={skinTonePopoverOpen}
cells={row.cells} onOpenChange={handleSkinTonePopoverOpenChange}
focusedCellKey={focusedCellKey} onSelectSkinTone={
emojiSkinToneDefault={fun.emojiSkinToneDefault}
onSelectEmoji={handleSelectEmoji}
onEmojiSkinToneDefaultChange={
fun.onEmojiSkinToneDefaultChange fun.onEmojiSkinToneDefaultChange
} }
/> />
); )}
})} </FunGridHeader>
</FunGridRowGroup> <FunGridRowGroup
</FunGridScrollerSection> aria-labelledby={section.header.key}
); colCount={section.colCount}
})} rowCount={section.rowCount}
</FunGridContainer> rowGroupOffset={section.rowGroup.rowGroupOffset}
</FunKeyboard> rowGroupSize={section.rowGroup.rowGroupSize}
)} >
</FunScroller> {section.rowGroup.rows.map(row => {
return (
<Row
key={row.key}
i18n={i18n}
rowIndex={row.rowIndex}
cells={row.cells}
focusedCellKey={focusedCellKey}
emojiSkinToneDefault={fun.emojiSkinToneDefault}
onSelectEmoji={handleSelectEmoji}
onEmojiSkinToneDefaultChange={
fun.onEmojiSkinToneDefaultChange
}
/>
);
})}
</FunGridRowGroup>
</FunGridScrollerSection>
);
})}
</FunGridContainer>
</FunKeyboard>
)}
</FunScroller>
</Tooltip.Provider>
</FunPanelBody> </FunPanelBody>
</FunPanel> </FunPanel>
); );
@ -572,8 +576,8 @@ const Cell = memo(function Cell(props: CellProps): JSX.Element {
return getEmojiVariantByParentKeyAndSkinTone(emojiParent.key, skinTone); return getEmojiVariantByParentKeyAndSkinTone(emojiParent.key, skinTone);
}, [emojiParent, skinTone]); }, [emojiParent, skinTone]);
const handlePress = useCallback( const handleClick = useCallback(
(event: PressEvent) => { (event: PointerEvent) => {
if (emojiHasSkinToneVariants && emojiSkinToneDefault == null) { if (emojiHasSkinToneVariants && emojiSkinToneDefault == null) {
setPopoverOpen(true); setPopoverOpen(true);
return; return;
@ -585,7 +589,7 @@ const Cell = memo(function Cell(props: CellProps): JSX.Element {
skinTone, skinTone,
}; };
const shouldClose = const shouldClose =
(event.pointerType === 'keyboard' || event.pointerType === 'virtual') && event.nativeEvent.pointerType !== 'mouse' &&
!(event.ctrlKey || event.metaKey); !(event.ctrlKey || event.metaKey);
onSelectEmoji(emojiSelection, shouldClose); onSelectEmoji(emojiSelection, shouldClose);
}, },
@ -654,12 +658,20 @@ const Cell = memo(function Cell(props: CellProps): JSX.Element {
colIndex={props.colIndex} colIndex={props.colIndex}
rowIndex={props.rowIndex} rowIndex={props.rowIndex}
> >
<TooltipTrigger> <FunTooltip
side="top"
content={`:${emojiShortNameDisplay}:`}
collisionBoundarySelector=".FunScroller__Viewport"
collisionPadding={6}
// `skipDelayDuration=0` doesn't work with `disableHoverableContent`
// FIX: https://github.com/radix-ui/primitives/pull/3562
// disableHoverableContent
>
<FunItemButton <FunItemButton
ref={popoverTriggerRef} ref={popoverTriggerRef}
excludeFromTabOrder={!props.isTabbable} excludeFromTabOrder={!props.isTabbable}
aria-label={emojiName} aria-label={emojiName}
onPress={handlePress} onClick={handleClick}
onLongPress={handleLongPress} onLongPress={handleLongPress}
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu}
longPressAccessibilityDescription={i18n( longPressAccessibilityDescription={i18n(
@ -668,9 +680,7 @@ const Cell = memo(function Cell(props: CellProps): JSX.Element {
> >
<FunStaticEmoji role="presentation" size={32} emoji={emojiVariant} /> <FunStaticEmoji role="presentation" size={32} emoji={emojiVariant} />
</FunItemButton> </FunItemButton>
<FunTooltip placement="top">{`:${emojiShortNameDisplay}:`}</FunTooltip> </FunTooltip>
</TooltipTrigger>
d
{emojiHasSkinToneVariants && ( {emojiHasSkinToneVariants && (
<Popover <Popover
data-fun-overlay data-fun-overlay

View file

@ -2,6 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import type { Range } from '@tanstack/react-virtual'; import type { Range } from '@tanstack/react-virtual';
import { defaultRangeExtractor, useVirtualizer } from '@tanstack/react-virtual'; import { defaultRangeExtractor, useVirtualizer } from '@tanstack/react-virtual';
import type { PointerEvent } from 'react';
import React, { import React, {
memo, memo,
useCallback, useCallback,
@ -11,7 +12,6 @@ import React, {
useState, useState,
useId, useId,
} from 'react'; } from 'react';
import type { PressEvent } from 'react-aria';
import { VisuallyHidden } from 'react-aria'; import { VisuallyHidden } from 'react-aria';
import { LRUCache } from 'lru-cache'; import { LRUCache } from 'lru-cache';
import { FunItemButton } from '../base/FunItem'; import { FunItemButton } from '../base/FunItem';
@ -356,8 +356,8 @@ export function FunPanelGifs({
[] []
); );
const handlePressGif = useCallback( const handleClickGif = useCallback(
(_event: PressEvent, gifSelection: FunGifSelection) => { (_event: PointerEvent, gifSelection: FunGifSelection) => {
onFunSelectGif(gifSelection); onFunSelectGif(gifSelection);
onSelectGif(gifSelection); onSelectGif(gifSelection);
setSelectedItemKey(null); setSelectedItemKey(null);
@ -520,7 +520,7 @@ export function FunPanelGifs({
itemOffset={item.start} itemOffset={item.start}
itemLane={item.lane} itemLane={item.lane}
isTabbable={isTabbable} isTabbable={isTabbable}
onPressGif={handlePressGif} onClickGif={handleClickGif}
fetchGif={fetchGif} fetchGif={fetchGif}
/> />
); );
@ -542,16 +542,16 @@ const Item = memo(function Item(props: {
itemOffset: number; itemOffset: number;
itemLane: number; itemLane: number;
isTabbable: boolean; isTabbable: boolean;
onPressGif: (event: PressEvent, gifSelection: FunGifSelection) => void; onClickGif: (event: PointerEvent, gifSelection: FunGifSelection) => void;
fetchGif: typeof tenorDownload; fetchGif: typeof tenorDownload;
}) { }) {
const { onPressGif, fetchGif } = props; const { onClickGif, fetchGif } = props;
const handlePress = useCallback( const handleClick = useCallback(
async (event: PressEvent) => { async (event: PointerEvent) => {
onPressGif(event, { gif: props.gif }); onClickGif(event, { gif: props.gif });
}, },
[props.gif, onPressGif] [props.gif, onClickGif]
); );
const descriptionId = `FunGifsPanelItem__GifDescription--${props.gif.id}`; const descriptionId = `FunGifsPanelItem__GifDescription--${props.gif.id}`;
@ -606,7 +606,7 @@ const Item = memo(function Item(props: {
> >
<FunItemButton <FunItemButton
aria-label={props.gif.title} aria-label={props.gif.title}
onPress={handlePress} onClick={handleClick}
excludeFromTabOrder={!props.isTabbable} excludeFromTabOrder={!props.isTabbable}
> >
{src != null && ( {src != null && (

View file

@ -1,6 +1,6 @@
// Copyright 2025 Signal Messenger, LLC // Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import type { CSSProperties } from 'react'; import type { CSSProperties, PointerEvent } from 'react';
import React, { import React, {
memo, memo,
useCallback, useCallback,
@ -9,7 +9,6 @@ import React, {
useRef, useRef,
useState, useState,
} from 'react'; } from 'react';
import type { PressEvent } from 'react-aria';
import type { import type {
StickerPackType, StickerPackType,
StickerType, StickerType,
@ -344,8 +343,8 @@ export function FunPanelStickers({
return searchInput.length > 0; return searchInput.length > 0;
}, [searchInput]); }, [searchInput]);
const handlePressSticker = useCallback( const handleClickSticker = useCallback(
(event: PressEvent, stickerSelection: FunStickerSelection) => { (event: PointerEvent, stickerSelection: FunStickerSelection) => {
onFunSelectSticker(stickerSelection); onFunSelectSticker(stickerSelection);
onSelectSticker(stickerSelection); onSelectSticker(stickerSelection);
if (!(event.ctrlKey || event.metaKey)) { if (!(event.ctrlKey || event.metaKey)) {
@ -356,8 +355,8 @@ export function FunPanelStickers({
[onFunSelectSticker, onSelectSticker, onClose] [onFunSelectSticker, onSelectSticker, onClose]
); );
const handlePressTimeSticker = useCallback( const handleClickTimeSticker = useCallback(
(event: PressEvent, style: FunTimeStickerStyle) => { (event: PointerEvent, style: FunTimeStickerStyle) => {
onSelectTimeSticker?.(style); onSelectTimeSticker?.(style);
if (!(event.ctrlKey || event.metaKey)) { if (!(event.ctrlKey || event.metaKey)) {
onClose(); onClose();
@ -492,8 +491,8 @@ export function FunPanelStickers({
cells={row.cells} cells={row.cells}
stickerLookup={stickerLookup} stickerLookup={stickerLookup}
focusedCellKey={focusedCellKey} focusedCellKey={focusedCellKey}
onPressSticker={handlePressSticker} onClickSticker={handleClickSticker}
onPressTimeSticker={handlePressTimeSticker} onClickTimeSticker={handleClickTimeSticker}
/> />
); );
})} })}
@ -515,11 +514,11 @@ const Row = memo(function Row(props: {
stickerLookup: StickerLookup; stickerLookup: StickerLookup;
cells: ReadonlyArray<CellLayoutNode>; cells: ReadonlyArray<CellLayoutNode>;
focusedCellKey: CellKey | null; focusedCellKey: CellKey | null;
onPressSticker: ( onClickSticker: (
event: PressEvent, event: PointerEvent,
stickerSelection: FunStickerSelection stickerSelection: FunStickerSelection
) => void; ) => void;
onPressTimeSticker: (event: PressEvent, style: FunTimeStickerStyle) => void; onClickTimeSticker: (event: PointerEvent, style: FunTimeStickerStyle) => void;
}): JSX.Element { }): JSX.Element {
return ( return (
<FunGridRow rowIndex={props.rowIndex}> <FunGridRow rowIndex={props.rowIndex}>
@ -537,8 +536,8 @@ const Row = memo(function Row(props: {
colIndex={cell.colIndex} colIndex={cell.colIndex}
stickerLookup={props.stickerLookup} stickerLookup={props.stickerLookup}
isTabbable={isTabbable} isTabbable={isTabbable}
onPressSticker={props.onPressSticker} onClickSticker={props.onClickSticker}
onPressTimeSticker={props.onPressTimeSticker} onClickTimeSticker={props.onClickTimeSticker}
/> />
); );
})} })}
@ -553,28 +552,28 @@ const Cell = memo(function Cell(props: {
rowIndex: number; rowIndex: number;
stickerLookup: StickerLookup; stickerLookup: StickerLookup;
isTabbable: boolean; isTabbable: boolean;
onPressSticker: ( onClickSticker: (
event: PressEvent, event: PointerEvent,
stickerSelection: FunStickerSelection stickerSelection: FunStickerSelection
) => void; ) => void;
onPressTimeSticker: (event: PressEvent, style: FunTimeStickerStyle) => void; onClickTimeSticker: (event: PointerEvent, style: FunTimeStickerStyle) => void;
}): JSX.Element { }): JSX.Element {
const { onPressSticker, onPressTimeSticker } = props; const { onClickSticker, onClickTimeSticker } = props;
const stickerLookupItem = props.stickerLookup[props.value]; const stickerLookupItem = props.stickerLookup[props.value];
const handlePress = useCallback( const handleClick = useCallback(
(event: PressEvent) => { (event: PointerEvent) => {
if (stickerLookupItem.kind === 'sticker') { if (stickerLookupItem.kind === 'sticker') {
onPressSticker(event, { onClickSticker(event, {
stickerPackId: stickerLookupItem.sticker.packId, stickerPackId: stickerLookupItem.sticker.packId,
stickerId: stickerLookupItem.sticker.id, stickerId: stickerLookupItem.sticker.id,
stickerUrl: stickerLookupItem.sticker.url, stickerUrl: stickerLookupItem.sticker.url,
}); });
} else if (stickerLookupItem.kind === 'timeSticker') { } else if (stickerLookupItem.kind === 'timeSticker') {
onPressTimeSticker(event, stickerLookupItem.style); onClickTimeSticker(event, stickerLookupItem.style);
} }
}, },
[stickerLookupItem, onPressSticker, onPressTimeSticker] [stickerLookupItem, onClickSticker, onClickTimeSticker]
); );
return ( return (
@ -590,7 +589,7 @@ const Cell = memo(function Cell(props: {
? (stickerLookupItem.sticker.emoji ?? '') ? (stickerLookupItem.sticker.emoji ?? '')
: stickerLookupItem.style : stickerLookupItem.style
} }
onPress={handlePress} onClick={handleClick}
> >
{stickerLookupItem.kind === 'sticker' && ( {stickerLookupItem.kind === 'sticker' && (
<FunSticker <FunSticker

View file

@ -15,6 +15,69 @@
"updated": "2018-09-18T19:19:27.699Z", "updated": "2018-09-18T19:19:27.699Z",
"reasonDetail": "Part of runtime library for C++ transpiled code" "reasonDetail": "Part of runtime library for C++ transpiled code"
}, },
{
"rule": "React-useRef",
"path": "node_modules/@radix-ui/react-tooltip/dist/index.js",
"line": " const isOpenDelayedRef = React.useRef(true);",
"reasonCategory": "usageTrusted",
"updated": "2025-05-30T20:26:57.154Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@radix-ui/react-tooltip/dist/index.js",
"line": " const isPointerInTransitRef = React.useRef(false);",
"reasonCategory": "usageTrusted",
"updated": "2025-05-30T20:26:57.154Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@radix-ui/react-tooltip/dist/index.js",
"line": " const skipDelayTimerRef = React.useRef(0);",
"reasonCategory": "usageTrusted",
"updated": "2025-05-30T20:26:57.154Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@radix-ui/react-tooltip/dist/index.js",
"line": " const openTimerRef = React.useRef(0);",
"reasonCategory": "usageTrusted",
"updated": "2025-05-30T20:26:57.154Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@radix-ui/react-tooltip/dist/index.js",
"line": " const wasOpenDelayedRef = React.useRef(false);",
"reasonCategory": "usageTrusted",
"updated": "2025-05-30T20:26:57.154Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@radix-ui/react-tooltip/dist/index.js",
"line": " const ref = React.useRef(null);",
"reasonCategory": "usageTrusted",
"updated": "2025-05-30T20:26:57.154Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@radix-ui/react-tooltip/dist/index.js",
"line": " const isPointerDownRef = React.useRef(false);",
"reasonCategory": "usageTrusted",
"updated": "2025-05-30T20:26:57.154Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@radix-ui/react-tooltip/dist/index.js",
"line": " const hasPointerMoveOpenedRef = React.useRef(false);",
"reasonCategory": "usageTrusted",
"updated": "2025-05-30T20:26:57.154Z"
},
{
"rule": "React-useRef",
"path": "node_modules/@radix-ui/react-tooltip/dist/index.js",
"line": " const ref = React.useRef(null);",
"reasonCategory": "usageTrusted",
"updated": "2025-05-30T20:26:57.154Z"
},
{ {
"rule": "DOM-innerHTML", "rule": "DOM-innerHTML",
"path": "node_modules/@signalapp/quill-cjs/core/editor.js", "path": "node_modules/@signalapp/quill-cjs/core/editor.js",
@ -1842,13 +1905,6 @@
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2025-02-19T20:14:46.879Z" "updated": "2025-02-19T20:14:46.879Z"
}, },
{
"rule": "React-useRef",
"path": "ts/components/fun/base/FunItem.tsx",
"line": " const innerRef = useRef<HTMLButtonElement>(null);",
"reasonCategory": "usageTrusted",
"updated": "2025-04-23T23:43:10.675Z"
},
{ {
"rule": "React-useRef", "rule": "React-useRef",
"path": "ts/components/fun/base/FunScroller.tsx", "path": "ts/components/fun/base/FunScroller.tsx",
@ -1905,6 +1961,13 @@
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2025-02-19T20:14:46.879Z" "updated": "2025-02-19T20:14:46.879Z"
}, },
{
"rule": "React-useRef",
"path": "ts/components/fun/base/FunTooltip.tsx",
"line": " const ref = useRef<HTMLButtonElement>(null);",
"reasonCategory": "usageTrusted",
"updated": "2025-05-30T20:26:57.154Z"
},
{ {
"rule": "React-useRef", "rule": "React-useRef",
"path": "ts/components/fun/data/infinite.ts", "path": "ts/components/fun/data/infinite.ts",