From fc9eac8bde71a63724c6497210302f4c7e644f95 Mon Sep 17 00:00:00 2001 From: Johannes Raggam Date: Fri, 23 Oct 2026 20:22:44 +0200 Subject: [PATCH] feat(pat-tooltip): Add content-text as source, which extracts the triggers text without markup. --- src/pat/tooltip/documentation.md | 18 +++++++++++++++- src/pat/tooltip/tooltip.js | 12 ++++++++++- src/pat/tooltip/tooltip.test.js | 36 ++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/src/pat/tooltip/documentation.md b/src/pat/tooltip/documentation.md index 28804469f..c55f7f754 100644 --- a/src/pat/tooltip/documentation.md +++ b/src/pat/tooltip/documentation.md @@ -24,6 +24,22 @@ By default, the tooltip uses the `title` attribute as its source. To use the The label is displayed as plain text and remains on the trigger element to provide its accessible name. +To use the trigger's text without its child markup, set `source: content-text`. +This includes visually hidden text, making it useful for icon links with a +screen-reader label: + + + + Settings + + +The tooltip displays `Settings` as plain text, without copying the SVG or the +label's styling. Define `sr-only` in your site's CSS to visually hide the label. +Leading and trailing whitespace is removed; the original contents remain in place. +This source uses `textContent`, so text inside an SVG (such as a ``) is +also included, even when the SVG has `aria-hidden="true"`. + ### Display Tooltips are shown when clicking on the triggering element. @@ -169,7 +185,7 @@ The available options are: | `position-list` | `auto` | `tl` `tm` `tr` `rt` `rm` `rb` `br` `bm` `bl` `lb` `lm` `lt` | The priority in which the pattern will try to position the tooltip. With the tooltip is positioned where the most space is on the screen. The two letters indicate the position of the triangle as opposed to the tooltip body. Adding `force` will force the tooltip position, even if it would end up out of view. | Multiple value | | `position-policy` | `auto` | `auto` `force` | Policy used to place a tooltip: either always use a listed position, or allow other positions if no space is available for the listed positions. | Mutually exclusive | | `trigger` | `click` | `click` `hover` | Sets which user action should make the tooltip appear. | Mutually exclusive | -| `source` | `title` | `ajax` `content` `title`, `aria-label` | Select where the contents of the tooltip is taken from: `ajax` load the tooltip contents from the link target url or from a selector given by the `href` attrobite. `content`: tooltip contents are the tooltip elements children. `title`, `aria-label`: tooltip content is the title resp aria-label attribute value. | Mutually exclusive | +| `source` | `title` | `ajax` `content` `content-text` `title` `aria-label` | Select where the contents of the tooltip is taken from: `ajax` load the tooltip contents from the link target url or from a selector given by the `href` attrobite. `content`: tooltip contents are the tooltip elements children. `content-text`: trimmed text content of the trigger, including visually hidden text, displayed as plain text. `title`, `aria-label`: tooltip content is the title resp aria-label attribute value. | Mutually exclusive | | `delay` | `0` | _time_ | The delay for the tooltip to appear, expressed in milliseconds or with `m` (minutes), `s` (seconds), `ms` (milliseconds) units. | Time | | `mark-inactive` | `true` | `true` `false` | Should we add inactive class to the tooltip trigger? | Bool | | `closing` | `auto` | `auto` `sticky` `close-button` | Auto means that the tooltip will disappear when the user clicks out of the tooltip, or — in case of hover triggered tooltips — hovers away from the trigger element. `close-button` will add a close button to the tooltip which must be used to close the tooltip. | Mutually exclusive | diff --git a/src/pat/tooltip/tooltip.js b/src/pat/tooltip/tooltip.js index d235420e4..94bbd9978 100644 --- a/src/pat/tooltip/tooltip.js +++ b/src/pat/tooltip/tooltip.js @@ -29,7 +29,13 @@ const all_positions = [ parser.addArgument("position-list", [], all_positions, true); parser.addArgument("position-policy", "auto", ["auto", "force"]); parser.addArgument("trigger", "click", ["click", "hover", "none"]); -parser.addArgument("source", "title", ["ajax", "aria-label", "content", "title"]); +parser.addArgument("source", "title", [ + "ajax", + "aria-label", + "content", + "content-text", + "title", +]); parser.addArgument("ajax-data-type", "html", ["html", "markdown"]); parser.addArgument("closing", "auto", ["auto", "sticky", "close-button"]); parser.addArgument("delay"); @@ -213,6 +219,10 @@ class Pattern extends BasePattern { // Tooltip content from trigger childs. content = this.el.innerHTML; tippy_options.allowHTML = true; + } else if (opts.source === "content-text") { + // Include visually hidden text without copying child markup. + content = this.el.textContent.trim(); + tippy_options.allowHTML = false; } else if (opts.source === "ajax") { // Tooltiop content from AJAX request. content = document.createElement("progress"); diff --git a/src/pat/tooltip/tooltip.test.js b/src/pat/tooltip/tooltip.test.js index 4175beba9..f11b80743 100644 --- a/src/pat/tooltip/tooltip.test.js +++ b/src/pat/tooltip/tooltip.test.js @@ -1315,6 +1315,42 @@ describe("pat-tooltip", () => { ).textContent; expect(expected).toBe(tooltip_content); }); + + describe("6.10 - source: content-text", () => { + it.each([ + [ + "6.10.1 - extracts the visually hidden label without SVG markup", + '<svg aria-hidden="true"><path d="M0 0h10v10z" /></svg><span class="sr-only" style="position: absolute; clip: rect(0, 0, 0, 0)">Save changes</span>', + "Save changes", + ], + [ + "6.10.2 - renders nested markup and escaped HTML as plain text", + "<span><strong>Save</strong> & <em>close</em></span>", + "<strong>Save</strong> & close", + ], + ["6.10.3 - trims whitespace-only content to an empty string", " ", ""], + ])("%s", async (_description, markup, expected) => { + document.body.innerHTML = ` + <a class="pat-tooltip" href="#save" + data-pat-tooltip="source: content-text; trigger: hover"> + ${markup} + </a> + `; + const el = document.querySelector(".pat-tooltip"); + const original_markup = el.innerHTML; + const instance = new Pattern(el); + await events.await_pattern_init(instance); + + el.dispatchEvent(new Event("focus")); + await utils.timeout(1); + + const content = instance.tippy.popper.querySelector(".tippy-content"); + expect(content.textContent).toBe(expected); + expect(content.children).toHaveLength(0); + expect(el.innerHTML).toBe(original_markup); + instance.destroy(); + }); + }); }); describe(`7 - if the 'source' parameter is 'ajax'`, () => {