Skip to content

Commit

Permalink
Merge pull request #284 from theKashey/correct-d.ts-exports
Browse files Browse the repository at this point in the history
fix: correct d.ts exports
  • Loading branch information
theKashey committed Feb 16, 2024
2 parents 6659516 + a9be7a4 commit 617388a
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 8 deletions.
7 changes: 6 additions & 1 deletion UI/UI.d.ts
Expand Up @@ -83,6 +83,11 @@ export function useFocusController(...shards: HTMLElement[]):FocusControl;
*/
export function useFocusScope():FocusControl


export type FocusCallbacks = {
onFocus():void;
onBlur():void;
}
/**
* returns information about FocusState of a given node
* @example
Expand All @@ -91,7 +96,7 @@ export function useFocusScope():FocusControl
* return <div ref={ref} onFocus={onFocus}>{active ? 'is focused' : 'not focused'}</div>
* ```
*/
export function useFocusState<T extends Element>():{
export function useFocusState<T extends Element>(callbacks?: FocusCallbacks ):{
/**
* is currently focused, or is focus is inside
*/
Expand Down
58 changes: 57 additions & 1 deletion _tests/hooks.spec.js
Expand Up @@ -3,7 +3,8 @@ import {
render,
} from '@testing-library/react';
import { expect } from 'chai';
import { useFocusController } from '../src/UI';
import sinon from 'sinon';
import { useFocusController, useFocusState } from '../src/UI';

describe('Hooks w/o sidecar', () => {
it('controls focus', async () => {
Expand All @@ -27,4 +28,59 @@ describe('Hooks w/o sidecar', () => {
await p;
expect(document.activeElement).to.be.equal(document.getElementById('b1'));
});

it('focus tracking', async () => {
const Capture = ({ children, id, ...callbacks }) => {
const { onFocus, ref, active } = useFocusState(callbacks);
return (
<button id={id} onFocus={onFocus} ref={ref}>
{children}
{active ? '+' : '-'}
</button>
);
};
const onparentblur = sinon.spy();

const onfocus = sinon.spy();
const onblur = sinon.spy();

const Suite = () => {
const { onFocus } = useFocusState({ onBlur: onparentblur });
return (
<>
<div onFocus={onFocus}>
<Capture id="1">test1</Capture>
<Capture id="2" onFocus={onfocus} onBlur={onblur}>test2</Capture>
<Capture id="3">test3</Capture>
</div>
<button id="0" />
</>
);
};

const { container } = render(
<Suite />,
);
document.getElementById('1').focus();
expect(container.innerHTML).to.be.equal('<div><button id="1">test1+</button><button id="2">test2-</button><button id="3">test3-</button></div><button id="0"></button>');
sinon.assert.notCalled(onfocus);
sinon.assert.notCalled(onblur);

document.getElementById('2').focus();
expect(container.innerHTML).to.be.equal('<div><button id="1">test1-</button><button id="2">test2+</button><button id="3">test3-</button></div><button id="0"></button>');
sinon.assert.calledOnce(onfocus);
sinon.assert.notCalled(onblur);

document.getElementById('3').focus();
expect(container.innerHTML).to.be.equal('<div><button id="1">test1-</button><button id="2">test2-</button><button id="3">test3+</button></div><button id="0"></button>');
sinon.assert.calledOnce(onfocus);
sinon.assert.calledOnce(onblur);

sinon.assert.notCalled(onparentblur);

document.getElementById('0').focus();
expect(container.innerHTML).to.be.equal('<div><button id="1">test1-</button><button id="2">test2-</button><button id="3">test3-</button></div><button id="0"></button>');
// blur on parent will be called only once, this is important
sinon.assert.calledOnce(onparentblur);
});
});
79 changes: 79 additions & 0 deletions react-focus-lock.d.ts
Expand Up @@ -30,4 +30,83 @@ export class FreeFocusInside extends React.Component<FreeFocusProps> {
* Secures the focus around the node
*/
export class InFocusGuard extends React.Component<InFocusGuardProps> {
}

/**
* Moves focus inside a given node
*/
export function useFocusInside(node: React.RefObject<HTMLElement>): void;

export type FocusOptions = {
/**
* enables focus cycle
* @default true
*/
cycle?: boolean;
/**
* limits focusables to tabbables (tabindex>=0) elements only
* @default true
*/
onlyTabbable?:boolean
}

export type FocusControl = {
/**
* moves focus to the current scope, can be considered as autofocus
*/
autoFocus():Promise<void>;
/**
* focuses the next element in the scope.
* If active element is not in the scope, autofocus will be triggered first
*/
focusNext(options:FocusOptions):Promise<void>;
/**
* focuses the prev element in the scope.
* If active element is not in the scope, autofocus will be triggered first
*/
focusPrev():Promise<void>;
}


/**
* returns FocusControl over the union given elements, one or many
* - can be used outside of FocusLock
* @see {@link useFocusScope} for use cases inside of FocusLock
*/
export function useFocusController(...shards: HTMLElement[]):FocusControl;

/**
* returns FocusControl over the current FocusLock
* - can be used only within FocusLock
* - can be used by disabled FocusLock
* @see {@link useFocusController} for use cases outside of FocusLock
*/
export function useFocusScope():FocusControl

export type FocusCallbacks = {
onFocus():void;
onBlur():void;
}
/**
* returns information about FocusState of a given node
* @example
* ```tsx
* const {active, ref, onFocus} = useFocusState();
* return <div ref={ref} onFocus={onFocus}>{active ? 'is focused' : 'not focused'}</div>
* ```
*/
export function useFocusState<T extends Element>(callbacks?: FocusCallbacks ):{
/**
* is currently focused, or is focus is inside
*/
active: boolean;
/**
* focus handled. SHALL be passed to the node down
*/
onFocus: React.FocusEventHandler<T>;
/**
* reference to the node
* only required to capture current status of the node
*/
ref: React.RefObject<T>;
}
3 changes: 3 additions & 0 deletions src/index.js
Expand Up @@ -2,4 +2,7 @@ import FocusLock from './Combination';

export * from './UI';

// no named export yes, as it will interfere with eslint rules
// export { FocusLock };

export default FocusLock;
28 changes: 22 additions & 6 deletions src/use-focus-state.js
Expand Up @@ -41,20 +41,24 @@ const getFocusState = (target, current) => {
return 'within-boundary';
};

export const useFocusState = () => {
export const useFocusState = (callbacks = {}) => {
const [active, setActive] = useState(false);
const [state, setState] = useState('');
const ref = useRef(null);
const focusState = useRef({});
const stateTracker = useRef(false);

// initial focus
useEffect(() => {
if (ref.current) {
setActive(
ref.current === document.activeElement
|| ref.current.contains(document.activeElement),
);
const isAlreadyFocused = ref.current === document.activeElement
|| ref.current.contains(document.activeElement);
setActive(isAlreadyFocused);
setState(getFocusState(document.activeElement, ref.current));

if (isAlreadyFocused && callbacks.onFocus) {
callbacks.onFocus();
}
}
}, []);

Expand All @@ -75,8 +79,20 @@ export const useFocusState = () => {
});
const fin = mainbus.on('assign', () => {
// focus event propagation is ended
setActive(focusState.current.focused || false);
const newState = focusState.current.focused || false;
setActive(newState);
setState(focusState.current.state || '');

if (newState !== stateTracker.current) {
stateTracker.current = newState;
if (newState) {
// eslint-disable-next-line no-unused-expressions
callbacks.onFocus && callbacks.onFocus();
} else {
// eslint-disable-next-line no-unused-expressions
callbacks.onBlur && callbacks.onBlur();
}
}
});
return () => {
fout();
Expand Down

0 comments on commit 617388a

Please sign in to comment.