11'use client'
2- import React , { PropsWithChildren , useCallback , useState } from 'react'
2+ import React , { type PropsWithChildren , useCallback , useState , useRef , useEffect } from 'react'
33import clsx from 'clsx'
44import { LiveProvider , LiveEditor , LiveError , LivePreview } from 'react-live'
55import { useColorMode } from '../../context/color-modes/useColorMode'
@@ -18,12 +18,23 @@ type ReactCodeBlockProps = {
1818 jsxScope : Record < string , unknown >
1919} & PropsWithChildren < HTMLElement >
2020
21+ const getFocusableElements = ( ) => {
22+ const focusableElementsQuery = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
23+
24+ return Array . from ( document . querySelectorAll < HTMLElement > ( focusableElementsQuery ) ) . filter ( el => {
25+ const style = window . getComputedStyle ( el )
26+ return style . display !== 'none' && style . visibility !== 'hidden' && ! el . hasAttribute ( 'disabled' )
27+ } )
28+ }
29+
2130export function ReactCodeBlock ( props : ReactCodeBlockProps ) {
2231 const { colorMode, setColorMode} = useColorMode ( )
2332 const { basePath} = useConfig ( )
2433 const initialCode = getCodeFromChildren ( props . children )
2534 const [ code , setCode ] = useState ( initialCode )
2635 const shouldShowPreview = [ 'tsx' , 'jsx' ] . includes ( props [ 'data-language' ] )
36+ const editorRef = useRef < HTMLDivElement > ( null )
37+ const resetButtonRef = useRef < HTMLButtonElement > ( null )
2738
2839 /**
2940 * Transforms code to prepend basePath to img src attributes
@@ -43,6 +54,41 @@ export function ReactCodeBlock(props: ReactCodeBlockProps) {
4354
4455 const noInline = props [ 'data-filename' ] === 'noinline' || false
4556
57+ useEffect ( ( ) => {
58+ const editor = editorRef . current
59+
60+ if ( ! editor ) return
61+
62+ const onKeyDown = ( e : KeyboardEvent ) => {
63+ if ( e . key !== 'Tab' ) {
64+ return
65+ }
66+
67+ if ( e . shiftKey ) {
68+ e . preventDefault ( )
69+ // We know that the previous focusable element is always the reset button
70+ resetButtonRef . current ?. focus ( )
71+ return
72+ }
73+
74+ const focusableElements = getFocusableElements ( )
75+
76+ const currentIndex = focusableElements . findIndex ( el => el === resetButtonRef . current )
77+
78+ if ( currentIndex !== - 1 ) {
79+ e . preventDefault ( )
80+ const nextIndex = currentIndex + 1
81+ focusableElements [ nextIndex ] ?. focus ( )
82+ }
83+ }
84+
85+ editor . addEventListener ( 'keydown' , onKeyDown )
86+
87+ return ( ) => {
88+ editor . removeEventListener ( 'keydown' , onKeyDown )
89+ }
90+ } , [ ] )
91+
4692 return (
4793 < >
4894 < LiveProvider transformCode = { transformCodeWithBasePath } code = { code } scope = { props . jsxScope } noInline = { noInline } >
@@ -76,11 +122,11 @@ export function ReactCodeBlock(props: ReactCodeBlockProps) {
76122 < Button size = "small" leadingVisual = { CopyIcon } onClick = { handleCopy } >
77123 Copy
78124 </ Button >
79- < Button size = "small" leadingVisual = { UndoIcon } onClick = { handleReset } >
125+ < Button size = "small" leadingVisual = { UndoIcon } onClick = { handleReset } ref = { resetButtonRef } >
80126 Reset
81127 </ Button >
82128 </ div >
83- < div className = { styles . Editor } >
129+ < div className = { styles . Editor } ref = { editorRef } >
84130 < LiveEditor theme = { colorMode === 'light' ? lightTheme : darkTheme } onChange = { setCode } />
85131 </ div >
86132 { shouldShowPreview && (
0 commit comments