@jsenv/dom 0.14.2 → 0.14.4

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 +121 -56
  2. package/package.json +1 -1
package/dist/jsenv_dom.js CHANGED
@@ -208,9 +208,10 @@ const dispatchInternalCustomEvent = (
208
208
  customEventDetail,
209
209
  ) => {
210
210
  const customEvent = new CustomEvent(customEventName, {
211
- detail: resolveEventDetail(customEventDetail),
211
+ detail: customEventDetail || {},
212
212
  cancelable: true,
213
213
  });
214
+ chainEvent(customEvent, customEventDetail?.event);
214
215
  return el.dispatchEvent(customEvent);
215
216
  };
216
217
 
@@ -224,10 +225,11 @@ const dispatchPublicCustomEvent = (
224
225
  customEventDetail,
225
226
  ) => {
226
227
  const customEvent = new CustomEvent(customEventName, {
227
- detail: resolveEventDetail(customEventDetail),
228
+ detail: customEventDetail || {},
228
229
  bubbles: true,
229
230
  cancelable: true,
230
231
  });
232
+ chainEvent(customEvent, customEventDetail?.event);
231
233
  return el.dispatchEvent(customEvent);
232
234
  };
233
235
 
@@ -239,26 +241,34 @@ const dispatchPublicCustomEvent = (
239
241
  */
240
242
  const dispatchCustomEvent = (el, customEventName, customEventDetail) => {
241
243
  const customEvent = new CustomEvent(customEventName, {
242
- detail: resolveEventDetail(customEventDetail),
244
+ detail: customEventDetail || {},
243
245
  cancelable: true,
244
246
  });
247
+ chainEvent(customEvent, customEventDetail?.event);
245
248
  const result = el.dispatchEvent(customEvent);
246
249
  return result;
247
250
  };
248
251
 
249
- const resolveEventDetail = (customEventDetail) => {
250
- const { event, ...rest } = customEventDetail ?? {};
251
- const isWrappedCustomEvent = event?.detail?.event !== undefined;
252
- if (!isWrappedCustomEvent) {
253
- return { ...rest, event };
252
+ const chainEvent = (customEvent, parentEvent) => {
253
+ if (!parentEvent) {
254
+ return customEvent;
254
255
  }
255
- // Keep `event` as the direct parent so callers see the immediate facade.
256
- // Build eventChain as [root, ...grandparents] — oldest first, excluding `event`.
257
- const previousChain = event.detail.eventChain;
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
+ }
262
+ // Always build eventChain from the first wrapping so callers can rely on it
263
+ // being present whenever `parentEvent` is set.
264
+ // eventChain = [oldest, ..., parentEvent] — the full ancestor list including the direct parent.
265
+ const previousChain = parentEvent.detail?.eventChain;
258
266
  const eventChain = previousChain
259
- ? [...previousChain, event.detail.event]
260
- : [event.detail.event];
261
- return { ...rest, event, eventChain };
267
+ ? [...previousChain, parentEvent]
268
+ : [parentEvent];
269
+ customEvent.detail.event = parentEvent;
270
+ customEvent.detail.eventChain = eventChain;
271
+ return customEvent;
262
272
  };
263
273
 
264
274
  /**
@@ -288,12 +298,6 @@ const findEvent = (event, predicate) => {
288
298
  }
289
299
  }
290
300
  }
291
- const initiator = event.detail?.event;
292
- if (initiator) {
293
- if (match(initiator)) {
294
- return initiator;
295
- }
296
- }
297
301
  return undefined;
298
302
  };
299
303
 
@@ -314,32 +318,31 @@ const resolveEventPredicate = (predicate) => {
314
318
  */
315
319
  const formatEventSideEffect = (e, sideEffect) => {
316
320
  const parts = [];
317
- if (e.detail?.event !== undefined) {
321
+ if (e.detail?.eventChain) {
318
322
  const chain = e.detail.eventChain;
319
- const initiator = chain ? chain[0] : e.detail.event;
323
+ const initiator = chain[0];
320
324
  parts.push(
321
- `"${initiator.type}" on ${getElementSignature(initiator.target)}`,
325
+ `"${getEventLabel(initiator)}" on ${getElementSignature(initiator.target)}`,
322
326
  );
323
- if (chain) {
324
- for (const chainedEvent of chain.slice(1)) {
325
- parts.push(chainedEvent.type);
326
- }
327
- parts.push(e.detail.event.type);
327
+ // chain[0] is shown as initiator above; chain includes event as last element
328
+ for (const chainedEvent of chain.slice(1)) {
329
+ parts.push(getEventLabel(chainedEvent));
328
330
  }
329
- parts.push(e.type);
331
+ parts.push(getEventLabel(e));
330
332
  } else {
331
- parts.push(`"${e.type}" on ${getElementSignature(e.target)}`);
333
+ parts.push(`"${getEventLabel(e)}" on ${getElementSignature(e.target)}`);
332
334
  }
333
335
  return `${parts.join(" -> ")} -> ${sideEffect}`;
334
336
  };
335
337
 
336
338
  /**
337
339
  * Creates a stateful debug logger that groups side effects by their native initiator event.
340
+ * Use createCategory(name, color) to get a typed logger function for each concern.
338
341
  *
339
342
  * Usage:
340
- * const log = createEventGroupLogger();
341
- * log(e, "navi_action_requested"); // opens/reuses a group for the initiator event
342
- * log("plain message"); // logs inside the current group (or standalone)
343
+ * const logger = createEventGroupLogger();
344
+ * const logAction = logger.createCategory("[action]", "#e67e22");
345
+ * logAction(e, "action started"); // opens/reuses a group for the initiator event
343
346
  *
344
347
  * The group closes automatically after the current JS task completes (setTimeout 0).
345
348
  */
@@ -358,14 +361,18 @@ const createEventGroupLogger = () => {
358
361
  }, 0);
359
362
  };
360
363
 
361
- return (eOrMessage, sideEffect, ...args) => {
362
- if (!(eOrMessage instanceof Event)) {
363
- console.debug(eOrMessage);
364
+ const log = (category, color, e, ...args) => {
365
+ if (!(e instanceof Event)) {
366
+ console.debug(
367
+ `%c${category}`,
368
+ `color:${color};font-weight:bold`,
369
+ e,
370
+ ...args,
371
+ );
364
372
  return;
365
373
  }
366
- const e = eOrMessage;
367
374
  const chain = e.detail?.eventChain;
368
- const initiator = chain ? chain[0] : (e.detail?.event ?? e);
375
+ const initiator = chain ? chain[0] : e;
369
376
  if (initiator !== currentInitiator) {
370
377
  if (currentInitiator !== null) {
371
378
  clearTimeout(closeGroupTimeout);
@@ -373,33 +380,66 @@ const createEventGroupLogger = () => {
373
380
  console.groupEnd();
374
381
  }
375
382
  const label = initiator.target
376
- ? `"${initiator.type}" on ${getElementSignature(initiator.target)}`
377
- : `"${initiator.type}"`;
383
+ ? `"${getEventLabel(initiator)}" on ${getElementSignature(initiator.target)}`
384
+ : `"${getEventLabel(initiator)}"`;
378
385
  console.group(label);
379
386
  currentInitiator = initiator;
380
387
  }
381
- const line = formatSideEffectLine(e, sideEffect);
382
- console.debug(line, ...args);
388
+ const line = formatSideEffectLine(e, category);
389
+ console.debug(`%c${line}`, `color:${color};font-weight:bold`, ...args);
383
390
  scheduleGroupEnd();
384
391
  };
392
+
393
+ return {
394
+ createCategory: (name, color = "inherit") => {
395
+ return (e, ...args) => {
396
+ log(name, color, e, ...args);
397
+ };
398
+ },
399
+ };
385
400
  };
386
401
 
387
- const formatSideEffectLine = (e, sideEffect) => {
388
- const parts = [];
402
+ const formatSideEffectLine = (e, prefix) => {
403
+ const parts = [prefix];
389
404
  const chain = e.detail?.eventChain;
390
405
  if (chain) {
391
- // chain[0] is the root event, already shown as the group label — skip it
406
+ // chain[0] is the root event, already shown as the group label — skip it.
407
+ // chain includes the direct parent (e.detail.event) as its last element.
392
408
  for (const chainedEvent of chain.slice(1)) {
393
- parts.push(chainedEvent.type);
394
- }
395
- if (e.detail?.event) {
396
- parts.push(e.detail.event.type);
409
+ parts.push(getEventLabel(chainedEvent));
397
410
  }
398
411
  }
399
- parts.push(sideEffect);
400
412
  return parts.join(" -> ");
401
413
  };
402
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
+
403
443
  const createIterableWeakSet = () => {
404
444
  const objectWeakRefSet = new Set();
405
445
 
@@ -4197,6 +4237,19 @@ const getFocusVisibilityInfo = (node, { excludeAriaHidden } = {}) => {
4197
4237
  ) {
4198
4238
  return { visible: false, reason: "inside closed popover element" };
4199
4239
  }
4240
+ // Open popovers and open dialogs render in the top layer: they escape
4241
+ // the normal layout/stacking context of their DOM ancestors.
4242
+ // No need to check further up the tree.
4243
+ if (elementIsDialog(nodeOrAncestor) && nodeOrAncestor.open) {
4244
+ break;
4245
+ }
4246
+ if (
4247
+ nodeOrAncestor.popover !== null &&
4248
+ nodeOrAncestor.popover !== undefined &&
4249
+ nodeOrAncestor.matches(":popover-open")
4250
+ ) {
4251
+ break;
4252
+ }
4200
4253
  nodeOrAncestor = nodeOrAncestor.parentNode;
4201
4254
  }
4202
4255
  return { visible: true, reason: "no reason to be hidden" };
@@ -4309,10 +4362,7 @@ const elementIsFocusable = (node, { excludeAriaHidden } = {}) => {
4309
4362
  }
4310
4363
  return canFocus(node);
4311
4364
  }
4312
- if (
4313
- ["button", "select", "datalist", "iframe", "textarea"].indexOf(nodeName) >
4314
- -1
4315
- ) {
4365
+ if (FOCUSABLE_NODE_NAME_SET.has(nodeName)) {
4316
4366
  return canFocus(node);
4317
4367
  }
4318
4368
  if (["a", "area"].indexOf(nodeName) > -1) {
@@ -4338,6 +4388,14 @@ const elementIsFocusable = (node, { excludeAriaHidden } = {}) => {
4338
4388
  }
4339
4389
  return false;
4340
4390
  };
4391
+ const FOCUSABLE_NODE_NAME_SET = new Set([
4392
+ "button",
4393
+ "select",
4394
+ "datalist",
4395
+ "dialog",
4396
+ "iframe",
4397
+ "textarea",
4398
+ ]);
4341
4399
 
4342
4400
  const canInteract = (element) => {
4343
4401
  if (element.disabled) {
@@ -4364,9 +4422,16 @@ const canInteract = (element) => {
4364
4422
  * @returns {Element|null}
4365
4423
  */
4366
4424
  const findFocusDelegateTarget = (el) => {
4367
- if (!el.hasAttribute("navi-focus-delegate")) {
4425
+ const naviFocusDelegate = el.getAttribute("navi-focus-delegate");
4426
+ if (naviFocusDelegate === null || naviFocusDelegate === undefined) {
4368
4427
  return null;
4369
4428
  }
4429
+ if (naviFocusDelegate) {
4430
+ const delegateTarget = document.getElementById(naviFocusDelegate);
4431
+ if (delegateTarget && elementIsFocusable(delegateTarget)) {
4432
+ return delegateTarget;
4433
+ }
4434
+ }
4370
4435
  let ancestor = el.parentElement;
4371
4436
  while (ancestor) {
4372
4437
  if (elementIsFocusable(ancestor)) {
@@ -15081,4 +15146,4 @@ const useResizeStatus = (elementRef, { as = "number" } = {}) => {
15081
15146
  };
15082
15147
  };
15083
15148
 
15084
- export { EASING, activeElementSignal, addActiveElementEffect, addAttributeEffect, allowWheelThrough, appendStyles, captureScrollState, 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 };
15149
+ 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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/dom",
3
- "version": "0.14.2",
3
+ "version": "0.14.4",
4
4
  "type": "module",
5
5
  "description": "DOM utilities for writing frontend code",
6
6
  "repository": {