Move symbol palette to module and add overlay (#5396)

GitOrigin-RevId: 649dd56aaecd60662bd2bf534bda323ee541874f
This commit is contained in:
Alf Eaton
2021-11-10 10:50:01 +00:00
committed by Copybot
parent 89f21a9b93
commit 0aa9678a26
31 changed files with 245 additions and 1572 deletions

View File

@@ -88,7 +88,7 @@ block content
ng-class="{ 'ide-history-open' : (ui.view == 'history' && history.isV2) }",
layout="main",
ng-hide="state.loading",
resize-on="layout:chat:resize,history:toggle,layout:flat-screen:toggle",
resize-on="layout:chat:resize,history:toggle,layout:flat-screen:toggle,symbol-palette-toggled",
minimum-restore-size-west="130"
custom-toggler-pane=hasFeature('custom-togglers') ? "west" : false
custom-toggler-msg-when-open=hasFeature('custom-togglers') ? translate("tooltip_hide_filetree") : false

View File

@@ -1,44 +0,0 @@
.ui-layout-center(
ng-controller="ReviewPanelController",
ng-class="{\
'rp-unsupported': editor.showRichText,\
'rp-state-current-file': (reviewPanel.subView === SubViews.CUR_FILE),\
'rp-state-current-file-expanded': (reviewPanel.subView === SubViews.CUR_FILE && ui.reviewPanelOpen),\
'rp-state-current-file-mini': (reviewPanel.subView === SubViews.CUR_FILE && !ui.reviewPanelOpen),\
'rp-state-overview': (reviewPanel.subView === SubViews.OVERVIEW),\
'rp-size-mini': ui.miniReviewPanelVisible,\
'rp-size-expanded': ui.reviewPanelOpen,\
'rp-layout-left': reviewPanel.layoutToLeft,\
'rp-loading-threads': reviewPanel.loadingThreads,\
}"
)
.loading-panel(
ng-show="(!editor.sharejs_doc || editor.opening) && !editor.error_state",
style=showRichText ? "top: 32px" : "",
)
span(ng-show="editor.open_doc_id")
i.fa.fa-spin.fa-refresh
|   #{translate("loading")}…
span(ng-show="!editor.open_doc_id")
i.fa.fa-arrow-left
|   #{translate("open_a_file_on_the_left")}
if moduleIncludesAvailable('editor:main')
!= moduleIncludes('editor:main', locals)
else
.toolbar.toolbar-editor
.multi-selection-ongoing(
ng-show="multiSelectedCount > 0"
)
.multi-selection-message
h4 {{ multiSelectedCount }} #{translate('files_selected')}
if showNewSourceEditor
if moduleIncludesAvailable('editor:source-editor')
!= moduleIncludes('editor:source-editor', locals)
else
include ./source-editor
if !isRestrictedTokenMember
include ./review-panel

View File

@@ -17,10 +17,10 @@
vertical-resizable-panes="symbol-palette-resizer"
vertical-resizable-panes-hidden-externally-on="symbol-palette-toggled"
vertical-resizable-panes-hidden-initially="true"
vertical-resizable-panes-default-size="196"
vertical-resizable-panes-min-size="144"
vertical-resizable-panes-default-size="250"
vertical-resizable-panes-min-size="250"
vertical-resizable-panes-max-size="336"
vertical-resizable-panes-resize-on="layout:flat-screen:toggle"
vertical-resizable-panes-resize-on="layout:flat-screen:toggle,symbol-palette-toggled"
)
.div(vertical-resizable-top)
@@ -55,5 +55,7 @@
if !isRestrictedTokenMember
include ./review-panel
.div(vertical-resizable-bottom)
include ./symbol-palette
if moduleIncludesAvailable('editor:symbol-palette')
.div(vertical-resizable-bottom)
!= moduleIncludes('editor:symbol-palette', locals)

View File

@@ -12,10 +12,7 @@ div.full-size(
custom-toggler-msg-when-open=hasFeature('custom-togglers') ? translate("tooltip_hide_pdf") : false
custom-toggler-msg-when-closed=hasFeature('custom-togglers') ? translate("tooltip_show_pdf") : false
)
if showSymbolPalette
include ./editor-with-symbol-palette
else
include ./editor-no-symbol-palette
include ./editor-pane
.ui-layout-east
div(ng-if="ui.pdfLayout == 'sideBySide'")

View File

@@ -1,2 +0,0 @@
if showSymbolPalette
symbol-palette(show="editor.showSymbolPalette" handle-select="editor.insertSymbol")

View File

@@ -737,6 +737,7 @@ module.exports = {
tprLinkedFileInfo: [],
tprLinkedFileRefreshError: [],
contactUsModal: [],
editorToolbarButtons: [],
},
moduleImportSequence: ['launchpad', 'server-ce-scripts', 'user-activate'],

View File

@@ -90,8 +90,8 @@
"file_name_in_this_project": "",
"file_outline": "",
"files_cannot_include_invalid_characters": "",
"find_out_more_about_latex_symbols": "",
"find_out_more_about_the_file_outline": "",
"find_the_symbols_you_need_with_premium": "",
"first_error_popup_label": "",
"following_paths_conflict": "",
"free_accounts_have_timeout_upgrade_to_increase": "",
@@ -170,6 +170,7 @@
"layout": "",
"learn_how_to_make_documents_compile_quickly": "",
"learn_more_about_link_sharing": "",
"learn_more_about_the_symbol_palette": "",
"link_sharing_is_off": "",
"link_sharing_is_on": "",
"link_to_github": "",
@@ -345,6 +346,7 @@
"too_recently_compiled": "",
"total_words": "",
"try_it_for_free": "",
"try_premium_for_free": "",
"try_recompile_project": "",
"try_refresh_page": "",
"turn_off_link_sharing": "",
@@ -367,6 +369,7 @@
"view_warning": "",
"view_warning_plural": "",
"we_cant_find_any_sections_or_subsections_in_this_file": "",
"with_premium_subscription_you_also_get": "",
"word_count": "",
"work_offline": "",
"work_with_non_overleaf_users": "",

View File

@@ -1,9 +1,9 @@
import { useTranslation } from 'react-i18next'
import { useEditorContext } from '../../../shared/context/editor-context'
import Icon from '../../../shared/components/icon'
import StartFreeTrialButton from '../../../shared/components/start-free-trial-button'
import { memo } from 'react'
import PdfLogEntry from './pdf-log-entry'
import UpgradeBenefits from '../../../shared/components/upgrade-benefits'
function TimeoutUpgradePrompt() {
const { t } = useTranslation()
@@ -26,38 +26,7 @@ function TimeoutUpgradePrompt() {
<p>{t('free_accounts_have_timeout_upgrade_to_increase')}</p>
<p>{t('plus_upgraded_accounts_receive')}:</p>
<div>
<ul className="list-unstyled">
<li>
<Icon type="check" />
&nbsp;
{t('unlimited_projects')}
</li>
<li>
<Icon type="check" />
&nbsp;
{t('collabs_per_proj', { collabcount: 'Multiple' })}
</li>
<li>
<Icon type="check" />
&nbsp;
{t('full_doc_history')}
</li>
<li>
<Icon type="check" />
&nbsp;
{t('sync_to_dropbox')}
</li>
<li>
<Icon type="check" />
&nbsp;
{t('sync_to_github')}
</li>
<li>
<Icon type="check" />
&nbsp;
{t('compile_larger_projects')}
</li>
</ul>
<UpgradeBenefits />
</div>
{isProjectOwner && (
<p className="text-center">

View File

@@ -1,61 +0,0 @@
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()
// searching with matches: show the matched symbols
// searching with no matches: show a message
// note: include empty tab panels so that aria-controls on tabs can still reference the panel ids
if (filteredSymbols) {
return (
<>
{filteredSymbols.length ? (
<SymbolPaletteItems
items={filteredSymbols}
handleSelect={handleSelect}
focusInput={focusInput}
/>
) : (
<div className="symbol-palette-empty">{t('no_symbols_found')}</div>
)}
<TabPanels>
{categories.map(category => (
<TabPanel key={category.id} tabIndex={-1} />
))}
</TabPanels>
</>
)
}
// not searching: show the symbols grouped by category
return (
<TabPanels>
{categories.map(category => (
<TabPanel key={category.id} tabIndex={-1}>
<SymbolPaletteItems
items={categorisedSymbols[category.id]}
handleSelect={handleSelect}
focusInput={focusInput}
/>
</TabPanel>
))}
</TabPanels>
)
}
SymbolPaletteBody.propTypes = {
categories: PropTypes.arrayOf(PropTypes.object).isRequired,
categorisedSymbols: PropTypes.object,
filteredSymbols: PropTypes.arrayOf(PropTypes.object),
handleSelect: PropTypes.func.isRequired,
focusInput: PropTypes.func.isRequired,
}

View File

@@ -1,103 +0,0 @@
import { Tabs } from '@reach/tabs'
import { 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 SymbolPaletteBody from './symbol-palette-body'
import SymbolPaletteTabs from './symbol-palette-tabs'
// import SymbolPaletteInfoLink from './symbol-palette-info-link'
import BetaBadge from '../../../shared/components/beta-badge'
import '@reach/tabs/styles.css'
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
}
const words = input.trim().split(/\s+/)
return words.reduceRight(
(symbols, word) =>
matchSorter(symbols, word, {
keys: ['command', 'description', 'character', 'aliases'],
threshold: matchSorter.rankings.CONTAINS,
}),
symbols
)
}, [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 (
<Tabs className="symbol-palette-container">
<div className="symbol-palette">
<div className="symbol-palette-header">
<SymbolPaletteTabs
categories={categories}
disabled={input.length > 0}
/>
<div className="symbol-palette-header-group">
<BetaBadge
tooltip={{
id: 'tooltip-symbol-palette-beta',
text:
'The Symbol Palette is a beta feature. Click here to give feedback.',
placement: 'top',
}}
url="https://forms.gle/BybHV5svGE8rJ6Ki9"
/>
{/* NOTE: replace the beta badge with this info link when rolling out to all users */}
{/* <SymbolPaletteInfoLink /> */}
<SymbolPaletteSearch setInput={setInput} inputRef={inputRef} />
</div>
</div>
<div className="symbol-palette-body">
<SymbolPaletteBody
categories={categories}
categorisedSymbols={categorisedSymbols}
filteredSymbols={filteredSymbols}
handleSelect={handleSelect}
focusInput={focusInput}
/>
</div>
</div>
</Tabs>
)
}
SymbolPaletteContent.propTypes = {
handleSelect: PropTypes.func.isRequired,
}

View File

@@ -1,29 +0,0 @@
import { Button, OverlayTrigger, Tooltip } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
export default function SymbolPaletteInfoLink() {
const { t } = useTranslation()
return (
<OverlayTrigger
placement="top"
trigger={['hover', 'focus']}
overlay={
<Tooltip id="tooltip-symbol-palette-info">
{t('find_out_more_about_latex_symbols')}
</Tooltip>
}
>
<Button
bsStyle="link"
bsSize="small"
className="symbol-palette-info-link"
href="https://www.overleaf.com/learn/latex/List_of_Greek_letters_and_math_symbols"
target="_blank"
rel="noopener noreferrer"
>
<span className="info-badge" />
</Button>
</OverlayTrigger>
)
}

View File

@@ -1,67 +0,0 @@
import { 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 (
<OverlayTrigger
placement="top"
trigger={['hover', 'focus']}
overlay={
<Tooltip id={`tooltip-symbol-${symbol.codepoint}`}>
<div className="symbol-palette-item-description">
{symbol.description}
</div>
<div className="symbol-palette-item-command">{symbol.command}</div>
{symbol.notes && (
<div className="symbol-palette-item-notes">{symbol.notes}</div>
)}
</Tooltip>
}
>
<button
key={symbol.codepoint}
className="symbol-palette-item"
onClick={() => handleSelect(symbol)}
onKeyDown={handleKeyDown}
tabIndex={focused ? 0 : -1}
ref={buttonRef}
role="option"
aria-label={symbol.description}
aria-selected={focused ? 'true' : 'false'}
>
{symbol.character}
</button>
</OverlayTrigger>
)
}
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,
}

View File

@@ -1,86 +0,0 @@
import { 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 (
<div className="symbol-palette-items" role="listbox" aria-label="Symbols">
{items.map((symbol, index) => (
<SymbolPaletteItem
key={symbol.codepoint}
symbol={symbol}
handleSelect={symbol => {
handleSelect(symbol)
setFocusedIndex(index)
}}
handleKeyDown={handleKeyDown}
focused={index === focusedIndex}
/>
))}
</div>
)
}
SymbolPaletteItems.propTypes = {
items: PropTypes.arrayOf(
PropTypes.shape({
codepoint: PropTypes.string.isRequired,
})
).isRequired,
handleSelect: PropTypes.func.isRequired,
focusInput: PropTypes.func.isRequired,
}

View File

@@ -1,44 +0,0 @@
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import PropTypes from 'prop-types'
import { FormControl } from 'react-bootstrap'
import useDebounce from '../../../shared/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()
const inputRefCallback = useCallback(
element => {
inputRef.current = element
},
[inputRef]
)
return (
<FormControl
className="symbol-palette-search"
type="search"
inputRef={inputRefCallback}
id="symbol-palette-input"
aria-label="Search"
value={localInput}
placeholder={t('search') + '…'}
onChange={event => {
setLocalInput(event.target.value)
}}
/>
)
}
SymbolPaletteSearch.propTypes = {
setInput: PropTypes.func.isRequired,
inputRef: PropTypes.object.isRequired,
}

View File

@@ -1,27 +0,0 @@
import { TabList, Tab } from '@reach/tabs'
import PropTypes from 'prop-types'
export default function SymbolPaletteTabs({ categories, disabled }) {
return (
<TabList aria-label="Symbol Categories" className="symbol-palette-tab-list">
{categories.map(category => (
<Tab
key={category.id}
disabled={disabled}
className="symbol-palette-tab"
>
{category.label}
</Tab>
))}
</TabList>
)
}
SymbolPaletteTabs.propTypes = {
categories: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
})
).isRequired,
disabled: PropTypes.bool,
}

View File

@@ -1,14 +0,0 @@
import PropTypes from 'prop-types'
import SymbolPaletteContent from './symbol-palette-content'
export default function SymbolPalette({ show, handleSelect }) {
if (!show) {
return null
}
return <SymbolPaletteContent handleSelect={handleSelect} />
}
SymbolPalette.propTypes = {
show: PropTypes.bool,
handleSelect: PropTypes.func.isRequired,
}

View File

@@ -1,6 +0,0 @@
import App from '../../../base'
import { react2angular } from 'react2angular'
import SymbolPalette from '../components/symbol-palette'
App.component('symbolPalette', react2angular(SymbolPalette))

View File

@@ -1,872 +0,0 @@
[
{
"category": "Greek",
"command": "\\alpha",
"codepoint": "U+1D6FC",
"description": "Lowercase Greek letter alpha",
"aliases": ["a", "α"],
"notes": ""
},
{
"category": "Greek",
"command": "\\beta",
"codepoint": "U+1D6FD",
"description": "Lowercase Greek letter beta",
"aliases": ["b", "β"],
"notes": ""
},
{
"category": "Greek",
"command": "\\gamma",
"codepoint": "U+1D6FE",
"description": "Lowercase Greek letter gamma",
"aliases": ["γ"],
"notes": ""
},
{
"category": "Greek",
"command": "\\delta",
"codepoint": "U+1D6FF",
"description": "Lowercase Greek letter delta",
"aliases": ["δ"],
"notes": ""
},
{
"category": "Greek",
"command": "\\varepsilon",
"codepoint": "U+1D700",
"description": "Lowercase Greek letter epsilon, varepsilon",
"aliases": ["ε"],
"notes": ""
},
{
"category": "Greek",
"command": "\\epsilon",
"codepoint": "U+1D716",
"description": "Lowercase Greek letter epsilon lunate",
"aliases": ["ε"],
"notes": ""
},
{
"category": "Greek",
"command": "\\zeta",
"codepoint": "U+1D701",
"description": "Lowercase Greek letter zeta",
"aliases": ["ζ"],
"notes": ""
},
{
"category": "Greek",
"command": "\\eta",
"codepoint": "U+1D702",
"description": "Lowercase Greek letter eta",
"aliases": ["η"],
"notes": ""
},
{
"category": "Greek",
"command": "\\vartheta",
"codepoint": "U+1D717",
"description": "Lowercase Greek letter curly theta, vartheta",
"aliases": ["θ"],
"notes": ""
},
{
"category": "Greek",
"command": "\\theta",
"codepoint": "U+1D703",
"description": "Lowercase Greek letter theta",
"aliases": ["θ"],
"notes": ""
},
{
"category": "Greek",
"command": "\\iota",
"codepoint": "U+1D704",
"description": "Lowercase Greek letter iota",
"aliases": ["ι"],
"notes": ""
},
{
"category": "Greek",
"command": "\\kappa",
"codepoint": "U+1D705",
"description": "Lowercase Greek letter kappa",
"aliases": ["κ"],
"notes": ""
},
{
"category": "Greek",
"command": "\\lambda",
"codepoint": "U+1D706",
"description": "Lowercase Greek letter lambda",
"aliases": ["λ"],
"notes": ""
},
{
"category": "Greek",
"command": "\\mu",
"codepoint": "U+1D707",
"description": "Lowercase Greek letter mu",
"aliases": ["μ"],
"notes": ""
},
{
"category": "Greek",
"command": "\\nu",
"codepoint": "U+1D708",
"description": "Lowercase Greek letter nu",
"aliases": ["ν"],
"notes": ""
},
{
"category": "Greek",
"command": "\\xi",
"codepoint": "U+1D709",
"description": "Lowercase Greek letter xi",
"aliases": ["ξ"],
"notes": ""
},
{
"category": "Greek",
"command": "\\pi",
"codepoint": "U+1D70B",
"description": "Lowercase Greek letter pi",
"aliases": ["π"],
"notes": ""
},
{
"category": "Greek",
"command": "\\varrho",
"codepoint": "U+1D71A",
"description": "Lowercase Greek letter rho, varrho",
"aliases": ["ρ"],
"notes": ""
},
{
"category": "Greek",
"command": "\\rho",
"codepoint": "U+1D70C",
"description": "Lowercase Greek letter rho",
"aliases": ["ρ"],
"notes": ""
},
{
"category": "Greek",
"command": "\\sigma",
"codepoint": "U+1D70E",
"description": "Lowercase Greek letter sigma",
"aliases": ["σ"],
"notes": ""
},
{
"category": "Greek",
"command": "\\varsigma",
"codepoint": "U+1D70D",
"description": "Lowercase Greek letter final sigma, varsigma",
"aliases": ["ς"],
"notes": ""
},
{
"category": "Greek",
"command": "\\tau",
"codepoint": "U+1D70F",
"description": "Lowercase Greek letter tau",
"aliases": ["τ"],
"notes": ""
},
{
"category": "Greek",
"command": "\\upsilon",
"codepoint": "U+1D710",
"description": "Lowercase Greek letter upsilon",
"aliases": ["υ"],
"notes": ""
},
{
"category": "Greek",
"command": "\\phi",
"codepoint": "U+1D719",
"description": "Lowercase Greek letter phi",
"aliases": ["φ"],
"notes": ""
},
{
"category": "Greek",
"command": "\\varphi",
"codepoint": "U+1D711",
"description": "Lowercase Greek letter phi, varphi",
"aliases": ["φ"],
"notes": ""
},
{
"category": "Greek",
"command": "\\chi",
"codepoint": "U+1D712",
"description": "Lowercase Greek letter chi",
"aliases": ["χ"],
"notes": ""
},
{
"category": "Greek",
"command": "\\psi",
"codepoint": "U+1D713",
"description": "Lowercase Greek letter psi",
"aliases": ["ψ"],
"notes": ""
},
{
"category": "Greek",
"command": "\\omega",
"codepoint": "U+1D714",
"description": "Lowercase Greek letter omega",
"aliases": ["ω"],
"notes": ""
},
{
"category": "Greek",
"command": "\\Gamma",
"codepoint": "U+00393",
"description": "Uppercase Greek letter Gamma",
"aliases": ["Γ"],
"notes": ""
},
{
"category": "Greek",
"command": "\\Delta",
"codepoint": "U+00394",
"description": "Uppercase Greek letter Delta",
"aliases": ["Δ"],
"notes": ""
},
{
"category": "Greek",
"command": "\\Theta",
"codepoint": "U+00398",
"description": "Uppercase Greek letter Theta",
"aliases": ["Θ"],
"notes": ""
},
{
"category": "Greek",
"command": "\\Lambda",
"codepoint": "U+0039B",
"description": "Uppercase Greek letter Lambda",
"aliases": ["Λ"],
"notes": ""
},
{
"category": "Greek",
"command": "\\Xi",
"codepoint": "U+0039E",
"description": "Uppercase Greek letter Xi",
"aliases": ["Ξ"],
"notes": ""
},
{
"category": "Greek",
"command": "\\Pi",
"codepoint": "U+003A0",
"description": "Uppercase Greek letter Pi",
"aliases": ["Π"],
"notes": "Use \\prod for the product."
},
{
"category": "Greek",
"command": "\\Sigma",
"codepoint": "U+003A3",
"description": "Uppercase Greek letter Sigma",
"aliases": ["Σ"],
"notes": "Use \\sum for the sum."
},
{
"category": "Greek",
"command": "\\Upsilon",
"codepoint": "U+003A5",
"description": "Uppercase Greek letter Upsilon",
"aliases": ["Υ"],
"notes": ""
},
{
"category": "Greek",
"command": "\\Phi",
"codepoint": "U+003A6",
"description": "Uppercase Greek letter Phi",
"aliases": ["Φ"],
"notes": ""
},
{
"category": "Greek",
"command": "\\Psi",
"codepoint": "U+003A8",
"description": "Uppercase Greek letter Psi",
"aliases": ["Ψ"],
"notes": ""
},
{
"category": "Greek",
"command": "\\Omega",
"codepoint": "U+003A9",
"description": "Uppercase Greek letter Omega",
"aliases": ["Ω"],
"notes": ""
},
{
"category": "Relations",
"command": "\\neq",
"codepoint": "U+02260",
"description": "Not equal",
"aliases": ["!="],
"notes": ""
},
{
"category": "Relations",
"command": "\\leq",
"codepoint": "U+02264",
"description": "Less than or equal",
"aliases": ["<="],
"notes": ""
},
{
"category": "Relations",
"command": "\\geq",
"codepoint": "U+02265",
"description": "Greater than or equal",
"aliases": [">="],
"notes": ""
},
{
"category": "Relations",
"command": "\\ll",
"codepoint": "U+0226A",
"description": "Much less than",
"aliases": ["<<"],
"notes": ""
},
{
"category": "Relations",
"command": "\\gg",
"codepoint": "U+0226B",
"description": "Much greater than",
"aliases": [">>"],
"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",
"aliases": ["x"],
"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",
"aliases": ["<-"],
"notes": ""
},
{
"category": "Arrows",
"command": "\\rightarrow",
"codepoint": "U+02192",
"description": "Rightward arrow",
"aliases": ["->"],
"notes": ""
},
{
"category": "Arrows",
"command": "\\leftrightarrow",
"codepoint": "U+02194",
"description": "Left and right arrow",
"aliases": ["<->"],
"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",
"aliases": ["<="],
"notes": ""
},
{
"category": "Arrows",
"command": "\\Rightarrow",
"codepoint": "U+021D2",
"description": "Implies",
"aliases": ["=>"],
"notes": ""
},
{
"category": "Arrows",
"command": "\\Leftrightarrow",
"codepoint": "U+021D4",
"description": "Left and right double arrow",
"aliases": ["<=>"],
"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": "Triangle",
"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": ""
}
]

View File

@@ -1,45 +0,0 @@
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
}

View File

@@ -19,7 +19,6 @@ import './components/spellMenu'
import './directives/aceEditor'
import './directives/toggleSwitch'
import './controllers/SavingNotificationController'
import '../../features/symbol-palette/controllers/symbol-palette-controller'
let EditorManager
export default EditorManager = (function () {

View File

@@ -0,0 +1,44 @@
import Icon from './icon'
import { useTranslation } from 'react-i18next'
import { memo } from 'react'
function UpgradeBenefits() {
const { t } = useTranslation()
return (
<ul className="list-unstyled upgrade-benefits">
<li>
<Icon type="check" />
&nbsp;
{t('unlimited_projects')}
</li>
<li>
<Icon type="check" />
&nbsp;
{t('collabs_per_proj', { collabcount: 'Multiple' })}
</li>
<li>
<Icon type="check" />
&nbsp;
{t('full_doc_history')}
</li>
<li>
<Icon type="check" />
&nbsp;
{t('sync_to_dropbox')}
</li>
<li>
<Icon type="check" />
&nbsp;
{t('sync_to_github')}
</li>
<li>
<Icon type="check" />
&nbsp;
{t('compile_larger_projects')}
</li>
</ul>
)
}
export default memo(UpgradeBenefits)

View File

@@ -29,6 +29,9 @@ EditorContext.Provider.propTypes = {
hasPremiumCompile: PropTypes.bool,
loading: PropTypes.bool,
renameProject: PropTypes.func.isRequired,
showSymbolPalette: PropTypes.bool,
toggleSymbolPalette: PropTypes.func,
insertSymbol: PropTypes.func,
isProjectOwner: PropTypes.bool,
isRestrictedTokenMember: PropTypes.bool,
rootFolder: PropTypes.shape({
@@ -72,6 +75,9 @@ export function EditorProvider({ children, settings }) {
const [projectName, setProjectName] = useScopeValue('project.name')
const [rootFolder] = useScopeValue('rootFolder')
const [permissionsLevel] = useScopeValue('permissionsLevel')
const [showSymbolPalette] = useScopeValue('editor.showSymbolPalette')
const [toggleSymbolPalette] = useScopeValue('editor.toggleSymbolPalette')
const [insertSymbol] = useScopeValue('editor.insertSymbol')
useEffect(() => {
if (ide?.socket) {
@@ -123,6 +129,9 @@ export function EditorProvider({ children, settings }) {
isProjectOwner: owner?._id === window.user.id,
isRestrictedTokenMember: window.isRestrictedTokenMember,
rootFolder,
showSymbolPalette,
toggleSymbolPalette,
insertSymbol,
}),
[
cobranding,
@@ -132,6 +141,9 @@ export function EditorProvider({ children, settings }) {
permissionsLevel,
owner?._id,
rootFolder,
showSymbolPalette,
toggleSymbolPalette,
insertSymbol,
]
)

View File

@@ -1,20 +0,0 @@
import SymbolPalette from '../js/features/symbol-palette/components/symbol-palette'
export const Interactive = args => {
return (
<div style={{ maxWidth: 700, marginTop: 120 }}>
<SymbolPalette {...args} />
</div>
)
}
export default {
title: 'Symbol Palette',
component: SymbolPalette,
args: {
show: true,
},
argTypes: {
handleSelect: { action: 'handleSelect' },
},
}

View File

@@ -99,3 +99,7 @@
@import 'app/editor/history-v2.less';
@import 'app/metrics.less';
@import 'app/open-in-overleaf.less';
// module styles
// TODO: find a way for modules to add styles dynamically
@import 'modules/symbol-palette.less';

View File

@@ -15,7 +15,6 @@
@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;

View File

@@ -330,6 +330,14 @@
color: @toolbar-btn-active-color;
background-color: @toolbar-btn-active-bg-color;
box-shadow: @toolbar-btn-active-shadow;
&:focus {
color: @toolbar-btn-active-color;
&:not(:focus-visible) {
outline: none;
}
}
}
&:focus {

View File

@@ -136,3 +136,4 @@
@symbol-palette-item-color: @ol-blue-gray-3;
@symbol-palette-selected-tab-bg: #fff;
@symbol-palette-selected-tab-color: @ol-blue;
@symbol-palette-text-shadow-color: @ol-blue-gray-1;

View File

@@ -1121,3 +1121,4 @@
@symbol-palette-item-color: #fff;
@symbol-palette-selected-tab-bg: @ol-blue-gray-4;
@symbol-palette-selected-tab-color: #fff;
@symbol-palette-text-shadow-color: @ol-blue-gray-6;

View File

@@ -0,0 +1,155 @@
.symbol-palette-container {
height: 100%;
width: 100%;
position: relative;
.symbol-palette {
display: flex;
flex-direction: column;
background: @symbol-palette-bg;
color: @symbol-palette-color;
width: 100%;
height: 100%;
min-height: 220px; // allow space for the overlay contents
}
.symbol-palette-header-outer {
flex-shrink: 0;
display: flex;
flex-wrap: nowrap;
justify-content: space-between;
align-items: flex-start;
font-family: @font-family-sans-serif;
font-size: 16px;
background: @symbol-palette-header-background;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
.symbol-palette-header {
flex: 1;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
}
.symbol-palette-tab-list[data-reach-tab-list] {
background: none;
border-bottom: none;
flex-wrap: wrap;
}
.symbol-palette-tab[data-reach-tab][data-selected] {
background: @symbol-palette-selected-tab-bg;
color: @symbol-palette-selected-tab-color;
border-bottom-color: transparent;
}
.symbol-palette-body {
flex: 1;
overflow-y: auto;
}
.symbol-palette-items {
display: flex;
flex-wrap: wrap;
padding: @padding-xs;
}
.symbol-palette-item {
font-family: 'Stix Two Math', serif;
font-size: 24px;
line-height: 42px;
height: 42px;
width: 42px;
margin: @margin-xs;
color: @symbol-palette-item-color;
background: @symbol-palette-item-bg;
border: 1px solid transparent;
border-radius: @border-radius-base;
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: @margin-xs;
}
.symbol-palette-empty {
display: flex;
align-items: center;
justify-content: center;
padding: @padding-sm;
}
.symbol-palette-search {
padding: 2px @padding-sm;
margin: @margin-xs;
line-height: 1;
height: auto;
width: auto;
}
.symbol-palette-header-group {
display: flex;
align-items: center;
white-space: nowrap;
margin-left: @margin-xs;
}
.symbol-palette-info-link,
.symbol-palette-info-link:focus,
.symbol-palette-info-link:hover {
color: inherit;
}
.symbol-palette-close-button {
background: transparent;
color: @symbol-palette-color;
padding-left: @padding-sm;
padding-right: @padding-sm;
margin-left: @margin-xs;
font-size: 24px;
font-weight: bold;
line-height: 1;
}
.symbol-palette-overlay {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: fade(@symbol-palette-bg, 75%);
color: @symbol-palette-color;
display: flex;
flex-direction: column;
padding: 0 @padding-lg @padding-sm;
align-items: center;
text-shadow: 0 0 8px @symbol-palette-text-shadow-color;
min-height: 200px;
overflow: auto;
h4 {
font-weight: bold;
color: @symbol-palette-color;
text-align: center;
}
.symbol-palette-close-button {
position: absolute;
top: 0;
right: 0;
}
.upgrade-benefits {
column-count: 2;
}
}
}

View File

@@ -30,7 +30,6 @@
"log_entry_maximum_entries": "Maximum log entries limit hit",
"log_entry_maximum_entries_title": "__total__ issues total. Showing the first __displayed__",
"log_entry_maximum_entries_message": "<0>Tip</0>: Try to fix the first error and recompile. Often one error causes many later error messages",
"log_entry_description": "Log entry with level: __level__",
"navigate_log_source": "Navigate to log position in source code: __location__",
"other_output_files": "Download other output files",
"refresh": "Refresh",
@@ -1484,7 +1483,10 @@
"category_relations": "Relations",
"category_misc": "Misc",
"no_symbols_found": "No symbols found",
"find_out_more_about_latex_symbols": "Find out more about LaTeX symbols",
"learn_more_about_the_symbol_palette": "Learn more about the Symbol Palette and how to use it",
"find_the_symbols_you_need_with_premium": "Find the symbols you need faster with Overleaf Premium",
"with_premium_subscription_you_also_get": "With an Overleaf Premium subscription you also get",
"try_premium_for_free": "Try Premium for free",
"search": "Search",
"also": "Also",
"add_email": "Add Email",

View File

@@ -1,104 +0,0 @@
import { expect } from 'chai'
import sinon from 'sinon'
import { screen, render, fireEvent, waitFor } from '@testing-library/react'
import SymbolPalette from '../../../../../frontend/js/features/symbol-palette/components/symbol-palette'
describe('symbol palette', function () {
let clock
before(function () {
clock = sinon.useFakeTimers({
toFake: ['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval'],
})
})
after(function () {
clock.runAll()
clock.restore()
})
it('handles keyboard interaction', async function () {
this.timeout(10000)
const handleSelect = sinon.stub()
const { container } = render(
<SymbolPalette show handleSelect={handleSelect} />
)
// 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')
expect(symbols).to.have.length(2)
})
// check the tabs are disabled
expect(selectedTab.disabled).to.be.true
// 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({
aliases: ['Π'],
category: 'Greek',
character: 'Π',
codepoint: 'U+003A0',
command: '\\Pi',
description: 'Uppercase Greek letter Pi',
notes: 'Use \\prod for the product.',
})
})
})