Popover
A popover displays rich content in a portal that is aligned to a target.
class _Popover extends StatefulWidget {
@override
State<_Popover> createState() => _State();
}
class _State extends State<_Popover> with SingleTickerProviderStateMixin {
late FPopoverController controller;
@override
void initState() {
super.initState();
controller = FPopoverController(vsync: this);
}
@override
Widget build(BuildContext context) => FPopover(
controller: controller,
followerBuilder: (context, style, _) => Padding(
padding: const EdgeInsets.only(left: 20, top: 14, right: 20, bottom: 10),
child: SizedBox(
width: 288,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Dimensions', style: context.theme.typography.base),
const SizedBox(height: 7),
Text(
'Set the dimensions for the layer.',
style: context.theme.typography.sm.copyWith(
color: context.theme.colorScheme.mutedForeground,
fontWeight: FontWeight.w300,
),
),
const SizedBox(height: 15),
for (final (label, value) in [
('Width', '100%'),
('Max. Width', '300px'),
('Height', '25px'),
('Max. Height', 'none'),
]) ...[
Row(
children: [
Expanded(child: Text(label, style: context.theme.typography.sm)),
Expanded(flex: 2, child: FTextField(initialValue: value)),
],
),
const SizedBox(height: 7),
]
],
),
),
),
target: IntrinsicWidth(
child: FButton(
style: FButtonStyle.outline,
onPress: controller.toggle,
label: const Text('Open popover'),
),
),
);
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
Usage
FPopover(...)
const FPopover(
controller: FPopoverController(),
followerAnchor: Alignment.topCenter,
targetAnchor: Alignment.bottomCenter,
directionPadding: false,
hideOnTapOutside: true,
shift: FPortalFollowerShift.flip,
follower: (context) => const Placeholder(),
child: const Placeholder(),
);
FPopover.tappable()
.
const FPopover.tappable(
controller: FPopoverController(),
followerAnchor: Alignment.topCenter,
targetAnchor: Alignment.bottomCenter,
directionPadding: false,
hideOnTapOutside: true,
shift: FPortalFollowerShift.flip,
follower: (context) => const Placeholder(),
child: const Placeholder(),
);
Examples
Horizontal Alignment
You can change how the popover is aligned to the button.
class _Popover extends StatefulWidget {
@override
State<_Popover> createState() => _State();
}
class _State extends State<_Popover> with SingleTickerProviderStateMixin {
late FPopoverController controller;
@override
void initState() {
super.initState();
controller = FPopoverController(vsync: this);
}
@override
Widget build(BuildContext context) => FPopover(
controller: controller,
followerAnchor: Alignment.topLeft,
targetAnchor: Alignment.topRight,
followerBuilder: (context, style, _) => Padding(
padding: const EdgeInsets.only(left: 20, top: 14, right: 20, bottom: 10),
child: SizedBox(
width: 288,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Dimensions', style: context.theme.typography.base),
const SizedBox(height: 7),
Text(
'Set the dimensions for the layer.',
style: context.theme.typography.sm.copyWith(
color: context.theme.colorScheme.mutedForeground,
fontWeight: FontWeight.w300,
),
),
const SizedBox(height: 15),
for (final (label, value) in [
('Width', '100%'),
('Max. Width', '300px'),
('Height', '25px'),
('Max. Height', 'none'),
]) ...[
Row(
children: [
Expanded(child: Text(label, style: context.theme.typography.sm)),
Expanded(flex: 2, child: FTextField(initialValue: value)),
],
),
const SizedBox(height: 7),
]
],
),
),
),
target: IntrinsicWidth(
child: FButton(
style: FButtonStyle.outline,
onPress: controller.toggle,
label: const Text('Open popover'),
),
),
);
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
Tapping outside Does Not Close Popover
class _Popover extends StatefulWidget {
@override
State<_Popover> createState() => _State();
}
class _State extends State<_Popover> with SingleTickerProviderStateMixin {
late FPopoverController controller;
@override
void initState() {
super.initState();
controller = FPopoverController(vsync: this);
}
@override
Widget build(BuildContext context) => FPopover(
controller: controller,
hideOnTapOutside: false,
followerBuilder: (context, style, _) => Padding(
padding: const EdgeInsets.only(left: 20, top: 14, right: 20, bottom: 10),
child: SizedBox(
width: 288,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Dimensions', style: context.theme.typography.base),
const SizedBox(height: 7),
Text(
'Set the dimensions for the layer.',
style: context.theme.typography.sm.copyWith(
color: context.theme.colorScheme.mutedForeground,
fontWeight: FontWeight.w300,
),
),
const SizedBox(height: 15),
for (final (label, value) in [
('Width', '100%'),
('Max. Width', '300px'),
('Height', '25px'),
('Max. Height', 'none'),
]) ...[
Row(
children: [
Expanded(child: Text(label, style: context.theme.typography.sm)),
Expanded(flex: 2, child: FTextField(initialValue: value)),
],
),
const SizedBox(height: 7),
]
],
),
),
),
target: IntrinsicWidth(
child: FButton(
style: FButtonStyle.outline,
onPress: controller.toggle,
label: const Text('Open popover'),
),
),
);
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
Flip along Axis
The popover can be flipped along the overflowing axis to stay within the viewport boundaries.
class _Popover extends StatefulWidget {
@override
State<_Popover> createState() => _State();
}
class _State extends State<_Popover> with SingleTickerProviderStateMixin {
late FPopoverController controller;
@override
void initState() {
super.initState();
controller = FPopoverController(vsync: this);
}
@override
Widget build(BuildContext context) => FPopover(
controller: controller,
shift: FPortalFollowerShift.flip,
followerBuilder: (context, style, _) => Padding(
padding: const EdgeInsets.only(left: 20, top: 14, right: 20, bottom: 10),
child: SizedBox(
width: 288,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Dimensions', style: context.theme.typography.base),
const SizedBox(height: 7),
Text(
'Set the dimensions for the layer.',
style: context.theme.typography.sm.copyWith(
color: context.theme.colorScheme.mutedForeground,
fontWeight: FontWeight.w300,
),
),
const SizedBox(height: 15),
for (final (label, value) in [
('Width', '100%'),
('Max. Width', '300px'),
('Height', '25px'),
('Max. Height', 'none'),
]) ...[
Row(
children: [
Expanded(child: Text(label, style: context.theme.typography.sm)),
Expanded(flex: 2, child: FTextField(initialValue: value)),
],
),
const SizedBox(height: 7),
]
],
),
),
),
target: IntrinsicWidth(
child: FButton(
style: FButtonStyle.outline,
onPress: controller.toggle,
label: const Text('Open popover'),
),
),
);
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
Shift along Axis
The popover can be shifted along the overflowing axis to stay within the viewport boundaries.
class _Popover extends StatefulWidget {
@override
State<_Popover> createState() => _State();
}
class _State extends State<_Popover> with SingleTickerProviderStateMixin {
late FPopoverController controller;
@override
void initState() {
super.initState();
controller = FPopoverController(vsync: this);
}
@override
Widget build(BuildContext context) => FPopover(
controller: controller,
shift: FPortalFollowerShift.along,
followerBuilder: (context, style, _) => Padding(
padding: const EdgeInsets.only(left: 20, top: 14, right: 20, bottom: 10),
child: SizedBox(
width: 288,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Dimensions', style: context.theme.typography.base),
const SizedBox(height: 7),
Text(
'Set the dimensions for the layer.',
style: context.theme.typography.sm.copyWith(
color: context.theme.colorScheme.mutedForeground,
fontWeight: FontWeight.w300,
),
),
const SizedBox(height: 15),
for (final (label, value) in [
('Width', '100%'),
('Max. Width', '300px'),
('Height', '25px'),
('Max. Height', 'none'),
]) ...[
Row(
children: [
Expanded(child: Text(label, style: context.theme.typography.sm)),
Expanded(flex: 2, child: FTextField(initialValue: value)),
],
),
const SizedBox(height: 7),
]
],
),
),
),
target: IntrinsicWidth(
child: FButton(
style: FButtonStyle.outline,
onPress: controller.toggle,
label: const Text('Open popover'),
),
),
);
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
No Shifting
The popover is not shifted to stay within the viewport boundaries, even if it overflows.
class _Popover extends StatefulWidget {
@override
State<_Popover> createState() => _State();
}
class _State extends State<_Popover> with SingleTickerProviderStateMixin {
late FPopoverController controller;
@override
void initState() {
super.initState();
controller = FPopoverController(vsync: this);
}
@override
Widget build(BuildContext context) => FPopover(
controller: controller,
shift: FPortalFollowerShift.none,
followerBuilder: (context, style, _) => Padding(
padding: const EdgeInsets.only(left: 20, top: 14, right: 20, bottom: 10),
child: SizedBox(
width: 288,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Dimensions', style: context.theme.typography.base),
const SizedBox(height: 7),
Text(
'Set the dimensions for the layer.',
style: context.theme.typography.sm.copyWith(
color: context.theme.colorScheme.mutedForeground,
fontWeight: FontWeight.w300,
),
),
const SizedBox(height: 15),
for (final (label, value) in [
('Width', '100%'),
('Max. Width', '300px'),
('Height', '25px'),
('Max. Height', 'none'),
]) ...[
Row(
children: [
Expanded(child: Text(label, style: context.theme.typography.sm)),
Expanded(flex: 2, child: FTextField(initialValue: value)),
],
),
const SizedBox(height: 7),
]
],
),
),
),
target: IntrinsicWidth(
child: FButton(
style: FButtonStyle.outline,
onPress: controller.toggle,
label: const Text('Open popover'),
),
),
);
@override
void dispose() {
controller.dispose();
super.dispose();
}
}