diff --git a/services/web/frontend/js/shared/components/accessible-modal.tsx b/services/web/frontend/js/shared/components/accessible-modal.tsx
index b30bca5f49..dbd8269f9e 100644
--- a/services/web/frontend/js/shared/components/accessible-modal.tsx
+++ b/services/web/frontend/js/shared/components/accessible-modal.tsx
@@ -1,31 +1,30 @@
import { useCallback } from 'react'
import { Modal, ModalProps } from 'react-bootstrap'
-// a bootstrap Modal with its `aria-hidden` attribute removed. Visisble modals
-// should not have their `aria-hidden` attribute set but that's a bug in our
-// version of react-bootstrap.
-function AccessibleModal({ show, ...otherProps }: ModalProps) {
- // use a callback ref to track the modal. This will re-run the function
- // when the element node or any of the dependencies are updated
- const setModalRef = useCallback(
+// A wrapper for the v0.33 React Bootstrap Modal component,
+// which ensures that the `aria-hidden` attribute is not set on the modal when it's visible,
+// and that role="dialog" is not duplicated.
+// https://github.com/react-bootstrap/react-bootstrap/issues/4790
+// There are other ARIA attributes on these modals which could be improved,
+// but this at least makes them accessible for tests.
+function AccessibleModal(props: ModalProps) {
+ const modalRef = useCallback(
element => {
- if (!element) return
-
- const modalNode = element._modal && element._modal.modalNode
- if (!modalNode) return
-
- if (show) {
- modalNode.removeAttribute('aria-hidden')
- } else {
- modalNode.setAttribute('aria-hidden', 'true')
+ const modalNode = element?._modal?.modalNode
+ if (modalNode) {
+ if (props.show) {
+ modalNode.removeAttribute('role')
+ modalNode.removeAttribute('aria-hidden')
+ } else {
+ // NOTE: possibly not ever used, as the modal is only rendered when shown
+ modalNode.setAttribute('aria-hidden', 'true')
+ }
}
},
- // `show` is necessary as a dependency, but eslint thinks it is not
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [show]
+ [props.show]
)
- return
+ return
}
export default AccessibleModal
diff --git a/services/web/frontend/js/shared/components/beta-badge.tsx b/services/web/frontend/js/shared/components/beta-badge.tsx
index 11a078eb2d..80e5eaec46 100644
--- a/services/web/frontend/js/shared/components/beta-badge.tsx
+++ b/services/web/frontend/js/shared/components/beta-badge.tsx
@@ -1,24 +1,20 @@
+import type { FC, ReactNode } from 'react'
+import classnames from 'classnames'
import Tooltip from './tooltip'
import { OverlayTriggerProps } from 'react-bootstrap'
type TooltipProps = {
id: string
- text: React.ReactNode
+ text: ReactNode
placement?: OverlayTriggerProps['placement']
className?: string
}
-type BetaBadgeProps = {
+const BetaBadge: FC<{
tooltip: TooltipProps
url?: string
phase?: string
-}
-
-function BetaBadge({
- tooltip,
- url = '/beta/participate',
- phase = 'beta',
-}: BetaBadgeProps) {
+}> = ({ tooltip, url = '/beta/participate', phase = 'beta' }) => {
let badgeClass
switch (phase) {
case 'release':
@@ -46,7 +42,7 @@ function BetaBadge({
href={url}
target="_blank"
rel="noopener noreferrer"
- className={`badge ${badgeClass}`}
+ className={classnames('badge', badgeClass)}
>
{tooltip.text}
diff --git a/services/web/frontend/js/shared/components/controlled-dropdown.js b/services/web/frontend/js/shared/components/controlled-dropdown.js
deleted file mode 100644
index 368456cb2b..0000000000
--- a/services/web/frontend/js/shared/components/controlled-dropdown.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import React from 'react'
-import { Dropdown } from 'react-bootstrap'
-import PropTypes from 'prop-types'
-import useDropdown from '../hooks/use-dropdown'
-
-export default function ControlledDropdown(props) {
- const dropdownProps = useDropdown(Boolean(props.defaultOpen))
-
- 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: dropdownProps.open })
- }
-
- // Overlay
- if ('show' in child.props) {
- return React.cloneElement(child, { show: dropdownProps.open })
- }
-
- // anything else
- return React.cloneElement(child)
- })}
-
- )
-}
-ControlledDropdown.propTypes = {
- children: PropTypes.any,
- defaultOpen: PropTypes.bool,
- id: PropTypes.string,
- className: PropTypes.string,
-}
diff --git a/services/web/frontend/js/shared/components/controlled-dropdown.tsx b/services/web/frontend/js/shared/components/controlled-dropdown.tsx
new file mode 100644
index 0000000000..c870cbe846
--- /dev/null
+++ b/services/web/frontend/js/shared/components/controlled-dropdown.tsx
@@ -0,0 +1,34 @@
+import { Children, cloneElement, type FC, isValidElement } from 'react'
+import { Dropdown, DropdownProps } from 'react-bootstrap'
+import useDropdown from '../hooks/use-dropdown'
+
+const ControlledDropdown: FC<
+ DropdownProps & { defaultOpen?: boolean }
+> = props => {
+ const dropdownProps = useDropdown(Boolean(props.defaultOpen))
+
+ return (
+
+ {Children.map(props.children, child => {
+ if (!isValidElement(child)) {
+ return child
+ }
+
+ // Dropdown.Menu
+ if ('open' in child.props) {
+ return cloneElement(child, { open: dropdownProps.open })
+ }
+
+ // Overlay
+ if ('show' in child.props) {
+ return cloneElement(child, { show: dropdownProps.open })
+ }
+
+ // anything else
+ return cloneElement(child)
+ })}
+
+ )
+}
+
+export default ControlledDropdown
diff --git a/services/web/frontend/js/shared/components/tooltip.tsx b/services/web/frontend/js/shared/components/tooltip.tsx
index 106bcdff25..2b10620ab3 100644
--- a/services/web/frontend/js/shared/components/tooltip.tsx
+++ b/services/web/frontend/js/shared/components/tooltip.tsx
@@ -1,28 +1,20 @@
+import type { FC, ReactNode } from 'react'
import {
OverlayTrigger,
OverlayTriggerProps,
Tooltip as BSTooltip,
} from 'react-bootstrap'
-type OverlayTriggerCustomProps = {
+type OverlayProps = Omit & {
shouldUpdatePosition?: boolean // Not officially documented https://stackoverflow.com/a/43138470
-} & OverlayTriggerProps
-
-type TooltipProps = {
- children: React.ReactNode
- description: React.ReactNode
- id: string
- overlayProps?: Omit
- tooltipProps?: BSTooltip.TooltipProps
}
-function Tooltip({
- id,
- description,
- children,
- tooltipProps,
- overlayProps,
-}: TooltipProps) {
+const Tooltip: FC<{
+ description: ReactNode
+ id: string
+ overlayProps?: OverlayProps
+ tooltipProps?: BSTooltip.TooltipProps
+}> = ({ id, description, children, tooltipProps, overlayProps }) => {
return (
+
+ Test
+
+
+ Some content
+
+ )
+
+ cy.findByRole('dialog').should('have.length', 1)
+ })
+
+ it('does not render a hidden modal', function () {
+ const handleHide = cy.stub()
+
+ cy.mount(
+
+
+ Test
+
+
+ Some content
+
+ )
+
+ cy.findByRole('dialog', { hidden: true }).should('have.length', 0)
+ })
+})