atomiks / tippyjs

Tooltip, popover, dropdown, and menu library
https://atomiks.github.io/tippyjs/
MIT License
11.92k stars 520 forks source link

V3 API / Wishlist 🌟 #201

Closed atomiks closed 5 years ago

atomiks commented 6 years ago

V3 Philosophy

Reduce mutations and side effects. There are a lot of mutations and side-effects happening right now everywhere in the lib which has caused strange hard to debug issues in the past and still does.

Since v0 the API has been to set the title attribute on the reference which is the tooltip content (if not html).

The idea behind that was it's just like the native tooltip, but a tippy one instead - all you have to do is invoke tippy(). Additionally, it provided automatic fallback for unsupported browsers. But this involves removing the title and replacing it with a tippy one, a mutation that has caused various kinds of issues.

No setup for beginners: declaratively indicate that elements should have a tooltip 🐤

<button data-tippy="My tooltip">My button</button>

On load, all references with a data-tippy will have their tooltips initialized with the value specified. This isn't required though, it's just a "no setup required" way to create tooltips as long as the script is loaded in the document.

TODO: List of changes so far

Tests 🚥

v3 won't be released until it has extensive coverage of the entire library. I've been putting this off because it's a lot of work but it will be done for v3.

tippy() call

tippy(reference, { invalidOption: true })
// Error: invalidOption is not a valid option

selector is not a good name for the first argument given to tippy, it can be a wide range of values. Therefore it will be named targets (inspired by animejs).

tooltips will be renamed to instances

tippy(firstArg, secondArg)
/*
{
  targets: firstArg,
  options: { ...Defaults, ...secondArg },
  instances: [tip, tip, ...],
  destroyAll() { ... }
}
*/

Instances

The tippy instance will now contain this property to easily access the element nodes:

popperChildren: {
  tooltip: Element,
  content: Element,
  arrow: Element || null,
  backdrop: Element || null
}
instance.set(options)

Currently there is no way to update options at runtime reliably except for ones that aren't init-related. You need to destroy the tooltip instance and make a new one. Also it involves mutating the options object. For example, while options.delay can be updated any time and the change will be reflected, options.arrow can't.

instance.set({ arrow: true, duration: 0 }) // the tooltip will be "re-rendered" to reflect the change

Naming

is will be added as a prefix to things that are bools to make them more semantic.

In tippy instances:

state: {
  isEnabled,
  isDestroyed,
  isVisible
}

In tippy.browser

{
  isIE: /MSIE |Trident\//.test(nav.userAgent),
  isIOS: /iPhone|iPad|iPod/.test(nav.platform) && !win.MSStream,
  isSupported: 'MutationObserver' in win,
  supportsTouch: 'ontouchstart' in win,
  isUsingTouch: false,
  userInputDetectionEnabled: true,
  onUserInputChange: () => {}
}

DOM

setAttr(
  reference,
  options.target ? 'data-tippy-delegate' : 'data-tippy-reference'
)

CSS

Options

Removed: ~title~ attribute, ~html~

We'll just have a content option, which covers both title and html. This is distinct from setting a value to data-tippy if you don't want it created on load.

<button data-tippy-content="My tooltip">My button<button>
<script>tippy('button')</script>

or

<button>My button</button>
<script>
tippy('button', {
  // text
  content: 'My tooltip',
  // html
  content: '<strong>My tooltip</strong>',
  // element
  content: document.querySelector('template'),
  // clone element (no more selector string)
  content: document.querySelector('template').cloneNode(true)
})
</script>

Ensures accessibility by determining if the reference element can receive focus.

export const elementCanReceiveFocus = el =>
  matches.call(
    el,
    'a[href],area[href],button,details,input,textarea,select,iframe,[tabindex]'
  ) && !el.hasAttribute('disabled')

// Usage
if (!elementCanReceiveFocus(reference)) {
  // set tabindex="0" attribute.
}

Browser support 💻

For V3, I'm officially declaring that I don't care about browsers whose usage share is less than 0.5% worldwide. 📉

This means IE11 is still supported. I will continue to care about it until it falls under the threshold 😞 .

Desktop fallback 🖥

Fallback will need to be handled manually, as it's a mutation that I don't want to deal with and can vary a lot based on your usage during the lifetime of a tooltip.

const content = 'My tooltip'

tippy(reference, { content })

if (!tippy.browser.isSupported) {
  reference.title = content
}

Mobile fallback 📱

title attributes don't show up on mobile. The ideal solution would honestly to be just inline the tooltip content next to the reference if it's not a supported browser. It won't be pretty, but at least the tooltip content will be shown to the user.

Maybe I can have a built-on solution for this.

pomartel commented 6 years ago

Thanks for this very useful library! I think right now you only use a Mutation Observer for dynamic titles. Could an observer also be used to automatically create new tooltips and watch the DOM for changes when new elements with a [title] are added?

atomiks commented 6 years ago

@pomartel you can use event delegation now which solves that problem. A Mutation Observer would be inefficient for that sort of behavior.

pomartel commented 6 years ago

Got it! I was trying to use event delegation to have a parent be the trigger for a tippy on a child element. Like in this screenshot here where you hover on the label and the tippy displays on the child question mark wrapped in a span tag.

polls_for_pages

Right now I use custom code with _tippy.show() and _tippy.hide(). Would there be an easier way to do this with event delegation?

atomiks commented 6 years ago

Event delegation just defers creating the instance for children until they are hovered over, but it can't do something like that unfortunately. It has been suggested though (#129) so I will try to implement it soon.

mother10 commented 6 years ago

Would it be possible to have a version number inside the tippy js-file?

atomiks commented 6 years ago

@mother10 sure, I think I can use a Rollup banner for that, along with a property tippy.version on the module itself.

mother10 commented 6 years ago

Ah that would be great too, but i meant inside the tippy.js file so when you look inside with a text editor or something you can see you have the correct version. Like with bootstrap.

atomiks commented 6 years ago

You mean this (banner?):

banner

mother10 commented 6 years ago

This is what i see from the tippy.js that was installed with NPM:

image

atomiks commented 6 years ago

Yep, I haven't added one yet, but I'm going to make a PR for it and clean up all the Rollup scripts 😄 so the next version will have it. Not really related to V3 as it's not a breaking change

rrfaria commented 6 years ago

I would suggest template string in options to inicialize lib Example:

tippy('.btn', {
template:"<div>text</div>"
))

or

const template = `
        <div>text</div>
        <ul>
            <li>test</li>
            <li>test</li>
        </ul>
    `;
`
tippy('.btn', {
template:template
))
j-blandford commented 6 years ago

I am using Tippy with Angular. When the new version comes out I will make a DefinitelyTyped definition for the new API

Thanks for creating such a customizable library 👍

rcheung9 commented 6 years ago

Allow html strings in the html option would be good. I'm using a templating library like Mustache to create the markup.

vairamsvsjdo commented 6 years ago

This thread is good. Would prefer a feature list and accept in a project. Makes it easy to see which one are proposed, accepted, developed, completed.

vairamsvsjdo commented 6 years ago

Enable to use as a web component

my tippy with props used for example: {{titlte}} my tippy 2
atomiks commented 6 years ago

@vairamsvsjdo this project is too small to bother with something like that I think

I've updated the OP to show a todo list of changes. If you have any objections, mention them here.

atomiks commented 6 years ago

Here's a taste of the benefits of a .set() method and having the content of the tooltip as an option.

This is how the vanilla AJAX example works from the docs now:

const INITIAL_CONTENT = 'Loading...'

const state = {
  isFetching: false,
  canFetch: true
}

tippy('.btn', {
  content: INITIAL_CONTENT,
  async onShow(tip) {
    if (state.isFetching || !state.canFetch) return

    state.isFetching = true
    state.canFetch = false

    try {
      const response = await fetch('https://unsplash.it/200/?random')
      const blob = await response.blob()
      const url = URL.createObjectURL(blob)
      tip.set({ content: `<img width="200" height="200" src="${url}" />` })
      state.isFetching = false
    } catch (e) {
      tip.set({ content: `Fetch failed. ${e}` })
      state.isFetching = false
    }
  },
  onHidden(tip) {
    state.canFetch = true
    tip.set({ content: INITIAL_CONTENT })
  },
  popperOptions: {
    modifiers: {
      preventOverflow: { enabled: false },
      hide: { enabled: false }
    }
  }
})

I think this is very clean and more declarative than manually updating the tooltip. I'm working to perfect the API as much as possible from the lessons learned from v0 to v2.

With JSX for React/Hyperapp etc:

<Tippy
  onShow={actions.ajax.onShow}
  onHidden={actions.ajax.onHidden}
  arrow={true}
  content={
    <div>
      {state.ajax.error && `Fetch failed. ${state.ajax.error}`}
      {!state.ajax.imageSrc ? (
        "Loading..."
      ) : (
        <img
          style={{ display: "block" }}
          width="200"
          height="200"
          src={state.ajax.imageSrc}
        />
      )}
    </div>
  }
>
  <button class="btn">Hover for a new image</button>
</Tippy>

You can expect an alpha release sometime in the next week. 😌

Nsbx commented 6 years ago

I don't know if is better to create new issue or post here but when i installed tippy just now, i have chosen the all.min.js version. But in my project i already have popper.js (for bootstrap) so my idea is as follows, why not create a new version who include only tippy and css without popper ? I know i can include only tippy and css but i find include only one file is more pratical. thank to you and thank for your lib, she's awesome