Skip to content

Commit

Permalink
fix: nested return focus, fixes #68
Browse files Browse the repository at this point in the history
  • Loading branch information
theKashey committed Jun 15, 2019
1 parent 8b1c956 commit df4b313
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 14 deletions.
78 changes: 78 additions & 0 deletions _tests/FocusLock.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import EnzymeReactAdapter from 'enzyme-adapter-react-16';

configureEnzyme({adapter: new EnzymeReactAdapter()});

const tick = () => new Promise(resolve => setTimeout(resolve, 1));

describe('react-focus-lock', () => {
beforeEach(() => {
Expand Down Expand Up @@ -148,6 +149,83 @@ describe('react-focus-lock', () => {
}, 1);
});

it('Should return focus to the original place - nested case', async () => {
let counter = 0;

class Test extends Component {
state = {
focused: false,
c: counter++,
};

toggle = () => {
this.setState({
focused: !this.state.focused,
});
};

render() {
return (
<div>
<FocusLock returnFocus>
<div>
<span className={`clickTarget${this.state.c}`} onClick={this.toggle}/>
text
<button className="action2">d-action{this.state.c}</button>
{this.state.focused && <Test/>}
text
</div>
</FocusLock>
</div>
);
}
}

const wrapper = mount((
<div>
<div>
text
<button className="action1">d-action1</button>
text
<button className="action1" autoFocus>top focused</button>
</div>
<Test/>
</div>
));

expect(document.activeElement.innerHTML).to.be.equal('d-action0');
wrapper.find('.clickTarget0').simulate('click');

await tick();

expect(document.activeElement.innerHTML).to.be.equal('d-action1');
wrapper.find('.clickTarget1').simulate('click');

await tick();

expect(document.activeElement.innerHTML).to.be.equal('d-action2');
wrapper.find('.clickTarget2').simulate('click');

await tick();

expect(document.activeElement.innerHTML).to.be.equal('d-action3');
wrapper.find('.clickTarget2').simulate('click');

await tick();

expect(document.activeElement.innerHTML).to.be.equal('d-action2');
wrapper.find('.clickTarget1').simulate('click');

await tick();

expect(document.activeElement.innerHTML).to.be.equal('d-action1');
wrapper.find('.clickTarget0').simulate('click');

await tick();

expect(document.activeElement.innerHTML).to.be.equal('d-action0');
});

it('Should focus on inputs', (done) => {
const wrapper = mount(<div>
<div>
Expand Down
20 changes: 12 additions & 8 deletions src/Lock.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,6 @@ class FocusLock extends Component {

onDeactivation = () => {
this.isActive = false;
if (
this.props.returnFocus &&
this.originalFocusedElement &&
this.originalFocusedElement.focus
) {
this.originalFocusedElement.focus();
this.originalFocusedElement = null;
}
if (this.props.onDeactivation) {
this.props.onDeactivation(this.state.observed);
}
Expand All @@ -59,6 +51,17 @@ class FocusLock extends Component {
}
};

returnFocus = () => {
if (
this.props.returnFocus &&
this.originalFocusedElement &&
this.originalFocusedElement.focus
) {
this.originalFocusedElement.focus();
this.originalFocusedElement = null;
}
};

// active status is tracked outside React state
isActive = false;

Expand Down Expand Up @@ -122,6 +125,7 @@ class FocusLock extends Component {
shards={shards}
onActivation={this.onActivation}
onDeactivation={this.onDeactivation}
returnFocus={this.returnFocus}
/>
)}
{children}
Expand Down
10 changes: 7 additions & 3 deletions src/Trap.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,11 +167,11 @@ const detachHandler = () => {

function reducePropsToState(propsList) {
return propsList
.filter(({ disabled }) => !disabled)
.slice(-1)[0];
.filter(({ disabled }) => !disabled);
}

function handleStateChangeOnClient(trap) {
function handleStateChangeOnClient(traps) {
const trap = traps.slice(-1)[0];
if (trap && !lastActiveTrap) {
attachHandler();
}
Expand All @@ -183,6 +183,10 @@ function handleStateChangeOnClient(trap) {

if (lastTrap && !sameTrap) {
lastTrap.onDeactivation();
// return focus only of last trap was removed
if (!traps.filter(({ onActivation }) => onActivation === lastTrap.onActivation).length) {
lastTrap.returnFocus();
}
}

if (trap) {
Expand Down
16 changes: 13 additions & 3 deletions stories/ReturnFocus.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,27 @@ const bg = {
backgroundColor: '#FEE'
};

let counter =0;

class Trap1 extends Component {
state = {
disabled: true
disabled: !this.props.active,
sub: false,
x: counter++,
};

toggle = () => {
setTimeout(() => {
this.setState({disabled: !this.state.disabled});
}, 1000);
}, 10);
};

openNew = () => {
this.setState({sub: true});
}

render() {
const {disabled} = this.state;
const {disabled, sub, x} = this.state;
return (
<div>
Place focus here
Expand All @@ -40,6 +48,8 @@ class Trap1 extends Component {
<button>BUTTON</button>
<a href='#'>link somethere</a> <br/>
<button onClick={this.toggle}>DEACTIVATE</button>
<button onClick={this.openNew}>Open another modal {x}</button>
{sub && <Trap1 active/>}
</FocusLock>
}
</div>
Expand Down

0 comments on commit df4b313

Please sign in to comment.