Skip to content

Commit

Permalink
feat: add crossFrame property to control iframe behaviour, fixes #104
Browse files Browse the repository at this point in the history
  • Loading branch information
theKashey committed Apr 17, 2020
1 parent a3ef2f0 commit 486a7e0
Show file tree
Hide file tree
Showing 7 changed files with 52 additions and 19 deletions.
8 changes: 8 additions & 0 deletions interfaces.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ export interface ReactFocusLockProps<ChildrenType = React.ReactNode, LockProps=R
*/
persistentFocus?: boolean;

/**
* enables aggressive focus capturing within iframes
* - once disabled allows focus to move outside of iframe, if enabled inside iframe
* - once enabled keep focus in the lock, no matter where lock is active (default)
* @default true
*/
crossFrame: boolean;

/**
* enables or disables autoFocusing feature.
* If enabled - will move focus inside Lock, selecting the first or autoFocusable element
Expand Down
4 changes: 4 additions & 0 deletions src/Lock.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const FocusLock = React.forwardRef((props, parentRef) => {
disabled,
noFocusGuards,
persistentFocus,
crossFrame,
autoFocus,
allowTextSelection,
group,
Expand Down Expand Up @@ -132,6 +133,7 @@ const FocusLock = React.forwardRef((props, parentRef) => {
observed={realObserved}
disabled={disabled}
persistentFocus={persistentFocus}
crossFrame={crossFrame}
autoFocus={autoFocus}
whiteList={whiteList}
shards={shards}
Expand Down Expand Up @@ -166,6 +168,7 @@ FocusLock.propTypes = {
allowTextSelection: bool,
autoFocus: bool,
persistentFocus: bool,
crossFrame: bool,

group: string,
className: string,
Expand All @@ -189,6 +192,7 @@ FocusLock.defaultProps = {
noFocusGuards: false,
autoFocus: true,
persistentFocus: false,
crossFrame: true,
allowTextSelection: undefined,
group: undefined,
className: undefined,
Expand Down
39 changes: 26 additions & 13 deletions src/Trap.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import withSideEffect from 'react-clientside-effect';
import moveFocusInside, { focusInside, focusIsHidden, getFocusabledIn } from 'focus-lock';
import { deferAction } from './util';
import { mediumFocus, mediumBlur, mediumEffect } from './medium';
import moveFocusInside, {focusInside, focusIsHidden, getFocusabledIn} from 'focus-lock';
import {deferAction} from './util';
import {mediumFocus, mediumBlur, mediumEffect} from './medium';

const focusOnBody = () => (
document && document.activeElement === document.body
Expand All @@ -25,7 +25,7 @@ const focusWhitelisted = activeElement => (
);

const recordPortal = (observerNode, portaledElement) => {
lastPortaledElement = { observerNode, portaledElement };
lastPortaledElement = {observerNode, portaledElement};
};

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

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

const focusWasOutside = (crossFrameOption) => {
if(crossFrameOption){
// with cross frame return true for any value
return Boolean(focusWasOutsideWindow);
}
// in other case return only of focus went a while aho
return focusWasOutsideWindow === "meanwhile"
}

const activateTrap = () => {
let result = false;
if (lastActiveTrap) {
const {
observed, persistentFocus, autoFocus, shards,
observed, persistentFocus, autoFocus, shards, crossFrame,
} = lastActiveTrap;
const workingNode = observed || (lastPortaledElement && lastPortaledElement.portaledElement);
const activeElement = document && document.activeElement;
Expand All @@ -74,7 +83,7 @@ const activateTrap = () => {

if (!activeElement || focusWhitelisted(activeElement)) {
if (
(persistentFocus || focusWasOutsideWindow)
(persistentFocus || focusWasOutside(crossFrame))
|| !isFreeFocus()
|| (!lastActiveFocus && autoFocus)
) {
Expand All @@ -101,12 +110,12 @@ const activateTrap = () => {
if (document) {
const newActiveElement = document && document.activeElement;
const allNodes = getFocusabledIn(workingArea);
const focusedItem = allNodes.find(({ node }) => node === newActiveElement);
const focusedItem = allNodes.find(({node}) => node === newActiveElement);
if (focusedItem) {
// remove old focus
allNodes
.filter(({ guard, node }) => guard && node.dataset.focusAutoGuard)
.forEach(({ node }) => node.removeAttribute('tabIndex'));
.filter(({guard, node}) => guard && node.dataset.focusAutoGuard)
.forEach(({node}) => node.removeAttribute('tabIndex'));

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

const FocusWatcher = () => null;

const FocusTrap = ({ children }) => (
const FocusTrap = ({children}) => (
<div onBlur={onBlur} onFocus={onFocus}>
{children}
</div>
Expand All @@ -152,7 +161,11 @@ FocusTrap.propTypes = {
};

const onWindowBlur = () => {
focusWasOutsideWindow = true;
focusWasOutsideWindow = "just";
// using setTimeout to set this variable after React/sidecar reaction
setTimeout(() => {
focusWasOutsideWindow = "meanwhile";
}, 0);
};

const attachHandler = () => {
Expand All @@ -169,7 +182,7 @@ const detachHandler = () => {

function reducePropsToState(propsList) {
return propsList
.filter(({ disabled }) => !disabled);
.filter(({disabled}) => !disabled);
}

function handleStateChangeOnClient(traps) {
Expand All @@ -186,7 +199,7 @@ function handleStateChangeOnClient(traps) {
if (lastTrap && !sameTrap) {
lastTrap.onDeactivation();
// return focus only of last trap was removed
if (!traps.filter(({ id }) => id === lastTrap.id).length) {
if (!traps.filter(({id}) => id === lastTrap.id).length) {
// allow defer is no other trap is awaiting restore
lastTrap.returnFocus(!trap);
}
Expand Down
6 changes: 5 additions & 1 deletion stories/Default.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@ class Trap extends Component {

render() {
const {disabled} = this.state;
const query = (new URL(document.location)).searchParams;
return (
<FocusLock disabled={this.state.disabled}>
<FocusLock
disabled={this.state.disabled}
crossFrame={query.get('crossFrame')==="true"}
>
{disabled && <div>
! this is a <b>real trap</b>.<br/>
We will steal your focus ! <br /><br />
Expand Down
7 changes: 4 additions & 3 deletions stories/Iframe.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class Trap extends Component {

render() {
const {disabled} = this.state;
const {crossFrame} = this.props;
return (
<FocusLock disabled={this.state.disabled}>
{disabled && <div>
Expand All @@ -34,7 +35,7 @@ class Trap extends Component {
You will cycle over this. Never leaving <br/>
<input placeholder="input1"/>

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

<input placeholder="input2"/>

Expand All @@ -49,11 +50,11 @@ class Trap extends Component {
}
}

const App = () =>
const App = (props) =>
<div style={styles}>
<input placeholder="input1"/>
<div style={bg}> Inaccessible <a href='#'>Link</a> outside</div>
<Trap />
<Trap {...props} />
<div style={bg}> Inaccessible <a href='#'>Link</a> outside</div>
<input placeholder="input1"/>
</div>;
Expand Down
2 changes: 2 additions & 0 deletions stories/Jump.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ class Trap1 extends Component {
<button onClick={this.toggle}>!ACTIVATE THE TRAP!</button>
</div>

<p>hint: guards are disabled</p>

{!disabled && <FocusLock returnFocus noFocusGuards>
<button>BUTTON-2</button>
<a href='#'>link somethere</a> <br/>
Expand Down
5 changes: 3 additions & 2 deletions stories/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const frameStyle = {
const Frame = ({children}) => (<div style={frameStyle}>{children}</div>);

storiesOf('Focus lock', module)
.add('codesanbox example', () => <Frame><DefaultAll/></Frame>)
.add('codesandbox example', () => <Frame><DefaultAll/></Frame>)
.add('TabIndex example', () => <Frame><TabIndex/></Frame>)
.add('autofocus', () => <Frame><AutoFocus/></Frame>)
.add('return focus', () => <Frame><ReturnFocus/></Frame>);
Expand Down Expand Up @@ -77,6 +77,7 @@ storiesOf('Disabled', module)

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

0 comments on commit 486a7e0

Please sign in to comment.