Repository: sphinx-gallery/sphinx-gallery

Issue:
Make thumbnails grid- and flexbox-compatible
At the moment, sphinx-gallery generates a div for each gallery item.
These divs are `float: left` and have a pre-determined width, which does not render very nicely on most screen sizes or for long titles.

I suggest that for each subsection of the gallery, we add a parent div to these item divs.
This would allow users to either use css `grid` or `flex` (which we could also implement here directly).
It boils down to adding a new div before iterating through each gallery item and closing this div after:
https://github.com/sphinx-gallery/sphinx-gallery/blob/a68a48686138741ac38dcc15930293caeebcf484/sphinx_gallery/gen_rst.py#L371-L382

Also, maybe we could use [`sphinx-design`](https://github.com/executablebooks/sphinx-design) to handle the grid for us, but this would add a new dependency.

Happy to here what you think about this!

Discussion:
alexisthual: I think this PR is good to go, let me know what you think :slightly_smiling_face: 

Here are some explanations to ease the code review:
- there is a new div `.sphx-glr-thumbnails` which is the parent div of all gallery items. It is a css grid (flexbox doesn't render well with `justify-content: space-between`)
- `.sphx-glr-thumbcontainer` is now light/dark theme compatible; it uses flexbox to align children horizontally
- instead of using HTML figure + figure caption to generate thumbnails, we now have img + div with text
- an HTML `a` link covers the entire `.sphx-glr-thumbcontainer` for users to click
- prettified `theme_override.css` with prettierjs :lipstick: 
- overall, this yields a shorter template for `.sphx-glr-thumbcontainer` (fewer lines), so I needed to update tests accordingly (in particular `test_full.py:test_minigallery_directive`)
- documented changes in the changelog

I tested this on a new version of nilearn's documentation and it feels nice:
![Screenshot from 2022-01-18 15-11-27](https://user-images.githubusercontent.com/13835654/149957445-63f77e61-8928-4fc3-be18-d44bf86312ad.png)

larsoner: @lucyleeow do you want to look first?
alexisthual: > My only note is: is this comment relevant? [#905 (comment)](https://github.com/sphinx-gallery/sphinx-gallery/issues/905#issuecomment-1011353892)

Here are some thoughts about the comment.
This PR changes the DOM structure of thumbnails. The `<figure>` and `<figcaption>` elements are turned into an `<img>` and a `<div>` respectively. It will most likely disable user's custom CSS although it won't throw an error :slightly_smiling_face: 

In order for users to have clear instructions on how to update their custom CSS, I think I could write a comment with all needed details in this PR, and add a link to it in the changelogs.

larsoner: I tested this out locally on MNE-Python's sphinx gallery page and it worked well! @alexisthual have you also tested it locally on some projects you currently maintain/contribute to? If so, +1 for merge from me, except just one more thing...

>  I think I could write a comment with all needed details in this PR, and add a link to it in the changelogs.

Indeed I think this would be valuable. Feel free to push this, and @lucyleeow and I can give it a read and merge!
alexisthual: > I tested this out locally on MNE-Python's sphinx gallery page and it worked well!

Yaaaay, glad to hear that :smile: 

> @alexisthual have you also tested it locally on some projects you currently maintain/contribute to?

I am actually contributing this as part of a big theme refactoring for [nilearn](https://github.com/nilearn/nilearn) (see [relevant PR](https://github.com/nilearn/nilearn/issues/3122) if you're interested). We are switching to [furo](https://github.com/pradyunsg/furo/), and this PR works great with this theme.

> Indeed I think this would be valuable. Feel free to push this,

Great, I'll do this soon!
larsoner: > We are switching to furo, and this PR works great with this theme.

Okay great -- MNE uses pydata-sphinx-theme so it's good to know it looks good in multiple themes
alexisthual: ## Thumbnail CSS migration guide

In case your project includes some custom css dedicated to `sphinx-gallery` thumbnails, here are some pointer on how to update it.

### Thumbnails grid

Thumbnails are now displayed using [css grids](https://css-tricks.com/snippets/css/complete-guide-grid/) instead of [css float](https://www.w3schools.com/css/css_float.asp).
A new parent div was added to align thumbnails:
```html
<div class="sphx-glr-thumbnails">
  <div class="sphx-glr-thumbcontainer">...</div>
  <div class="sphx-glr-thumbcontainer">...</div>
  ...
</div>
```

You can tweak the grid behaviour by modifying `.sphx-glr-thumbnails` in your custom css file.

### Thumbnail

Concerning `.sphx-glr-thumbcontainer` divs, inner class names as well as overall DOM structure was modified.

Previous thumbnail DOM structure:
```html
<div class="sphx-glr-thumbcontainer" tooltip="{tooltip}">
  <div class="figure align-default">
    <img />
    <p class="caption">
      <span class="caption-text">
        <a class="reference internal">
          <span class="std std-ref">{link}</span>
        </a>
      </span>
      <a class="headerlink">¶</a>
    </p>
  </div>
</div>
```

New thumbnail DOM structure:
```html
<div class="sphx-glr-thumbcontainer" tooltip="{tooltip}">
  <img />
  <p>
    <a class="reference internal">
      <span class="std std-ref">{link}</span>
    </a>
  </p>
  <div class="sphx-glr-thumbnail-title">{link}</div>
</div>
```

Your custom css should be modified to match these new class names.
alexisthual: > Indeed I think this would be valuable. Feel free to push this, and @lucyleeow and I can give it a read and merge!

I just added a comment above (which can easily be edited) and a link to the changelog.

Also, I added one last change to make the grid columns responsive. Each column will be at least 160px (so that two columns can fit on smartphones), but these columns will be able to grow if more space is available (they can't grow too much though, if not it means we could add a new column).
Here are some tests on a variety of small screen sizes, I think it works well!

 
![Screenshot from 2022-01-23 23-01-10](https://user-images.githubusercontent.com/13835654/150699686-74fa76e9-93b5-4cc6-8c7d-9730187f3949.png)
![Screenshot from 2022-01-23 23-01-22](https://user-images.githubusercontent.com/13835654/150699687-b7e8d54a-f747-441f-9d41-01e7893fa996.png)
![Screenshot from 2022-01-23 23-01-39](https://user-images.githubusercontent.com/13835654/150699691-2dc8ed08-3bed-4c71-b11f-4e1649da23ae.png)

![Screenshot from 2022-01-23 23-02-03](https://user-images.githubusercontent.com/13835654/150699681-548c9092-1fb3-4a69-b1e1-e4f6d11697ac.png)

alexisthual: > I would be tempted to add this to [`faq.rst`](https://github.com/sphinx-gallery/sphinx-gallery/blob/master/doc/faq.rst) as a section with something like 'Why has my thumbnail appearance changed?'

Done :slightly_smiling_face: The new subsection in the FAQ mainly contains a link to the migration guide and to the PR.

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index e65ffb5..7329034 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -8,6 +8,19 @@ In this version, the "Out:" prefix applied to code outputs is now created from
 CSS pseudo-elements instead of additional real text. For more details, see
 `#896 <https://github.com/sphinx-gallery/sphinx-gallery/pull/896>`.
 
+**Implemented enhancements:**
+
+
+**Fixed bugs:**
+
+-  Display gallery items using CSS grid instead of floating `#906 <https://github.com/sphinx-gallery/sphinx-gallery/pull/906>`__, see `migration guide <https://github.com/sphinx-gallery/sphinx-gallery/pull/906#issuecomment-1019542067>`__ to adapt custom css for thumbnails (`alexisthual <https://github.com/alexisthual>`__)
+
+**Closed issues:**
+
+
+**Merged pull requests:**
+
+
 v0.10.1
 -------
 
diff --git a/doc/faq.rst b/doc/faq.rst
index 8e97824..bab0741 100644
--- a/doc/faq.rst
+++ b/doc/faq.rst
@@ -46,3 +46,16 @@ Alternatively, you can set ``capture_repr`` to be an empty tuple
 (``'capture_repr': ()``), which will imitate the behavior of Sphinx-Gallery
 prior to v0.5.0. This will also prevent you from getting any other unwanted
 output that did not occur prior to v0.5.0.
+
+Why has my thumbnail appearance changed?
+----------------------------------------
+
+The DOM structure of thumbnails was refactored in order to make them responsive
+and aligned on a css grid. These changes might make your existing custom css
+obsolete. You can read our
+`custom css migration guide for thumbnails <https://github.com/sphinx-gallery/sphinx-gallery/pull/906#issuecomment-1019542067>`_
+for pointers on how to update your css.
+
+
+.. seealso::
+    `Github PR #906 <https://github.com/sphinx-gallery/sphinx-gallery/pull/906>`_
diff --git a/sphinx_gallery/_static/sg_gallery.css b/sphinx_gallery/_static/sg_gallery.css
index 62947b7..cf0be73 100644
--- a/sphinx_gallery/_static/sg_gallery.css
+++ b/sphinx_gallery/_static/sg_gallery.css
@@ -3,27 +3,72 @@ Sphinx-Gallery has compatible CSS to fix default sphinx themes
 Tested for Sphinx 1.3.1 for all themes: default, alabaster, sphinxdoc,
 scrolls, agogo, traditional, nature, haiku, pyramid
 Tested for Read the Docs theme 0.1.7 */
+.sphx-glr-thumbnails {
+  width: 100%;
+
+  /* align thumbnails on a grid */
+  justify-content: space-between;
+  display: grid;
+  /* each grid column should be at least 160px (this will determine
+  the actual number of columns) and then take as much of the
+  remaining width as possible */
+  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
+  gap: 15px;
+}
+.sphx-glr-thumbnails .toctree-wrapper {
+  /* hide empty toctree divs added to the DOM
+  by sphinx even though the toctree is hidden
+  (they would fill grid places with empty divs) */
+  display: none;
+}
 .sphx-glr-thumbcontainer {
-  background: #fff;
-  border: solid #fff 1px;
+  background: transparent;
   -moz-border-radius: 5px;
   -webkit-border-radius: 5px;
   border-radius: 5px;
-  box-shadow: none;
-  float: left;
-  margin: 5px;
-  min-height: 230px;
-  padding-top: 5px;
+  box-shadow: 0 0 10px #6c757d40;
+
+  /* useful to absolutely position link in div */
   position: relative;
+
+  /* thumbnail width should include padding and borders
+  and take all available space */
+  box-sizing: border-box;
+  width: 100%;
+  padding: 10px;
+  border: 1px solid transparent;
+
+  /* align content in thumbnail */
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 7px;
+}
+.sphx-glr-thumbcontainer p {
+  position: absolute;
+  top: 0;
+  left: 0;
+}
+.sphx-glr-thumbcontainer p,
+.sphx-glr-thumbcontainer p a {
+  /* link should cover the whole thumbnail div */
+  width: 100%;
+  height: 100%;
+}
+.sphx-glr-thumbcontainer p a span {
+  /* text within link should be masked
+  (we are just interested in the href) */
+  display: none;
 }
 .sphx-glr-thumbcontainer:hover {
-  border: solid #b4ddfc 1px;
-  box-shadow: 0 0 15px rgba(142, 176, 202, 0.5);
+  border: 1px solid #0069d9;
+  cursor: pointer;
 }
 .sphx-glr-thumbcontainer a.internal {
   bottom: 0;
   display: block;
   left: 0;
+  box-sizing: border-box;
   padding: 150px 10px 0;
   position: absolute;
   right: 0;
@@ -36,7 +81,7 @@ thumbnail with its default link Background color */
 }
 
 .sphx-glr-thumbcontainer p {
-  margin: 0 0 .1em 0;
+  margin: 0 0 0.1em 0;
 }
 .sphx-glr-thumbcontainer .figure {
   margin: 10px;
@@ -66,7 +111,7 @@ thumbnail with its default link Background color */
   border-color: #333 transparent;
   border-width: 18px 0 0 20px;
   bottom: 58%;
-  content: '';
+  content: "";
   left: 85%;
   position: absolute;
   z-index: 99;
@@ -121,7 +166,7 @@ blockquote.sphx-glr-script-out {
 }
 
 div.sphx-glr-footer {
-    text-align: center;
+  text-align: center;
 }
 
 div.sphx-glr-download {
@@ -131,7 +176,7 @@ div.sphx-glr-download {
 
 div.sphx-glr-download a {
   background-color: #ffc;
-  background-image: linear-gradient(to bottom, #FFC, #d5d57e);
+  background-image: linear-gradient(to bottom, #ffc, #d5d57e);
   border-radius: 4px;
   border: 1px solid #c2c22d;
   color: #000;
@@ -152,7 +197,8 @@ div.sphx-glr-download code.download {
 }
 
 div.sphx-glr-download a:hover {
-  box-shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 5px rgba(0,0,0,.25);
+  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1),
+    0 1px 5px rgba(0, 0, 0, 0.25);
   text-decoration: none;
   background-image: none;
   background-color: #d5d57e;
@@ -193,7 +239,7 @@ div.sphx-glr-animation {
   display: block;
   max-width: 100%;
 }
-div.sphx-glr-animation .animation{
+div.sphx-glr-animation .animation {
   display: block;
 }
 
@@ -208,7 +254,7 @@ p.sphx-glr-signature a.reference.external {
   display: table;
 }
 
-.sphx-glr-clear{
+.sphx-glr-clear {
   clear: both;
 }
 
diff --git a/sphinx_gallery/backreferences.py b/sphinx_gallery/backreferences.py
index 3cc40ba..884e611 100644
--- a/sphinx_gallery/backreferences.py
+++ b/sphinx_gallery/backreferences.py
@@ -242,14 +242,16 @@ THUMBNAIL_TEMPLATE = """
 
 .. only:: html
 
- .. figure:: /{thumbnail}
-     :alt: {title}
+  .. image:: /{thumbnail}
+    :alt: {title}
 
-     :ref:`sphx_glr_{ref_name}`
+  :ref:`sphx_glr_{ref_name}`
 
 .. raw:: html
 
+      <div class="sphx-glr-thumbnail-title">{title}</div>
     </div>
+
 """
 
 BACKREF_THUMBNAIL_TEMPLATE = THUMBNAIL_TEMPLATE + """
diff --git a/sphinx_gallery/gen_rst.py b/sphinx_gallery/gen_rst.py
index 85f05e0..cc79b9a 100644
--- a/sphinx_gallery/gen_rst.py
+++ b/sphinx_gallery/gen_rst.py
@@ -343,6 +343,21 @@ def _get_readme(dir_, gallery_conf, raise_error=True):
     return None
 
 
+THUMBNAIL_PARENT_DIV = """
+.. raw:: html
+
+    <div class="sphx-glr-thumbnails">
+
+"""
+
+THUMBNAIL_PARENT_DIV_CLOSE = """
+.. raw:: html
+
+    </div>
+
+"""
+
+
 def generate_dir_rst(src_dir, target_dir, gallery_conf, seen_backrefs):
     """Generate the gallery reStructuredText for an example directory."""
     head_ref = os.path.relpath(target_dir, gallery_conf['src_dir'])
@@ -368,6 +383,11 @@ def generate_dir_rst(src_dir, target_dir, gallery_conf, seen_backrefs):
     # sort them
     sorted_listdir = sorted(
         listdir, key=gallery_conf['within_subsection_order'](src_dir))
+
+    # Add div containing all thumbnails;
+    # this is helpful for controlling grid or flexbox behaviours
+    fhindex += THUMBNAIL_PARENT_DIV
+
     entries_text = []
     costs = []
     build_target_dir = os.path.relpath(target_dir, gallery_conf['src_dir'])
@@ -392,9 +412,8 @@ def generate_dir_rst(src_dir, target_dir, gallery_conf, seen_backrefs):
     for entry_text in entries_text:
         fhindex += entry_text
 
-    # clear at the end of the section
-    fhindex += """.. raw:: html\n
-    <div class="sphx-glr-clear"></div>\n\n"""
+    # Close thumbnail parent div
+    fhindex += THUMBNAIL_PARENT_DIV_CLOSE
 
     return fhindex, costs
 
@@ -1144,7 +1163,8 @@ def save_rst_example(example_rst, example_file, time_elapsed,
                                         replace_py_ipynb(fname),
                                         binder_badge_rst,
                                         ref_fname)
-    example_rst += SPHX_GLR_SIG
+    if gallery_conf['show_signature']:
+        example_rst += SPHX_GLR_SIG
 
     write_file_new = re.sub(r'\.py$', '.rst.new', example_file)
     with codecs.open(write_file_new, 'w', encoding="utf-8") as f:


Test patch:
diff --git a/sphinx_gallery/tests/test_backreferences.py b/sphinx_gallery/tests/test_backreferences.py
index 5afb819..3274516 100644
--- a/sphinx_gallery/tests/test_backreferences.py
+++ b/sphinx_gallery/tests/test_backreferences.py
@@ -20,15 +20,17 @@ REFERENCE = r"""
 
 .. only:: html
 
- .. figure:: /fake_dir/images/thumb/sphx_glr_test_file_thumb.png
-     :alt: test title
+  .. image:: /fake_dir/images/thumb/sphx_glr_test_file_thumb.png
+    :alt: test title
 
-     :ref:`sphx_glr_fake_dir_test_file.py`
+  :ref:`sphx_glr_fake_dir_test_file.py`
 
 .. raw:: html
 
-    </div>{1}
-"""
+      <div class="sphx-glr-thumbnail-title">test title</div>
+    </div>
+
+{1}"""
 
 
 @pytest.mark.parametrize('content, tooltip, is_backref', [
@@ -56,10 +58,10 @@ def test_thumbnail_div(content, tooltip, is_backref):
                                  check=False)
     if is_backref:
         extra = """
-
 .. only:: not html
 
- * :ref:`sphx_glr_fake_dir_test_file.py`"""
+ * :ref:`sphx_glr_fake_dir_test_file.py`
+"""
     else:
         extra = ''
     reference = REFERENCE.format(tooltip, extra)
diff --git a/sphinx_gallery/tests/test_full.py b/sphinx_gallery/tests/test_full.py
index 3ba93c7..4e18d44 100644
--- a/sphinx_gallery/tests/test_full.py
+++ b/sphinx_gallery/tests/test_full.py
@@ -772,19 +772,19 @@ def test_backreference_labels(sphinx_app):
 @pytest.mark.parametrize(
     'test, nlines, filenamesortkey', [
         # first example, no heading
-        ('Test 1-N', 6, False),
+        ('Test 1-N', 5, False),
         # first example, default heading, default level
-        ('Test 1-D-D', 8, False),
+        ('Test 1-D-D', 7, False),
         # first example, default heading, custom level
-        ('Test 1-D-C', 8, False),
+        ('Test 1-D-C', 7, False),
         # first example, custom heading, default level
-        ('Test 1-C-D', 9, False),
+        ('Test 1-C-D', 8, False),
         # both examples, no heading
-        ('Test 2-N', 11, True),
+        ('Test 2-N', 10, True),
         # both examples, default heading, default level
-        ('Test 2-D-D', 14, True),
+        ('Test 2-D-D', 13, True),
         # both examples, custom heading, custom level
-        ('Test 2-C-C', 15, True),
+        ('Test 2-C-C', 14, True),
     ]
 )
 def test_minigallery_directive(sphinx_app, test, nlines, filenamesortkey):
diff --git a/sphinx_gallery/tests/test_gen_gallery.py b/sphinx_gallery/tests/test_gen_gallery.py
index 705f1c5..75e061c 100644
--- a/sphinx_gallery/tests/test_gen_gallery.py
+++ b/sphinx_gallery/tests/test_gen_gallery.py
@@ -164,6 +164,12 @@ def test_spaces_in_files_warn(sphinx_app_wrapper):
 
 
 def _check_order(sphinx_app, key):
+    """
+    Iterates through sphx-glr-thumbcontainer divs from index.rst lines
+    and reads given key from the tooltip.
+    Test that these keys appear in a specific order.
+    """
+
     index_fname = os.path.join(sphinx_app.outdir, '..', 'ex', 'index.rst')
     order = list()
     regex = '.*:%s=(.):.*' % key