Skip to content

Commit

Permalink
fix: trap focus in opened modal (#2278)
Browse files Browse the repository at this point in the history
### 🎯 Goal

When a modal dialog is displayed in chat, it's still possible to `Tab`
your way through the focusable elements outside of the modal. The common
practice is to "trap" the focus inside the modal, so that `Tab` cycles
through the focusable elements inside the modal only.

### 🛠 Implementation details

Implementing focus trap properly is not as easy as it seems - so it's
better to use a proven solution. This PR uses `FocusScope` from React
Aria.

### 🎨 UI Changes


https://github.com/GetStream/stream-chat-react/assets/975978/a6ac10f0-b4a9-44b2-9beb-2a822ceb0ab2
  • Loading branch information
myandrienko committed Feb 21, 2024
1 parent 706cf3d commit 8f48b52
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 43 deletions.
58 changes: 58 additions & 0 deletions examples/typescript/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1917,6 +1917,57 @@
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45"
integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==

"@react-aria/focus@^3.16.1":
version "3.16.1"
resolved "https://registry.yarnpkg.com/@react-aria/focus/-/focus-3.16.1.tgz#557a451cbe901153d23045ce27851b05709db24a"
integrity sha512-3ZEYc+hWqDQX7fA54ZOTkED8OGXs9+K9fYmjD1IdjZJAJS/2/AJ95PgIQ29zBkl9D9TAi4Nb3tJ/3+H/02UzoA==
dependencies:
"@react-aria/interactions" "^3.21.0"
"@react-aria/utils" "^3.23.1"
"@react-types/shared" "^3.22.0"
"@swc/helpers" "^0.5.0"
clsx "^2.0.0"

"@react-aria/interactions@^3.21.0":
version "3.21.0"
resolved "https://registry.yarnpkg.com/@react-aria/interactions/-/interactions-3.21.0.tgz#c04f4eb59ae70b723d7be8d5f8eb4e2802087a49"
integrity sha512-sPuzEl4Xq/BR5gbYr2R/sDzwlX9NdJ02i8Ew2rEy2hLMlf1jAeUAdTg/G+K9baWJ8acV9fZv6h/mdV3dXGLPSg==
dependencies:
"@react-aria/ssr" "^3.9.1"
"@react-aria/utils" "^3.23.1"
"@react-types/shared" "^3.22.0"
"@swc/helpers" "^0.5.0"

"@react-aria/ssr@^3.9.1":
version "3.9.1"
resolved "https://registry.yarnpkg.com/@react-aria/ssr/-/ssr-3.9.1.tgz#a1252fd5ef87eada810dd9dd6751a5e21359d1d2"
integrity sha512-NqzkLFP8ZVI4GSorS0AYljC13QW2sc8bDqJOkBvkAt3M8gbcAXJWVRGtZBCRscki9RZF+rNlnPdg0G0jYkhJcg==
dependencies:
"@swc/helpers" "^0.5.0"

"@react-aria/utils@^3.23.1":
version "3.23.1"
resolved "https://registry.yarnpkg.com/@react-aria/utils/-/utils-3.23.1.tgz#a082a5ffb97a7b9c03d522dcedfc251af8473e44"
integrity sha512-iXibf9ojqdoygbvy/++v5cKLKgjc/5ZmKV8/9u/2Hkpha1cf5Td/Z+Vl42B6giUBAsuDio5kuZYfYC7Uk+t8ag==
dependencies:
"@react-aria/ssr" "^3.9.1"
"@react-stately/utils" "^3.9.0"
"@react-types/shared" "^3.22.0"
"@swc/helpers" "^0.5.0"
clsx "^2.0.0"

"@react-stately/utils@^3.9.0":
version "3.9.0"
resolved "https://registry.yarnpkg.com/@react-stately/utils/-/utils-3.9.0.tgz#9cb2c8eea5dd1b58256ecb436b963c01526bae37"
integrity sha512-yPKFY1F88HxuZ15BG2qwAYxtpE4HnIU0Ofi4CuBE0xC6I8mwo4OQjDzi+DZjxQngM9D6AeTTD6F1V8gkozA0Gw==
dependencies:
"@swc/helpers" "^0.5.0"

"@react-types/shared@^3.22.0":
version "3.22.0"
resolved "https://registry.yarnpkg.com/@react-types/shared/-/shared-3.22.0.tgz#70f85aad46cd225f7fcb29f1c2b5213163605074"
integrity sha512-yVOekZWbtSmmiThGEIARbBpnmUIuePFlLyctjvCbgJgGhz8JnEJOipLQ/a4anaWfzAgzSceQP8j/K+VOOePleA==

"@rgrove/parse-xml@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@rgrove/parse-xml/-/parse-xml-3.0.0.tgz#29d45eadeb6c9a701038cfb9fab2356a7bdc71d5"
Expand Down Expand Up @@ -2118,6 +2169,13 @@
"@svgr/plugin-svgo" "^5.5.0"
loader-utils "^2.0.0"

"@swc/helpers@^0.5.0":
version "0.5.6"
resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.6.tgz#d16d8566b7aea2bef90d059757e2d77f48224160"
integrity sha512-aYX01Ke9hunpoCexYAgQucEpARGQ5w/cqHFrIR+e9gdKb1QWTsVJuTJ2ozQzIAxLyRQe/m+2RqzkyOOGiMKRQA==
dependencies:
tslib "^2.4.0"

"@testing-library/dom@*":
version "7.31.2"
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.31.2.tgz#df361db38f5212b88555068ab8119f5d841a8c4a"
Expand Down
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"@babel/runtime": "^7.23.6",
"@braintree/sanitize-url": "^6.0.4",
"@popperjs/core": "^2.11.5",
"@react-aria/focus": "^3.16.1",
"clsx": "^2.0.0",
"dayjs": "^1.10.4",
"emoji-regex": "^9.2.0",
Expand Down Expand Up @@ -157,10 +158,10 @@
"@types/lodash.throttle": "^4.1.7",
"@types/lodash.uniqby": "^4.7.7",
"@types/moment": "^2.13.0",
"@types/react": "^18.0.8",
"@types/react-dom": "^18.0.3",
"@types/react-image-gallery": "^1.0.5",
"@types/react-is": "^17.0.3",
"@types/react": "^18.2.55",
"@types/react-dom": "^18.2.19",
"@types/react-image-gallery": "^1.2.4",
"@types/react-is": "^18.2.4",
"@types/textarea-caret": "3.0.0",
"@types/uuid": "^8.3.0",
"@typescript-eslint/eslint-plugin": "4.27.0",
Expand Down
37 changes: 20 additions & 17 deletions src/components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { PropsWithChildren, useEffect, useRef } from 'react';
import { FocusScope } from '@react-aria/focus';

import { CloseIconRound } from './icons';

Expand Down Expand Up @@ -42,24 +43,26 @@ export const Modal = ({ children, onClose, open }: PropsWithChildren<ModalProps>

return (
<div className='str-chat__modal str-chat__modal--open' onClick={handleClick}>
<button className='str-chat__modal__close-button' ref={closeRef} title={t<string>('Close')}>
{themeVersion === '2' && <CloseIconRound />}
<FocusScope autoFocus contain>
<button className='str-chat__modal__close-button' ref={closeRef} title={t<string>('Close')}>
{themeVersion === '2' && <CloseIconRound />}

{themeVersion === '1' && (
<>
{t<string>('Close')}
<svg height='10' width='10' xmlns='http://www.w3.org/2000/svg'>
<path
d='M9.916 1.027L8.973.084 5 4.058 1.027.084l-.943.943L4.058 5 .084 8.973l.943.943L5 5.942l3.973 3.974.943-.943L5.942 5z'
fillRule='evenodd'
/>
</svg>
</>
)}
</button>
<div className='str-chat__modal__inner str-chat-react__modal__inner' ref={innerRef}>
{children}
</div>
{themeVersion === '1' && (
<>
{t<string>('Close')}
<svg height='10' width='10' xmlns='http://www.w3.org/2000/svg'>
<path
d='M9.916 1.027L8.973.084 5 4.058 1.027.084l-.943.943L4.058 5 .084 8.973l.943.943L5 5.942l3.973 3.974.943-.943L5.942 5z'
fillRule='evenodd'
/>
</svg>
</>
)}
</button>
<div className='str-chat__modal__inner str-chat-react__modal__inner' ref={innerRef}>
{children}
</div>
</FocusScope>
</div>
);
};
109 changes: 87 additions & 22 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1955,6 +1955,57 @@
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.5.tgz#db5a11bf66bdab39569719555b0f76e138d7bd64"
integrity sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw==

"@react-aria/focus@^3.16.1":
version "3.16.1"
resolved "https://registry.yarnpkg.com/@react-aria/focus/-/focus-3.16.1.tgz#557a451cbe901153d23045ce27851b05709db24a"
integrity sha512-3ZEYc+hWqDQX7fA54ZOTkED8OGXs9+K9fYmjD1IdjZJAJS/2/AJ95PgIQ29zBkl9D9TAi4Nb3tJ/3+H/02UzoA==
dependencies:
"@react-aria/interactions" "^3.21.0"
"@react-aria/utils" "^3.23.1"
"@react-types/shared" "^3.22.0"
"@swc/helpers" "^0.5.0"
clsx "^2.0.0"

"@react-aria/interactions@^3.21.0":
version "3.21.0"
resolved "https://registry.yarnpkg.com/@react-aria/interactions/-/interactions-3.21.0.tgz#c04f4eb59ae70b723d7be8d5f8eb4e2802087a49"
integrity sha512-sPuzEl4Xq/BR5gbYr2R/sDzwlX9NdJ02i8Ew2rEy2hLMlf1jAeUAdTg/G+K9baWJ8acV9fZv6h/mdV3dXGLPSg==
dependencies:
"@react-aria/ssr" "^3.9.1"
"@react-aria/utils" "^3.23.1"
"@react-types/shared" "^3.22.0"
"@swc/helpers" "^0.5.0"

"@react-aria/ssr@^3.9.1":
version "3.9.1"
resolved "https://registry.yarnpkg.com/@react-aria/ssr/-/ssr-3.9.1.tgz#a1252fd5ef87eada810dd9dd6751a5e21359d1d2"
integrity sha512-NqzkLFP8ZVI4GSorS0AYljC13QW2sc8bDqJOkBvkAt3M8gbcAXJWVRGtZBCRscki9RZF+rNlnPdg0G0jYkhJcg==
dependencies:
"@swc/helpers" "^0.5.0"

"@react-aria/utils@^3.23.1":
version "3.23.1"
resolved "https://registry.yarnpkg.com/@react-aria/utils/-/utils-3.23.1.tgz#a082a5ffb97a7b9c03d522dcedfc251af8473e44"
integrity sha512-iXibf9ojqdoygbvy/++v5cKLKgjc/5ZmKV8/9u/2Hkpha1cf5Td/Z+Vl42B6giUBAsuDio5kuZYfYC7Uk+t8ag==
dependencies:
"@react-aria/ssr" "^3.9.1"
"@react-stately/utils" "^3.9.0"
"@react-types/shared" "^3.22.0"
"@swc/helpers" "^0.5.0"
clsx "^2.0.0"

"@react-stately/utils@^3.9.0":
version "3.9.0"
resolved "https://registry.yarnpkg.com/@react-stately/utils/-/utils-3.9.0.tgz#9cb2c8eea5dd1b58256ecb436b963c01526bae37"
integrity sha512-yPKFY1F88HxuZ15BG2qwAYxtpE4HnIU0Ofi4CuBE0xC6I8mwo4OQjDzi+DZjxQngM9D6AeTTD6F1V8gkozA0Gw==
dependencies:
"@swc/helpers" "^0.5.0"

"@react-types/shared@^3.22.0":
version "3.22.0"
resolved "https://registry.yarnpkg.com/@react-types/shared/-/shared-3.22.0.tgz#70f85aad46cd225f7fcb29f1c2b5213163605074"
integrity sha512-yVOekZWbtSmmiThGEIARbBpnmUIuePFlLyctjvCbgJgGhz8JnEJOipLQ/a4anaWfzAgzSceQP8j/K+VOOePleA==

"@rgrove/parse-xml@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@rgrove/parse-xml/-/parse-xml-3.0.0.tgz#29d45eadeb6c9a701038cfb9fab2356a7bdc71d5"
Expand Down Expand Up @@ -2199,6 +2250,13 @@
"@stream-io/escape-string-regexp" "^5.0.1"
lodash.deburr "^4.1.0"

"@swc/helpers@^0.5.0":
version "0.5.6"
resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.6.tgz#d16d8566b7aea2bef90d059757e2d77f48224160"
integrity sha512-aYX01Ke9hunpoCexYAgQucEpARGQ5w/cqHFrIR+e9gdKb1QWTsVJuTJ2ozQzIAxLyRQe/m+2RqzkyOOGiMKRQA==
dependencies:
tslib "^2.4.0"

"@testing-library/dom@^7.28.1":
version "7.30.0"
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.30.0.tgz#53697851f7708a1448cc30b74a2ea056dd709cd6"
Expand Down Expand Up @@ -2527,35 +2585,42 @@
integrity sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==

"@types/prop-types@*":
version "15.7.5"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
version "15.7.11"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.11.tgz#2596fb352ee96a1379c657734d4b913a613ad563"
integrity sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==

"@types/react-dom@^18.0.0", "@types/react-dom@^18.0.3":
"@types/react-dom@^18.0.0":
version "18.0.5"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.5.tgz#330b2d472c22f796e5531446939eacef8378444a"
integrity sha512-OWPWTUrY/NIrjsAPkAk1wW9LZeIjSvkXRhclsFO8CZcZGCOg2G0YZy4ft+rOyYxy8B7ui5iZzi9OkDebZ7/QSA==
dependencies:
"@types/react" "*"

"@types/react-image-gallery@^1.0.5":
version "1.0.5"
resolved "https://registry.yarnpkg.com/@types/react-image-gallery/-/react-image-gallery-1.0.5.tgz#563234ac0fa131e2920199d191fca0c1594ba29d"
integrity sha512-CjIjFXmV7zGRO/FkdtPK31Rwj0ud7Xz/9e5o4DCX2Lx96jR8+0xCmKeUAQQVN3pKc6jUjOC0f/aEylhKg8vhAw==
"@types/react-dom@^18.2.19":
version "18.2.19"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.19.tgz#b84b7c30c635a6c26c6a6dfbb599b2da9788be58"
integrity sha512-aZvQL6uUbIJpjZk4U8JZGbau9KDeAwMfmhyWorxgBkqDIEf6ROjRozcmPIicqsUwPUjbkDfHKgGee1Lq65APcA==
dependencies:
"@types/react" "*"

"@types/react-is@^17.0.3":
version "17.0.3"
resolved "https://registry.yarnpkg.com/@types/react-is/-/react-is-17.0.3.tgz#2d855ba575f2fc8d17ef9861f084acc4b90a137a"
integrity sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw==
"@types/react-image-gallery@^1.2.4":
version "1.2.4"
resolved "https://registry.yarnpkg.com/@types/react-image-gallery/-/react-image-gallery-1.2.4.tgz#17c2e3416a5c9ecab14588eac593d4a7aa583163"
integrity sha512-H0xpmT5rlSH0qiTvcUDCPDLRBi3J3Xa4COqaDqGb7ffLFpQoPAxpZdNuKCuThhFI0xJmNnMubZiD6B3kCBHtcw==
dependencies:
"@types/react" "*"

"@types/react@*", "@types/react@^18.0.8":
version "18.0.14"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.14.tgz#e016616ffff51dba01b04945610fe3671fdbe06d"
integrity sha512-x4gGuASSiWmo0xjDLpm5mPb52syZHJx02VKbqUKdLmKtAwIh63XClGsiTI1K6DO5q7ox4xAsQrU+Gl3+gGXF9Q==
"@types/react-is@^18.2.4":
version "18.2.4"
resolved "https://registry.yarnpkg.com/@types/react-is/-/react-is-18.2.4.tgz#95a92829de452662348ce08349ca65623c50daf7"
integrity sha512-wBc7HgmbCcrvw0fZjxbgz/xrrlZKzEqmABBMeSvpTvdm25u6KI6xdIi9pRE2G0C1Lw5ETFdcn4UbYZ4/rpqUYw==
dependencies:
"@types/react" "*"

"@types/react@*", "@types/react@^18.2.55":
version "18.2.55"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.55.tgz#38141821b7084404b5013742bc4ae08e44da7a67"
integrity sha512-Y2Tz5P4yz23brwm2d7jNon39qoAtMMmalOQv6+fEFt1mT+FcM3D841wDpoUvFXhaYenuROCy3FZYqdTjM7qVyA==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
Expand All @@ -2572,9 +2637,9 @@
integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==

"@types/scheduler@*":
version "0.16.2"
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
version "0.16.8"
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.8.tgz#ce5ace04cfeabe7ef87c0091e50752e36707deff"
integrity sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==

"@types/semver@^7.3.12":
version "7.3.13"
Expand Down Expand Up @@ -5009,9 +5074,9 @@ cssstyle@^2.3.0:
cssom "~0.3.6"

csstype@^3.0.2:
version "3.1.0"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.0.tgz#4ddcac3718d787cf9df0d1b7d15033925c8f29f2"
integrity sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==
version "3.1.3"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==

cyclist@^1.0.1:
version "1.0.1"
Expand Down

0 comments on commit 8f48b52

Please sign in to comment.