Some checks failed
Security / Security check (push) Failing after 2s
New ChainInlineHelp (intro strip: what this is + what will happen, with an optional 'Learn more' into the doc sheet) and ChainFieldHelp / ChainFieldLabel (a '?' affordance per field). First applied to the add-satellite dialog, which asked for a bare 'name' with no hint of what a satellite is or does (usertest): it now leads with a plain explanation + a federation 'Learn more', and the name field carries a '?'. Both widgets are quiet by design. Verified: field_help_test covers the widgets + that the dialog explains itself; dialog_shots_test.dart (a reusable headed dialog-capture harness) proved the layout in light + dark. Studio 0.76.0. Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
169 lines
5.4 KiB
Dart
169 lines
5.4 KiB
Dart
// In-place help — the "explain it where it happens" pattern
|
|
// (usertest: the Add-satellite dialog asked for a name with no hint
|
|
// of what a satellite is or what the name does). Two pieces:
|
|
//
|
|
// • ChainInlineHelp — a calm intro strip at the top of a dialog
|
|
// or surface: one plain sentence saying what this is and what
|
|
// will happen, with an optional "Learn more" link into the
|
|
// full doc sheet.
|
|
// • ChainFieldHelp — a small "?" affordance to sit next to a
|
|
// single field's label; tap/hover reveals a one-line
|
|
// explanation. Use it only where a field genuinely needs it.
|
|
//
|
|
// Both are intentionally quiet: help should be present, not loud.
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import '../theme/tokens.dart';
|
|
|
|
/// A one-sentence intro strip for the top of a dialog or panel.
|
|
/// [onLearnMore] wires the "Learn more" link to a doc sheet
|
|
/// (`showFaiDoc`), shown only when provided.
|
|
class ChainInlineHelp extends StatelessWidget {
|
|
final String text;
|
|
final IconData icon;
|
|
final VoidCallback? onLearnMore;
|
|
final String? learnMoreLabel;
|
|
|
|
const ChainInlineHelp({
|
|
super.key,
|
|
required this.text,
|
|
this.icon = Icons.info_outline,
|
|
this.onLearnMore,
|
|
this.learnMoreLabel,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
return Container(
|
|
padding: const EdgeInsets.all(ChainSpace.md),
|
|
decoration: BoxDecoration(
|
|
color: theme.colorScheme.surfaceContainerHigh,
|
|
borderRadius: BorderRadius.circular(ChainRadius.sm),
|
|
border: Border.all(color: theme.colorScheme.outlineVariant),
|
|
),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Icon(icon, size: 16, color: theme.colorScheme.primary),
|
|
const SizedBox(width: ChainSpace.sm),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
text,
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
color: theme.colorScheme.onSurface,
|
|
height: 1.4,
|
|
),
|
|
),
|
|
if (onLearnMore != null) ...[
|
|
const SizedBox(height: ChainSpace.xs),
|
|
InkWell(
|
|
onTap: onLearnMore,
|
|
borderRadius: BorderRadius.circular(ChainRadius.sm),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 2),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
learnMoreLabel ?? 'Learn more',
|
|
style: theme.textTheme.labelSmall?.copyWith(
|
|
color: theme.colorScheme.primary,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
const SizedBox(width: 2),
|
|
Icon(
|
|
Icons.arrow_forward,
|
|
size: 12,
|
|
color: theme.colorScheme.primary,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// A "?" info affordance for a single field. Sit it next to the
|
|
/// field's label; hover shows the [message] as a tooltip, and a
|
|
/// tap reveals it too (touch / keyboard users who don't hover).
|
|
/// Semantics carry [message] for screen readers.
|
|
class ChainFieldHelp extends StatefulWidget {
|
|
final String message;
|
|
|
|
const ChainFieldHelp({super.key, required this.message});
|
|
|
|
@override
|
|
State<ChainFieldHelp> createState() => _ChainFieldHelpState();
|
|
}
|
|
|
|
class _ChainFieldHelpState extends State<ChainFieldHelp> {
|
|
final _tooltipKey = GlobalKey<TooltipState>();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
return Tooltip(
|
|
key: _tooltipKey,
|
|
message: widget.message,
|
|
waitDuration: const Duration(milliseconds: 300),
|
|
triggerMode: TooltipTriggerMode.manual,
|
|
preferBelow: false,
|
|
child: Semantics(
|
|
button: true,
|
|
label: widget.message,
|
|
child: InkResponse(
|
|
radius: 14,
|
|
// Manual trigger so a tap (not just hover) reveals it.
|
|
onTap: () => _tooltipKey.currentState?.ensureTooltipVisible(),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(2),
|
|
child: Icon(
|
|
Icons.help_outline,
|
|
size: 14,
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// A field label with a trailing [ChainFieldHelp]. Convenience for
|
|
/// the common "label + ?" row above a TextField.
|
|
class ChainFieldLabel extends StatelessWidget {
|
|
final String label;
|
|
final String help;
|
|
|
|
const ChainFieldLabel({super.key, required this.label, required this.help});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
return Row(
|
|
children: [
|
|
Text(
|
|
label,
|
|
style: theme.textTheme.labelMedium?.copyWith(
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
const SizedBox(width: 4),
|
|
ChainFieldHelp(message: help),
|
|
],
|
|
);
|
|
}
|
|
}
|