Skip to content

Commit

Permalink
Add tooltip component (#2843)
Browse files Browse the repository at this point in the history
* Add Tooltip component to common

Will be used to provide backwards compatibility when we switch to CSS tooltips.

All other methods of creating tooltips are deprecated and this component-based method should be used instead.

* Modify direct child instead of using container element

Instead of using a container to house the tooltip, we'll now modify the
first direct child of the Tooltip component.

The Tooltip component will ensure that:
- children are passed to it
- only one child is present
- that child is an actual HTML Element and not a text node, or similar
- that child is currently present in the DOM

Only after all of the above are satisfied, will the tooltip be created
on that element. We store a reference to the DOM node that the tooltip
should be created on, then use this to perform tooltip actions via
jQuery. If this element gets changes (e.g. the tooltip content is
updated to another element) then the tooltip will be recreated.

If any of the first 3 requirements are not satisfied, an error will
be thrown to alert the developer to their misuse of this component.

To make this work, we do need to overwrite the title attribute of
the element with the tooltip, but this is the only solution other than
specifying `title` as an option when making the tooltip, but this is
not accessible by screenreaders unless they simulate a hover on the
element.

* Add warning about component overwriting `title` attr

* Update previous uses of Tooltip component
  • Loading branch information
davwheat authored May 10, 2021
1 parent 9bfb7f9 commit f9e8424
Show file tree
Hide file tree
Showing 10 changed files with 409 additions and 70 deletions.
68 changes: 68 additions & 0 deletions js/@types/tooltips/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* Selection of options accepted by [Bootstrap's tooltips](https://getbootstrap.com/docs/3.3/javascript/#tooltips-options).
*
* ---
*
* Not all options are present from Bootstrap to discourage the use of options
* that will be deprecated in the future.
*
* More commonly used options that will be deprecated remain, but are marked as
* such.
*
* @see https://getbootstrap.com/docs/3.3/javascript/#tooltips-options
*/
export interface TooltipCreationOptions {
/**
* Whether HTML content is allowed in the tooltip.
*
* ---
*
* **Warning:** this is a possible XSS attack vector. This option shouldn't
* be used wherever possible, and will not work when we migrate to CSS-only
* tooltips.
*
* @deprecated
*/
html?: boolean;
/**
* Tooltip position around the target element.
*/
placement?: 'top' | 'bottom' | 'left' | 'right';
/**
* Sets the delay between a trigger state occurring and the tooltip appearing
* on-screen.
*
* ---
*
* **Warning:** this option will be removed when we switch to CSS-only
* tooltips.
*
* @deprecated
*/
delay?: number;
/**
* Value used if no `title` attribute is present on the HTML element.
*
* If a function is given, it will be called with its `this` reference set to
* the element that the tooltip is attached to.
*/
title?: string;
/**
* How the tooltip is triggered.
*
* Either on `hover`, on `hover focus` (either of the two).
*
* ---
*
* **Warning:** `manual`, `click` and `focus` on its own are deprecated options
* which will not be supported in the future.
*/
trigger?: 'hover' | 'hover focus';
}

/**
* Creates a tooltip on a jQuery element reference.
*
* Returns the same jQuery reference to allow for method chaining.
*/
export type TooltipJQueryFunction = (tooltipOptions?: TooltipCreationOptions | 'destroy' | 'show' | 'hide') => JQuery;
11 changes: 3 additions & 8 deletions js/shims.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import * as _$ from 'jquery';
// Globals from flarum/core
import Application from './src/common/Application';

import type { TooltipJQueryFunction } from './@types/tooltips';

/**
* flarum/core exposes several extensions globally:
*
Expand All @@ -25,14 +27,7 @@ declare global {

// Extend JQuery with our custom functions, defined with $.fn
interface JQuery {
/**
* Creates a tooltip on a jQuery element reference.
*
* Optionally accepts placement and delay options.
*
* Returns the same reference to allow for method chaining.
*/
tooltip: (tooltipOptions?: { placement?: 'top' | 'bottom' | 'left' | 'right'; delay?: number }) => JQuery;
tooltip: TooltipJQueryFunction;
}
}

Expand Down
2 changes: 2 additions & 0 deletions js/src/common/compat.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import GroupBadge from './components/GroupBadge';
import TextEditor from './components/TextEditor';
import TextEditorButton from './components/TextEditorButton';
import EditUserModal from './components/EditUserModal';
import Tooltip from './components/Tooltip';
import Model from './Model';
import Application from './Application';
import fullTime from './helpers/fullTime';
Expand Down Expand Up @@ -141,6 +142,7 @@ export default {
'components/GroupBadge': GroupBadge,
'components/TextEditor': TextEditor,
'components/TextEditorButton': TextEditorButton,
'components/Tooltip': Tooltip,
'components/EditUserModal': EditUserModal,
Model: Model,
Application: Application,
Expand Down
26 changes: 15 additions & 11 deletions js/src/common/components/Badge.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Tooltip from './Tooltip';
import Component from '../Component';
import icon from '../helpers/icon';
import extract from '../utils/extract';
import classList from '../utils/classList';

/**
* The `Badge` component represents a user/discussion badge, indicating some
Expand All @@ -17,19 +18,22 @@ import extract from '../utils/extract';
*/
export default class Badge extends Component {
view() {
const attrs = Object.assign({}, this.attrs);
const type = extract(attrs, 'type');
const iconName = extract(attrs, 'icon');
const { type, icon: iconName, label, ...attrs } = this.attrs;

attrs.className = 'Badge ' + (type ? 'Badge--' + type : '') + ' ' + (attrs.className || '');
attrs.title = extract(attrs, 'label') || '';
const className = classList('Badge', [type && `Badge--${type}`], attrs.className);

return <span {...attrs}>{iconName ? icon(iconName, { className: 'Badge-icon' }) : m.trust('&nbsp;')}</span>;
}
const iconChild = iconName ? icon(iconName, { className: 'Badge-icon' }) : m.trust('&nbsp;');

const badgeAttrs = {
className,
...attrs,
};

const badgeNode = <div {...badgeAttrs}>{iconChild}</div>;

oncreate(vnode) {
super.oncreate(vnode);
// If we don't have a tooltip label, don't render the tooltip component.
if (!label) return badgeNode;

if (this.attrs.label) this.$().tooltip();
return <Tooltip text={label}>{badgeNode}</Tooltip>;
}
}
11 changes: 4 additions & 7 deletions js/src/common/components/TextEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import listItems from '../helpers/listItems';
import Button from './Button';

import BasicEditorDriver from '../utils/BasicEditorDriver';
import Tooltip from './Tooltip';

/**
* The `TextEditor` component displays a textarea with controls, including a
Expand Down Expand Up @@ -108,13 +109,9 @@ export default class TextEditor extends Component {
if (this.attrs.preview) {
items.add(
'preview',
Button.component({
icon: 'far fa-eye',
className: 'Button Button--icon',
onclick: this.attrs.preview,
title: app.translator.trans('core.forum.composer.preview_tooltip'),
oncreate: (vnode) => $(vnode.dom).tooltip(),
})
<Tooltip text={app.translator.trans('core.forum.composer.preview_tooltip')}>
<Button icon="far fa-eye" className="Button Button--icon" onclick={this.attrs.preview} />
</Tooltip>
);
}

Expand Down
17 changes: 11 additions & 6 deletions js/src/common/components/TextEditorButton.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import Button from './Button';
import Tooltip from './Tooltip';

/**
* The `TextEditorButton` component displays a button suitable for the text
* editor toolbar.
*/
export default class TextEditorButton extends Button {
static initAttrs(attrs) {
super.initAttrs(attrs);
view(vnode) {
const originalView = super.view(vnode);

attrs.className = attrs.className || 'Button Button--icon Button--link';
// Steal tooltip label from the Button superclass
const tooltipText = originalView.attrs.title;
delete originalView.attrs.title;

return <Tooltip text={tooltipText}>{originalView}</Tooltip>;
}

oncreate(vnode) {
super.oncreate(vnode);
static initAttrs(attrs) {
super.initAttrs(attrs);

this.$().tooltip();
attrs.className = attrs.className || 'Button Button--icon Button--link';
}
}
Loading

0 comments on commit f9e8424

Please sign in to comment.