瀏覽代碼

Add TooltipController (w-tooltip) & remove bootstrap tooltips

Adds a new Stimulus controller for Tippy.js tooltips to replace only the Bootstrap tooltip usage & removes the bootstrap JS tooltip code.

- Dashboard - moderation listing panel
- Reports - Workflow listing status tooltip
- Shared Avatar - user name tooltip
- Workflow listing - steps item tooltip

Closes #8565
LB Johnston 1 年之前
父節點
當前提交
2ad71493a9

+ 0 - 108
client/scss/components/_tooltips.scss

@@ -1,108 +0,0 @@
-// From Bootstrap v3.0.0
-.tooltip {
-  position: absolute;
-  z-index: 1030;
-  display: block;
-  font-size: 12px;
-  line-height: 1.4;
-  opacity: 0;
-  visibility: visible;
-}
-
-.tooltip.in {
-  opacity: 0.9;
-}
-
-.tooltip.top {
-  padding: 5px 0;
-}
-
-.tooltip.right {
-  padding: 0 5px;
-}
-
-.tooltip.bottom {
-  padding: 5px 0;
-}
-
-.tooltip.left {
-  padding: 0 5px;
-}
-
-.tooltip-inner {
-  max-width: 200px;
-  padding: 3px 8px;
-  color: theme('colors.text-button');
-  text-align: center;
-  text-decoration: none;
-  background-color: theme('colors.surface-tooltip');
-  border-radius: 4px;
-}
-
-.tooltip-arrow {
-  position: absolute;
-  width: 0;
-  height: 0;
-  border-color: transparent;
-  border-style: solid;
-}
-
-.tooltip.top .tooltip-arrow {
-  bottom: 0;
-  inset-inline-start: 50%;
-  margin-inline-start: -5px;
-  border-top-color: theme('colors.surface-tooltip');
-  border-width: 5px 5px 0;
-}
-
-.tooltip.top-left .tooltip-arrow {
-  bottom: 0;
-  inset-inline-start: 5px;
-  border-top-color: theme('colors.surface-tooltip');
-  border-width: 5px 5px 0;
-}
-
-.tooltip.top-right .tooltip-arrow {
-  inset-inline-end: 5px;
-  bottom: 0;
-  border-top-color: theme('colors.surface-tooltip');
-  border-width: 5px 5px 0;
-}
-
-.tooltip.right .tooltip-arrow {
-  top: 50%;
-  inset-inline-start: 0;
-  margin-top: -5px;
-  border-inline-end-color: theme('colors.surface-tooltip');
-  border-width: 5px 5px 5px 0;
-}
-
-.tooltip.left .tooltip-arrow {
-  top: 50%;
-  inset-inline-end: 0;
-  margin-top: -5px;
-  border-inline-start-color: theme('colors.surface-tooltip');
-  border-width: 5px 0 5px 5px;
-}
-
-.tooltip.bottom .tooltip-arrow {
-  top: 0;
-  inset-inline-start: 50%;
-  margin-inline-start: -5px;
-  border-bottom-color: theme('colors.surface-tooltip');
-  border-width: 0 5px 5px;
-}
-
-.tooltip.bottom-left .tooltip-arrow {
-  top: 0;
-  inset-inline-start: 5px;
-  border-bottom-color: theme('colors.surface-tooltip');
-  border-width: 0 5px 5px;
-}
-
-.tooltip.bottom-right .tooltip-arrow {
-  top: 0;
-  inset-inline-end: 5px;
-  border-bottom-color: theme('colors.surface-tooltip');
-  border-width: 0 5px 5px;
-}

+ 0 - 1
client/scss/core.scss

@@ -148,7 +148,6 @@ These are classes for components.
 @import 'components/progressbar';
 @import 'components/summary';
 @import 'components/whats-new';
-@import 'components/tooltips';
 @import 'components/grid.legacy';
 @import 'components/footer';
 @import 'components/loading-mask';

+ 136 - 0
client/src/controllers/TooltipController.test.js

@@ -0,0 +1,136 @@
+import { Application } from '@hotwired/stimulus';
+import { TooltipController } from './TooltipController';
+
+describe('TooltipController', () => {
+  let application;
+
+  beforeEach(async () => {
+    document.body.innerHTML = `
+<section>
+  <button
+    type="button"
+    id="tooltip-default"
+    data-controller="w-tooltip"
+    data-w-tooltip-content-value="Extra content"
+  >
+    CONTENT
+  </button>
+  <button
+    id="tooltip-custom"
+    type="button"
+    data-controller="w-tooltip"
+    data-w-tooltip-content-value="Tippy top content"
+    data-w-tooltip-placement-value="top"
+    data-action="custom:show->w-tooltip#show custom:hide->w-tooltip#hide"
+  >
+    CONTENT
+  </button>
+</section`;
+
+    application = Application.start();
+    application.register('w-tooltip', TooltipController);
+
+    await Promise.resolve(requestAnimationFrame);
+
+    // set all animation durations to 0 so that tests can ignore animation delays
+    // Tippy relies on transitionend which is not yet supported in JSDom
+    // https://github.com/jsdom/jsdom/issues/1781
+
+    document
+      .querySelectorAll('[data-controller="w-tooltip"]')
+      .forEach((element) => {
+        application
+          .getControllerForElementAndIdentifier(element, 'w-tooltip')
+          .tippy.setProps({ duration: 0 }); // tippy will merge props with whatever has already been set
+      });
+  });
+
+  afterEach(() => {
+    application?.stop();
+  });
+
+  it('should create a tooltip when hovered & remove it when focus moves away', async () => {
+    const tooltipTrigger = document.getElementById('tooltip-default');
+
+    expect(document.querySelectorAll('[role="tooltip"]')).toHaveLength(0);
+
+    tooltipTrigger.dispatchEvent(new Event('mouseenter'));
+
+    await Promise.resolve(requestAnimationFrame);
+
+    expect(document.querySelectorAll('[role="tooltip"]')).toHaveLength(1);
+    const tooltip = document.querySelector('[role="tooltip"]');
+
+    expect(tooltip).toBeTruthy();
+
+    expect(tooltip.textContent).toEqual('Extra content');
+    expect(tooltip.dataset.placement).toEqual('bottom'); // the default placement
+  });
+
+  it('should create a tooltip that accepts a different placement value', async () => {
+    const tooltipTrigger = document.getElementById('tooltip-custom');
+
+    expect(document.querySelectorAll('[role="tooltip"]')).toHaveLength(0);
+
+    tooltipTrigger.dispatchEvent(new Event('mouseenter'));
+
+    await Promise.resolve(requestAnimationFrame);
+
+    const tooltip = document.querySelector('[role="tooltip"]');
+
+    expect(tooltip.textContent).toEqual('Tippy top content');
+    expect(tooltip.dataset.placement).toEqual('top');
+  });
+
+  it('should destroy the tippy instance on disconnect', async () => {
+    const tooltipTrigger = document.getElementById('tooltip-default');
+
+    const controller = application.getControllerForElementAndIdentifier(
+      tooltipTrigger,
+      'w-tooltip',
+    );
+
+    expect(controller.tippy).toBeDefined();
+    jest.spyOn(controller.tippy, 'destroy');
+    expect(controller.tippy.destroy).not.toHaveBeenCalled();
+
+    tooltipTrigger.removeAttribute('data-controller');
+
+    await Promise.resolve(requestAnimationFrame);
+
+    expect(controller.tippy.destroy).toHaveBeenCalled();
+  });
+
+  it('should support actions for show and hide', () => {
+    expect(document.querySelectorAll('[role="tooltip"]')).toHaveLength(0);
+
+    const tooltipTrigger = document.getElementById('tooltip-custom');
+
+    tooltipTrigger.dispatchEvent(new CustomEvent('custom:show'));
+
+    expect(document.querySelectorAll('[role="tooltip"]')).toHaveLength(1);
+
+    tooltipTrigger.dispatchEvent(new CustomEvent('custom:hide'));
+
+    expect(document.querySelectorAll('[role="tooltip"]')).toHaveLength(0);
+  });
+
+  it('should keep content in sync with any data attribute changes', async () => {
+    const tooltipTrigger = document.getElementById('tooltip-custom');
+
+    expect(document.querySelectorAll('[role="tooltip"]')).toHaveLength(0);
+
+    tooltipTrigger.dispatchEvent(new Event('mouseenter'));
+
+    const tooltip = document.querySelector('[role="tooltip"]');
+
+    expect(tooltip.textContent).toEqual('Tippy top content');
+
+    // change the content value
+    tooltipTrigger.setAttribute('data-w-tooltip-content-value', 'NEW content!');
+
+    await Promise.resolve(requestAnimationFrame);
+
+    expect(tooltip.textContent).toEqual('NEW content!');
+  });
+});

+ 56 - 0
client/src/controllers/TooltipController.ts

@@ -0,0 +1,56 @@
+import { Controller } from '@hotwired/stimulus';
+import tippy, { Placement, Props, Instance } from 'tippy.js';
+import { hideTooltipOnEsc } from '../includes/initTooltips';
+
+/**
+ * A Tippy.js tooltip with simple popover content.
+ *
+ * @example
+ * <button type="button" data-controller="w-tooltip" data-w-tooltip-content-value="More detail here">
+ *  A button with a tooltip
+ * </button>
+ */
+export class TooltipController extends Controller<HTMLElement> {
+  static values = {
+    content: String,
+    placement: { default: 'bottom', type: String },
+  };
+
+  declare contentValue: string;
+  declare placementValue: Placement;
+  tippy?: Instance<Props>;
+
+  connect() {
+    this.tippy = tippy(this.element, this.options);
+  }
+
+  contentValueChanged(newValue: string, oldValue: string) {
+    if (!oldValue || oldValue === newValue) return;
+    this.tippy?.setProps(this.options);
+  }
+
+  placementValueChanged(newValue: string, oldValue: string) {
+    if (!oldValue || oldValue === newValue) return;
+    this.tippy?.setProps(this.options);
+  }
+
+  hide() {
+    this.tippy?.hide();
+  }
+
+  show() {
+    this.tippy?.show();
+  }
+
+  get options(): Partial<Props> {
+    return {
+      content: this.contentValue,
+      placement: this.placementValue,
+      plugins: [hideTooltipOnEsc],
+    };
+  }
+
+  disconnect() {
+    this.tippy?.destroy();
+  }
+}

+ 2 - 0
client/src/controllers/index.ts

@@ -15,6 +15,7 @@ import { SubmitController } from './SubmitController';
 import { SwapController } from './SwapController';
 import { SyncController } from './SyncController';
 import { TagController } from './TagController';
+import { TooltipController } from './TooltipController';
 import { UpgradeController } from './UpgradeController';
 
 /**
@@ -36,5 +37,6 @@ export const coreControllerDefinitions: Definition[] = [
   { controllerConstructor: SwapController, identifier: 'w-swap' },
   { controllerConstructor: SyncController, identifier: 'w-sync' },
   { controllerConstructor: TagController, identifier: 'w-tag' },
+  { controllerConstructor: TooltipController, identifier: 'w-tooltip' },
   { controllerConstructor: UpgradeController, identifier: 'w-upgrade' },
 ];

+ 16 - 0
docs/releases/5.1.md

@@ -301,3 +301,19 @@ Note: The `data-w-tag-options-value` is a JSON object serialised into string. Dj
 Wagtail will try to use the cache called "renditions". If no such cache exists, it will fall back to using the default cache.
 You can [configure the "renditions" cache](custom_image_renditions_cache) to use a different cache backend or to provide 
 additional configuration parameters.
+
+### Tooltips now rely on new data attributes
+
+The undocumented Bootstrap jQuery tooltip widget is no longer in use, you will need to update any HTML that is using these attributes to the new syntax.
+
+**Old syntax**
+
+```html
+<span data-wagtail-tooltip="Tooltip content here">Label</span>
+```
+
+**New syntax**
+
+```html
+<span data-controller="w-tooltip" data-w-tooltip-content-value="Tooltip content here">Label</span>
+```

+ 0 - 386
wagtail/admin/static_src/wagtailadmin/js/vendor/bootstrap-tooltip.js

@@ -1,386 +0,0 @@
-/* ========================================================================
- * Bootstrap: tooltip.js v3.0.0
- * http://twbs.github.com/bootstrap/javascript.html#tooltip
- * Inspired by the original jQuery.tipsy by Jason Frame
- * ========================================================================
- * Copyright 2012 Twitter, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- * ======================================================================== */
-
-
-+function ($) { "use strict";
-
-  // TOOLTIP PUBLIC CLASS DEFINITION
-  // ===============================
-
-  var Tooltip = function (element, options) {
-    this.type       =
-    this.options    =
-    this.enabled    =
-    this.timeout    =
-    this.hoverState =
-    this.$element   = null
-
-    this.init('tooltip', element, options)
-  }
-
-  Tooltip.DEFAULTS = {
-    animation: true
-  , placement: 'top'
-  , selector: false
-  , template: '<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
-  , trigger: 'hover focus'
-  , title: ''
-  , delay: 0
-  , html: false
-  , container: false
-  }
-
-  Tooltip.prototype.init = function (type, element, options) {
-    this.enabled  = true
-    this.type     = type
-    this.$element = $(element)
-    this.options  = this.getOptions(options)
-
-    var triggers = this.options.trigger.split(' ')
-
-    for (var i = triggers.length; i--;) {
-      var trigger = triggers[i]
-
-      if (trigger == 'click') {
-        this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this))
-      } else if (trigger != 'manual') {
-        var eventIn  = trigger == 'hover' ? 'mouseenter' : 'focus'
-        var eventOut = trigger == 'hover' ? 'mouseleave' : 'blur'
-
-        this.$element.on(eventIn  + '.' + this.type, this.options.selector, $.proxy(this.enter, this))
-        this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this))
-      }
-    }
-
-    this.options.selector ?
-      (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) :
-      this.fixTitle()
-  }
-
-  Tooltip.prototype.getDefaults = function () {
-    return Tooltip.DEFAULTS
-  }
-
-  Tooltip.prototype.getOptions = function (options) {
-    options = $.extend({}, this.getDefaults(), this.$element.data(), options)
-
-    if (options.delay && typeof options.delay == 'number') {
-      options.delay = {
-        show: options.delay
-      , hide: options.delay
-      }
-    }
-
-    return options
-  }
-
-  Tooltip.prototype.getDelegateOptions = function () {
-    var options  = {}
-    var defaults = this.getDefaults()
-
-    this._options && $.each(this._options, function (key, value) {
-      if (defaults[key] != value) options[key] = value
-    })
-
-    return options
-  }
-
-  Tooltip.prototype.enter = function (obj) {
-    var self = obj instanceof this.constructor ?
-      obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type)
-
-    clearTimeout(self.timeout)
-
-    self.hoverState = 'in'
-
-    if (!self.options.delay || !self.options.delay.show) return self.show()
-
-    self.timeout = setTimeout(function () {
-      if (self.hoverState == 'in') self.show()
-    }, self.options.delay.show)
-  }
-
-  Tooltip.prototype.leave = function (obj) {
-    var self = obj instanceof this.constructor ?
-      obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type)
-
-    clearTimeout(self.timeout)
-
-    self.hoverState = 'out'
-
-    if (!self.options.delay || !self.options.delay.hide) return self.hide()
-
-    self.timeout = setTimeout(function () {
-      if (self.hoverState == 'out') self.hide()
-    }, self.options.delay.hide)
-  }
-
-  Tooltip.prototype.show = function () {
-    var e = $.Event('show.bs.'+ this.type)
-
-    if (this.hasContent() && this.enabled) {
-      this.$element.trigger(e)
-
-      if (e.isDefaultPrevented()) return
-
-      var $tip = this.tip()
-
-      this.setContent()
-
-      if (this.options.animation) $tip.addClass('fade')
-
-      var placement = typeof this.options.placement == 'function' ?
-        this.options.placement.call(this, $tip[0], this.$element[0]) :
-        this.options.placement
-
-      var autoToken = /\s?auto?\s?/i
-      var autoPlace = autoToken.test(placement)
-      if (autoPlace) placement = placement.replace(autoToken, '') || 'top'
-
-      $tip
-        .detach()
-        .css({ top: 0, left: 0, display: 'block' })
-        .addClass(placement)
-
-      this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element)
-
-      var pos          = this.getPosition()
-      var actualWidth  = $tip[0].offsetWidth
-      var actualHeight = $tip[0].offsetHeight
-
-      if (autoPlace) {
-        var $parent = this.$element.parent()
-
-        var orgPlacement = placement
-        var docScroll    = document.documentElement.scrollTop || document.body.scrollTop
-        var parentWidth  = this.options.container == 'body' ? window.innerWidth  : $parent.outerWidth()
-        var parentHeight = this.options.container == 'body' ? window.innerHeight : $parent.outerHeight()
-        var parentLeft   = this.options.container == 'body' ? 0 : $parent.offset().left
-
-        placement = placement == 'bottom' && pos.top   + pos.height  + actualHeight - docScroll > parentHeight  ? 'top'    :
-                    placement == 'top'    && pos.top   - docScroll   - actualHeight < 0                         ? 'bottom' :
-                    placement == 'right'  && pos.right + actualWidth > parentWidth                              ? 'left'   :
-                    placement == 'left'   && pos.left  - actualWidth < parentLeft                               ? 'right'  :
-                    placement
-
-        $tip
-          .removeClass(orgPlacement)
-          .addClass(placement)
-      }
-
-      var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight)
-
-      this.applyPlacement(calculatedOffset, placement)
-      this.$element.trigger('shown.bs.' + this.type)
-    }
-  }
-
-  Tooltip.prototype.applyPlacement = function(offset, placement) {
-    var replace
-    var $tip   = this.tip()
-    var width  = $tip[0].offsetWidth
-    var height = $tip[0].offsetHeight
-
-    // manually read margins because getBoundingClientRect includes difference
-    var marginTop = parseInt($tip.css('margin-top'), 10)
-    var marginLeft = parseInt($tip.css('margin-left'), 10)
-
-    // we must check for NaN for ie 8/9
-    if (isNaN(marginTop))  marginTop  = 0
-    if (isNaN(marginLeft)) marginLeft = 0
-
-    offset.top  = offset.top  + marginTop
-    offset.left = offset.left + marginLeft
-
-    $tip
-      .offset(offset)
-      .addClass('in')
-
-    // check to see if placing tip in new offset caused the tip to resize itself
-    var actualWidth  = $tip[0].offsetWidth
-    var actualHeight = $tip[0].offsetHeight
-
-    if (placement == 'top' && actualHeight != height) {
-      replace = true
-      offset.top = offset.top + height - actualHeight
-    }
-
-    if (/bottom|top/.test(placement)) {
-      var delta = 0
-
-      if (offset.left < 0) {
-        delta       = offset.left * -2
-        offset.left = 0
-
-        $tip.offset(offset)
-
-        actualWidth  = $tip[0].offsetWidth
-        actualHeight = $tip[0].offsetHeight
-      }
-
-      this.replaceArrow(delta - width + actualWidth, actualWidth, 'left')
-    } else {
-      this.replaceArrow(actualHeight - height, actualHeight, 'top')
-    }
-
-    if (replace) $tip.offset(offset)
-  }
-
-  Tooltip.prototype.replaceArrow = function(delta, dimension, position) {
-    this.arrow().css(position, delta ? (50 * (1 - delta / dimension) + "%") : '')
-  }
-
-  Tooltip.prototype.setContent = function () {
-    var $tip  = this.tip()
-    var title = this.getTitle()
-
-    $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title)
-    $tip.removeClass('fade in top bottom left right')
-  }
-
-  Tooltip.prototype.hide = function () {
-    var that = this
-    var $tip = this.tip()
-    var e    = $.Event('hide.bs.' + this.type)
-
-    function complete() {
-      if (that.hoverState != 'in') $tip.detach()
-    }
-
-    this.$element.trigger(e)
-
-    if (e.isDefaultPrevented()) return
-
-    $tip.removeClass('in')
-
-    $.support.transition && this.$tip.hasClass('fade') ?
-      $tip
-        .one($.support.transition.end, complete)
-        .emulateTransitionEnd(150) :
-      complete()
-
-    this.$element.trigger('hidden.bs.' + this.type)
-
-    return this
-  }
-
-  Tooltip.prototype.fixTitle = function () {
-    var $e = this.$element
-    if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') {
-      $e.attr('data-original-title', $e.attr('title') || '').attr('title', '')
-    }
-  }
-
-  Tooltip.prototype.hasContent = function () {
-    return this.getTitle()
-  }
-
-  Tooltip.prototype.getPosition = function () {
-    var el = this.$element[0]
-    return $.extend({}, (typeof el.getBoundingClientRect == 'function') ? el.getBoundingClientRect() : {
-      width: el.offsetWidth
-    , height: el.offsetHeight
-    }, this.$element.offset())
-  }
-
-  Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) {
-    return placement == 'bottom' ? { top: pos.top + pos.height,   left: pos.left + pos.width / 2 - actualWidth / 2  } :
-           placement == 'top'    ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2  } :
-           placement == 'left'   ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } :
-        /* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width   }
-  }
-
-  Tooltip.prototype.getTitle = function () {
-    var title
-    var $e = this.$element
-    var o  = this.options
-
-    title = $e.attr('data-original-title')
-      || (typeof o.title == 'function' ? o.title.call($e[0]) :  o.title)
-
-    return title
-  }
-
-  Tooltip.prototype.tip = function () {
-    return this.$tip = this.$tip || $(this.options.template)
-  }
-
-  Tooltip.prototype.arrow = function () {
-    return this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow')
-  }
-
-  Tooltip.prototype.validate = function () {
-    if (!this.$element[0].parentNode) {
-      this.hide()
-      this.$element = null
-      this.options  = null
-    }
-  }
-
-  Tooltip.prototype.enable = function () {
-    this.enabled = true
-  }
-
-  Tooltip.prototype.disable = function () {
-    this.enabled = false
-  }
-
-  Tooltip.prototype.toggleEnabled = function () {
-    this.enabled = !this.enabled
-  }
-
-  Tooltip.prototype.toggle = function (e) {
-    var self = e ? $(e.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type) : this
-    self.tip().hasClass('in') ? self.leave(self) : self.enter(self)
-  }
-
-  Tooltip.prototype.destroy = function () {
-    this.hide().$element.off('.' + this.type).removeData('bs.' + this.type)
-  }
-
-
-  // TOOLTIP PLUGIN DEFINITION
-  // =========================
-
-  var old = $.fn.tooltip
-
-  $.fn.tooltip = function (option) {
-    return this.each(function () {
-      var $this   = $(this)
-      var data    = $this.data('bs.tooltip')
-      var options = typeof option == 'object' && option
-
-      if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options)))
-      if (typeof option == 'string') data[option]()
-    })
-  }
-
-  $.fn.tooltip.Constructor = Tooltip
-
-
-  // TOOLTIP NO CONFLICT
-  // ===================
-
-  $.fn.tooltip.noConflict = function () {
-    $.fn.tooltip = old
-    return this
-  }
-
-}(window.jQuery);

+ 1 - 13
wagtail/admin/templates/wagtailadmin/home/workflow_objects_to_moderate.html

@@ -62,7 +62,7 @@
                             </td>
                             <td class="tasks" valign="top">
                                 {% for task in workflow_tasks %}
-                                    <span data-wagtail-tooltip="{{ task.name }}: {{ task.status_display }}">
+                                    <span data-controller="w-tooltip" data-w-tooltip-content-value="{{ task.name }}: {{ task.status_display }}">
                                         {% if task.status == 'approved' %}
                                             {% icon "success" title=task.status_display classname="default" %}
                                         {% elif task.status == 'rejected' %}
@@ -86,19 +86,7 @@
 
     <script src="{% versioned_static 'wagtailadmin/js/modal-workflow.js' %}"></script>
     <script src="{% versioned_static 'wagtailadmin/js/workflow-action.js' %}"></script>
-    <script src="{% versioned_static 'wagtailadmin/js/vendor/bootstrap-tooltip.js' %}"></script>
     <script>
         document.addEventListener('DOMContentLoaded', () => ActivateWorkflowActionsForDashboard('{{ csrf_token|escapejs }}'));
-        /* Tooltips used by the workflow status component */
-        $(function() {
-            $('[data-wagtail-tooltip]').tooltip({
-                animation: false,
-                title: function() {
-                    return $(this).attr('data-wagtail-tooltip');
-                },
-                trigger: 'hover',
-                placement: 'bottom',
-            });
-        });
     </script>
 {% endif %}

+ 0 - 1
wagtail/admin/templates/wagtailadmin/pages/_editor_js.html

@@ -26,5 +26,4 @@
 <script src="{% versioned_static 'wagtailadmin/js/vendor/urlify.js' %}"></script>
 <script src="{% versioned_static 'wagtailadmin/js/workflow-action.js' %}"></script>
 <script src="{% versioned_static 'wagtailadmin/js/workflow-status.js' %}"></script>
-<script src="{% versioned_static 'wagtailadmin/js/vendor/bootstrap-tooltip.js' %}"></script>
 {% hook_output 'insert_editor_js' %}

+ 0 - 10
wagtail/admin/templates/wagtailadmin/pages/edit.html

@@ -87,16 +87,6 @@
                 }
             );
 
-            /* Tooltips used by the workflow status component */
-            $('[data-wagtail-tooltip]').tooltip({
-                animation: false,
-                title: function() {
-                    return $(this).attr('data-wagtail-tooltip');
-                },
-                trigger: 'hover',
-                placement: 'bottom',
-            });
-
             {% get_comments_enabled as comments_enabled %}
             {% if comments_enabled %}
                 // Initialise comments UI

+ 1 - 20
wagtail/admin/templates/wagtailadmin/reports/workflow.html

@@ -58,7 +58,7 @@
                             <h2>{{ workflow_name }}</h2>
                             {% trans 'Incomplete task' as incomplete_title %}
                             {% for task in workflow_state.all_tasks_with_status %}
-                                <span data-wagtail-tooltip="{{ task.name }}: {{ task.status_display }}">
+                                <span data-controller="w-tooltip" data-w-tooltip-content-value="{{ task.name }}: {{ task.status_display }}">
                                     {% if task.status == 'approved' %}
                                         {% icon "success" title=task.status_display classname="initial" %}
                                     {% elif task.status == 'rejected' %}
@@ -79,22 +79,3 @@
         <p>{% trans "No pages/snippets have been submitted for moderation yet" %}</p>
     {% endif %}
 {% endblock %}
-
-{% block extra_js %}
-    {{ block.super }}
-
-    <script src="{% versioned_static 'wagtailadmin/js/vendor/bootstrap-tooltip.js' %}"></script>
-
-    <script>
-        $(function() {
-            $('[data-wagtail-tooltip]').tooltip({
-                animation: false,
-                title: function() {
-                    return $(this).attr('data-wagtail-tooltip');
-                },
-                trigger: 'hover',
-                placement: 'bottom',
-            });
-        })
-    </script>
-{% endblock %}

+ 1 - 1
wagtail/admin/templates/wagtailadmin/shared/avatar.html

@@ -8,7 +8,7 @@
     - `size` (string?) - small, large, square
     - `tooltip` (string?) - Modifier classes
 {% endcomment %}
-<span class='{% classnames "avatar" size classname %}' {% if tooltip %} data-wagtail-tooltip="{{ tooltip }}" {% endif %}>
+<span class='{% classnames "avatar" size classname %}' {% if tooltip %} data-controller="w-tooltip" data-w-tooltip-content-value="{{ tooltip }}" {% endif %}>
     {% if size == 'small' %}
         <img src="{% avatar_url user size=25 %}" alt="" decoding="async" loading="lazy"/>
     {% elif size == 'large' %}

+ 0 - 20
wagtail/admin/templates/wagtailadmin/shared/workflow_history/index.html

@@ -16,23 +16,3 @@
         {% include "wagtailadmin/shared/workflow_history/results.html" %}
     </div>
 {% endblock %}
-
-{% block extra_js %}
-    {{ block.super }}
-
-    {# load editor js to get tooltips #}
-    {% include "wagtailadmin/pages/_editor_js.html" %}
-
-    <script>
-        $(function() {
-            $('[data-wagtail-tooltip]').tooltip({
-                animation: false,
-                title: function() {
-                    return $(this).attr('data-wagtail-tooltip');
-                },
-                trigger: 'hover',
-                placement: 'bottom',
-            });
-        })
-    </script>
-{% endblock %}

+ 1 - 14
wagtail/admin/templates/wagtailadmin/workflows/index.html

@@ -65,7 +65,7 @@
                                 <ul class="workflow-tasks">
                                     {% for task in workflow.tasks|slice:":5" %}
                                         <a href="{% url 'wagtailadmin_workflows:edit_task' task.pk %}">
-                                            <li class="workflow-tasks__task" data-wagtail-tooltip="{% blocktrans trimmed with forloop.counter as step_number %}Step {{ step_number }}{% endblocktrans %}: {{ task.name }}">
+                                            <li class="workflow-tasks__task" data-controller="w-tooltip" data-w-tooltip-content-value="{% blocktrans trimmed with forloop.counter as step_number %}Step {{ step_number }}{% endblocktrans %}: {{ task.name }}">
                                                 <div class="workflow-tasks__step">{% blocktrans trimmed with forloop.counter as step_number %}Step {{ step_number }}{% endblocktrans %}</div>
                                                 <h4 class="workflow-tasks__name">{{ task.name }}</h4>
                                             </li>
@@ -96,17 +96,4 @@
 {% block extra_js %}
     {{ block.super }}
     {% include "wagtailadmin/pages/_editor_js.html" %}
-
-    <script>
-        $(function() {
-            $('[data-wagtail-tooltip]').tooltip({
-                animation: false,
-                title: function() {
-                    return $(this).attr('data-wagtail-tooltip');
-                },
-                trigger: 'hover',
-                placement: 'bottom',
-            });
-        });
-    </script>
 {% endblock %}

+ 0 - 13
wagtail/contrib/modeladmin/templates/modeladmin/create.html

@@ -16,19 +16,6 @@
     {{ media.js }}
 
     {% prepopulated_slugs %}
-
-    <script>
-        $(function() {
-            $('[data-wagtail-tooltip]').tooltip({
-                animation: false,
-                title: function() {
-                    return $(this).attr('data-wagtail-tooltip');
-                },
-                trigger: 'hover',
-                placement: 'bottom',
-            });
-        })
-    </script>
 {% endblock %}
 
 {% block content %}

+ 1 - 3
wagtail/contrib/modeladmin/tests/test_simple_modeladmin.py

@@ -674,9 +674,7 @@ class TestEditView(WagtailTestUtils, TestCase):
         self.assertContains(response, "The Lord of the Rings")
 
         # "Last updated" timestamp should be present
-        self.assertContains(
-            response, 'data-wagtail-tooltip="Sept. 30, 2021, 10:01 a.m."'
-        )
+        self.assertContains(response, 'data-tippy-content="Sept. 30, 2021, 10:01 a.m."')
         # History link should be present
         self.assertContains(response, 'href="/admin/modeladmintest/book/history/1/"')
 

+ 0 - 11
wagtail/snippets/templates/wagtailsnippets/snippets/edit.html

@@ -50,17 +50,6 @@
             window.wagtailConfig = window.wagtailConfig || {};
             window.wagtailConfig.ACTIVE_CONTENT_LOCALE = '{{ locale.language_code|escapejs }}';
         {% endif %}
-
-        $(function() {
-            $('[data-wagtail-tooltip]').tooltip({
-                animation: false,
-                title: function() {
-                    return $(this).attr('data-wagtail-tooltip');
-                },
-                trigger: 'hover',
-                placement: 'bottom',
-            });
-        });
     </script>
 
     {% if workflow_enabled %}