@tamagui/roving-focus 1.88.12 → 1.89.0-1706308641099

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.
@@ -0,0 +1,184 @@
1
+ import { createCollection } from "@tamagui/collection";
2
+ import { useComposedRefs } from "@tamagui/compose-refs";
3
+ import { isWeb } from "@tamagui/constants";
4
+ import { Stack, createStyledContext, useEvent } from "@tamagui/core";
5
+ import { composeEventHandlers, withStaticProperties } from "@tamagui/helpers";
6
+ import { useControllableState } from "@tamagui/use-controllable-state";
7
+ import { useDirection } from "@tamagui/use-direction";
8
+ import * as React from "react";
9
+ import { jsx } from "react/jsx-runtime";
10
+ const ENTRY_FOCUS = "rovingFocusGroup.onEntryFocus",
11
+ EVENT_OPTIONS = {
12
+ bubbles: !1,
13
+ cancelable: !0
14
+ },
15
+ RovingFocusGroupImpl = React.forwardRef((props, forwardedRef) => {
16
+ const {
17
+ __scopeRovingFocusGroup,
18
+ orientation,
19
+ loop = !1,
20
+ dir,
21
+ currentTabStopId: currentTabStopIdProp,
22
+ defaultCurrentTabStopId,
23
+ onCurrentTabStopIdChange,
24
+ onEntryFocus,
25
+ ...groupProps
26
+ } = props,
27
+ ref = React.useRef(null),
28
+ composedRefs = useComposedRefs(forwardedRef, ref),
29
+ direction = useDirection(dir),
30
+ [currentTabStopId = null, setCurrentTabStopId] = useControllableState({
31
+ prop: currentTabStopIdProp,
32
+ defaultProp: defaultCurrentTabStopId ?? null,
33
+ onChange: onCurrentTabStopIdChange
34
+ }),
35
+ [isTabbingBackOut, setIsTabbingBackOut] = React.useState(!1),
36
+ handleEntryFocus = useEvent(onEntryFocus),
37
+ getItems = useCollection(__scopeRovingFocusGroup || ROVING_FOCUS_GROUP_CONTEXT),
38
+ isClickFocusRef = React.useRef(!1),
39
+ [focusableItemsCount, setFocusableItemsCount] = React.useState(0);
40
+ return React.useEffect(() => {
41
+ const node = ref.current;
42
+ if (node) return node.addEventListener(ENTRY_FOCUS, handleEntryFocus), () => node.removeEventListener(ENTRY_FOCUS, handleEntryFocus);
43
+ }, [handleEntryFocus]), /* @__PURE__ */jsx(RovingFocusProvider, {
44
+ scope: __scopeRovingFocusGroup,
45
+ orientation,
46
+ dir: direction,
47
+ loop,
48
+ currentTabStopId,
49
+ onItemFocus: React.useCallback(tabStopId => setCurrentTabStopId(tabStopId), [setCurrentTabStopId]),
50
+ onItemShiftTab: React.useCallback(() => setIsTabbingBackOut(!0), []),
51
+ onFocusableItemAdd: React.useCallback(() => setFocusableItemsCount(prevCount => prevCount + 1), []),
52
+ onFocusableItemRemove: React.useCallback(() => setFocusableItemsCount(prevCount => prevCount - 1), []),
53
+ children: /* @__PURE__ */jsx(Stack, {
54
+ tabIndex: isTabbingBackOut || focusableItemsCount === 0 ? -1 : 0,
55
+ "data-orientation": orientation,
56
+ ...groupProps,
57
+ ref: composedRefs,
58
+ style: [{
59
+ outline: "none"
60
+ }, props.style],
61
+ onMouseDown: composeEventHandlers(props.onMouseDown, () => {
62
+ isClickFocusRef.current = !0;
63
+ }),
64
+ onFocus: composeEventHandlers(props.onFocus, event => {
65
+ const isKeyboardFocus = !isClickFocusRef.current;
66
+ if (event.target === event.currentTarget && isKeyboardFocus && !isTabbingBackOut) {
67
+ const entryFocusEvent = new CustomEvent(ENTRY_FOCUS, EVENT_OPTIONS);
68
+ if (event.currentTarget.dispatchEvent(entryFocusEvent), !entryFocusEvent.defaultPrevented) {
69
+ const items = getItems().filter(item => item.focusable),
70
+ activeItem = items.find(item => item.active),
71
+ currentItem = items.find(item => item.id === currentTabStopId),
72
+ candidateNodes = [activeItem, currentItem, ...items].filter(Boolean).map(item => item.ref.current);
73
+ focusFirst(candidateNodes);
74
+ }
75
+ }
76
+ isClickFocusRef.current = !1;
77
+ }),
78
+ onBlur: composeEventHandlers(props.onBlur, () => setIsTabbingBackOut(!1))
79
+ })
80
+ });
81
+ }),
82
+ ITEM_NAME = "RovingFocusGroupItem",
83
+ RovingFocusGroupItem = React.forwardRef((props, forwardedRef) => {
84
+ const {
85
+ __scopeRovingFocusGroup,
86
+ focusable = !0,
87
+ active = !1,
88
+ tabStopId,
89
+ ...itemProps
90
+ } = props,
91
+ autoId = React.useId(),
92
+ id = tabStopId || autoId,
93
+ context = useRovingFocusContext(__scopeRovingFocusGroup),
94
+ isCurrentTabStop = context.currentTabStopId === id,
95
+ getItems = useCollection(__scopeRovingFocusGroup || ROVING_FOCUS_GROUP_CONTEXT),
96
+ {
97
+ onFocusableItemAdd,
98
+ onFocusableItemRemove
99
+ } = context;
100
+ return React.useEffect(() => {
101
+ if (focusable) return onFocusableItemAdd(), () => onFocusableItemRemove();
102
+ }, [focusable, onFocusableItemAdd, onFocusableItemRemove]), /* @__PURE__ */jsx(Collection.ItemSlot, {
103
+ __scopeCollection: __scopeRovingFocusGroup || ROVING_FOCUS_GROUP_CONTEXT,
104
+ id,
105
+ focusable,
106
+ active,
107
+ children: /* @__PURE__ */jsx(Stack, {
108
+ tabIndex: isCurrentTabStop ? 0 : -1,
109
+ "data-orientation": context.orientation,
110
+ ...itemProps,
111
+ ref: forwardedRef,
112
+ onMouseDown: composeEventHandlers(props.onMouseDown, event => {
113
+ focusable ? context.onItemFocus(id) : event.preventDefault();
114
+ }),
115
+ onFocus: composeEventHandlers(props.onFocus, () => context.onItemFocus(id)),
116
+ ...(isWeb && {
117
+ onKeyDown: composeEventHandlers(props.onKeyDown, event => {
118
+ if (event.key === "Tab" && event.shiftKey) {
119
+ context.onItemShiftTab();
120
+ return;
121
+ }
122
+ if (event.target !== event.currentTarget) return;
123
+ const focusIntent = getFocusIntent(event, context.orientation, context.dir);
124
+ if (focusIntent !== void 0) {
125
+ event.preventDefault();
126
+ let candidateNodes = getItems().filter(item => item.focusable).map(item => item.ref.current);
127
+ if (focusIntent === "last") candidateNodes.reverse();else if (focusIntent === "prev" || focusIntent === "next") {
128
+ focusIntent === "prev" && candidateNodes.reverse();
129
+ const currentIndex = candidateNodes.indexOf(event.currentTarget);
130
+ candidateNodes = context.loop ? wrapArray(candidateNodes, currentIndex + 1) : candidateNodes.slice(currentIndex + 1);
131
+ }
132
+ setTimeout(() => focusFirst(candidateNodes));
133
+ }
134
+ })
135
+ })
136
+ })
137
+ });
138
+ });
139
+ RovingFocusGroupItem.displayName = ITEM_NAME;
140
+ const GROUP_NAME = "RovingFocusGroup",
141
+ [Collection, useCollection] = createCollection(GROUP_NAME),
142
+ {
143
+ Provider: RovingFocusProvider,
144
+ useStyledContext: useRovingFocusContext
145
+ } = createStyledContext(),
146
+ ROVING_FOCUS_GROUP_CONTEXT = "RovingFocusGroupContext",
147
+ RovingFocusGroup = withStaticProperties(React.forwardRef((props, forwardedRef) => /* @__PURE__ */jsx(Collection.Provider, {
148
+ __scopeCollection: props.__scopeRovingFocusGroup || ROVING_FOCUS_GROUP_CONTEXT,
149
+ children: /* @__PURE__ */jsx(Collection.Slot, {
150
+ __scopeCollection: props.__scopeRovingFocusGroup || ROVING_FOCUS_GROUP_CONTEXT,
151
+ children: /* @__PURE__ */jsx(RovingFocusGroupImpl, {
152
+ ...props,
153
+ ref: forwardedRef
154
+ })
155
+ })
156
+ })), {
157
+ Item: RovingFocusGroupItem
158
+ });
159
+ RovingFocusGroup.displayName = GROUP_NAME;
160
+ const MAP_KEY_TO_FOCUS_INTENT = {
161
+ ArrowLeft: "prev",
162
+ ArrowUp: "prev",
163
+ ArrowRight: "next",
164
+ ArrowDown: "next",
165
+ PageUp: "first",
166
+ Home: "first",
167
+ PageDown: "last",
168
+ End: "last"
169
+ };
170
+ function getDirectionAwareKey(key, dir) {
171
+ return dir !== "rtl" ? key : key === "ArrowLeft" ? "ArrowRight" : key === "ArrowRight" ? "ArrowLeft" : key;
172
+ }
173
+ function getFocusIntent(event, orientation, dir) {
174
+ const key = getDirectionAwareKey(event.key, dir);
175
+ if (!(orientation === "vertical" && ["ArrowLeft", "ArrowRight"].includes(key)) && !(orientation === "horizontal" && ["ArrowUp", "ArrowDown"].includes(key))) return MAP_KEY_TO_FOCUS_INTENT[key];
176
+ }
177
+ function focusFirst(candidates) {
178
+ const PREVIOUSLY_FOCUSED_ELEMENT = document.activeElement;
179
+ for (const candidate of candidates) if (candidate === PREVIOUSLY_FOCUSED_ELEMENT || (candidate.focus(), document.activeElement !== PREVIOUSLY_FOCUSED_ELEMENT)) return;
180
+ }
181
+ function wrapArray(array, startIndex) {
182
+ return array.map((_, index) => array[(startIndex + index) % array.length]);
183
+ }
184
+ export { RovingFocusGroup };
@@ -0,0 +1 @@
1
+ export * from "./RovingFocusGroup.mjs";
@@ -0,0 +1,184 @@
1
+ import { createCollection } from "@tamagui/collection";
2
+ import { useComposedRefs } from "@tamagui/compose-refs";
3
+ import { isWeb } from "@tamagui/constants";
4
+ import { Stack, createStyledContext, useEvent } from "@tamagui/core";
5
+ import { composeEventHandlers, withStaticProperties } from "@tamagui/helpers";
6
+ import { useControllableState } from "@tamagui/use-controllable-state";
7
+ import { useDirection } from "@tamagui/use-direction";
8
+ import * as React from "react";
9
+ import { jsx } from "react/jsx-runtime";
10
+ const ENTRY_FOCUS = "rovingFocusGroup.onEntryFocus",
11
+ EVENT_OPTIONS = {
12
+ bubbles: !1,
13
+ cancelable: !0
14
+ },
15
+ RovingFocusGroupImpl = React.forwardRef((props, forwardedRef) => {
16
+ const {
17
+ __scopeRovingFocusGroup,
18
+ orientation,
19
+ loop = !1,
20
+ dir,
21
+ currentTabStopId: currentTabStopIdProp,
22
+ defaultCurrentTabStopId,
23
+ onCurrentTabStopIdChange,
24
+ onEntryFocus,
25
+ ...groupProps
26
+ } = props,
27
+ ref = React.useRef(null),
28
+ composedRefs = useComposedRefs(forwardedRef, ref),
29
+ direction = useDirection(dir),
30
+ [currentTabStopId = null, setCurrentTabStopId] = useControllableState({
31
+ prop: currentTabStopIdProp,
32
+ defaultProp: defaultCurrentTabStopId ?? null,
33
+ onChange: onCurrentTabStopIdChange
34
+ }),
35
+ [isTabbingBackOut, setIsTabbingBackOut] = React.useState(!1),
36
+ handleEntryFocus = useEvent(onEntryFocus),
37
+ getItems = useCollection(__scopeRovingFocusGroup || ROVING_FOCUS_GROUP_CONTEXT),
38
+ isClickFocusRef = React.useRef(!1),
39
+ [focusableItemsCount, setFocusableItemsCount] = React.useState(0);
40
+ return React.useEffect(() => {
41
+ const node = ref.current;
42
+ if (node) return node.addEventListener(ENTRY_FOCUS, handleEntryFocus), () => node.removeEventListener(ENTRY_FOCUS, handleEntryFocus);
43
+ }, [handleEntryFocus]), /* @__PURE__ */jsx(RovingFocusProvider, {
44
+ scope: __scopeRovingFocusGroup,
45
+ orientation,
46
+ dir: direction,
47
+ loop,
48
+ currentTabStopId,
49
+ onItemFocus: React.useCallback(tabStopId => setCurrentTabStopId(tabStopId), [setCurrentTabStopId]),
50
+ onItemShiftTab: React.useCallback(() => setIsTabbingBackOut(!0), []),
51
+ onFocusableItemAdd: React.useCallback(() => setFocusableItemsCount(prevCount => prevCount + 1), []),
52
+ onFocusableItemRemove: React.useCallback(() => setFocusableItemsCount(prevCount => prevCount - 1), []),
53
+ children: /* @__PURE__ */jsx(Stack, {
54
+ tabIndex: isTabbingBackOut || focusableItemsCount === 0 ? -1 : 0,
55
+ "data-orientation": orientation,
56
+ ...groupProps,
57
+ ref: composedRefs,
58
+ style: [{
59
+ outline: "none"
60
+ }, props.style],
61
+ onMouseDown: composeEventHandlers(props.onMouseDown, () => {
62
+ isClickFocusRef.current = !0;
63
+ }),
64
+ onFocus: composeEventHandlers(props.onFocus, event => {
65
+ const isKeyboardFocus = !isClickFocusRef.current;
66
+ if (event.target === event.currentTarget && isKeyboardFocus && !isTabbingBackOut) {
67
+ const entryFocusEvent = new CustomEvent(ENTRY_FOCUS, EVENT_OPTIONS);
68
+ if (event.currentTarget.dispatchEvent(entryFocusEvent), !entryFocusEvent.defaultPrevented) {
69
+ const items = getItems().filter(item => item.focusable),
70
+ activeItem = items.find(item => item.active),
71
+ currentItem = items.find(item => item.id === currentTabStopId),
72
+ candidateNodes = [activeItem, currentItem, ...items].filter(Boolean).map(item => item.ref.current);
73
+ focusFirst(candidateNodes);
74
+ }
75
+ }
76
+ isClickFocusRef.current = !1;
77
+ }),
78
+ onBlur: composeEventHandlers(props.onBlur, () => setIsTabbingBackOut(!1))
79
+ })
80
+ });
81
+ }),
82
+ ITEM_NAME = "RovingFocusGroupItem",
83
+ RovingFocusGroupItem = React.forwardRef((props, forwardedRef) => {
84
+ const {
85
+ __scopeRovingFocusGroup,
86
+ focusable = !0,
87
+ active = !1,
88
+ tabStopId,
89
+ ...itemProps
90
+ } = props,
91
+ autoId = React.useId(),
92
+ id = tabStopId || autoId,
93
+ context = useRovingFocusContext(__scopeRovingFocusGroup),
94
+ isCurrentTabStop = context.currentTabStopId === id,
95
+ getItems = useCollection(__scopeRovingFocusGroup || ROVING_FOCUS_GROUP_CONTEXT),
96
+ {
97
+ onFocusableItemAdd,
98
+ onFocusableItemRemove
99
+ } = context;
100
+ return React.useEffect(() => {
101
+ if (focusable) return onFocusableItemAdd(), () => onFocusableItemRemove();
102
+ }, [focusable, onFocusableItemAdd, onFocusableItemRemove]), /* @__PURE__ */jsx(Collection.ItemSlot, {
103
+ __scopeCollection: __scopeRovingFocusGroup || ROVING_FOCUS_GROUP_CONTEXT,
104
+ id,
105
+ focusable,
106
+ active,
107
+ children: /* @__PURE__ */jsx(Stack, {
108
+ tabIndex: isCurrentTabStop ? 0 : -1,
109
+ "data-orientation": context.orientation,
110
+ ...itemProps,
111
+ ref: forwardedRef,
112
+ onMouseDown: composeEventHandlers(props.onMouseDown, event => {
113
+ focusable ? context.onItemFocus(id) : event.preventDefault();
114
+ }),
115
+ onFocus: composeEventHandlers(props.onFocus, () => context.onItemFocus(id)),
116
+ ...(isWeb && {
117
+ onKeyDown: composeEventHandlers(props.onKeyDown, event => {
118
+ if (event.key === "Tab" && event.shiftKey) {
119
+ context.onItemShiftTab();
120
+ return;
121
+ }
122
+ if (event.target !== event.currentTarget) return;
123
+ const focusIntent = getFocusIntent(event, context.orientation, context.dir);
124
+ if (focusIntent !== void 0) {
125
+ event.preventDefault();
126
+ let candidateNodes = getItems().filter(item => item.focusable).map(item => item.ref.current);
127
+ if (focusIntent === "last") candidateNodes.reverse();else if (focusIntent === "prev" || focusIntent === "next") {
128
+ focusIntent === "prev" && candidateNodes.reverse();
129
+ const currentIndex = candidateNodes.indexOf(event.currentTarget);
130
+ candidateNodes = context.loop ? wrapArray(candidateNodes, currentIndex + 1) : candidateNodes.slice(currentIndex + 1);
131
+ }
132
+ setTimeout(() => focusFirst(candidateNodes));
133
+ }
134
+ })
135
+ })
136
+ })
137
+ });
138
+ });
139
+ RovingFocusGroupItem.displayName = ITEM_NAME;
140
+ const GROUP_NAME = "RovingFocusGroup",
141
+ [Collection, useCollection] = createCollection(GROUP_NAME),
142
+ {
143
+ Provider: RovingFocusProvider,
144
+ useStyledContext: useRovingFocusContext
145
+ } = createStyledContext(),
146
+ ROVING_FOCUS_GROUP_CONTEXT = "RovingFocusGroupContext",
147
+ RovingFocusGroup = withStaticProperties(React.forwardRef((props, forwardedRef) => /* @__PURE__ */jsx(Collection.Provider, {
148
+ __scopeCollection: props.__scopeRovingFocusGroup || ROVING_FOCUS_GROUP_CONTEXT,
149
+ children: /* @__PURE__ */jsx(Collection.Slot, {
150
+ __scopeCollection: props.__scopeRovingFocusGroup || ROVING_FOCUS_GROUP_CONTEXT,
151
+ children: /* @__PURE__ */jsx(RovingFocusGroupImpl, {
152
+ ...props,
153
+ ref: forwardedRef
154
+ })
155
+ })
156
+ })), {
157
+ Item: RovingFocusGroupItem
158
+ });
159
+ RovingFocusGroup.displayName = GROUP_NAME;
160
+ const MAP_KEY_TO_FOCUS_INTENT = {
161
+ ArrowLeft: "prev",
162
+ ArrowUp: "prev",
163
+ ArrowRight: "next",
164
+ ArrowDown: "next",
165
+ PageUp: "first",
166
+ Home: "first",
167
+ PageDown: "last",
168
+ End: "last"
169
+ };
170
+ function getDirectionAwareKey(key, dir) {
171
+ return dir !== "rtl" ? key : key === "ArrowLeft" ? "ArrowRight" : key === "ArrowRight" ? "ArrowLeft" : key;
172
+ }
173
+ function getFocusIntent(event, orientation, dir) {
174
+ const key = getDirectionAwareKey(event.key, dir);
175
+ if (!(orientation === "vertical" && ["ArrowLeft", "ArrowRight"].includes(key)) && !(orientation === "horizontal" && ["ArrowUp", "ArrowDown"].includes(key))) return MAP_KEY_TO_FOCUS_INTENT[key];
176
+ }
177
+ function focusFirst(candidates) {
178
+ const PREVIOUSLY_FOCUSED_ELEMENT = document.activeElement;
179
+ for (const candidate of candidates) if (candidate === PREVIOUSLY_FOCUSED_ELEMENT || (candidate.focus(), document.activeElement !== PREVIOUSLY_FOCUSED_ELEMENT)) return;
180
+ }
181
+ function wrapArray(array, startIndex) {
182
+ return array.map((_, index) => array[(startIndex + index) % array.length]);
183
+ }
184
+ export { RovingFocusGroup };
@@ -0,0 +1 @@
1
+ export * from "./RovingFocusGroup.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tamagui/roving-focus",
3
- "version": "1.88.12",
3
+ "version": "1.89.0-1706308641099",
4
4
  "sideEffects": [
5
5
  "*.css"
6
6
  ],
@@ -32,21 +32,21 @@
32
32
  }
33
33
  },
34
34
  "dependencies": {
35
- "@tamagui/collection": "1.88.12",
36
- "@tamagui/compose-refs": "1.88.12",
37
- "@tamagui/constants": "1.88.12",
38
- "@tamagui/core": "1.88.12",
39
- "@tamagui/create-context": "1.88.12",
40
- "@tamagui/helpers": "1.88.12",
41
- "@tamagui/use-controllable-state": "1.88.12",
42
- "@tamagui/use-direction": "1.88.12",
43
- "@tamagui/use-event": "1.88.12"
35
+ "@tamagui/collection": "1.89.0-1706308641099",
36
+ "@tamagui/compose-refs": "1.89.0-1706308641099",
37
+ "@tamagui/constants": "1.89.0-1706308641099",
38
+ "@tamagui/core": "1.89.0-1706308641099",
39
+ "@tamagui/create-context": "1.89.0-1706308641099",
40
+ "@tamagui/helpers": "1.89.0-1706308641099",
41
+ "@tamagui/use-controllable-state": "1.89.0-1706308641099",
42
+ "@tamagui/use-direction": "1.89.0-1706308641099",
43
+ "@tamagui/use-event": "1.89.0-1706308641099"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "react": "*"
47
47
  },
48
48
  "devDependencies": {
49
- "@tamagui/build": "1.88.12",
49
+ "@tamagui/build": "1.89.0-1706308641099",
50
50
  "react": "^18.2.0"
51
51
  },
52
52
  "publishConfig": {