Symbol Palette: get rid of @reach/tabs

This commit is contained in:
yu-i-i
2025-05-04 16:55:45 +02:00
parent 281174eee9
commit ea2c644695
7 changed files with 112 additions and 40 deletions

View File

@@ -1,4 +1,3 @@
import { TabPanels, TabPanel } from '@reach/tabs'
import { useTranslation } from 'react-i18next'
import PropTypes from 'prop-types'
import SymbolPaletteItems from './symbol-palette-items'
@@ -9,6 +8,7 @@ export default function SymbolPaletteBody({
filteredSymbols,
handleSelect,
focusInput,
activeCategoryId,
}) {
const { t } = useTranslation()
@@ -17,7 +17,7 @@ export default function SymbolPaletteBody({
// note: include empty tab panels so that aria-controls on tabs can still reference the panel ids
if (filteredSymbols) {
return (
<>
<div className="symbol-palette-panels">
{filteredSymbols.length ? (
<SymbolPaletteItems
items={filteredSymbols}
@@ -28,29 +28,43 @@ export default function SymbolPaletteBody({
<div className="symbol-palette-empty">{t('no_symbols_found')}</div>
)}
<TabPanels>
{categories.map(category => (
<TabPanel key={category.id} tabIndex={-1} />
))}
</TabPanels>
</>
{categories.map(category => (
<div
key={category.id}
role="tabpanel"
className="symbol-palette-panel"
id={`symbol-palette-panel-${category.id}`}
aria-labelledby={`symbol-palette-tab-${category.id}`}
hidden
/>
))}
</div>
)
}
// not searching: show the symbols grouped by category
return (
<TabPanels>
{categories.map(category => (
<TabPanel key={category.id} tabIndex={-1}>
<div className="symbol-palette-panels">
{categories.map((category) => (
<div
key={category.id}
id={`symbol-palette-panel-${category.id}`}
className="symbol-palette-panel"
role="tabpanel"
aria-labelledby={`symbol-palette-tab-${category.id}`}
hidden={category.id !== activeCategoryId}
>
<SymbolPaletteItems
items={categorisedSymbols[category.id]}
handleSelect={handleSelect}
focusInput={focusInput}
/>
</TabPanel>
</div>
))}
</TabPanels>
</div>
)
}
SymbolPaletteBody.propTypes = {
categories: PropTypes.arrayOf(PropTypes.object).isRequired,
@@ -58,4 +72,5 @@ SymbolPaletteBody.propTypes = {
filteredSymbols: PropTypes.arrayOf(PropTypes.object),
handleSelect: PropTypes.func.isRequired,
focusInput: PropTypes.func.isRequired,
activeCategoryId: PropTypes.string.isRequired,
}

View File

@@ -11,7 +11,7 @@ export default function SymbolPaletteCloseButton() {
type="button"
className="btn-close symbol-palette-close-button"
onClick={toggleSymbolPalette}
aria-label={t('clear_search')}
aria-label={t('close')}
>
</button>
</div>

View File

@@ -1,4 +1,3 @@
import { Tabs } from '@reach/tabs'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import PropTypes from 'prop-types'
@@ -10,8 +9,6 @@ import SymbolPaletteBody from './symbol-palette-body'
import SymbolPaletteTabs from './symbol-palette-tabs'
import SymbolPaletteCloseButton from './symbol-palette-close-button'
import '@reach/tabs/styles.css'
export default function SymbolPaletteContent({ handleSelect }) {
const [input, setInput] = useState('')
@@ -19,6 +16,7 @@ export default function SymbolPaletteContent({ handleSelect }) {
// build the list of categories with translated labels
const categories = useMemo(() => createCategories(t), [t])
const [activeCategoryId, setActiveCategoryId] = useState(categories[0]?.id)
// group the symbols by category
const categorisedSymbols = useMemo(
@@ -59,18 +57,23 @@ export default function SymbolPaletteContent({ handleSelect }) {
inputRef.current.focus()
}
}, [])
return (
<Tabs className="symbol-palette-container">
<div className="symbol-palette-container">
<div className="symbol-palette">
<div className="symbol-palette-header-outer">
<div className="symbol-palette-header">
<SymbolPaletteTabs categories={categories} />
<SymbolPaletteTabs
categories={categories}
activeCategoryId={activeCategoryId}
setActiveCategoryId={setActiveCategoryId}
/>
<div className="symbol-palette-header-group">
<SymbolPaletteSearch setInput={setInput} inputRef={inputRef} />
<SymbolPaletteCloseButton />
</div>
</div>
<div className="symbol-palette-header-group">
<SymbolPaletteCloseButton />
</div>
</div>
<div className="symbol-palette-body">
<SymbolPaletteBody
@@ -79,10 +82,11 @@ export default function SymbolPaletteContent({ handleSelect }) {
filteredSymbols={filteredSymbols}
handleSelect={handleSelect}
focusInput={focusInput}
activeCategoryId={activeCategoryId}
/>
</div>
</div>
</Tabs>
</div>
)
}
SymbolPaletteContent.propTypes = {

View File

@@ -42,4 +42,4 @@ export default function SymbolPaletteSearch({ setInput, inputRef }) {
SymbolPaletteSearch.propTypes = {
setInput: PropTypes.func.isRequired,
inputRef: PropTypes.object.isRequired,
};
}

View File

@@ -1,22 +1,76 @@
import { TabList, Tab } from '@reach/tabs'
import PropTypes from 'prop-types'
import { useState, useRef } from 'react'
export default function SymbolPaletteTabs({
categories,
activeCategoryId,
setActiveCategoryId,
}) {
const buttonRefs = useRef([])
const focusTab = (index) => {
setActiveCategoryId(categories[index].id)
buttonRefs.current[index]?.focus()
}
const handleKeyDown = (e, index) => {
switch (e.key) {
case 'ArrowRight':
focusTab((index + 1) % categories.length)
break
case 'ArrowLeft':
focusTab((index - 1 + categories.length) % categories.length)
break
case 'Home':
case 'PageUp':
focusTab(0)
break
case 'End':
case 'PageDown':
focusTab(categories.length - 1)
break
default:
break
}
}
export default function SymbolPaletteTabs({ categories }) {
return (
<TabList aria-label="Symbol Categories" className="symbol-palette-tab-list">
{categories.map(category => (
<Tab key={category.id} className="symbol-palette-tab">
{category.label}
</Tab>
))}
</TabList>
<div
role="tablist"
aria-label="Symbol Categories"
className="symbol-palette-tab-list"
tabIndex={0}
>
{categories.map((category, index) => {
const selected = activeCategoryId === category.id
return (
<button
key={category.id}
role="tab"
type="button"
className="symbol-palette-tab"
id={`symbol-palette-tab-${category.id}`}
aria-controls={`symbol-palette-panel-${category.id}`}
aria-selected={selected}
tabIndex={selected ? 0 : -1}
ref={(el) => (buttonRefs.current[index] = el)}
onClick={() => setActiveCategoryId(category.id)}
onKeyDown={(e) => handleKeyDown(e, index)}
>
{category.label}
</button>
)
})}
</div>
)
}
SymbolPaletteTabs.propTypes = {
categories: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
})
).isRequired,
categories: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
})).isRequired,
activeCategoryId: PropTypes.string.isRequired,
setActiveCategoryId: PropTypes.func.isRequired,
}

View File

@@ -155,7 +155,7 @@
.symbol-palette-close-button-outer {
display: flex;
align-items: center;
margin-right: var(--spacing-01);
margin-right: var(--spacing-05);
}
.symbol-palette-close-button {

View File

@@ -207,7 +207,6 @@
"@pollyjs/adapter-node-http": "^6.0.6",
"@pollyjs/core": "^6.0.6",
"@pollyjs/persister-fs": "^6.0.6",
"@reach/tabs": "0.18.0",
"@replit/codemirror-emacs": "overleaf/codemirror-emacs#4394c03858f27053f8768258e9493866e06e938e",
"@replit/codemirror-indentation-markers": "overleaf/codemirror-indentation-markers#78264032eb286bc47871569ae87bff5ca1c6c161",
"@replit/codemirror-vim": "overleaf/codemirror-vim#1bef138382d948018f3f9b8a4d7a70ab61774e4b",