@bento/focus-lock 0.0.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/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 GoDaddy Operating Company, LLC.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,136 @@
1
+ # FocusLock
2
+
3
+ The `@bento/focus-lock` package provides focus management for containing and
4
+ controlling keyboard focus within specific areas of your application. Built on
5
+ top of React ARIA's FocusScope, it ensures focus remains trapped within
6
+ designated boundaries, making it essential for modals, dialogs, drawers, select
7
+ popovers, and other overlay components.
8
+
9
+ ## Installation
10
+
11
+ ```shell
12
+ npm install --save @bento/focus-lock
13
+ ```
14
+
15
+ ## Props
16
+
17
+ The following properties are available to be used on the `FocusLock` component:
18
+
19
+ | Prop | Type | Required | Description |
20
+ |------|------|----------|------------|
21
+ | `contain` | `boolean` | No | Whether to contain focus within the scope.
22
+ When true, focus will cycle between focusable elements within the scope. |
23
+ | `restoreFocus` | `boolean` | No | Whether to restore focus to the previously focused element when the focus scope unmounts. |
24
+ | `autoFocus` | `boolean` | No | Whether to automatically focus the first focusable element when the focus scope mounts. |
25
+ | `children` | `ReactNode` | No | The content to render inside the focus lock.
26
+ Can be a single element or multiple elements. |
27
+ | `onFocusEnter` | `(e: FocusEvent<Element, Element>) => void` | No | Callback fired when focus enters the scope |
28
+ | `onFocusLeave` | `(e: FocusEvent<Element, Element>) => void` | No | Callback fired when focus leaves the scope |
29
+ | `className` | `string \| ((state: FocusLockState) => string)` | No | Render prop for className |
30
+ | `style` | `((state: FocusLockState) => CSSProperties) \| CSSProperties` | No | Render prop for style |
31
+ | `slot` | `string` | No | A named part of a component that can be customized. This is implemented by the consuming component.
32
+ The exposed slot names of a component are available in the components documentation. |
33
+ | `slots` | `Record<string, object \| Function>` | No | An object that contains the customizations for the slots.
34
+ The main way you interact with the slot system as a consumer. |
35
+
36
+ For all other properties specified on the `FocusLock` component, they will be
37
+ passed down to the underlying React ARIA FocusScope component.
38
+
39
+ ## Examples
40
+
41
+ The simplest use case is to wrap your modal or dialog content with `FocusLock`
42
+ and enable focus containment.
43
+
44
+ <Source language='tsx' code={ SourceBasic } />
45
+
46
+ Focus scopes can be nested, allowing you to have multiple layers of focus
47
+ containment. When a nested scope is active, focus is trapped within the
48
+ innermost scope.
49
+
50
+ <Source language='tsx' code={ SourceNested } />
51
+
52
+ The `FocusLock` component applies data attributes directly to its children
53
+ without introducing a wrapper element. This example demonstrates multiple
54
+ children (backdrop and content).
55
+
56
+ <Source language='tsx' code={ SourceOverlay } />
57
+
58
+ When used with a single child, the focus lock applies data attributes to that child element.
59
+
60
+ <Source language='tsx' code={ SourceSelect } />
61
+
62
+ Focus lock is particularly useful for multi-step forms where you want to keep focus within the current step.
63
+
64
+ <Source language='tsx' code={ SourceForm } />
65
+
66
+ ## Customization
67
+
68
+ The `FocusLock` component is created using the `@bento/slots` package and allows
69
+ assignment of the custom `slot` property for overrides. The component applies
70
+ props to the underlying React ARIA `FocusScope` component and data attributes to
71
+ its children.
72
+
73
+ ### Slots
74
+
75
+ The `@bento/focus-lock` component is registered as `BentoFocusLock` and can be
76
+ customized using the slot system. See the `@bento/slots` package for more
77
+ information on how to use the `slot` and `slots` properties.
78
+
79
+ Render prop function receives a state object with the following properties:
80
+
81
+ ```typescript
82
+ interface FocusLockState {
83
+ hasFocus: boolean; // Whether focus is currently within the scope
84
+ isContained: boolean; // Whether focus is contained (same as contain prop)
85
+ }
86
+ ```
87
+
88
+ ### Data Attributes
89
+
90
+ The following data attributes are automatically applied to the children of the `FocusLock` component:
91
+
92
+ | Attribute | Description | Example Values |
93
+ | ---------------------- | ------------------------------------------------ | --------------- |
94
+ | `data-focus-contained` | Indicates whether focus is contained | "true" / "false"|
95
+ | `data-has-focus` | Indicates whether the scope currently has focus | "true" / "false"|
96
+
97
+ These data attributes can be targeted using CSS selectors for styling. When
98
+ using data attributes for styling, ensure you scope them properly with a
99
+ className to avoid affecting unrelated elements:
100
+
101
+ ```css
102
+ .my-modal[data-focus-contained="true"] {
103
+ outline: 2px solid blue;
104
+ }
105
+
106
+ .my-modal[data-has-focus="true"] {
107
+ background-color: rgba(0, 0, 0, 0.05);
108
+ }
109
+ ```
110
+
111
+ Apply the scoping className to your FocusLock children:
112
+
113
+ ```tsx
114
+ <FocusLock contain restoreFocus autoFocus>
115
+ <div className="my-modal">
116
+ Modal content
117
+ </div>
118
+ </FocusLock>
119
+ ```
120
+
121
+ ## Accessibility
122
+
123
+ Focus management is crucial for accessibility. The `FocusLock` component ensures
124
+ that keyboard users can navigate within the focus scope using Tab and Shift+Tab,
125
+ focus is trapped within the scope when `contain` is enabled, focus is
126
+ automatically restored to the previously focused element when the scope is
127
+ removed (when `restoreFocus` is enabled), and the first focusable element is
128
+ automatically focused when the scope is mounted (when `autoFocus` is enabled).
129
+
130
+ When using focus lock, follow these accessibility guidelines:
131
+
132
+ - Always provide a way to exit the focus scope (e.g., a close button or escape key handler)
133
+ - Use `restoreFocus` to ensure users return to their previous location when the scope is closed
134
+ - Use `autoFocus` to immediately draw attention to important content like modals
135
+ - Consider using `aria-modal` on modal dialogs to provide additional context to screen readers
136
+ - Ensure all focusable elements within the scope are keyboard accessible
package/README.mdx ADDED
@@ -0,0 +1,142 @@
1
+ import { Meta, Story, ArgTypes, Controls, Source } from '@storybook/addon-docs/blocks';
2
+ import * as Stories from './focus-lock.stories.tsx';
3
+
4
+ import SourceBasic from './examples/basic.tsx?raw';
5
+ import SourceNested from './examples/nested.tsx?raw';
6
+ import SourceOverlay from './examples/overlay.tsx?raw';
7
+ import SourceSelect from './examples/select.tsx?raw';
8
+ import SourceForm from './examples/form.tsx?raw';
9
+
10
+ <Meta of={Stories} name="Overview" />
11
+
12
+ # FocusLock
13
+
14
+ The `@bento/focus-lock` package provides focus management for containing and
15
+ controlling keyboard focus within specific areas of your application. Built on
16
+ top of React ARIA's FocusScope, it ensures focus remains trapped within
17
+ designated boundaries, making it essential for modals, dialogs, drawers, select
18
+ popovers, and other overlay components.
19
+
20
+ ## Installation
21
+
22
+ ```shell
23
+ npm install --save @bento/focus-lock
24
+ ```
25
+
26
+ ## Props
27
+
28
+ The following properties are available to be used on the `FocusLock` component:
29
+
30
+ <ArgTypes of={Stories.Props} />
31
+
32
+ For all other properties specified on the `FocusLock` component, they will be
33
+ passed down to the underlying React ARIA FocusScope component.
34
+
35
+ ## Examples
36
+
37
+ The simplest use case is to wrap your modal or dialog content with `FocusLock`
38
+ and enable focus containment.
39
+
40
+ <Source language='tsx' code={ SourceBasic } />
41
+ <Story of={Stories.Basic} inline />
42
+ <Controls of={Stories.Basic} />
43
+
44
+ Focus scopes can be nested, allowing you to have multiple layers of focus
45
+ containment. When a nested scope is active, focus is trapped within the
46
+ innermost scope.
47
+
48
+ <Source language='tsx' code={ SourceNested } />
49
+ <Story of={Stories.Nested} inline />
50
+ <Controls of={Stories.Nested} />
51
+
52
+ The `FocusLock` component applies data attributes directly to its children
53
+ without introducing a wrapper element. This example demonstrates multiple
54
+ children (backdrop and content).
55
+
56
+ <Source language='tsx' code={ SourceOverlay } />
57
+ <Story of={Stories.Overlay} inline />
58
+ <Controls of={Stories.Overlay} />
59
+
60
+ When used with a single child, the focus lock applies data attributes to that child element.
61
+
62
+ <Source language='tsx' code={ SourceSelect } />
63
+ <Story of={Stories.Select} inline />
64
+ <Controls of={Stories.Select} />
65
+
66
+ Focus lock is particularly useful for multi-step forms where you want to keep focus within the current step.
67
+
68
+ <Source language='tsx' code={ SourceForm } />
69
+ <Story of={Stories.Form} inline />
70
+ <Controls of={Stories.Form} />
71
+
72
+ ## Customization
73
+
74
+ The `FocusLock` component is created using the `@bento/slots` package and allows
75
+ assignment of the custom `slot` property for overrides. The component applies
76
+ props to the underlying React ARIA `FocusScope` component and data attributes to
77
+ its children.
78
+
79
+ ### Slots
80
+
81
+ The `@bento/focus-lock` component is registered as `BentoFocusLock` and can be
82
+ customized using the slot system. See the `@bento/slots` package for more
83
+ information on how to use the `slot` and `slots` properties.
84
+
85
+ Render prop function receives a state object with the following properties:
86
+
87
+ ```typescript
88
+ interface FocusLockState {
89
+ hasFocus: boolean; // Whether focus is currently within the scope
90
+ isContained: boolean; // Whether focus is contained (same as contain prop)
91
+ }
92
+ ```
93
+
94
+ ### Data Attributes
95
+
96
+ The following data attributes are automatically applied to the children of the `FocusLock` component:
97
+
98
+ | Attribute | Description | Example Values |
99
+ | ---------------------- | ------------------------------------------------ | --------------- |
100
+ | `data-focus-contained` | Indicates whether focus is contained | "true" / "false"|
101
+ | `data-has-focus` | Indicates whether the scope currently has focus | "true" / "false"|
102
+
103
+ These data attributes can be targeted using CSS selectors for styling. When
104
+ using data attributes for styling, ensure you scope them properly with a
105
+ className to avoid affecting unrelated elements:
106
+
107
+ ```css
108
+ .my-modal[data-focus-contained="true"] {
109
+ outline: 2px solid blue;
110
+ }
111
+
112
+ .my-modal[data-has-focus="true"] {
113
+ background-color: rgba(0, 0, 0, 0.05);
114
+ }
115
+ ```
116
+
117
+ Apply the scoping className to your FocusLock children:
118
+
119
+ ```tsx
120
+ <FocusLock contain restoreFocus autoFocus>
121
+ <div className="my-modal">
122
+ Modal content
123
+ </div>
124
+ </FocusLock>
125
+ ```
126
+
127
+ ## Accessibility
128
+
129
+ Focus management is crucial for accessibility. The `FocusLock` component ensures
130
+ that keyboard users can navigate within the focus scope using Tab and Shift+Tab,
131
+ focus is trapped within the scope when `contain` is enabled, focus is
132
+ automatically restored to the previously focused element when the scope is
133
+ removed (when `restoreFocus` is enabled), and the first focusable element is
134
+ automatically focused when the scope is mounted (when `autoFocus` is enabled).
135
+
136
+ When using focus lock, follow these accessibility guidelines:
137
+
138
+ - Always provide a way to exit the focus scope (e.g., a close button or escape key handler)
139
+ - Use `restoreFocus` to ensure users return to their previous location when the scope is closed
140
+ - Use `autoFocus` to immediately draw attention to important content like modals
141
+ - Consider using `aria-modal` on modal dialogs to provide additional context to screen readers
142
+ - Ensure all focusable elements within the scope are keyboard accessible
package/dist/index.cjs ADDED
@@ -0,0 +1,44 @@
1
+ 'use strict';
2
+
3
+ var React = require('react');
4
+ var useDataAttributes = require('@bento/use-data-attributes');
5
+ var interactions = require('@react-aria/interactions');
6
+ var slots = require('@bento/slots');
7
+ var focus = require('@react-aria/focus');
8
+ var useProps = require('@bento/use-props');
9
+
10
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
11
+
12
+ var React__default = /*#__PURE__*/_interopDefault(React);
13
+
14
+ // src/index.tsx
15
+ var FocusLock = slots.withSlots("BentoFocusLock", function FocusLock2(args) {
16
+ const { contain = false, restoreFocus = false, autoFocus = false, children, onFocusEnter, onFocusLeave } = args;
17
+ const [hasFocus, setHasFocus] = React.useState(false);
18
+ const { focusWithinProps } = interactions.useFocusWithin({
19
+ onFocusWithinChange: function onFocusWithinChange(isFocusWithin) {
20
+ setHasFocus(isFocusWithin);
21
+ },
22
+ onFocusWithin: onFocusEnter,
23
+ onBlurWithin: onFocusLeave
24
+ });
25
+ const state = {
26
+ hasFocus,
27
+ isContained: contain
28
+ };
29
+ const { apply } = useProps.useProps(args, state);
30
+ const data = useDataAttributes.useDataAttributes({
31
+ "focus-contained": contain,
32
+ "has-focus": hasFocus
33
+ });
34
+ if (!children) return null;
35
+ const spread = apply({ contain, restoreFocus, autoFocus }, ["children", "onFocusEnter", "onFocusLeave"]);
36
+ const kids = { ...focusWithinProps, ...data };
37
+ return /* @__PURE__ */ React__default.default.createElement(focus.FocusScope, { ...spread }, React.Children.map(children, function applyDataAttributes(child) {
38
+ return React.isValidElement(child) ? React.cloneElement(child, kids) : child;
39
+ }));
40
+ });
41
+
42
+ exports.FocusLock = FocusLock;
43
+ //# sourceMappingURL=index.cjs.map
44
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.tsx"],"names":["withSlots","FocusLock","useState","useFocusWithin","useProps","useDataAttributes","React","FocusScope","Children","isValidElement","cloneElement"],"mappings":";;;;;;;;;;;;;;AAiIO,IAAM,SAAA,GAAYA,eAAA,CAAU,gBAAA,EAAkB,SAASC,WAAU,IAAA,EAAsB;AAC5F,EAAA,MAAM,EAAE,OAAA,GAAU,KAAA,EAAO,YAAA,GAAe,KAAA,EAAO,YAAY,KAAA,EAAO,QAAA,EAAU,YAAA,EAAc,YAAA,EAAa,GAAI,IAAA;AAC3G,EAAA,MAAM,CAAC,QAAA,EAAU,WAAW,CAAA,GAAIC,eAAS,KAAK,CAAA;AAG9C,EAAA,MAAM,EAAE,gBAAA,EAAiB,GAAIC,2BAAA,CAAe;AAAA,IAC1C,mBAAA,EAAqB,SAAS,mBAAA,CAAoB,aAAA,EAAe;AAC/D,MAAA,WAAA,CAAY,aAAa,CAAA;AAAA,IAC3B,CAAA;AAAA,IACA,aAAA,EAAe,YAAA;AAAA,IACf,YAAA,EAAc;AAAA,GACf,CAAA;AAGD,EAAA,MAAM,KAAA,GAAwB;AAAA,IAC5B,QAAA;AAAA,IACA,WAAA,EAAa;AAAA,GACf;AAGA,EAAA,MAAM,EAAE,KAAA,EAAM,GAAIC,iBAAA,CAAS,MAAM,KAAK,CAAA;AAGtC,EAAA,MAAM,OAAOC,mCAAA,CAAkB;AAAA,IAC7B,iBAAA,EAAmB,OAAA;AAAA,IACnB,WAAA,EAAa;AAAA,GACd,CAAA;AAED,EAAA,IAAI,CAAC,UAAU,OAAO,IAAA;AAKtB,EAAA,MAAM,MAAA,GAAS,KAAA,CAAM,EAAE,OAAA,EAAS,YAAA,EAAc,SAAA,EAAU,EAAG,CAAC,UAAA,EAAY,cAAA,EAAgB,cAAc,CAAC,CAAA;AAKvG,EAAA,MAAM,IAAA,GAAO,EAAE,GAAG,gBAAA,EAAkB,GAAG,IAAA,EAAK;AAE5C,EAAA,uBACEC,sBAAA,CAAA,aAAA,CAACC,oBAAY,GAAG,MAAA,EAAA,EACbC,eAAS,GAAA,CAAI,QAAA,EAAU,SAAS,mBAAA,CAAoB,KAAA,EAAO;AAC1D,IAAA,OAAOC,qBAAe,KAAK,CAAA,GAAIC,kBAAA,CAAa,KAAA,EAA6B,IAAI,CAAA,GAAI,KAAA;AAAA,EACnF,CAAC,CACH,CAAA;AAEJ,CAAC","file":"index.cjs","sourcesContent":["import React, { Children, cloneElement, isValidElement, type ReactNode, useState } from 'react';\nimport { useDataAttributes } from '@bento/use-data-attributes';\nimport { useFocusWithin } from '@react-aria/interactions';\nimport { withSlots, type Slots } from '@bento/slots';\nimport { FocusScope } from '@react-aria/focus';\nimport { useProps } from '@bento/use-props';\n\n/**\n * State object passed to render prop functions\n * @public\n */\nexport interface FocusLockState {\n /**\n * Whether focus is currently within the scope\n */\n hasFocus: boolean;\n\n /**\n * Whether focus is contained within the scope\n */\n isContained: boolean;\n}\n\nexport interface FocusLockProps extends Slots {\n /**\n * Whether to contain focus within the scope.\n * When true, focus will cycle between focusable elements within the scope.\n *\n * @default false\n */\n contain?: boolean;\n\n /**\n * Whether to restore focus to the previously focused element when the focus scope unmounts.\n *\n * @default false\n */\n restoreFocus?: boolean;\n\n /**\n * Whether to automatically focus the first focusable element when the focus scope mounts.\n *\n * @default false\n */\n autoFocus?: boolean;\n\n /**\n * The content to render inside the focus lock.\n * Can be a single element or multiple elements.\n */\n children?: ReactNode;\n\n /**\n * Callback fired when focus enters the scope\n */\n onFocusEnter?: (e: React.FocusEvent) => void;\n\n /**\n * Callback fired when focus leaves the scope\n */\n onFocusLeave?: (e: React.FocusEvent) => void;\n\n /**\n * Render prop for className\n */\n className?: ((state: FocusLockState) => string) | string;\n\n /**\n * Render prop for style\n */\n style?: ((state: FocusLockState) => React.CSSProperties) | React.CSSProperties;\n}\n\n/**\n * FocusLock manages focus within a scope, preventing focus from escaping and optionally\n * restoring focus when the scope is removed. Built on top of React ARIA's FocusScope.\n *\n * The FocusLock primitive provides essential focus management for modals, dialogs, drawers,\n * select popovers, and other overlay components that need to trap focus within their boundaries.\n *\n * This component does not add any wrapper elements - it applies data attributes directly to\n * its children, allowing for flexible composition with multiple elements or single elements.\n *\n * @component\n * @param args - The properties {@link FocusLockProps} passed to the FocusLock component.\n *\n * @example\n * ```tsx\n * // Overlay with multiple children (backdrop + content)\n * <FocusLock contain restoreFocus autoFocus>\n * <div slot=\"backdrop\" />\n * <div slot=\"content\">\n * <h2>Modal Title</h2>\n * <button>Close</button>\n * </div>\n * </FocusLock>\n * ```\n *\n * @example\n * ```tsx\n * // Select popover with single child\n * <FocusLock contain restoreFocus autoFocus>\n * <div className=\"popover\">\n * <ListBox>\n * <ListBoxItem>Option 1</ListBoxItem>\n * <ListBoxItem>Option 2</ListBoxItem>\n * </ListBox>\n * </div>\n * </FocusLock>\n * ```\n *\n * @example\n * ```tsx\n * // With render props for dynamic styling\n * <FocusLock\n * contain\n * className={({ hasFocus, isContained }) =>\n * `modal ${isContained ? 'contained' : ''} ${hasFocus ? 'focused' : ''}`\n * }\n * style={({ hasFocus }) => ({\n * opacity: hasFocus ? 1 : 0.8\n * })}\n * >\n * <div>Content</div>\n * </FocusLock>\n * ```\n *\n * @public\n */\nexport const FocusLock = withSlots('BentoFocusLock', function FocusLock(args: FocusLockProps) {\n const { contain = false, restoreFocus = false, autoFocus = false, children, onFocusEnter, onFocusLeave } = args;\n const [hasFocus, setHasFocus] = useState(false);\n\n // Track focus within the scope using React ARIA\n const { focusWithinProps } = useFocusWithin({\n onFocusWithinChange: function onFocusWithinChange(isFocusWithin) {\n setHasFocus(isFocusWithin);\n },\n onFocusWithin: onFocusEnter,\n onBlurWithin: onFocusLeave\n });\n\n // Create state object for render props\n const state: FocusLockState = {\n hasFocus,\n isContained: contain\n };\n\n // Pass state to useProps so render props can access it\n const { apply } = useProps(args, state);\n\n // Generate data attributes for focus lock state\n const data = useDataAttributes({\n 'focus-contained': contain,\n 'has-focus': hasFocus\n });\n\n if (!children) return null;\n\n //\n // Apply props to FocusScope for slot inheritance\n //\n const spread = apply({ contain, restoreFocus, autoFocus }, ['children', 'onFocusEnter', 'onFocusLeave']);\n\n //\n // Merge focus tracking props with data attributes to apply to children\n //\n const kids = { ...focusWithinProps, ...data };\n\n return (\n <FocusScope {...spread}>\n {Children.map(children, function applyDataAttributes(child) {\n return isValidElement(child) ? cloneElement(child as React.ReactElement, kids) : child;\n })}\n </FocusScope>\n );\n}) as (props: FocusLockProps) => React.ReactElement | null;\n"]}
@@ -0,0 +1,118 @@
1
+ import React, { ReactNode } from 'react';
2
+ import { Slots } from '@bento/slots';
3
+
4
+ /**
5
+ * State object passed to render prop functions
6
+ * @public
7
+ */
8
+ interface FocusLockState {
9
+ /**
10
+ * Whether focus is currently within the scope
11
+ */
12
+ hasFocus: boolean;
13
+ /**
14
+ * Whether focus is contained within the scope
15
+ */
16
+ isContained: boolean;
17
+ }
18
+ interface FocusLockProps extends Slots {
19
+ /**
20
+ * Whether to contain focus within the scope.
21
+ * When true, focus will cycle between focusable elements within the scope.
22
+ *
23
+ * @default false
24
+ */
25
+ contain?: boolean;
26
+ /**
27
+ * Whether to restore focus to the previously focused element when the focus scope unmounts.
28
+ *
29
+ * @default false
30
+ */
31
+ restoreFocus?: boolean;
32
+ /**
33
+ * Whether to automatically focus the first focusable element when the focus scope mounts.
34
+ *
35
+ * @default false
36
+ */
37
+ autoFocus?: boolean;
38
+ /**
39
+ * The content to render inside the focus lock.
40
+ * Can be a single element or multiple elements.
41
+ */
42
+ children?: ReactNode;
43
+ /**
44
+ * Callback fired when focus enters the scope
45
+ */
46
+ onFocusEnter?: (e: React.FocusEvent) => void;
47
+ /**
48
+ * Callback fired when focus leaves the scope
49
+ */
50
+ onFocusLeave?: (e: React.FocusEvent) => void;
51
+ /**
52
+ * Render prop for className
53
+ */
54
+ className?: ((state: FocusLockState) => string) | string;
55
+ /**
56
+ * Render prop for style
57
+ */
58
+ style?: ((state: FocusLockState) => React.CSSProperties) | React.CSSProperties;
59
+ }
60
+ /**
61
+ * FocusLock manages focus within a scope, preventing focus from escaping and optionally
62
+ * restoring focus when the scope is removed. Built on top of React ARIA's FocusScope.
63
+ *
64
+ * The FocusLock primitive provides essential focus management for modals, dialogs, drawers,
65
+ * select popovers, and other overlay components that need to trap focus within their boundaries.
66
+ *
67
+ * This component does not add any wrapper elements - it applies data attributes directly to
68
+ * its children, allowing for flexible composition with multiple elements or single elements.
69
+ *
70
+ * @component
71
+ * @param args - The properties {@link FocusLockProps} passed to the FocusLock component.
72
+ *
73
+ * @example
74
+ * ```tsx
75
+ * // Overlay with multiple children (backdrop + content)
76
+ * <FocusLock contain restoreFocus autoFocus>
77
+ * <div slot="backdrop" />
78
+ * <div slot="content">
79
+ * <h2>Modal Title</h2>
80
+ * <button>Close</button>
81
+ * </div>
82
+ * </FocusLock>
83
+ * ```
84
+ *
85
+ * @example
86
+ * ```tsx
87
+ * // Select popover with single child
88
+ * <FocusLock contain restoreFocus autoFocus>
89
+ * <div className="popover">
90
+ * <ListBox>
91
+ * <ListBoxItem>Option 1</ListBoxItem>
92
+ * <ListBoxItem>Option 2</ListBoxItem>
93
+ * </ListBox>
94
+ * </div>
95
+ * </FocusLock>
96
+ * ```
97
+ *
98
+ * @example
99
+ * ```tsx
100
+ * // With render props for dynamic styling
101
+ * <FocusLock
102
+ * contain
103
+ * className={({ hasFocus, isContained }) =>
104
+ * `modal ${isContained ? 'contained' : ''} ${hasFocus ? 'focused' : ''}`
105
+ * }
106
+ * style={({ hasFocus }) => ({
107
+ * opacity: hasFocus ? 1 : 0.8
108
+ * })}
109
+ * >
110
+ * <div>Content</div>
111
+ * </FocusLock>
112
+ * ```
113
+ *
114
+ * @public
115
+ */
116
+ declare const FocusLock: (props: FocusLockProps) => React.ReactElement | null;
117
+
118
+ export { FocusLock, type FocusLockProps, type FocusLockState };
@@ -0,0 +1,118 @@
1
+ import React, { ReactNode } from 'react';
2
+ import { Slots } from '@bento/slots';
3
+
4
+ /**
5
+ * State object passed to render prop functions
6
+ * @public
7
+ */
8
+ interface FocusLockState {
9
+ /**
10
+ * Whether focus is currently within the scope
11
+ */
12
+ hasFocus: boolean;
13
+ /**
14
+ * Whether focus is contained within the scope
15
+ */
16
+ isContained: boolean;
17
+ }
18
+ interface FocusLockProps extends Slots {
19
+ /**
20
+ * Whether to contain focus within the scope.
21
+ * When true, focus will cycle between focusable elements within the scope.
22
+ *
23
+ * @default false
24
+ */
25
+ contain?: boolean;
26
+ /**
27
+ * Whether to restore focus to the previously focused element when the focus scope unmounts.
28
+ *
29
+ * @default false
30
+ */
31
+ restoreFocus?: boolean;
32
+ /**
33
+ * Whether to automatically focus the first focusable element when the focus scope mounts.
34
+ *
35
+ * @default false
36
+ */
37
+ autoFocus?: boolean;
38
+ /**
39
+ * The content to render inside the focus lock.
40
+ * Can be a single element or multiple elements.
41
+ */
42
+ children?: ReactNode;
43
+ /**
44
+ * Callback fired when focus enters the scope
45
+ */
46
+ onFocusEnter?: (e: React.FocusEvent) => void;
47
+ /**
48
+ * Callback fired when focus leaves the scope
49
+ */
50
+ onFocusLeave?: (e: React.FocusEvent) => void;
51
+ /**
52
+ * Render prop for className
53
+ */
54
+ className?: ((state: FocusLockState) => string) | string;
55
+ /**
56
+ * Render prop for style
57
+ */
58
+ style?: ((state: FocusLockState) => React.CSSProperties) | React.CSSProperties;
59
+ }
60
+ /**
61
+ * FocusLock manages focus within a scope, preventing focus from escaping and optionally
62
+ * restoring focus when the scope is removed. Built on top of React ARIA's FocusScope.
63
+ *
64
+ * The FocusLock primitive provides essential focus management for modals, dialogs, drawers,
65
+ * select popovers, and other overlay components that need to trap focus within their boundaries.
66
+ *
67
+ * This component does not add any wrapper elements - it applies data attributes directly to
68
+ * its children, allowing for flexible composition with multiple elements or single elements.
69
+ *
70
+ * @component
71
+ * @param args - The properties {@link FocusLockProps} passed to the FocusLock component.
72
+ *
73
+ * @example
74
+ * ```tsx
75
+ * // Overlay with multiple children (backdrop + content)
76
+ * <FocusLock contain restoreFocus autoFocus>
77
+ * <div slot="backdrop" />
78
+ * <div slot="content">
79
+ * <h2>Modal Title</h2>
80
+ * <button>Close</button>
81
+ * </div>
82
+ * </FocusLock>
83
+ * ```
84
+ *
85
+ * @example
86
+ * ```tsx
87
+ * // Select popover with single child
88
+ * <FocusLock contain restoreFocus autoFocus>
89
+ * <div className="popover">
90
+ * <ListBox>
91
+ * <ListBoxItem>Option 1</ListBoxItem>
92
+ * <ListBoxItem>Option 2</ListBoxItem>
93
+ * </ListBox>
94
+ * </div>
95
+ * </FocusLock>
96
+ * ```
97
+ *
98
+ * @example
99
+ * ```tsx
100
+ * // With render props for dynamic styling
101
+ * <FocusLock
102
+ * contain
103
+ * className={({ hasFocus, isContained }) =>
104
+ * `modal ${isContained ? 'contained' : ''} ${hasFocus ? 'focused' : ''}`
105
+ * }
106
+ * style={({ hasFocus }) => ({
107
+ * opacity: hasFocus ? 1 : 0.8
108
+ * })}
109
+ * >
110
+ * <div>Content</div>
111
+ * </FocusLock>
112
+ * ```
113
+ *
114
+ * @public
115
+ */
116
+ declare const FocusLock: (props: FocusLockProps) => React.ReactElement | null;
117
+
118
+ export { FocusLock, type FocusLockProps, type FocusLockState };
package/dist/index.js ADDED
@@ -0,0 +1,38 @@
1
+ import React, { useState, Children, cloneElement, isValidElement } from 'react';
2
+ import { useDataAttributes } from '@bento/use-data-attributes';
3
+ import { useFocusWithin } from '@react-aria/interactions';
4
+ import { withSlots } from '@bento/slots';
5
+ import { FocusScope } from '@react-aria/focus';
6
+ import { useProps } from '@bento/use-props';
7
+
8
+ // src/index.tsx
9
+ var FocusLock = withSlots("BentoFocusLock", function FocusLock2(args) {
10
+ const { contain = false, restoreFocus = false, autoFocus = false, children, onFocusEnter, onFocusLeave } = args;
11
+ const [hasFocus, setHasFocus] = useState(false);
12
+ const { focusWithinProps } = useFocusWithin({
13
+ onFocusWithinChange: function onFocusWithinChange(isFocusWithin) {
14
+ setHasFocus(isFocusWithin);
15
+ },
16
+ onFocusWithin: onFocusEnter,
17
+ onBlurWithin: onFocusLeave
18
+ });
19
+ const state = {
20
+ hasFocus,
21
+ isContained: contain
22
+ };
23
+ const { apply } = useProps(args, state);
24
+ const data = useDataAttributes({
25
+ "focus-contained": contain,
26
+ "has-focus": hasFocus
27
+ });
28
+ if (!children) return null;
29
+ const spread = apply({ contain, restoreFocus, autoFocus }, ["children", "onFocusEnter", "onFocusLeave"]);
30
+ const kids = { ...focusWithinProps, ...data };
31
+ return /* @__PURE__ */ React.createElement(FocusScope, { ...spread }, Children.map(children, function applyDataAttributes(child) {
32
+ return isValidElement(child) ? cloneElement(child, kids) : child;
33
+ }));
34
+ });
35
+
36
+ export { FocusLock };
37
+ //# sourceMappingURL=index.js.map
38
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.tsx"],"names":["FocusLock"],"mappings":";;;;;;;;AAiIO,IAAM,SAAA,GAAY,SAAA,CAAU,gBAAA,EAAkB,SAASA,WAAU,IAAA,EAAsB;AAC5F,EAAA,MAAM,EAAE,OAAA,GAAU,KAAA,EAAO,YAAA,GAAe,KAAA,EAAO,YAAY,KAAA,EAAO,QAAA,EAAU,YAAA,EAAc,YAAA,EAAa,GAAI,IAAA;AAC3G,EAAA,MAAM,CAAC,QAAA,EAAU,WAAW,CAAA,GAAI,SAAS,KAAK,CAAA;AAG9C,EAAA,MAAM,EAAE,gBAAA,EAAiB,GAAI,cAAA,CAAe;AAAA,IAC1C,mBAAA,EAAqB,SAAS,mBAAA,CAAoB,aAAA,EAAe;AAC/D,MAAA,WAAA,CAAY,aAAa,CAAA;AAAA,IAC3B,CAAA;AAAA,IACA,aAAA,EAAe,YAAA;AAAA,IACf,YAAA,EAAc;AAAA,GACf,CAAA;AAGD,EAAA,MAAM,KAAA,GAAwB;AAAA,IAC5B,QAAA;AAAA,IACA,WAAA,EAAa;AAAA,GACf;AAGA,EAAA,MAAM,EAAE,KAAA,EAAM,GAAI,QAAA,CAAS,MAAM,KAAK,CAAA;AAGtC,EAAA,MAAM,OAAO,iBAAA,CAAkB;AAAA,IAC7B,iBAAA,EAAmB,OAAA;AAAA,IACnB,WAAA,EAAa;AAAA,GACd,CAAA;AAED,EAAA,IAAI,CAAC,UAAU,OAAO,IAAA;AAKtB,EAAA,MAAM,MAAA,GAAS,KAAA,CAAM,EAAE,OAAA,EAAS,YAAA,EAAc,SAAA,EAAU,EAAG,CAAC,UAAA,EAAY,cAAA,EAAgB,cAAc,CAAC,CAAA;AAKvG,EAAA,MAAM,IAAA,GAAO,EAAE,GAAG,gBAAA,EAAkB,GAAG,IAAA,EAAK;AAE5C,EAAA,uBACE,KAAA,CAAA,aAAA,CAAC,cAAY,GAAG,MAAA,EAAA,EACb,SAAS,GAAA,CAAI,QAAA,EAAU,SAAS,mBAAA,CAAoB,KAAA,EAAO;AAC1D,IAAA,OAAO,eAAe,KAAK,CAAA,GAAI,YAAA,CAAa,KAAA,EAA6B,IAAI,CAAA,GAAI,KAAA;AAAA,EACnF,CAAC,CACH,CAAA;AAEJ,CAAC","file":"index.js","sourcesContent":["import React, { Children, cloneElement, isValidElement, type ReactNode, useState } from 'react';\nimport { useDataAttributes } from '@bento/use-data-attributes';\nimport { useFocusWithin } from '@react-aria/interactions';\nimport { withSlots, type Slots } from '@bento/slots';\nimport { FocusScope } from '@react-aria/focus';\nimport { useProps } from '@bento/use-props';\n\n/**\n * State object passed to render prop functions\n * @public\n */\nexport interface FocusLockState {\n /**\n * Whether focus is currently within the scope\n */\n hasFocus: boolean;\n\n /**\n * Whether focus is contained within the scope\n */\n isContained: boolean;\n}\n\nexport interface FocusLockProps extends Slots {\n /**\n * Whether to contain focus within the scope.\n * When true, focus will cycle between focusable elements within the scope.\n *\n * @default false\n */\n contain?: boolean;\n\n /**\n * Whether to restore focus to the previously focused element when the focus scope unmounts.\n *\n * @default false\n */\n restoreFocus?: boolean;\n\n /**\n * Whether to automatically focus the first focusable element when the focus scope mounts.\n *\n * @default false\n */\n autoFocus?: boolean;\n\n /**\n * The content to render inside the focus lock.\n * Can be a single element or multiple elements.\n */\n children?: ReactNode;\n\n /**\n * Callback fired when focus enters the scope\n */\n onFocusEnter?: (e: React.FocusEvent) => void;\n\n /**\n * Callback fired when focus leaves the scope\n */\n onFocusLeave?: (e: React.FocusEvent) => void;\n\n /**\n * Render prop for className\n */\n className?: ((state: FocusLockState) => string) | string;\n\n /**\n * Render prop for style\n */\n style?: ((state: FocusLockState) => React.CSSProperties) | React.CSSProperties;\n}\n\n/**\n * FocusLock manages focus within a scope, preventing focus from escaping and optionally\n * restoring focus when the scope is removed. Built on top of React ARIA's FocusScope.\n *\n * The FocusLock primitive provides essential focus management for modals, dialogs, drawers,\n * select popovers, and other overlay components that need to trap focus within their boundaries.\n *\n * This component does not add any wrapper elements - it applies data attributes directly to\n * its children, allowing for flexible composition with multiple elements or single elements.\n *\n * @component\n * @param args - The properties {@link FocusLockProps} passed to the FocusLock component.\n *\n * @example\n * ```tsx\n * // Overlay with multiple children (backdrop + content)\n * <FocusLock contain restoreFocus autoFocus>\n * <div slot=\"backdrop\" />\n * <div slot=\"content\">\n * <h2>Modal Title</h2>\n * <button>Close</button>\n * </div>\n * </FocusLock>\n * ```\n *\n * @example\n * ```tsx\n * // Select popover with single child\n * <FocusLock contain restoreFocus autoFocus>\n * <div className=\"popover\">\n * <ListBox>\n * <ListBoxItem>Option 1</ListBoxItem>\n * <ListBoxItem>Option 2</ListBoxItem>\n * </ListBox>\n * </div>\n * </FocusLock>\n * ```\n *\n * @example\n * ```tsx\n * // With render props for dynamic styling\n * <FocusLock\n * contain\n * className={({ hasFocus, isContained }) =>\n * `modal ${isContained ? 'contained' : ''} ${hasFocus ? 'focused' : ''}`\n * }\n * style={({ hasFocus }) => ({\n * opacity: hasFocus ? 1 : 0.8\n * })}\n * >\n * <div>Content</div>\n * </FocusLock>\n * ```\n *\n * @public\n */\nexport const FocusLock = withSlots('BentoFocusLock', function FocusLock(args: FocusLockProps) {\n const { contain = false, restoreFocus = false, autoFocus = false, children, onFocusEnter, onFocusLeave } = args;\n const [hasFocus, setHasFocus] = useState(false);\n\n // Track focus within the scope using React ARIA\n const { focusWithinProps } = useFocusWithin({\n onFocusWithinChange: function onFocusWithinChange(isFocusWithin) {\n setHasFocus(isFocusWithin);\n },\n onFocusWithin: onFocusEnter,\n onBlurWithin: onFocusLeave\n });\n\n // Create state object for render props\n const state: FocusLockState = {\n hasFocus,\n isContained: contain\n };\n\n // Pass state to useProps so render props can access it\n const { apply } = useProps(args, state);\n\n // Generate data attributes for focus lock state\n const data = useDataAttributes({\n 'focus-contained': contain,\n 'has-focus': hasFocus\n });\n\n if (!children) return null;\n\n //\n // Apply props to FocusScope for slot inheritance\n //\n const spread = apply({ contain, restoreFocus, autoFocus }, ['children', 'onFocusEnter', 'onFocusLeave']);\n\n //\n // Merge focus tracking props with data attributes to apply to children\n //\n const kids = { ...focusWithinProps, ...data };\n\n return (\n <FocusScope {...spread}>\n {Children.map(children, function applyDataAttributes(child) {\n return isValidElement(child) ? cloneElement(child as React.ReactElement, kids) : child;\n })}\n </FocusScope>\n );\n}) as (props: FocusLockProps) => React.ReactElement | null;\n"]}
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "@bento/focus-lock",
3
+ "version": "0.0.1",
4
+ "description": "Focus lock primitive for managing focus within a scope",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "scripts": {
9
+ "build": "tsup-node",
10
+ "lint": "biome lint && tsc",
11
+ "posttest": "npm run lint",
12
+ "prepublishOnly": "node ../../scripts/compile-readme.ts",
13
+ "pretest": "npm run build",
14
+ "test": "vitest --run",
15
+ "test:watch": "vitest"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/godaddy/bento.git"
20
+ },
21
+ "keywords": [
22
+ "accessibility",
23
+ "aria",
24
+ "bento",
25
+ "component",
26
+ "focus",
27
+ "focus-lock",
28
+ "focus-scope",
29
+ "focus-trap",
30
+ "library",
31
+ "react"
32
+ ],
33
+ "author": "GoDaddy Operating Company, LLC",
34
+ "license": "MIT",
35
+ "bugs": {
36
+ "url": "https://github.com/godaddy/bento/issues"
37
+ },
38
+ "homepage": "https://github.com/godaddy/bento#readme",
39
+ "files": [
40
+ "dist",
41
+ "src",
42
+ "package.json"
43
+ ],
44
+ "dependencies": {
45
+ "@bento/slots": "^0.2.0",
46
+ "@bento/use-data-attributes": "^0.1.1",
47
+ "@bento/use-props": "^0.2.0",
48
+ "@react-aria/focus": "^3.18.6",
49
+ "@react-aria/interactions": "^3.22.6"
50
+ },
51
+ "devDependencies": {
52
+ "@bento/button": "*",
53
+ "@bento/container": "*",
54
+ "@bento/heading": "*",
55
+ "@bento/listbox": "*",
56
+ "@bento/radio": "*",
57
+ "@bento/text": "*"
58
+ },
59
+ "peerDependencies": {
60
+ "react": "18.x || 19.x",
61
+ "react-dom": "18.x || 19.x"
62
+ },
63
+ "exports": {
64
+ ".": {
65
+ "import": {
66
+ "types": "./dist/index.d.ts",
67
+ "default": "./dist/index.js"
68
+ },
69
+ "require": {
70
+ "types": "./dist/index.d.cts",
71
+ "default": "./dist/index.cjs"
72
+ }
73
+ },
74
+ "./package.json": "./package.json"
75
+ },
76
+ "publishConfig": {
77
+ "access": "public",
78
+ "registry": "https://registry.npmjs.org/"
79
+ }
80
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,177 @@
1
+ import React, { Children, cloneElement, isValidElement, type ReactNode, useState } from 'react';
2
+ import { useDataAttributes } from '@bento/use-data-attributes';
3
+ import { useFocusWithin } from '@react-aria/interactions';
4
+ import { withSlots, type Slots } from '@bento/slots';
5
+ import { FocusScope } from '@react-aria/focus';
6
+ import { useProps } from '@bento/use-props';
7
+
8
+ /**
9
+ * State object passed to render prop functions
10
+ * @public
11
+ */
12
+ export interface FocusLockState {
13
+ /**
14
+ * Whether focus is currently within the scope
15
+ */
16
+ hasFocus: boolean;
17
+
18
+ /**
19
+ * Whether focus is contained within the scope
20
+ */
21
+ isContained: boolean;
22
+ }
23
+
24
+ export interface FocusLockProps extends Slots {
25
+ /**
26
+ * Whether to contain focus within the scope.
27
+ * When true, focus will cycle between focusable elements within the scope.
28
+ *
29
+ * @default false
30
+ */
31
+ contain?: boolean;
32
+
33
+ /**
34
+ * Whether to restore focus to the previously focused element when the focus scope unmounts.
35
+ *
36
+ * @default false
37
+ */
38
+ restoreFocus?: boolean;
39
+
40
+ /**
41
+ * Whether to automatically focus the first focusable element when the focus scope mounts.
42
+ *
43
+ * @default false
44
+ */
45
+ autoFocus?: boolean;
46
+
47
+ /**
48
+ * The content to render inside the focus lock.
49
+ * Can be a single element or multiple elements.
50
+ */
51
+ children?: ReactNode;
52
+
53
+ /**
54
+ * Callback fired when focus enters the scope
55
+ */
56
+ onFocusEnter?: (e: React.FocusEvent) => void;
57
+
58
+ /**
59
+ * Callback fired when focus leaves the scope
60
+ */
61
+ onFocusLeave?: (e: React.FocusEvent) => void;
62
+
63
+ /**
64
+ * Render prop for className
65
+ */
66
+ className?: ((state: FocusLockState) => string) | string;
67
+
68
+ /**
69
+ * Render prop for style
70
+ */
71
+ style?: ((state: FocusLockState) => React.CSSProperties) | React.CSSProperties;
72
+ }
73
+
74
+ /**
75
+ * FocusLock manages focus within a scope, preventing focus from escaping and optionally
76
+ * restoring focus when the scope is removed. Built on top of React ARIA's FocusScope.
77
+ *
78
+ * The FocusLock primitive provides essential focus management for modals, dialogs, drawers,
79
+ * select popovers, and other overlay components that need to trap focus within their boundaries.
80
+ *
81
+ * This component does not add any wrapper elements - it applies data attributes directly to
82
+ * its children, allowing for flexible composition with multiple elements or single elements.
83
+ *
84
+ * @component
85
+ * @param args - The properties {@link FocusLockProps} passed to the FocusLock component.
86
+ *
87
+ * @example
88
+ * ```tsx
89
+ * // Overlay with multiple children (backdrop + content)
90
+ * <FocusLock contain restoreFocus autoFocus>
91
+ * <div slot="backdrop" />
92
+ * <div slot="content">
93
+ * <h2>Modal Title</h2>
94
+ * <button>Close</button>
95
+ * </div>
96
+ * </FocusLock>
97
+ * ```
98
+ *
99
+ * @example
100
+ * ```tsx
101
+ * // Select popover with single child
102
+ * <FocusLock contain restoreFocus autoFocus>
103
+ * <div className="popover">
104
+ * <ListBox>
105
+ * <ListBoxItem>Option 1</ListBoxItem>
106
+ * <ListBoxItem>Option 2</ListBoxItem>
107
+ * </ListBox>
108
+ * </div>
109
+ * </FocusLock>
110
+ * ```
111
+ *
112
+ * @example
113
+ * ```tsx
114
+ * // With render props for dynamic styling
115
+ * <FocusLock
116
+ * contain
117
+ * className={({ hasFocus, isContained }) =>
118
+ * `modal ${isContained ? 'contained' : ''} ${hasFocus ? 'focused' : ''}`
119
+ * }
120
+ * style={({ hasFocus }) => ({
121
+ * opacity: hasFocus ? 1 : 0.8
122
+ * })}
123
+ * >
124
+ * <div>Content</div>
125
+ * </FocusLock>
126
+ * ```
127
+ *
128
+ * @public
129
+ */
130
+ export const FocusLock = withSlots('BentoFocusLock', function FocusLock(args: FocusLockProps) {
131
+ const { contain = false, restoreFocus = false, autoFocus = false, children, onFocusEnter, onFocusLeave } = args;
132
+ const [hasFocus, setHasFocus] = useState(false);
133
+
134
+ // Track focus within the scope using React ARIA
135
+ const { focusWithinProps } = useFocusWithin({
136
+ onFocusWithinChange: function onFocusWithinChange(isFocusWithin) {
137
+ setHasFocus(isFocusWithin);
138
+ },
139
+ onFocusWithin: onFocusEnter,
140
+ onBlurWithin: onFocusLeave
141
+ });
142
+
143
+ // Create state object for render props
144
+ const state: FocusLockState = {
145
+ hasFocus,
146
+ isContained: contain
147
+ };
148
+
149
+ // Pass state to useProps so render props can access it
150
+ const { apply } = useProps(args, state);
151
+
152
+ // Generate data attributes for focus lock state
153
+ const data = useDataAttributes({
154
+ 'focus-contained': contain,
155
+ 'has-focus': hasFocus
156
+ });
157
+
158
+ if (!children) return null;
159
+
160
+ //
161
+ // Apply props to FocusScope for slot inheritance
162
+ //
163
+ const spread = apply({ contain, restoreFocus, autoFocus }, ['children', 'onFocusEnter', 'onFocusLeave']);
164
+
165
+ //
166
+ // Merge focus tracking props with data attributes to apply to children
167
+ //
168
+ const kids = { ...focusWithinProps, ...data };
169
+
170
+ return (
171
+ <FocusScope {...spread}>
172
+ {Children.map(children, function applyDataAttributes(child) {
173
+ return isValidElement(child) ? cloneElement(child as React.ReactElement, kids) : child;
174
+ })}
175
+ </FocusScope>
176
+ );
177
+ }) as (props: FocusLockProps) => React.ReactElement | null;