@arun-dev/headless 4.6.0 → 4.7.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.
@@ -1,3 +1,7 @@
1
+ import {
2
+ byDocumentOrder,
3
+ rovingIndex
4
+ } from "../chunk-2S4XG4VD.js";
1
5
  import {
2
6
  useControlled
3
7
  } from "../chunk-UEIB3I3T.js";
@@ -46,18 +50,13 @@ import { jsx } from "react/jsx-runtime";
46
50
  function idPart(value) {
47
51
  return value.replace(/\s+/g, "_");
48
52
  }
49
- function documentOrder(a, b) {
50
- const x = a.ref.current;
51
- const y = b.ref.current;
52
- if (!x || !y) return 0;
53
- return x.compareDocumentPosition(y) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;
54
- }
55
53
  function TabsRoot({
56
54
  value: valueProp,
57
55
  defaultValue,
58
56
  onValueChange,
59
57
  orientation = "horizontal",
60
58
  activationMode = "automatic",
59
+ focusableWhenDisabled: focusableWhenDisabledProp = false,
61
60
  className,
62
61
  children,
63
62
  render,
@@ -80,12 +79,16 @@ function TabsRoot({
80
79
  const [tabs, setTabs] = useState([]);
81
80
  const register = useCallback((entry) => {
82
81
  setTabs(
83
- (current) => [...current.filter((t) => t.value !== entry.value), entry].sort(documentOrder)
82
+ (current) => [...current.filter((t) => t.value !== entry.value), entry].sort(byDocumentOrder)
84
83
  );
85
84
  return () => setTabs((current) => current.filter((t) => t !== entry));
86
85
  }, []);
87
- const enabledTabs = useMemo(() => tabs.filter((t) => !t.disabled), [tabs]);
88
- const tabStopValue = tabs.length === 0 ? value : (enabledTabs.find((t) => t.value === value) ?? enabledTabs[0])?.value;
86
+ const focusableWhenDisabled = focusableWhenDisabledProp && activationMode === "manual";
87
+ const navigableTabs = useMemo(
88
+ () => focusableWhenDisabled ? tabs : tabs.filter((t) => !t.disabled),
89
+ [tabs, focusableWhenDisabled]
90
+ );
91
+ const tabStopValue = tabs.length === 0 ? value : (navigableTabs.find((t) => t.value === value) ?? navigableTabs[0])?.value;
89
92
  const baseId = useId();
90
93
  const context = useMemo(
91
94
  () => ({
@@ -93,13 +96,24 @@ function TabsRoot({
93
96
  setValue,
94
97
  orientation,
95
98
  activationMode,
96
- enabledTabs,
99
+ navigableTabs,
100
+ focusableWhenDisabled,
97
101
  tabStopValue,
98
102
  register,
99
103
  tabId: (v) => `${baseId}-tab-${idPart(v)}`,
100
104
  panelId: (v) => `${baseId}-panel-${idPart(v)}`
101
105
  }),
102
- [value, setValue, orientation, activationMode, enabledTabs, tabStopValue, register, baseId]
106
+ [
107
+ value,
108
+ setValue,
109
+ orientation,
110
+ activationMode,
111
+ navigableTabs,
112
+ focusableWhenDisabled,
113
+ tabStopValue,
114
+ register,
115
+ baseId
116
+ ]
103
117
  );
104
118
  const element = useRender({
105
119
  render,
@@ -112,33 +126,8 @@ function TabsRoot({
112
126
 
113
127
  // src/tabs/TabsList.tsx
114
128
  import { useRef } from "react";
115
-
116
- // src/core/rovingFocus.ts
117
- function rovingIndex(key, {
118
- count,
119
- current,
120
- orientation,
121
- rtl
122
- }) {
123
- if (count === 0) return null;
124
- const [prevKey, nextKey] = orientation === "vertical" ? ["ArrowUp", "ArrowDown"] : rtl ? ["ArrowRight", "ArrowLeft"] : ["ArrowLeft", "ArrowRight"];
125
- switch (key) {
126
- case nextKey:
127
- return current < 0 ? 0 : (current + 1) % count;
128
- case prevKey:
129
- return current < 0 ? count - 1 : (current - 1 + count) % count;
130
- case "Home":
131
- return 0;
132
- case "End":
133
- return count - 1;
134
- default:
135
- return null;
136
- }
137
- }
138
-
139
- // src/tabs/TabsList.tsx
140
129
  function TabsList({ className, children, render, ...rest }) {
141
- const { orientation, enabledTabs } = useTabsRootContext("List");
130
+ const { orientation, navigableTabs } = useTabsRootContext("List");
142
131
  const elementRef = useRef(null);
143
132
  return useRender({
144
133
  render,
@@ -152,19 +141,19 @@ function TabsList({ className, children, render, ...rest }) {
152
141
  children,
153
142
  ref: elementRef,
154
143
  onKeyDown(event) {
155
- const current = enabledTabs.findIndex((t) => t.ref.current === event.target);
144
+ const current = navigableTabs.findIndex((t) => t.ref.current === event.target);
156
145
  if (current < 0) return;
157
146
  const list = elementRef.current;
158
147
  const rtl = list ? getComputedStyle(list).direction === "rtl" : false;
159
148
  const next = rovingIndex(event.key, {
160
- count: enabledTabs.length,
149
+ count: navigableTabs.length,
161
150
  current,
162
151
  orientation,
163
152
  rtl
164
153
  });
165
154
  if (next === null) return;
166
155
  event.preventDefault();
167
- enabledTabs[next]?.ref.current?.focus();
156
+ navigableTabs[next]?.ref.current?.focus();
168
157
  }
169
158
  },
170
159
  consumerProps: rest
@@ -176,6 +165,7 @@ import { useLayoutEffect, useRef as useRef2 } from "react";
176
165
  function TabsTab({
177
166
  value,
178
167
  disabled = false,
168
+ onClick,
179
169
  className,
180
170
  children,
181
171
  render,
@@ -185,6 +175,7 @@ function TabsTab({
185
175
  value: selectedValue,
186
176
  setValue,
187
177
  activationMode,
178
+ focusableWhenDisabled,
188
179
  tabStopValue,
189
180
  register,
190
181
  tabId,
@@ -206,8 +197,10 @@ function TabsTab({
206
197
  "aria-selected": selected,
207
198
  "aria-controls": panelId(value),
208
199
  tabIndex: value === tabStopValue ? 0 : -1,
209
- // The platform keeps a disabled button out of focus and activation.
210
- disabled: disabled || void 0,
200
+ // The platform keeps a disabled button out of focus and activation. Focusable when
201
+ // disabled, it is aria-disabled instead, and activation is blocked here.
202
+ disabled: disabled && !focusableWhenDisabled || void 0,
203
+ "aria-disabled": disabled && focusableWhenDisabled || void 0,
211
204
  ...selectedDataAttributes(selected, disabled),
212
205
  className,
213
206
  children,
@@ -219,7 +212,8 @@ function TabsTab({
219
212
  if (activationMode === "automatic" && !disabled) setValue(value);
220
213
  }
221
214
  },
222
- consumerProps: rest
215
+ // A native `disabled` would block your onClick; aria-disabled does not, so it is left off.
216
+ consumerProps: disabled ? rest : { ...rest, onClick }
223
217
  });
224
218
  }
225
219
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arun-dev/headless",
3
- "version": "4.6.0",
3
+ "version": "4.7.0",
4
4
  "description": "Unstyled React behaviour primitives — render engine, controlled/uncontrolled state, and data-* state projection. Ships no CSS and no class names.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -64,6 +64,16 @@
64
64
  "default": "./dist/dialog/index.cjs"
65
65
  }
66
66
  },
67
+ "./menu": {
68
+ "import": {
69
+ "types": "./dist/menu/index.d.ts",
70
+ "default": "./dist/menu/index.js"
71
+ },
72
+ "require": {
73
+ "types": "./dist/menu/index.d.cts",
74
+ "default": "./dist/menu/index.cjs"
75
+ }
76
+ },
67
77
  "./popover": {
68
78
  "import": {
69
79
  "types": "./dist/popover/index.d.ts",