Skip to content

Commit 87ce794

Browse files
heiskrCopilot
andauthored
Tighten code comments in frame article and layout components (#63436)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7c52b57f-2c29-4aa1-8233-99d0d6e13571
1 parent 8e658e7 commit 87ce794

22 files changed

Lines changed: 245 additions & 451 deletions

‎src/frame/components/ClientSideHashFocus.tsx‎

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import { useEffect } from 'react'
22

3-
// When an in-page anchor link is clicked (e.g. href="#section-id"),
4-
// browsers scroll to the target but may not move keyboard focus to it.
5-
// This component listens for hash changes and programmatically focuses
6-
// the target element so screen reader and keyboard users land at the
7-
// correct position.
3+
// Hash links such as href="#section-id" can scroll without moving keyboard focus.
4+
// Focusing the target keeps screen reader and keyboard users at the anchor.
85
export function ClientSideHashFocus() {
96
useEffect(() => {
107
const handleHashChange = () => {
@@ -17,8 +14,7 @@ export function ClientSideHashFocus() {
1714
}
1815
}
1916

20-
// Handle initial page load with a hash (e.g. direct link to
21-
// docs.github.com/en/discussions#guides-2)
17+
// Direct links such as /en/discussions#guides-2 need this before hashchange fires.
2218
handleHashChange()
2319

2420
window.addEventListener('hashchange', handleHashChange)

‎src/frame/components/ClientSideRefresh.tsx‎

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,20 @@
11
import { useRouter } from 'next/router'
22
import useSWR from 'swr'
33

4-
// This component is never mounted when you're in production mode.
5-
// Only when running in `NODE_ENV==='development'`.
6-
// It will reload the content every time the current page is focussed
7-
// (from being not focussed).
4+
// NODE_ENV === 'development' mounts this to reload the page when the tab regains focus.
5+
// SWR focus revalidation follows the Page Visibility API:
6+
// https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API
87
export default function ClientSideRefresh() {
98
const router = useRouter()
109

1110
useSWR(
1211
router.asPath,
1312
() => {
14-
// Remember, in NextJS, the `router.locale` is never including the
15-
// `router.asPath`. So we have to make sure it's always there
16-
// otherwise, after this hook runs, you lose that `/en` prefix
17-
// in the URL on the address bar.
13+
// Prefix router.asPath with router.locale so refreshes keep the localized URL.
1814
router.replace(`/${router.locale}${router.asPath}`, undefined, { scroll: false })
1915
},
2016
{
21-
// Implied here is that `revalidateOnFocus: true` which the default
22-
// and it means that the `useSWR` hook will make a listener on the
23-
// the Page Visibility API.
24-
// https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API
25-
// It effectively means that the callback of this hook will run every
26-
// time the browser window is put back to being visible.
27-
28-
// The `revalidateOnMount` is crucial because it means that we don't
29-
// bother executing the hook callback when it was first mounted
30-
// because, naturally, the first time you mount it, it will not
31-
// need to refresh because it's as fresh as it gets already.
17+
// Skip mount revalidation because initial content is current.
3218
revalidateOnMount: false,
3319
},
3420
)

‎src/frame/components/CodeTabsGroup.tsx‎

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,12 @@ import { sendEvent } from '@/events/components/events'
2424
import { EventType } from '@/events/types'
2525
import { useTranslation } from '@/languages/components/useTranslation'
2626

27-
// React-native replacement for the imperative CodeTabs enhancer (#6619). The old
28-
// component scanned `#article-contents` for `.ghd-codetabs`, inserted a foreign
29-
// `.ghd-codetabs-nav` mountPoint as the container's first child, portaled a nav
30-
// into it, and toggled panel attributes. Those mutations are destructive surgery
31-
// on React-owned nodes and break on client-side navigation teardown. Instead, the
32-
// article body hast maps each `.ghd-codetabs` container to <CodeTabsGroup>, which
33-
// reads its `.ghd-codetab` panel children straight from props and renders the nav
34-
// and panels itself. No DOM scanning, no portal, no foreign nodes.
35-
//
36-
// The selected language lives in CodeLanguageContext so multiple code-tab groups
37-
// on one page stay in sync and share the language cookie, matching the previous
38-
// single-component behavior.
27+
// CodeTabsGroup avoids DOM scanning, portals, and panel-attribute mutations so
28+
// React owns the code-tab nodes through client-side navigation.
29+
// The article body hast maps each .ghd-codetabs container to CodeTabsGroup, which
30+
// reads .ghd-codetab panel children from props and renders the nav and panels.
31+
// CodeLanguageContext keeps code-tab groups on a page in sync and shares the
32+
// language cookie.
3933

4034
type CodeLanguageContextT = {
4135
language: string
@@ -47,10 +41,9 @@ const CodeLanguageContext = createContext<CodeLanguageContextT>({
4741
setLanguage: () => {},
4842
})
4943

44+
// CodeTabsProvider starts empty so SSR and first hydration select each group's first tab.
45+
// It applies the cookie preference after hydration, avoiding an SSR/client mismatch.
5046
export function CodeTabsProvider({ children }: { children: ReactNode }) {
51-
// Start empty so server + first client render select each group's first tab
52-
// (deterministic, hydration-safe). The cookie preference is applied after
53-
// hydration, the same moment the old imperative enhancer used to run.
5447
const [language, setLanguageState] = useState('')
5548

5649
useEffect(() => {
@@ -104,8 +97,7 @@ export function CodeTabsGroup({ className, children, ...rest }: CodeTabsGroupPro
10497
const { language, setLanguage } = useContext(CodeLanguageContext)
10598
const baseId = useId()
10699

107-
// Pull the `.ghd-codetab` panel children straight from the converted hast. Fail
108-
// open (render the original markup) if the expected metadata isn't present.
100+
// Fail open to original markup if converted hast lacks code-tab metadata.
109101
const tabs: PanelTab[] = Children.toArray(children)
110102
.filter((child): child is ReactElement<{ className?: string }> => isValidElement(child))
111103
.filter((child) => hasClass(child.props.className, 'ghd-codetab'))

‎src/frame/components/CopyButton.tsx‎

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,13 @@ type CopyButtonProps = ComponentPropsWithoutRef<'button'> & {
66
'data-clipboard'?: string
77
}
88

9-
// React replacement for the imperative `copy-code.ts` enhancer. The code-block
10-
// header (`content-render/unified/code-header.ts`) emits this button into the
11-
// HTML AST next to a hidden `<pre data-clipboard="<id>">` holding the raw code.
12-
// When the article body is rendered from hast (instead of dangerouslySetInnerHTML),
13-
// `MarkdownContent` maps that `<button class="js-btn-copy">` to this component so
14-
// React owns the node rather than a post-hydration `document.querySelectorAll`.
15-
//
16-
// Analytics is intentionally NOT sent here: a global delegated click listener in
17-
// `events/components/events.ts` already records `.js-btn-copy` clicks.
9+
// React replacement for the imperative copy-code.ts enhancer. The code-block
10+
// header in content-render/unified/code-header.ts emits this button into the HTML
11+
// AST next to a hidden pre[data-clipboard] element that holds the raw code.
12+
// MarkdownContent maps button.js-btn-copy to CopyButton so React owns the node
13+
// instead of a post-hydration document.querySelectorAll pass.
14+
// Do not send analytics here: events/components/events.ts already records
15+
// .js-btn-copy clicks through its delegated click listener.
1816
export function CopyButton({ className, children, ...props }: CopyButtonProps) {
1917
const [copied, setCopied] = useState(false)
2018
const buttonRef = useRef<HTMLButtonElement>(null)
@@ -31,9 +29,7 @@ export function CopyButton({ className, children, ...props }: CopyButtonProps) {
3129
const handleClick = useCallback(async () => {
3230
if (!clipboardId) return
3331

34-
// The hidden <pre> is a sibling of this button inside the code-block header,
35-
// so look it up locally to avoid copying a different block that happens to
36-
// share the same content hash.
32+
// Look up the sibling hidden pre locally so a reused content hash cannot copy another block.
3733
const scope: Element | Document = buttonRef.current?.parentElement ?? document
3834
const pre = scope.querySelector<HTMLElement>(`pre[data-clipboard="${CSS.escape(clipboardId)}"]`)
3935
const text = pre?.innerText
@@ -42,8 +38,7 @@ export function CopyButton({ className, children, ...props }: CopyButtonProps) {
4238
try {
4339
await navigator.clipboard.writeText(text)
4440
} catch {
45-
// Clipboard write can be blocked (permissions, insecure context, etc.).
46-
// Don't show a false "Copied!" state.
41+
// A blocked clipboard write must not show a false Copied state.
4742
return
4843
}
4944

‎src/frame/components/DefaultLayout.module.scss‎

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,26 @@
55

66
.mainContent {
77
scroll-margin-top: 5rem;
8-
// Keep anything that overflows the main column from scrolling the page
9-
// sideways. This backstopped the article section rules while they overshot
10-
// ±1500px to reach the rails; those now span the body column and no further,
11-
// so it is only a guard for wide content (tables, code) today. `clip` (not
12-
// hidden/auto) creates no scroll container, so the sticky rails and secondary
13-
// bar are unaffected.
8+
// Clip wide content such as tables and code without creating a scroll container.
9+
// Sticky rails and the secondary bar still resolve against the viewport.
1410
overflow-x: clip;
1511
}
1612

17-
// The search results page splits into rail + results at brand's `medium`
18-
// breakpoint rather than waiting for `d-lg-flex` at 1012px, so the facet rail is
19-
// available on tablets. Applied only on /search.
13+
// Search results split into facet rail and results at Brand's medium breakpoint,
14+
// before d-lg-flex at 1012px, so filters are available on tablets. Applied only
15+
// on search results pages, versioned or not.
2016
.searchColumns {
2117
@media (min-width: $brand-breakpoint-medium) {
2218
display: flex;
2319
}
2420
}
2521

26-
// Sticky descendants share the header/bar offset; the sub-bar adds 40px.
27-
// These modifiers follow the same visibility state as OverviewSubBar.
22+
// Sticky descendants share the header and secondary-bar offset.
23+
// The sub-bar adds 40px, matching OverviewSubBar visibility.
2824
//
29-
// The header half of the stack comes from --docs-header-height, not a literal
30-
// 65px: the Brand header is 3.5rem below 48rem and 4rem above, so a fixed px
31-
// stack would be wrong at one of the two sizes. The 45px is the secondary bar,
32-
// which is a fixed height; keep it in step with DocsSecondaryBar.module.scss
33-
// and SidebarNav.module.scss, which compose the same two parts.
25+
// The header height comes from --docs-header-height because the Brand header is
26+
// 3.5rem below 48rem and 4rem above. Keep the fixed 45px secondary-bar height
27+
// in step with DocsSecondaryBar.module.scss and SidebarNav.module.scss.
3428
.stickyStack {
3529
--docs-sticky-stack: calc(var(--docs-header-height) + 45px);
3630
}
@@ -62,11 +56,8 @@
6256
}
6357
}
6458

65-
// The a11y skip link is a filled accent chip. Was primer/css
66-
// `color-bg-accent-emphasis color-fg-on-emphasis`, which paints the chip from
67-
// the PRC palette on a Brand surface. The Brand accent fill inverts between
68-
// modes (dark green in light, light green in dark), so onEmphasis text is
69-
// correct here.
59+
// The skip link uses the Brand accent fill so its onEmphasis text inverts with
60+
// the page's light and dark Brand surfaces.
7061
.skipButton {
7162
background-color: var(--brand-color-accent-primary);
7263
color: var(--brand-color-text-onEmphasis);

‎src/frame/components/DefaultLayout.tsx‎

Lines changed: 26 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,12 @@ const MINIMAL_RENDER = Boolean(JSON.parse(process.env.MINIMAL_RENDER || 'false')
2828

2929
type Props = {
3030
children?: React.ReactNode
31-
// Whether this page renders the right-rail "In this article" drawer (article +
32-
// automated pages do; REST reference pages do not). Controls whether the
33-
// secondary bar's collapsed Overview menu yields to the drawer at xxl.
31+
// Article and automated pages render the right-rail drawer; REST pages do not.
32+
// The secondary bar's collapsed Overview menu yields to that drawer at xxl.
3433
hasDrawer?: boolean
3534
}
35+
// The non-homepage branch wraps the secondary bar and article content in SelectionProvider
36+
// so the collapsed Overview menu and article body share platform/tool selection.
3637
export const DefaultLayout = (props: Props) => {
3738
const mainContext = useMainContext()
3839
const {
@@ -52,17 +53,14 @@ export const DefaultLayout = (props: Props) => {
5253
const { languages } = useLanguages()
5354
const [isNarrowMenuOpen, setIsNarrowMenuOpen] = useState(false)
5455

55-
// This is only true when we do search indexing which renders every page
56-
// just to be able to `cheerio` load the main body (and the meta
57-
// keywords tag).
56+
// Search indexing renders every page so Cheerio can read the body and meta keywords.
5857
if (MINIMAL_RENDER) {
5958
return (
6059
<div>
6160
<Head>
6261
<title>{page.fullTitle}</title>
6362
</Head>
6463

65-
{/* For local site search indexing */}
6664
<div className="d-none d-xl-block" data-search="breadcrumbs">
6765
<Breadcrumbs />
6866
</div>
@@ -143,7 +141,6 @@ export const DefaultLayout = (props: Props) => {
143141
<title>{page.fullTitle}</title>
144142
) : null}
145143

146-
{/* For Google and Bots */}
147144
<meta name="description" content={metaDescription} />
148145
{page.hidden && <meta name="robots" content="noindex" />}
149146
{Object.values(languages)
@@ -161,7 +158,6 @@ export const DefaultLayout = (props: Props) => {
161158
)
162159
})}
163160

164-
{/* For analytics events */}
165161
{router.locale && <meta name="path-language" content={router.locale} />}
166162
{currentVersion && <meta name="path-version" content={currentVersion} />}
167163
{currentProduct && <meta name="path-product" content={currentProduct.id} />}
@@ -178,7 +174,6 @@ export const DefaultLayout = (props: Props) => {
178174
)}
179175
{status && <meta name="status" content={status.toString()} />}
180176

181-
{/* OpenGraph data */}
182177
{page.fullTitle && (
183178
<>
184179
<meta property="og:site_name" content="GitHub Docs" />
@@ -188,15 +183,13 @@ export const DefaultLayout = (props: Props) => {
188183
<meta property="og:image" content={getSocialCardImage()} />
189184
</>
190185
)}
191-
{/* Twitter Meta Tags */}
192186
<meta name="twitter:card" content="summary" />
193187
<meta property="twitter:domain" content={new URL(fullUrl).hostname} />
194188
<meta property="twitter:url" content={fullUrl} />
195189
<meta name="twitter:title" content={page.fullTitle} />
196190
{page.introPlainText && <meta name="twitter:description" content={page.introPlainText} />}
197191
<meta name="twitter:image" content={getSocialCardImage()} />
198192

199-
{/* LLM-friendly alternate formats */}
200193
<link
201194
rel="alternate"
202195
type="text/markdown"
@@ -220,7 +213,6 @@ export const DefaultLayout = (props: Props) => {
220213
/>
221214
</Head>
222215

223-
{/* a11y */}
224216
<a
225217
href="#main-content"
226218
className={cx('visually-hidden skip-button', styles.skipButton)}
@@ -246,10 +238,6 @@ export const DefaultLayout = (props: Props) => {
246238
</div>
247239
</div>
248240
) : (
249-
// SelectionProvider wraps both the secondary bar and the content so the
250-
// bar's collapsed "In this article" menu (OverviewMenu) sees the same
251-
// platform/tool selection as the article body and filters its headings
252-
// accordingly.
253241
<SelectionProvider>
254242
<ActiveSectionProvider>
255243
<DocsSecondaryBar />
@@ -263,52 +251,45 @@ export const DefaultLayout = (props: Props) => {
263251
)
264252
}
265253

266-
// The doc-tree rail + content column, split out so it can read the collapse
267-
// context that DefaultLayout provides. On desktop the rail shows unless
268-
// collapsed; on mobile it shows inline (in the page flow, like desktop) only
269-
// when the nav is opened from the secondary bar. The content column (flex-1)
270-
// fills the row when the rail is absent.
254+
// LayoutBody reads SidebarCollapseContext after DefaultLayout provides it.
255+
// On mobile, the inline rail shows only when opened from the secondary bar.
256+
// LayoutBody matches SidebarNav's search-page gate instead of router.route
257+
// because src/pages/search.tsx and src/pages/[versionId]/search.tsx must agree
258+
// on the facet rail.
259+
// It mirrors OverviewSubBar's render gate so sticky-stack classes describe a bar
260+
// that renders.
261+
// Search results split the facet rail beside results at Brand's medium
262+
// breakpoint; route gating keeps other pages on d-lg-flex at 1012px.
263+
// The desktop rail-collapse cookie does not hide the open mobile nav. Otherwise
264+
// the content column hides with no drawer visible and shows a blank area instead
265+
// of the doc tree.
266+
// Search ignores the collapse cookie because it has no DocsSecondaryBar toggle
267+
// to restore filters.
268+
// Sticky elements need an explicit height because their scroll container sets
269+
// overflow:auto.
270+
// The sticky-stack class publishes the header and bar offset for descendants
271+
// such as article table headers.
272+
// Keeping OverviewSubBar inside main lets it start at the doc-tree drawer's
273+
// right edge and share that band with the drawer; mainContent uses overflow-x:clip,
274+
// so sticky still resolves against the viewport.
271275
type LayoutBodyProps = {
272276
children?: React.ReactNode
273277
hasDrawer?: boolean
274278
}
275279
const LayoutBody = ({ children, hasDrawer }: LayoutBodyProps) => {
276280
const { collapsed, mobileNavOpen } = useSidebarCollapsed()
277281
const { currentProduct } = useMainContext()
278-
// Matches SidebarNav's own gate rather than testing router.route. There are two search
279-
// pages, src/pages/search.tsx and src/pages/[versionId]/search.tsx, so a route test
280-
// for '/search' misses every versioned search URL, and this check would then disagree
281-
// with SidebarNav about whether the rail is a facet rail.
282282
const isSearchResultsPage = currentProduct?.id === 'search'
283-
// Mirrors OverviewSubBar's own render gate (it returns null at <= 1 item), so
284-
// the sticky-stack classes below describe the bar that actually renders.
285283
const miniTocItems = useMiniTocItems()
286284
const hasSubBar = miniTocItems.length > 1
287285
return (
288-
// `d-lg-flex` only goes side-by-side at 1012px. The search page's facet rail
289-
// is meant to sit beside the results from brand's `medium` breakpoint, so it
290-
// gets an earlier split of its own. Route-gated, so no other page moves.
291286
<div className={cx('d-lg-flex', isSearchResultsPage && styles.searchColumns)}>
292-
{/* `collapsed` is the desktop rail-collapse state (persisted). The inline
293-
mobile nav is independent, so still render the sidebar when it's open.
294-
Otherwise opening the mobile nav while the desktop rail is collapsed
295-
hides the content column (contentHiddenForNav) with no drawer to show,
296-
so the open nav displays a blank area instead of the doc tree.
297-
298-
Search is exempt: the cookie is shared with the doc-tree rail, but the
299-
search page has no toggle to undo it (DocsSecondaryBar returns null
300-
there), so honouring it would strand the filters with no way back. */}
301287
{collapsed && !mobileNavOpen && !isSearchResultsPage ? null : (
302288
<SidebarNav mobileOpen={mobileNavOpen} />
303289
)}
304-
{/* Need to set an explicit height for sticky elements since we also
305-
set overflow to auto */}
306290
<div
307291
className={cx(
308292
'flex-column flex-1 min-width-0',
309-
// Publish the sticky-stack height to everything in the column (article
310-
// table headers read it). Driven by the same values as OverviewSubBar's
311-
// visibility modifier just below, so the offset and the bar agree.
312293
styles.stickyStack,
313294
hasSubBar && styles.stickyStackWithSubBar,
314295
hasSubBar &&
@@ -318,14 +299,6 @@ const LayoutBody = ({ children, hasDrawer }: LayoutBodyProps) => {
318299
)}
319300
>
320301
<main id="main-content" className={styles.mainContent}>
321-
{/* Inside <main>, not before it: as a preceding sibling the "Skip to
322-
main content" link jumped the reader straight past the page's only
323-
in-article navigation. Still within the content column, so on
324-
desktop it starts at the doc-tree drawer's right edge and runs to
325-
the screen edge, sharing that band with the drawer rather than
326-
cutting across above it. (.mainContent uses `overflow-x: clip`,
327-
which creates no scroll container, so sticky still resolves against
328-
the viewport.) */}
329302
<OverviewSubBar hasDrawer={hasDrawer} />
330303
<DeprecationBanner />
331304
<RestBanner />

0 commit comments

Comments
 (0)