Replace reakit/Composite with react-roving-tabindex (#6985)
* fix: replace reakit composite with react-roving-tabindex * fix: touch points * fix: focus stuck at first list item * fix: document history navigation * fix: remove ununsed ListItem components * fix: keyboard navigation in recent search list * fix: updated lib
This commit is contained in:
@@ -1,13 +1,9 @@
|
||||
import { RovingTabIndexProvider } from "@getoutline/react-roving-tabindex";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import {
|
||||
useCompositeState,
|
||||
Composite,
|
||||
CompositeStateReturn,
|
||||
} from "reakit/Composite";
|
||||
|
||||
type Props = React.HTMLAttributes<HTMLDivElement> & {
|
||||
children: (composite: CompositeStateReturn) => React.ReactNode;
|
||||
children: () => React.ReactNode;
|
||||
onEscape?: (ev: React.KeyboardEvent<HTMLDivElement>) => void;
|
||||
};
|
||||
|
||||
@@ -15,40 +11,36 @@ function ArrowKeyNavigation(
|
||||
{ children, onEscape, ...rest }: Props,
|
||||
ref: React.RefObject<HTMLDivElement>
|
||||
) {
|
||||
const composite = useCompositeState();
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(ev) => {
|
||||
(ev: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (onEscape) {
|
||||
if (ev.nativeEvent.isComposing) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.key === "Escape") {
|
||||
ev.preventDefault();
|
||||
onEscape(ev);
|
||||
}
|
||||
|
||||
if (
|
||||
ev.key === "ArrowUp" &&
|
||||
composite.currentId === composite.items[0].id
|
||||
// If the first item is focused and the user presses ArrowUp
|
||||
ev.currentTarget.firstElementChild === document.activeElement
|
||||
) {
|
||||
onEscape(ev);
|
||||
}
|
||||
}
|
||||
},
|
||||
[composite.currentId, composite.items, onEscape]
|
||||
[onEscape]
|
||||
);
|
||||
|
||||
return (
|
||||
<Composite
|
||||
{...rest}
|
||||
{...composite}
|
||||
onKeyDown={handleKeyDown}
|
||||
role="menu"
|
||||
ref={ref}
|
||||
>
|
||||
{children(composite)}
|
||||
</Composite>
|
||||
<RovingTabIndexProvider options={{ focusOnClick: true, direction: "both" }}>
|
||||
<div {...rest} onKeyDown={handleKeyDown} ref={ref}>
|
||||
{children()}
|
||||
</div>
|
||||
</RovingTabIndexProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import {
|
||||
useFocusEffect,
|
||||
useRovingTabIndex,
|
||||
} from "@getoutline/react-roving-tabindex";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { CompositeStateReturn, CompositeItem } from "reakit/Composite";
|
||||
import styled, { css } from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import { s } from "@shared/styles";
|
||||
@@ -32,7 +35,7 @@ type Props = {
|
||||
showPin?: boolean;
|
||||
showDraft?: boolean;
|
||||
showTemplate?: boolean;
|
||||
} & CompositeStateReturn;
|
||||
};
|
||||
|
||||
const SEARCH_RESULT_REGEX = /<b\b[^>]*>(.*?)<\/b>/gi;
|
||||
|
||||
@@ -49,6 +52,15 @@ function DocumentListItem(
|
||||
const user = useCurrentUser();
|
||||
const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean();
|
||||
|
||||
let itemRef: React.Ref<HTMLAnchorElement> =
|
||||
React.useRef<HTMLAnchorElement>(null);
|
||||
if (ref) {
|
||||
itemRef = ref;
|
||||
}
|
||||
|
||||
const { focused, ...rovingTabIndex } = useRovingTabIndex(itemRef, false);
|
||||
useFocusEffect(focused, itemRef);
|
||||
|
||||
const {
|
||||
document,
|
||||
showParentDocuments,
|
||||
@@ -68,9 +80,8 @@ function DocumentListItem(
|
||||
!document.isDraft && !document.isArchived && !document.isTemplate;
|
||||
|
||||
return (
|
||||
<CompositeItem
|
||||
as={DocumentLink}
|
||||
ref={ref}
|
||||
<DocumentLink
|
||||
ref={itemRef}
|
||||
dir={document.dir}
|
||||
role="menuitem"
|
||||
$isStarred={document.isStarred}
|
||||
@@ -82,6 +93,7 @@ function DocumentListItem(
|
||||
},
|
||||
}}
|
||||
{...rest}
|
||||
{...rovingTabIndex}
|
||||
>
|
||||
<Content>
|
||||
<Heading dir={document.dir}>
|
||||
@@ -142,7 +154,7 @@ function DocumentListItem(
|
||||
modal={false}
|
||||
/>
|
||||
</Actions>
|
||||
</CompositeItem>
|
||||
</DocumentLink>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,16 +11,12 @@ import {
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { CompositeStateReturn } from "reakit/Composite";
|
||||
import styled, { css } from "styled-components";
|
||||
import { s } from "@shared/styles";
|
||||
import Document from "~/models/Document";
|
||||
import Event from "~/models/Event";
|
||||
import Avatar from "~/components/Avatar";
|
||||
import CompositeItem, {
|
||||
Props as ItemProps,
|
||||
} from "~/components/List/CompositeItem";
|
||||
import Item, { Actions } from "~/components/List/Item";
|
||||
import Item, { Actions, Props as ItemProps } from "~/components/List/Item";
|
||||
import Time from "~/components/Time";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import RevisionMenu from "~/menus/RevisionMenu";
|
||||
@@ -32,7 +28,7 @@ type Props = {
|
||||
document: Document;
|
||||
event: Event;
|
||||
latest?: boolean;
|
||||
} & CompositeStateReturn;
|
||||
};
|
||||
|
||||
const EventListItem = ({ event, latest, document, ...rest }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -176,11 +172,7 @@ const BaseItem = React.forwardRef(function _BaseItem(
|
||||
{ to, ...rest }: ItemProps,
|
||||
ref?: React.Ref<HTMLAnchorElement>
|
||||
) {
|
||||
if (to) {
|
||||
return <CompositeListItem to={to} ref={ref} {...rest} />;
|
||||
}
|
||||
|
||||
return <ListItem ref={ref} {...rest} />;
|
||||
return <ListItem to={to} ref={ref} {...rest} />;
|
||||
});
|
||||
|
||||
const Subtitle = styled.span`
|
||||
@@ -240,8 +232,4 @@ const ListItem = styled(Item)`
|
||||
${ItemStyle}
|
||||
`;
|
||||
|
||||
const CompositeListItem = styled(CompositeItem)`
|
||||
${ItemStyle}
|
||||
`;
|
||||
|
||||
export default observer(EventListItem);
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
CompositeStateReturn,
|
||||
CompositeItem as BaseCompositeItem,
|
||||
} from "reakit/Composite";
|
||||
import Item, { Props as ItemProps } from "./Item";
|
||||
|
||||
export type Props = ItemProps & CompositeStateReturn;
|
||||
|
||||
function CompositeItem(
|
||||
{ to, ...rest }: Props,
|
||||
ref?: React.Ref<HTMLAnchorElement>
|
||||
) {
|
||||
return <BaseCompositeItem as={Item} to={to} {...rest} ref={ref} />;
|
||||
}
|
||||
|
||||
export default React.forwardRef(CompositeItem);
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
useFocusEffect,
|
||||
useRovingTabIndex,
|
||||
} from "@getoutline/react-roving-tabindex";
|
||||
import { LocationDescriptor } from "history";
|
||||
import * as React from "react";
|
||||
import styled, { useTheme } from "styled-components";
|
||||
@@ -33,6 +37,18 @@ const ListItem = (
|
||||
const theme = useTheme();
|
||||
const compact = !subtitle;
|
||||
|
||||
let itemRef: React.Ref<HTMLAnchorElement> =
|
||||
React.useRef<HTMLAnchorElement>(null);
|
||||
if (ref) {
|
||||
itemRef = ref;
|
||||
}
|
||||
|
||||
const { focused, ...rovingTabIndex } = useRovingTabIndex(
|
||||
itemRef as React.RefObject<HTMLAnchorElement>,
|
||||
to ? false : true
|
||||
);
|
||||
useFocusEffect(focused, itemRef as React.RefObject<HTMLAnchorElement>);
|
||||
|
||||
const content = (selected: boolean) => (
|
||||
<>
|
||||
{image && <Image>{image}</Image>}
|
||||
@@ -59,13 +75,20 @@ const ListItem = (
|
||||
if (to) {
|
||||
return (
|
||||
<Wrapper
|
||||
ref={ref}
|
||||
ref={itemRef}
|
||||
$border={border}
|
||||
$small={small}
|
||||
activeStyle={{
|
||||
background: theme.accent,
|
||||
}}
|
||||
{...rest}
|
||||
{...rovingTabIndex}
|
||||
onClick={(ev) => {
|
||||
if (rest.onClick) {
|
||||
rest.onClick(ev);
|
||||
}
|
||||
rovingTabIndex.onClick(ev);
|
||||
}}
|
||||
as={NavLink}
|
||||
to={to}
|
||||
>
|
||||
@@ -75,7 +98,7 @@ const ListItem = (
|
||||
}
|
||||
|
||||
return (
|
||||
<Wrapper ref={ref} $border={border} $small={small} {...rest}>
|
||||
<Wrapper ref={itemRef} $border={border} $small={small} {...rest}>
|
||||
{content(false)}
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
@@ -42,7 +42,7 @@ const PaginatedDocumentList = React.memo<Props>(function PaginatedDocumentList({
|
||||
fetch={fetch}
|
||||
options={options}
|
||||
renderError={(props) => <Error {...props} />}
|
||||
renderItem={(item: Document, _index, compositeProps) => (
|
||||
renderItem={(item: Document, _index) => (
|
||||
<DocumentListItem
|
||||
key={item.id}
|
||||
document={item}
|
||||
@@ -52,7 +52,6 @@ const PaginatedDocumentList = React.memo<Props>(function PaginatedDocumentList({
|
||||
showPublished={showPublished}
|
||||
showTemplate={showTemplate}
|
||||
showDraft={showDraft}
|
||||
{...compositeProps}
|
||||
/>
|
||||
)}
|
||||
{...rest}
|
||||
|
||||
@@ -30,13 +30,12 @@ const PaginatedEventList = React.memo<Props>(function PaginatedEventList({
|
||||
heading={heading}
|
||||
fetch={fetch}
|
||||
options={options}
|
||||
renderItem={(item: Event, index, compositeProps) => (
|
||||
renderItem={(item: Event, index) => (
|
||||
<EventListItem
|
||||
key={item.id}
|
||||
event={item}
|
||||
document={document}
|
||||
latest={index === 0}
|
||||
{...compositeProps}
|
||||
/>
|
||||
)}
|
||||
renderHeading={(name) => <Heading>{name}</Heading>}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { withTranslation, WithTranslation } from "react-i18next";
|
||||
import { Waypoint } from "react-waypoint";
|
||||
import { CompositeStateReturn } from "reakit/Composite";
|
||||
import { Pagination } from "@shared/constants";
|
||||
import RootStore from "~/stores/RootStore";
|
||||
import ArrowKeyNavigation from "~/components/ArrowKeyNavigation";
|
||||
@@ -30,11 +29,7 @@ type Props<T> = WithTranslation &
|
||||
loading?: React.ReactElement;
|
||||
items?: T[];
|
||||
className?: string;
|
||||
renderItem: (
|
||||
item: T,
|
||||
index: number,
|
||||
compositeProps: CompositeStateReturn
|
||||
) => React.ReactNode;
|
||||
renderItem: (item: T, index: number) => React.ReactNode;
|
||||
renderError?: (options: {
|
||||
error: Error;
|
||||
retry: () => void;
|
||||
@@ -194,10 +189,10 @@ class PaginatedList<T extends PaginatedItem> extends React.Component<Props<T>> {
|
||||
onEscape={onEscape}
|
||||
className={this.props.className}
|
||||
>
|
||||
{(composite: CompositeStateReturn) => {
|
||||
{() => {
|
||||
let previousHeading = "";
|
||||
return items.slice(0, this.renderCount).map((item, index) => {
|
||||
const children = this.props.renderItem(item, index, composite);
|
||||
const children = this.props.renderItem(item, index);
|
||||
|
||||
// If there is no renderHeading method passed then no date
|
||||
// headings are rendered
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import {
|
||||
useFocusEffect,
|
||||
useRovingTabIndex,
|
||||
} from "@getoutline/react-roving-tabindex";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { CompositeItem } from "reakit/Composite";
|
||||
import styled, { css } from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import { s, ellipsis } from "@shared/styles";
|
||||
@@ -34,10 +37,18 @@ function DocumentListItem(
|
||||
) {
|
||||
const { document, highlight, context, shareId, ...rest } = props;
|
||||
|
||||
let itemRef: React.Ref<HTMLAnchorElement> =
|
||||
React.useRef<HTMLAnchorElement>(null);
|
||||
if (ref) {
|
||||
itemRef = ref;
|
||||
}
|
||||
|
||||
const { focused, ...rovingTabIndex } = useRovingTabIndex(itemRef, false);
|
||||
useFocusEffect(focused, itemRef);
|
||||
|
||||
return (
|
||||
<CompositeItem
|
||||
as={DocumentLink}
|
||||
ref={ref}
|
||||
<DocumentLink
|
||||
ref={itemRef}
|
||||
dir={document.dir}
|
||||
to={{
|
||||
pathname: shareId
|
||||
@@ -48,6 +59,13 @@ function DocumentListItem(
|
||||
},
|
||||
}}
|
||||
{...rest}
|
||||
{...rovingTabIndex}
|
||||
onClick={(ev) => {
|
||||
if (rest.onClick) {
|
||||
rest.onClick(ev);
|
||||
}
|
||||
rovingTabIndex.onClick(ev);
|
||||
}}
|
||||
>
|
||||
<Content>
|
||||
<Heading dir={document.dir}>
|
||||
@@ -66,7 +84,7 @@ function DocumentListItem(
|
||||
/>
|
||||
}
|
||||
</Content>
|
||||
</CompositeItem>
|
||||
</DocumentLink>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -206,7 +206,7 @@ function SearchPopover({ shareId }: Props) {
|
||||
<NoResults>{t("No results for {{query}}", { query })}</NoResults>
|
||||
}
|
||||
loading={<PlaceholderList count={3} header={{ height: 20 }} />}
|
||||
renderItem={(item: SearchResult, index, compositeProps) => (
|
||||
renderItem={(item: SearchResult, index) => (
|
||||
<SearchListItem
|
||||
key={item.document.id}
|
||||
shareId={shareId}
|
||||
@@ -215,7 +215,6 @@ function SearchPopover({ shareId }: Props) {
|
||||
context={item.context}
|
||||
highlight={cachedQuery}
|
||||
onClick={handleSearchItemClick}
|
||||
{...compositeProps}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user