]> git.ipfire.org Git - thirdparty/bootstrap.git/commitdiff
Dropdown — Add option to make the dropdown menu clickable (#33389)
authorRohit Sharma <rohit2sharma95@gmail.com>
Tue, 20 Apr 2021 05:19:57 +0000 (10:49 +0530)
committerGitHub <noreply@github.com>
Tue, 20 Apr 2021 05:19:57 +0000 (08:19 +0300)
js/src/dropdown.js
js/tests/unit/dropdown.spec.js
site/content/docs/5.0/components/dropdowns.md

index 2fc2cde21bf0a0009ab5b67e376dcc6faf3a0577..d26fa96cab92b48135f6501e37a0fa5e172c36d7 100644 (file)
@@ -75,7 +75,8 @@ const Default = {
   boundary: 'clippingParents',
   reference: 'toggle',
   display: 'dynamic',
-  popperConfig: null
+  popperConfig: null,
+  autoClose: true
 }
 
 const DefaultType = {
@@ -83,7 +84,8 @@ const DefaultType = {
   boundary: '(string|element)',
   reference: '(string|element|object)',
   display: 'string',
-  popperConfig: '(null|object|function)'
+  popperConfig: '(null|object|function)',
+  autoClose: '(boolean|string)'
 }
 
 /**
@@ -127,9 +129,8 @@ class Dropdown extends BaseComponent {
 
     const isActive = this._element.classList.contains(CLASS_NAME_SHOW)
 
-    Dropdown.clearMenus()
-
     if (isActive) {
+      this.hide()
       return
     }
 
@@ -424,7 +425,7 @@ class Dropdown extends BaseComponent {
 
     for (let i = 0, len = toggles.length; i < len; i++) {
       const context = Data.get(toggles[i], DATA_KEY)
-      if (!context) {
+      if (!context || context._config.autoClose === false) {
         continue
       }
 
@@ -437,8 +438,13 @@ class Dropdown extends BaseComponent {
       }
 
       if (event) {
-        // Don't close the menu if the clicked element or one of its parents is the dropdown button
-        if ([context._element].some(element => event.composedPath().includes(element))) {
+        const composedPath = event.composedPath()
+        const isMenuTarget = composedPath.includes(context._menu)
+        if (
+          composedPath.includes(context._element) ||
+          (context._config.autoClose === 'inside' && !isMenuTarget) ||
+          (context._config.autoClose === 'outside' && isMenuTarget)
+        ) {
           continue
         }
 
index 14bf8407bf944e172f4b8a3b6f2080d797db3e31..cfb93379268363dee7d45b6e604bc514ec2f73c3 100644 (file)
@@ -216,18 +216,17 @@ describe('Dropdown', () => {
       const firstDropdownEl = fixtureEl.querySelector('.first')
       const secondDropdownEl = fixtureEl.querySelector('.second')
       const dropdown1 = new Dropdown(btnDropdown1)
-      const dropdown2 = new Dropdown(btnDropdown2)
 
       firstDropdownEl.addEventListener('shown.bs.dropdown', () => {
         expect(btnDropdown1.classList.contains('show')).toEqual(true)
         spyOn(dropdown1._popper, 'destroy')
-        dropdown2.toggle()
+        btnDropdown2.click()
       })
 
-      secondDropdownEl.addEventListener('shown.bs.dropdown', () => {
+      secondDropdownEl.addEventListener('shown.bs.dropdown', () => setTimeout(() => {
         expect(dropdown1._popper.destroy).toHaveBeenCalled()
         done()
-      })
+      }))
 
       dropdown1.toggle()
     })
@@ -1739,6 +1738,100 @@ describe('Dropdown', () => {
       toggle.dispatchEvent(keydownEscape)
       toggle.dispatchEvent(keyupEscape)
     })
+
+    it('should close dropdown (only) by clicking inside the dropdown menu when it has data-attribute `data-bs-auto-close="inside"`', done => {
+      fixtureEl.innerHTML = [
+        '<div class="dropdown">',
+        '  <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-auto-close="inside">Dropdown toggle</button>',
+        '  <div class="dropdown-menu">',
+        '    <a class="dropdown-item" href="#">Dropdown item</a>',
+        ' </div>',
+        '</div>'
+      ]
+
+      const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
+      const dropdownMenu = fixtureEl.querySelector('.dropdown-menu')
+
+      const expectDropdownToBeOpened = () => setTimeout(() => {
+        expect(dropdownToggle.classList.contains('show')).toEqual(true)
+        dropdownMenu.click()
+      }, 150)
+
+      dropdownToggle.addEventListener('shown.bs.dropdown', () => {
+        document.documentElement.click()
+        expectDropdownToBeOpened()
+      })
+
+      dropdownToggle.addEventListener('hidden.bs.dropdown', () => setTimeout(() => {
+        expect(dropdownToggle.classList.contains('show')).toEqual(false)
+        done()
+      }))
+
+      dropdownToggle.click()
+    })
+
+    it('should close dropdown (only) by clicking outside the dropdown menu when it has data-attribute `data-bs-auto-close="outside"`', done => {
+      fixtureEl.innerHTML = [
+        '<div class="dropdown">',
+        '  <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-auto-close="outside">Dropdown toggle</button>',
+        '  <div class="dropdown-menu">',
+        '    <a class="dropdown-item" href="#">Dropdown item</a>',
+        ' </div>',
+        '</div>'
+      ]
+
+      const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
+      const dropdownMenu = fixtureEl.querySelector('.dropdown-menu')
+
+      const expectDropdownToBeOpened = () => setTimeout(() => {
+        expect(dropdownToggle.classList.contains('show')).toEqual(true)
+        document.documentElement.click()
+      }, 150)
+
+      dropdownToggle.addEventListener('shown.bs.dropdown', () => {
+        dropdownMenu.click()
+        expectDropdownToBeOpened()
+      })
+
+      dropdownToggle.addEventListener('hidden.bs.dropdown', () => {
+        expect(dropdownToggle.classList.contains('show')).toEqual(false)
+        done()
+      })
+
+      dropdownToggle.click()
+    })
+
+    it('should not close dropdown by clicking inside or outside the dropdown menu when it has data-attribute `data-bs-auto-close="false"`', done => {
+      fixtureEl.innerHTML = [
+        '<div class="dropdown">',
+        '  <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-auto-close="false">Dropdown toggle</button>',
+        '  <div class="dropdown-menu">',
+        '    <a class="dropdown-item" href="#">Dropdown item</a>',
+        ' </div>',
+        '</div>'
+      ]
+
+      const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
+      const dropdownMenu = fixtureEl.querySelector('.dropdown-menu')
+
+      const expectDropdownToBeOpened = (shouldTriggerClick = true) => setTimeout(() => {
+        expect(dropdownToggle.classList.contains('show')).toEqual(true)
+        if (shouldTriggerClick) {
+          document.documentElement.click()
+        } else {
+          done()
+        }
+
+        expectDropdownToBeOpened(false)
+      }, 150)
+
+      dropdownToggle.addEventListener('shown.bs.dropdown', () => {
+        dropdownMenu.click()
+        expectDropdownToBeOpened()
+      })
+
+      dropdownToggle.click()
+    })
   })
 
   describe('jQueryInterface', () => {
index b5ded08a5b1ac4cef76af113de92e4cf6edc6ad1..d29212790af814c82824ff8524627a0ecf22b458 100644 (file)
@@ -903,6 +903,56 @@ Use `data-bs-offset` or `data-bs-reference` to change the location of the dropdo
 </div>
 {{< /example >}}
 
+### Auto close behavior
+
+By default, the dropdown menu is closed when clicking inside or outside the dropdown menu. You can use the `autoClose` option to change this behavior of the dropdown.
+
+{{< example >}}
+<div class="btn-group">
+  <button class="btn btn-secondary dropdown-toggle" type="button" id="defaultDropdown" data-bs-toggle="dropdown" data-bs-auto-close="true" aria-expanded="false">
+    Default dropdown
+  </button>
+  <ul class="dropdown-menu" aria-labelledby="defaultDropdown">
+    <li><a class="dropdown-item" href="#">Menu item</a></li>
+    <li><a class="dropdown-item" href="#">Menu item</a></li>
+    <li><a class="dropdown-item" href="#">Menu item</a></li>
+  </ul>
+</div>
+
+<div class="btn-group">
+  <button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuClickableOutside" data-bs-toggle="dropdown" data-bs-auto-close="inside" aria-expanded="false">
+    Clickable outside
+  </button>
+  <ul class="dropdown-menu" aria-labelledby="dropdownMenuClickableOutside">
+    <li><a class="dropdown-item" href="#">Menu item</a></li>
+    <li><a class="dropdown-item" href="#">Menu item</a></li>
+    <li><a class="dropdown-item" href="#">Menu item</a></li>
+  </ul>
+</div>
+
+<div class="btn-group">
+  <button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuClickableInside" data-bs-toggle="dropdown" data-bs-auto-close="outside" aria-expanded="false">
+    Clickable inside
+  </button>
+  <ul class="dropdown-menu" aria-labelledby="dropdownMenuClickableInside">
+    <li><a class="dropdown-item" href="#">Menu item</a></li>
+    <li><a class="dropdown-item" href="#">Menu item</a></li>
+    <li><a class="dropdown-item" href="#">Menu item</a></li>
+  </ul>
+</div>
+
+<div class="btn-group">
+  <button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuClickable" data-bs-toggle="dropdown" data-bs-auto-close="false" aria-expanded="false">
+    Manual close
+  </button>
+  <ul class="dropdown-menu" aria-labelledby="dropdownMenuClickable">
+    <li><a class="dropdown-item" href="#">Menu item</a></li>
+    <li><a class="dropdown-item" href="#">Menu item</a></li>
+    <li><a class="dropdown-item" href="#">Menu item</a></li>
+  </ul>
+</div>
+{{< /example >}}
+
 ## Sass
 
 ### Variables
@@ -967,7 +1017,7 @@ Regardless of whether you call your dropdown via JavaScript or instead use the d
 
 ### Options
 
-Options can be passed via data attributes or JavaScript. For data attributes, append the option name to `data-bs-`, as in `data-bs-offset=""`.
+Options can be passed via data attributes or JavaScript. For data attributes, append the option name to `data-bs-`, as in `data-bs-offset=""`. Make sure to change the case type of the option name from camelCase to kebab-case when passing the options via data attributes. For example, instead of using `data-bs-autoClose="false"`, use `data-bs-auto-close="false"`.
 
 <table class="table">
   <thead>
@@ -1007,6 +1057,20 @@ Options can be passed via data attributes or JavaScript. For data attributes, ap
         <p>For more information refer to Popper's <a href="https://popper.js.org/docs/v2/modifiers/offset/#options">offset docs</a>.</p>
       </td>
     </tr>
+    <tr>
+      <td><code>autoClose</code></td>
+      <td>boolean | string</td>
+      <td><code>true</code></td>
+      <td>
+        <p>Configure the auto close behavior of the dropdown:</p>
+        <ul>
+          <li><code>true</code> - the dropdown will be closed by clicking outside or inside the dropdown menu.</li>
+          <li><code>false</code> - the dropdown will be closed by clicking the toggle button and manually calling <code>hide</code> or <code>toggle</code> method. (Also will not be closed by pressing <kbd>esc</kbd> key)</li>
+          <li><code>'inside'</code> - the dropdown will be closed (only) by clicking inside the dropdown menu.</li>
+          <li><code>'outside'</code> - the dropdown will be closed (only) by clicking outside the dropdown menu.</li>
+        </ul>
+      </td>
+    </tr>
     <tr>
       <td><code>popperConfig</code></td>
       <td>null | object | function</td>