Skip to content

Commit c697b9e

Browse files
authored
Merge branch 'develop' into fix/hindi-translations
2 parents 6574b83 + 35dc27f commit c697b9e

26 files changed

+1114
-624
lines changed

client/common/useModalClose.js

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { useEffect, useRef } from 'react';
2+
import useKeyDownHandlers from './useKeyDownHandlers';
3+
4+
/**
5+
* Common logic for Modal, Overlay, etc.
6+
*
7+
* Pass in the `onClose` handler.
8+
*
9+
* Can optionally pass in a ref, in case the `onClose` function needs to use the ref.
10+
*
11+
* Calls the provided `onClose` function on:
12+
* - Press Escape key.
13+
* - Click outside the element.
14+
*
15+
* Returns a ref to attach to the outermost element of the modal.
16+
*
17+
* @param {() => void} onClose
18+
* @param {React.MutableRefObject<HTMLElement | null>} [passedRef]
19+
* @return {React.MutableRefObject<HTMLElement | null>}
20+
*/
21+
export default function useModalClose(onClose, passedRef) {
22+
const createdRef = useRef(null);
23+
const modalRef = passedRef || createdRef;
24+
25+
useEffect(() => {
26+
modalRef.current?.focus();
27+
28+
function handleClick(e) {
29+
// ignore clicks on the component itself
30+
if (modalRef.current && !modalRef.current.contains(e.target)) {
31+
onClose?.();
32+
}
33+
}
34+
35+
document.addEventListener('click', handleClick, false);
36+
37+
return () => {
38+
document.removeEventListener('click', handleClick, false);
39+
};
40+
}, [onClose, modalRef]);
41+
42+
useKeyDownHandlers({ escape: onClose });
43+
44+
return modalRef;
45+
}

client/components/Dropdown.jsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import styled from 'styled-components';
44
import { remSize, prop } from '../theme';
55
import IconButton from '../common/IconButton';
66

7-
const DropdownWrapper = styled.ul`
7+
export const DropdownWrapper = styled.ul`
88
background-color: ${prop('Modal.background')};
99
border: 1px solid ${prop('Modal.border')};
1010
box-shadow: 0 0 18px 0 ${prop('shadowColor')};
@@ -52,6 +52,7 @@ const DropdownWrapper = styled.ul`
5252
& button span,
5353
& a {
5454
padding: ${remSize(8)} ${remSize(16)};
55+
font-size: ${remSize(12)};
5556
}
5657
5758
* {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import PropTypes from 'prop-types';
2+
import React, { forwardRef, useCallback, useRef, useState } from 'react';
3+
import useModalClose from '../../common/useModalClose';
4+
import DownArrowIcon from '../../images/down-filled-triangle.svg';
5+
import { DropdownWrapper } from '../Dropdown';
6+
7+
// TODO: enable arrow keys to navigate options from list
8+
9+
const DropdownMenu = forwardRef(
10+
(
11+
{ children, anchor, 'aria-label': ariaLabel, align, className, classes },
12+
ref
13+
) => {
14+
// Note: need to use a ref instead of a state to avoid stale closures.
15+
const focusedRef = useRef(false);
16+
17+
const [isOpen, setIsOpen] = useState(false);
18+
19+
const close = useCallback(() => setIsOpen(false), [setIsOpen]);
20+
21+
const anchorRef = useModalClose(close, ref);
22+
23+
const toggle = useCallback(() => {
24+
setIsOpen((prevState) => !prevState);
25+
}, [setIsOpen]);
26+
27+
const handleFocus = () => {
28+
focusedRef.current = true;
29+
};
30+
31+
const handleBlur = () => {
32+
focusedRef.current = false;
33+
setTimeout(() => {
34+
if (!focusedRef.current) {
35+
close();
36+
}
37+
}, 200);
38+
};
39+
40+
return (
41+
<div ref={anchorRef} className={className}>
42+
<button
43+
className={classes.button}
44+
aria-label={ariaLabel}
45+
tabIndex="0"
46+
onClick={toggle}
47+
onBlur={handleBlur}
48+
onFocus={handleFocus}
49+
>
50+
{anchor ?? <DownArrowIcon focusable="false" aria-hidden="true" />}
51+
</button>
52+
{isOpen && (
53+
<DropdownWrapper
54+
className={classes.list}
55+
align={align}
56+
onMouseUp={() => {
57+
setTimeout(close, 0);
58+
}}
59+
onBlur={handleBlur}
60+
onFocus={handleFocus}
61+
>
62+
{children}
63+
</DropdownWrapper>
64+
)}
65+
</div>
66+
);
67+
}
68+
);
69+
70+
DropdownMenu.propTypes = {
71+
/**
72+
* Provide <MenuItem> elements as children to control the contents of the menu.
73+
*/
74+
children: PropTypes.node.isRequired,
75+
/**
76+
* Can optionally override the contents of the button which opens the menu.
77+
* Defaults to <DownArrowIcon>
78+
*/
79+
anchor: PropTypes.node,
80+
'aria-label': PropTypes.string.isRequired,
81+
align: PropTypes.oneOf(['left', 'right']),
82+
className: PropTypes.string,
83+
classes: PropTypes.shape({
84+
button: PropTypes.string,
85+
list: PropTypes.string
86+
})
87+
};
88+
89+
DropdownMenu.defaultProps = {
90+
anchor: null,
91+
align: 'right',
92+
className: '',
93+
classes: {}
94+
};
95+
96+
export default DropdownMenu;
+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import PropTypes from 'prop-types';
2+
import React from 'react';
3+
import ButtonOrLink from '../../common/ButtonOrLink';
4+
5+
// TODO: combine with NavMenuItem
6+
7+
function MenuItem({ hideIf, ...rest }) {
8+
if (hideIf) {
9+
return null;
10+
}
11+
12+
return (
13+
<li>
14+
<ButtonOrLink {...rest} />
15+
</li>
16+
);
17+
}
18+
19+
MenuItem.propTypes = {
20+
...ButtonOrLink.propTypes,
21+
onClick: PropTypes.func,
22+
value: PropTypes.string,
23+
/**
24+
* Provides a way to deal with optional items.
25+
*/
26+
hideIf: PropTypes.bool
27+
};
28+
29+
MenuItem.defaultProps = {
30+
onClick: null,
31+
value: null,
32+
hideIf: false
33+
};
34+
35+
export default MenuItem;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import React from 'react';
2+
import { useMediaQuery } from 'react-responsive';
3+
import styled from 'styled-components';
4+
import { prop, remSize } from '../../theme';
5+
import DropdownMenu from './DropdownMenu';
6+
7+
import DownFilledTriangleIcon from '../../images/down-filled-triangle.svg';
8+
import MoreIconSvg from '../../images/more.svg';
9+
10+
const DotsHorizontal = styled(MoreIconSvg)`
11+
transform: rotate(90deg);
12+
`;
13+
14+
const TableDropdownIcon = () => {
15+
// TODO: centralize breakpoints
16+
const isMobile = useMediaQuery({ maxWidth: 770 });
17+
18+
return isMobile ? (
19+
<DotsHorizontal focusable="false" aria-hidden="true" />
20+
) : (
21+
<DownFilledTriangleIcon focusable="false" aria-hidden="true" />
22+
);
23+
};
24+
25+
const TableDropdown = styled(DropdownMenu).attrs({
26+
align: 'right',
27+
anchor: <TableDropdownIcon />
28+
})`
29+
& > button {
30+
width: ${remSize(25)};
31+
height: ${remSize(25)};
32+
padding: 0;
33+
& svg {
34+
max-width: 100%;
35+
max-height: 100%;
36+
}
37+
& polygon,
38+
& path {
39+
fill: ${prop('inactiveTextColor')};
40+
}
41+
}
42+
& ul {
43+
top: 63%;
44+
right: calc(100% - 26px);
45+
}
46+
`;
47+
48+
export default TableDropdown;

client/components/Nav/NavBar.jsx

+6-28
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,18 @@
11
import PropTypes from 'prop-types';
2-
import React, {
3-
useCallback,
4-
useEffect,
5-
useMemo,
6-
useRef,
7-
useState
8-
} from 'react';
9-
import useKeyDownHandlers from '../../modules/IDE/hooks/useKeyDownHandlers';
2+
import React, { useCallback, useMemo, useRef, useState } from 'react';
3+
import useModalClose from '../../common/useModalClose';
104
import { MenuOpenContext, NavBarContext } from './contexts';
115

126
function NavBar({ children, className }) {
137
const [dropdownOpen, setDropdownOpen] = useState('none');
148

159
const timerRef = useRef(null);
1610

17-
const nodeRef = useRef(null);
11+
const handleClose = useCallback(() => {
12+
setDropdownOpen('none');
13+
}, [setDropdownOpen]);
1814

19-
useEffect(() => {
20-
function handleClick(e) {
21-
if (!nodeRef.current) {
22-
return;
23-
}
24-
if (nodeRef.current.contains(e.target)) {
25-
return;
26-
}
27-
setDropdownOpen('none');
28-
}
29-
document.addEventListener('mousedown', handleClick, false);
30-
return () => {
31-
document.removeEventListener('mousedown', handleClick, false);
32-
};
33-
}, [nodeRef, setDropdownOpen]);
34-
35-
useKeyDownHandlers({
36-
escape: () => setDropdownOpen('none')
37-
});
15+
const nodeRef = useModalClose(handleClose);
3816

3917
const clearHideTimeout = useCallback(() => {
4018
if (timerRef.current) {

client/i18n.js

+4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { initReactI18next } from 'react-i18next';
33
import Backend from 'i18next-http-backend';
44

55
import {
6+
be,
67
enUS,
78
es,
89
ja,
@@ -22,6 +23,7 @@ import {
2223
const fallbackLng = ['en-US'];
2324

2425
export const availableLanguages = [
26+
'be',
2527
'de',
2628
'en-US',
2729
'es-419',
@@ -40,6 +42,7 @@ export const availableLanguages = [
4042

4143
export function languageKeyToLabel(lang) {
4244
const languageMap = {
45+
be: 'Bengali',
4346
de: 'Deutsch',
4447
'en-US': 'English',
4548
'es-419': 'Español',
@@ -60,6 +63,7 @@ export function languageKeyToLabel(lang) {
6063

6164
export function languageKeyToDateLocale(lang) {
6265
const languageMap = {
66+
be,
6367
de,
6468
'en-US': enUS,
6569
'es-419': es,

0 commit comments

Comments
 (0)