Skip to content

[CDX-376] Emit events on user interaction - Part 2#36

Merged
esezen merged 9 commits intocdx-376-components-ui-emit-events-on-user-interactionfrom
cdx-376-components-ui-emit-events-on-user-interaction-2
Feb 26, 2026
Merged

[CDX-376] Emit events on user interaction - Part 2#36
esezen merged 9 commits intocdx-376-components-ui-emit-events-on-user-interactionfrom
cdx-376-components-ui-emit-events-on-user-interaction-2

Conversation

@esezen
Copy link
Contributor

@esezen esezen commented Feb 9, 2026

Continuation of #35. Emits events on the parent element rather than window.

Why did I introduce a new package for Markdown?

@esezen esezen marked this pull request as ready for review February 10, 2026 15:55
Copilot AI review requested due to automatic review settings February 10, 2026 15:55
@constructor-claude-bedrock

This comment has been minimized.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates UI component event emission to support scoped, bubbling CustomEvents dispatched from component root elements (via refs), while keeping window-based listeners working for backwards compatibility.

Changes:

  • Enhance dispatchCioEvent to optionally dispatch on a provided DOM target and enable bubbling.
  • Convert ProductCard, Carousel, and Card to forwardRef components and dispatch events from their root elements.
  • Add Storybook MDX docs/demos for scoped event listening and enable GFM tables via remark-gfm.

Reviewed changes

Copilot reviewed 14 out of 15 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/utils/events.ts Adds optional target dispatch and enables bubbling for scoped event listening.
src/types/carouselTypes.ts Extends carousel context typing to include a root ref for scoped dispatch.
src/components/product-card.tsx Adds forwardRef, stores root element ref, and dispatches events on the component root.
src/components/carousel.tsx Adds forwardRef, stores root element ref, and dispatches nav events on the component root.
src/components/card.tsx Converts Card root to forwardRef so parents can attach refs to the root element.
spec/utils/events.test.ts Updates existing expectations and adds coverage for target dispatch + bubbling + fallbacks.
spec/components/product-card/product-card.test.tsx Updates tests to listen on root refs; adds bubbling + non-cross-pollination coverage.
spec/components/Carousel/Carousel.test.tsx Updates tests to listen on root refs; adds bubbling + non-cross-pollination coverage.
src/stories/components/ProductCard/ProductCardEvents.stories.tsx Adds interactive demo story for scoped ProductCard event listening.
src/stories/components/ProductCard/Code Examples - Events.mdx Adds MDX docs page with a GFM table of events and example code.
src/stories/components/Carousel/CarouselEvents.stories.tsx Adds interactive demo story for scoped Carousel event listening.
src/stories/components/Carousel/Code Examples - Events.mdx Adds MDX docs page with a GFM table of events and example code.
package.json Adds remark-gfm to support MDX markdown tables.
.storybook/main.ts Configures Storybook docs to use remark-gfm for MDX compilation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +44 to +49
* any ancestor. When no target is provided (or during SSR), falls back to
* dispatching on `window` for backwards compatibility.
*
* @param eventName - A key from {@link CioEventDetailMap} (use `CIO_EVENTS` constants).
* @param detail - The strongly-typed payload for the event.
* @param target - Optional DOM element to dispatch the event on. Falls back to `window`.
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSDoc says that when no target is provided “(or during SSR)”, it falls back to dispatching on window, but the implementation always no-ops when window is undefined. Update the doc to reflect actual behavior (SSR no-op), or adjust the SSR guard logic so that providing a non-window target can still dispatch when window is missing (only if CustomEvent is available in that environment).

Suggested change
* any ancestor. When no target is provided (or during SSR), falls back to
* dispatching on `window` for backwards compatibility.
*
* @param eventName - A key from {@link CioEventDetailMap} (use `CIO_EVENTS` constants).
* @param detail - The strongly-typed payload for the event.
* @param target - Optional DOM element to dispatch the event on. Falls back to `window`.
* any ancestor. When no target is provided in a browser environment, this
* falls back to dispatching on `window` for backwards compatibility. In
* non-browser environments (e.g., SSR) where `window` is undefined, this
* function is a no-op and does not dispatch any event.
*
* @param eventName - A key from {@link CioEventDetailMap} (use `CIO_EVENTS` constants).
* @param detail - The strongly-typed payload for the event.
* @param target - Optional EventTarget to dispatch the event on. When omitted
* in a browser environment, the event is dispatched on `window`.

Copilot uses AI. Check for mistakes.
// (e.g. ref.current.addEventListener(CIO_EVENTS.carousel.next, handler))
// Type assertion preserves generic support that forwardRef normally erases
const Carousel = forwardRef(CarouselInner) as <T = Product>(
props: CarouselOpts<T> & { ref?: React.Ref<HTMLDivElement> },
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typing the forwarded-ref component as CarouselOpts<T> & { ref?: ... } is misleading because ref is not a normal prop in React; it’s provided via RefAttributes. Prefer typing this as CarouselOpts<T> & React.RefAttributes<HTMLDivElement> (or PropsWithoutRef<...> & RefAttributes<...>) to match React’s conventions and avoid consumers treating ref like a regular prop.

Suggested change
props: CarouselOpts<T> & { ref?: React.Ref<HTMLDivElement> },
props: CarouselOpts<T> & React.RefAttributes<HTMLDivElement>,

Copilot uses AI. Check for mistakes.
dispatchCioEvent(CIO_EVENTS.productCard.click, { product });
dispatchCioEvent(CIO_EVENTS.productCard.click, { product }, rootRef.current);
onProductClick?.();
}, [product, onProductClick]);
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other callbacks in this file include rootRef in their dependency arrays for consistency, but handleProductClick does not. Even though useRef is stable, keeping dependency patterns consistent helps avoid confusion and reduces lint-rule exceptions; consider adding rootRef to the dependency array here as well.

Suggested change
}, [product, onProductClick]);
}, [product, onProductClick, rootRef]);

Copilot uses AI. Check for mistakes.
Comment on lines +374 to +384
const setRootRef = useCallback(
(node: HTMLDivElement | null) => {
(rootRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
if (typeof ref === 'function') {
ref(node);
} else if (ref) {
(ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
}
},
[ref],
);
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The casts to React.MutableRefObject are unnecessary for rootRef because useRef already returns a mutable ref (rootRef.current = node works). Simplifying this (and potentially extracting a small shared mergeRefs helper) would reduce verbosity and duplication, especially since similar ref-merging logic exists in CarouselBase.

Copilot uses AI. Check for mistakes.
@constructor-claude-bedrock

This comment has been minimized.

@esezen esezen merged commit c93fdcc into cdx-376-components-ui-emit-events-on-user-interaction Feb 26, 2026
2 checks passed
@esezen esezen deleted the cdx-376-components-ui-emit-events-on-user-interaction-2 branch February 26, 2026 19:26
@constructor-claude-bedrock
Copy link

Code Review Results

Strengths

The pub-sub event architecture is well-designed: events are dispatched on the source element with bubbles: true so consumers can listen on any ancestor, the type system is strong with CioEventDetailMap providing typed payloads, test coverage is thorough with isolation tests (cross-pollination), and the Storybook documentation with live event-log demos is excellent.

Critical Issues

None.

Important Issues

[File: src/components/carousel.tsx Line: L131] The reInit listener is registered but never cleaned up in the useEffect return. Only the select listener is removed. This is a memory leak: if the carousel re-mounts or the api changes, the old reInit handler accumulates. The cleanup should also call api?.off('reInit', onSelect).

[File: src/components/product-card.tsx Lines: L392-L398] The handleProductClick / AddToCartButton interaction relies implicitly on e.stopPropagation() in AddToCartButton to prevent productCard.click from also being dispatched on add-to-cart clicks. This implicit coupling is fragile: removing stopPropagation from the button (e.g. during a refactor) would silently break the event contract. A comment documenting this dependency would prevent future regressions.

[File: src/stories/components/Carousel/CarouselEvents.stories.tsx Lines: L24-L28] price, salePrice, and reviewsCount are generated with Math.random(), producing non-deterministic values in Storybook. Use fixed values to keep story renders reproducible for visual regression tests. Additionally, reviewsCount is typed as number here (Math.floor(...)) while the test fixtures in Carousel.test.tsx use strings (e.g. '100'). This inconsistency can mask type-narrowing bugs -- align story mock data to use strings consistently.

Suggestions

[File: src/utils/events.ts Line: L51] Consider exporting a type CioEventName = keyof CioEventDetailMap helper so consumers who want to write typed addEventListener wrappers can do so without importing both CIO_EVENTS and the full interface map separately.

[File: spec/components/product-card/product-card.test.tsx Line: L538] The long inline object literal for product2 exceeds the project line length and should be extracted to a named constant at the top of the Pub-Sub Events describe block, consistent with how mockProductData is defined at the top of the file.

[File: src/stories/components/ProductCard/ProductCardEvents.stories.tsx Lines: L48-L61] The ProductCard JSX block has inconsistent indentation relative to its parent (6-space indent vs. 2-space used elsewhere). Aligning it would match the project Prettier config.

Overall Assessment: Needs Work

esezen added a commit that referenced this pull request Feb 27, 2026
* Implementation

* Tests

* Cleanup

* Test no-op

* Stop propagation

* [CDX-376] Emit events on user interaction - Part 2 (#36)

* Emit on component rather than window

* Lint and types

* add comments

* Add docs

* Fix MDX table issue

* Lint

* Remove rootRef

* Remove refs

* Revert export style

* Remove stopPropagation

* remove unnecessary test

* Bring back comment

* Add product to callbacks

* lint

* Simplify carousel event

* Cleanup

* Use a different selector

* Handle addToWishlist
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants