]> git.ipfire.org Git - thirdparty/bootstrap.git/commitdiff
More close button updates (#41937)
authorMark Otto <markd.otto@gmail.com>
Tue, 16 Dec 2025 22:05:02 +0000 (14:05 -0800)
committerMark Otto <markdotto@gmail.com>
Fri, 9 Jan 2026 04:08:33 +0000 (20:08 -0800)
* Clean up close button more

* New placeholder for docs examples

* CSS lint fix

scss/buttons/_close.scss
site/src/components/shortcodes/CloseButton.astro [new file with mode: 0644]
site/src/content/docs/components/alerts.mdx
site/src/content/docs/components/dialog.mdx
site/src/content/docs/components/navbar.mdx
site/src/content/docs/components/offcanvas.mdx
site/src/content/docs/components/toasts.mdx
site/src/libs/placeholder.ts
site/src/types/auto-import.d.ts

index 9fc526e9a8451198d18341d6cf53d7f6a240f306..de65bd13c5b6c835159a11b8b4a583b187bd03ff 100644 (file)
@@ -3,15 +3,11 @@
 @use "../variables" as *;
 @use "../mixins/border-radius" as *;
 @use "../mixins/color-mode" as *;
+@use "../mixins/focus-ring" as *;
 
 // scss-docs-start close-variables
 $btn-close-size:             1.25rem !default;
-// $btn-close-width:            1em !default;
-// $btn-close-height:           $btn-close-width !default;
-// $btn-close-padding-x:        .25em !default;
-// $btn-close-padding-y:        $btn-close-padding-x !default;
 $btn-close-color:            var(--#{$prefix}fg-body) !default;
-$btn-close-focus-shadow:     $focus-ring-box-shadow !default;
 $btn-close-opacity:          .5 !default;
 $btn-close-hover-opacity:    .75 !default;
 $btn-close-focus-opacity:    .85 !default;
@@ -30,7 +26,6 @@ $btn-close-disabled-opacity: .25 !default;
     --#{$prefix}btn-close-color: #{$btn-close-color};
     --#{$prefix}btn-close-opacity: #{$btn-close-opacity};
     --#{$prefix}btn-close-hover-opacity: #{$btn-close-hover-opacity};
-    --#{$prefix}btn-close-focus-shadow: #{$btn-close-focus-shadow};
     --#{$prefix}btn-close-focus-opacity: #{$btn-close-focus-opacity};
     --#{$prefix}btn-close-disabled-opacity: #{$btn-close-disabled-opacity};
     // scss-docs-end close-css-vars
@@ -41,14 +36,14 @@ $btn-close-disabled-opacity: .25 !default;
     padding: 0;
     color: var(--#{$prefix}btn-close-color);
     background: transparent;
-    // background: transparent var(--#{$prefix}btn-close-bg) center / $btn-close-width auto no-repeat; // include transparent for button elements
-    // filter: var(--#{$prefix}btn-close-filter);
     border: 0; // for button elements
+    @include border-radius(var(--#{$prefix}border-radius-sm));
     opacity: var(--#{$prefix}btn-close-opacity);
 
     > svg {
-      // width: 100%;
-      // height: 100%
+      display: block;
+      width: 100%;
+      height: 100%;
       fill: currentcolor;
     }
 
@@ -60,8 +55,7 @@ $btn-close-disabled-opacity: .25 !default;
     }
 
     &:focus {
-      outline: 0;
-      box-shadow: var(--#{$prefix}btn-close-focus-shadow);
+      @include focus-ring();
       opacity: var(--#{$prefix}btn-close-focus-opacity);
     }
 
@@ -72,23 +66,4 @@ $btn-close-disabled-opacity: .25 !default;
       opacity: var(--#{$prefix}btn-close-disabled-opacity);
     }
   }
-
-  // @mixin btn-close-white() {
-  //   // --#{$prefix}btn-close-filter: #{$btn-close-filter-dark};
-  // }
-
-  // .btn-close-white {
-  //   @include btn-close-white();
-  // }
-
-  // :root,
-  // [data-bs-theme="light"] {
-  //   // --#{$prefix}btn-close-filter: #{$btn-close-filter};
-  // }
-
-  // @if $enable-dark-mode {
-  //   @include color-mode(dark, true) {
-  //     @include btn-close-white();
-  //   }
-  // }
 }
diff --git a/site/src/components/shortcodes/CloseButton.astro b/site/src/components/shortcodes/CloseButton.astro
new file mode 100644 (file)
index 0000000..16e3fe6
--- /dev/null
@@ -0,0 +1,21 @@
+---
+interface Props {
+  /**
+   * Additional CSS classes to add to the button (e.g., "me-2 m-auto").
+   */
+  class?: string
+  /**
+   * The component to dismiss (e.g., "dialog", "alert", "offcanvas", "toast").
+   * Sets the `data-bs-dismiss` attribute.
+   */
+  dismiss?: string
+}
+
+const { class: className, dismiss } = Astro.props
+---
+
+<button type="button" class:list={["btn-close", className]} data-bs-dismiss={dismiss} aria-label="Close">
+  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="20" height="20" fill="none">
+    <path fill="currentcolor" d="M12 0a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm-.646 4.646a.5.5 0 0 0-.707 0L8 7.293 5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.647a.5.5 0 1 0 .708.707L8 8.707l2.647 2.646a.5.5 0 1 0 .707-.707L8.707 8l2.646-2.646a.5.5 0 0 0 0-.708z"/>
+  </svg>
+</button>
index f226223200765e470399c471fe8f42b7bc3c676e..b5326d33a9e0eac87596fac016c1db36fb0d10f7 100644 (file)
@@ -111,7 +111,7 @@ You can see this in action with a live demo:
 
 <Example code={`<div class="alert alert-warning alert-dismissible fade show" role="alert">
     <strong>Holy guacamole!</strong> You should check in on some of those fields below.
-    <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
+    <CloseButton dismiss="alert" />
   </div>`} />
 
 <Callout type="warning">
index 486c32b21eb26a63890c2c209b05c53ae80959b5..a7e26d2c5b33b924e679bb93f8b2ef9fbacecbf7 100644 (file)
@@ -29,7 +29,7 @@ Toggle a dialog by clicking the button below. The dialog uses the native `showMo
 <dialog class="dialog" id="exampleDialog">
   <div class="dialog-header">
     <h1 class="dialog-title">Dialog title</h1>
-    <button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close"></button>
+    <CloseButton dismiss="dialog" />
   </div>
   <div class="dialog-body">
     <p>This is a native dialog element. It uses the browser's built-in modal behavior for accessibility and focus management.</p>
@@ -50,7 +50,11 @@ The markup for a dialog is straightforward:
 <dialog class="dialog" id="exampleDialog">
   <div class="dialog-header">
     <h1 class="dialog-title">Dialog title</h1>
-    <button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close"></button>
+    <button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close">
+      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="20" height="20" fill="none">
+        <path fill="currentcolor" d="M12 0a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm-.646 4.646a.5.5 0 0 0-.707 0L8 7.293 5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.647a.5.5 0 1 0 .708.707L8 8.707l2.647 2.646a.5.5 0 1 0 .707-.707L8.707 8l2.646-2.646a.5.5 0 0 0 0-.708z"/>
+      </svg>
+    </button>
   </div>
   <div class="dialog-body">
     <p>Dialog body content goes here.</p>
@@ -73,7 +77,7 @@ When `backdrop` is set to `static`, the dialog will not close when clicking outs
 <dialog class="dialog" id="staticBackdropDialog">
   <div class="dialog-header">
     <h1 class="dialog-title">Static backdrop</h1>
-    <button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close"></button>
+    <CloseButton dismiss="dialog" />
   </div>
   <div class="dialog-body">
     <p>I will not close if you click outside of me. Use the close button or press Escape.</p>
@@ -94,7 +98,7 @@ When dialogs have content that exceeds the viewport height, the entire dialog sc
 <dialog class="dialog" id="scrollingLongDialog">
   <div class="dialog-header">
     <h1 class="dialog-title">Scrolling dialog</h1>
-    <button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close"></button>
+    <CloseButton dismiss="dialog" />
   </div>
   <div class="dialog-body">
     <p>This is some placeholder content to show the scrolling behavior for dialogs. When the content exceeds the viewport height, you can scroll the entire dialog within the window—the header, body, and footer all move together.</p>
@@ -122,7 +126,7 @@ You can also create a scrollable dialog that scrolls the dialog body while keepi
 <dialog class="dialog dialog-scrollable" id="scrollableBodyDialog">
   <div class="dialog-header">
     <h1 class="dialog-title">Scrollable body</h1>
-    <button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close"></button>
+    <CloseButton dismiss="dialog" />
   </div>
   <div class="dialog-body">
     <p>This is some placeholder content to show the scrolling behavior for dialogs. We use repeated line breaks to demonstrate how content can exceed the dialog's inner height, showing scrolling within the body while the header and footer remain fixed.</p>
@@ -149,7 +153,11 @@ You can also create a scrollable dialog that scrolls the dialog body while keepi
 <dialog class="dialog dialog-scrollable" id="scrollableBodyDialog">
   <div class="dialog-header">
     <h1 class="dialog-title">Scrollable body</h1>
-    <button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close"></button>
+    <button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close">
+      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="20" height="20" fill="none">
+        <path fill="currentcolor" d="M12 0a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm-.646 4.646a.5.5 0 0 0-.707 0L8 7.293 5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.647a.5.5 0 1 0 .708.707L8 8.707l2.647 2.646a.5.5 0 1 0 .707-.707L8.707 8l2.646-2.646a.5.5 0 0 0 0-.708z"/>
+      </svg>
+    </button>
   </div>
   <div class="dialog-body">
     <!-- Long content here -->
@@ -166,7 +174,7 @@ For a dialog that extends beyond the viewport and scrolls as a whole, add `.dial
   <div class="dialog-box">
     <div class="dialog-header">
       <h1 class="dialog-title">Overflow dialog</h1>
-      <button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close"></button>
+      <CloseButton dismiss="dialog" />
     </div>
     <div class="dialog-body">
       <p>This dialog extends beyond the viewport height. Scroll to see more content. Notice how the entire dialog—including header and footer—shifts up and down as you scroll.</p>
@@ -209,7 +217,7 @@ When a toggle trigger is inside an open dialog, clicking it will **swap** dialog
 <dialog class="dialog" id="swapDialog1">
   <div class="dialog-header">
     <h1 class="dialog-title">First dialog</h1>
-    <button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close"></button>
+    <CloseButton dismiss="dialog" />
   </div>
   <div class="dialog-body">
     <p>Click below to swap to a second dialog. Notice the backdrop stays visible—no flash!</p>
@@ -223,7 +231,7 @@ When a toggle trigger is inside an open dialog, clicking it will **swap** dialog
 <dialog class="dialog" id="swapDialog2">
   <div class="dialog-header">
     <h1 class="dialog-title">Second dialog</h1>
-    <button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close"></button>
+    <CloseButton dismiss="dialog" />
   </div>
   <div class="dialog-body">
     <p>This is the second dialog. You can swap back to the first, or close this one entirely.</p>
@@ -280,7 +288,7 @@ By default, dialogs open as modals using the native `showModal()` method. You ca
 <dialog class="dialog" id="nonModalDialog">
   <div class="dialog-header">
     <h1 class="dialog-title">Non-modal dialog</h1>
-    <button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close"></button>
+    <CloseButton dismiss="dialog" />
   </div>
   <div class="dialog-body">
     <p>This dialog doesn't block the page. You can still interact with content behind it.</p>
@@ -315,7 +323,7 @@ Dialogs have three optional sizes, available via modifier classes to be placed o
 <dialog class="dialog dialog-xl" id="exampleDialogXl">
   <div class="dialog-header">
     <h1 class="dialog-title">Extra large dialog</h1>
-    <button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close"></button>
+    <CloseButton dismiss="dialog" />
   </div>
   <div class="dialog-body">
     <p>This is an extra large dialog using the <code>.dialog-xl</code> class.</p>
@@ -325,7 +333,7 @@ Dialogs have three optional sizes, available via modifier classes to be placed o
 <dialog class="dialog dialog-lg" id="exampleDialogLg">
   <div class="dialog-header">
     <h1 class="dialog-title">Large dialog</h1>
-    <button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close"></button>
+    <CloseButton dismiss="dialog" />
   </div>
   <div class="dialog-body">
     <p>This is a large dialog using the <code>.dialog-lg</code> class.</p>
@@ -335,7 +343,7 @@ Dialogs have three optional sizes, available via modifier classes to be placed o
 <dialog class="dialog dialog-sm" id="exampleDialogSm">
   <div class="dialog-header">
     <h1 class="dialog-title">Small dialog</h1>
-    <button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close"></button>
+    <CloseButton dismiss="dialog" />
   </div>
   <div class="dialog-body">
     <p>This is a small dialog using the <code>.dialog-sm</code> class.</p>
@@ -365,7 +373,7 @@ Use `.dialog-fullscreen` to make the dialog cover the entire viewport.
 <dialog class="dialog dialog-fullscreen" id="exampleDialogFullscreen">
   <div class="dialog-header">
     <h1 class="dialog-title">Fullscreen dialog</h1>
-    <button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close"></button>
+    <CloseButton dismiss="dialog" />
   </div>
   <div class="dialog-body">
     <p>This dialog covers the entire viewport.</p>
@@ -393,7 +401,7 @@ Responsive fullscreen variants are also available. These make the dialog fullscr
 <dialog class="dialog dialog-fullscreen-lg-down" id="exampleDialogFullscreenLg">
   <div class="dialog-header">
     <h1 class="dialog-title">Fullscreen below lg</h1>
-    <button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close"></button>
+    <CloseButton dismiss="dialog" />
   </div>
   <div class="dialog-body">
     <p>This dialog is fullscreen below the <code>lg</code> breakpoint.</p>
index f9347633218049fb02562d4e404b532d6442a6b1..3d4b01f82a9aec07dc700cd8420599a1847905e9 100644 (file)
@@ -626,7 +626,7 @@ In the example below, to create an offcanvas navbar that is always collapsed acr
       <div class="offcanvas offcanvas-end" tabindex="-1" id="offcanvasNavbar" aria-labelledby="offcanvasNavbarLabel">
         <div class="offcanvas-header">
           <h5 class="offcanvas-title" id="offcanvasNavbarLabel">Offcanvas</h5>
-          <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
+          <CloseButton dismiss="offcanvas" />
         </div>
         <div class="offcanvas-body">
           <ul class="navbar-nav justify-content-end flex-grow-1 pe-3">
@@ -684,7 +684,7 @@ When using offcanvas in a dark navbar, be aware that you may need to have a dark
       <div class="offcanvas offcanvas-end text-bg-dark" tabindex="-1" id="offcanvasDarkNavbar" aria-labelledby="offcanvasDarkNavbarLabel">
         <div class="offcanvas-header">
           <h5 class="offcanvas-title" id="offcanvasDarkNavbarLabel">Dark offcanvas</h5>
-          <button type="button" class="btn-close btn-close-white" data-bs-dismiss="offcanvas" aria-label="Close"></button>
+          <CloseButton dismiss="offcanvas" />
         </div>
         <div class="offcanvas-body">
           <ul class="navbar-nav justify-content-end flex-grow-1 pe-3">
index f20873c0e3402f654dcc58bf7571610c3a41d7b4..92e8b1f609dc927ce79161b620d488965588ef05 100644 (file)
@@ -26,7 +26,7 @@ Below is an offcanvas example that is shown by default (via `.show` on `.offcanv
 <Example class="bd-example-offcanvas p-0 bg-body-tertiary overflow-hidden" code={`<div class="offcanvas offcanvas-start show" tabindex="-1" id="offcanvas" aria-labelledby="offcanvasLabel">
     <div class="offcanvas-header">
       <h5 class="offcanvas-title" id="offcanvasLabel">Offcanvas</h5>
-      <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
+      <CloseButton dismiss="offcanvas" />
     </div>
     <div class="offcanvas-body">
       Content for the offcanvas goes here. You can place just about any Bootstrap component or custom elements here.
@@ -52,7 +52,7 @@ You can use a link with the `href` attribute, or a button with the `data-bs-targ
   <div class="offcanvas offcanvas-start" tabindex="-1" id="offcanvasExample" aria-labelledby="offcanvasExampleLabel">
     <div class="offcanvas-header">
       <h5 class="offcanvas-title" id="offcanvasExampleLabel">Offcanvas</h5>
-      <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
+      <CloseButton dismiss="offcanvas" />
     </div>
     <div class="offcanvas-body">
       <div>
@@ -80,7 +80,7 @@ Scrolling the `<body>` element is disabled when an offcanvas and its backdrop ar
   <div class="offcanvas offcanvas-start" data-bs-scroll="true" data-bs-backdrop="false" tabindex="-1" id="offcanvasScrolling" aria-labelledby="offcanvasScrollingLabel">
     <div class="offcanvas-header">
       <h5 class="offcanvas-title" id="offcanvasScrollingLabel">Offcanvas with body scrolling</h5>
-      <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
+      <CloseButton dismiss="offcanvas" />
     </div>
     <div class="offcanvas-body">
       <p>Try scrolling the rest of the page to see this option in action.</p>
@@ -96,7 +96,7 @@ You can also enable `<body>` scrolling with a visible backdrop.
   <div class="offcanvas offcanvas-start" data-bs-scroll="true" tabindex="-1" id="offcanvasWithBothOptions" aria-labelledby="offcanvasWithBothOptionsLabel">
     <div class="offcanvas-header">
       <h5 class="offcanvas-title" id="offcanvasWithBothOptionsLabel">Backdrop with scrolling</h5>
-      <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
+      <CloseButton dismiss="offcanvas" />
     </div>
     <div class="offcanvas-body">
       <p>Try scrolling the rest of the page to see this option in action.</p>
@@ -114,7 +114,7 @@ When backdrop is set to static, the offcanvas will not close when clicking outsi
   <div class="offcanvas offcanvas-start" data-bs-backdrop="static" tabindex="-1" id="staticBackdrop" aria-labelledby="staticBackdropLabel">
     <div class="offcanvas-header">
       <h5 class="offcanvas-title" id="staticBackdropLabel">Offcanvas</h5>
-      <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
+      <CloseButton dismiss="offcanvas" />
     </div>
     <div class="offcanvas-body">
       <div>
@@ -130,7 +130,7 @@ Change the appearance of offcanvases with utilities to better match them to diff
 <Example class="bd-example-offcanvas p-0 bg-body-secondary overflow-hidden" code={`<div class="offcanvas offcanvas-start show" tabindex="-1" id="offcanvasDark" aria-labelledby="offcanvasDarkLabel" data-bs-theme="dark">
     <div class="offcanvas-header">
       <h5 class="offcanvas-title" id="offcanvasDarkLabel">Offcanvas</h5>
-      <button type="button" class="btn-close btn-close-white" data-bs-dismiss="offcanvasDark" aria-label="Close"></button>
+      <CloseButton dismiss="offcanvasDark" />
     </div>
     <div class="offcanvas-body">
       <p>Place offcanvas content here.</p>
@@ -157,7 +157,7 @@ To make a responsive offcanvas, replace the `.offcanvas` base class with a respo
   <div class="offcanvas-lg offcanvas-end" tabindex="-1" id="offcanvasResponsive" aria-labelledby="offcanvasResponsiveLabel">
     <div class="offcanvas-header">
       <h5 class="offcanvas-title" id="offcanvasResponsiveLabel">Responsive offcanvas</h5>
-      <button type="button" class="btn-close" data-bs-dismiss="offcanvas" data-bs-target="#offcanvasResponsive" aria-label="Close"></button>
+      <CloseButton dismiss="offcanvas" />
     </div>
     <div class="offcanvas-body">
       <p class="mb-0">This is content within an <code>.offcanvas-lg</code>.</p>
@@ -180,7 +180,7 @@ Try the top, right, and bottom examples out below.
   <div class="offcanvas offcanvas-top" tabindex="-1" id="offcanvasTop" aria-labelledby="offcanvasTopLabel">
     <div class="offcanvas-header">
       <h5 class="offcanvas-title" id="offcanvasTopLabel">Offcanvas top</h5>
-      <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
+      <CloseButton dismiss="offcanvas" />
     </div>
     <div class="offcanvas-body">
       ...
@@ -192,7 +192,7 @@ Try the top, right, and bottom examples out below.
   <div class="offcanvas offcanvas-end" tabindex="-1" id="offcanvasRight" aria-labelledby="offcanvasRightLabel">
     <div class="offcanvas-header">
       <h5 class="offcanvas-title" id="offcanvasRightLabel">Offcanvas right</h5>
-      <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
+      <CloseButton dismiss="offcanvas" />
     </div>
     <div class="offcanvas-body">
       ...
@@ -204,7 +204,7 @@ Try the top, right, and bottom examples out below.
   <div class="offcanvas offcanvas-bottom" tabindex="-1" id="offcanvasBottom" aria-labelledby="offcanvasBottomLabel">
     <div class="offcanvas-header">
       <h5 class="offcanvas-title" id="offcanvasBottomLabel">Offcanvas bottom</h5>
-      <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
+      <CloseButton dismiss="offcanvas" />
     </div>
     <div class="offcanvas-body small">
       ...
index 3b5da8da0097c9b729a71bb540562f14da5a68db..0330eb2073c494284aab68c2679df4a7a12140d3 100644 (file)
@@ -28,7 +28,7 @@ Toasts are as flexible as you need and have very little required markup. At a mi
       <Placeholder width="20" height="20" background="#007aff" class="rounded me-2" text={false} title={false} />
       <strong class="me-auto">Bootstrap</strong>
       <small>11 mins ago</small>
-      <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
+      <CloseButton dismiss="toast" />
     </div>
     <div class="toast-body">
       Hello, world! This is a toast message.
@@ -49,7 +49,7 @@ Click the button below to show a toast (positioned with our utilities in the low
       <Placeholder width="20" height="20" background="#007aff" class="rounded me-2" text={false} title={false} />
       <strong class="me-auto">Bootstrap</strong>
       <small>11 mins ago</small>
-      <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
+      <CloseButton dismiss="toast" />
     </div>
     <div class="toast-body">
       Hello, world! This is a toast message.
@@ -70,7 +70,7 @@ Click the button below to show a toast (positioned with our utilities in the low
       <img src="..." class="rounded me-2" alt="...">
       <strong class="me-auto">Bootstrap</strong>
       <small>11 mins ago</small>
-      <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
+      <CloseButton dismiss="toast" />
     </div>
     <div class="toast-body">
       Hello, world! This is a toast message.
@@ -92,7 +92,7 @@ Toasts are slightly translucent to blend in with what’s below them.
       <Placeholder width="20" height="20" background="#007aff" class="rounded me-2" text={false} title={false} />
       <strong class="me-auto">Bootstrap</strong>
       <small class="text-body-secondary">11 mins ago</small>
-      <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
+      <CloseButton dismiss="toast" />
     </div>
     <div class="toast-body">
       Hello, world! This is a toast message.
@@ -109,7 +109,7 @@ You can stack toasts by wrapping them in a toast container, which will verticall
         <Placeholder width="20" height="20" background="#007aff" class="rounded me-2" text={false} title={false} />
         <strong class="me-auto">Bootstrap</strong>
         <small class="text-body-secondary">just now</small>
-        <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
+        <CloseButton dismiss="toast" />
       </div>
       <div class="toast-body">
         See? Just like this.
@@ -121,7 +121,7 @@ You can stack toasts by wrapping them in a toast container, which will verticall
         <Placeholder width="20" height="20" background="#007aff" class="rounded me-2" text={false} title={false} />
         <strong class="me-auto">Bootstrap</strong>
         <small class="text-body-secondary">2 seconds ago</small>
-        <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
+        <CloseButton dismiss="toast" />
       </div>
       <div class="toast-body">
         Heads up, toasts will stack automatically
@@ -138,7 +138,7 @@ Customize your toasts by removing sub-components, tweaking them with [utilities]
       <div class="toast-body">
         Hello, world! This is a toast message.
       </div>
-      <button type="button" class="btn-close me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
+      <CloseButton dismiss="toast" class="me-2 m-auto" />
     </div>
   </div>`} />
 
@@ -156,14 +156,14 @@ Alternatively, you can also add additional controls and components to toasts.
 
 ### Color schemes
 
-Building on the above example, you can create different toast color schemes with our [color]([[docsref:/utilities/colors]]) and [background]([[docsref:/utilities/background]]) utilities. Here we’ve added `.text-bg-primary` to the `.toast`, and then added `.btn-close-white` to our close button. For a crisp edge, we remove the default border with `.border-0`.
+Building on the above example, you can create different toast color schemes with our [color]([[docsref:/utilities/colors]]) and [background]([[docsref:/utilities/background]]) utilities. Here we’ve added `.text-bg-primary` to the `.toast`. For a crisp edge, we remove the default border with `.border-0`.
 
 <Example code={`<div class="toast align-items-center text-bg-primary border-0" role="alert" aria-live="assertive" aria-atomic="true">
     <div class="d-flex">
       <div class="toast-body">
         Hello, world! This is a toast message.
       </div>
-      <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
+      <CloseButton dismiss="toast" class="me-2 m-auto" />
     </div>
   </div>`} />
 
@@ -218,7 +218,7 @@ For systems that generate more notifications, consider using a wrapping element
           <Placeholder width="20" height="20" background="#007aff" class="rounded me-2" text={false} title={false} />
           <strong class="me-auto">Bootstrap</strong>
           <small class="text-body-secondary">just now</small>
-          <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
+          <CloseButton dismiss="toast" />
         </div>
         <div class="toast-body">
           See? Just like this.
@@ -230,7 +230,7 @@ For systems that generate more notifications, consider using a wrapping element
           <Placeholder width="20" height="20" background="#007aff" class="rounded me-2" text={false} title={false} />
           <strong class="me-auto">Bootstrap</strong>
           <small class="text-body-secondary">2 seconds ago</small>
-          <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
+          <CloseButton dismiss="toast" />
         </div>
         <div class="toast-body">
           Heads up, toasts will stack automatically
@@ -250,7 +250,7 @@ You can also get fancy with flexbox utilities to align toasts horizontally and/o
         <Placeholder width="20" height="20" background="#007aff" class="rounded me-2" text={false} title={false} />
         <strong class="me-auto">Bootstrap</strong>
         <small>11 mins ago</small>
-        <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
+        <CloseButton dismiss="toast" />
       </div>
       <div class="toast-body">
         Hello, world! This is a toast message.
@@ -281,7 +281,7 @@ When using `autohide: false`, you must add a close button to allow users to dism
       <Placeholder width="20" height="20" background="#007aff" class="rounded me-2" text={false} title={false} />
       <strong class="me-auto">Bootstrap</strong>
       <small>11 mins ago</small>
-      <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
+      <CloseButton dismiss="toast" />
     </div>
     <div class="toast-body">
       Hello, world! This is a toast message.
index 2bae6e1cad596f70a13c4a8e8acaeea724a6f645..e0269bb2834115e79c3fa3593e1a78936890c65d 100644 (file)
@@ -3,6 +3,12 @@ import * as htmlparser2 from 'htmlparser2'
 import { getData } from './data'
 
 const placeholderRegex = /<Placeholder\s+([^>]+)\/>/g
+const closeButtonRegex = /<CloseButton\s*([^>]*?)\/>/g
+
+// Close button SVG icon
+const CLOSE_BUTTON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="20" height="20" fill="none">
+      <path fill="currentcolor" d="M12 0a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm-.646 4.646a.5.5 0 0 0-.707 0L8 7.293 5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.647a.5.5 0 1 0 .708.707L8 8.707l2.647 2.646a.5.5 0 1 0 .707-.707L8.707 8l2.646-2.646a.5.5 0 0 0 0-.708z"/>
+    </svg>`
 
 /**
  * Generates all the placeholder attributes and options required to render a placeholder.
@@ -56,8 +62,25 @@ export function getPlaceholder(userOptions: Partial<PlaceholderOptions>): Placeh
   }
 }
 
+/**
+ * Renders a CloseButton component to its HTML string representation.
+ * Supports optional `dismiss` and `class` attributes.
+ */
+function renderCloseButtonToString(attributes: Record<string, string>): string {
+  const dismiss = attributes.dismiss
+  const extraClass = attributes.class
+  const dismissAttr = dismiss ? ` data-bs-dismiss="${dismiss}"` : ''
+  const classValue = extraClass ? `btn-close ${extraClass}` : 'btn-close'
+
+  return `<button type="button" class="${classValue}"${dismissAttr} aria-label="Close">
+      ${CLOSE_BUTTON_SVG}
+    </button>`
+}
+
 /**
  * Replaces placeholders described using the `<Placeholder />` component in HTML markup with the expected HTML content.
+ * Also replaces `<CloseButton />` components with the full close button HTML.
+ *
  * This is useful to render examples that have a pretty large set of constraints:
  *
  *  - The provided HTML code is not valid MDX (e.g. unclosed void elements like <img>) but can contain the
@@ -72,6 +95,24 @@ export function getPlaceholder(userOptions: Partial<PlaceholderOptions>): Placeh
  * If you are not sure if you need to use this function, you probably don't.
  */
 export function replacePlaceholdersInHtml(html: string) {
+  // Replace CloseButton components
+  html = html.replace(closeButtonRegex, (match) => {
+    const document = htmlparser2.parseDocument(match, { xmlMode: true })
+    const closeButtonElement = document.firstChild
+
+    if (
+      document.children.length > 1 ||
+      !closeButtonElement ||
+      closeButtonElement.type !== htmlparser2.ElementType.Tag ||
+      closeButtonElement.name !== 'CloseButton'
+    ) {
+      throw new Error('Invalid CloseButton element.')
+    }
+
+    return renderCloseButtonToString(closeButtonElement.attribs as Record<string, string>)
+  })
+
+  // Replace Placeholder components
   return html.replace(placeholderRegex, (match) => {
     const document = htmlparser2.parseDocument(match, { xmlMode: true })
     const placeholderElement = document.firstChild
index 17195966ce1e72c2d34d01961cfacfaa47e61190..7525e22d1d9d304da8480af464b6ec7b6dae5140 100644 (file)
@@ -11,6 +11,7 @@ export declare global {
   export const CSSVariables: typeof import('@shortcodes/CSSVariables.astro').default
   export const Callout: typeof import('@shortcodes/Callout.astro').default
   export const CalloutDeprecatedDarkVariants: typeof import('@shortcodes/CalloutDeprecatedDarkVariants.astro').default
+  export const CloseButton: typeof import('@shortcodes/CloseButton.astro').default
   export const Code: typeof import('@shortcodes/Code.astro').default
   export const DeprecatedIn: typeof import('@shortcodes/DeprecatedIn.astro').default
   export const Details: typeof import('@shortcodes/Details.astro').default