Skip to content

Commit e9a083c

Browse files
authored
Feat/add on arrow release (#145)
* feat: implement onArrowRelease * feat: add ProgressBar to consume onArrowRelease * fix: remove not need indention changes
1 parent b162b00 commit e9a083c

6 files changed

Lines changed: 141 additions & 1 deletion

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,10 @@ Receives `direction` (`left`, `right`, `up`, `down`), `extraProps` (see below) a
466466
This callback HAS to return `true` if you want to proceed with the default directional navigation behavior, or `false`
467467
if you want to block the navigation in the specified direction.
468468

469+
##### `onArrowRelease` (function)
470+
Callback that is called when the component is focused and Arrow key is released.
471+
Receives `direction` (`left`, `right`, `up`, `down`), `extraProps` (see below) as argument.
472+
469473
##### `onFocus` (function)
470474
Callback that is called when component gets focus.
471475
Receives `FocusableComponentLayout`, `extraProps` and `FocusDetails` as arguments.

src/App.tsx

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ function Menu({ focusKey: focusKeyParam }: MenuProps) {
153153
onEnterPress: () => {},
154154
onEnterRelease: () => {},
155155
onArrowPress: () => true,
156+
onArrowRelease: () => {},
156157
onFocus: () => {},
157158
onBlur: () => {},
158159
extraProps: { foo: 'bar' }
@@ -384,6 +385,85 @@ const ScrollingRows = styled.div`
384385
flex-grow: 1;
385386
`;
386387

388+
interface ProgressBarWrapperProps {
389+
focused: boolean;
390+
}
391+
392+
interface ProgressBarProgressProps {
393+
percent: number;
394+
focused: boolean;
395+
}
396+
397+
const ProgressBarWrapper = styled.div<ProgressBarWrapperProps>`
398+
position: absolute;
399+
bottom: 95px;
400+
right: 100px;
401+
width: 540px;
402+
height: 24px;
403+
background-color: gray;
404+
border-radius: 21px;
405+
border-color: white;
406+
border-style: solid;
407+
border-width: ${({ focused }) => (focused ? '6px' : 0)};
408+
box-sizing: border-box;
409+
`;
410+
411+
const ProgressBarProgress = styled.div<ProgressBarProgressProps>`
412+
width: ${({ percent }) => `${percent}%`};
413+
height: 100%;
414+
background-color: ${({ focused }) =>
415+
focused ? 'deepskyblue' : 'dodgerblue'};
416+
border-radius: 21px;
417+
`;
418+
419+
const defaultPercent = 10;
420+
const seekPercent = 10;
421+
const delayedTime = 100;
422+
const DIRECTION_RIGHT = 'right';
423+
424+
function ProgressBar() {
425+
const [percent, setPercent] = useState(defaultPercent);
426+
const timerRef = useRef<NodeJS.Timer | null>(null);
427+
const { ref, focused } = useFocusable({
428+
onArrowPress: (direction: string) => {
429+
if (direction === DIRECTION_RIGHT && timerRef.current === null) {
430+
timerRef.current = setInterval(() => {
431+
setPercent((prevPercent) =>
432+
prevPercent >= 100 ? prevPercent : prevPercent + seekPercent
433+
);
434+
}, delayedTime);
435+
return true;
436+
}
437+
return true;
438+
},
439+
onArrowRelease: (direction: string) => {
440+
if (direction === DIRECTION_RIGHT) {
441+
clearInterval(timerRef.current);
442+
timerRef.current = null;
443+
}
444+
}
445+
});
446+
useEffect(() => {
447+
if (!focused) {
448+
setPercent(defaultPercent);
449+
}
450+
}, [focused]);
451+
useEffect(
452+
() => () => {
453+
if (timerRef.current !== null) {
454+
clearInterval(timerRef.current);
455+
timerRef.current = null;
456+
}
457+
},
458+
[]
459+
);
460+
return (
461+
<ProgressBarWrapper ref={ref} focused={focused}>
462+
<ProgressBarProgress percent={percent} focused={focused} />
463+
</ProgressBarWrapper>
464+
);
465+
}
466+
387467
function Content() {
388468
const { ref, focusKey } = useFocusable();
389469

@@ -416,6 +496,7 @@ function Content() {
416496
? selectedAsset.title
417497
: 'Press "Enter" to select an asset'}
418498
</SelectedItemTitle>
499+
<ProgressBar />
419500
</SelectedItemWrapper>
420501
<ScrollingRows ref={ref}>
421502
<div>

src/SpatialNavigation.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ interface FocusableComponent {
8080
onEnterPress: (details?: KeyPressDetails) => void;
8181
onEnterRelease: () => void;
8282
onArrowPress: (direction: string, details: KeyPressDetails) => boolean;
83+
onArrowRelease: (direction: string) => void;
8384
onFocus: (layout: FocusableComponentLayout, details: FocusDetails) => void;
8485
onBlur: (layout: FocusableComponentLayout, details: FocusDetails) => void;
8586
onUpdateFocus: (focused: boolean) => void;
@@ -106,6 +107,7 @@ interface FocusableComponentUpdatePayload {
106107
onEnterPress: (details?: KeyPressDetails) => void;
107108
onEnterRelease: () => void;
108109
onArrowPress: (direction: string, details: KeyPressDetails) => boolean;
110+
onArrowRelease: (direction: string) => void;
109111
onFocus: (layout: FocusableComponentLayout, details: FocusDetails) => void;
110112
onBlur: (layout: FocusableComponentLayout, details: FocusDetails) => void;
111113
}
@@ -830,6 +832,14 @@ class SpatialNavigationService {
830832
if (eventType === KEY_ENTER && this.focusKey) {
831833
this.onEnterRelease();
832834
}
835+
836+
if (this.focusKey && (
837+
eventType === DIRECTION_LEFT ||
838+
eventType === DIRECTION_RIGHT ||
839+
eventType === DIRECTION_UP ||
840+
eventType === DIRECTION_DOWN)) {
841+
this.onArrowRelease(eventType)
842+
}
833843
};
834844

835845
window.addEventListener('keyup', this.keyUpEventListener);
@@ -921,6 +931,28 @@ class SpatialNavigationService {
921931
);
922932
}
923933

934+
onArrowRelease(direction: string) {
935+
const component = this.focusableComponents[this.focusKey];
936+
937+
/* Guard against last-focused component being unmounted at time of onArrowRelease (e.g due to UI fading out) */
938+
if (!component) {
939+
this.log('onArrowRelease', 'noComponent');
940+
941+
return;
942+
}
943+
944+
/* Suppress onArrowRelease if the last-focused item happens to lose its 'focused' status. */
945+
if (!component.focusable) {
946+
this.log('onArrowRelease', 'componentNotFocusable');
947+
948+
return;
949+
}
950+
951+
if (component.onArrowRelease) {
952+
component.onArrowRelease(direction);
953+
}
954+
}
955+
924956
/**
925957
* Move focus by direction, if you can't use buttons or focusing by key.
926958
*
@@ -1275,6 +1307,7 @@ class SpatialNavigationService {
12751307
onEnterPress,
12761308
onEnterRelease,
12771309
onArrowPress,
1310+
onArrowRelease,
12781311
onFocus,
12791312
onBlur,
12801313
saveLastFocusedChild,
@@ -1295,6 +1328,7 @@ class SpatialNavigationService {
12951328
onEnterPress,
12961329
onEnterRelease,
12971330
onArrowPress,
1331+
onArrowRelease,
12981332
onFocus,
12991333
onBlur,
13001334
onUpdateFocus,

src/__tests__/SpatialNavigation.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,8 @@ describe('SpatialNavigation', () => {
133133
onEnterRelease: () => {},
134134
onFocus: () => {},
135135
onBlur: () => {},
136-
onArrowPress: () => true
136+
onArrowPress: () => true,
137+
onArrowRelease: () => {},
137138
});
138139

139140
SpatialNavigation.navigateByDirection('right', {});

src/__tests__/domNodes.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export const createRootNode = () => {
3636
onFocus: () => {},
3737
onBlur: () => {},
3838
onArrowPress: () => true,
39+
onArrowRelease: () => {},
3940
onUpdateFocus: () => {},
4041
onUpdateHasFocusedChild: () => {}
4142
});
@@ -79,6 +80,7 @@ export const createHorizontalLayout = () => {
7980
onFocus: () => {},
8081
onBlur: () => {},
8182
onArrowPress: () => true,
83+
onArrowRelease: () => {},
8284
onUpdateFocus: () => {},
8385
onUpdateHasFocusedChild: () => {}
8486
});
@@ -118,6 +120,7 @@ export const createHorizontalLayout = () => {
118120
onFocus: () => {},
119121
onBlur: () => {},
120122
onArrowPress: () => true,
123+
onArrowRelease: () => {},
121124
onUpdateFocus: () => {},
122125
onUpdateHasFocusedChild: () => {}
123126
});
@@ -157,6 +160,7 @@ export const createHorizontalLayout = () => {
157160
onFocus: () => {},
158161
onBlur: () => {},
159162
onArrowPress: () => true,
163+
onArrowRelease: () => {},
160164
onUpdateFocus: () => {},
161165
onUpdateHasFocusedChild: () => {}
162166
});
@@ -200,6 +204,7 @@ export const createVerticalLayout = () => {
200204
onFocus: () => {},
201205
onBlur: () => {},
202206
onArrowPress: () => true,
207+
onArrowRelease: () => {},
203208
onUpdateFocus: () => {},
204209
onUpdateHasFocusedChild: () => {}
205210
});
@@ -239,6 +244,7 @@ export const createVerticalLayout = () => {
239244
onFocus: () => {},
240245
onBlur: () => {},
241246
onArrowPress: () => true,
247+
onArrowRelease: () => {},
242248
onUpdateFocus: () => {},
243249
onUpdateHasFocusedChild: () => {}
244250
});

src/useFocusable.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ export type ArrowPressHandler<P = object> = (
3030
details: KeyPressDetails
3131
) => boolean;
3232

33+
export type ArrowReleaseHandler<P = object> = (
34+
direction: string,
35+
props: P,
36+
) => void;
37+
3338
export type FocusHandler<P = object> = (
3439
layout: FocusableComponentLayout,
3540
props: P,
@@ -55,6 +60,7 @@ export interface UseFocusableConfig<P = object> {
5560
onEnterPress?: EnterPressHandler<P>;
5661
onEnterRelease?: EnterReleaseHandler<P>;
5762
onArrowPress?: ArrowPressHandler<P>;
63+
onArrowRelease?: ArrowReleaseHandler<P>;
5864
onFocus?: FocusHandler<P>;
5965
onBlur?: BlurHandler<P>;
6066
extraProps?: P;
@@ -81,6 +87,7 @@ const useFocusableHook = <P>({
8187
onEnterPress = noop,
8288
onEnterRelease = noop,
8389
onArrowPress = () => true,
90+
onArrowRelease = noop,
8491
onFocus = noop,
8592
onBlur = noop,
8693
extraProps
@@ -102,6 +109,10 @@ const useFocusableHook = <P>({
102109
[extraProps, onArrowPress]
103110
);
104111

112+
const onArrowReleaseHandler = useCallback((direction: string) => {
113+
onArrowRelease(direction, extraProps);
114+
}, [onArrowRelease, extraProps])
115+
105116
const onFocusHandler = useCallback(
106117
(layout: FocusableComponentLayout, details: FocusDetails) => {
107118
onFocus(layout, extraProps, details);
@@ -149,6 +160,7 @@ const useFocusableHook = <P>({
149160
onEnterPress: onEnterPressHandler,
150161
onEnterRelease: onEnterReleaseHandler,
151162
onArrowPress: onArrowPressHandler,
163+
onArrowRelease: onArrowReleaseHandler,
152164
onFocus: onFocusHandler,
153165
onBlur: onBlurHandler,
154166
onUpdateFocus: (isFocused = false) => setFocused(isFocused),
@@ -182,6 +194,7 @@ const useFocusableHook = <P>({
182194
onEnterPress: onEnterPressHandler,
183195
onEnterRelease: onEnterReleaseHandler,
184196
onArrowPress: onArrowPressHandler,
197+
onArrowRelease: onArrowReleaseHandler,
185198
onFocus: onFocusHandler,
186199
onBlur: onBlurHandler
187200
});
@@ -194,6 +207,7 @@ const useFocusableHook = <P>({
194207
onEnterPressHandler,
195208
onEnterReleaseHandler,
196209
onArrowPressHandler,
210+
onArrowReleaseHandler,
197211
onFocusHandler,
198212
onBlurHandler
199213
]);

0 commit comments

Comments
 (0)