@primer/doctocat-nextjs 0.5.4-rc.183f5d1 → 0.5.4-rc.3b1107b

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  'use client'
2
- import React, {PropsWithChildren, useCallback, useState} from 'react'
2
+ import React, {type PropsWithChildren, useCallback, useState, useRef, useEffect} from 'react'
3
3
  import clsx from 'clsx'
4
4
  import {LiveProvider, LiveEditor, LiveError, LivePreview} from 'react-live'
5
5
  import {useColorMode} from '../../context/color-modes/useColorMode'
@@ -18,12 +18,23 @@ type ReactCodeBlockProps = {
18
18
  jsxScope: Record<string, unknown>
19
19
  } & PropsWithChildren<HTMLElement>
20
20
 
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
+
21
30
  export function ReactCodeBlock(props: ReactCodeBlockProps) {
22
31
  const {colorMode, setColorMode} = useColorMode()
23
32
  const {basePath} = useConfig()
24
33
  const initialCode = getCodeFromChildren(props.children)
25
34
  const [code, setCode] = useState(initialCode)
26
35
  const shouldShowPreview = ['tsx', 'jsx'].includes(props['data-language'])
36
+ const editorRef = useRef<HTMLDivElement>(null)
37
+ const resetButtonRef = useRef<HTMLButtonElement>(null)
27
38
 
28
39
  /**
29
40
  * Transforms code to prepend basePath to img src attributes
@@ -43,6 +54,41 @@ export function ReactCodeBlock(props: ReactCodeBlockProps) {
43
54
 
44
55
  const noInline = props['data-filename'] === 'noinline' || false
45
56
 
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
+
46
92
  return (
47
93
  <>
48
94
  <LiveProvider transformCode={transformCodeWithBasePath} code={code} scope={props.jsxScope} noInline={noInline}>
@@ -76,11 +122,11 @@ export function ReactCodeBlock(props: ReactCodeBlockProps) {
76
122
  <Button size="small" leadingVisual={CopyIcon} onClick={handleCopy}>
77
123
  Copy
78
124
  </Button>
79
- <Button size="small" leadingVisual={UndoIcon} onClick={handleReset}>
125
+ <Button size="small" leadingVisual={UndoIcon} onClick={handleReset} ref={resetButtonRef}>
80
126
  Reset
81
127
  </Button>
82
128
  </div>
83
- <div className={styles.Editor}>
129
+ <div className={styles.Editor} ref={editorRef}>
84
130
  <LiveEditor theme={colorMode === 'light' ? lightTheme : darkTheme} onChange={setCode} />
85
131
  </div>
86
132
  {shouldShowPreview && (
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primer/doctocat-nextjs",
3
- "version": "0.5.4-rc.183f5d1",
3
+ "version": "0.5.4-rc.3b1107b",
4
4
  "description": "A Next.js theme for building Primer documentation sites",
5
5
  "main": "index.js",
6
6
  "type": "module",