react-overflow-tabs 1.0.0 → 1.1.1

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
@@ -3,7 +3,8 @@
3
3
  A headless React hook to gracefully collapse overflowing tabs into a dropdown menu.
4
4
  Works with any UI framework (Tailwind, Bootstrap, MUI, custom CSS).
5
5
 
6
- [![npm version](https://img.shields.io/npm/v/react-overflow-tabs.svg)](https://www.npmjs.com/package/react-overflow-tabs)
6
+ [![npm version](https://img.shields.io/npm/v/react-overflow-tabs.svg)](https://www.npmjs.com/package/react-overflow-tabs)
7
+ [![Bundle Size](https://img.shields.io/bundlephobia/minzip/react-overflow-tabs)](https://bundlephobia.com/package/react-overflow-tabs)
7
8
  [![license](https://img.shields.io/npm/l/react-overflow-tabs.svg)](LICENSE)
8
9
 
9
10
  A lightweight React hook for responsive tab navigation with automatic overflow handling.
@@ -128,18 +129,18 @@ const Example = () => {
128
129
 
129
130
  #### Options (`IOverflowTabsOptions`)
130
131
 
131
- | Option | Type | Default | Description |
132
- | ------------- | ----------------------------------------- | ----------------------- | --------------------------------------------------------------------------- |
133
- | `container` | `RefObject<HTMLElement>` \| `HTMLElement` | **required** | The container element to observe. |
134
- | `tabSelector` | `string` | `"[data-overflow-key]"` | Selector for tab elements. Each tab must have a unique `data-overflow-key`. |
135
- | `disabled` | `boolean` | `false` | Temporarily disable overflow tracking. |
132
+ | Option | Type | Default | Description |
133
+ | ------------- | ----------------------------------------- | --------------------- | -------------------------------------------------------------------------------------------- |
134
+ | `container` | `RefObject<HTMLElement>` \| `HTMLElement` | **required** | The container element to observe. |
135
+ | `tabSelector` | `string` | `"data-overflow-key"` | Attribute that marks tabs (`"data-tab"` or `"[data-tab]"`). Its unique value is the tab key. |
136
+ | `disabled` | `boolean` | `false` | Temporarily disable overflow tracking; all tabs are reported as visible. |
136
137
 
137
138
  #### Return (`IOverflowState`)
138
139
 
139
140
  | Key | Type | Description |
140
141
  | --------------- | ---------- | ------------------------------------------------ |
141
142
  | `visibleKeys` | `string[]` | Keys of tabs currently visible in the container. |
142
- | `overflowKeys` | `string[]` | Keys of tabs pushed into overflow (hidden). |
143
+ | `overflowKeys` | `string[]` | Keys of tabs pushed into overflow, in DOM order. |
143
144
  | `isOverflowing` | `boolean` | Whether overflow is currently happening. |
144
145
 
145
146
  ---
@@ -148,7 +149,7 @@ const Example = () => {
148
149
 
149
150
  ```tsx
150
151
  import { useRef } from "react";
151
- import useOverflowTabs from "./useOverflowTabs";
152
+ import { useOverflowTabs } from "react-overflow-tabs";
152
153
 
153
154
  const Example = () => {
154
155
  const containerRef = useRef<HTMLDivElement>(null);
@@ -207,6 +208,9 @@ const Example = () => {
207
208
  - Uses **`IntersectionObserver`** under the hood → supported in modern browsers.
208
209
  - `threshold: 0.999` by default → requires _full visibility_ to count as visible.
209
210
  - Works with **RTL** layouts, as it only tracks visibility.
211
+ - Tabs added, removed or re-keyed after mount are picked up automatically (via `MutationObserver`).
212
+ - `tabSelector` must be an attribute name; CSS selectors such as `.tab` are rejected with an error.
213
+ - Without `IntersectionObserver` (e.g. SSR or jsdom), all tabs are reported as visible.
210
214
 
211
215
  ---
212
216
 
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ "use strict";var m=Object.defineProperty;var x=Object.getOwnPropertyDescriptor;var A=Object.getOwnPropertyNames;var K=Object.prototype.hasOwnProperty;var M=(t,e)=>{for(var s in e)m(t,s,{get:e[s],enumerable:!0})},H=(t,e,s,f)=>{if(e&&typeof e=="object"||typeof e=="function")for(let a of A(e))!K.call(t,a)&&a!==s&&m(t,a,{get:()=>e[a],enumerable:!(f=x(e,a))||f.enumerable});return t};var _=t=>H(m({},"__esModule",{value:!0}),t);var $={};M($,{useOverflowTabs:()=>g});module.exports=_($);var c=require("react");var k=t=>t?"current"in t?t.current:t:null,I=k;var R=/^-?[A-Za-z_][\w-]*$/,z=(t="data-overflow-key")=>{let e=t.trim();if(e.startsWith("[")&&e.endsWith("]")&&(e=e.slice(1,-1).trim()),!R.test(e))throw new Error(`react-overflow-tabs: invalid tabSelector "${t}". Expected an attribute name such as "data-overflow-key" or "[data-overflow-key]".`);return e},d=z;var O=.999,E={visibleKeys:[],overflowKeys:[],isOverflowing:!1},h=(t,e)=>t.length===e.length&&t.every((s,f)=>s===e[f]),C=(t,e)=>h(t.visibleKeys,e.visibleKeys)&&h(t.overflowKeys,e.overflowKeys),N=({container:t,tabSelector:e,disabled:s=!1})=>{let[f,a]=(0,c.useState)(E),[u,L]=(0,c.useState)(null),T=d(e);return(0,c.useEffect)(()=>{L(I(t))}),(0,c.useEffect)(()=>{let p=r=>a(o=>C(o,r)?o:r);if(!u){p(E);return}let w=[],v=new Set,y=()=>{let r=[],o=[];for(let b of w){let l=b.getAttribute(T);l&&(v.has(b)?o:r).push(l)}p({visibleKeys:r,overflowKeys:o,isOverflowing:o.length>0})},n=s||typeof IntersectionObserver=="undefined"?null:new IntersectionObserver(r=>{for(let o of r)o.intersectionRatio<O?v.add(o.target):v.delete(o.target);y()},{root:u,threshold:O}),S=()=>{let r=Array.from(u.querySelectorAll(`[${T}]`)),o=new Set(r),b=new Set(w);for(let l of w)o.has(l)||(n==null||n.unobserve(l),v.delete(l));for(let l of r)b.has(l)||n==null||n.observe(l);w=r,y()};S();let i=typeof MutationObserver=="undefined"?null:new MutationObserver(S);return i==null||i.observe(u,{childList:!0,subtree:!0,attributes:!0,attributeFilter:[T]}),()=>{n==null||n.disconnect(),i==null||i.disconnect()}},[u,T,s]),f},g=N;0&&(module.exports={useOverflowTabs});
@@ -9,11 +9,15 @@ interface IOverflowTabsOptions<T extends HTMLElement = HTMLElement> {
9
9
  */
10
10
  container: RefObject<T | null> | T;
11
11
  /**
12
- * Attribute name for tabs.
13
- * Each tab must have this attribute with a unique value.
14
- * Default: "data-overflow-key" or "[data-overflow-key]"
12
+ * Attribute name that marks tabs, with or without square brackets.
13
+ * Each tab must have this attribute with a unique value, which is reported as its key.
14
+ * Default: "data-overflow-key"
15
15
  */
16
16
  tabSelector?: string;
17
+ /**
18
+ * Temporarily disable overflow tracking; all tabs are reported as visible.
19
+ * Default: false
20
+ */
17
21
  disabled?: boolean;
18
22
  }
19
23
  interface IOverflowState {
@@ -22,6 +26,6 @@ interface IOverflowState {
22
26
  isOverflowing: boolean;
23
27
  }
24
28
 
25
- declare const useOverflowTabs: <T extends HTMLElement = HTMLElement>(options: IOverflowTabsOptions<T>) => IOverflowState;
29
+ declare const useOverflowTabs: <T extends HTMLElement = HTMLElement>({ container, tabSelector, disabled, }: IOverflowTabsOptions<T>) => IOverflowState;
26
30
 
27
31
  export { type IOverflowState, type IOverflowTabsOptions, type TOverflowDirection, useOverflowTabs };
package/dist/index.d.ts CHANGED
@@ -9,11 +9,15 @@ interface IOverflowTabsOptions<T extends HTMLElement = HTMLElement> {
9
9
  */
10
10
  container: RefObject<T | null> | T;
11
11
  /**
12
- * Attribute name for tabs.
13
- * Each tab must have this attribute with a unique value.
14
- * Default: "data-overflow-key" or "[data-overflow-key]"
12
+ * Attribute name that marks tabs, with or without square brackets.
13
+ * Each tab must have this attribute with a unique value, which is reported as its key.
14
+ * Default: "data-overflow-key"
15
15
  */
16
16
  tabSelector?: string;
17
+ /**
18
+ * Temporarily disable overflow tracking; all tabs are reported as visible.
19
+ * Default: false
20
+ */
17
21
  disabled?: boolean;
18
22
  }
19
23
  interface IOverflowState {
@@ -22,6 +26,6 @@ interface IOverflowState {
22
26
  isOverflowing: boolean;
23
27
  }
24
28
 
25
- declare const useOverflowTabs: <T extends HTMLElement = HTMLElement>(options: IOverflowTabsOptions<T>) => IOverflowState;
29
+ declare const useOverflowTabs: <T extends HTMLElement = HTMLElement>({ container, tabSelector, disabled, }: IOverflowTabsOptions<T>) => IOverflowState;
26
30
 
27
31
  export { type IOverflowState, type IOverflowTabsOptions, type TOverflowDirection, useOverflowTabs };
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- "use strict";var u=Object.defineProperty;var E=Object.getOwnPropertyDescriptor;var I=Object.getOwnPropertyNames;var K=Object.prototype.hasOwnProperty;var h=(e,t)=>{for(var o in t)u(e,o,{get:t[o],enumerable:!0})},x=(e,t,o,a)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of I(t))!K.call(e,r)&&r!==o&&u(e,r,{get:()=>t[r],enumerable:!(a=E(t,r))||a.enumerable});return e};var z=e=>x(u({},"__esModule",{value:!0}),e);var M={};h(M,{useOverflowTabs:()=>w});module.exports=z(M);var l=require("react");var A=e=>e?typeof e=="object"&&"current"in e?e.current:e:null,y=A;var H=e=>{let t=e!=null?e:"[data-overflow-key]";return t.startsWith("[")&&t.endsWith("]")?t.slice(1,-1):t},p=H;var L=e=>{let[t,o]=(0,l.useState)([]),[a,r]=(0,l.useState)([]),[g,b]=(0,l.useState)(!1);return(0,l.useEffect)(()=>{let c=y(e.container);if(!c)return;let m=p(e.tabSelector),v=Array.from(c.querySelectorAll(`[${m}]`));if(o(v.map(f=>f.getAttribute(m))),e.disabled==!0){r([]),b(!1);return}let O=new Set,i=new Set,T=new IntersectionObserver(f=>{let d=!1;for(let n of f){let s=n.target.getAttribute(m);if(!s)continue;O.add(s);let S=i.size;n.intersectionRatio<1?i.add(s):i.delete(s),i.size!=S&&(d=!0)}if(d){let n=Array.from(i);r(n.reverse()),o(Array.from(O).filter(s=>!n.includes(s))),b(n.length>0)}},{root:c,threshold:.999});return v.forEach(f=>T.observe(f)),()=>{T.disconnect()}},[e.container,e.tabSelector,e.disabled]),{visibleKeys:t,overflowKeys:a,isOverflowing:g}},w=L;0&&(module.exports={useOverflowTabs});
1
+ import{useEffect as S,useState as I}from"react";var L=e=>e?"current"in e?e.current:e:null,p=L;var x=/^-?[A-Za-z_][\w-]*$/,A=(e="data-overflow-key")=>{let t=e.trim();if(t.startsWith("[")&&t.endsWith("]")&&(t=t.slice(1,-1).trim()),!x.test(t))throw new Error(`react-overflow-tabs: invalid tabSelector "${e}". Expected an attribute name such as "data-overflow-key" or "[data-overflow-key]".`);return t},y=A;var d=.999,O={visibleKeys:[],overflowKeys:[],isOverflowing:!1},E=(e,t)=>e.length===t.length&&e.every((f,w)=>f===t[w]),K=(e,t)=>E(e.visibleKeys,t.visibleKeys)&&E(e.overflowKeys,t.overflowKeys),M=({container:e,tabSelector:t,disabled:f=!1})=>{let[w,h]=I(O),[a,g]=I(null),i=y(t);return S(()=>{g(p(e))}),S(()=>{let v=r=>h(o=>K(o,r)?o:r);if(!a){v(O);return}let c=[],u=new Set,b=()=>{let r=[],o=[];for(let T of c){let n=T.getAttribute(i);n&&(u.has(T)?o:r).push(n)}v({visibleKeys:r,overflowKeys:o,isOverflowing:o.length>0})},s=f||typeof IntersectionObserver=="undefined"?null:new IntersectionObserver(r=>{for(let o of r)o.intersectionRatio<d?u.add(o.target):u.delete(o.target);b()},{root:a,threshold:d}),m=()=>{let r=Array.from(a.querySelectorAll(`[${i}]`)),o=new Set(r),T=new Set(c);for(let n of c)o.has(n)||(s==null||s.unobserve(n),u.delete(n));for(let n of r)T.has(n)||s==null||s.observe(n);c=r,b()};m();let l=typeof MutationObserver=="undefined"?null:new MutationObserver(m);return l==null||l.observe(a,{childList:!0,subtree:!0,attributes:!0,attributeFilter:[i]}),()=>{s==null||s.disconnect(),l==null||l.disconnect()}},[a,i,f]),w},H=M;export{H as useOverflowTabs};
package/package.json CHANGED
@@ -1,15 +1,16 @@
1
1
  {
2
2
  "name": "react-overflow-tabs",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
+ "type": "module",
4
5
  "description": "A lightweight React hook for responsive tab navigation with automatic overflow handling. Works with any UI framework (Tailwind, Bootstrap, MUI, custom CSS).",
5
- "main": "dist/index.js",
6
- "module": "dist/index.mjs",
6
+ "main": "dist/index.cjs",
7
+ "module": "dist/index.js",
7
8
  "types": "dist/index.d.ts",
8
9
  "exports": {
9
10
  ".": {
10
11
  "types": "./dist/index.d.ts",
11
- "import": "./dist/index.mjs",
12
- "require": "./dist/index.js"
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
13
14
  },
14
15
  "./package.json": "./package.json"
15
16
  },
@@ -19,7 +20,11 @@
19
20
  "sideEffects": false,
20
21
  "scripts": {
21
22
  "build": "tsup",
22
- "format": "prettier --write ./**/*.{js,jsx,ts,tsx,cjs,json,css,scss}",
23
+ "format": "prettier --write .",
24
+ "format:check": "prettier --check .",
25
+ "lint": "eslint .",
26
+ "lint:fix": "eslint . --fix",
27
+ "test": "vitest run",
23
28
  "typecheck": "tsc -p tsconfig.json --noEmit"
24
29
  },
25
30
  "repository": {
@@ -61,17 +66,23 @@
61
66
  "react": ">=18 <20"
62
67
  },
63
68
  "devDependencies": {
69
+ "@eslint/js": "^9.34.0",
64
70
  "@semantic-release/changelog": "^6.0.3",
65
71
  "@semantic-release/git": "^10.0.1",
66
- "@semantic-release/github": "^11.0.4",
67
- "@semantic-release/npm": "^12.0.2",
72
+ "@testing-library/react": "^16.3.3",
68
73
  "@types/react": "^19.1.10",
69
74
  "conventional-changelog-conventionalcommits": "^9.1.0",
75
+ "eslint": "^9.34.0",
76
+ "globals": "^17.12.0",
77
+ "jsdom": "^29.1.1",
70
78
  "prettier": "^3.6.2",
71
79
  "react": "^19.1.1",
80
+ "react-dom": "^19.1.1",
72
81
  "semantic-release": "^24.2.7",
73
82
  "tsup": "^8.5.0",
74
- "typescript": "^5.9.2"
83
+ "typescript": "^5.9.2",
84
+ "typescript-eslint": "^8.41.0",
85
+ "vitest": "^4.1.11"
75
86
  },
76
87
  "engines": {
77
88
  "node": ">=18"
package/dist/index.mjs DELETED
@@ -1 +0,0 @@
1
- import{useEffect as E,useState as f}from"react";var g=e=>e?typeof e=="object"&&"current"in e?e.current:e:null,T=g;var S=e=>{let t=e!=null?e:"[data-overflow-key]";return t.startsWith("[")&&t.endsWith("]")?t.slice(1,-1):t},d=S;var I=e=>{let[t,a]=f([]),[y,c]=f([]),[p,m]=f(!1);return E(()=>{let l=T(e.container);if(!l)return;let i=d(e.tabSelector),u=Array.from(l.querySelectorAll(`[${i}]`));if(a(u.map(s=>s.getAttribute(i))),e.disabled==!0){c([]),m(!1);return}let b=new Set,n=new Set,v=new IntersectionObserver(s=>{let O=!1;for(let r of s){let o=r.target.getAttribute(i);if(!o)continue;b.add(o);let w=n.size;r.intersectionRatio<1?n.add(o):n.delete(o),n.size!=w&&(O=!0)}if(O){let r=Array.from(n);c(r.reverse()),a(Array.from(b).filter(o=>!r.includes(o))),m(r.length>0)}},{root:l,threshold:.999});return u.forEach(s=>v.observe(s)),()=>{v.disconnect()}},[e.container,e.tabSelector,e.disabled]),{visibleKeys:t,overflowKeys:y,isOverflowing:p}},K=I;export{K as useOverflowTabs};