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!

Comments:

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