Skip to content

Commit

Permalink
feat: shards and remove-scroll
Browse files Browse the repository at this point in the history
  • Loading branch information
theKashey committed Mar 11, 2019
1 parent 0f5d421 commit dbd4d5f
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 98 deletions.
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,24 @@ Works on any browser and any platform. Roughly `5kb`, excluding babel-runtime an
- `[onClickOutside]` - on click outside of "focus" area. (actually on any event "outside")
- `[onEscapeKey]` - on Esc key pressed (and not defaultPrevented)
- `[gapMode]` - the way removed ScrollBar would be _compensated_ - margin(default), or padding. See [scroll-locky documentation](https://github.com/theKashey/react-scroll-locky#gap-modes) to find the one you need.
- `[noIsolation]` - disables pointer event isolation
- `[shards]` - a list of Refs to be considered as a part of locks.

## Additional API
### Exposed from React-Focus-Lock
- `AutoFocusInside` - to mark autofocusable element
- `MoveFocusInside` - to move focus inside or a component mount
- `InFocusGuard` - to "guard" shard node.

### Exposed from React-Scroll-Locky
- `FocusPane` - to create a container with proper dimensions (it's more about right coordinate) set.

See [react-focus-lock](https://github.com/theKashey/react-focus-lock) for details.

### Exposed from React-Remove-Scroll
- `classNames.fullWidth` - "100%" width (will not change on scrollbar removal)
- `classNames.zeroRight` - "0" right (will not change on scrollbar removal)

See [React-Remove-Scroll](https://github.com/theKashey/react-remove-scroll) for details.

> PS: Version 1 used React-scroll-locky which was replaced by remove-scroll.
# Licence
MIT
Expand Down
104 changes: 73 additions & 31 deletions example/app.tsx
Original file line number Diff line number Diff line change
@@ -1,53 +1,95 @@
import * as React from 'react';
import {Component} from 'react';
import {GHCorner} from 'react-gh-corner';
import {Toggle} from 'react-powerplug';
import {AppWrapper} from './styled';
import {FocusOn, FocusPane, AutoFocusInside, MoveFocusInside} from "../src";
import {FocusOn, AutoFocusInside, MoveFocusInside, InFocusGuard, classNames} from "../src";

export interface AppState {
enabled: boolean;
}

const FocusPane = ({children}) => <div className={classNames.fullWidth}>{children}</div>;

const ScrollBox = React.forwardRef(({children}, ref: any) => (
<div ref={ref} style={{overflow: 'scroll', height: '200px', width: '300px', backgroundColor: 'rgba(0,0,0,0.3)'}}>
<button>{children}</button>
<ul>
{Array(100).fill(1).map((_, index) => <li>{index}</li>)}
</ul>
<InFocusGuard>
<button>{children}</button>
</InFocusGuard>
</div>
));

const repoUrl = 'https://github.com/zzarcon/';
export default class App extends Component <{}, AppState> {
state: AppState = {
enabled: false
};

toggleRef = React.createRef<any>();
scrollRef = React.createRef<any>()

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

render() {
return (
<AppWrapper>
<FocusPane>
<GHCorner openInNewTab href={repoUrl}/>
<button>outside</button>
outside
<FocusOn
enabled={this.state.enabled}
// onClickOutside={this.toggle}
onEscapeKey={this.toggle}
>
inside
<button>inside</button>

<button onClick={this.toggle}>{this.state.enabled ? 'disable' : 'enable'}</button>
<MoveFocusInside key={`k-${this.state.enabled}`}>
<button>inside</button>
</MoveFocusInside>
<button>inside</button>
</FocusOn>
<button>outside</button>
Example!
{
Array(100).fill(1).map((_, x) =>
<div key={`k${x}`}>
{Array(100).fill(1).map((_, x) => <span key={`k${x}`}> *{x}</span>)}
</div>
)
}
</FocusPane>
</AppWrapper>
<Toggle>
{({on, toggle}) => (
<AppWrapper>
<FocusPane>
<GHCorner openInNewTab href={repoUrl}/>
<button>outside</button>
outside

<button onClick={toggle} ref={this.toggleRef}>toggle drop</button>
<button onClick={toggle}>toggle drop 2</button>
{on && <div style={{backgroundColor: '#EEE'}}>
<FocusOn
scrollLock={true}
onClickOutside={toggle}
onEscapeKey={toggle}
shards={[this.toggleRef, this.scrollRef]}
>
Holala!!
<button onClick={toggle}>close</button>
<ScrollBox>innerbox</ScrollBox>
</FocusOn>
</div>}


<ScrollBox ref={this.scrollRef}>outer box</ScrollBox>


<FocusOn
enabled={this.state.enabled}
// onClickOutside={this.toggle}
onEscapeKey={this.toggle}
>
inside
<button>inside</button>

<button onClick={this.toggle}>{this.state.enabled ? 'disable' : 'enable'}</button>
<MoveFocusInside key={`k-${this.state.enabled}`}>
<button>inside</button>
</MoveFocusInside>
<button>inside</button>
</FocusOn>
<button>outside</button>
Example!
{
Array(100).fill(1).map((_, x) =>
<div key={`k${x}`}>
{Array(10).fill(1).map((_, x) => <span key={`k${x}`}> *{x}</span>)}
</div>
)
}
</FocusPane>
</AppWrapper>
)}
</Toggle>
)
}
}
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@
"author": "Anton Korzunov <thekashey@gmail.com>",
"license": "MIT",
"devDependencies": {
"react-powerplug": "^1.0.0",
"size-limit": "^0.21.1",
"ts-react-toolbox": "^0.1.22"
},
"dependencies": {
"aria-hidden": "^1.0.0",
"react-focus-lock": "^1.17.6",
"react-scroll-locky": "^1.4.0"
"aria-hidden": "^1.1.1",
"react-focus-lock": "^1.18.2",
"react-remove-scroll": "^1.0.5"
},
"engines": {
"node": ">=8.5.0"
Expand Down
16 changes: 16 additions & 0 deletions src/InteractivityDisabler.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as React from 'react';
import {styleSinglentone} from 'react-style-singleton';

const Style = styleSinglentone();

export const focusHiddenMarker = 'data-focus-on-hidden';

const styles = `
[${focusHiddenMarker}] {
pointer-events: none !important;
}
`;

export const InteractivityDisabler: React.SFC = () => (
<Style styles={styles}/>
)
98 changes: 68 additions & 30 deletions src/component.tsx
Original file line number Diff line number Diff line change
@@ -1,74 +1,112 @@
import * as React from 'react';
import {Component} from 'react';
import {ScrollLocky} from 'react-scroll-locky';
import {RemoveScroll} from 'react-remove-scroll';
import ReactFocusLock from 'react-focus-lock'
import {hideOthers} from 'aria-hidden';

type GapMode = 'padding' | 'margin';
import {focusHiddenMarker, InteractivityDisabler} from "./InteractivityDisabler";

export interface ReactFocusOnProps {
enabled?: boolean;
scrollLock?: boolean;
focusLock?: boolean;

autoFocus?: boolean;
onActivation?: (node: HTMLElement) => void;
onDeactivation?: () => void;

gapMode?: GapMode;

onClickOutside?: () => void;
onEscapeKey?: (event: Event) => void;

noIsolation?: boolean;

shards?: Array<React.RefObject<any> | HTMLElement>;
}

const extractRef = (ref: React.RefObject<any> | HTMLElement): HTMLElement => (
('current' in ref) ? ref.current : ref
);

export class ReactFocusOn extends Component<ReactFocusOnProps> {
private _undo?: () => void;

onActivation = (node: HTMLElement) => {
this._undo = hideOthers(node);
private lockProps = {
onClick: (e: React.MouseEvent) => e.preventDefault(),
};

private onActivation = (node: HTMLElement) => {
this._undo = hideOthers(
[node, ...(this.props.shards || []).map(extractRef)],
document.body,
this.props.noIsolation ? undefined : focusHiddenMarker
);
const {onActivation} = this.props;
if (onActivation) {
onActivation(node);
}
node.addEventListener('keyup', this.onKeyPress);
document.body.addEventListener('keyup', this.onKeyPress);
document.addEventListener('keyup', this.onKeyPress);
document.addEventListener('click', this.onClick);
};

onDeactivation = (node: HTMLElement) => {
private onDeactivation = (node: HTMLElement) => {
this._undo!();
const {onDeactivation} = this.props;
if (onDeactivation) {
onDeactivation();
}
node.removeEventListener('keyup', this.onKeyPress);
document.body.removeEventListener('keyup', this.onKeyPress);
document.removeEventListener('keyup', this.onKeyPress);
document.removeEventListener('click', this.onClick);
};

onKeyPress = (event: KeyboardEvent) => {
private onKeyPress = (event: KeyboardEvent) => {
if (event.defaultPrevented) {
return;
}
const code = event.key || event.keyCode;
if ((event.code === 'Escape' || code === 27) && this.props.onEscapeKey) {
if ((event.code === 'Escape' || code === 'Escape' || code === 27) && this.props.onEscapeKey) {
this.props.onEscapeKey(event);
}
};

private onClick = (event: MouseEvent) => {
const {shards = [], onClickOutside} = this.props;
if (event.defaultPrevented) {
return;
}
if (
shards
.map(extractRef)
.some(node => node && node.contains(event.target as any) || node === event.target)
) {
return;
}
if (onClickOutside) {
onClickOutside();
}
};

render() {
const {children, autoFocus, gapMode, onClickOutside, enabled = true} = this.props;
const {children, autoFocus, shards, enabled = true, scrollLock = true, focusLock = true} = this.props;
return (
<ScrollLocky
enabled={enabled}
onEscape={onClickOutside}
gapMode={gapMode}
>
<ReactFocusLock
returnFocus
autoFocus={autoFocus}
onActivation={this.onActivation}
onDeactivation={this.onDeactivation}
disabled={!enabled}
<>
<RemoveScroll
enabled={enabled && scrollLock}
shards={shards}
>
{children}
</ReactFocusLock>
</ScrollLocky>
<InteractivityDisabler/>
<ReactFocusLock
returnFocus
autoFocus={autoFocus}
onActivation={this.onActivation}
onDeactivation={this.onDeactivation}
disabled={!(enabled && focusLock)}
shards={shards}

lockProps={this.lockProps}
>
{children}
</ReactFocusLock>
</RemoveScroll>
</>
);
}
}
}
9 changes: 7 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import {RemoveScroll} from 'react-remove-scroll';

export {ReactFocusOn as FocusOn} from './component';
export {ScrollLockyPane as FocusPane} from 'react-scroll-locky';
export {AutoFocusInside, MoveFocusInside} from 'react-focus-lock';
export {AutoFocusInside, MoveFocusInside, InFocusGuard} from 'react-focus-lock';

export const classNames = {
...RemoveScroll.classNames,
};
Loading

0 comments on commit dbd4d5f

Please sign in to comment.