Skip to content
academia.sh

Lesson 04 / 24

Default Behavior and Cancellation

The browser's built-in response to events and the request to cancel it; the cancelability condition, why cancellation is not the same as stopping propagation, the passive listener's constraint, and when cancellation is legitimate.

Contents

In previous lessons, listeners only added: an event arrived, code ran, a behavior was attached to the page. But the browser has its own behaviors too, and they work with no listener at all. Clicking a link starts navigation, a submit button submits the form, a checkbox toggles its checked state, the space bar scrolls the page.

When a listener wants to put its own behavior in place of these, it has to cancel the work the browser was going to do. This lesson establishes how cancellation is requested, under what conditions it is accepted, and why it is not the same thing as stopping propagation.

The Default Action

An event’s default action is the browser’s built-in response to that event. It is written neither in the document nor in the style; it comes from the meaning of the markup. Because a link element means “leads to another resource,” navigation starts when it is clicked. Because a submit button means “submits the form,” the entry list is prepared when it is clicked.

Two properties of the default action are critical.

The first is its order. The default action runs after the event’s propagation finishes. Any listener along the propagation path — as much the target’s as the ancestors’ — can cancel the action. An ancestor node’s listener in the bubble phase can stop a navigation that started at the target.

The second is chainability. Some default actions produce a new event. Clicking a label produces a click event on the form field it is bound to; that click has its own default action too. A single user gesture can therefore produce more than one event and more than one default action; which link of the chain the cancellation happens at determines the result.

The Cancellation Request

A listener requests cancellation through the event object. The request is not always accepted: the event has to be cancelable. Some events, like a scroll notification, are produced after the browser has already done the work, and cannot be cancelled; the request is silently ignored.

// default.mjs — propagation, cancellation, and the order of the default action
const element = (name, ...children) => {
  const d = { name, children, parent: null, listeners: [] };
  for (const c of children) c.parent = d;
  return d;
};
const listen = (node, label, action, passive = false) =>
  node.listeners.push({ label, action, passive });

function dispatch(target, { type, cancelable, defaultAction }) {
  const chain = [];
  for (let d = target; d; d = d.parent) chain.push(d);

  const event = { type, cancelable, defaultPrevented: false, stopped: false };
  const log = [];
  event.preventDefault = function () {
    if (this.inPassiveListener) return log.push("  [ignored] passive listener");
    if (event.cancelable === false) return log.push("  [ignored] non-cancelable event");
    event.defaultPrevented = true;
  };

  for (const node of chain) {          // bubble phase: target to root
    if (event.stopped) break;
    for (const d of node.listeners) {
      event.inPassiveListener = d.passive;
      log.push(`  ${node.name}/${d.label} (prevented=${event.defaultPrevented})`);
      d.action?.(event);
    }
  }
  log.push(event.defaultPrevented ? `  default action: cancelled` : `  default action: ${defaultAction}`);
  return log;
}

function tree(setUpListeners) {
  const link = element("a");
  const nav = element("nav", link);
  element("main", nav);
  setUpListeners(link, nav);
  return link;
}
const CLICK = { type: "click", cancelable: true, defaultAction: "navigation" };

const scenarios = [
  ["unimpeded", CLICK, (link, nav) => { listen(link, "link"); listen(nav, "nav"); }],
  ["preventDefault", CLICK, (link, nav) => {
    listen(link, "link", (o) => o.preventDefault()); listen(nav, "nav");
  }],
  ["stopPropagation", CLICK, (link, nav) => {
    listen(link, "link", (o) => { o.stopped = true; }); listen(nav, "nav");
  }],
  ["non-cancelable event", { type: "scroll", cancelable: false, defaultAction: "scrolling" },
    (link) => { listen(link, "link", (o) => o.preventDefault()); }],
  ["passive listener", { type: "wheel", cancelable: true, defaultAction: "scrolling" },
    (link) => { listen(link, "link", (o) => o.preventDefault(), true); }],
];

for (const [name, eventDef, setUp] of scenarios) {
  console.log(name);
  for (const line of dispatch(tree(setUp), eventDef)) console.log(line);
}
unimpeded
  a/link (prevented=false)
  nav/nav (prevented=false)
  default action: navigation
preventDefault
  a/link (prevented=false)
  nav/nav (prevented=true)
  default action: cancelled
stopPropagation
  a/link (prevented=false)
  default action: navigation
non-cancelable event
  a/link (prevented=false)
  [ignored] non-cancelable event
  default action: scrolling
passive listener
  a/link (prevented=false)
  [ignored] passive listener
  default action: scrolling

In the second scenario, the listener at the target requests cancellation; the event continues on its path, and the listener at the ancestor node reads prevented=true. In the browser API, this flag’s name is defaultPrevented. Propagation continuing is not a defect, it is the design itself: later listeners on the path can see the decision and adjust their own behavior to it.

Separating Cancellation from Stopping

The third scenario shows this lesson’s most commonly confused point. Propagation was stopped — the ancestor listener was never called — but navigation still happened.

The two operations are independent of each other:

  • Stopping propagation prevents the event from reaching other listeners. It does not touch the browser’s own behavior.
  • Requesting cancellation prevents the browser’s behavior. It does not prevent the event from continuing on its path.

Two mistakes follow from this. First, code that wants to stop navigation but stops propagation instead does not stop the navigation. Second, code that only wants to do its own work but stops propagation along with cancellation silently disables delegated listeners elsewhere on the page. The two operations should be requested separately, and in most cases only one of them is needed.

The Passive Listener

The last scenario shows a different constraint. In scroll-related events, the browser faces a dilemma: it cannot start scrolling without knowing whether the listener will cancel it or not. For this reason, it waits for the listener to finish; a long-running listener holds up the page under the user’s finger.

A passive listener removes this uncertainty. With an option given at registration time, the listener announces “I will not request cancellation”; the browser starts scrolling without waiting for the listener. If cancellation is requested contrary to the announcement, the request is ignored.

A practical rule follows from this: listeners that observe scrolling but do not change it are registered as passive. A listener that puts its own behavior in place of scrolling cannot be passive and accepts the cost of being waited for.

The Fine Points of Cancellation

In some events, cancellation means permission. In a drag-and-drop operation, the default action of the event produced while passing over an element is “refuse the drop”; the drop does not happen unless it is cancelled. Here, requesting cancellation does not block the behavior, it opens it instead. This is why the assumption “cancellation always blocks” is wrong; what an event’s default action is needs to be known separately for each one.

Cancellation may not undo state. When a checkbox is clicked, the checked-state change happens before the event propagates; cancellation reverts the change. The current value read inside the listener may therefore no longer be valid after cancellation.

Something needs to be put in place of a cancelled action. A page that cancels a form’s submission and sends the data itself in its own code behaves correctly. A page that cancels submission and puts nothing in its place ends the user’s task with no visible reason. The same principle applies to links: code that cancels navigation also takes away the user’s ability to open it in a new tab with the middle button, copy the address, or see where the link goes.

Keyboard and accessibility behaviors are not cancelled. A page that cancels the tab key’s focus-moving behavior becomes unusable by keyboard. Likewise, cancelling text selection, the right-click menu, and zooming takes away the user’s control. These behaviors are cancelled only when an equivalent path is put in their place; the limits of this are set in the Focus and Keyboard Interaction lesson.

Cancellation’s Irreversibility

A flag being readable does not mean it is changeable. There is no operation that undoes a cancellation. The first cancellation request on the path settles the decision; later listeners only see it.

The design consequence of this is: the decision to cancel should be made by the listener that owns the behavior. Code that will cancel the measurement form’s submission is the form’s own listener; a general listener looking at the form from the outside makes the decision without seeing the form’s own logic.

The same certainty holds in the reverse direction too. If a listener requests unconditional cancellation at an early point in propagation, it is not possible for later listeners to bring the behavior back; the only remedy is moving the condition inside that listener. As long as the condition stays outside, cancellation turns into a switch that silently and permanently shuts the behavior off.

Leaving the page is the most visible example of this rule. A page with unsaved changes can cancel the event produced before leaving and have the browser ask for confirmation. The page does not determine the confirmation’s text or form; the browser does, and it usually requires the user to have interacted with the page beforehand. The rule is the same: a cancellation request is a suggestion, and it finds a response only within the browser’s acceptance conditions.

Summary

  • The default action is the browser’s built-in response to an event, coming from the meaning of the markup; it runs after propagation finishes.
  • Every listener on the propagation path can request cancellation; the request is accepted only when the event is cancelable, otherwise it is silently ignored.
  • Cancellation and stopping propagation are independent operations: one blocks the browser’s behavior, the other blocks the event from reaching other listeners.
  • The event continues on its path even after being cancelled; later listeners can read the decision through a flag but cannot undo it.
  • A passive listener announces ahead of time that it will not request cancellation; the browser starts scrolling without waiting, and a contrary request is ignored.
  • In some events, cancellation means permission, not blocking; every cancelled behavior needs an equivalent path put in its place.

Next Step

This lesson set the keyboard apart among behaviors that should not be cancelled and left the reason for later. The reason is this: not all interaction with the page is built with a pointing device. The user moves between elements with the tab key, presses a button with space, moves between options with the arrow keys; a screen reader follows the same path. At the center of this movement is a single concept: at any moment, at most one element in the document receives input. The next lesson covers how this element is determined, what the order is built from, and what information keyboard events carry.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close