Skip to content

Commit 486a7e0

Browse files
committed
feat: add crossFrame property to control iframe behaviour, fixes #104
1 parent a3ef2f0 commit 486a7e0

File tree

7 files changed

+52
-19
lines changed

7 files changed

+52
-19
lines changed

interfaces.d.ts

+8
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ export interface ReactFocusLockProps<ChildrenType = React.ReactNode, LockProps=R
2424
*/
2525
persistentFocus?: boolean;
2626

27+
/**
28+
* enables aggressive focus capturing within iframes
29+
* - once disabled allows focus to move outside of iframe, if enabled inside iframe
30+
* - once enabled keep focus in the lock, no matter where lock is active (default)
31+
* @default true
32+
*/
33+
crossFrame: boolean;
34+
2735
/**
2836
* enables or disables autoFocusing feature.
2937
* If enabled - will move focus inside Lock, selecting the first or autoFocusable element

src/Lock.js

+4
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const FocusLock = React.forwardRef((props, parentRef) => {
2121
disabled,
2222
noFocusGuards,
2323
persistentFocus,
24+
crossFrame,
2425
autoFocus,
2526
allowTextSelection,
2627
group,
@@ -132,6 +133,7 @@ const FocusLock = React.forwardRef((props, parentRef) => {
132133
observed={realObserved}
133134
disabled={disabled}
134135
persistentFocus={persistentFocus}
136+
crossFrame={crossFrame}
135137
autoFocus={autoFocus}
136138
whiteList={whiteList}
137139
shards={shards}
@@ -166,6 +168,7 @@ FocusLock.propTypes = {
166168
allowTextSelection: bool,
167169
autoFocus: bool,
168170
persistentFocus: bool,
171+
crossFrame: bool,
169172

170173
group: string,
171174
className: string,
@@ -189,6 +192,7 @@ FocusLock.defaultProps = {
189192
noFocusGuards: false,
190193
autoFocus: true,
191194
persistentFocus: false,
195+
crossFrame: true,
192196
allowTextSelection: undefined,
193197
group: undefined,
194198
className: undefined,

src/Trap.js

+26-13
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import * as React from 'react';
22
import PropTypes from 'prop-types';
33
import withSideEffect from 'react-clientside-effect';
4-
import moveFocusInside, { focusInside, focusIsHidden, getFocusabledIn } from 'focus-lock';
5-
import { deferAction } from './util';
6-
import { mediumFocus, mediumBlur, mediumEffect } from './medium';
4+
import moveFocusInside, {focusInside, focusIsHidden, getFocusabledIn} from 'focus-lock';
5+
import {deferAction} from './util';
6+
import {mediumFocus, mediumBlur, mediumEffect} from './medium';
77

88
const focusOnBody = () => (
99
document && document.activeElement === document.body
@@ -25,7 +25,7 @@ const focusWhitelisted = activeElement => (
2525
);
2626

2727
const recordPortal = (observerNode, portaledElement) => {
28-
lastPortaledElement = { observerNode, portaledElement };
28+
lastPortaledElement = {observerNode, portaledElement};
2929
};
3030

3131
const focusIsPortaledPair = element => (
@@ -58,11 +58,20 @@ function autoGuard(startIndex, end, step, allNodes) {
5858

5959
const extractRef = ref => ((ref && 'current' in ref) ? ref.current : ref);
6060

61+
const focusWasOutside = (crossFrameOption) => {
62+
if(crossFrameOption){
63+
// with cross frame return true for any value
64+
return Boolean(focusWasOutsideWindow);
65+
}
66+
// in other case return only of focus went a while aho
67+
return focusWasOutsideWindow === "meanwhile"
68+
}
69+
6170
const activateTrap = () => {
6271
let result = false;
6372
if (lastActiveTrap) {
6473
const {
65-
observed, persistentFocus, autoFocus, shards,
74+
observed, persistentFocus, autoFocus, shards, crossFrame,
6675
} = lastActiveTrap;
6776
const workingNode = observed || (lastPortaledElement && lastPortaledElement.portaledElement);
6877
const activeElement = document && document.activeElement;
@@ -74,7 +83,7 @@ const activateTrap = () => {
7483

7584
if (!activeElement || focusWhitelisted(activeElement)) {
7685
if (
77-
(persistentFocus || focusWasOutsideWindow)
86+
(persistentFocus || focusWasOutside(crossFrame))
7887
|| !isFreeFocus()
7988
|| (!lastActiveFocus && autoFocus)
8089
) {
@@ -101,12 +110,12 @@ const activateTrap = () => {
101110
if (document) {
102111
const newActiveElement = document && document.activeElement;
103112
const allNodes = getFocusabledIn(workingArea);
104-
const focusedItem = allNodes.find(({ node }) => node === newActiveElement);
113+
const focusedItem = allNodes.find(({node}) => node === newActiveElement);
105114
if (focusedItem) {
106115
// remove old focus
107116
allNodes
108-
.filter(({ guard, node }) => guard && node.dataset.focusAutoGuard)
109-
.forEach(({ node }) => node.removeAttribute('tabIndex'));
117+
.filter(({guard, node}) => guard && node.dataset.focusAutoGuard)
118+
.forEach(({node}) => node.removeAttribute('tabIndex'));
110119

111120
const focusedIndex = allNodes.indexOf(focusedItem);
112121
autoGuard(focusedIndex, allNodes.length, +1, allNodes);
@@ -141,7 +150,7 @@ const onFocus = (event) => {
141150

142151
const FocusWatcher = () => null;
143152

144-
const FocusTrap = ({ children }) => (
153+
const FocusTrap = ({children}) => (
145154
<div onBlur={onBlur} onFocus={onFocus}>
146155
{children}
147156
</div>
@@ -152,7 +161,11 @@ FocusTrap.propTypes = {
152161
};
153162

154163
const onWindowBlur = () => {
155-
focusWasOutsideWindow = true;
164+
focusWasOutsideWindow = "just";
165+
// using setTimeout to set this variable after React/sidecar reaction
166+
setTimeout(() => {
167+
focusWasOutsideWindow = "meanwhile";
168+
}, 0);
156169
};
157170

158171
const attachHandler = () => {
@@ -169,7 +182,7 @@ const detachHandler = () => {
169182

170183
function reducePropsToState(propsList) {
171184
return propsList
172-
.filter(({ disabled }) => !disabled);
185+
.filter(({disabled}) => !disabled);
173186
}
174187

175188
function handleStateChangeOnClient(traps) {
@@ -186,7 +199,7 @@ function handleStateChangeOnClient(traps) {
186199
if (lastTrap && !sameTrap) {
187200
lastTrap.onDeactivation();
188201
// return focus only of last trap was removed
189-
if (!traps.filter(({ id }) => id === lastTrap.id).length) {
202+
if (!traps.filter(({id}) => id === lastTrap.id).length) {
190203
// allow defer is no other trap is awaiting restore
191204
lastTrap.returnFocus(!trap);
192205
}

stories/Default.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,12 @@ class Trap extends Component {
2121

2222
render() {
2323
const {disabled} = this.state;
24+
const query = (new URL(document.location)).searchParams;
2425
return (
25-
<FocusLock disabled={this.state.disabled}>
26+
<FocusLock
27+
disabled={this.state.disabled}
28+
crossFrame={query.get('crossFrame')==="true"}
29+
>
2630
{disabled && <div>
2731
! this is a <b>real trap</b>.<br/>
2832
We will steal your focus ! <br /><br />

stories/Iframe.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class Trap extends Component {
2121

2222
render() {
2323
const {disabled} = this.state;
24+
const {crossFrame} = this.props;
2425
return (
2526
<FocusLock disabled={this.state.disabled}>
2627
{disabled && <div>
@@ -34,7 +35,7 @@ class Trap extends Component {
3435
You will cycle over this. Never leaving <br/>
3536
<input placeholder="input1"/>
3637

37-
<iframe src="/" style={{width:'100%', height: '60%'}}/>
38+
<iframe src={`/iframe.html?id=focus-lock--codesandbox-example&crossFrame=${crossFrame}`} style={{width:'100%', height: '400px'}}/>
3839

3940
<input placeholder="input2"/>
4041

@@ -49,11 +50,11 @@ class Trap extends Component {
4950
}
5051
}
5152

52-
const App = () =>
53+
const App = (props) =>
5354
<div style={styles}>
5455
<input placeholder="input1"/>
5556
<div style={bg}> Inaccessible <a href='#'>Link</a> outside</div>
56-
<Trap />
57+
<Trap {...props} />
5758
<div style={bg}> Inaccessible <a href='#'>Link</a> outside</div>
5859
<input placeholder="input1"/>
5960
</div>;

stories/Jump.js

+2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ class Trap1 extends Component {
3232
<button onClick={this.toggle}>!ACTIVATE THE TRAP!</button>
3333
</div>
3434

35+
<p>hint: guards are disabled</p>
36+
3537
{!disabled && <FocusLock returnFocus noFocusGuards>
3638
<button>BUTTON-2</button>
3739
<a href='#'>link somethere</a> <br/>

stories/index.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const frameStyle = {
3232
const Frame = ({children}) => (<div style={frameStyle}>{children}</div>);
3333

3434
storiesOf('Focus lock', module)
35-
.add('codesanbox example', () => <Frame><DefaultAll/></Frame>)
35+
.add('codesandbox example', () => <Frame><DefaultAll/></Frame>)
3636
.add('TabIndex example', () => <Frame><TabIndex/></Frame>)
3737
.add('autofocus', () => <Frame><AutoFocus/></Frame>)
3838
.add('return focus', () => <Frame><ReturnFocus/></Frame>);
@@ -77,6 +77,7 @@ storiesOf('Disabled', module)
7777

7878
storiesOf('Excotic', module)
7979
.add('video', () => <Frame><Video/></Frame>)
80-
.add('iframe', () => <Frame><Iframe/></Frame>)
80+
.add('iframe - crossframe', () => <Frame><Iframe crossFrame/></Frame>)
81+
.add('iframe - free', () => <Frame><Iframe crossFrame={false}/></Frame>)
8182
.add('sidecar', () => <Frame><SideCar/></Frame>)
8283
.add('tabbable parent', () => <Frame><TabbableParent/></Frame>);

0 commit comments

Comments
 (0)