Enable keyboard navigation in member invite list (#7102)
* feat: keyboard nav in share popover * fix: memoize
This commit is contained in:
@@ -5,10 +5,11 @@ import * as React from "react";
|
||||
type Props = React.HTMLAttributes<HTMLDivElement> & {
|
||||
children: () => React.ReactNode;
|
||||
onEscape?: (ev: React.KeyboardEvent<HTMLDivElement>) => void;
|
||||
items: unknown[];
|
||||
};
|
||||
|
||||
function ArrowKeyNavigation(
|
||||
{ children, onEscape, ...rest }: Props,
|
||||
{ children, onEscape, items, ...rest }: Props,
|
||||
ref: React.RefObject<HTMLDivElement>
|
||||
) {
|
||||
const handleKeyDown = React.useCallback(
|
||||
@@ -36,7 +37,10 @@ function ArrowKeyNavigation(
|
||||
);
|
||||
|
||||
return (
|
||||
<RovingTabIndexProvider options={{ focusOnClick: true, direction: "both" }}>
|
||||
<RovingTabIndexProvider
|
||||
options={{ focusOnClick: true, direction: "both" }}
|
||||
items={items}
|
||||
>
|
||||
<div {...rest} onKeyDown={handleKeyDown} ref={ref}>
|
||||
{children()}
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ import styled, { useTheme } from "styled-components";
|
||||
import { s, ellipsis } from "@shared/styles";
|
||||
import Flex from "~/components/Flex";
|
||||
import NavLink from "~/components/NavLink";
|
||||
import { hover } from "~/styles";
|
||||
|
||||
export type Props = Omit<React.HTMLAttributes<HTMLAnchorElement>, "title"> & {
|
||||
/** An icon or image to display to the left of the list item */
|
||||
@@ -16,6 +17,8 @@ export type Props = Omit<React.HTMLAttributes<HTMLAnchorElement>, "title"> & {
|
||||
to?: LocationDescriptor;
|
||||
/** An optional click handler, if provided the list item will have hover styles */
|
||||
onClick?: React.MouseEventHandler<HTMLAnchorElement>;
|
||||
/** An optional keydown handler, if provided the list item will have hover styles */
|
||||
onKeyDown?: React.KeyboardEventHandler<HTMLAnchorElement>;
|
||||
/** Whether to match the location exactly */
|
||||
exact?: boolean;
|
||||
/** The title of the list item */
|
||||
@@ -28,10 +31,22 @@ export type Props = Omit<React.HTMLAttributes<HTMLAnchorElement>, "title"> & {
|
||||
border?: boolean;
|
||||
/** Whether to display the list item in a compact style */
|
||||
small?: boolean;
|
||||
/** Whether to enable keyboard navigation */
|
||||
keyboardNavigation?: boolean;
|
||||
};
|
||||
|
||||
const ListItem = (
|
||||
{ image, title, subtitle, actions, small, border, to, ...rest }: Props,
|
||||
{
|
||||
image,
|
||||
title,
|
||||
subtitle,
|
||||
actions,
|
||||
small,
|
||||
border,
|
||||
to,
|
||||
keyboardNavigation,
|
||||
...rest
|
||||
}: Props,
|
||||
ref?: React.Ref<HTMLAnchorElement>
|
||||
) => {
|
||||
const theme = useTheme();
|
||||
@@ -45,7 +60,7 @@ const ListItem = (
|
||||
|
||||
const { focused, ...rovingTabIndex } = useRovingTabIndex(
|
||||
itemRef as React.RefObject<HTMLAnchorElement>,
|
||||
to ? false : true
|
||||
keyboardNavigation || to ? false : true
|
||||
);
|
||||
useFocusEffect(focused, itemRef as React.RefObject<HTMLAnchorElement>);
|
||||
|
||||
@@ -89,6 +104,12 @@ const ListItem = (
|
||||
}
|
||||
rovingTabIndex.onClick(ev);
|
||||
}}
|
||||
onKeyDown={(ev) => {
|
||||
if (rest.onKeyDown) {
|
||||
rest.onKeyDown(ev);
|
||||
}
|
||||
rovingTabIndex.onKeyDown(ev);
|
||||
}}
|
||||
as={NavLink}
|
||||
to={to}
|
||||
>
|
||||
@@ -98,7 +119,25 @@ const ListItem = (
|
||||
}
|
||||
|
||||
return (
|
||||
<Wrapper ref={itemRef} $border={border} $small={small} {...rest}>
|
||||
<Wrapper
|
||||
ref={itemRef}
|
||||
$border={border}
|
||||
$small={small}
|
||||
{...rest}
|
||||
{...rovingTabIndex}
|
||||
onClick={(ev) => {
|
||||
if (rest.onClick) {
|
||||
rest.onClick(ev);
|
||||
}
|
||||
rovingTabIndex.onClick(ev);
|
||||
}}
|
||||
onKeyDown={(ev) => {
|
||||
if (rest.onKeyDown) {
|
||||
rest.onKeyDown(ev);
|
||||
}
|
||||
rovingTabIndex.onKeyDown(ev);
|
||||
}}
|
||||
>
|
||||
{content(false)}
|
||||
</Wrapper>
|
||||
);
|
||||
@@ -123,7 +162,13 @@ const Wrapper = styled.a<{
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:${hover},
|
||||
&:focus,
|
||||
&:focus-within {
|
||||
background: ${(props) =>
|
||||
props.onClick ? props.theme.secondaryBackground : "inherit"};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import isEqual from "lodash/isEqual";
|
||||
import { observable, action } from "mobx";
|
||||
import { observable, action, computed } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { withTranslation, WithTranslation } from "react-i18next";
|
||||
@@ -39,7 +39,9 @@ type Props<T> = WithTranslation &
|
||||
};
|
||||
|
||||
@observer
|
||||
class PaginatedList<T extends PaginatedItem> extends React.Component<Props<T>> {
|
||||
class PaginatedList<T extends PaginatedItem> extends React.PureComponent<
|
||||
Props<T>
|
||||
> {
|
||||
@observable
|
||||
error?: Error;
|
||||
|
||||
@@ -145,6 +147,11 @@ class PaginatedList<T extends PaginatedItem> extends React.Component<Props<T>> {
|
||||
}
|
||||
};
|
||||
|
||||
@computed
|
||||
get itemsToRender() {
|
||||
return this.props.items?.slice(0, this.renderCount) ?? [];
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
items = [],
|
||||
@@ -188,10 +195,11 @@ class PaginatedList<T extends PaginatedItem> extends React.Component<Props<T>> {
|
||||
aria-label={this.props["aria-label"]}
|
||||
onEscape={onEscape}
|
||||
className={this.props.className}
|
||||
items={this.itemsToRender}
|
||||
>
|
||||
{() => {
|
||||
let previousHeading = "";
|
||||
return items.slice(0, this.renderCount).map((item, index) => {
|
||||
return this.itemsToRender.map((item, index) => {
|
||||
const children = this.props.renderItem(item, index);
|
||||
|
||||
// If there is no renderHeading method passed then no date
|
||||
|
||||
@@ -20,6 +20,7 @@ import useBoolean from "~/hooks/useBoolean";
|
||||
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
||||
import useKeyDown from "~/hooks/useKeyDown";
|
||||
import usePolicy from "~/hooks/usePolicy";
|
||||
import usePrevious from "~/hooks/usePrevious";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { EmptySelectValue, Permission } from "~/types";
|
||||
import { collectionPath, urlify } from "~/utils/routeHelpers";
|
||||
@@ -56,6 +57,11 @@ function SharePopover({ collection, visible, onRequestClose }: Props) {
|
||||
CollectionPermission.Read
|
||||
);
|
||||
|
||||
const prevPendingIds = usePrevious(pendingIds);
|
||||
|
||||
const suggestionsRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const searchInputRef = React.useRef<HTMLInputElement | null>(null);
|
||||
|
||||
useKeyDown(
|
||||
"Escape",
|
||||
(ev) => {
|
||||
@@ -97,6 +103,19 @@ function SharePopover({ collection, visible, onRequestClose }: Props) {
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (prevPendingIds && pendingIds.length > prevPendingIds.length) {
|
||||
setQuery("");
|
||||
searchInputRef.current?.focus();
|
||||
} else if (prevPendingIds && pendingIds.length < prevPendingIds.length) {
|
||||
const firstPending = suggestionsRef.current?.firstElementChild;
|
||||
|
||||
if (firstPending) {
|
||||
(firstPending as HTMLAnchorElement).focus();
|
||||
}
|
||||
}
|
||||
}, [pendingIds, prevPendingIds]);
|
||||
|
||||
const handleQuery = React.useCallback(
|
||||
(event) => {
|
||||
showPicker();
|
||||
@@ -119,6 +138,39 @@ function SharePopover({ collection, visible, onRequestClose }: Props) {
|
||||
[setPendingIds]
|
||||
);
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(ev: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (ev.nativeEvent.isComposing) {
|
||||
return;
|
||||
}
|
||||
if (ev.key === "ArrowDown" && !ev.shiftKey) {
|
||||
ev.preventDefault();
|
||||
|
||||
if (ev.currentTarget.value) {
|
||||
const length = ev.currentTarget.value.length;
|
||||
const selectionStart = ev.currentTarget.selectionStart || 0;
|
||||
if (selectionStart < length) {
|
||||
ev.currentTarget.selectionStart = length;
|
||||
ev.currentTarget.selectionEnd = length;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const firstSuggestion = suggestionsRef.current?.firstElementChild;
|
||||
|
||||
if (firstSuggestion) {
|
||||
(firstSuggestion as HTMLAnchorElement).focus();
|
||||
}
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleEscape = React.useCallback(
|
||||
() => searchInputRef.current?.focus(),
|
||||
[]
|
||||
);
|
||||
|
||||
const inviteAction = React.useMemo(
|
||||
() =>
|
||||
createAction({
|
||||
@@ -292,8 +344,10 @@ function SharePopover({ collection, visible, onRequestClose }: Props) {
|
||||
<Wrapper>
|
||||
{can.update && (
|
||||
<SearchInput
|
||||
ref={searchInputRef}
|
||||
onChange={handleQuery}
|
||||
onClick={showPicker}
|
||||
onKeyDown={handleKeyDown}
|
||||
query={query}
|
||||
back={backButton}
|
||||
action={rightButton}
|
||||
@@ -303,11 +357,13 @@ function SharePopover({ collection, visible, onRequestClose }: Props) {
|
||||
{picker && (
|
||||
<div>
|
||||
<Suggestions
|
||||
ref={suggestionsRef}
|
||||
query={query}
|
||||
collection={collection}
|
||||
pendingIds={pendingIds}
|
||||
addPendingId={handleAddPendingId}
|
||||
removePendingId={handleRemovePendingId}
|
||||
onEscape={handleEscape}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -18,6 +18,7 @@ import useBoolean from "~/hooks/useBoolean";
|
||||
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
||||
import useKeyDown from "~/hooks/useKeyDown";
|
||||
import usePolicy from "~/hooks/usePolicy";
|
||||
import usePrevious from "~/hooks/usePrevious";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { Permission } from "~/types";
|
||||
import { documentPath, urlify } from "~/utils/routeHelpers";
|
||||
@@ -64,6 +65,11 @@ function SharePopover({
|
||||
DocumentPermission.Read
|
||||
);
|
||||
|
||||
const prevPendingIds = usePrevious(pendingIds);
|
||||
|
||||
const suggestionsRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const searchInputRef = React.useRef<HTMLInputElement | null>(null);
|
||||
|
||||
useKeyDown(
|
||||
"Escape",
|
||||
(ev) => {
|
||||
@@ -107,6 +113,19 @@ function SharePopover({
|
||||
}
|
||||
}, [picker]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (prevPendingIds && pendingIds.length > prevPendingIds.length) {
|
||||
setQuery("");
|
||||
searchInputRef.current?.focus();
|
||||
} else if (prevPendingIds && pendingIds.length < prevPendingIds.length) {
|
||||
const firstPending = suggestionsRef.current?.firstElementChild;
|
||||
|
||||
if (firstPending) {
|
||||
(firstPending as HTMLAnchorElement).focus();
|
||||
}
|
||||
}
|
||||
}, [pendingIds, prevPendingIds]);
|
||||
|
||||
const inviteAction = React.useMemo(
|
||||
() =>
|
||||
createAction({
|
||||
@@ -202,6 +221,39 @@ function SharePopover({
|
||||
[setPendingIds]
|
||||
);
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(ev: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (ev.nativeEvent.isComposing) {
|
||||
return;
|
||||
}
|
||||
if (ev.key === "ArrowDown" && !ev.shiftKey) {
|
||||
ev.preventDefault();
|
||||
|
||||
if (ev.currentTarget.value) {
|
||||
const length = ev.currentTarget.value.length;
|
||||
const selectionStart = ev.currentTarget.selectionStart || 0;
|
||||
if (selectionStart < length) {
|
||||
ev.currentTarget.selectionStart = length;
|
||||
ev.currentTarget.selectionEnd = length;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const firstSuggestion = suggestionsRef.current?.firstElementChild;
|
||||
|
||||
if (firstSuggestion) {
|
||||
(firstSuggestion as HTMLAnchorElement).focus();
|
||||
}
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleEscape = React.useCallback(
|
||||
() => searchInputRef.current?.focus(),
|
||||
[]
|
||||
);
|
||||
|
||||
const permissions = React.useMemo(
|
||||
() =>
|
||||
[
|
||||
@@ -266,8 +318,10 @@ function SharePopover({
|
||||
<Wrapper>
|
||||
{can.manageUsers && (
|
||||
<SearchInput
|
||||
ref={searchInputRef}
|
||||
onChange={handleQuery}
|
||||
onClick={showPicker}
|
||||
onKeyDown={handleKeyDown}
|
||||
query={query}
|
||||
back={backButton}
|
||||
action={rightButton}
|
||||
@@ -275,15 +329,15 @@ function SharePopover({
|
||||
)}
|
||||
|
||||
{picker && (
|
||||
<div>
|
||||
<Suggestions
|
||||
document={document}
|
||||
query={query}
|
||||
pendingIds={pendingIds}
|
||||
addPendingId={handleAddPendingId}
|
||||
removePendingId={handleRemovePendingId}
|
||||
/>
|
||||
</div>
|
||||
<Suggestions
|
||||
ref={suggestionsRef}
|
||||
document={document}
|
||||
query={query}
|
||||
pendingIds={pendingIds}
|
||||
addPendingId={handleAddPendingId}
|
||||
removePendingId={handleRemovePendingId}
|
||||
onEscape={handleEscape}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div style={{ display: picker ? "none" : "block" }}>
|
||||
|
||||
@@ -15,7 +15,9 @@ export const ListItem = styled(BaseListItem).attrs({
|
||||
padding: 6px 16px;
|
||||
border-radius: 8px;
|
||||
|
||||
&: ${hover} ${InviteIcon} {
|
||||
&: ${hover} ${InviteIcon},
|
||||
&:focus ${InviteIcon},
|
||||
&:focus-within ${InviteIcon} {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AnimatePresence } from "framer-motion";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { mergeRefs } from "react-merge-refs";
|
||||
import Flex from "~/components/Flex";
|
||||
import useMobile from "~/hooks/useMobile";
|
||||
import Input, { NativeInput } from "../../Input";
|
||||
@@ -10,13 +11,18 @@ type Props = {
|
||||
query: string;
|
||||
onChange: React.ChangeEventHandler;
|
||||
onClick: React.MouseEventHandler;
|
||||
onKeyDown: React.KeyboardEventHandler;
|
||||
back: React.ReactNode;
|
||||
action: React.ReactNode;
|
||||
};
|
||||
|
||||
export function SearchInput({ onChange, onClick, query, back, action }: Props) {
|
||||
export const SearchInput = React.forwardRef(function _SearchInput(
|
||||
{ onChange, onClick, onKeyDown, query, back, action }: Props,
|
||||
ref: React.Ref<HTMLInputElement>
|
||||
) {
|
||||
const { t } = useTranslation();
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
const isMobile = useMobile();
|
||||
|
||||
const focusInput = React.useCallback(
|
||||
@@ -39,6 +45,7 @@ export function SearchInput({ onChange, onClick, query, back, action }: Props) {
|
||||
value={query}
|
||||
onChange={onChange}
|
||||
onClick={onClick}
|
||||
onKeyDown={onKeyDown}
|
||||
autoFocus
|
||||
margin={0}
|
||||
flex
|
||||
@@ -52,15 +59,16 @@ export function SearchInput({ onChange, onClick, query, back, action }: Props) {
|
||||
{back}
|
||||
<NativeInput
|
||||
key="input"
|
||||
ref={inputRef}
|
||||
ref={mergeRefs([inputRef, ref])}
|
||||
placeholder={`${t("Add or invite")}…`}
|
||||
value={query}
|
||||
onChange={onChange}
|
||||
onClick={onClick}
|
||||
onKeyDown={onKeyDown}
|
||||
style={{ padding: "6px 0" }}
|
||||
/>
|
||||
{action}
|
||||
</AnimatePresence>
|
||||
</HeaderInput>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { isEmail } from "class-validator";
|
||||
import concat from "lodash/concat";
|
||||
import { observer } from "mobx-react";
|
||||
import { CheckmarkIcon, CloseIcon, GroupIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
@@ -11,6 +12,7 @@ import Collection from "~/models/Collection";
|
||||
import Document from "~/models/Document";
|
||||
import Group from "~/models/Group";
|
||||
import User from "~/models/User";
|
||||
import ArrowKeyNavigation from "~/components/ArrowKeyNavigation";
|
||||
import Avatar from "~/components/Avatar";
|
||||
import { AvatarSize, IAvatar } from "~/components/Avatar/Avatar";
|
||||
import Empty from "~/components/Empty";
|
||||
@@ -40,18 +42,24 @@ type Props = {
|
||||
removePendingId: (id: string) => void;
|
||||
/** Show group suggestions. */
|
||||
showGroups?: boolean;
|
||||
/** Handles escape from suggestions list */
|
||||
onEscape?: (ev: React.KeyboardEvent<HTMLDivElement>) => void;
|
||||
};
|
||||
|
||||
export const Suggestions = observer(
|
||||
({
|
||||
document,
|
||||
collection,
|
||||
query,
|
||||
pendingIds,
|
||||
addPendingId,
|
||||
removePendingId,
|
||||
showGroups,
|
||||
}: Props) => {
|
||||
React.forwardRef(function _Suggestions(
|
||||
{
|
||||
document,
|
||||
collection,
|
||||
query,
|
||||
pendingIds,
|
||||
addPendingId,
|
||||
removePendingId,
|
||||
showGroups,
|
||||
onEscape,
|
||||
}: Props,
|
||||
ref: React.Ref<HTMLDivElement>
|
||||
) {
|
||||
const neverRenderedList = React.useRef(false);
|
||||
const { users, groups } = useStores();
|
||||
const { t } = useTranslation();
|
||||
@@ -174,34 +182,57 @@ export const Suggestions = observer(
|
||||
neverRenderedList.current = false;
|
||||
|
||||
return (
|
||||
<>
|
||||
{pending.map((suggestion) => (
|
||||
<PendingListItem
|
||||
{...getListItemProps(suggestion)}
|
||||
key={suggestion.id}
|
||||
onClick={() => removePendingId(suggestion.id)}
|
||||
actions={
|
||||
<>
|
||||
<InvitedIcon />
|
||||
<RemoveIcon />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{pending.length > 0 &&
|
||||
(suggestionsWithPending.length > 0 || isEmpty) && <Separator />}
|
||||
{suggestionsWithPending.map((suggestion) => (
|
||||
<ListItem
|
||||
{...getListItemProps(suggestion as User)}
|
||||
key={suggestion.id}
|
||||
onClick={() => addPendingId(suggestion.id)}
|
||||
actions={<InviteIcon />}
|
||||
/>
|
||||
))}
|
||||
{isEmpty && <Empty style={{ marginTop: 22 }}>{t("No matches")}</Empty>}
|
||||
</>
|
||||
<ArrowKeyNavigation
|
||||
ref={ref}
|
||||
onEscape={onEscape}
|
||||
aria-label={t("Suggestions for invitation")}
|
||||
items={concat(pending, suggestionsWithPending)}
|
||||
>
|
||||
{() => [
|
||||
...pending.map((suggestion) => (
|
||||
<PendingListItem
|
||||
keyboardNavigation
|
||||
{...getListItemProps(suggestion)}
|
||||
key={suggestion.id}
|
||||
onClick={() => removePendingId(suggestion.id)}
|
||||
onKeyDown={(ev) => {
|
||||
if (ev.key === "Enter") {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
removePendingId(suggestion.id);
|
||||
}
|
||||
}}
|
||||
actions={
|
||||
<>
|
||||
<InvitedIcon />
|
||||
<RemoveIcon />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)),
|
||||
pending.length > 0 &&
|
||||
(suggestionsWithPending.length > 0 || isEmpty) && <Separator />,
|
||||
...suggestionsWithPending.map((suggestion) => (
|
||||
<ListItem
|
||||
keyboardNavigation
|
||||
{...getListItemProps(suggestion as User)}
|
||||
key={suggestion.id}
|
||||
onClick={() => addPendingId(suggestion.id)}
|
||||
onKeyDown={(ev) => {
|
||||
if (ev.key === "Enter") {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
addPendingId(suggestion.id);
|
||||
}
|
||||
}}
|
||||
actions={<InviteIcon />}
|
||||
/>
|
||||
)),
|
||||
isEmpty && <Empty style={{ marginTop: 22 }}>{t("No matches")}</Empty>,
|
||||
]}
|
||||
</ArrowKeyNavigation>
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const InvitedIcon = styled(CheckmarkIcon)`
|
||||
|
||||
@@ -270,6 +270,7 @@ function Search(props: Props) {
|
||||
ref={resultListRef}
|
||||
onEscape={handleEscape}
|
||||
aria-label={t("Search Results")}
|
||||
items={data ?? []}
|
||||
>
|
||||
{() =>
|
||||
data?.length
|
||||
|
||||
@@ -34,6 +34,7 @@ function RecentSearches(
|
||||
ref={ref}
|
||||
onEscape={onEscape}
|
||||
aria-label={t("Recent searches")}
|
||||
items={searches.recent}
|
||||
>
|
||||
{() =>
|
||||
searches.recent.map((searchQuery) => (
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@getoutline/react-roving-tabindex": "^3.2.2",
|
||||
"@getoutline/react-roving-tabindex": "^3.2.4",
|
||||
"@getoutline/y-prosemirror": "^1.0.18",
|
||||
"@hocuspocus/extension-throttle": "1.1.2",
|
||||
"@hocuspocus/provider": "1.1.2",
|
||||
|
||||
@@ -280,6 +280,7 @@
|
||||
"Add or invite": "Add or invite",
|
||||
"Viewer": "Viewer",
|
||||
"Editor": "Editor",
|
||||
"Suggestions for invitation": "Suggestions for invitation",
|
||||
"No matches": "No matches",
|
||||
"{{ userName }} was removed from the document": "{{ userName }} was removed from the document",
|
||||
"Could not remove user": "Could not remove user",
|
||||
|
||||
@@ -2776,10 +2776,10 @@
|
||||
dependencies:
|
||||
prop-types "^15.8.1"
|
||||
|
||||
"@getoutline/react-roving-tabindex@^3.2.2":
|
||||
version "3.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@getoutline/react-roving-tabindex/-/react-roving-tabindex-3.2.2.tgz#a3d957b0aa298d8c601a02a575cbfb263dcd940e"
|
||||
integrity sha512-K8uk2BQpngOTrJUMvw/Ai3zOmKNkEo2wmjnkfgHrpW6eOgzTh3VCDOLdqCMNBNqFlNwJudpVRD6EuXNO77cQ0w==
|
||||
"@getoutline/react-roving-tabindex@^3.2.4":
|
||||
version "3.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@getoutline/react-roving-tabindex/-/react-roving-tabindex-3.2.4.tgz#c1c9ec97865a89d3ce3a4a665e712f91f16ea23c"
|
||||
integrity sha512-XlzvLD0dVkzVSXSKBap8cBUSt6mMzI1FfNqQ6R1JwCOcO+LO6QKMojrGuvaccP45RpLoAT5Sjy5UYyRWmF+BZg==
|
||||
dependencies:
|
||||
warning "^4.0.3"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user