Skip to content

Should popover/dialog show/hide when already shown/hidden throw? #9045

@jakearchibald

Description

@jakearchibald

Current state

  • close() on a closed <dialog>.
  • show() on an open <dialog>, unless that dialog is in the popover showing state.

…do not throw.

Whereas:

  • showModal() on an open dialog
  • show() on an open dialog that is also in the popover showing state.
  • showPopover() on an open popover
  • hidePopover() on a closed popover

…throw an error.

So the current state of things is that dialog is inconsistent with itself, and popover is inconsistent with dialog.

Elsewhere on the platform

Looking at other cases of hide/remove/delete in the web platform, none of these throw:

  • new Set().delete('foo')
  • ('hello').replace('world', '')
  • [].pop()
  • document.createElement('div').removeAttribute('foo')
  • document.createElement('div').remove()
  • document.createElement('div').classList.remove('foo')
  • document.createElement('div').removeEventListener(() => {})
  • document.createElement('div').classList.replace('foo', 'bar') (although it does return false)

Counter examples:

  • el.removeChild(otherEl) throws if otherEl is not a child of el.
  • el.removeAttributeNode(attributeNode) throws if attributeNode is not an attribute node of el.
  • document.exitFullscreen() throws if no element is fullscreen.

removeChild and removeAttributeNode are pretty old APIs that developers tend to avoid in favour of friendlier equivalents.

exitFullscreen is the most interesting example, since it's related to top level.

Looking at 'add' cases:

  • new Set(['foo', 'bar']).add('foo')
  • el.classList.add('foo')
  • el.addEventListener(callback)

All behave set-like. As in, if the item is already in the set, it doesn't throw, and it doesn't change the order of items. It's a no-op.

el.requestFullscreen(options) on an already-fullscreen element will adapt to changes in options, but it will not fire a fullscreenchange event.

el.append(otherEl) will remove otherEl from its parent, and add otherEl as the last child of el. This happens even if otherEl is already the last child of el, and it's observable in a bunch of ways, including mutation observers. But this is specifically 'append', not 'add'.

The current mindset in the frameworks world is to let developer declare the state they want, and the framework figures out what needs to change to get to that state. None of the frameworks throw if the developer re-declares the current state.

Proposal for <dialog>

It's pretty weird that show() followed by showModal() will throw, whereas showModal() followed by show() will no-op.

Option 1: 'show' throws if already shown

show() or showModal() followed by show() or showModal() will throw.

This is at least consistent between the two methods, but it doesn't seem to fit with the majority of the platform. You could say the behaviour here is justified in being unusual due to the different ways a dialog can be shown, but that wouldn't be consistent with requestFullscreen(options) either.

Option 2: 'show' is a no-op if already shown

With show() or showModal() followed by show() or showModal(), the second call will be a no-op.

This is at least consistent between the two methods, but it seems weird that showModal() would no-op resulting in a not-modal dialog.

Option 3: Update the type of 'show'

show() followed by showModal() will 'upgrade' the dialog to a modal dialog.

showModal() followed by show() will 'downgrade' the dialog to a non-modal dialog.

This feels consistent with how requestFullscreen(options) will react to changes in options even if the element is already fullscreen.

Option 4: showModal() is a more specific version of show()

show() followed by showModal() will 'upgrade' the dialog to a modal dialog.

showModal() followed by show() is a no-op, because the dialog is already shown.

This seems less flexible, but it feels like it makes sense. We could add showModeless() in future to do the more specific thing, or add an option like show({ mode: 'modeless' }).

Proposal for popover

I think we should match the majority of the platform, and open-when-already-open, and close-when-already-closed, should not throw.

Option 1: Second showPopover() moves popover to the top

This could be achieved by hiding then re-showing the popover, which would be like el.append(). If the developer is calling showPopover(), it seems like they feel this popover is important at this time, which suggests move-to-top is the right thing to do.

If this makes sense, showModal on <dialog> should do the same.

Option 2: Second showPopover() is a no-op

That's consistent with most of the platform.

Proposal for popover on <dialog>

Option 1: <dialog> cannot popover

Similar to requestFullscreen, calling showPopover() on a <dialog> will always throw, even if the dialog isn't open. That removes the overlap between these two features, and it's consistent with fullscreen.

Option 2: Avoid both features being active at the same time

  • If show() is not a no-op, then it should throw if the element is in the popover showing state.
  • If showModal() is not a no-op, then it should throw if the element is in the popover showing state.
  • If showPopover() is not a no-op, then it should throw if the element is a dialog, and open.

I'm not sure this complexity is worth it, and it's inconsistent with requestFullscreen.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions