diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 3c709e3abf..07b62f8c5a 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -19,6 +19,11 @@ "cannot_invite_non_user": "", "cannot_invite_self": "", "cannot_verify_user_not_robot": "", + "category_arrows": "", + "category_greek": "", + "category_misc": "", + "category_operators": "", + "category_relations": "", "change_or_cancel-cancel": "", "change_or_cancel-change": "", "change_or_cancel-or": "", @@ -185,6 +190,7 @@ "no_new_commits_in_github": "", "no_other_projects_found": "", "no_preview_available": "", + "no_symbols_found": "", "normal": "", "off": "", "ok": "", @@ -240,6 +246,7 @@ "revoke": "", "revoke_invite": "", "run_syntax_check_now": "", + "search": "", "select_a_file": "", "select_a_project": "", "select_an_output_file": "", diff --git a/services/web/frontend/fonts/STIXTwoMath/LICENSE.txt b/services/web/frontend/fonts/STIXTwoMath/LICENSE.txt new file mode 100644 index 0000000000..11b7b8e889 --- /dev/null +++ b/services/web/frontend/fonts/STIXTwoMath/LICENSE.txt @@ -0,0 +1,92 @@ +Copyright 2001-2021 The STIX Fonts Project Authors (https://github.com/stipub/stixfonts), with Reserved Font Name "TM Math". STIX Fonts™ is a trademark of The Institute of Electrical and Electronics Engineers, Inc. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/services/web/frontend/fonts/STIXTwoMath/STIXTwoMath-Regular.woff2 b/services/web/frontend/fonts/STIXTwoMath/STIXTwoMath-Regular.woff2 new file mode 100644 index 0000000000..76f8db85a4 Binary files /dev/null and b/services/web/frontend/fonts/STIXTwoMath/STIXTwoMath-Regular.woff2 differ diff --git a/services/web/frontend/fonts/stix-two-math.css b/services/web/frontend/fonts/stix-two-math.css new file mode 100644 index 0000000000..4fd5d2840c --- /dev/null +++ b/services/web/frontend/fonts/stix-two-math.css @@ -0,0 +1,5 @@ +@font-face { + font-family: "Stix Two Math"; + src: url("./STIXTwoMath/STIXTwoMath-Regular.woff2") + format("woff2"); +} diff --git a/services/web/frontend/js/features/symbol-palette/components/symbol-palette-body.js b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-body.js new file mode 100644 index 0000000000..d6a8908354 --- /dev/null +++ b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-body.js @@ -0,0 +1,53 @@ +import React from 'react' +import { TabPanels, TabPanel } from '@reach/tabs' +import { useTranslation } from 'react-i18next' +import PropTypes from 'prop-types' +import SymbolPaletteItems from './symbol-palette-items' + +export default function SymbolPaletteBody({ + categories, + categorisedSymbols, + filteredSymbols, + handleSelect, + focusInput, +}) { + const { t } = useTranslation() + + // not searching: show the symbols grouped by category + if (!filteredSymbols) { + return ( + + {categories.map(category => ( + + + + ))} + + ) + } + + // searching with no matches: show a message + if (!filteredSymbols.length) { + return
{t('no_symbols_found')}
+ } + + // searching with matches: show the matched symbols + return ( + + ) +} +SymbolPaletteBody.propTypes = { + categories: PropTypes.arrayOf(PropTypes.object).isRequired, + categorisedSymbols: PropTypes.object, + filteredSymbols: PropTypes.arrayOf(PropTypes.object), + handleSelect: PropTypes.func.isRequired, + focusInput: PropTypes.func.isRequired, +} diff --git a/services/web/frontend/js/features/symbol-palette/components/symbol-palette-content.js b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-content.js new file mode 100644 index 0000000000..127026e1b8 --- /dev/null +++ b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-content.js @@ -0,0 +1,82 @@ +import { Tabs } from '@reach/tabs' +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import PropTypes from 'prop-types' +import { matchSorter } from 'match-sorter' + +import symbols from '../data/symbols.json' +import { buildCategorisedSymbols, createCategories } from '../utils/categories' +import SymbolPaletteSearch from './symbol-palette-search' + +import '@reach/tabs/styles.css' +import SymbolPaletteBody from './symbol-palette-body' +import SymbolPaletteTabs from './symbol-palette-tabs' + +export default function SymbolPaletteContent({ handleSelect }) { + const [input, setInput] = useState('') + + const { t } = useTranslation() + + // build the list of categories with translated labels + const categories = useMemo(() => createCategories(t), [t]) + + // group the symbols by category + const categorisedSymbols = useMemo( + () => buildCategorisedSymbols(categories), + [categories] + ) + + // select symbols which match the input + const filteredSymbols = useMemo(() => { + if (input === '') { + return null + } + + return matchSorter(symbols, input, { + keys: ['command', 'description'], + threshold: matchSorter.rankings.CONTAINS, + }) + }, [input]) + + const inputRef = useRef(null) + + // allow the input to be focused + const focusInput = useCallback(() => { + if (inputRef.current) { + inputRef.current.focus() + } + }, []) + + // focus the input when the symbol palette is opened + useEffect(() => { + if (inputRef.current) { + inputRef.current.focus() + } + }, []) + + return ( + +
+
+ 0} + /> + +
+
+ +
+
+
+ ) +} +SymbolPaletteContent.propTypes = { + handleSelect: PropTypes.func.isRequired, +} diff --git a/services/web/frontend/js/features/symbol-palette/components/symbol-palette-item.js b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-item.js new file mode 100644 index 0000000000..9ffaf1db63 --- /dev/null +++ b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-item.js @@ -0,0 +1,68 @@ +import React, { useEffect, useRef } from 'react' +import { OverlayTrigger, Tooltip } from 'react-bootstrap' +import PropTypes from 'prop-types' + +export default function SymbolPaletteItem({ + focused, + handleSelect, + handleKeyDown, + symbol, +}) { + const buttonRef = useRef(null) + + // call focus() on this item when appropriate + useEffect(() => { + if ( + focused && + buttonRef.current && + document.activeElement?.closest('.symbol-palette-items') + ) { + buttonRef.current.focus() + } + }, [focused]) + + return ( + +
+ {symbol.description} +
+ + {symbol.notes && ( +
{symbol.notes}
+ )} + + } + > + +
+ ) +} +SymbolPaletteItem.propTypes = { + symbol: PropTypes.shape({ + codepoint: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, + command: PropTypes.string.isRequired, + character: PropTypes.string.isRequired, + notes: PropTypes.string, + }), + handleKeyDown: PropTypes.func.isRequired, + handleSelect: PropTypes.func.isRequired, + focused: PropTypes.bool, +} diff --git a/services/web/frontend/js/features/symbol-palette/components/symbol-palette-items.js b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-items.js new file mode 100644 index 0000000000..bb65da71db --- /dev/null +++ b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-items.js @@ -0,0 +1,86 @@ +import React, { useCallback, useEffect, useState } from 'react' +import PropTypes from 'prop-types' +import SymbolPaletteItem from './symbol-palette-item' + +export default function SymbolPaletteItems({ + items, + handleSelect, + focusInput, +}) { + const [focusedIndex, setFocusedIndex] = useState(0) + + // reset the focused item when the list of items changes + useEffect(() => { + setFocusedIndex(0) + }, [items]) + + // navigate through items with left and right arrows + const handleKeyDown = useCallback( + event => { + if (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) { + return + } + + switch (event.key) { + // focus previous item + case 'ArrowLeft': + case 'ArrowUp': + setFocusedIndex(index => (index > 0 ? index - 1 : items.length - 1)) + break + + // focus next item + case 'ArrowRight': + case 'ArrowDown': + setFocusedIndex(index => (index < items.length - 1 ? index + 1 : 0)) + break + + // focus first item + case 'Home': + setFocusedIndex(0) + break + + // focus last item + case 'End': + setFocusedIndex(items.length - 1) + break + + // allow the default action + case 'Enter': + case ' ': + break + + // any other key returns focus to the input + default: + focusInput() + break + } + }, + [focusInput, items.length] + ) + + return ( +
+ {items.map((symbol, index) => ( + { + handleSelect(symbol) + setFocusedIndex(index) + }} + handleKeyDown={handleKeyDown} + focused={index === focusedIndex} + /> + ))} +
+ ) +} +SymbolPaletteItems.propTypes = { + items: PropTypes.arrayOf( + PropTypes.shape({ + codepoint: PropTypes.string.isRequired, + }) + ).isRequired, + handleSelect: PropTypes.func.isRequired, + focusInput: PropTypes.func.isRequired, +} diff --git a/services/web/frontend/js/features/symbol-palette/components/symbol-palette-search.js b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-search.js new file mode 100644 index 0000000000..140abc476c --- /dev/null +++ b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-search.js @@ -0,0 +1,36 @@ +import React, { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import PropTypes from 'prop-types' +import useDebounce from '../hooks/use-debounce' + +export default function SymbolPaletteSearch({ setInput, inputRef }) { + const [localInput, setLocalInput] = useState('') + + // debounce the search input until a typing delay + const debouncedLocalInput = useDebounce(localInput, 250) + + useEffect(() => { + setInput(debouncedLocalInput) + }, [debouncedLocalInput, setInput]) + + const { t } = useTranslation() + + return ( + { + setLocalInput(event.target.value) + }} + /> + ) +} +SymbolPaletteSearch.propTypes = { + setInput: PropTypes.func.isRequired, + inputRef: PropTypes.object.isRequired, +} diff --git a/services/web/frontend/js/features/symbol-palette/components/symbol-palette-tabs.js b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-tabs.js new file mode 100644 index 0000000000..a060ff8e79 --- /dev/null +++ b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-tabs.js @@ -0,0 +1,24 @@ +import React from 'react' +import { TabList, Tab } from '@reach/tabs' +import PropTypes from 'prop-types' + +export default function SymbolPaletteTabs({ categories, disabled }) { + return ( + + {categories.map(category => ( + + {category.label} + + ))} + + ) +} +SymbolPaletteTabs.propTypes = { + categories: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + }) + ).isRequired, + disabled: PropTypes.bool, +} diff --git a/services/web/frontend/js/features/symbol-palette/components/symbol-palette.js b/services/web/frontend/js/features/symbol-palette/components/symbol-palette.js new file mode 100644 index 0000000000..cf9da84f13 --- /dev/null +++ b/services/web/frontend/js/features/symbol-palette/components/symbol-palette.js @@ -0,0 +1,15 @@ +import React from 'react' +import PropTypes from 'prop-types' +import SymbolPaletteContent from './symbol-palette-content' + +export default function SymbolPalette({ show, handleSelect }) { + if (!show) { + return null + } + + return +} +SymbolPalette.propTypes = { + show: PropTypes.bool, + handleSelect: PropTypes.func.isRequired, +} diff --git a/services/web/frontend/js/features/symbol-palette/data/symbols.json b/services/web/frontend/js/features/symbol-palette/data/symbols.json new file mode 100644 index 0000000000..ac32904147 --- /dev/null +++ b/services/web/frontend/js/features/symbol-palette/data/symbols.json @@ -0,0 +1,821 @@ +[ + { + "category": "Greek", + "command": "\\alpha", + "codepoint": "U+1D6FC", + "description": "lowercase Greek letter alpha", + "notes": "" + }, + { + "category": "Greek", + "command": "\\beta", + "codepoint": "U+1D6FD", + "description": "lowercase Greek letter beta", + "notes": "" + }, + { + "category": "Greek", + "command": "\\gamma", + "codepoint": "U+1D6FE", + "description": "lowercase Greek letter gamma", + "notes": "" + }, + { + "category": "Greek", + "command": "\\delta", + "codepoint": "U+1D6FF", + "description": "lowercase Greek letter delta", + "notes": "" + }, + { + "category": "Greek", + "command": "\\varepsilon", + "codepoint": "U+1D700", + "description": "lowercase Greek letter epsilon, varepsilon", + "notes": "" + }, + { + "category": "Greek", + "command": "\\epsilon", + "codepoint": "U+1D716", + "description": "lowercase Greek letter epsilon lunate", + "notes": "" + }, + { + "category": "Greek", + "command": "\\zeta", + "codepoint": "U+1D701", + "description": "lowercase Greek letter zeta", + "notes": "" + }, + { + "category": "Greek", + "command": "\\eta", + "codepoint": "U+1D702", + "description": "lowercase Greek letter eta", + "notes": "" + }, + { + "category": "Greek", + "command": "\\vartheta", + "codepoint": "U+1D717", + "description": "lowercase Greek letter curly theta, vartheta", + "notes": "" + }, + { + "category": "Greek", + "command": "\\theta", + "codepoint": "U+1D703", + "description": "lowercase Greek letter theta", + "notes": "" + }, + { + "category": "Greek", + "command": "\\iota", + "codepoint": "U+1D704", + "description": "lowercase Greek letter iota", + "notes": "" + }, + { + "category": "Greek", + "command": "\\kappa", + "codepoint": "U+1D705", + "description": "lowercase Greek letter kappa", + "notes": "" + }, + { + "category": "Greek", + "command": "\\lambda", + "codepoint": "U+1D706", + "description": "lowercase Greek letter lambda", + "notes": "" + }, + { + "category": "Greek", + "command": "\\mu", + "codepoint": "U+1D707", + "description": "lowercase Greek letter mu", + "notes": "" + }, + { + "category": "Greek", + "command": "\\nu", + "codepoint": "U+1D708", + "description": "lowercase Greek letter nu", + "notes": "" + }, + { + "category": "Greek", + "command": "\\xi", + "codepoint": "U+1D709", + "description": "lowercase Greek letter xi", + "notes": "" + }, + { + "category": "Greek", + "command": "\\pi", + "codepoint": "U+1D70B", + "description": "lowercase Greek letter pi", + "notes": "" + }, + { + "category": "Greek", + "command": "\\varrho", + "codepoint": "U+1D71A", + "description": "lowercase Greek letter varrho", + "notes": "" + }, + { + "category": "Greek", + "command": "\\rho", + "codepoint": "U+1D70C", + "description": "lowercase Greek letter rho", + "notes": "" + }, + { + "category": "Greek", + "command": "\\sigma", + "codepoint": "U+1D70E", + "description": "lowercase Greek letter sigma", + "notes": "" + }, + { + "category": "Greek", + "command": "\\varsigma", + "codepoint": "U+1D70D", + "description": "lowercase Greek letter final sigma, varsigma", + "notes": "" + }, + { + "category": "Greek", + "command": "\\tau", + "codepoint": "U+1D70F", + "description": "lowercase Greek letter tau", + "notes": "" + }, + { + "category": "Greek", + "command": "\\upsilon", + "codepoint": "U+1D710", + "description": "lowercase Greek letter upsilon", + "notes": "" + }, + { + "category": "Greek", + "command": "\\phi", + "codepoint": "U+1D719", + "description": "lowercase Greek letter phi", + "notes": "" + }, + { + "category": "Greek", + "command": "\\varphi", + "codepoint": "U+1D711", + "description": "lowercase Greek letter varphi", + "notes": "" + }, + { + "category": "Greek", + "command": "\\chi", + "codepoint": "U+1D712", + "description": "lowercase Greek letter chi", + "notes": "" + }, + { + "category": "Greek", + "command": "\\psi", + "codepoint": "U+1D713", + "description": "lowercase Greek letter psi", + "notes": "" + }, + { + "category": "Greek", + "command": "\\omega", + "codepoint": "U+1D714", + "description": "lowercase Greek letter omega", + "notes": "" + }, + { + "category": "Greek", + "command": "\\Gamma", + "codepoint": "U+00393", + "description": "uppercase Greek letter Gamma", + "notes": "" + }, + { + "category": "Greek", + "command": "\\Delta", + "codepoint": "U+00394", + "description": "uppercase Greek letter Delta", + "notes": "" + }, + { + "category": "Greek", + "command": "\\Theta", + "codepoint": "U+00398", + "description": "uppercase Greek letter Theta", + "notes": "" + }, + { + "category": "Greek", + "command": "\\Lambda", + "codepoint": "U+0039B", + "description": "uppercase Greek letter Lambda", + "notes": "" + }, + { + "category": "Greek", + "command": "\\Xi", + "codepoint": "U+0039E", + "description": "uppercase Greek letter Xi", + "notes": "" + }, + { + "category": "Greek", + "command": "\\Pi", + "codepoint": "U+003A0", + "description": "uppercase Greek letter Pi", + "notes": "Use \\prod for the product." + }, + { + "category": "Greek", + "command": "\\Sigma", + "codepoint": "U+003A3", + "description": "uppercase Greek letter Sigma", + "notes": "Use \\sum for the sum." + }, + { + "category": "Greek", + "command": "\\Upsilon", + "codepoint": "U+003A5", + "description": "uppercase Greek letter Upsilon", + "notes": "" + }, + { + "category": "Greek", + "command": "\\Phi", + "codepoint": "U+003A6", + "description": "uppercase Greek letter Phi", + "notes": "" + }, + { + "category": "Greek", + "command": "\\Psi", + "codepoint": "U+003A8", + "description": "uppercase Greek letter Psi", + "notes": "" + }, + { + "category": "Greek", + "command": "\\Omega", + "codepoint": "U+003A9", + "description": "uppercase Greek letter Omega", + "notes": "" + }, + { + "category": "Relations", + "command": "\\neq", + "codepoint": "U+02260", + "description": "not equal", + "notes": "" + }, + { + "category": "Relations", + "command": "\\leq", + "codepoint": "U+02264", + "description": "less than or equal", + "notes": "" + }, + { + "category": "Relations", + "command": "\\geq", + "codepoint": "U+02265", + "description": "greater than or equal", + "notes": "" + }, + { + "category": "Relations", + "command": "\\ll", + "codepoint": "U+0226A", + "description": "much less than", + "notes": "" + }, + { + "category": "Relations", + "command": "\\gg", + "codepoint": "U+0226B", + "description": "much greater than", + "notes": "" + }, + { + "category": "Relations", + "command": "\\prec", + "codepoint": "U+0227A", + "description": "precedes", + "notes": "" + }, + { + "category": "Relations", + "command": "\\succ", + "codepoint": "U+0227B", + "description": "succeeds", + "notes": "" + }, + { + "category": "Relations", + "command": "\\in", + "codepoint": "U+02208", + "description": "set membership", + "notes": "" + }, + { + "category": "Relations", + "command": "\\notin", + "codepoint": "U+02209", + "description": "negated set membership", + "notes": "" + }, + { + "category": "Relations", + "command": "\\ni", + "codepoint": "U+0220B", + "description": "contains", + "notes": "" + }, + { + "category": "Relations", + "command": "\\subset", + "codepoint": "U+02282", + "description": "subset", + "notes": "" + }, + { + "category": "Relations", + "command": "\\subseteq", + "codepoint": "U+02286", + "description": "subset or equals", + "notes": "" + }, + { + "category": "Relations", + "command": "\\supset", + "codepoint": "U+02283", + "description": "superset", + "notes": "" + }, + { + "category": "Relations", + "command": "\\simeq", + "codepoint": "U+02243", + "description": "similar", + "notes": "" + }, + { + "category": "Relations", + "command": "\\approx", + "codepoint": "U+02248", + "description": "approximate", + "notes": "" + }, + { + "category": "Relations", + "command": "\\equiv", + "codepoint": "U+02261", + "description": "identical with", + "notes": "" + }, + { + "category": "Relations", + "command": "\\cong", + "codepoint": "U+02245", + "description": "congruent with", + "notes": "" + }, + { + "category": "Relations", + "command": "\\mid", + "codepoint": "U+02223", + "description": "mid, divides, vertical bar, modulus, absolute value", + "notes": "Use \\lvert...\\rvert for the absolute value." + }, + { + "category": "Relations", + "command": "\\nmid", + "codepoint": "U+02224", + "description": "negated mid, not divides", + "notes": "Requires \\usepackage{amssymb}." + }, + { + "category": "Relations", + "command": "\\parallel", + "codepoint": "U+02225", + "description": "parallel, double vertical bar, norm", + "notes": "Use \\lVert...\\rVert for the norm." + }, + { + "category": "Relations", + "command": "\\perp", + "codepoint": "U+027C2", + "description": "perpendicular", + "notes": "" + }, + { + "category": "Operators", + "command": "\\times", + "codepoint": "U+000D7", + "description": "cross product, multiplication", + "notes": "" + }, + { + "category": "Operators", + "command": "\\div", + "codepoint": "U+000F7", + "description": "division", + "notes": "" + }, + { + "category": "Operators", + "command": "\\cap", + "codepoint": "U+02229", + "description": "intersection", + "notes": "" + }, + { + "category": "Operators", + "command": "\\cup", + "codepoint": "U+0222A", + "description": "union", + "notes": "" + }, + { + "category": "Operators", + "command": "\\cdot", + "codepoint": "U+022C5", + "description": "dot product, multiplication", + "notes": "" + }, + { + "category": "Operators", + "command": "\\cdots", + "codepoint": "U+022EF", + "description": "centered dots", + "notes": "" + }, + { + "category": "Operators", + "command": "\\bullet", + "codepoint": "U+02219", + "description": "bullet", + "notes": "" + }, + { + "category": "Operators", + "command": "\\circ", + "codepoint": "U+025E6", + "description": "circle", + "notes": "" + }, + { + "category": "Operators", + "command": "\\wedge", + "codepoint": "U+02227", + "description": "wedge, logical and", + "notes": "" + }, + { + "category": "Operators", + "command": "\\vee", + "codepoint": "U+02228", + "description": "vee, logical or", + "notes": "" + }, + { + "category": "Operators", + "command": "\\setminus", + "codepoint": "U+0005C", + "description": "set minus, backslash", + "notes": "Use \\backslash for a backslash." + }, + { + "category": "Operators", + "command": "\\oplus", + "codepoint": "U+02295", + "description": "plus sign in circle", + "notes": "" + }, + { + "category": "Operators", + "command": "\\otimes", + "codepoint": "U+02297", + "description": "multiply sign in circle", + "notes": "" + }, + { + "category": "Operators", + "command": "\\sum", + "codepoint": "U+02211", + "description": "summation operator", + "notes": "Use \\Sigma for the letter Sigma." + }, + { + "category": "Operators", + "command": "\\prod", + "codepoint": "U+0220F", + "description": "product operator", + "notes": "Use \\Pi for the letter Pi." + }, + { + "category": "Operators", + "command": "\\bigcap", + "codepoint": "U+022C2", + "description": "intersection operator", + "notes": "" + }, + { + "category": "Operators", + "command": "\\bigcup", + "codepoint": "U+022C3", + "description": "union operator", + "notes": "" + }, + { + "category": "Operators", + "command": "\\int", + "codepoint": "U+0222B", + "description": "integral operator", + "notes": "" + }, + { + "category": "Operators", + "command": "\\iint", + "codepoint": "U+0222C", + "description": "double integral operator", + "notes": "Requires \\usepackage{amsmath}." + }, + { + "category": "Operators", + "command": "\\iiint", + "codepoint": "U+0222D", + "description": "triple integral operator", + "notes": "Requires \\usepackage{amsmath}." + }, + { + "category": "Arrows", + "command": "\\leftarrow", + "codepoint": "U+02190", + "description": "leftward arrow", + "notes": "" + }, + { + "category": "Arrows", + "command": "\\rightarrow", + "codepoint": "U+02192", + "description": "rightward arrow", + "notes": "" + }, + { + "category": "Arrows", + "command": "\\leftrightarrow", + "codepoint": "U+02194", + "description": "left and right arrow", + "notes": "" + }, + { + "category": "Arrows", + "command": "\\uparrow", + "codepoint": "U+02191", + "description": "upward arrow", + "notes": "" + }, + { + "category": "Arrows", + "command": "\\downarrow", + "codepoint": "U+02193", + "description": "downward arrow", + "notes": "" + }, + { + "category": "Arrows", + "command": "\\Leftarrow", + "codepoint": "U+021D0", + "description": "is implied by", + "notes": "" + }, + { + "category": "Arrows", + "command": "\\Rightarrow", + "codepoint": "U+021D2", + "description": "implies", + "notes": "" + }, + { + "category": "Arrows", + "command": "\\Leftrightarrow", + "codepoint": "U+021D4", + "description": "left and right double arrow", + "notes": "" + }, + { + "category": "Arrows", + "command": "\\mapsto", + "codepoint": "U+021A6", + "description": "maps to, rightward", + "notes": "" + }, + { + "category": "Arrows", + "command": "\\nearrow", + "codepoint": "U+02197", + "description": "ne pointing arrow", + "notes": "" + }, + { + "category": "Arrows", + "command": "\\searrow", + "codepoint": "U+02198", + "description": "se pointing arrow", + "notes": "" + }, + { + "category": "Arrows", + "command": "\\rightleftharpoons", + "codepoint": "U+021CC", + "description": "right harpoon over left", + "notes": "" + }, + { + "category": "Arrows", + "command": "\\leftharpoonup", + "codepoint": "U+021BC", + "description": "left harpoon up", + "notes": "" + }, + { + "category": "Arrows", + "command": "\\rightharpoonup", + "codepoint": "U+021C0", + "description": "right harpoon up", + "notes": "" + }, + { + "category": "Arrows", + "command": "\\leftharpoondown", + "codepoint": "U+021BD", + "description": "left harpoon down", + "notes": "" + }, + { + "category": "Arrows", + "command": "\\rightharpoondown", + "codepoint": "U+021C1", + "description": "right harpoon down", + "notes": "" + }, + { + "category": "Misc", + "command": "\\infty", + "codepoint": "U+0221E", + "description": "infinity", + "notes": "" + }, + { + "category": "Misc", + "command": "\\partial", + "codepoint": "U+1D715", + "description": "partial differential", + "notes": "" + }, + { + "category": "Misc", + "command": "\\nabla", + "codepoint": "U+02207", + "description": "nabla, del, hamilton operator", + "notes": "" + }, + { + "category": "Misc", + "command": "\\varnothing", + "codepoint": "U+02300", + "description": "empty set", + "notes": "Requires \\usepackage{amssymb}." + }, + { + "category": "Misc", + "command": "\\forall", + "codepoint": "U+02200", + "description": "for all", + "notes": "" + }, + { + "category": "Misc", + "command": "\\exists", + "codepoint": "U+02203", + "description": "there exists", + "notes": "" + }, + { + "category": "Misc", + "command": "\\neg", + "codepoint": "U+000AC", + "description": "not sign", + "notes": "" + }, + { + "category": "Misc", + "command": "\\Re", + "codepoint": "U+0211C", + "description": "real part", + "notes": "" + }, + { + "category": "Misc", + "command": "\\Im", + "codepoint": "U+02111", + "description": "imaginary part", + "notes": "" + }, + { + "category": "Misc", + "command": "\\Box", + "codepoint": "U+025A1", + "description": "square", + "notes": "Requires \\usepackage{amssymb}." + }, + { + "category": "Misc", + "command": "\\triangle", + "codepoint": "U+025B3", + "description": "trangle", + "notes": "" + }, + { + "category": "Misc", + "command": "\\aleph", + "codepoint": "U+02135", + "description": "Hebrew letter aleph", + "notes": "" + }, + { + "category": "Misc", + "command": "\\wp", + "codepoint": "U+02118", + "description": "Weierstrass letter p", + "notes": "" + }, + { + "category": "Misc", + "command": "\\#", + "codepoint": "U+00023", + "description": "number sign, hashtag", + "notes": "" + }, + { + "category": "Misc", + "command": "\\$", + "codepoint": "U+00024", + "description": "dollar sign", + "notes": "" + }, + { + "category": "Misc", + "command": "\\%", + "codepoint": "U+00025", + "description": "percent sign", + "notes": "" + }, + { + "category": "Misc", + "command": "\\&", + "codepoint": "U+00026", + "description": "et sign, and, ampersand", + "notes": "" + }, + { + "category": "Misc", + "command": "\\{", + "codepoint": "U+0007B", + "description": "left curly brace", + "notes": "" + }, + { + "category": "Misc", + "command": "\\}", + "codepoint": "U+0007D", + "description": "right curly brace", + "notes": "" + }, + { + "category": "Misc", + "command": "\\langle", + "codepoint": "U+027E8", + "description": "left angle bracket, bra", + "notes": "" + }, + { + "category": "Misc", + "command": "\\rangle", + "codepoint": "U+027E9", + "description": "right angle bracket, ket", + "notes": "" + } +] diff --git a/services/web/frontend/js/features/symbol-palette/hooks/use-debounce.js b/services/web/frontend/js/features/symbol-palette/hooks/use-debounce.js new file mode 100644 index 0000000000..497117cdfa --- /dev/null +++ b/services/web/frontend/js/features/symbol-palette/hooks/use-debounce.js @@ -0,0 +1,17 @@ +import { useEffect, useState } from 'react' + +export default function useDebounce(value, delay = 0) { + const [debouncedValue, setDebouncedValue] = useState(value) + + useEffect(() => { + const timer = window.setTimeout(() => { + setDebouncedValue(value) + }, delay) + + return () => { + window.clearTimeout(timer) + } + }, [value, delay]) + + return debouncedValue +} diff --git a/services/web/frontend/js/features/symbol-palette/utils/categories.js b/services/web/frontend/js/features/symbol-palette/utils/categories.js new file mode 100644 index 0000000000..383f08cf78 --- /dev/null +++ b/services/web/frontend/js/features/symbol-palette/utils/categories.js @@ -0,0 +1,45 @@ +import symbols from '../data/symbols.json' + +export function createCategories(t) { + return [ + { + id: 'Greek', + label: t('category_greek'), + }, + { + id: 'Arrows', + label: t('category_arrows'), + }, + { + id: 'Operators', + label: t('category_operators'), + }, + { + id: 'Relations', + label: t('category_relations'), + }, + { + id: 'Misc', + label: t('category_misc'), + }, + ] +} + +export function buildCategorisedSymbols(categories) { + const output = {} + + for (const category of categories) { + output[category.id] = [] + } + + for (const item of symbols) { + if (item.category in output) { + item.character = String.fromCodePoint( + parseInt(item.codepoint.replace(/^U\+0*/, ''), 16) + ) + output[item.category].push(item) + } + } + + return output +} diff --git a/services/web/frontend/stories/symbol-palette.stories.js b/services/web/frontend/stories/symbol-palette.stories.js new file mode 100644 index 0000000000..609e050684 --- /dev/null +++ b/services/web/frontend/stories/symbol-palette.stories.js @@ -0,0 +1,18 @@ +import React from 'react' + +import SymbolPalette from '../js/features/symbol-palette/components/symbol-palette' + +export const Interactive = args => { + return +} + +export default { + title: 'Symbol Palette', + component: SymbolPalette, + args: { + show: true, + }, + argTypes: { + handleSelect: { action: 'handleSelect' }, + }, +} diff --git a/services/web/frontend/stylesheets/app/editor.less b/services/web/frontend/stylesheets/app/editor.less index 6168da159a..d6d13b98b5 100644 --- a/services/web/frontend/stylesheets/app/editor.less +++ b/services/web/frontend/stylesheets/app/editor.less @@ -15,6 +15,7 @@ @import './editor/publish-modal.less'; @import './editor/outline.less'; @import './editor/logs.less'; +@import './editor/symbol-palette.less'; @ui-layout-toggler-def-height: 50px; @ui-resizer-size: 7px; diff --git a/services/web/frontend/stylesheets/app/editor/symbol-palette.less b/services/web/frontend/stylesheets/app/editor/symbol-palette.less new file mode 100644 index 0000000000..96c2b80c26 --- /dev/null +++ b/services/web/frontend/stylesheets/app/editor/symbol-palette.less @@ -0,0 +1,75 @@ +.symbol-palette { + display: flex; + flex-direction: column; + background: @symbol-palette-bg; + color: @symbol-palette-color; + max-width: 600px; +} + +.symbol-palette-header { + flex-shrink: 0; + display: flex; + justify-content: space-between; + font-family: @font-family-sans-serif; + font-size: 16px; + background: @symbol-palette-header-background; +} + +.symbol-palette-header [data-reach-tab] { + border-bottom: none; +} + +.symbol-palette-header [data-reach-tab][data-selected] { + background: @symbol-palette-selected-tab-bg; +} + +.symbol-palette-body { + flex: 1; +} + +.symbol-palette-items { + display: flex; + flex-wrap: wrap; + padding: 5px; +} + +.symbol-palette-item { + font-family: 'Stix Two Math', serif; + font-size: 24px; + line-height: 42px; + height: 42px; + width: 42px; + margin: 5px; + color: @symbol-palette-item-color; + background: @symbol-palette-item-bg; + border: 1px solid transparent; + border-radius: 3px; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.symbol-palette-item-command { + font-family: monospace; + font-weight: bold; +} + +.symbol-palette-item-notes { + margin-top: 5px; +} + +.symbol-palette-empty { + display: flex; + align-items: center; + justify-content: center; + padding: 10px; +} + +.symbol-palette-search { + color: @symbol-palette-search-color; + border-radius: 21px; + border: none; + padding: 2px 10px; + margin: 5px; + line-height: 1; +} diff --git a/services/web/frontend/stylesheets/core/ol-light-variables.less b/services/web/frontend/stylesheets/core/ol-light-variables.less index d21e8e3d67..78d3b6bb03 100644 --- a/services/web/frontend/stylesheets/core/ol-light-variables.less +++ b/services/web/frontend/stylesheets/core/ol-light-variables.less @@ -127,3 +127,12 @@ @chat-new-message-textarea-bg: #fff; @chat-new-message-textarea-color: @ol-blue-gray-6; @chat-new-message-border-color: @ol-blue-gray-1; + +// Symbol Palette +@symbol-palette-bg: #fff; +@symbol-palette-color: @ol-blue-gray-3; +@symbol-palette-header-background: #fff; +@symbol-palette-item-bg: @gray-lighter; +@symbol-palette-item-color: @ol-blue-gray-5; +@symbol-palette-search-color: @ol-blue-gray-4; +@symbol-palette-selected-tab-bg: @gray-lighter; diff --git a/services/web/frontend/stylesheets/core/variables.less b/services/web/frontend/stylesheets/core/variables.less index a6a749a349..42d4e2ecce 100644 --- a/services/web/frontend/stylesheets/core/variables.less +++ b/services/web/frontend/stylesheets/core/variables.less @@ -1112,3 +1112,12 @@ // Input suggestions @input-suggestion-v-offset: 4px; + +// Symbol Palette +@symbol-palette-bg: @ol-blue-gray-4; +@symbol-palette-color: #fff; +@symbol-palette-header-background: @ol-blue-gray-5; +@symbol-palette-item-bg: @ol-blue-gray-5; +@symbol-palette-item-color: #fff; +@symbol-palette-search-color: @ol-blue-gray-4; +@symbol-palette-selected-tab-bg: @ol-blue-gray-4; diff --git a/services/web/frontend/stylesheets/style.less b/services/web/frontend/stylesheets/style.less index 0170e18687..453129f9b6 100644 --- a/services/web/frontend/stylesheets/style.less +++ b/services/web/frontend/stylesheets/style.less @@ -1,6 +1,7 @@ @import (less) '../fonts/lato.css'; @import (less) '../fonts/merriweather.css'; @import (less) '../fonts/source-code-pro.css'; +@import (less) '../fonts/stix-two-math.css'; @is-overleaf-light: false; @show-rich-text: true; diff --git a/services/web/locales/en.json b/services/web/locales/en.json index a4ae06ffda..f7443453be 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -1429,6 +1429,13 @@ "hotkey_toggle_review_panel": "Toggle review panel", "hotkey_toggle_track_changes": "Toggle track changes", "hotkey_add_a_comment": "Add a comment", + "category_greek": "Greek", + "category_arrows": "Arrows", + "category_operators": "Operators", + "category_relations": "Relations", + "category_misc": "Misc", + "no_symbols_found": "No symbols found", + "search": "Search", "also": "Also", "add_email": "Add Email" } diff --git a/services/web/package-lock.json b/services/web/package-lock.json index 0f4740abca..c76e68e981 100644 --- a/services/web/package-lock.json +++ b/services/web/package-lock.json @@ -4419,6 +4419,38 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, + "@reach/auto-id": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@reach/auto-id/-/auto-id-0.15.0.tgz", + "integrity": "sha512-iACaCcZeqAhDf4OOwJpmHHS/LaRj9z3Ip8JmlhpCrFWV2YOIiiZk42amlBZX6CKH66Md+eriYZQk3TyAjk6Oxg==", + "requires": { + "@reach/utils": "0.15.0", + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" + } + } + }, + "@reach/descendants": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@reach/descendants/-/descendants-0.15.0.tgz", + "integrity": "sha512-MGs+7sGjx07sm29Wc2d/Ya72qwatNVHofnYwwHMT6LImv99kE5TQMCgkMSDeRwqQxHURmcjb4I7Tab+ahvCGiw==", + "requires": { + "@reach/utils": "0.15.0", + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" + } + } + }, "@reach/router": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/@reach/router/-/router-1.3.4.tgz", @@ -4431,6 +4463,41 @@ "react-lifecycles-compat": "^3.0.4" } }, + "@reach/tabs": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@reach/tabs/-/tabs-0.15.0.tgz", + "integrity": "sha512-W3FZw0DMGChwIRDBZYCw66NdiGkyapYs6xjOqQV2Ph7DMaMQ0lYck5bn40EtVi3O5x+8cAKmV+ubbHVUh+WSig==", + "requires": { + "@reach/auto-id": "0.15.0", + "@reach/descendants": "0.15.0", + "@reach/utils": "0.15.0", + "prop-types": "^15.7.2", + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" + } + } + }, + "@reach/utils": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@reach/utils/-/utils-0.15.0.tgz", + "integrity": "sha512-JHHN7T5ucFiuQbqkgv8ECbRWKfRiJxrO/xHR3fHf+f2C7mVs/KkJHhYtovS1iEapR4silygX9PY0+QUmHPOTYw==", + "requires": { + "tiny-warning": "^1.0.3", + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" + } + } + }, "@react-dnd/asap": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.0.tgz", @@ -18220,7 +18287,7 @@ "functional-red-black-tree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", "dev": true }, "functions-have-names": { @@ -23121,7 +23188,7 @@ "microtime-nodejs": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/microtime-nodejs/-/microtime-nodejs-1.0.0.tgz", - "integrity": "sha1-iFlASvLipGKhXJzWvyxORo2r2+g=" + "integrity": "sha512-SthP/4JW6HUIZfgM0nadNtwKm/WMH0+z1i4RsPDnud+UasjoABzSkCk3eMhIRzipgwPhkdAYpTI69X4II4j1pA==" }, "miller-rabin": { "version": "4.0.1", @@ -24128,7 +24195,7 @@ "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, "ncp": { @@ -29783,7 +29850,7 @@ "require-like": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz", - "integrity": "sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A==", + "integrity": "sha1-rW8wwTvs15cBDEaK+ndcDAprR/o=", "dev": true }, "require-main-filename": { @@ -33480,6 +33547,11 @@ "dev": true, "optional": true }, + "tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, "tlds": { "version": "1.210.0", "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.210.0.tgz", diff --git a/services/web/package.json b/services/web/package.json index b5e7a3f13c..56922a3ac1 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -54,6 +54,7 @@ "@pollyjs/adapter-node-http": "^4.2.1", "@pollyjs/core": "^4.2.1", "@pollyjs/persister-fs": "^4.2.1", + "@reach/tabs": "^0.15.0", "@sentry/browser": "^6.3.5", "@uppy/core": "^1.15.0", "@uppy/dashboard": "^1.11.0", diff --git a/services/web/test/frontend/features/symbol-palette/components/symbol-palette.test.js b/services/web/test/frontend/features/symbol-palette/components/symbol-palette.test.js new file mode 100644 index 0000000000..af662f5b5d --- /dev/null +++ b/services/web/test/frontend/features/symbol-palette/components/symbol-palette.test.js @@ -0,0 +1,102 @@ +import { expect } from 'chai' +import sinon from 'sinon' +import React from 'react' +import { screen, render, fireEvent, waitFor } from '@testing-library/react' +import SymbolPalette from '../../../../../frontend/js/features/symbol-palette/components/symbol-palette' + +// eslint-disable-next-line mocha/no-skipped-tests +describe.skip('symbol palette', function () { + // let clock + // + // beforeEach(function () { + // clock = sinon.useFakeTimers({ + // toFake: ['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval'], + // }) + // }) + // + // afterEach(function () { + // clock.restore() + // }) + + it('handles keyboard interaction', async function () { + this.timeout(10000) + + const handleSelect = sinon.stub() + + const { container } = render( + + ) + + // check the number of tabs + const tabs = await screen.findAllByRole('tab') + expect(tabs).to.have.length(5) + + let selectedTab + let symbols + + // the first tab should be selected + selectedTab = await screen.getByRole('tab', { selected: true }) + expect(selectedTab.textContent).to.equal('Greek') + symbols = await screen.findAllByRole('option') + expect(symbols).to.have.length(39) + + // click to select the third tab + tabs[2].click() + selectedTab = await screen.getByRole('tab', { selected: true }) + expect(selectedTab.textContent).to.equal('Operators') + symbols = await screen.findAllByRole('option') + expect(symbols).to.have.length(20) + + // press the left arrow to select the second tab + fireEvent.keyDown(selectedTab, { key: 'ArrowLeft' }) + selectedTab = await screen.getByRole('tab', { selected: true }) + expect(selectedTab.textContent).to.equal('Arrows') + symbols = await screen.findAllByRole('option') + expect(symbols).to.have.length(16) + + // select the search input + const input = await screen.getByRole('searchbox') + input.click() + + // type in the search input + fireEvent.change(input, { target: { value: 'pi' } }) + + // make sure all scheduled microtasks have executed + // clock.runAll() + + // wait for the symbols to be filtered + await waitFor(async () => { + symbols = await screen.findAllByRole('option') + console.log(symbols.length) + expect(symbols).to.have.length(2) + }) + + // press Tab to select the symbols + fireEvent.keyDown(container, { key: 'Tab' }) + + // get the selected symbol + let selectedSymbol + + selectedSymbol = await screen.getByRole('option', { selected: true }) + expect(selectedSymbol.textContent).to.equal('𝜋') + + // move to the next symbol + fireEvent.keyDown(selectedSymbol, { key: 'ArrowRight' }) + + // wait for the symbol to be selected + selectedSymbol = await screen.getByRole('option', { selected: true }) + expect(selectedSymbol.textContent).to.equal('Π') + + // click on the selected symbol + selectedSymbol.click() + + expect(handleSelect).to.have.been.calledWith({ + category: 'Greek', + character: 'Π', + codepoint: 'U+003A0', + command: '\\Pi', + description: 'uppercase Greek letter Pi', + notes: 'Use \\prod for the product.', + }) + }) +})