Commit 8ee4ceca authored by Robert Knight's avatar Robert Knight

Adapt to `useRef` type changes

`useRef(null as T|null)` now returns a `{ current: T|null }` instead of
`{ current: T }` as it did before. ie. it no longer drops the the null.
This makes sense but conflicted with a pattern we used in many places to
create a non-null ref: `useRef(/** @type {T|null} */ (null))`.

Resolve this by changing all non-nullable refs, for elements which are
set after the initial render, to cast the `useRef` result instead of the
init value.

```
const nonNullRef = /** @type {{ current: T }} */ (useRef());
```
parent 2bb54dcd
......@@ -33,6 +33,8 @@ function NotebookIframe({ config, groupId }) {
);
}
/** @typedef {import('../util/emitter').Emitter} Emitter */
/**
* @typedef NotebookModalProps
* @prop {import('../util/emitter').EventBus} eventBus
......@@ -53,9 +55,7 @@ export default function NotebookModal({ eventBus, config }) {
const [isHidden, setIsHidden] = useState(true);
const [groupId, setGroupId] = useState(/** @type {string|null} */ (null));
const originalDocumentOverflowStyle = useRef('');
const emitter = useRef(
/** @type {ReturnType<eventBus['createEmitter']>|null} */ (null)
);
const emitterRef = useRef(/** @type {Emitter|null} */ (null));
// Stores the original overflow CSS property of document.body and reset it
// when the component is destroyed
......@@ -78,24 +78,22 @@ export default function NotebookModal({ eventBus, config }) {
}, [isHidden]);
useEffect(() => {
emitter.current = eventBus.createEmitter();
emitter.current.subscribe(
'openNotebook',
(/** @type {string} */ groupId) => {
setIsHidden(false);
setIframeKey(iframeKey => iframeKey + 1);
setGroupId(groupId);
}
);
const emitter = eventBus.createEmitter();
emitter.subscribe('openNotebook', (/** @type {string} */ groupId) => {
setIsHidden(false);
setIframeKey(iframeKey => iframeKey + 1);
setGroupId(groupId);
});
emitterRef.current = emitter;
return () => {
emitter.current.destroy();
emitter.destroy();
};
}, [eventBus]);
const onClose = () => {
setIsHidden(true);
emitter.current.publish('closeNotebook');
emitterRef.current?.publish('closeNotebook');
};
if (groupId === null) {
......
......@@ -10,7 +10,7 @@ import { TinyEmitter } from 'tiny-emitter';
*
* @implements Destroyable
*/
class Emitter {
export class Emitter {
/**
* @param {TinyEmitter} emitter
*/
......
......@@ -60,6 +60,9 @@ function AnnotationShareControl({
const closePanel = () => setOpen(false);
// Interactions outside of the component when it is open should close it
//
// @ts-expect-error - `useElementShouldClose` requires a non-nullable ref,
// but `shareRef` is nullable.
useElementShouldClose(shareRef, isOpen, closePanel);
useEffect(() => {
......
......@@ -87,7 +87,7 @@ function Excerpt({
useState(true);
// Container for the excerpt's content.
const contentElement = useRef(/** @type {HTMLDivElement|null} */ (null));
const contentElement = /** @type {{ current: HTMLDivElement }} */ (useRef());
// Measured height of `contentElement` in pixels.
const [contentHeight, setContentHeight] = useState(0);
......
......@@ -402,15 +402,16 @@ export default function MarkdownEditor({
useEffect(() => {
if (!preview) {
input.current.focus();
input.current?.focus();
}
}, [preview]);
const togglePreview = () => setPreview(!preview);
const handleCommand = command => {
const inputEl = input.current;
handleToolbarCommand(command, inputEl);
onEditText({ text: inputEl.value });
if (input.current) {
handleToolbarCommand(command, input.current);
onEditText({ text: input.current.value });
}
};
const handleKeyDown = event => {
......
......@@ -28,7 +28,7 @@ export default function MarkdownView({
() => (markdown ? renderMarkdown(markdown) : ''),
[markdown]
);
const content = useRef(/** @type {HTMLDivElement|null} */ (null));
const content = /** @type {{ current: HTMLDivElement }} */ (useRef());
useEffect(() => {
replaceLinksWithEmbeds(content.current, {
......
......@@ -138,7 +138,7 @@ export default function Menu({
//
// These handlers close the menu when the user taps or clicks outside the
// menu or presses Escape.
const menuRef = useRef(/** @type {HTMLDivElement|null} */ (null));
const menuRef = /** @type {{ current: HTMLDivElement }} */ (useRef());
// Menu element should close via `closeMenu` whenever it's open and there
// are user interactions outside of it (e.g. clicks) in the document
......
......@@ -81,9 +81,9 @@ export default function MenuItem({
const hasLeftIcon = icon || isSubmenuItem;
const hasRightIcon = icon && isSubmenuItem;
const menuItemRef = useRef(
/** @type {(HTMLAnchorElement & HTMLDivElement)|null} */ (null)
);
const menuItemRef =
/** @type {{ current: HTMLAnchorElement & HTMLDivElement }} */ (useRef());
let focusTimer = null;
let renderedIcon = null;
......
......@@ -28,7 +28,7 @@ export default function MenuKeyboardNavigation({
children,
visible,
}) {
const menuRef = useRef(/** @type {HTMLDivElement|null} */ (null));
const menuRef = /** @type {{ current: HTMLDivElement }} */ (useRef());
useEffect(() => {
let focusTimer = null;
......
......@@ -28,7 +28,7 @@ import { useStoreProxy } from '../store/use-store';
export default function SearchInput({ alwaysExpanded, query, onSearch }) {
const store = useStoreProxy();
const isLoading = store.isLoading();
const input = useRef(/** @type {HTMLInputElement|null} */ (null));
const input = /** @type {{ current: HTMLInputElement }} */ (useRef());
// The active filter query from the previous render.
const [prevQuery, setPrevQuery] = useState(query);
......
......@@ -19,7 +19,7 @@ import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
* @param {SliderProps} props
*/
export default function Slider({ children, visible }) {
const containerRef = useRef(/** @type {HTMLDivElement|null} */ (null));
const containerRef = /** @type {{ current: HTMLDivElement }} */ (useRef());
const [containerHeight, setContainerHeight] = useState(visible ? 'auto' : 0);
// Whether the content is currently partially or wholly visible. This is
......
......@@ -38,7 +38,7 @@ function TagEditor({
tagList,
tags: tagsService,
}) {
const inputEl = useRef(/** @type {HTMLInputElement|null} */ (null));
const inputEl = /** @type {{ current: HTMLInputElement }} */ (useRef());
const [suggestions, setSuggestions] = useState(/** @type {string[]} */ ([]));
const [activeItem, setActiveItem] = useState(-1); // -1 is unselected
const [suggestionsListOpen, setSuggestionsListOpen] = useState(false);
......@@ -48,7 +48,7 @@ function TagEditor({
});
// Set up callback to monitor outside click events to close the AutocompleteList
const closeWrapperRef = useRef(/** @type {HTMLElement|null} */ (null));
const closeWrapperRef = /** @type {{ current: HTMLElement }} */ (useRef());
useElementShouldClose(closeWrapperRef, suggestionsListOpen, () => {
setSuggestionsListOpen(false);
});
......
......@@ -144,5 +144,5 @@ export function useStoreProxy() {
return cleanup;
}, [cache, store]);
return proxy.current;
return /** @type {SidebarStore} */ (proxy.current);
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment