11'use client'
2- import React , { PropsWithChildren , useCallback , useState , useRef , useEffect , useId } from 'react'
2+ import React , { type PropsWithChildren , useCallback , useState , useRef , useEffect , useId } from 'react'
33import clsx from 'clsx'
44import { LiveProvider , LiveEditor , LiveError , LivePreview } from 'react-live'
55import { useColorMode } from '../../context/color-modes/useColorMode'
@@ -21,6 +21,15 @@ type ReactCodeBlockProps = {
2121 jsxScope : Record < string , unknown >
2222} & PropsWithChildren < HTMLElement >
2323
24+ const getFocusableElements = ( ) => {
25+ const focusableElementsQuery = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
26+
27+ return Array . from ( document . querySelectorAll < HTMLElement > ( focusableElementsQuery ) ) . filter ( el => {
28+ const style = window . getComputedStyle ( el )
29+ return style . display !== 'none' && style . visibility !== 'hidden' && ! el . hasAttribute ( 'disabled' )
30+ } )
31+ }
32+
2433export function ReactCodeBlock ( props : ReactCodeBlockProps ) {
2534 const uniqueId = useId ( )
2635 const { colorMode, setColorMode} = useColorMode ( )
@@ -31,6 +40,7 @@ export function ReactCodeBlock(props: ReactCodeBlockProps) {
3140 const [ isCodePaneCollapsed , setIsCodePaneCollapsed ] = useState < boolean | null > ( null )
3241 const [ initialPosition , setInitialPosition ] = useState < number | null > ( null )
3342 const editorRef = useRef < HTMLDivElement > ( null )
43+ const resetButtonRef = useRef < HTMLButtonElement > ( null )
3444 const shouldShowPreview = [ 'tsx' , 'jsx' ] . includes ( props [ 'data-language' ] )
3545
3646 // scroll back to the initial y pos on collapse state change
@@ -84,6 +94,41 @@ export function ReactCodeBlock(props: ReactCodeBlockProps) {
8494
8595 const noInline = props [ 'data-filename' ] === 'noinline' || false
8696
97+ useEffect ( ( ) => {
98+ const editor = editorRef . current
99+
100+ if ( ! editor ) return
101+
102+ const onKeyDown = ( e : KeyboardEvent ) => {
103+ if ( e . key !== 'Tab' ) {
104+ return
105+ }
106+
107+ if ( e . shiftKey ) {
108+ e . preventDefault ( )
109+ // We know that the previous focusable element is always the reset button
110+ resetButtonRef . current ?. focus ( )
111+ return
112+ }
113+
114+ const focusableElements = getFocusableElements ( )
115+
116+ const currentIndex = focusableElements . findIndex ( el => el === resetButtonRef . current )
117+
118+ if ( currentIndex !== - 1 ) {
119+ e . preventDefault ( )
120+ const nextIndex = currentIndex + 1
121+ focusableElements [ nextIndex ] ?. focus ( )
122+ }
123+ }
124+
125+ editor . addEventListener ( 'keydown' , onKeyDown )
126+
127+ return ( ) => {
128+ editor . removeEventListener ( 'keydown' , onKeyDown )
129+ }
130+ } , [ ] )
131+
87132 return (
88133 < >
89134 < LiveProvider transformCode = { transformCodeWithBasePath } code = { code } scope = { props . jsxScope } noInline = { noInline } >
@@ -117,7 +162,7 @@ export function ReactCodeBlock(props: ReactCodeBlockProps) {
117162 < Button size = "small" leadingVisual = { CopyIcon } onClick = { handleCopy } >
118163 Copy
119164 </ Button >
120- < Button size = "small" leadingVisual = { UndoIcon } onClick = { handleReset } >
165+ < Button size = "small" leadingVisual = { UndoIcon } onClick = { handleReset } ref = { resetButtonRef } >
121166 Reset
122167 </ Button >
123168 </ div >
0 commit comments