docs: initial works on formalized docs page
This commit is contained in:
Родитель
5efb2f6889
Коммит
e964f7ff0d
|
@ -1,16 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>arcade-machine-react</title>
|
||||
<link rel="stylesheet" href="./style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.10/lodash.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/benchmark/2.1.4/benchmark.min.js"></script>
|
||||
<script src="./demo/index.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,132 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { Suite, Event } from 'benchmark';
|
||||
import { IBenchmark, benchmarks } from './benchmarks';
|
||||
|
||||
declare const Benchmark: any;
|
||||
|
||||
/**
|
||||
* BenchmarkRow is rendered for each element in the list of benchmarks.
|
||||
*/
|
||||
export class BenchmarkRow extends React.Component<
|
||||
{ case: IBenchmark<any> },
|
||||
{ result: string; running: boolean }
|
||||
> {
|
||||
private readonly fixtureRef = React.createRef<HTMLTableDataCellElement>();
|
||||
|
||||
public state = { result: '', running: false };
|
||||
|
||||
private runSelf = () => {
|
||||
const suite = new Benchmark.Suite();
|
||||
this.addTestCase(suite);
|
||||
suite.run({ async: true });
|
||||
};
|
||||
|
||||
public addTestCase(suite: Suite) {
|
||||
const { name, setup, iterate } = this.props.case;
|
||||
const state = { container: this.fixtureRef.current! };
|
||||
|
||||
this.setState({ result: 'queued...' });
|
||||
|
||||
let setupResult: any;
|
||||
let didRun = false;
|
||||
let previous: any;
|
||||
suite.add(
|
||||
name,
|
||||
() => {
|
||||
if (!didRun) {
|
||||
this.setState({ result: 'running', running: true });
|
||||
setupResult = setup ? setup(state) : null;
|
||||
didRun = true;
|
||||
}
|
||||
|
||||
previous = iterate(state, setupResult, previous);
|
||||
},
|
||||
{
|
||||
onComplete: (ev: Event) => {
|
||||
const target: any = ev.target;
|
||||
if (target.error) {
|
||||
this.setState({ result: target.error.toString(), running: false });
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
running: false,
|
||||
result: `${Math.round(target.hz)} ops/sec ± ${target.stats.rme.toFixed(2)} (${(
|
||||
target.stats.mean * 1000
|
||||
).toFixed(3)}ms each)`,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<tr>
|
||||
<td>{this.props.case.name}</td>
|
||||
<td ref={this.fixtureRef}>{this.state.running ? <this.props.case.fixture /> : null}</td>
|
||||
<td>
|
||||
{this.state.result}
|
||||
<br />
|
||||
<button onClick={this.runSelf} disabled={this.state.running}>
|
||||
Run This
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Benchmarks is the table of runnable benchmarks.
|
||||
*/
|
||||
export class Benchmarks extends React.Component<{}, { running: boolean }> {
|
||||
public state = { running: false };
|
||||
|
||||
private readonly benchmarkRows: React.RefObject<BenchmarkRow>[] = benchmarks.map(() =>
|
||||
React.createRef(),
|
||||
);
|
||||
|
||||
private runAll = () => {
|
||||
const suite = new Benchmark.Suite();
|
||||
|
||||
this.benchmarkRows.forEach(row => {
|
||||
row.current!.addTestCase(suite);
|
||||
});
|
||||
|
||||
suite.on('complete', () => {
|
||||
this.setState({ running: false });
|
||||
});
|
||||
|
||||
suite.run({ async: true });
|
||||
};
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<h1>Benchmarks</h1>
|
||||
<div className="area">
|
||||
<table className="benchmarks">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Benchmark</th>
|
||||
<th>DOM Fixture</th>
|
||||
<th>
|
||||
Result{' '}
|
||||
<button onClick={this.runAll} disabled={this.state.running}>
|
||||
Run All
|
||||
</button>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{benchmarks.map((benchmark, i) => (
|
||||
<BenchmarkRow case={benchmark} ref={this.benchmarkRows[i]} key={i} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
import * as React from 'react';
|
||||
|
||||
export const FocusFormDemo = () => (
|
||||
<React.Fragment>
|
||||
<h1>A Form</h1>
|
||||
<div className="area">
|
||||
<form>
|
||||
<div>
|
||||
<input tabIndex={0} placeholder="Username" />
|
||||
</div>
|
||||
<div>
|
||||
<input tabIndex={0} placeholder="Password" type="password" />
|
||||
</div>
|
||||
<div>
|
||||
<textarea tabIndex={0} />
|
||||
</div>
|
||||
<div>
|
||||
<button tabIndex={0}>Submit</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
|
@ -1,39 +0,0 @@
|
|||
import * as React from 'react';
|
||||
|
||||
export const FocusGridDemo = () => (
|
||||
<React.Fragment>
|
||||
<h1>A Grid</h1>
|
||||
<div className="area">
|
||||
<div>
|
||||
<div className="square" tabIndex={0} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="square" tabIndex={0} />
|
||||
<div className="square" tabIndex={0} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="square" tabIndex={0} />
|
||||
<div className="square" tabIndex={0} />
|
||||
<div className="square" tabIndex={0} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="square" tabIndex={0} />
|
||||
<div className="square" tabIndex={0} />
|
||||
<div className="square" tabIndex={0} />
|
||||
<div className="square" tabIndex={0} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="square" tabIndex={0} />
|
||||
<div className="square" tabIndex={0} />
|
||||
<div className="square" tabIndex={0} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="square" tabIndex={0} />
|
||||
<div className="square" tabIndex={0} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="square" tabIndex={0} />
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
|
@ -1,41 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { ArcOnIncoming } from '../../src';
|
||||
|
||||
const ShouldNotFocus = ArcOnIncoming(
|
||||
ev => alert(`Unexpected incoming focus in: ${ev.next!.parentElement!.innerHTML}`),
|
||||
(props: { style?: any }) => <div className="square" tabIndex={0} {...props} />,
|
||||
);
|
||||
|
||||
export const FocusHiddenDemo = () => (
|
||||
<React.Fragment>
|
||||
<h1>Hidden Boxes</h1>
|
||||
To the right of each description is an invisible box. These should not be focusable, and will
|
||||
alert if you try to focus them.
|
||||
<div className="area">
|
||||
<div className="visibility-test">
|
||||
<div className="case-name" tabIndex={0}>
|
||||
base case (should focus)
|
||||
</div>
|
||||
<div>
|
||||
<div className="square" tabIndex={0} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="visibility-test">
|
||||
<div className="case-name" tabIndex={0}>
|
||||
display: none
|
||||
</div>
|
||||
<div>
|
||||
<ShouldNotFocus style={{ display: 'none' }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="visibility-test">
|
||||
<div className="case-name" tabIndex={0}>
|
||||
parent display: none
|
||||
</div>
|
||||
<div style={{ display: 'none' }}>
|
||||
<ShouldNotFocus />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
|
@ -1,53 +0,0 @@
|
|||
import * as React from 'react';
|
||||
|
||||
export class FocusHistoryDemo extends React.Component<{}, { ticker: number }> {
|
||||
private readonly boxes: string[] = [];
|
||||
|
||||
constructor(props: {}) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
ticker: 0,
|
||||
};
|
||||
|
||||
for (let i = 0; i < 50; i++) {
|
||||
this.boxes.push(String(`Box ${i}`));
|
||||
}
|
||||
|
||||
let k = 0;
|
||||
setInterval(() => this.setState({ ...this.state, ticker: ++k }), 2500);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<h1>History</h1>
|
||||
<h2>Prefer last focused element</h2>
|
||||
<div className="area" style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<div
|
||||
className="box"
|
||||
tabIndex={0}
|
||||
style={{ display: 'inline-block', marginLeft: 50, width: 150, height: 150 }}
|
||||
/>
|
||||
<div id="focus-inside1" style={{ display: 'inline-block', margin: 50 }}>
|
||||
<div className="box" tabIndex={0} style={{ width: 50, height: 50 }} />
|
||||
<div className="box" tabIndex={0} style={{ width: 50, height: 50 }} />
|
||||
<div className="box" tabIndex={0} style={{ width: 50, height: 50 }} />
|
||||
</div>
|
||||
</div>
|
||||
<h1>Adding/Removing Elements</h1>
|
||||
<div className="area">
|
||||
{this.boxes.map((box, i) => (
|
||||
<div className="box-wrapper" key={i}>
|
||||
{(i + this.state.ticker) % 2 === 0 ? (
|
||||
<div className="box" tabIndex={0}>
|
||||
{box}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
import { ArcAutoFocus, ArcUp, ArcDown } from '../../src';
|
||||
import * as React from 'react';
|
||||
|
||||
const AutofocusBox = ArcAutoFocus(
|
||||
class extends React.PureComponent<{ onClick: () => void }> {
|
||||
public render() {
|
||||
return (
|
||||
<div className="box" tabIndex={0} onClick={this.props.onClick}>
|
||||
I capture default focus! Click me to toggle!
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const UpDownOverrideBox = ({ index }: { index: number }) => (
|
||||
<div id={`override${index}`} className="box" tabIndex={0}>
|
||||
up/down override
|
||||
</div>
|
||||
);
|
||||
|
||||
const UpDownOverride1 = ArcUp('#override3', ArcDown('#override2', UpDownOverrideBox));
|
||||
const UpDownOverride2 = ArcUp('#override1', ArcDown('#override3', UpDownOverrideBox));
|
||||
const UpDownOverride3 = ArcUp('#override2', ArcDown('#override1', UpDownOverrideBox));
|
||||
|
||||
export class FocusHooksDemo extends React.Component<{}, { showAFBox: boolean }> {
|
||||
constructor(props: {}) {
|
||||
super(props);
|
||||
this.state = { showAFBox: true };
|
||||
}
|
||||
|
||||
private readonly toggleAFBox = () => {
|
||||
this.setState({ showAFBox: false });
|
||||
setTimeout(() => this.setState({ ...this.state, showAFBox: true }), 1000);
|
||||
};
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<h1>Special Handlers</h1>
|
||||
<div className="area">
|
||||
<div className="box-wrapper" style={{ width: '200px' }}>
|
||||
{this.state.showAFBox ? <AutofocusBox onClick={this.toggleAFBox} /> : null}
|
||||
</div>
|
||||
<div className="box-wrapper">
|
||||
<UpDownOverride1 index={1} />
|
||||
</div>
|
||||
<div className="box-wrapper">
|
||||
<UpDownOverride2 index={2} />
|
||||
</div>
|
||||
<div className="box-wrapper">
|
||||
<UpDownOverride3 index={3} />
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,104 +0,0 @@
|
|||
import { FocusArea, FocusExclude } from '../../src';
|
||||
import * as React from 'react';
|
||||
|
||||
export const FocusInsideDemo = () => (
|
||||
<React.Fragment>
|
||||
<h1>Focus Inside</h1>
|
||||
Transfer focus to elements inside me
|
||||
<div className="area" style={{ display: 'flex', justifyContent: 'space-evenly' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
With arc-focus-inside
|
||||
<FocusArea>
|
||||
<div id="focus-inside1" className="area">
|
||||
<div className="square" tabIndex={0} />
|
||||
<div className="square" tabIndex={0} />
|
||||
<div className="square" tabIndex={0} />
|
||||
</div>
|
||||
</FocusArea>
|
||||
<FocusArea>
|
||||
<div id="focus-inside1" className="area">
|
||||
<div className="square" tabIndex={0} />
|
||||
<div className="square" tabIndex={0} style={{ marginLeft: '100px' }} />
|
||||
</div>
|
||||
</FocusArea>
|
||||
<FocusArea>
|
||||
<div id="focus-inside1" className="area">
|
||||
<div className="square" tabIndex={0} />
|
||||
<div className="square" tabIndex={0} />
|
||||
<div className="square" tabIndex={0} />
|
||||
</div>
|
||||
</FocusArea>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
With focus excluded
|
||||
<FocusExclude>
|
||||
<div id="focus-inside1" className="area">
|
||||
<div className="square" tabIndex={0} />
|
||||
<div className="square" tabIndex={0} />
|
||||
<div className="square" tabIndex={0} />
|
||||
</div>
|
||||
</FocusExclude>
|
||||
<FocusExclude>
|
||||
<div id="focus-inside1" className="area">
|
||||
<div className="square" tabIndex={0} />
|
||||
<div className="square" tabIndex={0} style={{ marginLeft: '100px' }} />
|
||||
</div>
|
||||
</FocusExclude>
|
||||
<FocusExclude>
|
||||
<div id="focus-inside1" className="area">
|
||||
<div className="square" tabIndex={0} />
|
||||
<div className="square" tabIndex={0} />
|
||||
<div className="square" tabIndex={0} />
|
||||
</div>
|
||||
</FocusExclude>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
Without arc-focus-inside
|
||||
<div id="focus-inside1" className="area">
|
||||
<div className="square" tabIndex={0} />
|
||||
<div className="square" tabIndex={0} />
|
||||
<div className="square" tabIndex={0} />
|
||||
</div>
|
||||
<div id="focus-inside1" className="area">
|
||||
<div className="square" tabIndex={0} />
|
||||
<div
|
||||
className="square"
|
||||
tabIndex={0}
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
width: '50px',
|
||||
height: '50px',
|
||||
marginLeft: '100px',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div id="focus-inside1" className="area">
|
||||
<div className="square" tabIndex={0} />
|
||||
<div className="square" tabIndex={0} />
|
||||
<div className="square" tabIndex={0} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
|
@ -1,70 +0,0 @@
|
|||
import { ArcFocusTrap, FocusTrap } from '../../src';
|
||||
import * as React from 'react';
|
||||
|
||||
const Dialog = ArcFocusTrap(
|
||||
class extends React.Component<{ onClose: () => void }, { showTrap: boolean }> {
|
||||
private showTrap = () => this.setState({ showTrap: true });
|
||||
private hideTrap = () => this.setState({ showTrap: false });
|
||||
|
||||
constructor(props: { onClose: () => void }) {
|
||||
super(props);
|
||||
this.state = { showTrap: false };
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div className="area dialog">
|
||||
<div>
|
||||
<button tabIndex={0}>Button 1</button>
|
||||
<button tabIndex={0}>Button 2</button>
|
||||
</div>
|
||||
<div>
|
||||
<button tabIndex={0} onClick={this.showTrap}>
|
||||
Show nested focus trap
|
||||
</button>
|
||||
</div>
|
||||
{this.state.showTrap ? (
|
||||
<FocusTrap>
|
||||
<div style={{ border: '1px solid red' }}>
|
||||
<button tabIndex={0}>Button 1</button>
|
||||
<button tabIndex={0}>Button 2</button>
|
||||
<button onClick={this.hideTrap} tabIndex={0}>
|
||||
Hide nested trap
|
||||
</button>
|
||||
</div>
|
||||
</FocusTrap>
|
||||
) : null}
|
||||
<div>
|
||||
<button onClick={this.props.onClose} tabIndex={0}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export class FocusTrapsDemo extends React.Component<{}, { dialog: boolean }> {
|
||||
private readonly onDialogOpen = () => {
|
||||
this.setState({ dialog: true });
|
||||
};
|
||||
|
||||
private readonly onDialogClose = () => {
|
||||
this.setState({ dialog: false });
|
||||
};
|
||||
|
||||
public state = { dialog: false };
|
||||
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<h1>Focus Child Elements Only</h1>
|
||||
<button tabIndex={0} onClick={this.onDialogOpen}>
|
||||
Open Dialog
|
||||
</button>
|
||||
{this.state.dialog ? <Dialog onClose={this.onDialogClose} /> : null}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
import { selectSimpleElementBenchmark } from './select-simple-element';
|
||||
import { selectArcadeMachineBenchmark } from './select-arcade-machine';
|
||||
import { virtualArcadeSelectBenchmark } from './virtual-arcade-select';
|
||||
import { selectDeeplyNestedBenchmark } from './select-deeply-nested';
|
||||
|
||||
/**
|
||||
* IBenchmarkState is passed to the setup of each benchmark script.
|
||||
*/
|
||||
export interface IBenchmarkState {
|
||||
container: HTMLElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* IBenchmark describes a benchmark to run on the arcade machine.
|
||||
*/
|
||||
export interface IBenchmark<T, R = void> {
|
||||
name: string;
|
||||
fixture: React.ComponentType<any>;
|
||||
setup?: (container: IBenchmarkState) => T;
|
||||
iterate: (container: IBenchmarkState, setup: T, prev: R | undefined) => R;
|
||||
}
|
||||
|
||||
export const benchmarks: IBenchmark<any, any>[] = [
|
||||
selectSimpleElementBenchmark,
|
||||
selectArcadeMachineBenchmark,
|
||||
virtualArcadeSelectBenchmark,
|
||||
selectDeeplyNestedBenchmark,
|
||||
];
|
|
@ -1,32 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { ArcRoot, Button, defaultOptions } from '../../../src';
|
||||
import { IBenchmark } from './index';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
const mockInputMethod = {
|
||||
observe: new Subject<{ button: Button }>(),
|
||||
isSupported: true,
|
||||
};
|
||||
|
||||
export const selectArcadeMachineBenchmark: IBenchmark<void, boolean> = {
|
||||
name: 'arcade-machine focus',
|
||||
fixture: ArcRoot(
|
||||
() => (
|
||||
<React.Fragment>
|
||||
<div className="square box box1" tabIndex={0} />
|
||||
<div className="square box box2" tabIndex={0} />
|
||||
</React.Fragment>
|
||||
),
|
||||
{
|
||||
...defaultOptions(),
|
||||
inputs: [mockInputMethod],
|
||||
},
|
||||
),
|
||||
setup: state => {
|
||||
(state.container.querySelector('.box1') as HTMLElement).focus();
|
||||
},
|
||||
iterate: (_state, _setup, toggle) => {
|
||||
mockInputMethod.observe.next({ button: toggle ? Button.Right : Button.Left });
|
||||
return !toggle;
|
||||
},
|
||||
};
|
|
@ -1,48 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { ArcRoot, Button, defaultOptions, VirtualElementStore } from '../../../src';
|
||||
import { IBenchmark } from './index';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
const mockInputMethod = {
|
||||
observe: new Subject<{ button: Button }>(),
|
||||
isSupported: true,
|
||||
};
|
||||
|
||||
const elementStore = new VirtualElementStore();
|
||||
|
||||
interface INestedProps {
|
||||
levels: number;
|
||||
children: React.ReactElement<any>;
|
||||
}
|
||||
|
||||
const NestedElement: React.ComponentType<INestedProps> = ({ levels, children }: INestedProps) => (
|
||||
<div>{levels ? <NestedElement levels={levels - 1} children={children} /> : children}</div>
|
||||
);
|
||||
|
||||
export const selectDeeplyNestedBenchmark: IBenchmark<void, boolean> = {
|
||||
name: 'virtual store w/ highly nested elements (100 levels from the root)',
|
||||
fixture: ArcRoot(
|
||||
() => (
|
||||
<React.Fragment>
|
||||
<NestedElement levels={100}>
|
||||
<div className="square box box1" tabIndex={0} />
|
||||
</NestedElement>
|
||||
<NestedElement levels={100}>
|
||||
<div className="square box box2" tabIndex={0} />
|
||||
</NestedElement>
|
||||
</React.Fragment>
|
||||
),
|
||||
{
|
||||
...defaultOptions(),
|
||||
inputs: [mockInputMethod],
|
||||
elementStore,
|
||||
},
|
||||
),
|
||||
setup: state => {
|
||||
elementStore.element = state.container.querySelector('.box1') as HTMLElement;
|
||||
},
|
||||
iterate: (_state, _setup, toggle) => {
|
||||
mockInputMethod.observe.next({ button: toggle ? Button.Up : Button.Down });
|
||||
return !toggle;
|
||||
},
|
||||
};
|
|
@ -1,27 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { IBenchmark } from './index';
|
||||
|
||||
export const selectSimpleElementBenchmark: IBenchmark<[HTMLElement, HTMLElement], boolean> = {
|
||||
name: 'Select simple element (base case, no arcade-machine)',
|
||||
fixture: () => (
|
||||
<React.Fragment>
|
||||
<div className="square box box1" tabIndex={0} />
|
||||
<div className="square box box2" tabIndex={0} />
|
||||
</React.Fragment>
|
||||
),
|
||||
setup: state => {
|
||||
return [
|
||||
state.container.querySelector('.box1') as HTMLElement,
|
||||
state.container.querySelector('.box2') as HTMLElement,
|
||||
];
|
||||
},
|
||||
iterate: (_state, [a, b], toggle) => {
|
||||
if (toggle) {
|
||||
a.focus();
|
||||
} else {
|
||||
b.focus();
|
||||
}
|
||||
|
||||
return !toggle;
|
||||
},
|
||||
};
|
|
@ -1,35 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { ArcRoot, Button, defaultOptions, VirtualElementStore } from '../../../src';
|
||||
import { IBenchmark } from './index';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
const mockInputMethod = {
|
||||
observe: new Subject<{ button: Button }>(),
|
||||
isSupported: true,
|
||||
};
|
||||
|
||||
const elementStore = new VirtualElementStore();
|
||||
|
||||
export const virtualArcadeSelectBenchmark: IBenchmark<void, boolean> = {
|
||||
name: 'arcade-machine focus with a virtual element store',
|
||||
fixture: ArcRoot(
|
||||
() => (
|
||||
<React.Fragment>
|
||||
<div className="square box box1" tabIndex={0} />
|
||||
<div className="square box box2" tabIndex={0} />
|
||||
</React.Fragment>
|
||||
),
|
||||
{
|
||||
...defaultOptions(),
|
||||
inputs: [mockInputMethod],
|
||||
elementStore,
|
||||
},
|
||||
),
|
||||
setup: state => {
|
||||
elementStore.element = state.container.querySelector('.box1') as HTMLElement;
|
||||
},
|
||||
iterate: (_state, _setup, toggle) => {
|
||||
mockInputMethod.observe.next({ button: toggle ? Button.Right : Button.Left });
|
||||
return !toggle;
|
||||
},
|
||||
};
|
|
@ -1,14 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>arcade-machine-react</title>
|
||||
<link rel="stylesheet" href="./style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="./demo/index.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,33 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { ArcRoot, defaultOptions, VirtualElementStore } from '../src';
|
||||
import { FocusHooksDemo } from './components/FocusHooksDemo';
|
||||
import { FocusInsideDemo } from './components/FocusInsideDemo';
|
||||
import { FocusTrapsDemo } from './components/FocusTrapsDemo';
|
||||
import { FocusHistoryDemo } from './components/FocusHistoryDemo';
|
||||
import { FocusGridDemo } from './components/FocusGridDemo';
|
||||
import { FocusHiddenDemo } from './components/FocusHiddenDemo';
|
||||
import { Benchmarks } from './components/FocusBenchmarks';
|
||||
import { FocusFormDemo } from './components/FocusFormDemo';
|
||||
|
||||
const DemoApp = ArcRoot(
|
||||
() => (
|
||||
<div>
|
||||
<FocusHooksDemo />
|
||||
<FocusInsideDemo />
|
||||
<FocusTrapsDemo />
|
||||
<FocusFormDemo />
|
||||
<FocusHistoryDemo />
|
||||
<FocusGridDemo />
|
||||
<FocusHiddenDemo />
|
||||
</div>
|
||||
),
|
||||
{ ...defaultOptions(), elementStore: new VirtualElementStore() },
|
||||
);
|
||||
|
||||
const BenchmarkApp = () => <Benchmarks />;
|
||||
|
||||
render(
|
||||
window.location.toString().includes('benchmarks') ? <BenchmarkApp /> : <DemoApp />,
|
||||
document.getElementById('app'),
|
||||
);
|
117
demo/style.css
117
demo/style.css
|
@ -1,117 +0,0 @@
|
|||
:host {
|
||||
font-family: monospace;
|
||||
max-width: 960px;
|
||||
margin: 15px auto;
|
||||
display: block;
|
||||
}
|
||||
.area {
|
||||
border: 1px solid #000;
|
||||
margin: 15px 0;
|
||||
}
|
||||
.area:after {
|
||||
content: '';
|
||||
display: block;
|
||||
}
|
||||
.area.arc--selected {
|
||||
border-color: #f00;
|
||||
}
|
||||
.box-wrapper {
|
||||
width: 100px;
|
||||
display: inline-block;
|
||||
}
|
||||
.box {
|
||||
margin: 15px;
|
||||
background: #000;
|
||||
color: #fff;
|
||||
}
|
||||
.box.arc-selected,
|
||||
.box:focus {
|
||||
background: #f00;
|
||||
}
|
||||
.square {
|
||||
display: inline-block;
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
margin: 15px;
|
||||
background: #000;
|
||||
color: #fff;
|
||||
}
|
||||
.square.arc-selected,
|
||||
.square:focus {
|
||||
background: #f00;
|
||||
}
|
||||
form {
|
||||
display: flex;
|
||||
margin: 15px;
|
||||
align-content: center;
|
||||
}
|
||||
form div {
|
||||
margin-right: 5px;
|
||||
}
|
||||
input,
|
||||
button,
|
||||
textarea {
|
||||
border: 1px solid #000;
|
||||
padding: 5px 8px;
|
||||
border-radius: 0;
|
||||
box-shadow: 0;
|
||||
outline: 0 !important;
|
||||
}
|
||||
input.arc-selected,
|
||||
input:focus,
|
||||
button.arc-selected,
|
||||
button:focus,
|
||||
textarea.arc-selected,
|
||||
textarea:focus {
|
||||
border-color: #f00;
|
||||
}
|
||||
.scroll-restriction {
|
||||
overflow: auto;
|
||||
height: 100px;
|
||||
}
|
||||
.dialog {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 1;
|
||||
padding: 15px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.dialog button {
|
||||
display: inline-block;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.visibility-test {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 50px;
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.visibility-test .case-name {
|
||||
display: block;
|
||||
width: 200px;
|
||||
height: 50px;
|
||||
border: 1px solid black;
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.visibility-test .case-name.arc-selected,
|
||||
.visibility-test .case-name:focus {
|
||||
border-color: red;
|
||||
}
|
||||
|
||||
table.benchmarks th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
table.benchmarks td:nth-child(1) {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
table.benchmarks td:nth-child(2) {
|
||||
width: 500px;
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
module.exports = {
|
||||
entry: './demo/index.tsx',
|
||||
devtool: 'source-map',
|
||||
mode: 'development',
|
||||
output: {
|
||||
filename: './demo/index.js',
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.tsx', '.js'],
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'ts-loader',
|
||||
},
|
||||
],
|
||||
},
|
||||
devServer: {
|
||||
contentBase: __dirname,
|
||||
},
|
||||
};
|
|
@ -0,0 +1,79 @@
|
|||
.demo {
|
||||
max-width: 1280px;
|
||||
margin: 50px;
|
||||
margin-left: 250px;
|
||||
padding: 0 8px;
|
||||
|
||||
p {
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
p,
|
||||
blockquote,
|
||||
ul,
|
||||
ol {
|
||||
+ blockquote,
|
||||
+ ul,
|
||||
+ ol,
|
||||
+ p {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 0.5em 0;
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin: 0 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
height: 1px;
|
||||
border: none;
|
||||
color: #ccc;
|
||||
background-color: #ccc;
|
||||
margin: 2em 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-left: 2px solid #ccc;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: bold;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
:global .prism-code {
|
||||
margin: 1rem 0;
|
||||
background: rgb(42, 39, 52);
|
||||
padding: 8px 0;
|
||||
|
||||
:global .token-line {
|
||||
padding: 2px 8px;
|
||||
|
||||
&:nth-child(odd) {
|
||||
background: rgba(#fff, 0.02);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table {
|
||||
margin: 1rem 0;
|
||||
border-collapse: collapse;
|
||||
|
||||
td {
|
||||
border: 1px solid #ccc;
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,352 @@
|
|||
import * as React from 'react';
|
||||
import * as styles from './app.component.scss';
|
||||
import { Demo } from './demo';
|
||||
import { nav, Reference } from './nav/tree';
|
||||
import { Title } from './nav/title';
|
||||
import { Sidebar } from './nav/sidebar';
|
||||
|
||||
const navigation = nav('arcade-machine-react', {
|
||||
basics: nav('The Basics', {
|
||||
demo: nav('Demo: Hello, World!'),
|
||||
}),
|
||||
handlers: nav('Custom Handlers', {
|
||||
demo: nav('Demo: Set Next Elements'),
|
||||
}),
|
||||
focusTraps: nav('Focus Traps', {
|
||||
demo: nav('Demo: Modal with Focus Trap'),
|
||||
}),
|
||||
focusArea: nav('Focus Areas', {
|
||||
demo: nav('Demo: Focus Areas'),
|
||||
}),
|
||||
focusExclude: nav('Focus Exclusion', {
|
||||
demo: nav('Demo: Focus Exclusion'),
|
||||
}),
|
||||
scrollable: nav('Scrolling', {
|
||||
demo: nav('Demo: Scrolling'),
|
||||
}),
|
||||
faq: nav('FAQ'),
|
||||
});
|
||||
|
||||
export const App: React.FC = () => (
|
||||
<>
|
||||
<Sidebar node={navigation} />
|
||||
<div className={styles.demo}>
|
||||
<Title node={navigation} />
|
||||
|
||||
<p>
|
||||
Arcade machine is an abstraction layer over gamepads for web-based platforms. It handles
|
||||
directional navigation for the application, and includes rich React bindings for integration
|
||||
with your application.
|
||||
</p>
|
||||
|
||||
<Title node={navigation.basics} />
|
||||
<p>
|
||||
Arcade machine works both with keyboards and gamepads. If you have a controller, you can
|
||||
plug it into your PC now, otherwise you can play with these examples using your keyboard.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
To get started with arcade machine, you need to make two modifications to your application:
|
||||
wrap the root of your application in the <code>ArcRoot</code> HOC, and set a{' '}
|
||||
<code>tabIndex</code> property on all elements that should be focusable.
|
||||
<sup>
|
||||
<Reference node={navigation.faq}>Why?</Reference>
|
||||
</sup>{' '}
|
||||
The default focusable value is <code>tabIndex=0</code>.
|
||||
</p>
|
||||
|
||||
<Title node={navigation.basics.demo} />
|
||||
|
||||
<p>
|
||||
Here's a quick demonstration. You can navigate around the below example with your arrow
|
||||
keys, or with a connected controlled.
|
||||
</p>
|
||||
|
||||
<Demo name="hello-world" />
|
||||
|
||||
<Title node={navigation.handlers} />
|
||||
|
||||
<p>
|
||||
You can handle focus events using <code>{'<ArcScope />'}</code>. You can specify elements
|
||||
that should be focused after this one (by selector or the actual element), and additionally
|
||||
add handlers that hook into incoming, outgoing, and button press events. This is the base
|
||||
component on which many of the following components are built. The component takes these
|
||||
properties:
|
||||
</p>
|
||||
|
||||
<table className={styles.table}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<code>arcFocusLeft</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>undefined | string | HTMLElement</code>
|
||||
</td>
|
||||
<td>
|
||||
Element or selector which should be focused when navigating to the left of this
|
||||
component.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>arcFocusRight</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>undefined | string | HTMLElement</code>
|
||||
</td>
|
||||
<td>
|
||||
Element or selector which should be focused when navigating to the right of this
|
||||
component.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>arcFocusUp</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>undefined | string | HTMLElement</code>
|
||||
</td>
|
||||
<td>
|
||||
Element or selector which should be focused when navigating to above this component.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>arcFocusDown</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>undefined | string | HTMLElement</code>
|
||||
</td>
|
||||
<td>
|
||||
Element or selector which should be focused when navigating to below this component.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>onOutgoing</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>onOutgoing?(ev: ArcFocusEvent): void</code>
|
||||
</td>
|
||||
<td>
|
||||
Called with an IArcEvent focus is about to leave this element or one of its children.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>onIncoming</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>onIncoming?(ev: ArcFocusEvent): void</code>
|
||||
</td>
|
||||
<td>
|
||||
Called with an IArcEvent focus is about to enter this element or one of its children.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>onIncoming</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>onIncoming?(ev: ArcEvent): void</code>
|
||||
</td>
|
||||
<td>
|
||||
Triggers when a button is pressed in the element or one of its children. This will
|
||||
fire before the <code>onOutgoing</code> handler, for directional events.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<Title node={navigation.handlers.demo} />
|
||||
|
||||
<p>
|
||||
Here's an example of using overriding the "next" element. On these boxes, pressing up/down
|
||||
will instead focus on adjact boxes. Normally, if no focusable element can be found next, it
|
||||
would do nothing.
|
||||
</p>
|
||||
|
||||
<Demo name="scope-override-focus" />
|
||||
|
||||
<Title node={navigation.focusTraps} />
|
||||
|
||||
<p>
|
||||
Focus traps are used to force focus to stay within a subset of your application. This is
|
||||
great for use in modals and overlays. You can optionally set the <code>focusIn</code> and{' '}
|
||||
<code>focusOut</code> properties to HTML elements or selectors to specify where focus should
|
||||
be given when entering and leaving the focus trap, respectively.
|
||||
</p>
|
||||
|
||||
<table className={styles.table}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<code>focusIn</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>undefined | string | HTMLElement</code>
|
||||
</td>
|
||||
<td>Element or selector to give focus to when the focus trap is created.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>focusOut</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>undefined | string | HTMLElement</code>
|
||||
</td>
|
||||
<td>
|
||||
Element or selector to give focus to when the focus trap is released. If not provided,
|
||||
the last focused element before the trap was created will be shown.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<Title node={navigation.focusTraps.demo} />
|
||||
|
||||
<p>
|
||||
Here, we trap focus inside the modal when it's open. We also use{' '}
|
||||
<code>{`<ArcScope />`}</code> to close the modal when "back" (B on Xbox controllers, or
|
||||
Escape on the keyboard) is pressed.
|
||||
</p>
|
||||
|
||||
<Demo name="modal" />
|
||||
|
||||
<Title node={navigation.focusArea} />
|
||||
|
||||
<p>
|
||||
Focus areas are sections of the page which act as opaque focusable 'blocks', and then
|
||||
transfer their focus to a child. This is a common pattern seen if you have multiple rows of
|
||||
content, and want focus to transfer to the first element in each row when navigating down
|
||||
between rows. It takes, as its property, a <code>focusIn</code> property, which is a
|
||||
selector or HTML element to give focus to.
|
||||
</p>
|
||||
|
||||
<table className={styles.table}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<code>focusIn</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>undefined | string | HTMLElement</code>
|
||||
</td>
|
||||
<td>Element or selector to give focus to when the focus trap is created.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<Title node={navigation.focusArea.demo} />
|
||||
|
||||
<p>
|
||||
In this demo, regardless of where you are in the previous row, focus is always transferred
|
||||
to the left-most element of each node when you navigate into it.
|
||||
</p>
|
||||
|
||||
<Demo name="focus-areas" />
|
||||
|
||||
<Title node={navigation.focusExclude} />
|
||||
|
||||
<p>
|
||||
Focus exclusion areas can prevent their contents from being focused on. Simple enough. Good
|
||||
if you have content that's loading in or simply disabled. By default, it'll only exclude its
|
||||
direct child node, and it will prevent the entire subtree from being focused on if{' '}
|
||||
<code>deep</code> is set.
|
||||
</p>
|
||||
|
||||
<table className={styles.table}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<code>active</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>undefined | boolean</code>
|
||||
</td>
|
||||
<td>Whether the exclusion is active. Defaults to true if not provided.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<code>deep</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>undefined | boolean</code>
|
||||
</td>
|
||||
<td>Whether to exclude the entire subtree contained in this node.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<Title node={navigation.focusExclude.demo} />
|
||||
|
||||
<p>
|
||||
In this demo, regardless of where you are in the previous row, focus is always transferred
|
||||
to the left-most element of each node when you navigate into it.
|
||||
</p>
|
||||
|
||||
<Demo name="focus-exclude" />
|
||||
|
||||
<Title node={navigation.scrollable} />
|
||||
|
||||
<p>
|
||||
By default, browsers will automatically scroll whenever to the focused element when you use
|
||||
native focus. We have scrolling functionality built-in in the event you use the virtual
|
||||
element store (todo: document this), rather than native focus. Our implementation also
|
||||
supports smooth scrolling out of the box.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
To use arcade machine's scrolling, you wrap your scroll container with the{' '}
|
||||
<code>{`<Scrollable />`}</code> element. By default, it'll scroll vertically, but you can
|
||||
override this via properties.
|
||||
</p>
|
||||
|
||||
<table className={styles.table}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<code>vertical</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>undefined | boolean</code>
|
||||
</td>
|
||||
<td>Whether to scroll vertically, defaults to true.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<code>horizontal</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>undefined | boolean</code>
|
||||
</td>
|
||||
<td>Whether to scroll horizontally, defaults to true false.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<Title node={navigation.scrollable.demo} />
|
||||
|
||||
<Demo name="focus-scrollable" />
|
||||
|
||||
<Title node={navigation.faq} />
|
||||
|
||||
<dl>
|
||||
<dt>
|
||||
<a id="why-tabindex" /> Why do I need a <code>tabIndex</code> on focusable items?
|
||||
</dt>
|
||||
<dd>
|
||||
The DOM API lacks an efficient way to query focusable elements. Similar directional
|
||||
navigation implementations get around this by querying all elements on focus change, and
|
||||
manually looking for ones that should be focusable. We use the tabIndex property as a flag
|
||||
which allows us to efficiently filter out the noise.
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</>
|
||||
);
|
|
@ -0,0 +1,48 @@
|
|||
.wrapper {
|
||||
margin-top: 1rem;
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
|
||||
li {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0.75rem 2rem;
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
background: #ccc;
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.code,
|
||||
.demo,
|
||||
.demoHidden {
|
||||
height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.demo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.demoHidden {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.2em;
|
||||
cursor: pointer;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.code :global .prism-code {
|
||||
margin: 0;
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
import * as React from 'react';
|
||||
import { Highlighted } from './highlighted';
|
||||
import * as styles from './demo.component.scss';
|
||||
|
||||
interface IProps {
|
||||
name: string;
|
||||
}
|
||||
|
||||
const massageSource = (source: string) => {
|
||||
return source.replace(/(\.\.\/)+src/g, '@mixer/arcade-machine-react');
|
||||
};
|
||||
|
||||
const enum Tab {
|
||||
DemoHidden,
|
||||
DemoVisible,
|
||||
Code,
|
||||
}
|
||||
|
||||
interface IState {
|
||||
tab: Tab;
|
||||
}
|
||||
|
||||
let hidePrevious: (() => void) | undefined;
|
||||
|
||||
export class Demo extends React.PureComponent<IProps, IState> {
|
||||
public state: IState = { tab: Tab.DemoHidden };
|
||||
|
||||
public render() {
|
||||
const { default: Component } = require(`./demos/${this.props.name}`);
|
||||
const { default: source } = require(`!!raw-loader!./demos/${this.props.name}`);
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<ol className={styles.tabs}>
|
||||
<li
|
||||
className={this.state.tab !== Tab.Code ? styles.active : undefined}
|
||||
onClick={this.openDemo}
|
||||
>
|
||||
Demo
|
||||
</li>
|
||||
<li
|
||||
className={this.state.tab === Tab.Code ? styles.active : undefined}
|
||||
onClick={this.openCode}
|
||||
>
|
||||
Source
|
||||
</li>
|
||||
</ol>
|
||||
{this.state.tab === Tab.Code && (
|
||||
<div className={styles.code}>
|
||||
<Highlighted code={massageSource(source)} />
|
||||
</div>
|
||||
)}
|
||||
{this.state.tab === Tab.DemoVisible && (
|
||||
<div className={styles.demo}>
|
||||
<Component />
|
||||
</div>
|
||||
)}
|
||||
{this.state.tab === Tab.DemoHidden && (
|
||||
<div className={styles.demoHidden} onClick={this.openDemo}>
|
||||
Click to Open Demo
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private readonly hideDemo = () => {
|
||||
this.setState({ tab: Tab.DemoHidden });
|
||||
|
||||
if (hidePrevious === this.hideDemo) {
|
||||
hidePrevious = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
private readonly openDemo = () => {
|
||||
if (!hidePrevious || hidePrevious === this.hideDemo) {
|
||||
this.setState({ tab: Tab.DemoVisible });
|
||||
hidePrevious = this.hideDemo;
|
||||
} else {
|
||||
hidePrevious();
|
||||
setTimeout(this.openDemo, 100);
|
||||
}
|
||||
};
|
||||
|
||||
private readonly openCode = () => {
|
||||
this.setState({ tab: Tab.Code });
|
||||
|
||||
if (hidePrevious === this.hideDemo) {
|
||||
hidePrevious = undefined;
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import * as React from 'react';
|
||||
import * as styles from './hello-world.component.scss';
|
||||
|
||||
export const repeat = <T extends {}>(n: number, fn: (i: number) => T): T[] => {
|
||||
const data: T[] = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
data.push(fn(i));
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export const basicGrid = (width: number, height: number) => (
|
||||
<>
|
||||
{repeat(height, i => (
|
||||
<div className={styles.row} key={i}>
|
||||
{repeat(width, k => (
|
||||
<div className={styles.box} key={k} tabIndex={0} />
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
|
@ -0,0 +1,22 @@
|
|||
import * as React from 'react';
|
||||
import { ArcRoot, defaultOptions, FocusArea } from '../../src';
|
||||
import * as styles from './hello-world.component.scss';
|
||||
import { repeat } from './demo-utils';
|
||||
|
||||
export default ArcRoot(
|
||||
() => (
|
||||
<>
|
||||
{repeat(3, i => (
|
||||
<React.Fragment key={i}>
|
||||
<b>Content Row {i + 1}</b>
|
||||
<FocusArea className={styles.row} focusIn="[data-nth='0']">
|
||||
{repeat(5, k => (
|
||||
<div className={styles.box} data-nth={k} key={k} tabIndex={0} />
|
||||
))}
|
||||
</FocusArea>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</>
|
||||
),
|
||||
defaultOptions(),
|
||||
);
|
|
@ -0,0 +1,39 @@
|
|||
import * as React from 'react';
|
||||
import { ArcRoot, defaultOptions, FocusExclude } from '../../src';
|
||||
import * as styles from './hello-world.component.scss';
|
||||
import { repeat } from './demo-utils';
|
||||
|
||||
export default ArcRoot(() => {
|
||||
const [excludeActive, setActive] = React.useState(true);
|
||||
|
||||
return (
|
||||
<>
|
||||
<b>Normal Row</b>
|
||||
<div className={styles.row}>
|
||||
{repeat(5, k => (
|
||||
<div className={styles.box} key={k} tabIndex={0} />
|
||||
))}
|
||||
</div>
|
||||
<b>Excluded Row (active={String(excludeActive)})</b>
|
||||
<FocusExclude deep active={excludeActive}>
|
||||
<div className={styles.row}>
|
||||
{repeat(5, k => (
|
||||
<div className={styles.box} key={k} tabIndex={0} />
|
||||
))}
|
||||
</div>
|
||||
</FocusExclude>
|
||||
<b>Normal Row</b>
|
||||
<div className={styles.row}>
|
||||
{repeat(5, k => (
|
||||
<div className={styles.box} key={k} tabIndex={0} />
|
||||
))}
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={excludeActive}
|
||||
onChange={ev => setActive(ev.target.checked)}
|
||||
/>
|
||||
Exclusion is active?
|
||||
</>
|
||||
);
|
||||
}, defaultOptions());
|
|
@ -0,0 +1,21 @@
|
|||
import * as React from 'react';
|
||||
import { ArcRoot, defaultOptions, Scrollable, VirtualElementStore } from '../../src';
|
||||
import { basicGrid } from './demo-utils';
|
||||
|
||||
export default ArcRoot(
|
||||
() => (
|
||||
<>
|
||||
<b>Vertical Scrolling</b>
|
||||
<Scrollable>
|
||||
<div style={{ overflowY: 'scroll', height: 200, width: 500 }}>{basicGrid(5, 5)}</div>
|
||||
</Scrollable>
|
||||
<b>Horizontal Scrolling</b>
|
||||
<Scrollable horizontal vertical={false}>
|
||||
<div style={{ overflowX: 'scroll', height: 200, width: 500 }}>
|
||||
<div style={{ width: 2000 }}>{basicGrid(15, 2)}</div>
|
||||
</div>
|
||||
</Scrollable>
|
||||
</>
|
||||
),
|
||||
{ ...defaultOptions(), elementStore: new VirtualElementStore() },
|
||||
);
|
|
@ -0,0 +1,19 @@
|
|||
.row {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.box {
|
||||
margin: 1rem;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: #ccc;
|
||||
|
||||
&:focus,
|
||||
&.arc-selected {
|
||||
background: red;
|
||||
}
|
||||
}
|
||||
|
||||
:global .arc-selected {
|
||||
background: red;
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import * as React from 'react';
|
||||
import { ArcRoot, defaultOptions } from '../../src';
|
||||
import * as styles from './hello-world.component.scss';
|
||||
import { repeat } from './demo-utils';
|
||||
|
||||
export default ArcRoot(
|
||||
() => (
|
||||
<>
|
||||
{repeat(3, i => (
|
||||
<div className={styles.row} key={i}>
|
||||
{repeat(5, k => (
|
||||
<div className={styles.box} key={k} tabIndex={0} />
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
),
|
||||
defaultOptions(),
|
||||
);
|
|
@ -0,0 +1,34 @@
|
|||
.demo {
|
||||
position: relative;
|
||||
width: 500px;
|
||||
height: 300px;
|
||||
|
||||
button {
|
||||
background: #aaa;
|
||||
padding: 8px;
|
||||
|
||||
&:focus {
|
||||
background: red;
|
||||
color: #fff;
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(#000, 0.5);
|
||||
}
|
||||
|
||||
.inner {
|
||||
padding: 8px;
|
||||
background: #fff;
|
||||
border: 1px solid #000;
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
import * as React from 'react';
|
||||
import { ArcRoot, defaultOptions, FocusTrap, ArcScope, Button } from '../../src';
|
||||
import * as styles from './modal.component.scss';
|
||||
import { basicGrid } from './demo-utils';
|
||||
|
||||
export default ArcRoot(() => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
return (
|
||||
<div className={styles.demo}>
|
||||
<button onClick={() => setOpen(true)} tabIndex={0}>
|
||||
Open Modal
|
||||
</button>
|
||||
{basicGrid(5, 2)}
|
||||
{open && (
|
||||
<FocusTrap>
|
||||
<ArcScope onButton={ev => ev.event === Button.Back && setOpen(false)}>
|
||||
<div className={styles.modal}>
|
||||
<div className={styles.inner}>
|
||||
This is a modal!
|
||||
{basicGrid(5, 2)}
|
||||
<button onClick={() => setOpen(false)} tabIndex={0}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ArcScope>
|
||||
</FocusTrap>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}, defaultOptions());
|
|
@ -0,0 +1,22 @@
|
|||
import * as React from 'react';
|
||||
import { ArcRoot, defaultOptions, ArcScope } from '../../src';
|
||||
import * as styles from './hello-world.component.scss';
|
||||
|
||||
export default ArcRoot(
|
||||
() => (
|
||||
<>
|
||||
<div className={styles.row}>
|
||||
<ArcScope arcFocusDown={'#center'} arcFocusUp={'#right'}>
|
||||
<div tabIndex={0} id="left" className={styles.box} />
|
||||
</ArcScope>
|
||||
<ArcScope arcFocusDown={'#right'} arcFocusUp={'#left'}>
|
||||
<div tabIndex={0} id="center" className={styles.box} />
|
||||
</ArcScope>
|
||||
<ArcScope arcFocusDown={'#left'} arcFocusUp={'#center'}>
|
||||
<div tabIndex={0} id="right" className={styles.box} />
|
||||
</ArcScope>
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
defaultOptions(),
|
||||
);
|
|
@ -0,0 +1,18 @@
|
|||
import * as React from 'react';
|
||||
import Highlight, { defaultProps } from 'prism-react-renderer';
|
||||
|
||||
export const Highlighted: React.FC<{ code: string }> = props => (
|
||||
<Highlight {...defaultProps} code={props.code} language="jsx">
|
||||
{({ className, style, tokens, getLineProps, getTokenProps }) => (
|
||||
<pre className={className} style={style}>
|
||||
{tokens.map((line, i) => (
|
||||
<div {...getLineProps({ line, key: i })}>
|
||||
{line.map((token, key) => (
|
||||
<span {...getTokenProps({ token, key })} />
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</pre>
|
||||
)}
|
||||
</Highlight>
|
||||
);
|
|
@ -0,0 +1,40 @@
|
|||
$font-prefix: '../node_modules/@ibm/plex/';
|
||||
|
||||
@import '../node_modules/@ibm/plex/scss/mono/regular/latin1';
|
||||
@import '../node_modules/@ibm/plex/scss/mono/bold/latin1';
|
||||
@import '../node_modules/@ibm/plex/scss/serif/semibold/latin1';
|
||||
@import '../node_modules/@ibm/plex/scss/sans/regular/latin1';
|
||||
|
||||
:root {
|
||||
--font-family-sans: 'IBM Plex Sans', sans-serif;
|
||||
--font-family-serif: 'IBM Plex Serif', serif;
|
||||
--font-family-mono: 'IBM Plex Mono', monospace;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-family-sans);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: var(--font-family-serif);
|
||||
font-weight: normal;
|
||||
margin: 3rem 0 1rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
pre, code {
|
||||
font-family: var(--font-family-mono);
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
/// <reference path="types/styles.d.ts" />
|
||||
|
||||
import * as React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { App } from './app';
|
||||
|
||||
import '../node_modules/normalize.css/normalize.css';
|
||||
import '../node_modules/highlight.js/styles/github.css';
|
||||
import './index.scss';
|
||||
|
||||
const target = document.createElement('div');
|
||||
document.body.appendChild(target);
|
||||
|
||||
render(<App />, target);
|
|
@ -0,0 +1,30 @@
|
|||
.sidebar {
|
||||
position: fixed;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
width: 250px;
|
||||
font-size: 0.8em;
|
||||
|
||||
ol {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
li {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
margin: 0.5rem 0;
|
||||
|
||||
&:hover {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
&.active {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
import * as React from 'react';
|
||||
import { NavNode, Reference, flatten } from './tree';
|
||||
import * as styles from './sidebar.component.scss';
|
||||
import { Subscription, fromEvent } from 'rxjs';
|
||||
import { map, filter, throttleTime } from 'rxjs/operators';
|
||||
|
||||
const SidebarNode: React.FC<{ node: NavNode<any>; active: string | null }> = ({ node, active }) => {
|
||||
const { title, link, depth, ...children } = node;
|
||||
const childKeys = Object.keys(children);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Reference className={active === link ? styles.active : undefined} node={node}>
|
||||
{title}
|
||||
</Reference>
|
||||
{childKeys.length > 0 && (
|
||||
<ol>
|
||||
{childKeys.map(key => (
|
||||
<li key={key}>
|
||||
<SidebarNode node={children[key]} active={active} />
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export class Sidebar extends React.PureComponent<
|
||||
{ node: NavNode<any> },
|
||||
{ active: string | null }
|
||||
> {
|
||||
public state: { active: string | null } = { active: null };
|
||||
private subscriptions: Subscription[] = [];
|
||||
private cachedPositions?: [NavNode<any>, number][];
|
||||
|
||||
public componentDidMount() {
|
||||
this.subscriptions.push(
|
||||
fromEvent(window, 'scroll', { passive: true })
|
||||
.pipe(
|
||||
throttleTime(10),
|
||||
map(() => {
|
||||
const items = this.getPositions();
|
||||
const best = items.find(n => n[1] < window.scrollY + 50);
|
||||
return best ? best[0] : items[items.length - 1][0];
|
||||
}),
|
||||
filter(node => node !== this.state.active),
|
||||
)
|
||||
.subscribe(node => {
|
||||
this.setState({ active: node.link });
|
||||
}),
|
||||
);
|
||||
|
||||
this.subscriptions.push(
|
||||
fromEvent(window, 'resize').subscribe(() => (this.cachedPositions = undefined)),
|
||||
);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.subscriptions.forEach(s => s.unsubscribe());
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div className={styles.sidebar}>
|
||||
<SidebarNode node={this.props.node} active={this.state.active} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private getPositions = () => {
|
||||
if (this.cachedPositions) {
|
||||
return this.cachedPositions;
|
||||
}
|
||||
|
||||
this.cachedPositions = [];
|
||||
for (const node of flatten(this.props.node)) {
|
||||
const el = document.getElementById(node.link);
|
||||
if (el) {
|
||||
this.cachedPositions.push([node, el.offsetTop]);
|
||||
}
|
||||
}
|
||||
|
||||
this.cachedPositions.sort((a, b) => b[1] - a[1]);
|
||||
return this.cachedPositions;
|
||||
};
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import * as React from 'react';
|
||||
import { NavNode } from './tree';
|
||||
|
||||
export const Title: React.FC<{ node: NavNode<any> }> = ({ node }) =>
|
||||
React.createElement(`h${node.depth}`, { id: node.link }, [node.title]);
|
|
@ -0,0 +1,63 @@
|
|||
import * as React from 'react';
|
||||
|
||||
export interface NavTree {}
|
||||
|
||||
export type NavNode<T extends { [key: string]: NavNode<any> }> = {
|
||||
title: string;
|
||||
link: string;
|
||||
depth: number;
|
||||
} & T;
|
||||
|
||||
function increaseDepth<T extends { [key: string]: NavNode<any> }>(node: NavNode<T>): NavNode<T> {
|
||||
const { title, link, depth, ...children } = node;
|
||||
const out: any = { title, link, depth: depth + 1 };
|
||||
for (const key of Object.keys(children)) {
|
||||
out[key] = increaseDepth(children[key]);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flattens the nav tree.
|
||||
*/
|
||||
export const flatten = (node: NavNode<any>): NavNode<{}>[] => {
|
||||
const out: NavNode<{}>[] = [];
|
||||
|
||||
const queue = [node];
|
||||
let next: NavNode<any> | null;
|
||||
while ((next = queue.pop())) {
|
||||
const { title, link, depth, ...children } = next;
|
||||
out.push({ title, link, depth });
|
||||
|
||||
for (const key of Object.keys(children)) {
|
||||
queue.push(children[key]);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new navigation node.
|
||||
*/
|
||||
export const nav = <T extends { [key: string]: NavNode<any> }>(
|
||||
title: string,
|
||||
children?: T,
|
||||
): NavNode<T> =>
|
||||
increaseDepth<T>({
|
||||
title,
|
||||
link: title.toLowerCase().replace(/[^a-z0-9]/g, '-'),
|
||||
depth: 0,
|
||||
...children,
|
||||
} as NavNode<T>);
|
||||
|
||||
export const Reference: React.FC<{ node: NavNode<any>; className?: string }> = ({
|
||||
node,
|
||||
className,
|
||||
children,
|
||||
}) => (
|
||||
<a className={className} href={`#${node.link}`}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
|
@ -0,0 +1 @@
|
|||
declare module '*.scss';
|
|
@ -0,0 +1,68 @@
|
|||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const { resolve } = require('path');
|
||||
|
||||
module.exports = {
|
||||
entry: './docs/index.tsx',
|
||||
devtool: 'source-map',
|
||||
mode: 'development',
|
||||
output: {
|
||||
filename: 'bundle.js',
|
||||
path: resolve(__dirname, '..', 'dist', 'docs'),
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.tsx', '.js', '.woff', '.woff2'],
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'ts-loader',
|
||||
},
|
||||
{
|
||||
sideEffects: true,
|
||||
include: resolve(__dirname, 'index.scss'),
|
||||
use: ['style-loader', 'css-loader', 'sass-loader'],
|
||||
},
|
||||
{
|
||||
sideEffects: true,
|
||||
test: /\.css$/,
|
||||
use: ['style-loader', 'css-loader'],
|
||||
},
|
||||
{
|
||||
test: /\.component.scss$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'style-loader',
|
||||
},
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
modules: true,
|
||||
localIdentName: '[local]__[hash:base64:5]',
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: 'sass-loader',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpg|gif|woff|woff2)$/i,
|
||||
use: [
|
||||
{
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 8192,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
title: 'Arcade Machine Documentation',
|
||||
}),
|
||||
],
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
...require('./webpack.config'),
|
||||
mode: 'production',
|
||||
devtool: false,
|
||||
};
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
20
package.json
20
package.json
|
@ -9,27 +9,33 @@
|
|||
"test": "npm-run-all -p lint:ts test:unit test:fmt",
|
||||
"test:unit": "karma start test/karma.config.js --single-run",
|
||||
"test:watch": "karma start test/karma.config.js --watch",
|
||||
"test:fmt": "prettier --list-different \"{src,demo}/**/*.{json,ts,tsx}\"",
|
||||
"start": "webpack-dev-server --config demo/webpack.config.js",
|
||||
"test:fmt": "prettier --list-different \"{src,docs}/**/*.{json,ts,tsx}\"",
|
||||
"start": "webpack-dev-server --config docs/webpack.config.js",
|
||||
"build": "tsc && tsc -p tsconfig.cjs.json",
|
||||
"build:docs": "webpack --config docs/webpack.production.js",
|
||||
"prepare": "npm run build",
|
||||
"lint:ts": "tslint --project tsconfig.json --fix \"src/**/*.{ts,tsx}\"",
|
||||
"fmt": "prettier --write \"{src,demo}/**/*.{json,ts,tsx}\" && npm run lint:ts -- --fix"
|
||||
"fmt": "prettier --write \"{src,docs}/**/*.{json,ts,tsx}\" && npm run lint:ts -- --fix"
|
||||
},
|
||||
"author": "Connor Peet <connor@peet.io>",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@ibm/plex": "^2.0.0",
|
||||
"@types/benchmark": "^1.0.31",
|
||||
"@types/chai": "^4.1.7",
|
||||
"@types/enzyme": "^3.9.1",
|
||||
"@types/mocha": "^5.2.6",
|
||||
"@types/react": "^16.8.14",
|
||||
"@types/react-dom": "^16.8.4",
|
||||
"@types/react-highlight": "^0.12.1",
|
||||
"@types/winrt-uwp": "0.0.19",
|
||||
"benchmark": "^2.1.4",
|
||||
"chai": "^4.2.0",
|
||||
"css-loader": "^2.1.1",
|
||||
"enzyme": "^3.9.0",
|
||||
"enzyme-adapter-react-16": "^1.12.1",
|
||||
"file-loader": "^4.0.0",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"karma": "^4.1.0",
|
||||
"karma-chrome-launcher": "^2.2.0",
|
||||
"karma-mocha": "^1.3.0",
|
||||
|
@ -38,15 +44,23 @@
|
|||
"karma-typescript": "^4.0.0",
|
||||
"karma-webpack": "^3.0.5",
|
||||
"mocha": "^6.1.4",
|
||||
"node-sass": "^4.12.0",
|
||||
"normalize.css": "^8.0.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^1.17.0",
|
||||
"prism-react-renderer": "^0.1.6",
|
||||
"raw-loader": "^3.0.0",
|
||||
"react-dom": "^16.8.6",
|
||||
"react-highlight": "^0.12.0",
|
||||
"rxjs": "^6.2.1",
|
||||
"sass-loader": "^7.1.0",
|
||||
"style-loader": "^0.23.1",
|
||||
"ts-loader": "^5.3.3",
|
||||
"tslint": "^5.16.0",
|
||||
"tslint-config-prettier": "^1.18.0",
|
||||
"tslint-react": "^4.0.0",
|
||||
"typescript": "^3.4.4",
|
||||
"url-loader": "^2.0.0",
|
||||
"webpack": "^4.30.0",
|
||||
"webpack-cli": "^3.3.0",
|
||||
"webpack-dev-server": "^3.3.1"
|
||||
|
|
|
@ -9,6 +9,7 @@ import { instance } from '../singleton';
|
|||
*/
|
||||
export class FocusExclude extends React.PureComponent<{
|
||||
children: React.ReactNode;
|
||||
active?: boolean;
|
||||
deep?: boolean;
|
||||
}> {
|
||||
/**
|
||||
|
@ -27,7 +28,7 @@ export class FocusExclude extends React.PureComponent<{
|
|||
instance.getServices().stateContainer.add(this, {
|
||||
element,
|
||||
onIncoming: ev => {
|
||||
if (!ev.next) {
|
||||
if (!ev.next || this.props.active === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -43,7 +44,10 @@ export class FocusExclude extends React.PureComponent<{
|
|||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
instance.getServices().stateContainer.remove(this, this.node);
|
||||
const services = instance.maybeGetServices();
|
||||
if (services) {
|
||||
services.stateContainer.remove(this, this.node);
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
|
|
|
@ -2,6 +2,11 @@ import * as React from 'react';
|
|||
import { findElement, findFocusable } from '../internal-types';
|
||||
import { instance } from '../singleton';
|
||||
|
||||
export type FocusAreaProps = {
|
||||
children: React.ReactNode;
|
||||
focusIn?: HTMLElement | string;
|
||||
} & React.HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
/**
|
||||
* The ArcFocusArea acts as a virtual focus element which transfers focus
|
||||
* to child. Take, for example, a list of lists, like this:
|
||||
|
@ -37,10 +42,7 @@ import { instance } from '../singleton';
|
|||
* {myContent.map(content => <ContentElement data={content} />)}
|
||||
* </FocusArea>
|
||||
*/
|
||||
export class FocusArea extends React.PureComponent<{
|
||||
children: React.ReactNode;
|
||||
focusIn?: HTMLElement | string;
|
||||
}> {
|
||||
export class FocusArea extends React.PureComponent<FocusAreaProps> {
|
||||
private containerRef = React.createRef<HTMLDivElement>();
|
||||
|
||||
public componentDidMount() {
|
||||
|
@ -66,12 +68,17 @@ export class FocusArea extends React.PureComponent<{
|
|||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
instance.getServices().stateContainer.remove(this, this.containerRef.current!);
|
||||
const services = instance.maybeGetServices();
|
||||
if (services && this.containerRef.current) {
|
||||
services.stateContainer.remove(this, this.containerRef.current);
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { children, focusIn, ...htmlProps } = this.props;
|
||||
|
||||
return (
|
||||
<div tabIndex={0} ref={this.containerRef}>
|
||||
<div tabIndex={0} {...htmlProps} ref={this.containerRef}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -51,29 +51,26 @@ export class FocusTrap extends React.PureComponent<IFocusTrapProps> {
|
|||
}
|
||||
});
|
||||
|
||||
instance.getServices().stateContainer.add(this, {
|
||||
element,
|
||||
onOutgoing: ev => {
|
||||
if (ev.next && !element.contains(ev.next)) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
},
|
||||
});
|
||||
instance.getServices().root.narrow(element);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
const { stateContainer, elementStore } = instance.getServices();
|
||||
stateContainer.remove(this, this.containerRef.current!);
|
||||
const services = instance.maybeGetServices();
|
||||
if (!services) {
|
||||
return;
|
||||
}
|
||||
|
||||
services.root.restore(this.containerRef.current!);
|
||||
|
||||
if (this.props.focusOut) {
|
||||
const target = findElement(document.body, this.props.focusOut);
|
||||
if (target) {
|
||||
elementStore.element = target;
|
||||
services.elementStore.element = target;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
elementStore.element = this.previouslyFocusedElement;
|
||||
services.elementStore.element = this.previouslyFocusedElement;
|
||||
}
|
||||
|
||||
public render() {
|
||||
|
|
|
@ -11,6 +11,7 @@ import { InputService } from '../input';
|
|||
import { GamepadInput } from '../input/gamepad-input';
|
||||
import { IInputMethod } from '../input/input-method';
|
||||
import { KeyboardInput } from '../input/keyboard-input';
|
||||
import { RootStore } from '../root-store';
|
||||
import { IScrollingAlgorithm, ScrollExecutor } from '../scroll';
|
||||
import { NativeSmoothScrollingAlgorithm } from '../scroll/native-smooth-scrolling';
|
||||
import { ScrollRegistry } from '../scroll/scroll-registry';
|
||||
|
@ -44,6 +45,10 @@ export function defaultOptions(): IRootOptions {
|
|||
};
|
||||
}
|
||||
|
||||
interface IState {
|
||||
showChildren: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component for defining the root of the arcade-machine. This should be wrapped
|
||||
* around the root of your application, or its content. Only components
|
||||
|
@ -57,37 +62,21 @@ export function defaultOptions(): IRootOptions {
|
|||
*
|
||||
* export default ArcRoot(MyAppContent);
|
||||
*/
|
||||
class Root extends React.PureComponent<IRootOptions> {
|
||||
export class Root extends React.PureComponent<IRootOptions, IState> {
|
||||
public state = { showChildren: false };
|
||||
private focus?: FocusService;
|
||||
private stateContainer = new StateContainer();
|
||||
private scrollRegistry = new ScrollRegistry();
|
||||
private rootRef = React.createRef<HTMLDivElement>();
|
||||
private unmounted = new ReplaySubject<void>(1);
|
||||
|
||||
constructor(props: IRootOptions) {
|
||||
super(props);
|
||||
|
||||
instance.setServices({
|
||||
elementStore: this.props.elementStore,
|
||||
scrollRegistry: this.scrollRegistry,
|
||||
stateContainer: this.stateContainer,
|
||||
});
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
const focus = new FocusService(
|
||||
this.stateContainer,
|
||||
this.rootRef.current!,
|
||||
this.props.focus,
|
||||
this.props.elementStore,
|
||||
new ScrollExecutor(this.scrollRegistry, this.props.scrolling),
|
||||
);
|
||||
const input = new InputService(this.props.inputs);
|
||||
|
||||
input.events.pipe(takeUntil(this.unmounted)).subscribe(({ button, event }) => {
|
||||
if (focus.sendButton(button) && event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
public componentDidUpdate(_: IRootOptions, prevState: IState) {
|
||||
if (this.state.showChildren && !prevState.showChildren && this.focus) {
|
||||
this.focus.setDefaultFocus();
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
|
@ -96,8 +85,40 @@ class Root extends React.PureComponent<IRootOptions> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
return <div ref={this.rootRef}>{this.props.children}</div>;
|
||||
return <div ref={this.setRoot}>{this.state.showChildren && this.props.children}</div>;
|
||||
}
|
||||
|
||||
private readonly setRoot = (rootElement: HTMLDivElement | null) => {
|
||||
if (!rootElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const root = new RootStore(rootElement);
|
||||
|
||||
instance.setServices({
|
||||
elementStore: this.props.elementStore,
|
||||
root,
|
||||
scrollRegistry: this.scrollRegistry,
|
||||
stateContainer: this.stateContainer,
|
||||
});
|
||||
|
||||
const focus = (this.focus = new FocusService(
|
||||
this.stateContainer,
|
||||
root,
|
||||
this.props.focus,
|
||||
this.props.elementStore,
|
||||
new ScrollExecutor(this.scrollRegistry, this.props.scrolling),
|
||||
));
|
||||
|
||||
const input = new InputService(this.props.inputs);
|
||||
input.events.pipe(takeUntil(this.unmounted)).subscribe(({ button, event }) => {
|
||||
if (focus.sendButton(button) && event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
this.setState({ showChildren: true });
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -39,7 +39,10 @@ export class ArcScope extends React.PureComponent<Partial<IArcHandler>> {
|
|||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
instance.getServices().stateContainer.remove(this, this.node);
|
||||
const services = instance.maybeGetServices();
|
||||
if (services) {
|
||||
services.stateContainer.remove(this, this.node);
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
|
|
|
@ -10,8 +10,8 @@ import { instance } from '../singleton';
|
|||
*/
|
||||
export class Scrollable extends React.PureComponent<{
|
||||
children: React.ReactNode;
|
||||
horizontal: boolean;
|
||||
vertical: boolean;
|
||||
horizontal?: boolean;
|
||||
vertical?: boolean;
|
||||
}> {
|
||||
/**
|
||||
* The node this element is attached to.
|
||||
|
@ -29,13 +29,16 @@ export class Scrollable extends React.PureComponent<{
|
|||
this.node = element;
|
||||
instance.getServices().scrollRegistry.add({
|
||||
element,
|
||||
horizontal: this.props.horizontal,
|
||||
vertical: this.props.vertical,
|
||||
horizontal: this.props.horizontal === true,
|
||||
vertical: this.props.vertical !== false,
|
||||
});
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
instance.getServices().scrollRegistry.remove(this.node);
|
||||
const services = instance.maybeGetServices();
|
||||
if (services) {
|
||||
services.scrollRegistry.remove(this.node);
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { ArcEvent } from './arc-event';
|
||||
import { ArcFocusEvent } from './arc-focus-event';
|
||||
import { FocusContext, IElementStore, IFocusStrategy } from './focus';
|
||||
import { isFocusable, isNodeAttached, roundRect } from './focus/dom-utils';
|
||||
import { isFocusable, roundRect } from './focus/dom-utils';
|
||||
import { isForForm } from './focus/is-for-form';
|
||||
import { propogationStoped, resetEvent } from './internal-types';
|
||||
import { Button, IArcHandler, isDirectional } from './model';
|
||||
import { RootStore } from './root-store';
|
||||
import { ScrollExecutor } from './scroll';
|
||||
import { StateContainer } from './state/state-container';
|
||||
|
||||
|
@ -33,7 +34,7 @@ export class FocusService {
|
|||
|
||||
constructor(
|
||||
private readonly registry: StateContainer,
|
||||
private readonly root: HTMLElement,
|
||||
private readonly root: RootStore,
|
||||
private readonly strategies: IFocusStrategy[],
|
||||
private readonly elementStore: IElementStore,
|
||||
private readonly scroller: ScrollExecutor,
|
||||
|
@ -45,7 +46,7 @@ export class FocusService {
|
|||
* Wrapper around moveFocus to dispatch arcselectingnode event
|
||||
*/
|
||||
public selectNode(next: HTMLElement) {
|
||||
if (!this.root.contains(next)) {
|
||||
if (!this.root.element.contains(next)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -118,11 +119,11 @@ export class FocusService {
|
|||
});
|
||||
}
|
||||
|
||||
const context = new FocusContext(this.root, direction, this.strategies, {
|
||||
const context = new FocusContext(this.root.element, direction, this.strategies, {
|
||||
activeElement: selected,
|
||||
directive: this.registry.find(selected),
|
||||
previousElement: this.previousSelectedElement,
|
||||
referenceRect: this.root.contains(selected)
|
||||
referenceRect: this.root.element.contains(selected)
|
||||
? selected.getBoundingClientRect()
|
||||
: this.referenceRect,
|
||||
});
|
||||
|
@ -131,7 +132,7 @@ export class FocusService {
|
|||
context,
|
||||
directive: this.registry.find(selected),
|
||||
event: direction,
|
||||
next: context ? context.find(this.root) : null,
|
||||
next: context ? context.find(this.root.element) : null,
|
||||
target: selected,
|
||||
});
|
||||
}
|
||||
|
@ -142,9 +143,7 @@ export class FocusService {
|
|||
*/
|
||||
private bubbleInOut(ev: ArcFocusEvent, selected: HTMLElement): boolean {
|
||||
const originalNext = ev.next;
|
||||
if (isNodeAttached(selected, this.root)) {
|
||||
this.bubbleEvent(ev, 'onOutgoing', selected);
|
||||
}
|
||||
this.bubbleEvent(ev, 'onOutgoing', selected);
|
||||
|
||||
// Abort if the user handled
|
||||
if (ev.defaultPrevented || originalNext !== ev.next) {
|
||||
|
@ -184,7 +183,11 @@ export class FocusService {
|
|||
trigger: keyof IArcHandler,
|
||||
source: HTMLElement | null,
|
||||
): ArcEvent {
|
||||
for (let el = source; !propogationStoped(ev) && el !== this.root && el; el = el.parentElement) {
|
||||
for (
|
||||
let el = source;
|
||||
!propogationStoped(ev) && el !== this.root.element && el;
|
||||
el = el.parentElement
|
||||
) {
|
||||
if (el === undefined) {
|
||||
// tslint:disable-next-line
|
||||
console.warn(
|
||||
|
@ -212,8 +215,8 @@ export class FocusService {
|
|||
/**
|
||||
* Reset the focus if arcade-machine wanders out of root
|
||||
*/
|
||||
private setDefaultFocus() {
|
||||
const focusableElems = this.root.querySelectorAll('[tabIndex]');
|
||||
public setDefaultFocus() {
|
||||
const focusableElems = this.root.element.querySelectorAll('[tabIndex]');
|
||||
|
||||
// tslint:disable-next-line
|
||||
for (let i = 0; i < focusableElems.length; i += 1) {
|
||||
|
|
19
src/index.ts
19
src/index.ts
|
@ -1,13 +1,14 @@
|
|||
export * from './components/arc-autofocus';
|
||||
export * from './components/arc-root';
|
||||
export * from './components/arc-focus-area';
|
||||
export * from './components/arc-scope';
|
||||
export * from './components/arc-focus-trap';
|
||||
export * from './components/arc-exclude';
|
||||
export * from './model';
|
||||
export * from './arc-event';
|
||||
export * from './focus/virtual-element-store';
|
||||
export * from './focus/native-element-store';
|
||||
export * from './components/arc-autofocus';
|
||||
export * from './components/arc-exclude';
|
||||
export * from './components/arc-focus-area';
|
||||
export * from './components/arc-focus-trap';
|
||||
export * from './components/arc-root';
|
||||
export * from './components/arc-scope';
|
||||
export * from './components/arc-scrollable';
|
||||
export * from './focus/focus-by-distance';
|
||||
export * from './focus/focus-by-raycast';
|
||||
export * from './focus/focus-by-registry';
|
||||
export * from './focus/native-element-store';
|
||||
export * from './focus/virtual-element-store';
|
||||
export * from './model';
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
/**
|
||||
* Holds the current root element.
|
||||
*/
|
||||
export class RootStore {
|
||||
private readonly roots: HTMLElement[];
|
||||
|
||||
constructor(root: HTMLElement) {
|
||||
this.roots = [root];
|
||||
}
|
||||
|
||||
public get element() {
|
||||
return this.roots[this.roots.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Scopes to the given new root.
|
||||
*/
|
||||
public narrow(root: HTMLElement) {
|
||||
this.roots.push(root);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores a scoped element.
|
||||
*/
|
||||
public restore(fromRoot: HTMLElement) {
|
||||
const index = this.roots.lastIndexOf(fromRoot);
|
||||
if (index < 1) {
|
||||
// tslint:disable-next-line
|
||||
console.warn('arcade-machine: attempted to release a root we did not own');
|
||||
}
|
||||
|
||||
this.roots.splice(index, 1);
|
||||
}
|
||||
}
|
|
@ -27,14 +27,15 @@ export class ScrollExecutor {
|
|||
* are computed and passed into it.
|
||||
*/
|
||||
public scrollTo(targetElement: HTMLElement, referenceRect: ClientRect) {
|
||||
const horizontal = referenceRect.left < 0 || referenceRect.right > window.innerWidth;
|
||||
if (!horizontal && referenceRect.top >= 0 && referenceRect.bottom < window.innerHeight) {
|
||||
return;
|
||||
let parent: Readonly<IScrollableContainer> | undefined;
|
||||
for (const candidate of this.registry.getScrollContainers()) {
|
||||
if (candidate.element.contains(targetElement)) {
|
||||
if (!parent || parent.element.contains(candidate.element)) {
|
||||
parent = candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const parent = this.registry
|
||||
.getScrollContainers()
|
||||
.find(({ element }) => element.contains(targetElement));
|
||||
if (!parent) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -15,8 +15,9 @@ export class NativeSmoothScrollingAlgorithm implements IScrollingAlgorithm {
|
|||
targetElement: HTMLElement,
|
||||
rect: ClientRect,
|
||||
): void {
|
||||
const horizontal = horizontalDelta(rect);
|
||||
const vertical = verticalDelta(rect);
|
||||
const reference = parent.element.getBoundingClientRect();
|
||||
const horizontal = horizontalDelta(rect, reference);
|
||||
const vertical = verticalDelta(rect, reference);
|
||||
|
||||
try {
|
||||
if (parent.vertical && vertical) {
|
||||
|
|
|
@ -59,9 +59,10 @@ export class SmoothScrollingAlgorithm implements IScrollingAlgorithm {
|
|||
requestAnimationFrame(run);
|
||||
};
|
||||
|
||||
const reference = parent.element.getBoundingClientRect();
|
||||
if (parent.horizontal) {
|
||||
animate(
|
||||
horizontalDelta(referenceRect),
|
||||
horizontalDelta(referenceRect, reference),
|
||||
parent.element.scrollLeft,
|
||||
x => (parent.element.scrollLeft = x),
|
||||
);
|
||||
|
@ -69,7 +70,7 @@ export class SmoothScrollingAlgorithm implements IScrollingAlgorithm {
|
|||
|
||||
if (parent.vertical) {
|
||||
animate(
|
||||
verticalDelta(referenceRect),
|
||||
verticalDelta(referenceRect, reference),
|
||||
parent.element.scrollTop,
|
||||
x => (parent.element.scrollTop = x),
|
||||
);
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
* Returns the difference the page has to be moved horizontall to bring
|
||||
* the target rect into view.
|
||||
*/
|
||||
export function horizontalDelta(rect: ClientRect) {
|
||||
return rect.left < 0
|
||||
? rect.left
|
||||
: rect.right > window.innerWidth
|
||||
? rect.right - window.innerWidth
|
||||
export function horizontalDelta(rect: ClientRect, reference: ClientRect) {
|
||||
return rect.left < reference.left
|
||||
? rect.left - reference.left
|
||||
: rect.right > reference.right
|
||||
? rect.right - reference.right
|
||||
: 0;
|
||||
}
|
||||
|
||||
|
@ -14,10 +14,10 @@ export function horizontalDelta(rect: ClientRect) {
|
|||
* Returns the difference the page has to be moved vertically to bring
|
||||
* the target rect into view.
|
||||
*/
|
||||
export function verticalDelta(rect: ClientRect) {
|
||||
return rect.top < 0
|
||||
? rect.top
|
||||
: rect.bottom > window.innerHeight
|
||||
? rect.bottom - window.innerHeight
|
||||
export function verticalDelta(rect: ClientRect, reference: ClientRect) {
|
||||
return rect.top < reference.top
|
||||
? rect.top - reference.top
|
||||
: rect.bottom > reference.bottom
|
||||
? rect.bottom - reference.bottom
|
||||
: 0;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { IElementStore } from './focus';
|
||||
import { RootStore } from './root-store';
|
||||
import { ScrollRegistry } from './scroll/scroll-registry';
|
||||
import { StateContainer } from './state/state-container';
|
||||
|
||||
|
@ -6,6 +7,7 @@ import { StateContainer } from './state/state-container';
|
|||
* IArcServices is held in the ArcSingleton.
|
||||
*/
|
||||
export interface IArcServices {
|
||||
root: RootStore;
|
||||
elementStore: IElementStore;
|
||||
stateContainer: StateContainer;
|
||||
scrollRegistry: ScrollRegistry;
|
||||
|
@ -41,6 +43,7 @@ export class ArcSingleton {
|
|||
|
||||
this.services = {
|
||||
elementStore: services.elementStore!,
|
||||
root: services.root!,
|
||||
scrollRegistry: new ScrollRegistry(),
|
||||
stateContainer: new StateContainer(),
|
||||
...services,
|
||||
|
@ -57,6 +60,13 @@ export class ArcSingleton {
|
|||
|
||||
return this.services;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the services, or void if none found.
|
||||
*/
|
||||
public maybeGetServices(): IArcServices | void {
|
||||
return this.services;
|
||||
}
|
||||
}
|
||||
|
||||
export const instance = new ArcSingleton();
|
||||
|
|
Загрузка…
Ссылка в новой задаче