Skip to content

Commit 8834648

Browse files
committed
Merge branch 'main' into feat/file-tree
2 parents ed8c49b + bc9f9ea commit 8834648

File tree

20 files changed

+649
-90
lines changed

20 files changed

+649
-90
lines changed

site/lib/_sass/_site.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
@use 'components/tabs';
3737
@use 'components/theming';
3838
@use 'components/toc';
39+
@use 'components/tooltip';
3940
@use 'components/trailing';
4041

4142
// Styles for specific pages, alphabetically ordered.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
.tooltip-wrapper {
2+
position: relative;
3+
4+
a.tooltip-target {
5+
color: inherit;
6+
text-decoration: underline;
7+
text-decoration-style: dotted;
8+
}
9+
10+
.tooltip {
11+
visibility: hidden;
12+
13+
display: flex;
14+
position: absolute;
15+
z-index: var(--site-z-floating);
16+
top: 100%;
17+
left: 50%;
18+
transform: translateX(-50%);
19+
20+
flex-flow: column nowrap;
21+
width: 16rem;
22+
23+
background: var(--site-raised-bgColor);
24+
border: 0.05rem solid rgba(0, 0, 0, .125);
25+
border-radius: 0.75rem;
26+
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, .15);
27+
padding: 0.8rem;
28+
29+
font-size: 1rem;
30+
font-weight: normal;
31+
font-style: normal;
32+
33+
.tooltip-header {
34+
font-size: 1.2rem;
35+
font-weight: 500;
36+
margin-bottom: 0.25rem;
37+
}
38+
39+
.tooltip-content {
40+
font-size: 0.875rem;
41+
color: var(--site-secondary-textColor);
42+
}
43+
}
44+
45+
// On non-touch devices, show tooltip on hover or focus.
46+
@media all and not (pointer: coarse) {
47+
&:hover .tooltip {
48+
visibility: visible;
49+
}
50+
51+
&:focus-within .tooltip {
52+
visibility: visible;
53+
}
54+
}
55+
56+
// On touch devices, show tooltip on click (see global_scripts.dart).
57+
@media all and (pointer: coarse) {
58+
.tooltip.visible {
59+
visibility: visible;
60+
}
61+
}
62+
}

site/lib/main.dart

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,26 @@ Component get _docsFlutterDevSite => ContentApp.custom(
6565
components: _embeddableComponents,
6666
layouts: const [DocLayout(), TocLayout(), CatalogPageLayout()],
6767
theme: const ContentTheme.none(),
68-
secondaryOutputs: [const RobotsTxtOutput(), MarkdownOutput()],
68+
secondaryOutputs: [
69+
const RobotsTxtOutput(),
70+
MarkdownOutput(
71+
createHeader: (page) {
72+
final header = StringBuffer();
73+
if (page.data.page['title'] case final String title
74+
when title.isNotEmpty) {
75+
header.writeln('# $title');
76+
77+
if (page.data.page['description'] case final String description
78+
when description.isNotEmpty) {
79+
header.writeln();
80+
header.writeln('> $description');
81+
}
82+
}
83+
84+
return header.toString();
85+
},
86+
),
87+
],
6988
),
7089
);
7190

site/lib/src/client/global_scripts.dart

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ void _setUpSite() {
4444
_setUpExpandableCards();
4545
_setUpPlatformKeys();
4646
_setUpToc();
47+
_setUpTooltips();
4748
}
4849

4950
void _setUpSearchKeybindings() {
@@ -448,3 +449,93 @@ void _setUpTocActiveObserver() {
448449
observer.observe(headings.item(i) as web.Element);
449450
}
450451
}
452+
453+
void _setUpTooltips() {
454+
final tooltipWrappers = web.document.querySelectorAll('.tooltip-wrapper');
455+
456+
final isTouchscreen = web.window.matchMedia('(pointer: coarse)').matches;
457+
458+
void setup({required bool setUpClickListener}) {
459+
for (var i = 0; i < tooltipWrappers.length; i++) {
460+
final linkWrapper = tooltipWrappers.item(i) as web.HTMLElement;
461+
final target = linkWrapper.querySelector('.tooltip-target');
462+
final tooltip = linkWrapper.querySelector('.tooltip') as web.HTMLElement?;
463+
464+
if (target == null || tooltip == null) {
465+
continue;
466+
}
467+
_ensureVisible(tooltip);
468+
469+
if (setUpClickListener && isTouchscreen) {
470+
// On touchscreen devices, toggle tooltip visibility on tap.
471+
target.addEventListener(
472+
'click',
473+
((web.Event e) {
474+
final isVisible = tooltip.classList.contains('visible');
475+
if (!isVisible) {
476+
tooltip.classList.add('visible');
477+
e.preventDefault();
478+
}
479+
}).toJS,
480+
);
481+
}
482+
}
483+
}
484+
485+
void closeAll() {
486+
final visibleTooltips = web.document.querySelectorAll(
487+
'.tooltip.visible',
488+
);
489+
for (var i = 0; i < visibleTooltips.length; i++) {
490+
final tooltip = visibleTooltips.item(i) as web.HTMLElement;
491+
tooltip.classList.remove('visible');
492+
}
493+
}
494+
495+
setup(setUpClickListener: true);
496+
497+
// Reposition tooltips on window resize.
498+
web.EventStreamProviders.resizeEvent.forTarget(web.window).listen((_) {
499+
setup(setUpClickListener: false);
500+
});
501+
502+
// Close tooltips when clicking outside of any tooltip wrapper.
503+
web.EventStreamProviders.clickEvent.forTarget(web.document).listen((e) {
504+
if ((e.target as web.Element).closest('.tooltip-wrapper') == null) {
505+
closeAll();
506+
}
507+
});
508+
509+
// On touchscreen devices, close tooltips when scrolling.
510+
if (isTouchscreen) {
511+
web.EventStreamProviders.scrollEvent.forTarget(web.window).listen((_) {
512+
closeAll();
513+
});
514+
}
515+
}
516+
517+
/// Adjust the tooltip position to ensure it is fully inside the
518+
/// ancestor .content element.
519+
void _ensureVisible(web.HTMLElement tooltip) {
520+
final containerRect = tooltip.closest('.content')?.getBoundingClientRect();
521+
final tooltipRect = tooltip.getBoundingClientRect();
522+
final offset = double.parse(tooltip.getAttribute('data-adjusted') ?? '0');
523+
524+
final tooltipLeft = tooltipRect.left - offset;
525+
final tooltipRight = tooltipRect.right - offset;
526+
final containerLeft = containerRect?.left ?? 0.0;
527+
final containerRight = containerRect?.right ?? web.window.innerWidth;
528+
529+
if (tooltipLeft < containerLeft) {
530+
final offset = containerLeft - tooltipLeft;
531+
tooltip.style.left = 'calc(50% + ${offset}px)';
532+
tooltip.dataset['adjusted'] = offset.toString();
533+
} else if (tooltipRight > containerRight) {
534+
final offset = tooltipRight - containerRight;
535+
tooltip.style.left = 'calc(50% - ${offset}px)';
536+
tooltip.dataset['adjusted'] = (-offset).toString();
537+
} else {
538+
tooltip.style.left = '50%';
539+
tooltip.dataset['adjusted'] = '0';
540+
}
541+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Copyright 2025 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:jaspr/jaspr.dart';
6+
import 'package:jaspr_content/jaspr_content.dart';
7+
8+
import '../pages/glossary.dart';
9+
import '../util.dart';
10+
11+
/// A node-processing, page extension for Jaspr Content that looks for links to
12+
/// glossary entries and enhances them with interactive glossary tooltips.
13+
class GlossaryLinkProcessor implements PageExtension {
14+
const GlossaryLinkProcessor();
15+
16+
@override
17+
Future<List<Node>> apply(Page page, List<Node> nodes) async {
18+
final glossary = Glossary.fromList(page.data['glossary'] as List<Object?>);
19+
return _processNodes(nodes, glossary);
20+
}
21+
22+
List<Node> _processNodes(List<Node> nodes, Glossary glossary) {
23+
final processedNodes = <Node>[];
24+
25+
for (final node in nodes) {
26+
if (node is ElementNode &&
27+
node.tag == 'a' &&
28+
node.attributes['href']?.startsWith('/resources/glossary') == true) {
29+
// Found a glossary link, extract its id from the url and
30+
// create the tooltip component.
31+
32+
final id = Uri.parse(node.attributes['href']!).fragment;
33+
final entry = glossary.entries.where((e) => e.id == id).firstOrNull;
34+
35+
if (entry == null) {
36+
// If the glossary entry is not found, keep the original node.
37+
processedNodes.add(node);
38+
continue;
39+
}
40+
41+
processedNodes.add(
42+
ElementNode(
43+
'span',
44+
{'class': 'tooltip-wrapper'},
45+
[
46+
ElementNode('a', {
47+
...node.attributes,
48+
'class': [
49+
?node.attributes['class'],
50+
'tooltip-target',
51+
].toClasses,
52+
}, node.children),
53+
ComponentNode(GlossaryTooltip(entry: entry)),
54+
],
55+
),
56+
);
57+
} else if (node is ElementNode && node.children != null) {
58+
processedNodes.add(
59+
ElementNode(
60+
node.tag,
61+
node.attributes,
62+
_processNodes(node.children!, glossary),
63+
),
64+
);
65+
} else {
66+
processedNodes.add(node);
67+
}
68+
}
69+
70+
return processedNodes;
71+
}
72+
}
73+
74+
class GlossaryTooltip extends StatelessComponent {
75+
const GlossaryTooltip({required this.entry});
76+
77+
final GlossaryEntry entry;
78+
79+
@override
80+
Component build(BuildContext context) {
81+
return span(classes: 'tooltip', [
82+
span(classes: 'tooltip-header', [text(entry.term)]),
83+
span(classes: 'tooltip-content', [
84+
text(entry.shortDescription),
85+
text(' '),
86+
a(
87+
href: '/resources/glossary#${entry.id}',
88+
attributes: {
89+
'title':
90+
'Learn more about \'${entry.term}\' and '
91+
'find related resources.',
92+
},
93+
[text('Learn more')],
94+
),
95+
]),
96+
]);
97+
}
98+
}

site/lib/src/extensions/registry.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:jaspr_content/jaspr_content.dart';
66

77
import 'attribute_processor.dart';
88
import 'code_block_processor.dart';
9+
import 'glossary_link_processor.dart';
910
import 'header_extractor.dart';
1011
import 'header_processor.dart';
1112
import 'table_processor.dart';
@@ -18,4 +19,5 @@ const List<PageExtension> allNodeProcessingExtensions = [
1819
HeaderWrapperExtension(),
1920
TableWrapperExtension(),
2021
CodeBlockProcessor(),
22+
GlossaryLinkProcessor(),
2123
];

site/lib/src/style_hash.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
// dart format off
33

44
/// The generated hash of the `main.css` file.
5-
const generatedStylesHash = 'GGUgw576R9j6';
5+
const generatedStylesHash = 'ZFZ+YS8Vr+JP';

site/pubspec.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ environment:
99
dependencies:
1010
build: ^4.0.2
1111
collection: ^1.19.1
12-
crypto: ^3.0.6
12+
crypto: ^3.0.7
1313
html: ^0.15.6
1414
http: ^1.5.0
1515
jaspr: ^0.21.6
@@ -32,9 +32,9 @@ dev_dependencies:
3232
path: pkgs/analysis_defaults
3333
ref: f91ed8ecef6a0b31685804fe4102b25fda021460
3434
build_runner: ^2.10.1
35-
build_web_compilers: ^4.3.0
35+
build_web_compilers: ^4.4.0
3636
jaspr_builder: ^0.21.6
37-
sass: ^1.93.2
37+
sass: ^1.93.3
3838
sass_builder: ^2.4.0
3939

4040
jaspr:
582 KB
Loading
1.8 MB
Loading

0 commit comments

Comments
 (0)