Skip to content

Commit 73ecf6a

Browse files
authored
feat(ui,docs): complete two-search-box cross-link contract (#1022) (#1083)
Closes the reciprocal half of the two-search-box contract from PR #1081. Forward direction (already shipped in PR #1081): AppSearchBox results popover -> /docs/search/?q=<term> Reverse direction (this PR): - mkdocs Material override (mkdocs/overrides/main.html) injects a 'Search the rest of the site' link into every docs page footer area. - JS rewrites the anchor href to '/?q=<term>' when the docs URL carries a 'q' param, so the search continues seamlessly into the home AppSearchBox. Static fallback href='/' works without JS. - mkdocs.yml gains 'theme.custom_dir: overrides' to activate the override. - mkdocs/stylesheets/extra.css adds .hp-docs-cross-link styling using Material's design tokens (no hard-coded colors). Home AppSearchBox plumbing: - HomeShell exposes the appSearch slot (mirrors SectionShell). - app/page.tsx renders <AppSearchBox audience='home' /> via the slot, so '/?q=<term>' lands on a page that has the search box visible. - AppSearchBox seeds the input from window.location.search ?q= on mount and auto-opens the popover so the user sees results immediately. User typing always overrides the seeded value. Tests: - tests/unit/appSearchUrlSeed.test.tsx (4 cases) - seeds when ?q=architecture, doesn't seed when empty/absent, - typing overrides the seeded value. Validation: - yarn type-check OK; yarn lint clean (max-warnings 0). - 73 suites / 431 tests passing. - yarn test:a11y all 4 AA cases passing. - mkdocs build emits .hp-docs-cross-link aside on every docs page.
1 parent 4b22383 commit 73ecf6a

7 files changed

Lines changed: 168 additions & 4 deletions

File tree

apps/ui/app/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Metadata } from 'next';
22

3+
import { AppSearchBox } from '@/components/molecules/AppSearchBox';
34
import { CallToAction } from '@/components/molecules/CallToAction';
45
import { Hero } from '@/components/molecules/Hero';
56
import { ValuePropGrid } from '@/components/molecules/ValuePropGrid';
@@ -28,7 +29,7 @@ export const metadata: Metadata = buildMetadata({
2829
*/
2930
export default function HomePage() {
3031
return (
31-
<HomeShell>
32+
<HomeShell appSearch={<AppSearchBox audience="home" />}>
3233
<Hero
3334
kind="audience-router"
3435
headline="Intelligent retail, built on Azure's agentic platform."

apps/ui/components/molecules/AppSearchBox.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,24 @@ export function AppSearchBox({
140140
const [open, setOpen] = useState(false);
141141
const [activeIndex, setActiveIndex] = useState(-1);
142142

143+
// Seed the query from the `?q=` URL parameter (Issue #1022 cross-link
144+
// contract). When the mkdocs Material search results page links to
145+
// `/?q=<term>`, the home AppSearchBox should pre-populate so the user
146+
// continues their search seamlessly. Only runs once on mount; we never
147+
// overwrite user typing.
148+
useEffect(() => {
149+
if (typeof window === 'undefined') {
150+
return;
151+
}
152+
const params = new URLSearchParams(window.location.search);
153+
const incoming = params.get('q');
154+
if (incoming && incoming.length > 0) {
155+
setQuery(incoming);
156+
setOpen(true);
157+
}
158+
// eslint-disable-next-line react-hooks/exhaustive-deps -- mount only
159+
}, []);
160+
143161
const allowedAudiences = AUDIENCE_FILTER[audience];
144162

145163
const hits: SearchHit[] = useMemo(

apps/ui/components/templates/HomeShell.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,27 @@ import { SectionShell } from '../shared/SectionShell';
99
* - `laneSwitch` : the lane-switch persona toggle (filled by the
1010
* audience-IA persona switcher; never rendered on `/`
1111
* for the audience-router itself)
12+
* - `appSearch` : the in-app search box (Issue #1022). Rendered in the
13+
* header so the home page is searchable, with the same
14+
* cross-link contract to the mkdocs Material search.
1215
*
13-
* Adding a fifth shell variant requires amending ADR-034.
16+
* Adding a sixth shell variant requires amending ADR-034.
1417
*/
1518
export type HomeShellProps = {
1619
children: ReactNode;
1720
breadcrumb?: ReactNode;
1821
laneSwitch?: ReactNode;
22+
appSearch?: ReactNode;
1923
};
2024

21-
export function HomeShell({ children, breadcrumb, laneSwitch }: HomeShellProps): ReactElement {
25+
export function HomeShell({ children, breadcrumb, laneSwitch, appSearch }: HomeShellProps): ReactElement {
2226
return (
23-
<SectionShell variant="home" breadcrumb={breadcrumb} laneSwitch={laneSwitch}>
27+
<SectionShell
28+
variant="home"
29+
breadcrumb={breadcrumb}
30+
laneSwitch={laneSwitch}
31+
appSearch={appSearch}
32+
>
2433
{children}
2534
</SectionShell>
2635
);
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* Cross-link contract tests (Issue #1022).
3+
*
4+
* Validates that the reciprocal half of the two-search-box contract is in
5+
* place: a `?q=<term>` URL param landing on the home page seeds the
6+
* AppSearchBox so the user continues their search seamlessly. The forward
7+
* direction (AppSearchBox results → /docs/search/?q=...) is covered by
8+
* `appSearchMatcher.test.ts` and `AppSearchBox.test.tsx`.
9+
*/
10+
import { fireEvent, render, screen } from '@testing-library/react';
11+
12+
import { AppSearchBox } from '@/components/molecules/AppSearchBox';
13+
14+
describe('AppSearchBox URL ?q= seed (Issue #1022 reciprocal cross-link)', () => {
15+
beforeEach(() => {
16+
// Reset URL between cases so each test owns its query state.
17+
window.history.replaceState({}, '', '/');
18+
});
19+
20+
it('seeds the input when ?q= is present in the URL', () => {
21+
window.history.replaceState({}, '', '/?q=architecture');
22+
render(<AppSearchBox audience="home" />);
23+
const input = screen.getByPlaceholderText(
24+
'Search retailers + builders + deploy',
25+
) as HTMLInputElement;
26+
expect(input.value).toBe('architecture');
27+
// Auto-opens so the user sees results immediately.
28+
expect(screen.getByRole('listbox')).toBeInTheDocument();
29+
});
30+
31+
it('does not seed when ?q= is empty', () => {
32+
window.history.replaceState({}, '', '/?q=');
33+
render(<AppSearchBox audience="home" />);
34+
const input = screen.getByPlaceholderText(
35+
'Search retailers + builders + deploy',
36+
) as HTMLInputElement;
37+
expect(input.value).toBe('');
38+
expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
39+
});
40+
41+
it('does not seed when ?q= is absent', () => {
42+
window.history.replaceState({}, '', '/');
43+
render(<AppSearchBox audience="retailer" />);
44+
const input = screen.getByPlaceholderText('Search retailer pages') as HTMLInputElement;
45+
expect(input.value).toBe('');
46+
});
47+
48+
it('user typing overrides the seeded value', () => {
49+
window.history.replaceState({}, '', '/?q=initial');
50+
render(<AppSearchBox audience="home" />);
51+
const input = screen.getByPlaceholderText(
52+
'Search retailers + builders + deploy',
53+
) as HTMLInputElement;
54+
expect(input.value).toBe('initial');
55+
fireEvent.change(input, { target: { value: 'overridden' } });
56+
expect(input.value).toBe('overridden');
57+
});
58+
});

mkdocs/mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ site_dir: site
77

88
theme:
99
name: material
10+
custom_dir: overrides
1011
palette:
1112
- scheme: default
1213
primary: deep purple

mkdocs/overrides/main.html

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{#-
2+
mkdocs Material `main.html` override (Issue #1022).
3+
4+
Adds the reciprocal "Search the rest of the site" cross-link required by
5+
the two-search-box contract (ADR-034 + capability 42). The corresponding
6+
forward-link lives inside the in-app `AppSearchBox` molecule's results
7+
popover ("Search the docs →").
8+
9+
When the user is on a docs page with a `?q=` parameter, the JS snippet
10+
rewrites the link target to `/?q=<term>` so the query continues into the
11+
home AppSearchBox. The static fallback target is `/` — works without JS,
12+
lands on the audience-router home which exposes its own search box.
13+
14+
Telemetry: `data-telemetry="docs-cross-link-click"` so App Insights can
15+
correlate cross-search clicks with the forward direction
16+
(`app-search-cross-link-click`).
17+
-#}
18+
{% extends "base.html" %}
19+
20+
{% block content %}
21+
{{ super() }}
22+
<aside class="hp-docs-cross-link" aria-label="Cross-site search">
23+
<p>
24+
Looking for product, retailer, or builder pages?
25+
<a
26+
href="/"
27+
id="hp-docs-cross-link-anchor"
28+
data-telemetry="docs-cross-link-click"
29+
>Search the rest of the site →</a>
30+
</p>
31+
</aside>
32+
<script>
33+
(function () {
34+
var anchor = document.getElementById('hp-docs-cross-link-anchor');
35+
if (!anchor) {
36+
return;
37+
}
38+
try {
39+
var params = new URLSearchParams(window.location.search);
40+
var q = params.get('q');
41+
if (q && q.length > 0) {
42+
var target = '/?q=' + encodeURIComponent(q);
43+
anchor.setAttribute('href', target);
44+
}
45+
} catch (e) {
46+
/* defensive — static '/' fallback already lands on the home shell */
47+
}
48+
})();
49+
</script>
50+
{% endblock %}

mkdocs/stylesheets/extra.css

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,30 @@
2020
.mermaid {
2121
text-align: center;
2222
}
23+
24+
/*
25+
* Cross-link footer banner (Issue #1022).
26+
*
27+
* Renders the reciprocal "Search the rest of the site" link added by
28+
* mkdocs/overrides/partials/footer.html. Visually low-key but always
29+
* present so docs readers can pivot back to the app's audience-IA pages.
30+
*/
31+
.hp-docs-cross-link {
32+
margin: 1rem auto 0.5rem;
33+
padding: 0.5rem 1rem;
34+
max-width: 1440px;
35+
border-top: 1px solid var(--md-default-fg-color--lightest);
36+
text-align: center;
37+
font-size: 0.85rem;
38+
color: var(--md-default-fg-color--light);
39+
}
40+
41+
.hp-docs-cross-link a {
42+
font-weight: 600;
43+
text-decoration: none;
44+
}
45+
46+
.hp-docs-cross-link a:hover,
47+
.hp-docs-cross-link a:focus {
48+
text-decoration: underline;
49+
}

0 commit comments

Comments
 (0)