Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TS Strict update react-aria 'b' and 'L' #3930

Merged
merged 14 commits into from
Mar 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/@react-aria/button/stories/useButton.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ function InputButton(props: InputButtonProps) {
value = 'Test'
} = props;

let ref = useRef();
let ref = useRef(null);
let {buttonProps, isPressed} = useButton({...props, elementType: 'input'}, ref);

return (
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-aria/label/stories/useField.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ interface TextFieldProps {

const TextInputField = (props: TextFieldProps) => {
const {label, description, errorMessage, validationState} = props;
const ref = useRef<HTMLInputElement>();
const ref = useRef<HTMLInputElement>(null);
const {labelProps, fieldProps, descriptionProps, errorMessageProps} = useField(props);

return (
Expand Down
85 changes: 46 additions & 39 deletions packages/@react-aria/landmark/src/useLandmark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
*/

import {AriaLabelingProps, DOMAttributes, FocusableElement} from '@react-types/shared';
import {MutableRefObject, useCallback, useEffect, useState} from 'react';
import {RefObject, useCallback, useEffect, useState} from 'react';
import {useLayoutEffect} from '@react-aria/utils';
import {useSyncExternalStore} from 'use-sync-external-store/shim/index.js';

Expand Down Expand Up @@ -44,7 +44,7 @@ interface LandmarkManagerApi {
// from an older version of useLandmark against a newer version of
// LandmarkManager does not crash.
interface Landmark {
ref: MutableRefObject<Element>,
ref: RefObject<FocusableElement>,
role: AriaLandmarkRole,
label?: string,
lastFocused?: FocusableElement,
Expand All @@ -57,7 +57,7 @@ export interface LandmarkControllerOptions {
* The element from which to start navigating.
* @default document.activeElement
*/
from?: Element
from?: FocusableElement
}

/** A LandmarkController allows programmatic navigation of landmarks. */
Expand Down Expand Up @@ -140,8 +140,8 @@ class LandmarkManager implements LandmarkManagerApi {
this.isListening = false;
}

private focusLandmark(landmark: Element, direction: 'forward' | 'backward') {
this.landmarks.find(l => l.ref.current === landmark)?.focus(direction);
private focusLandmark(landmark: FocusableElement, direction: 'forward' | 'backward') {
this.landmarks.find(l => l.ref.current === landmark)?.focus?.(direction);
}

/**
Expand All @@ -160,7 +160,7 @@ class LandmarkManager implements LandmarkManagerApi {

private addLandmark(newLandmark: Landmark) {
this.setupIfNeeded();
if (this.landmarks.find(landmark => landmark.ref === newLandmark.ref)) {
if (this.landmarks.find(landmark => landmark.ref === newLandmark.ref) || !newLandmark.ref.current) {
return;
}

Expand Down Expand Up @@ -203,7 +203,7 @@ class LandmarkManager implements LandmarkManagerApi {
}
}

private removeLandmark(ref: MutableRefObject<Element>) {
private removeLandmark(ref: RefObject<Element>) {
this.landmarks = this.landmarks.filter(landmark => landmark.ref !== ref);
this.teardownIfNeeded();
}
Expand Down Expand Up @@ -241,10 +241,10 @@ class LandmarkManager implements LandmarkManagerApi {
* Get the landmark that is the closest parent in the DOM.
* Returns undefined if no parent is a landmark.
*/
private closestLandmark(element: Element) {
private closestLandmark(element: FocusableElement) {
let landmarkMap = new Map(this.landmarks.map(l => [l.ref.current, l]));
let currentElement = element;
while (currentElement && !landmarkMap.has(currentElement) && currentElement !== document.body) {
while (currentElement && !landmarkMap.has(currentElement) && currentElement !== document.body && currentElement.parentElement) {
currentElement = currentElement.parentElement;
}
return landmarkMap.get(currentElement);
Expand All @@ -256,7 +256,7 @@ class LandmarkManager implements LandmarkManagerApi {
* If not inside a landmark, will return first landmark.
* Returns undefined if there are no landmarks.
*/
private getNextLandmark(element: Element, {backward}: {backward?: boolean }) {
private getNextLandmark(element: FocusableElement, {backward}: {backward?: boolean }) {
let currentLandmark = this.closestLandmark(element);
let nextLandmarkIndex = backward ? this.landmarks.length - 1 : 0;
if (currentLandmark) {
Expand Down Expand Up @@ -293,7 +293,7 @@ class LandmarkManager implements LandmarkManagerApi {

// Skip over hidden landmarks.
let i = nextLandmarkIndex;
while (this.landmarks[nextLandmarkIndex].ref.current.closest('[aria-hidden=true]')) {
while (this.landmarks[nextLandmarkIndex].ref.current?.closest('[aria-hidden=true]')) {
nextLandmarkIndex += backward ? -1 : 1;
if (wrapIfNeeded()) {
return undefined;
Expand All @@ -315,7 +315,7 @@ class LandmarkManager implements LandmarkManagerApi {
private f6Handler(e: KeyboardEvent) {
if (e.key === 'F6') {
// If alt key pressed, focus main landmark, otherwise navigate forward or backward based on shift key.
let handled = e.altKey ? this.focusMain() : this.navigate(e.target as Element, e.shiftKey);
let handled = e.altKey ? this.focusMain() : this.navigate(e.target as FocusableElement, e.shiftKey);
if (handled) {
e.preventDefault();
e.stopPropagation();
Expand All @@ -325,15 +325,15 @@ class LandmarkManager implements LandmarkManagerApi {

private focusMain() {
let main = this.getLandmarkByRole('main');
if (main && document.contains(main.ref.current)) {
if (main && main.ref.current && document.contains(main.ref.current)) {
this.focusLandmark(main.ref.current, 'forward');
return true;
}

return false;
}

private navigate(from: Element, backward: boolean) {
private navigate(from: FocusableElement, backward: boolean) {
let nextLandmark = this.getNextLandmark(from, {
backward
});
Expand All @@ -352,7 +352,7 @@ class LandmarkManager implements LandmarkManagerApi {
}

// Otherwise, focus the landmark itself
if (document.contains(nextLandmark.ref.current)) {
if (nextLandmark.ref.current && document.contains(nextLandmark.ref.current)) {
this.focusLandmark(nextLandmark.ref.current, backward ? 'backward' : 'forward');
return true;
}
Expand All @@ -365,11 +365,11 @@ class LandmarkManager implements LandmarkManagerApi {
* Lets the last focused landmark know it was blurred if something else is focused.
*/
private focusinHandler(e: FocusEvent) {
let currentLandmark = this.closestLandmark(e.target as Element);
let currentLandmark = this.closestLandmark(e.target as FocusableElement);
if (currentLandmark && currentLandmark.ref.current !== e.target) {
this.updateLandmark({ref: currentLandmark.ref, lastFocused: e.target as FocusableElement});
}
let previousFocusedElement = e.relatedTarget as Element;
let previousFocusedElement = e.relatedTarget as FocusableElement;
if (previousFocusedElement) {
let closestPreviousLandmark = this.closestLandmark(previousFocusedElement);
if (closestPreviousLandmark && closestPreviousLandmark.ref.current === previousFocusedElement) {
Expand All @@ -382,7 +382,7 @@ class LandmarkManager implements LandmarkManagerApi {
* Track if the focus is lost to the body. If it is, do cleanup on the landmark that last had focus.
*/
private focusoutHandler(e: FocusEvent) {
let previousFocusedElement = e.target as Element;
let previousFocusedElement = e.target as FocusableElement;
let nextFocusedElement = e.relatedTarget;
// the === document seems to be a jest thing for focus to go there on generic blur event such as landmark.blur();
// browsers appear to send focus instead to document.body and the relatedTarget is null when that happens
Expand All @@ -395,26 +395,31 @@ class LandmarkManager implements LandmarkManagerApi {
}

public createLandmarkController(): LandmarkController {
let instance = this;
let instance: LandmarkManager | null = this;
instance.refCount++;
instance.setupIfNeeded();
return {
navigate(direction, opts) {
return instance.navigate(opts?.from || document.activeElement, direction === 'backward');
let element = opts?.from || (document!.activeElement as FocusableElement);
return instance!.navigate(element, direction === 'backward');
},
focusNext(opts) {
return instance.navigate(opts?.from || document.activeElement, false);
let element = opts?.from || (document!.activeElement as FocusableElement);
return instance!.navigate(element, false);
},
focusPrevious(opts) {
return instance.navigate(opts?.from || document.activeElement, true);
let element = opts?.from || (document!.activeElement as FocusableElement);
return instance!.navigate(element, true);
},
focusMain() {
return instance.focusMain();
return instance!.focusMain();
},
dispose() {
instance.refCount--;
instance.teardownIfNeeded();
instance = null;
if (instance) {
instance.refCount--;
instance.teardownIfNeeded();
instance = null;
}
}
};
}
Expand All @@ -433,35 +438,35 @@ class LandmarkManager implements LandmarkManagerApi {
/** Creates a LandmarkController, which allows programmatic navigation of landmarks. */
export function createLandmarkController(): LandmarkController {
// Get the current landmark manager and create a controller using it.
let instance = getLandmarkManager();
let controller = instance.createLandmarkController();
let instance: LandmarkManagerApi | null = getLandmarkManager();
let controller = instance?.createLandmarkController();

let unsubscribe = subscribe(() => {
// If the landmark manager changes, dispose the old
// controller and create a new one.
controller.dispose();
controller?.dispose();
instance = getLandmarkManager();
controller = instance.createLandmarkController();
controller = instance?.createLandmarkController();
});

// Return a wrapper that proxies requests to the current controller instance.
return {
navigate(direction, opts) {
return controller.navigate(direction, opts);
return controller!.navigate(direction, opts);
},
focusNext(opts) {
return controller.focusNext(opts);
return controller!.focusNext(opts);
},
focusPrevious(opts) {
return controller.focusPrevious(opts);
return controller!.focusPrevious(opts);
},
focusMain() {
return controller.focusMain();
return controller!.focusMain();
},
dispose() {
controller.dispose();
controller?.dispose();
unsubscribe();
controller = null;
controller = undefined;
instance = null;
}
};
Expand All @@ -472,7 +477,7 @@ export function createLandmarkController(): LandmarkController {
* @param props - Props for the landmark.
* @param ref - Ref to the landmark.
*/
export function useLandmark(props: AriaLandmarkProps, ref: MutableRefObject<FocusableElement>): LandmarkAria {
export function useLandmark(props: AriaLandmarkProps, ref: RefObject<FocusableElement>): LandmarkAria {
const {
role,
'aria-label': ariaLabel,
Expand All @@ -492,12 +497,14 @@ export function useLandmark(props: AriaLandmarkProps, ref: MutableRefObject<Focu
}, [setIsLandmarkFocused]);

useLayoutEffect(() => {
return manager.registerLandmark({ref, label, role, focus: focus || defaultFocus, blur});
if (manager) {
return manager.registerLandmark({ref, label, role, focus: focus || defaultFocus, blur});
}
}, [manager, label, ref, role, focus, defaultFocus, blur]);

useEffect(() => {
if (isLandmarkFocused) {
ref.current.focus();
ref.current?.focus();
}
}, [isLandmarkFocused, ref]);

Expand Down
36 changes: 21 additions & 15 deletions packages/@react-aria/landmark/stories/Landmark.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import {ActionGroup, Item} from '@react-spectrum/actiongroup';
import {Cell, Column, Row, TableBody, TableHeader, TableView} from '@react-spectrum/table';
import {Checkbox} from '@react-spectrum/checkbox';
import {classNames, useStyleProps} from '@react-spectrum/utils';
import {classNames, useFocusableRef, useStyleProps} from '@react-spectrum/utils';
import {createLandmarkController, useLandmark} from '../';
import {Flex} from '@react-spectrum/layout';
import {Link} from '@react-spectrum/link';
Expand Down Expand Up @@ -43,28 +43,28 @@ const OneWithNoFocusableChildrenExampleTemplate = (): Story<StoryProps> => (prop
const AllWithNoFocusableChildrenExampleTemplate = (): Story<StoryProps> => (props) => <AllWithNoFocusableChildrenExample {...props} />;

function Main(props) {
let ref = useRef();
let ref = useFocusableRef(null);
let {styleProps} = useStyleProps(props);
let {landmarkProps} = useLandmark({...props, role: 'main'}, ref);
return <main aria-label="Danni's unicorn corral" ref={ref} {...props} {...landmarkProps} {...styleProps}>{props.children}</main>;
}

function Navigation(props) {
let ref = useRef();
let ref = useFocusableRef(null);
let {styleProps} = useStyleProps(props);
let {landmarkProps} = useLandmark({...props, role: 'navigation'}, ref);
return <nav aria-label="Rainbow lookout" ref={ref} {...props} {...landmarkProps} {...styleProps}>{props.children}</nav>;
}

function Region(props) {
let ref = useRef();
let ref = useFocusableRef(null);
let {styleProps} = useStyleProps(props);
let {landmarkProps} = useLandmark({...props, role: 'region'}, ref);
return <article aria-label="The greens" ref={ref} {...props} {...landmarkProps} {...styleProps}>{props.children}</article>;
}

function Search(props) {
let ref = useRef();
let ref = useFocusableRef(null);
let {styleProps} = useStyleProps(props);
let {landmarkProps} = useLandmark({...props, role: 'search'}, ref);
return (
Expand Down Expand Up @@ -318,12 +318,18 @@ function IframeExample() {
let onLoad = (e: SyntheticEvent) => {
let iframe = e.target as HTMLIFrameElement;
let window = iframe.contentWindow;
let document = window.document;
let document = window?.document;
if (!window || !document) {
return;
}

let prevFocusedElement = null;
window.addEventListener('react-aria-landmark-navigation', (e: CustomEvent) => {
let prevFocusedElement: HTMLElement | null = null;
window.addEventListener('react-aria-landmark-navigation', ((e: CustomEvent) => {
e.preventDefault();
let el = document.activeElement;
if (!window || !document) {
return;
}
let el = document.activeElement as HTMLElement;
if (el !== document.body) {
prevFocusedElement = el;
}
Expand All @@ -337,9 +343,9 @@ function IframeExample() {
});

setTimeout(() => {
document.body.removeAttribute('data-react-aria-top-layer');
document?.body.removeAttribute('data-react-aria-top-layer');
}, 100);
});
}) as EventListener);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


// When the iframe is re-focused, restore focus back inside where it was before.
window.addEventListener('focus', () => {
Expand All @@ -353,17 +359,18 @@ function IframeExample() {
window.addEventListener('message', e => {
if (e.data.type === 'landmark-navigation') {
// (Can't use LandmarkController in this example because we need the controller instance inside the iframe)
document.body.dispatchEvent(new KeyboardEvent('keydown', {key: 'F6', shiftKey: e.data.direction === 'backward', bubbles: true}));
document?.body.dispatchEvent(new KeyboardEvent('keydown', {key: 'F6', shiftKey: e.data.direction === 'backward', bubbles: true}));
}
});
};

let ref = useRef<HTMLIFrameElement>(null);
useEffect(() => {
let onMessage = (e: MessageEvent) => {
let iframe = ref.current;
if (e.data.type === 'landmark-navigation') {
// Move focus to the iframe so that when focus is restored there, and we can redirect it back inside (below).
iframe.focus();
iframe?.focus();

// Now re-dispatch the keyboard event so landmark navigation outside the iframe picks it up.
controller.navigate(e.data.direction);
Expand All @@ -374,12 +381,11 @@ function IframeExample() {
return () => window.removeEventListener('message', onMessage);
}, [controller]);

let ref = useRef(null);
let {landmarkProps} = useLandmark({
role: 'main',
focus(direction) {
// when iframe landmark receives focus via landmark navigation, go to first/last landmark inside iframe.
ref.current.contentWindow.postMessage({
ref.current?.contentWindow?.postMessage({
type: 'landmark-navigation',
direction
});
Expand Down
Loading