Давайте создадим доступный компонент панели вкладок с автоматической активацией панели для вашего проекта React, используя TypeScript и SCSS.
Архитектура
В процессе создания идеи мы должны создать требования, которые помогут нам понять, как будет потребляться компонент. Мы будем использовать эти требования для формализации API компонента.
Требования
Компонент <TabPanel />
должен иметь возможность отображать набор вкладок с соответствующими панелями. Каждая вкладка может быть сгенерирована внутри самого компонента с помощью файла string[]
. Панели более сложны, так как их нужно будет связать с каждой вкладкой. Некоторые распространенные подходы можно увидеть ниже:
const tabs = ['tab 1', 'tab 2', 'tab 3']; // ‼️ Option 1: good; <TabPanel tabs={tabs}> <div>Content 1</div> <div>Content 2</div> <div>Content 3</div> </TabPanel> // ✅ Option 2: better; <TabPanel tabs={tabs}> {tab => { switch(tab) { case tabs[0]: return <div>Content 1</div>; case tabs[1]: return <div>Content 2</div>; default: return <div>Content 3</div>; } }} </TabPanel>
- Вариант 1. Эта реализация является краткой и очень похожа на то, как будет структурирован базовый вариант
HTML
. Однако компонент должен будет отслеживать, какую дочернюю запись отображать, и обрабатывать несоответствия массива между переданным массивомtab
и компонентомchildren
. - Вариант 2. Немного большая реализация с использованием шаблона
Render props
React. Существует хороший баланс между логикой инкапсулирующего компонента и передаваемыми свойствами.
Обе версии компонента требуют отслеживания активной вкладки. С первым вариантом мы можем вывести правильный дочерний элемент для рендеринга по соответствующему индексу выбранной вкладки. Второй вариант предпочтительнее из-за его прямой связи между свойством tabs
и визуализируемым дочерним элементом.
Родительскому компоненту может потребоваться контекст активной вкладки. Нам нужно будет синхронизировать (условно) нашего родителя после каждого изменения внутреннего состояния. Этого процесса можно легко достичь, используя компонент на основе класса и передав функцию обратного вызова в setState
.
useEffect
также можно использовать, хотя нам нужно убедиться, что наш обратный вызов не срабатывает во время начального монтирования.
Состав
Мы приступаем к созданию диаграммы, описывающей статические и динамические аспекты нашего компонента. Наша структура должна соответствовать соответствию доступности, определенному w3.org.
У нас нет сложных потоков данных или преобразований. Интерфейс нашего компонента может быть таким простым, как:
interface TabPanelProperties { onChange?: (tab: string) => void; children: (tab: string) => ReactElement; tabs: Readonly<string[]>; id: string; }
Выполнение
Для реализации нашего класса потребуется несколько методов установки и один метод получения:
state = { selectedTab: this.props.tabs[0] }; get contentId() { return `${this.state.selectedTab}-content`; } setSelectedTab = (selectedTab: string) => { this.setState({ selectedTab }, () => { (document.querySelector(`#${this.props.id} #${selectedTab}`) as HTMLButtonElement)?.focus(); if (this.props.onChange) this.props.onChange(selectedTab); }); } onTabChange = (event: SyntheticEvent) => { event.preventDefault(); const tabId: string = (event.target as HTMLButtonElement).id; this.setSelectedTab(tabId); }
Мы используем каждую запись массива props.tabs
как уникальную id
для каждой вкладки. this.contentId
напоминает область содержимого, которой управляет активная вкладка.
После каждого setSelectedTab
мы программно перемещаем фокус на следующую активную вкладку и запускаем props.onChange
для синхронизации состояния с родителем (если применимо).
Доступность
Поскольку мы выбрали автоматическую активацию панели, пользователь может переключать фокус, нажимая клавиши со стрелками влево и вправо. Чтобы добиться этого эффекта, мы применим прослушиватель onKeyDown
на каждой вкладке и переключим tabIndex
каждой кнопки, чтобы предотвратить смещение фокуса на неактивные кнопки.
onArrowKeyChange = (event: KeyboardEvent<HTMLButtonElement>) => { const { tabs } = this.props; const { selectedTab } = this.state; const keys = ['ArrowLeft', 'ArrowRight']; if (!keys.includes(event.key)) return; const tabIndex = tabs.indexOf(selectedTab); const firstTab = tabs[0]; const lastTab = tabs[tabs.length - 1]; switch (event.key) { case keys[0]: this.setSelectedTab(selectedTab === firstTab ? lastTab : tabs[tabIndex - 1] ); break; case keys[1]: default: this.setSelectedTab(selectedTab === lastTab ? firstTab : tabs[tabIndex + 1] ); break; } }; renderList = () => ( <div className="tabs-list" role="tablist"> {this.props.tabs.map(tab => { const isSelectedTab = tab === this.state.selectedTab; const className = `tab ${isSelectedTab ? 'tab-selected' : ''}`; return ( <button id={tab} key={tab} type="button" role="tab" onClick={this.onTabChange} tabIndex={isSelectedTab ? 0 : -1} onKeyDown={this.onArrowKeyChange} className={className} aria-controls={this.contentId} aria-selected={isSelectedTab} > {tab} </button> ) })} </div> );
Дорабатываем наш компонент с его методами renderContent
и render
:
renderPanel = () => ( <div id={this.contentId} role="tabpanel" tabIndex={0} className="tabs-content" aria-labelledby={this.state.selectedTab} > {this.props.children(this.state.selectedTab)} </div> ); render() { return ( <div id={this.props.id} className="tabs"> {this.renderList()} {this.renderPanel()} </div> ); }
Результат
Мы можем собрать все вместе в один компонент унифицированного класса вместе с несколькими строками SCSS, чтобы вы могли начать:
import { PureComponent, ReactElement, SyntheticEvent, KeyboardEvent } from 'react'; import './TabPanel.scss'; interface TabPanelProperties { onChange?: (tab: string) => void; children: (tab: string) => ReactElement; tabs: Readonly<string[]>; id: string; } export default class TabPanel extends PureComponent<TabPanelProperties> { state = { selectedTab: this.props.tabs[0] }; get contentId() { return `${this.state.selectedTab}-content`; } setSelectedTab = (selectedTab: string) => { this.setState({ selectedTab }, () => { const selector = `#${this.props.id} #${selectedTab}`; (document.querySelector(selector) as HTMLButtonElement)?.focus(); if (this.props.onChange) this.props.onChange(selectedTab); }); } onTabChange = (event: SyntheticEvent) => { event.preventDefault(); const tabId: string = (event.target as HTMLButtonElement).id; this.setSelectedTab(tabId); } onArrowKeyChange = (event: KeyboardEvent<HTMLButtonElement>) => { const { tabs } = this.props; const { selectedTab } = this.state; const keys = ['ArrowLeft', 'ArrowRight']; if (!keys.includes(event.key)) return; const tabIndex = tabs.indexOf(selectedTab); const firstTab = tabs[0]; const lastTab = tabs[tabs.length - 1]; switch (event.key) { case keys[0]: this.setSelectedTab(selectedTab === firstTab ? lastTab : tabs[tabIndex - 1] ); break; case keys[1]: default: this.setSelectedTab(selectedTab === lastTab ? firstTab : tabs[tabIndex + 1] ); break; } }; renderList = () => ( <div className="tabs-list" role="tablist"> {this.props.tabs.map(tab => { const isSelectedTab = tab === this.state.selectedTab; const className = `tab ${isSelectedTab ? 'tab-selected' : ''}`; return ( <button id={tab} key={tab} type="button" role="tab" onClick={this.onTabChange} tabIndex={isSelectedTab ? 0 : -1} onKeyDown={this.onArrowKeyChange} className={className} aria-controls={this.contentId} aria-selected={isSelectedTab} > {tab} </button> ) })} </div> ); renderPanel = () => ( <div id={this.contentId} role="tabpanel" tabIndex={0} className="tabs-content" aria-labelledby={this.state.selectedTab} > {this.props.children(this.state.selectedTab)} </div> ); render() { return ( <div id={this.props.id} className="tabs"> {this.renderList()} {this.renderPanel()} </div> ); } } .tabs-list { border-bottom: 1px solid #D5D5D5; .tab { text-transform: capitalize; font-size: 0.875rem; color: #A1A1A1; font-weight: 700; transition: color 0.3s; &:hover { color: #555555; } &-selected:hover, &-selected { color: #222222; } } }
Заключение
Наша реализация завершена и соответствует указанным требованиям.
Пожалуйста, измените любую часть компонента и улучшите маркировку специальных возможностей или обработку отключенных вкладок. Все ссылки в статье доступны ниже.
Надеюсь, вы найдете эту статью полезной для своего собственного проекта React!
Рекомендации
Реквизиты рендеринга — React
Термин «реквизит рендеринга относится к методу совместного использования кода между компонентами React с использованием реквизита, значением которого является…reactjs.org »
«Excalidraw – совместная работа на интерактивной доске стала проще
Excalidraw – это виртуальный инструмент для совместной работы на интерактивной доске, который позволяет легко рисовать схемы, напоминающие нарисованные от руки…excalidraw.com"
Повышение уровня кодирования
Спасибо, что являетесь частью нашего сообщества! Перед тем, как ты уйдешь:
- 👏 Хлопайте за историю и подписывайтесь на автора 👉
- 📰 Смотрите больше контента в публикации Level Up Coding
- 🔔 Подписывайтесь на нас: Twitter | ЛинкедИн | "Новостная рассылка"
🚀👉 Присоединяйтесь к коллективу талантов Level Up и найдите прекрасную работу