Stack with global z-index?

Issue

I have a DataTable in which some cells have links. Ideally, I would like to fetch a preview about the link’s content whenever hovering over the link, which I was able to achieve using the Stack widget. However, since the stacked preview is inside the DataCell, it seems like I’m not able to raise its "z-index" to be on top of the rest of the table.

messed up z-index

Is this not possible with Flutter, or is there a way around it?

The only way I imagine this working, without something to update a global z-index, would be for the cell to update a global state and then have the thumbnail preview appear on a Stack above the DataTable level. But I wish there was a less clunkier way to do it…

3 widgets I’ve tried but to no avail — they might work, I don’t know —:

  • Tooltip
  • Overlay
  • FloatingActionButton

My whole app is here, and the precise commit is 0303732. The relevant code is this ClickableLink widget:

import 'package:flutter/material.dart';
import 'package:flutter/gestures.dart';

import 'package:url_launcher/url_launcher.dart';

import '../schema/links.dart';

@immutable
class ClickableLink extends StatefulWidget {
  const ClickableLink({
    Key? key,
    required this.link,
    this.linkText,
    this.color = Colors.blue,
  }) : super(key: key);

  final Link link;
  final String? linkText;
  final Color color;

  @override
  State<ClickableLink> createState() => _ClickableLinkState();
}

class _ClickableLinkState extends State<ClickableLink> {
  Widget hoverWidget = const SizedBox.shrink();

  void _fetchPreview(PointerEvent pointerEvent) {
    setState(() {
      if (widget.link.host == 'online-go.com' && widget.link.prePath == 'game') {
        hoverWidget = Positioned(
          top: 25,
          child: Image.network('https://online-go.com/api/v1/games/${widget.link.id}/png'),
        );
      }
    });
  }

  void _onExit(PointerEvent pointerEvent) {
    setState(() {
      hoverWidget = const SizedBox.shrink();
    });
  }

  @override
  Widget build(BuildContext context) {
    return MouseRegion(
      onHover: _fetchPreview,
      onExit: _onExit,
      child: Stack(
        clipBehavior: Clip.none,
        children: [
          SelectableText.rich(
            TextSpan(
              text: widget.linkText ?? widget.link.id,
              style: TextStyle(color: widget.color),
              recognizer: TapGestureRecognizer()
                ..onTap = () async => launch(widget.link.completeLink),
            ),
          ),
          hoverWidget,
        ],
      ),
    );
  }
}

Solution

The problem here is due to the fact that your Stack widget, defined inside ClickableLink, will be at a "lower" point (inside your app widget tree) than every other GameResultCell.
So even the higher z-index will still be behind the other GameResultCells.

To fix this I would reccomend changing your structure and define an higher point in your structure to show the preview.

Another way could be using a library to nest your preview inside a tooltip. Take a look at this one for example:
just_the_tooltip: ^0.0.11+2. With this package, you could even use a StatelessWidget.

The result here is more similar to what I suppose you were expecting.

enter image description here

class ClickableLink extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return JustTheTooltip(
      content: Image.network(
        'https://online-go.com/api/v1/games/${widget.link.id}/png',
      ),
      child: SelectableText.rich(
        TextSpan(
          text: widget.linkText ?? widget.link.id,
          style: TextStyle(
            color: widget.color ??
                (DogempTheme.currentThemeIsLight(context)
                    ? const Color(0xff1158c7)
                    : Colors.orange.withOpacity(0.85)),
          ),
          recognizer: TapGestureRecognizer()
            ..onTap = () async => launch(widget.link.completeLink),
        ),
      ),
    );
  }
}

Lastly you could use a Dialog, but the resulting behaviour is a bit different.
Take a look at this code if you want to try:

class _ClickableLinkState extends State<ClickableLink> {
  Widget hoverWidget = const SizedBox.shrink();

  void _fetchPreview(PointerEvent pointerEvent) {
    showDialog(
      context: context,
      builder: (context) {
        return Dialog(
          backgroundColor: Colors.transparent,
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Image.network(
                  'https://online-go.com/api/v1/games/${widget.link.id}/png'),
              const SizedBox(
                height: 16.0,
              ),
              TextButton(
                  onPressed: () async => launch(widget.link.completeLink),
                  child: const Text('Go to complete link'))
            ],
          ),
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return MouseRegion(
      onHover: _fetchPreview,
      child: Stack(
        clipBehavior: Clip.none,
        children: [
          SelectableText.rich(
            TextSpan(
              text: widget.linkText ?? widget.link.id,
              style: TextStyle(
                color: widget.color ??
                    (DogempTheme.currentThemeIsLight(context)
                        ? const Color(0xff1158c7)
                        : Colors.orange.withOpacity(0.85)),
              ),
              recognizer: TapGestureRecognizer()
                ..onTap = () async => launch(widget.link.completeLink),
            ),
          ),
        ],
      ),
    );
  }
}

Answered By – L. Gangemi

Answer Checked By – David Goodson (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *