From 8029d4f2782f78fd23540484d960f8ba25798600 Mon Sep 17 00:00:00 2001 From: Mason Freed Date: Tue, 14 Jan 2025 20:59:48 -0800 Subject: [PATCH] Add dialog closedby="" and requestClose() The new closedby="" attribute for can control what closes the dialog, including allowing new light dismiss behavior, or disabling close requests for modal dialogs. The new dialogEl.requestClose() method acts as if a close request was issued by the user, firing the cancel event, and then (if the event isn't canceled) firing the close event and closing the dialog. This method does not require user activation. The existing closeWatcher.requestClose() method was updated to not require user activation either, to match this. Closes #9373. Closes #10164. Closes #10592. See also: https://github.com/openui/open-ui/issues/834, https://github.com/openui/open-ui/discussions/950 (especially https://github.com/openui/open-ui/discussions/950#discussioncomment-7592744), https://github.com/openui/open-ui/discussions/960, https://github.com/openui/open-ui/discussions/961. --- source | 389 +++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 337 insertions(+), 52 deletions(-) diff --git a/source b/source index 243c0c1cf7f..5ac4faf7eb7 100644 --- a/source +++ b/source @@ -4060,6 +4060,7 @@ a.setAttribute('href', 'https://example.com/'); // change the content attribute
  • The scroll event
  • The scrollend event
  • set up browsing context features
  • +
  • The clientX and clientY extension attributes of the MouseEvent interface
  • The following features and terms are defined in CSS Syntax: @@ -10932,6 +10933,9 @@ partial interface Document { set of not restored reason details, initially empty.

    +

    Each Document has an open dialogs list, which is a list of + dialog elements, initially empty.

    +

    The DocumentOrShadowRoot interface

    DOM defines the Content attributes:

    Global attributes
    +
    closedby
    open
    Accessibility considerations:
    @@ -61633,9 +61638,11 @@ interface HTMLDialogElement : HTMLElement { [CEReactions] attribute boolean open; attribute DOMString returnValue; + [CEReactions] attribute DOMString closedBy; [CEReactions] undefined show(); [CEReactions] undefined showModal(); [CEReactions] undefined close(optional DOMString returnValue); + [CEReactions] undefined requestClose(optional DOMString returnValue); };
    Uses HTMLDialogElement.
    @@ -61728,6 +61735,40 @@ interface HTMLDialogElement : HTMLElement { is a boolean attribute. When specified, it indicates that the dialog element is active and that the user can interact with it.

    +

    The closedby + content attribute is an enumerated attribute with the following keywords and + states:

    + + + + + + + + +
    Keyword + State + Brief description +
    any + Any + Close requests or clicks outside close the dialog. +
    closerequest + Close Request + Close requests close the dialog. +
    none + None + No user actions automatically close the dialog. +
    + +

    The closedby attribute's invalid value default and missing value + default are both the Auto state.

    + +

    The Auto state behaves as + Close Request state when the + dialog was shown using its showModal() + method; otherwise the None state.

    +

    A dialog element without an open attribute @@ -61781,6 +61822,27 @@ interface HTMLDialogElement : HTMLElement {

    The argument, if provided, provides a return value.

    +
    dialog.requestClose([ result ])
    +
    +

    Acts as if a close request was sent targeting + dialog, by first firing a cancel event, and if + that event is not canceled with preventDefault(), + proceeding to close the dialog in the same way as the close() method (including firing a close event).

    + +

    This is a helper utility that can be used to consolidate cancelation and closing logic into + the cancel and close event + handlers, by having all non-close request closing + affordances call this method.

    + +

    Note that this method ignores the closedby + attribute: that is, even if closedby is set to + "none", the same behavior will apply.

    + +

    The argument, if provided, provides a return value.

    +
    +
    dialog.returnValue [ = result ]

    Returns the dialog's return value.

    @@ -61839,8 +61901,8 @@ interface HTMLDialogElement : HTMLElement { method steps are:

      -
    1. If this has an open attribute and the - is modal flag of this is false, then return.

    2. +
    3. If this has an open attribute and + is modal of this is false, then return.

    4. If this has an open attribute, then throw an "InvalidStateError" DOMException.

    5. @@ -61862,6 +61924,11 @@ interface HTMLDialogElement : HTMLElement {
    6. Add an open attribute to this, whose value is the empty string.

    7. +
    8. Add this to this's node document's open + dialogs list.

    9. + +
    10. Set the dialog close watcher with this.

    11. +
    12. Set this's previously focused element to the focused element.

    13. @@ -61888,8 +61955,8 @@ interface HTMLDialogElement : HTMLElement { data-x="dom-dialog-showModal">showModal() method steps are:

        -
      1. If this has an open attribute and the - is modal flag of this is true, then return.

      2. +
      3. If this has an open attribute and + is modal of this is true, then return.

      4. If this has an open attribute, then throw an "InvalidStateError" DOMException.

      5. @@ -61926,7 +61993,14 @@ interface HTMLDialogElement : HTMLElement {
      6. Add an open attribute to this, whose value is the empty string.

      7. -
      8. Set the is modal flag of this to true.

      9. +
      10. Set is modal of this to true.

      11. + +
      12. Assert: this's node document's open + dialogs list does not contain + this.

      13. + +
      14. Add this to this's node document's open + dialogs list.

      15. Let this's node document be contain this, then add an element to the top layer given this.

      16. -
      17. -

        Set this's close watcher to the - result of establishing a close watcher given - this's relevant global object, with:

        - -
          -
        • cancelAction given - canPreventClose being to return the result of firing an event named cancel at this, with the cancelable attribute initialized to - canPreventClose.

        • - -
        • closeAction being to close the - dialog given this and null.

        • -
        -
      18. +
      19. Set the dialog close watcher with this.

      20. Set this's previously focused element to the focused element.

      21. @@ -61984,6 +62042,55 @@ interface HTMLDialogElement : HTMLElement {
      22. Run the dialog focusing steps given this.

      +

      To set the dialog close watcher, given a dialog + element dialog:

      + +
        +
      1. +

        Set dialog's close watcher to the + result of establishing a close watcher given + dialog's relevant global object, with:

        + +
          +
        • cancelAction given + canPreventClose being to return the result of firing + an event named cancel at dialog, with the + cancelable attribute initialized to + canPreventClose.

        • + +
        • closeAction being to close the + dialog given dialog and dialog's request close return + value.

        • + +
        • getEnabledState being to return + true if dialog's enable close watcher for requestClose() is true or dialog's + computed closed-by state is not None; otherwise false.

        • +
        +
      2. +
      + +

      To retrieve a dialog's computed closed-by state, given a dialog + dialog: + +

        +
      1. +

        If the state of dialog's closedby + attribute is Auto:

        + +
          +
        1. If dialog's is modal is true, then return Close Request.

        2. + +
        3. Return None.

        4. +
        +
      2. + +
      3. Return the state of dialog's closedby attribute.

      4. +
      +

      The dialog focusing steps, given a dialog element subject, are as follows:

      @@ -62044,7 +62151,10 @@ interface HTMLDialogElement : HTMLElement { data-x="list contains">contains removedNode, then remove an element from the top layer immediately given removedNode.

      -
    14. Set the is modal flag of removedNode to false.

    15. +
    16. Set is modal of removedNode to false.

    17. + +
    18. Remove removedNode from + removedNode's node document's open dialogs list.

    The HTMLDialogElement : HTMLElement {

  • Close the dialog this with returnValue.

  • +

    The requestClose(returnValue) method steps + are:

    + +
      +
    1. If this does not have an open + attribute, then return.

    2. + +
    3. Assert: this's close + watcher is not null.

    4. + +
    5. Set dialog's enable close watcher for requestClose() to true.

    6. + +
    7. If returnValue is not given, then set it to null.

    8. + +
    9. Set this's request close return value to + returnValue.

    10. + +
    11. Request to close dialog's + close watcher with false.

    12. + +
    13. Set dialog's enable close watcher for requestClose() to false.

    14. +
    +

    When a dialog element subject is to be closed, with null or a string result, run these steps:

    @@ -62078,17 +62214,22 @@ interface HTMLDialogElement : HTMLElement {
  • Remove subject's open attribute.

  • -
  • If the is modal flag of subject is true, then request an +

  • If is modal of subject is true, then request an element to be removed from the top layer given subject.

  • Let wasModal be the value of subject's is modal flag.

  • -
  • Set the is modal flag of subject to false.

  • +
  • Set is modal of subject to false.

  • + +
  • Remove subject from subject's + node document's open dialogs list.

  • If result is not null, then set the returnValue attribute to result.

  • +
  • Set the request close return value to null.

  • +
  • If subject's previously focused element is not null, then:

    @@ -62170,11 +62311,19 @@ interface HTMLDialogElement : HTMLElement {
    +

    The Document has a dialog pointerdown target, which is an HTML dialog element or null, initially null.

    +

    Each dialog element has a close watcher, which is a close watcher or null, initially null.

    -

    Each dialog element has an is modal flag. When a dialog - element is created, this flag must be set to false.

    +

    Each dialog element has a request close return value, which is a + string, initially null.

    + +

    Each dialog element has an enable close watcher for requestClose() boolean, initially false.

    + +

    Each dialog element has an is modal boolean, initially false.

    Each HTML element has a previously focused element which is null or an element, and it is initially null. When HTMLDialogElement : HTMLElement {


    +

    The closedBy IDL attribute must reflect the + closedby content attribute, limited to only + known values.

    +

    The open IDL attribute must reflect the open content attribute.

    @@ -62206,8 +62360,110 @@ interface HTMLDialogElement : HTMLElement { </dialog>
  • +

    Dialog light dismiss

    + +

    "Light dismiss" means that clicking outside of a dialog element whose closedby attribute is in the Any state will close the dialog element. This + is in addition to how such dialogs respond to close requests.

    +

    To light dismiss open dialogs, given a PointerEvent event:

    +
      +
    1. Assert: event's isTrusted attribute is true.

    2. + +
    3. Let document be event's target's node document.

    4. + +
    5. If document's open dialogs list is empty, then return.

    6. + +
    7. Let ancestor be the result of running nearest clicked dialog + given event.

    8. + +
    9. If event's type is + "pointerdown", then set document's + dialog pointerdown target to ancestor.

    10. + +
    11. +

      If event's type is + "pointerup", then:

      + +
        +
      1. Let sameTarget be true if ancestor is document's + dialog pointerdown target.

      2. + +
      3. Set document's dialog pointerdown target to null.

      4. + +
      5. If sameTarget is false, then return.

      6. + +
      7. Let topmostDialog be the last element of document's open + dialogs list.

      8. + +
      9. If ancestor is topmostDialog, then return.

      10. + +
      11. If topmostDialog's computed closed-by state is not Any, then return.

      12. + +
      13. Assert: topmostDialog's close watcher is not null.

      14. + +
      15. Request to close + topmostDialog's close watcher with + false.

      16. +
      +
    12. +
    + +

    To run light dismiss activities, given a PointerEvent + event:

    + +
      +
    1. Run light dismiss open popovers with event.

    2. + +
    3. Run light dismiss open dialogs with event.

    4. +
    + +

    Run light dismiss activities will be called by the Pointer Events spec when the user clicks + or touches anywhere on the page.

    + +

    To find the nearest clicked dialog, given a PointerEvent + event:

    + +
      +
    1. Let target be event's target.

    2. + +
    3. If target is a dialog element, target has an open attribute, target's is modal is + true, and event's clientX and + clientY are outside the bounds of target, + then return null. + +

      The check for clientX and clientY is because a pointer event that hits the ::backdrop pseudo element of a dialog will result in event having a + target of the dialog element itself.

      + +
    4. Let currentNode be target.

    5. + +
    6. +

      While currentNode is not null:

      + +
        +
      1. If currentNode is a dialog element and currentNode + has an open attribute, then return + currentNode.

      2. + +
      3. Set currentNode to currentNode's parent in the flat + tree.

      4. +
      +
    7. + +
    8. Return null.

    9. +

    Scripting

    @@ -75018,7 +75274,7 @@ Demos: element falling into one of the following categories:

      -
    • dialog elements whose is modal flag is true
    • +
    • dialog elements whose is modal is true
    • elements whose fullscreen flag is true
    @@ -82918,6 +83174,10 @@ body { display:none }
  • An is running cancel action boolean.

  • + +
  • A get enabled state, an algorithm + accepting no arguments and returning a boolean. This algorithm can never throw an + exception.

  • A close watcher closeWatcher is @@ -82928,8 +83188,10 @@ body { display:none }


    To establish a close watcher given a Window window, a list - of steps cancelAction, and a - list of steps closeAction:

    + of steps cancelAction, a + list of steps closeAction, + and an algorithm that returns a boolean getEnabledState:

    1. Assert: window's is running cancel action

      false
      + +
      get enabled state
      +
      getEnabledState
    2. @@ -82984,12 +83249,16 @@ body { display:none }

    To request to close a close - watcher closeWatcher:

    + watcher closeWatcher with boolean requireHistoryActionActivation:

    1. If closeWatcher is not active, then return true.

    2. +
    3. If the result of running closeWatcher's get enabled state is false, then return + true.

    4. +
    5. If closeWatcher's is running cancel action is true, then return true.

    6. @@ -82999,9 +83268,13 @@ body { display:none }
    7. If window's associated Document is not fully active, then return true.

    8. -
    9. Let canPreventClose be true if window's close watcher - manager's groups's size is less than window's close watcher manager's + +

    10. Let canPreventClose be true if requireHistoryActionActivation is + false, or if window's close watcher manager's groups's size is + less than window's close watcher manager's allowed number of groups, and window has history-action activation; otherwise false.

    11. @@ -83044,6 +83317,9 @@ body { display:none }
    12. If closeWatcher is not active, then return.

    13. +
    14. If the result of running closeWatcher's get enabled state is false, then return.

    15. +
    16. If closeWatcher's window's associated Document is not fully active, then return.

    17. @@ -83091,11 +83367,13 @@ body { display:none } in reverse order:

        -
      1. Set processedACloseWatcher to true.

      2. +
      3. If the result of running closeWatcher's get enabled state is true, set + processedACloseWatcher to true.

      4. Let shouldProceed be the result of requesting to close - closeWatcher.

      5. + closeWatcher with true.

      6. If shouldProceed is false, then break.

      @@ -83246,7 +83524,7 @@ dictionary CloseWatcherOptions {

      The requestClose() method steps are to request to close this's internal - close watcher.

      + close watcher with false.

      The close() method steps are to close this's @@ -84605,8 +84883,9 @@ dictionary DragEventInit : MouseEventInit {

      If there is no relevant pointing device, then initialize event's screenX, screenY, clientX, clientY, and button attributes to 0.

      + data-x="">screenX
      , screenY, clientX, clientY, + and button attributes to 0.

    18. Dispatch event at the specified @@ -85847,6 +86126,9 @@ dictionary DragEventInit : MouseEventInit {

    19. closeAction being to hide a popover given element, true, true, and false.

    20. + +
    21. getEnabledState being to return + true.

    @@ -86471,7 +86753,7 @@ dictionary DragEventInit : MouseEventInit {
  • expectedDocument is not null and element's node document is not expectedDocument;

  • -
  • element is a dialog element and its is modal flag +

  • element is a dialog element and its is modal is set to true; or

  • element's fullscreen flag is set,

  • @@ -86731,7 +87013,8 @@ dictionary DragEventInit : MouseEventInit { data-x="attr-popover-auto-state">auto state will close the popover. This is in addition to how such popovers respond to close requests.

    -

    To light dismiss open popovers, given an Event event:

    +

    To light dismiss open popovers, given a PointerEvent + event:

    1. Assert: event's DragEventInit : MouseEventInit {

    2. If topmostPopover is null, then return.

    3. -
    4. If event is a PointerEvent and event's type is "pointerdown", - then: set document's popover pointerdown target to the result of running - topmost clicked popover given target.

    5. +
    6. If event's type is "pointerdown", then: set document's popover + pointerdown target to the result of running topmost clicked popover given + target.

    7. -

      If event is a PointerEvent and event's type is "pointerup", - then:

      +

      If event's type is "pointerup", then:

      1. Let ancestor be the result of running topmost clicked popover @@ -86775,10 +87057,6 @@ dictionary DragEventInit : MouseEventInit {

      -

      Light dismiss open popovers will be called by the Pointer Events spec when the user clicks - or touches anywhere on the page.

      -

      To find the topmost clicked popover, given a Node node:

        @@ -142633,6 +142911,13 @@ interface External { HTML elements Classes to which the element belongs Set of space-separated tokens + + closedby + dialog + Which user actions will close the dialog + "any"; + "closerequest"; + "none"; color link