@diabolic/hangover 0.1.6 → 0.2.0

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.
package/README.md CHANGED
@@ -36,7 +36,6 @@ npm install @diabolic/hangover
36
36
 
37
37
  ```jsx
38
38
  import { Dropdown } from '@diabolic/hangover'
39
- import '@diabolic/hangover/styles'
40
39
 
41
40
  export default function App() {
42
41
  return (
@@ -59,7 +58,8 @@ export default function App() {
59
58
  }
60
59
  ```
61
60
 
62
- Use the exported styles subpath above. Do not import from `dist/...` directly.
61
+ Styles are imported automatically with the component. Do not import from
62
+ `dist/...` directly.
63
63
 
64
64
  ---
65
65
 
package/dist/index.cjs.js CHANGED
@@ -1,3 +1,4 @@
1
+ require('./hangover.css');
1
2
  'use strict';
2
3
 
3
4
  Object.defineProperty(exports, '__esModule', { value: true });
@@ -737,12 +738,16 @@ function DropdownContent({
737
738
  if (displayMode !== 'scroll') return;
738
739
  const list = contentRef.current;
739
740
  if (!list) return;
740
- function updateBottomSpace() {
741
+ let rafId = null;
742
+ function computeBottomSpace() {
743
+ rafId = null;
741
744
  const sections = list.querySelectorAll('[data-section-for]');
742
745
  const lastSection = sections[sections.length - 1];
743
746
  if (!lastSection) {
744
- list.style.paddingBottom = '';
745
- bottomPadRef.current = 0;
747
+ if (bottomPadRef.current !== 0) {
748
+ list.style.paddingBottom = '';
749
+ bottomPadRef.current = 0;
750
+ }
746
751
  return;
747
752
  }
748
753
 
@@ -755,20 +760,34 @@ function DropdownContent({
755
760
  const listTop = list.getBoundingClientRect().top;
756
761
  const lastTopWithinContent = lastSection.getBoundingClientRect().top - listTop + list.scrollTop;
757
762
  const spaceBelowLastTop = naturalScrollHeight - lastTopWithinContent;
758
- const pad = Math.max(0, available - spaceBelowLastTop);
763
+ const pad = Math.max(0, Math.round(available - spaceBelowLastTop));
764
+
765
+ // Only touch the DOM when the reserved space actually changes by a whole
766
+ // pixel — this ignores sub-pixel/floating jitter from the observer and
767
+ // keeps the value stable.
768
+ if (Math.abs(pad - bottomPadRef.current) < 1) return;
759
769
  bottomPadRef.current = pad;
760
770
  list.style.paddingBottom = `${pad}px`;
761
771
  }
762
- updateBottomSpace();
772
+
773
+ // Coalesce bursts of observer callbacks into a single measurement per frame.
774
+ function scheduleBottomSpace() {
775
+ if (rafId !== null) return;
776
+ rafId = requestAnimationFrame(computeBottomSpace);
777
+ }
778
+ scheduleBottomSpace();
763
779
 
764
780
  // Observe both the container (viewport resize) and every section (group
765
781
  // expand/collapse, late reflows) so the reserved space stays accurate.
766
- const observer = new ResizeObserver(updateBottomSpace);
782
+ const observer = new ResizeObserver(scheduleBottomSpace);
767
783
  observer.observe(list);
768
784
  list.querySelectorAll('[data-section-for]').forEach(section => {
769
785
  observer.observe(section);
770
786
  });
771
- return () => observer.disconnect();
787
+ return () => {
788
+ if (rafId !== null) cancelAnimationFrame(rafId);
789
+ observer.disconnect();
790
+ };
772
791
  }, [displayMode, contentRef, children, searchQuery]);
773
792
  function handleSearch(e) {
774
793
  fireEvent('search', {
package/dist/index.esm.js CHANGED
@@ -1,3 +1,4 @@
1
+ import './hangover.css';
1
2
  import { useContext, createContext, Children, cloneElement, useState, useRef, useCallback, useEffect, isValidElement, createElement, useMemo, forwardRef, useImperativeHandle } from 'react';
2
3
  import { createPortal } from 'react-dom';
3
4
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
@@ -733,12 +734,16 @@ function DropdownContent({
733
734
  if (displayMode !== 'scroll') return;
734
735
  const list = contentRef.current;
735
736
  if (!list) return;
736
- function updateBottomSpace() {
737
+ let rafId = null;
738
+ function computeBottomSpace() {
739
+ rafId = null;
737
740
  const sections = list.querySelectorAll('[data-section-for]');
738
741
  const lastSection = sections[sections.length - 1];
739
742
  if (!lastSection) {
740
- list.style.paddingBottom = '';
741
- bottomPadRef.current = 0;
743
+ if (bottomPadRef.current !== 0) {
744
+ list.style.paddingBottom = '';
745
+ bottomPadRef.current = 0;
746
+ }
742
747
  return;
743
748
  }
744
749
 
@@ -751,20 +756,34 @@ function DropdownContent({
751
756
  const listTop = list.getBoundingClientRect().top;
752
757
  const lastTopWithinContent = lastSection.getBoundingClientRect().top - listTop + list.scrollTop;
753
758
  const spaceBelowLastTop = naturalScrollHeight - lastTopWithinContent;
754
- const pad = Math.max(0, available - spaceBelowLastTop);
759
+ const pad = Math.max(0, Math.round(available - spaceBelowLastTop));
760
+
761
+ // Only touch the DOM when the reserved space actually changes by a whole
762
+ // pixel — this ignores sub-pixel/floating jitter from the observer and
763
+ // keeps the value stable.
764
+ if (Math.abs(pad - bottomPadRef.current) < 1) return;
755
765
  bottomPadRef.current = pad;
756
766
  list.style.paddingBottom = `${pad}px`;
757
767
  }
758
- updateBottomSpace();
768
+
769
+ // Coalesce bursts of observer callbacks into a single measurement per frame.
770
+ function scheduleBottomSpace() {
771
+ if (rafId !== null) return;
772
+ rafId = requestAnimationFrame(computeBottomSpace);
773
+ }
774
+ scheduleBottomSpace();
759
775
 
760
776
  // Observe both the container (viewport resize) and every section (group
761
777
  // expand/collapse, late reflows) so the reserved space stays accurate.
762
- const observer = new ResizeObserver(updateBottomSpace);
778
+ const observer = new ResizeObserver(scheduleBottomSpace);
763
779
  observer.observe(list);
764
780
  list.querySelectorAll('[data-section-for]').forEach(section => {
765
781
  observer.observe(section);
766
782
  });
767
- return () => observer.disconnect();
783
+ return () => {
784
+ if (rafId !== null) cancelAnimationFrame(rafId);
785
+ observer.disconnect();
786
+ };
768
787
  }, [displayMode, contentRef, children, searchQuery]);
769
788
  function handleSearch(e) {
770
789
  fireEvent('search', {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diabolic/hangover",
3
- "version": "0.1.6",
3
+ "version": "0.2.0",
4
4
  "description": "A headless-style, compound React dropdown/field-picker component library",
5
5
  "license": "MIT",
6
6
  "author": "bugrakaan",
@@ -26,8 +26,7 @@
26
26
  ".": {
27
27
  "import": "./dist/index.esm.js",
28
28
  "require": "./dist/index.cjs.js"
29
- },
30
- "./styles": "./dist/hangover.css"
29
+ }
31
30
  },
32
31
  "files": [
33
32
  "dist"