]> git.ipfire.org Git - thirdparty/bootstrap.git/commitdiff
Tooltip/Popover/Menu: small fixes (contenteditable, arrow color, docs) (#42550)
authorMark Otto <markd.otto@gmail.com>
Sat, 27 Jun 2026 03:15:31 +0000 (20:15 -0700)
committerGitHub <noreply@github.com>
Sat, 27 Jun 2026 03:15:31 +0000 (20:15 -0700)
* Menu: don't hijack arrow keys inside contenteditable elements

The keydown handler only exempted <input>/<textarea>, so arrow keys were
captured (and preventDefault'd) inside contenteditable hosts like rich-text
editors. Treat contenteditable targets like inputs. Fixes #41021.

* Popover: match bottom arrow fill to header background

A bottom-placed popover with a header had its arrow filled with
--popover-bg, so it didn't match the header it points into. Use
--popover-header-bg for the arrow when a header follows. Fixes #40993.

* Docs: clarify tooltip/popover title/content function arguments

The function receives the attached element as its first argument (and as
its this reference), not just this. Fixes #40571.

* Bump bundlewatch size thresholds

js/src/menu.js
js/tests/unit/menu.spec.js
scss/_popover.scss
site/src/content/docs/components/popover.mdx
site/src/content/docs/components/tooltip.mdx

index 28599a8f1af04f5275c19a666226ef048c9a0df8..69c2f9e00e9bfd926e1299952d9d4721a44f671c 100644 (file)
@@ -916,7 +916,9 @@ class Menu extends BaseComponent {
   }
 
   static dataApiKeydownHandler(event) {
-    const isInput = /input|textarea/i.test(event.target.tagName)
+    // Treat contenteditable hosts (e.g. rich-text editors) like inputs so the
+    // menu doesn't hijack their arrow keys.
+    const isInput = /input|textarea/i.test(event.target.tagName) || event.target.isContentEditable
     const isEscapeEvent = event.key === ESCAPE_KEY
     const isUpOrDownEvent = [ARROW_UP_KEY, ARROW_DOWN_KEY].includes(event.key)
     const isLeftOrRightEvent = [ARROW_LEFT_KEY, ARROW_RIGHT_KEY].includes(event.key)
index c88d54891ef8c078dec98d2130ff6f693e5ed8d2..9a02719f1ca9b117fde115dabbc58125c52f9dbf 100644 (file)
@@ -1712,6 +1712,36 @@ describe('Menu', () => {
       })
     })
 
+    it('should ignore keyboard events within contenteditable elements', () => {
+      return new Promise(resolve => {
+        fixtureEl.innerHTML = [
+          '<div>',
+          '  <button class="btn" data-bs-toggle="menu">Menu</button>',
+          '  <div class="menu">',
+          '    <a class="menu-item" href="#sub1">Submenu 1</a>',
+          '    <div class="editor" contenteditable="true"></div>',
+          '  </div>',
+          '</div>'
+        ].join('')
+
+        const triggerMenu = fixtureEl.querySelector('[data-bs-toggle="menu"]')
+        const editor = fixtureEl.querySelector('.editor')
+
+        triggerMenu.addEventListener('shown.bs.menu', () => {
+          editor.focus()
+          const keydown = createEvent('keydown')
+
+          keydown.key = 'ArrowUp'
+          editor.dispatchEvent(keydown)
+
+          expect(document.activeElement).toEqual(editor, 'contenteditable still focused')
+          resolve()
+        })
+
+        triggerMenu.click()
+      })
+    })
+
     it('should skip disabled element when using keyboard navigation', () => {
       return new Promise(resolve => {
         fixtureEl.innerHTML = [
index 9377f5f03ec2c75a40382fdf99a1ce184563d741..0060ff3ce4df86bcefb02c46a091f47de135d271 100644 (file)
@@ -136,6 +136,12 @@ $popover-tokens: defaults(
         top: var(--popover-border-width);
         border-block-end-color: var(--popover-bg);
       }
+
+      // When the popover has a header, the bottom arrow points into the header,
+      // so its fill should match the header background, not the body background.
+      &:has(+ .popover-header)::after {
+        border-block-end-color: var(--popover-header-bg);
+      }
     }
 
     // This will remove the popover-header's border just below the arrow
index e0e3e3d8244be31dd84555b4d8d10b2c3f5b301c..c81f5f7c915fe230a75f6011afdc765edadeb802 100644 (file)
@@ -254,7 +254,7 @@ Note that for security reasons the `sanitize`, `sanitizeFn`, and `allowList` opt
 | `animation` | boolean | `true` | Apply a CSS fade transition to the popover. |
 | `boundary` | string, element | `'clippingParents'` | Overflow constraint boundary of the popover (applies only to Floating UI’s shift middleware). By default, it’s `'clippingParents'` and can accept an HTMLElement reference (via JavaScript only). For more information refer to Floating UI’s [shift docs](https://floating-ui.com/docs/shift#boundary). |
 | `container` | string, element, false | `false` | Appends the popover to a specific element. Example: `container: 'body'`. This option is particularly useful in that it allows you to position the popover in the flow of the document near the triggering element -&nbsp;which will prevent the popover from floating away from the triggering element during a window resize. |
-| `content` | string, element, function | `''` | The popover’s text content. If a function is given, it will be called with its `this` reference set to the element that the popover is attached to. |
+| `content` | string, element, function | `''` | The popover’s text content. If a function is given, it will be called with the element it is attached to as its first argument, and with its `this` reference set to that same element. |
 | `customClass` | string, function | `''` | Add classes to the popover when it is shown. Note that these classes will be added in addition to any classes specified in the template. To add multiple classes, separate them with spaces: `'class-1 class-2'`. You can also pass a function that should return a single string containing additional class names. |
 | `delay` | number, object | `0` | Delay showing and hiding the popover (ms)—doesn’t apply to manual trigger type. If a number is supplied, delay is applied to both hide/show. Object structure is: `delay: { "show": 500, "hide": 100 }`. |
 | `fallbackPlacements` | array | `['top', 'right', 'bottom', 'left']` | Define fallback placements by providing a list of placements in array (in order of preference). For more information refer to Floating UI’s [flip docs](https://floating-ui.com/docs/flip#fallbackplacements). |
@@ -266,7 +266,7 @@ Note that for security reasons the `sanitize`, `sanitizeFn`, and `allowList` opt
 | `sanitizeFn` | null, function | `null` | Provide an alternative [content sanitization]([[docsref:/getting-started/javascript#sanitizer]]) function. This can be useful if you prefer to use a dedicated library to perform sanitization. |
 | `selector` | string, false | `false` | If a selector is provided, popover objects will be delegated to the specified targets. In practice, this is used to also apply popovers to dynamically added DOM elements (`jQuery.on` support). See [this issue]([[config:repo]]/issues/4215) and [an informative example](https://codepen.io/Johann-S/pen/djJYPb). **Note**: `title` attribute must not be used as a selector. |
 | `template` | string | `'<div class="popover" role="tooltip"><div class="popover-arrow"></div><h3 class="popover-header"></h3><div class="popover-body"></div></div>'` | Base HTML to use when creating the popover. The popover’s `title` will be injected into the `.popover-header`. The popover’s `content` will be injected into the `.popover-body`. `.popover-arrow` will become the popover’s arrow. The outermost wrapper element should have the `.popover` class and `role="tooltip"`. |
-| `title` | string, element, function | `''` | The popover title. If a function is given, it will be called with its `this` reference set to the element that the popover is attached to. |
+| `title` | string, element, function | `''` | The popover title. If a function is given, it will be called with the element it is attached to as its first argument, and with its `this` reference set to that same element. |
 | `trigger` | string | `'click'` | How popover is triggered: click, hover, focus, manual. You may pass multiple triggers; separate them with a space. `'manual'` indicates that the popover will be triggered programmatically via the `.popover('show')`, `.popover('hide')` and `.popover('toggle')` methods; this value cannot be combined with any other trigger. `'hover'` on its own will result in popovers that cannot be triggered via the keyboard, and should only be used if alternative methods for conveying the same information for keyboard users is present. |
 </BsTable>
 
index 2a5cc65d729d1068aaa51d15a659a1916e695a14..06c272002196dc80b53e1dbbd8e8f013758a4bb9 100644 (file)
@@ -250,7 +250,7 @@ Note that for security reasons the `sanitize`, `sanitizeFn`, and `allowList` opt
 | `sanitizeFn` | null, function | `null` | Provide an alternative [content sanitization]([[docsref:/getting-started/javascript#sanitizer]]) function. This can be useful if you prefer to use a dedicated library to perform sanitization. |
 | `selector` | string, false | `false` | If a selector is provided, tooltip objects will be delegated to the specified targets. In practice, this is used to also apply tooltips to dynamically added DOM elements (`jQuery.on` support). See [this issue]([[config:repo]]/issues/4215) and [an informative example](https://codepen.io/Johann-S/pen/djJYPb). **Note**: `title` attribute must not be used as a selector. |
 | `template` | string | `'<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'` | Base HTML to use when creating the tooltip. The tooltip’s `title` will be injected into the `.tooltip-inner`. `.tooltip-arrow` will become the tooltip’s arrow. The outermost wrapper element should have the `.tooltip` class and `role="tooltip"`. |
-| `title` | string, element, function | `''` | The tooltip title. If a function is given, it will be called with its `this` reference set to the element that the popover is attached to. |
+| `title` | string, element, function | `''` | The tooltip title. If a function is given, it will be called with the element it is attached to as its first argument, and with its `this` reference set to that same element. |
 | `trigger` | string | `'hover focus'` | How tooltip is triggered: click, hover, focus, manual. You may pass multiple triggers; separate them with a space. `'manual'` indicates that the tooltip will be triggered programmatically via the `.tooltip('show')`, `.tooltip('hide')` and `.tooltip('toggle')` methods; this value cannot be combined with any other trigger. `'hover'` on its own will result in tooltips that cannot be triggered via the keyboard, and should only be used if alternative methods for conveying the same information for keyboard users is present. |
 </BsTable>