Skip to content

Feat/compt 31 dom event hooks#8

Merged
a-elkhiraooui-ciscode merged 2 commits intodevelopfrom
feat/COMPT-31-dom-event-hooks
Mar 30, 2026
Merged

Feat/compt 31 dom event hooks#8
a-elkhiraooui-ciscode merged 2 commits intodevelopfrom
feat/COMPT-31-dom-event-hooks

Conversation

@a-elkhiraooui-ciscode
Copy link
Copy Markdown

Summary

  • What does this PR change?

Why

  • Why is this change needed?

Checklist

  • Added/updated tests (if behavior changed)
  • npm run lint passes
  • npm run typecheck passes
  • npm test passes
  • npm run build passes
  • Added a changeset (npx changeset) if this affects consumers

Notes

  • Anything reviewers should pay attention to?

…eIntersectionObserver

- useMediaQuery(query): tracks matchMedia via useSyncExternalStore, SSR-safe (server snapshot false)
- useWindowSize(): returns {width, height}, debounced 100ms on resize, SSR-safe ({0,0})
- useClickOutside(ref, handler): fires on mousedown/touchstart outside ref; handler via ref pattern
- useIntersectionObserver(ref, options?): IntersectionObserverEntry|null, disconnects on unmount
- All listeners registered in useEffect with cleanup return
- All SSR-safe: typeof window === undefined guards
- Zero runtime dependencies
- tsc --noEmit passes, lint passes (0 warnings), 26/26 tests pass, coverage >= 95%
- All four exported from src/hooks/index.ts -> src/index.ts
- Changeset added, copilot-instructions.md updated for epic COMPT-2
- remove accidental duplicated useMediaQuery suite block
- extract shared viewport setup in useWindowSize tests
- extract shared mount helper in useClickOutside tests
- keep behavior coverage unchanged
Copilot AI review requested due to automatic review settings March 30, 2026 13:22
@a-elkhiraooui-ciscode a-elkhiraooui-ciscode merged commit 788fe7e into develop Mar 30, 2026
5 checks passed
@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown

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

Adds a new “DOM & Events” hook group to HooksKit (COMPT-31), expanding the library’s hook surface area alongside accompanying tests, docs, and a changeset.

Changes:

  • Introduces four new hooks: useMediaQuery, useWindowSize, useClickOutside, useIntersectionObserver.
  • Adds Vitest + React Testing Library coverage for each new hook.
  • Updates the hooks barrel export list, project instructions, and adds a minor-version changeset.

Reviewed changes

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

Show a summary per file
File Description
src/hooks/useWindowSize.ts Adds debounced resize-based window size hook (and a helper).
src/hooks/useWindowSize.test.ts Tests initial size + debounced resize behavior + cleanup.
src/hooks/useMediaQuery.ts Adds matchMedia-backed query hook using useSyncExternalStore.
src/hooks/useMediaQuery.test.ts Tests initial match, updates, and unmount cleanup.
src/hooks/useIntersectionObserver.ts Adds IntersectionObserver-backed hook for latest entry.
src/hooks/useIntersectionObserver.test.ts Tests entry updates, observe/disconnect behavior.
src/hooks/useClickOutside.ts Adds outside-click/touch handler hook with stale-closure avoidance.
src/hooks/useClickOutside.test.ts Tests outside vs inside interactions and cleanup.
src/hooks/index.ts Exports the new hooks from the hooks barrel.
.github/instructions/copilot-instructions.md Updates docs to include COMPT-31 hooks and library naming/structure.
.changeset/COMPT-31-dom-event-hooks.md Declares a minor release and documents the new hooks.

height: number;
}

export function getWindowSize(): WindowSize {
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

getWindowSize is exported from this module, which makes it part of the public API via the hook barrel exports. If the intent is to expose only hooks, make this helper const/non-exported (or move it to an internal util) to avoid an unintended API surface and future breaking-change constraints.

Suggested change
export function getWindowSize(): WindowSize {
function getWindowSize(): WindowSize {

Copilot uses AI. Check for mistakes.
Comment on lines +8 to +21
const optionsRef = useRef(options);

useEffect(() => {
if (typeof window === 'undefined' || !ref.current) return;

const observer = new IntersectionObserver(([newEntry]) => {
if (newEntry) setEntry(newEntry);
}, optionsRef.current);

observer.observe(ref.current);
return () => {
observer.disconnect();
};
}, [ref]);
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

The effect captures the initial options and observes only the initial ref.current. If options change or the ref starts pointing to a different element after mount, the observer won’t be recreated/re-attached, so the hook can return stale results. Consider tracking the observed element (e.g., const node = ref.current) and including node + options in the effect dependencies (or updating optionsRef.current when options changes).

Suggested change
const optionsRef = useRef(options);
useEffect(() => {
if (typeof window === 'undefined' || !ref.current) return;
const observer = new IntersectionObserver(([newEntry]) => {
if (newEntry) setEntry(newEntry);
}, optionsRef.current);
observer.observe(ref.current);
return () => {
observer.disconnect();
};
}, [ref]);
useEffect(() => {
if (typeof window === 'undefined') return;
const node = ref.current;
if (!node) return;
const observer = new IntersectionObserver(([newEntry]) => {
if (newEntry) setEntry(newEntry);
}, options);
observer.observe(node);
return () => {
observer.disconnect();
};
}, [ref.current, options]);

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +16
if (typeof window === 'undefined' || !ref.current) return;

const observer = new IntersectionObserver(([newEntry]) => {
if (newEntry) setEntry(newEntry);
}, optionsRef.current);

Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

new IntersectionObserver(...) will throw in environments where the API is unavailable (older browsers, some test/embedded webviews). Add a feature-detection guard (e.g., check typeof IntersectionObserver !== 'undefined') and return null / no-op when unsupported to keep the hook safe to import/use.

Copilot uses AI. Check for mistakes.

useEffect(() => {
handlerRef.current = handler;
});
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

This effect runs after every render because it has no dependency array. Since it only needs to update the ref when handler changes, add [handler] as the dependency list to avoid unnecessary effect executions.

Suggested change
});
}, [handler]);

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +11
if (typeof window === 'undefined') return () => undefined;
const mql = window.matchMedia(query);
mql.addEventListener('change', callback);
return () => mql.removeEventListener('change', callback);
},
() => (typeof window !== 'undefined' ? window.matchMedia(query).matches : false),
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

window.matchMedia isn’t available in all browser-like environments (and is undefined in some DOM shims unless polyfilled). As written, calling useMediaQuery will throw if matchMedia is missing. Consider guarding for typeof window === 'undefined' || typeof window.matchMedia !== 'function' and falling back to false with a no-op subscription.

Suggested change
if (typeof window === 'undefined') return () => undefined;
const mql = window.matchMedia(query);
mql.addEventListener('change', callback);
return () => mql.removeEventListener('change', callback);
},
() => (typeof window !== 'undefined' ? window.matchMedia(query).matches : false),
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
return () => undefined;
}
const mql = window.matchMedia(query);
mql.addEventListener('change', callback);
return () => mql.removeEventListener('change', callback);
},
() =>
typeof window !== 'undefined' && typeof window.matchMedia === 'function'
? window.matchMedia(query).matches
: false,

Copilot uses AI. Check for mistakes.
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