A calm place to write long-form, and publish it to the open social web. skypress.blog/
0

Configure Feed

Select the types of activity you want to include in your feed.

Add RecordJsonViewer island (trigger button + modal)

+111
+18
src/components/RecordJsonViewer.presence.test.tsx
··· 1 + import { describe, it, expect } from 'vitest'; 2 + import { createElement } from 'react'; 3 + import { renderToStaticMarkup } from 'react-dom/server'; 4 + import RecordJsonViewer from './RecordJsonViewer'; 5 + 6 + describe( 'RecordJsonViewer presence', () => { 7 + it( 'default-exports a component that renders the trigger button, dialog closed', () => { 8 + const markup = renderToStaticMarkup( 9 + createElement( RecordJsonViewer, { 10 + highlightedHtml: '<code class="hljs language-json">{}</code>', 11 + } ) 12 + ); 13 + expect( markup ).toContain( 'record-json__trigger' ); 14 + expect( markup ).toContain( 'aria-label="View record JSON"' ); 15 + // Closed by default — no dialog in the initial (server) render. 16 + expect( markup ).not.toContain( 'role="dialog"' ); 17 + } ); 18 + } );
+93
src/components/RecordJsonViewer.tsx
··· 1 + import { useEffect, useRef, useState } from 'react'; 2 + 3 + export interface RecordJsonViewerProps { 4 + /** Sanitised, syntax-highlighted JSON markup — a `<code class="hljs language-json">…</code>` string. */ 5 + highlightedHtml: string; 6 + } 7 + 8 + /** 9 + * A code-icon button at the end of the reader eyebrow that opens a modal showing the 10 + * post's raw PDS record (its `value`) as syntax-highlighted JSON. The markup is 11 + * highlighted + sanitised server-side and passed in as `highlightedHtml`; this island 12 + * owns only the open/close interaction. JS-only enhancement (mounted `client:only`). 13 + */ 14 + export default function RecordJsonViewer( { highlightedHtml }: RecordJsonViewerProps ) { 15 + const [ open, setOpen ] = useState( false ); 16 + const triggerRef = useRef< HTMLButtonElement >( null ); 17 + const closeRef = useRef< HTMLButtonElement >( null ); 18 + 19 + function close() { 20 + setOpen( false ); 21 + // Return focus to the trigger that opened the dialog. 22 + triggerRef.current?.focus(); 23 + } 24 + 25 + // Move focus into the dialog on open, and close on Escape while it's open. 26 + useEffect( () => { 27 + if ( ! open ) { 28 + return; 29 + } 30 + closeRef.current?.focus(); 31 + const onKey = ( event: KeyboardEvent ) => { 32 + if ( event.key === 'Escape' ) { 33 + close(); 34 + } 35 + }; 36 + document.addEventListener( 'keydown', onKey ); 37 + return () => document.removeEventListener( 'keydown', onKey ); 38 + // `close` is stable for the lifetime of the open dialog; only `open` matters here. 39 + // eslint-disable-next-line react-hooks/exhaustive-deps 40 + }, [ open ] ); 41 + 42 + return ( 43 + <> 44 + <button 45 + ref={ triggerRef } 46 + type="button" 47 + className="record-json__trigger" 48 + aria-label="View record JSON" 49 + onClick={ () => setOpen( true ) } 50 + > 51 + <svg 52 + width="14" 53 + height="14" 54 + viewBox="0 0 24 24" 55 + fill="none" 56 + stroke="currentColor" 57 + strokeWidth="2" 58 + strokeLinecap="round" 59 + strokeLinejoin="round" 60 + aria-hidden="true" 61 + > 62 + <path d="m16 18 6-6-6-6" /> 63 + <path d="m8 6-6 6 6 6" /> 64 + </svg> 65 + </button> 66 + { open && ( 67 + <div className="record-json__backdrop" onClick={ close }> 68 + <div 69 + className="record-json__dialog" 70 + role="dialog" 71 + aria-modal="true" 72 + aria-label="Record JSON" 73 + onClick={ ( event ) => event.stopPropagation() } 74 + > 75 + <button 76 + ref={ closeRef } 77 + type="button" 78 + className="record-json__close" 79 + aria-label="Close" 80 + onClick={ close } 81 + > 82 + × 83 + </button> 84 + <pre 85 + className="record-json__code" 86 + dangerouslySetInnerHTML={ { __html: highlightedHtml } } 87 + /> 88 + </div> 89 + </div> 90 + ) } 91 + </> 92 + ); 93 + }