@jsenv/dom 0.14.3 → 0.14.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/jsenv_dom.js +100 -30
  2. package/package.json +1 -1
package/dist/jsenv_dom.js CHANGED
@@ -208,7 +208,7 @@ const dispatchInternalCustomEvent = (
208
208
  customEventDetail,
209
209
  ) => {
210
210
  const customEvent = new CustomEvent(customEventName, {
211
- detail: customEventDetail,
211
+ detail: customEventDetail || {},
212
212
  cancelable: true,
213
213
  });
214
214
  chainEvent(customEvent, customEventDetail?.event);
@@ -225,7 +225,7 @@ const dispatchPublicCustomEvent = (
225
225
  customEventDetail,
226
226
  ) => {
227
227
  const customEvent = new CustomEvent(customEventName, {
228
- detail: customEventDetail,
228
+ detail: customEventDetail || {},
229
229
  bubbles: true,
230
230
  cancelable: true,
231
231
  });
@@ -241,7 +241,7 @@ const dispatchPublicCustomEvent = (
241
241
  */
242
242
  const dispatchCustomEvent = (el, customEventName, customEventDetail) => {
243
243
  const customEvent = new CustomEvent(customEventName, {
244
- detail: customEventDetail,
244
+ detail: customEventDetail || {},
245
245
  cancelable: true,
246
246
  });
247
247
  chainEvent(customEvent, customEventDetail?.event);
@@ -253,6 +253,12 @@ const chainEvent = (customEvent, parentEvent) => {
253
253
  if (!parentEvent) {
254
254
  return customEvent;
255
255
  }
256
+ if (!customEvent.detail) {
257
+ console.warn(
258
+ `Event "${customEvent.type}" has no detail object. Cannot chain to parent event "${parentEvent.type}".`,
259
+ );
260
+ return customEvent;
261
+ }
256
262
  // Always build eventChain from the first wrapping so callers can rely on it
257
263
  // being present whenever `parentEvent` is set.
258
264
  // eventChain = [oldest, ..., parentEvent] — the full ancestor list including the direct parent.
@@ -316,15 +322,15 @@ const formatEventSideEffect = (e, sideEffect) => {
316
322
  const chain = e.detail.eventChain;
317
323
  const initiator = chain[0];
318
324
  parts.push(
319
- `"${initiator.type}" on ${getElementSignature(initiator.target)}`,
325
+ `"${getEventLabel(initiator)}" on ${getElementSignature(initiator.target)}`,
320
326
  );
321
327
  // chain[0] is shown as initiator above; chain includes event as last element
322
328
  for (const chainedEvent of chain.slice(1)) {
323
- parts.push(chainedEvent.type);
329
+ parts.push(getEventLabel(chainedEvent));
324
330
  }
325
- parts.push(e.type);
331
+ parts.push(getEventLabel(e));
326
332
  } else {
327
- parts.push(`"${e.type}" on ${getElementSignature(e.target)}`);
333
+ parts.push(`"${getEventLabel(e)}" on ${getElementSignature(e.target)}`);
328
334
  }
329
335
  return `${parts.join(" -> ")} -> ${sideEffect}`;
330
336
  };
@@ -374,8 +380,8 @@ const createEventGroupLogger = () => {
374
380
  console.groupEnd();
375
381
  }
376
382
  const label = initiator.target
377
- ? `"${initiator.type}" on ${getElementSignature(initiator.target)}`
378
- : `"${initiator.type}"`;
383
+ ? `"${getEventLabel(initiator)}" on ${getElementSignature(initiator.target)}`
384
+ : `"${getEventLabel(initiator)}"`;
379
385
  console.group(label);
380
386
  currentInitiator = initiator;
381
387
  }
@@ -400,12 +406,40 @@ const formatSideEffectLine = (e, prefix) => {
400
406
  // chain[0] is the root event, already shown as the group label — skip it.
401
407
  // chain includes the direct parent (e.detail.event) as its last element.
402
408
  for (const chainedEvent of chain.slice(1)) {
403
- parts.push(chainedEvent.type);
409
+ parts.push(getEventLabel(chainedEvent));
404
410
  }
405
411
  }
406
412
  return parts.join(" -> ");
407
413
  };
408
414
 
415
+ const getEventLabel = (e) => {
416
+ if (e.type === "mousedown" || e.type === "click") {
417
+ if (e.button !== 0) {
418
+ return `${e.type}:right_button`;
419
+ }
420
+ return e.type;
421
+ }
422
+ if (e.type === "keydown") {
423
+ const key = e.key === " " ? "space" : e.key?.toLowerCase();
424
+ const modifiers = [];
425
+ if (e.ctrlKey) {
426
+ modifiers.push("ctrl");
427
+ }
428
+ if (e.metaKey) {
429
+ modifiers.push("meta");
430
+ }
431
+ if (e.altKey) {
432
+ modifiers.push("alt");
433
+ }
434
+ if (e.shiftKey) {
435
+ modifiers.push("shift");
436
+ }
437
+ modifiers.push(key);
438
+ return `keydown:${modifiers.join("+")}`;
439
+ }
440
+ return e.type;
441
+ };
442
+
409
443
  const createIterableWeakSet = () => {
410
444
  const objectWeakRefSet = new Set();
411
445
 
@@ -473,6 +507,15 @@ const createIterableWeakSet = () => {
473
507
  };
474
508
  };
475
509
 
510
+ /**
511
+ * Creates a simple publish/subscribe pair.
512
+ *
513
+ * @param {boolean} [clearOnPublish=false] - When true, all subscribers are removed after each publish call.
514
+ * @returns {[publish: (...args: any[]) => any[], subscribe: (callback: Function) => () => void, clear: () => void]}
515
+ * - `publish(...args)` — calls all subscribers with the given arguments and returns their return values.
516
+ * - `subscribe(callback)` — registers a subscriber and returns an unsubscribe function.
517
+ * - `clear()` — removes all subscribers without calling them.
518
+ */
476
519
  const createPubSub = (clearOnPublish = false) => {
477
520
  const callbackSet = new Set();
478
521
 
@@ -4328,10 +4371,7 @@ const elementIsFocusable = (node, { excludeAriaHidden } = {}) => {
4328
4371
  }
4329
4372
  return canFocus(node);
4330
4373
  }
4331
- if (
4332
- ["button", "select", "datalist", "iframe", "textarea"].indexOf(nodeName) >
4333
- -1
4334
- ) {
4374
+ if (FOCUSABLE_NODE_NAME_SET.has(nodeName)) {
4335
4375
  return canFocus(node);
4336
4376
  }
4337
4377
  if (["a", "area"].indexOf(nodeName) > -1) {
@@ -4357,6 +4397,14 @@ const elementIsFocusable = (node, { excludeAriaHidden } = {}) => {
4357
4397
  }
4358
4398
  return false;
4359
4399
  };
4400
+ const FOCUSABLE_NODE_NAME_SET = new Set([
4401
+ "button",
4402
+ "select",
4403
+ "datalist",
4404
+ "dialog",
4405
+ "iframe",
4406
+ "textarea",
4407
+ ]);
4360
4408
 
4361
4409
  const canInteract = (element) => {
4362
4410
  if (element.disabled) {
@@ -4554,7 +4602,13 @@ const DEFAULT_BEHAVIORS = [
4554
4602
  {
4555
4603
  test: (el) => el.matches("input[type='radio'], input[type='checkbox']"),
4556
4604
  keys: {
4557
- space: "activate",
4605
+ space: (e) => {
4606
+ if (e.target.type === "radio" && e.target.checked) {
4607
+ // space on checked radio does nothing
4608
+ return "";
4609
+ }
4610
+ return "activate";
4611
+ },
4558
4612
  enter: (e) => (e.target.form ? "form_submit" : ""),
4559
4613
  arrowleft: "focus_nav",
4560
4614
  arrowright: "focus_nav",
@@ -4570,17 +4624,20 @@ const DEFAULT_BEHAVIORS = [
4570
4624
  keys: {
4571
4625
  escape: (e) => {
4572
4626
  if (e.target.type === "search") {
4627
+ if (e.target.readOnly) {
4628
+ return "";
4629
+ }
4573
4630
  return e.target.value ? "clear" : "";
4574
4631
  }
4575
4632
  return "";
4576
4633
  },
4577
4634
  enter: (e) => (e.target.form ? "form_submit" : ""),
4578
- arrowleft: "cursor_move",
4579
- arrowright: "cursor_move",
4580
- arrowup: "cursor_move",
4581
- arrowdown: "cursor_move",
4582
- home: "cursor_move",
4583
- end: "cursor_move",
4635
+ arrowleft: (e) => (e.target.readOnly ? "scroll" : "cursor_move"),
4636
+ arrowright: (e) => (e.target.readOnly ? "scroll" : "cursor_move"),
4637
+ arrowup: (e) => (e.target.readOnly ? "scroll" : "cursor_move"),
4638
+ arrowdown: (e) => (e.target.readOnly ? "scroll" : "cursor_move"),
4639
+ home: (e) => (e.target.readOnly ? "scroll" : "cursor_move"),
4640
+ end: (e) => (e.target.readOnly ? "scroll" : "cursor_move"),
4584
4641
  },
4585
4642
  fallback: (e) => (isTypingIntent(e) ? "type" : undefined),
4586
4643
  },
@@ -4619,7 +4676,7 @@ const DEFAULT_BEHAVIORS = [
4619
4676
  ),
4620
4677
  keys: {
4621
4678
  space: "activate",
4622
- enter: (e) => (e.target.form ? "form_submit" : ""),
4679
+ enter: "activate",
4623
4680
  arrowleft: "value_change",
4624
4681
  arrowright: "value_change",
4625
4682
  arrowup: "value_change",
@@ -4627,19 +4684,19 @@ const DEFAULT_BEHAVIORS = [
4627
4684
  },
4628
4685
  },
4629
4686
  {
4630
- // Color input: Space opens the color picker, Enter submits the form
4687
+ // Color input: Space opens the color picker, Enter too
4631
4688
  test: (el) => el.matches("input[type='color']"),
4632
4689
  keys: {
4633
4690
  space: "activate",
4634
- enter: (e) => (e.target.form ? "form_submit" : ""),
4691
+ enter: "activate",
4635
4692
  },
4636
4693
  },
4637
4694
  {
4638
- // File input: Space opens the picker, Enter submits the form
4695
+ // File input: Space opens the picker, Enter too
4639
4696
  test: (el) => el.matches("input[type='file']"),
4640
4697
  keys: {
4641
4698
  space: "activate",
4642
- enter: (e) => (e.target.form ? "form_submit" : ""),
4699
+ enter: "activate",
4643
4700
  },
4644
4701
  },
4645
4702
  {
@@ -4686,7 +4743,10 @@ const DEFAULT_BEHAVIORS = [
4686
4743
  {
4687
4744
  // SELECT: don't intercept anything while the dropdown may be open
4688
4745
  test: (el) => el.tagName === "SELECT",
4689
- keys: {},
4746
+ keys: {
4747
+ space: "activate",
4748
+ enter: "activate",
4749
+ },
4690
4750
  },
4691
4751
  {
4692
4752
  // Non-interactive elements: browser scrolls on Space and arrow keys
@@ -11517,9 +11577,19 @@ const visibleRectEffect = (
11517
11577
  * @param {HTMLElement} anchor - The anchor element to position against
11518
11578
  * @param {object} [options]
11519
11579
  * @param {string} [options.positionX="center"] - Preferred X placement, with viewport fallback.
11580
+ * "to-the-left" — element.right = anchor.left (sits entirely to the left of anchor)
11581
+ * "left-aligned" — element.left = anchor.left (left edges aligned)
11582
+ * "center" — element centered horizontally over anchor (default)
11583
+ * "right-aligned" — element.right = anchor.right (right edges aligned)
11584
+ * "to-the-right" — element.left = anchor.right (sits entirely to the right of anchor)
11520
11585
  * @param {string} [options.positionY="below"] - Preferred Y placement, with viewport fallback.
11521
- * @param {string} [options.positionXFixed] - Force X placement, skipping the fit-check.
11522
- * @param {string} [options.positionYFixed] - Force Y placement, skipping the fit-check.
11586
+ * "above" — element.bottom = anchor.top (sits above, no overlap)
11587
+ * "above-overlap" element.bottom = anchor.bottom (sits above, overlapping anchor)
11588
+ * "center" — element centered vertically over anchor
11589
+ * "below-overlap" — element.top = anchor.top (sits below, overlapping anchor)
11590
+ * "below" — element.top = anchor.bottom (sits below, no overlap) (default)
11591
+ * @param {string} [options.positionXFixed] - Force X placement, skipping the fit-check. Same values as positionX.
11592
+ * @param {string} [options.positionYFixed] - Force Y placement, skipping the fit-check. Same values as positionY.
11523
11593
  * @param {number} [options.alignToViewportEdgeWhenAnchorNearEdge=0] - Snap to viewport left
11524
11594
  * edge when anchor is within this many px of the left edge and element is wider than anchor.
11525
11595
  * @param {number} [options.minLeft=0] - Minimum left coordinate (document-relative).
@@ -15107,4 +15177,4 @@ const useResizeStatus = (elementRef, { as = "number" } = {}) => {
15107
15177
  };
15108
15178
  };
15109
15179
 
15110
- export { EASING, activeElementSignal, addActiveElementEffect, addAttributeEffect, allowWheelThrough, appendStyles, captureScrollState, chainEvent, contrastColor, createBackgroundColorTransition, createBackgroundTransition, createBorderRadiusTransition, createBorderTransition, createDragGestureController, createDragToMoveGestureController, createEventGroupLogger, createGroupTransitionController, createHeightTransition, createIterableWeakSet, createOpacityTransition, createPubSub, createStyleController, createTimelineTransition, createTransition, createTranslateXTransition, createValueEffect, createWidthTransition, cubicBezier, dispatchCustomEvent, dispatchInternalCustomEvent, dispatchPublicCustomEvent, dragAfterThreshold, elementIsFocusable, elementIsVisibleForFocus, elementIsVisuallyVisible, findAfter, findAncestor, findBefore, findDescendant, findEvent, findFocusDelegateTarget, findFocusable, formatEventSideEffect, getAvailableHeight, getAvailableWidth, getBackground, getBackgroundColor, getBorder, getBorderRadius, getBorderSizes, getContrastRatio, getDefaultStyles, getDragCoordinates, getDropTargetInfo, getElementSignature, getFirstVisuallyVisibleAncestor, getFocusVisibilityInfo, getHeight, getHeightWithoutTransition, getInnerHeight, getInnerWidth, getKeyboardEventDefaultAction, getLuminance, getMarginSizes, getMaxHeight, getMaxWidth, getMinHeight, getMinWidth, getOpacity, getOpacityWithoutTransition, getPaddingSizes, getPositionedParent, getPreferedColorScheme, getScrollBox, getScrollContainer, getScrollContainerSet, getScrollRelativeRect, getSelfAndAncestorScrolls, getStyle, getTranslateX, getTranslateXWithoutTransition, getTranslateY, getVisuallyVisibleInfo, getWidth, getWidthWithoutTransition, hasCSSSizeUnit, initFlexDetailsSet, initFocusGroup, initPositionSticky, isSameColor, isScrollable, measureLongestVisualLineWidth, measureScrollbar, measureWidestChildRow, mergeOneStyle, mergeTwoStyles, normalizeKeyboardKey, normalizeStyle, normalizeStyles, parseStyle, pickPositionRelativeTo, prefersDarkColors, prefersLightColors, preventFocusNav, preventFocusNavViaKeyboard, preventIntermediateScrollbar, resolveCSSColor, resolveCSSSize, resolveColorLuminance, resolveOklchLightness, scrollIntoViewScoped, scrollIntoViewWithStickyAwareness, setAttribute, setAttributes, setStyles, snapToPixel, startDragToReorder, startDragToResizeGesture, stickyAsRelativeCoords, stringifyStyle, trapFocusInside, trapScrollInside, useActiveElement, useAvailableHeight, useAvailableWidth, useMaxHeight, useMaxWidth, useResizeStatus, visibleRectEffect };
15180
+ export { EASING, activeElementSignal, addActiveElementEffect, addAttributeEffect, allowWheelThrough, appendStyles, captureScrollState, chainEvent, contrastColor, createBackgroundColorTransition, createBackgroundTransition, createBorderRadiusTransition, createBorderTransition, createDragGestureController, createDragToMoveGestureController, createEventGroupLogger, createGroupTransitionController, createHeightTransition, createIterableWeakSet, createOpacityTransition, createPubSub, createStyleController, createTimelineTransition, createTransition, createTranslateXTransition, createValueEffect, createWidthTransition, cubicBezier, dispatchCustomEvent, dispatchInternalCustomEvent, dispatchPublicCustomEvent, dragAfterThreshold, elementIsFocusable, elementIsVisibleForFocus, elementIsVisuallyVisible, findAfter, findAncestor, findBefore, findDescendant, findEvent, findFocusDelegateTarget, findFocusable, formatEventSideEffect, getAvailableHeight, getAvailableWidth, getBackground, getBackgroundColor, getBorder, getBorderRadius, getBorderSizes, getContrastRatio, getDefaultStyles, getDragCoordinates, getDropTargetInfo, getElementSignature, getFirstVisuallyVisibleAncestor, getFocusVisibilityInfo, getHeight, getHeightWithoutTransition, getInnerHeight, getInnerWidth, getKeyboardEventDefaultAction, getLuminance, getMarginSizes, getMaxHeight, getMaxWidth, getMinHeight, getMinWidth, getOpacity, getOpacityWithoutTransition, getPaddingSizes, getPositionedParent, getPreferedColorScheme, getScrollBox, getScrollContainer, getScrollContainerSet, getScrollRelativeRect, getSelfAndAncestorScrolls, getStyle, getTranslateX, getTranslateXWithoutTransition, getTranslateY, getVisuallyVisibleInfo, getWidth, getWidthWithoutTransition, hasCSSSizeUnit, initFlexDetailsSet, initFocusGroup, initPositionSticky, isSameColor, isScrollable, measureLongestVisualLineWidth, measureScrollbar, measureWidestChildRow, mergeOneStyle, mergeTwoStyles, normalizeKeyboardKey, normalizeStyle, normalizeStyles, parseStyle, performTabNavigation, pickPositionRelativeTo, prefersDarkColors, prefersLightColors, preventFocusNav, preventFocusNavViaKeyboard, preventIntermediateScrollbar, resolveCSSColor, resolveCSSSize, resolveColorLuminance, resolveOklchLightness, scrollIntoViewScoped, scrollIntoViewWithStickyAwareness, setAttribute, setAttributes, setStyles, snapToPixel, startDragToReorder, startDragToResizeGesture, stickyAsRelativeCoords, stringifyStyle, trapFocusInside, trapScrollInside, useActiveElement, useAvailableHeight, useAvailableWidth, useMaxHeight, useMaxWidth, useResizeStatus, visibleRectEffect };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/dom",
3
- "version": "0.14.3",
3
+ "version": "0.14.5",
4
4
  "type": "module",
5
5
  "description": "DOM utilities for writing frontend code",
6
6
  "repository": {