diff --git a/services/web/frontend/js/features/editor-navigation-toolbar/components/online-users-widget.js b/services/web/frontend/js/features/editor-navigation-toolbar/components/online-users-widget.js
index 2f9c3fde47..4979d2e848 100644
--- a/services/web/frontend/js/features/editor-navigation-toolbar/components/online-users-widget.js
+++ b/services/web/frontend/js/features/editor-navigation-toolbar/components/online-users-widget.js
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'
import { Dropdown, MenuItem, OverlayTrigger, Tooltip } from 'react-bootstrap'
import Icon from '../../../shared/components/icon'
import { getHueForUserId } from '../../../shared/utils/colors'
+import ControlledDropdown from '../../../shared/components/controlled-dropdown'
function OnlineUsersWidget({ onlineUsers, goToUser }) {
const { t } = useTranslation()
@@ -12,7 +13,7 @@ function OnlineUsersWidget({ onlineUsers, goToUser }) {
if (shouldDisplayDropdown) {
return (
-
+
))}
-
+
)
} else {
return (
diff --git a/services/web/frontend/js/features/preview/components/preview-download-button.js b/services/web/frontend/js/features/preview/components/preview-download-button.js
index f56a704c68..d2052eab92 100644
--- a/services/web/frontend/js/features/preview/components/preview-download-button.js
+++ b/services/web/frontend/js/features/preview/components/preview-download-button.js
@@ -4,6 +4,7 @@ import { Dropdown, OverlayTrigger, Tooltip } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import PreviewDownloadFileList from './preview-download-file-list'
import Icon from '../../../shared/components/icon'
+import ControlledDropdown from '../../../shared/components/controlled-dropdown'
function PreviewDownloadButton({
isCompiling,
@@ -40,7 +41,7 @@ function PreviewDownloadButton({
const hideTooltip = showText && pdfDownloadUrl
return (
-
-
+
)
}
diff --git a/services/web/frontend/js/features/preview/components/preview-logs-pane.js b/services/web/frontend/js/features/preview/components/preview-logs-pane.js
index 828231b411..d1a190260c 100644
--- a/services/web/frontend/js/features/preview/components/preview-logs-pane.js
+++ b/services/web/frontend/js/features/preview/components/preview-logs-pane.js
@@ -8,6 +8,7 @@ import PreviewDownloadFileList from './preview-download-file-list'
import PreviewError from './preview-error'
import Icon from '../../../shared/components/icon'
import usePersistedState from '../../../shared/hooks/use-persisted-state'
+import ControlledDropdown from '../../../shared/components/controlled-dropdown'
function PreviewLogsPane({
logEntries = { all: [], errors: [], warnings: [], typesetting: [] },
@@ -84,7 +85,7 @@ function PreviewLogsPane({
{t('clear_cached_files')}
-
-
+
)
diff --git a/services/web/frontend/js/features/preview/components/preview-recompile-button.js b/services/web/frontend/js/features/preview/components/preview-recompile-button.js
index c253540b6f..0fdc65985a 100644
--- a/services/web/frontend/js/features/preview/components/preview-recompile-button.js
+++ b/services/web/frontend/js/features/preview/components/preview-recompile-button.js
@@ -4,6 +4,7 @@ import { Dropdown, MenuItem, OverlayTrigger, Tooltip } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import classNames from 'classnames'
import Icon from '../../../shared/components/icon'
+import ControlledDropdown from '../../../shared/components/controlled-dropdown'
function PreviewRecompileButton({
compilerState: {
@@ -80,7 +81,7 @@ function PreviewRecompileButton({
)
const buttonElement = (
-
@@ -151,7 +152,7 @@ function PreviewRecompileButton({
{t('recompile_from_scratch')}
-
+
)
return showText ? (
diff --git a/services/web/frontend/js/shared/components/controlled-dropdown.js b/services/web/frontend/js/shared/components/controlled-dropdown.js
new file mode 100644
index 0000000000..f94d70fb84
--- /dev/null
+++ b/services/web/frontend/js/shared/components/controlled-dropdown.js
@@ -0,0 +1,47 @@
+import React, { useCallback, useState } from 'react'
+import { Dropdown } from 'react-bootstrap'
+import PropTypes from 'prop-types'
+
+export default function ControlledDropdown(props) {
+ const [open, setOpen] = useState(Boolean(props.defaultOpen))
+
+ const handleClick = useCallback(event => {
+ event.stopPropagation()
+ }, [])
+
+ const handleToggle = useCallback(value => {
+ setOpen(value)
+ }, [])
+
+ return (
+
+ {React.Children.map(props.children, child => {
+ if (!React.isValidElement(child)) {
+ return child
+ }
+
+ // Dropdown.Menu
+ if ('open' in child.props) {
+ return React.cloneElement(child, { open })
+ }
+
+ // Overlay
+ if ('show' in child.props) {
+ return React.cloneElement(child, { show: open })
+ }
+
+ // anything else
+ return React.cloneElement(child)
+ })}
+
+ )
+}
+ControlledDropdown.propTypes = {
+ children: PropTypes.any,
+ defaultOpen: PropTypes.bool,
+}
diff --git a/services/web/frontend/js/shared/context/layout-context.js b/services/web/frontend/js/shared/context/layout-context.js
index 79ba58ebb9..bac68a922f 100644
--- a/services/web/frontend/js/shared/context/layout-context.js
+++ b/services/web/frontend/js/shared/context/layout-context.js
@@ -26,10 +26,14 @@ export function LayoutProvider({ children }) {
const setView = useCallback(
value => {
- _setView(value)
- if (value === 'history') {
- $scope.toggleHistory()
- }
+ _setView(oldValue => {
+ // ensure that the "history:toggle" event is broadcast when switching in or out of history view
+ if (value === 'history' || oldValue === 'history') {
+ $scope.toggleHistory()
+ }
+
+ return value
+ })
},
[$scope, _setView]
)
diff --git a/services/web/frontend/stories/dropdown.stories.js b/services/web/frontend/stories/dropdown.stories.js
index 0c9c49845a..05270154fb 100644
--- a/services/web/frontend/stories/dropdown.stories.js
+++ b/services/web/frontend/stories/dropdown.stories.js
@@ -1,77 +1,10 @@
import React from 'react'
-import { Dropdown, DropdownButton, MenuItem } from 'react-bootstrap'
-import Icon from '../js/shared/components/icon'
-
-const MenuItems = () => (
- <>
-
-
-
-
-
- >
-)
-
-const defaultArgs = {
- bsStyle: 'default',
- title: 'Dropdown',
- pullRight: false,
- noCaret: false,
- className: '',
- defaultOpen: true,
-}
-
-export const Default = args => {
- return (
-
-
-
- )
-}
-Default.args = { ...defaultArgs }
-
-export const Primary = args => {
- return (
-
-
-
- )
-}
-Primary.args = { ...defaultArgs, bsStyle: 'primary' }
-
-export const RightAligned = args => {
- return (
-
-
-
-
-
- )
-}
-RightAligned.args = { ...defaultArgs, pullRight: true }
-
-export const SingleIconTransparent = args => {
- return (
-
-
-
-
-
- )
-}
-SingleIconTransparent.args = {
- ...defaultArgs,
- pullRight: true,
- title: ,
- noCaret: true,
- className: 'dropdown-toggle-no-background',
-}
+import { Dropdown, MenuItem } from 'react-bootstrap'
+import ControlledDropdown from '../js/shared/components/controlled-dropdown'
export const Customized = args => {
return (
- {
{args.title}
-
+
+
+
+
+
-
+
)
}
Customized.args = {
- ...defaultArgs,
- component: Dropdown,
title: 'Toggle & Menu used separately',
}
export default {
title: 'Dropdown',
- component: DropdownButton,
+ component: ControlledDropdown,
+ args: {
+ bsStyle: 'default',
+ title: 'Dropdown',
+ pullRight: false,
+ noCaret: false,
+ className: '',
+ defaultOpen: true,
+ },
}
diff --git a/services/web/package-lock.json b/services/web/package-lock.json
index b68465819f..9d85759e95 100644
--- a/services/web/package-lock.json
+++ b/services/web/package-lock.json
@@ -21275,7 +21275,17 @@
"string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
- "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "requires": {
+ "safe-buffer": "~5.2.0"
+ },
+ "dependencies": {
+ "safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
+ }
+ }
}
}
},
@@ -30243,13 +30253,12 @@
}
},
"react": {
- "version": "16.13.1",
- "resolved": "https://registry.npmjs.org/react/-/react-16.13.1.tgz",
- "integrity": "sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==",
+ "version": "17.0.2",
+ "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz",
+ "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==",
"requires": {
"loose-envify": "^1.1.0",
- "object-assign": "^4.1.1",
- "prop-types": "^15.6.2"
+ "object-assign": "^4.1.1"
}
},
"react-bootstrap": {
@@ -30677,14 +30686,13 @@
}
},
"react-dom": {
- "version": "16.13.1",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz",
- "integrity": "sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag==",
+ "version": "17.0.2",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
+ "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==",
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
- "prop-types": "^15.6.2",
- "scheduler": "^0.19.1"
+ "scheduler": "^0.20.2"
}
},
"react-draggable": {
@@ -32423,9 +32431,9 @@
}
},
"scheduler": {
- "version": "0.19.1",
- "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz",
- "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==",
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz",
+ "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==",
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
diff --git a/services/web/package.json b/services/web/package.json
index 434c060aed..55ef541cd9 100644
--- a/services/web/package.json
+++ b/services/web/package.json
@@ -141,11 +141,11 @@
"pug": "^3.0.1",
"pug-runtime": "^3.0.1",
"qrcode": "^1.4.4",
- "react": "^16.13.1",
+ "react": "^17.0.2",
"react-bootstrap": "^0.33.1",
"react-dnd": "^11.1.3",
"react-dnd-html5-backend": "^11.1.3",
- "react-dom": "^16.13.1",
+ "react-dom": "^17.0.2",
"react-error-boundary": "^2.3.1",
"react-i18next": "^11.7.1",
"react-linkify": "^1.0.0-alpha",
diff --git a/services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.js b/services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.js
index 9652b7596e..fc4a382a36 100644
--- a/services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.js
+++ b/services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.js
@@ -658,11 +658,11 @@ describe('', function () {
const submitButton = screen.getByRole('button', { name: 'Share' })
const respondWithError = async function (errorReason) {
- inputElement.focus()
+ fireEvent.focus(inputElement)
fireEvent.change(inputElement, {
target: { value: 'invited-author-1@example.com' },
})
- inputElement.blur()
+ fireEvent.blur(inputElement)
fetchMock.postOnce(
'express:/project/:projectId/invite',