@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 +9 -0
- package/README.md +136 -0
- package/README.mdx +142 -0
- package/dist/index.cjs +44 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +118 -0
- package/dist/index.d.ts +118 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/package.json +80 -0
- package/src/index.tsx +177 -0
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"]}
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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;
|