chore: Allow Button s to take action prop (#3204)
* Add ability for NudeButton to take action+context * Add example usage * Refactor to ActionButton, convert another example * Remove dupe label
This commit is contained in:
73
app/components/ActionButton.tsx
Normal file
73
app/components/ActionButton.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import Tooltip, { Props as TooltipProps } from "~/components/Tooltip";
|
||||||
|
import { Action, ActionContext } from "~/types";
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
/** Show the button in a disabled state */
|
||||||
|
disabled?: boolean;
|
||||||
|
/** Hide the button entirely if action is not applicable */
|
||||||
|
hideOnActionDisabled?: boolean;
|
||||||
|
/** Action to use on button */
|
||||||
|
action?: Action;
|
||||||
|
/** Context of action, must be provided with action */
|
||||||
|
context?: ActionContext;
|
||||||
|
/** If tooltip props are provided the button will be wrapped in a tooltip */
|
||||||
|
tooltip?: Omit<TooltipProps, "children">;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Button that can be used to trigger an action definition.
|
||||||
|
*/
|
||||||
|
const ActionButton = React.forwardRef(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
action,
|
||||||
|
context,
|
||||||
|
tooltip,
|
||||||
|
hideOnActionDisabled,
|
||||||
|
...rest
|
||||||
|
}: Props & React.HTMLAttributes<HTMLButtonElement>,
|
||||||
|
ref: React.Ref<HTMLButtonElement>
|
||||||
|
) => {
|
||||||
|
const disabled = rest.disabled;
|
||||||
|
|
||||||
|
if (!context || !action) {
|
||||||
|
return <button {...rest} ref={ref} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action?.visible && !action.visible(context) && hideOnActionDisabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const label =
|
||||||
|
typeof action.name === "function" ? action.name(context) : action.name;
|
||||||
|
|
||||||
|
const button = (
|
||||||
|
<button
|
||||||
|
{...rest}
|
||||||
|
aria-label={label}
|
||||||
|
disabled={disabled}
|
||||||
|
ref={ref}
|
||||||
|
onClick={
|
||||||
|
action?.perform && context
|
||||||
|
? (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
action.perform?.(context);
|
||||||
|
}
|
||||||
|
: rest.onClick
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{rest.children ?? label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (tooltip) {
|
||||||
|
return <Tooltip {...tooltip}>{button}</Tooltip>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default ActionButton;
|
||||||
@@ -1,12 +1,18 @@
|
|||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
import ActionButton, {
|
||||||
|
Props as ActionButtonProps,
|
||||||
|
} from "~/components/ActionButton";
|
||||||
|
|
||||||
const Button = styled.button.attrs((props) => ({
|
type Props = ActionButtonProps & {
|
||||||
type: "type" in props ? props.type : "button",
|
|
||||||
}))<{
|
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
size?: number;
|
size?: number;
|
||||||
}>`
|
type?: "button" | "submit" | "reset";
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledNudeButton = styled(ActionButton).attrs((props: Props) => ({
|
||||||
|
type: "type" in props ? props.type : "button",
|
||||||
|
}))<Props>`
|
||||||
width: ${(props) => props.width || props.size || 24}px;
|
width: ${(props) => props.width || props.size || 24}px;
|
||||||
height: ${(props) => props.height || props.size || 24}px;
|
height: ${(props) => props.height || props.size || 24}px;
|
||||||
background: none;
|
background: none;
|
||||||
@@ -20,4 +26,4 @@ const Button = styled.button.attrs((props) => ({
|
|||||||
color: inherit;
|
color: inherit;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default Button;
|
export default StyledNudeButton;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { PlusIcon } from "outline-icons";
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useDrop, useDrag, DropTargetMonitor } from "react-dnd";
|
import { useDrop, useDrag, DropTargetMonitor } from "react-dnd";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useLocation, useHistory, Link } from "react-router-dom";
|
import { useLocation, useHistory } from "react-router-dom";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { sortNavigationNodes } from "@shared/utils/collections";
|
import { sortNavigationNodes } from "@shared/utils/collections";
|
||||||
import Collection from "~/models/Collection";
|
import Collection from "~/models/Collection";
|
||||||
@@ -14,13 +14,13 @@ import CollectionIcon from "~/components/CollectionIcon";
|
|||||||
import Fade from "~/components/Fade";
|
import Fade from "~/components/Fade";
|
||||||
import Modal from "~/components/Modal";
|
import Modal from "~/components/Modal";
|
||||||
import NudeButton from "~/components/NudeButton";
|
import NudeButton from "~/components/NudeButton";
|
||||||
import Tooltip from "~/components/Tooltip";
|
import { createDocument } from "~/actions/definitions/documents";
|
||||||
|
import useActionContext from "~/hooks/useActionContext";
|
||||||
import useBoolean from "~/hooks/useBoolean";
|
import useBoolean from "~/hooks/useBoolean";
|
||||||
import usePolicy from "~/hooks/usePolicy";
|
import usePolicy from "~/hooks/usePolicy";
|
||||||
import useStores from "~/hooks/useStores";
|
import useStores from "~/hooks/useStores";
|
||||||
import CollectionMenu from "~/menus/CollectionMenu";
|
import CollectionMenu from "~/menus/CollectionMenu";
|
||||||
import { NavigationNode } from "~/types";
|
import { NavigationNode } from "~/types";
|
||||||
import { newDocumentPath } from "~/utils/routeHelpers";
|
|
||||||
import DocumentLink from "./DocumentLink";
|
import DocumentLink from "./DocumentLink";
|
||||||
import DropCursor from "./DropCursor";
|
import DropCursor from "./DropCursor";
|
||||||
import DropToImport from "./DropToImport";
|
import DropToImport from "./DropToImport";
|
||||||
@@ -220,6 +220,10 @@ function CollectionLink({
|
|||||||
}
|
}
|
||||||
}, [collection.id, ui.activeCollectionId, search]);
|
}, [collection.id, ui.activeCollectionId, search]);
|
||||||
|
|
||||||
|
const context = useActionContext({
|
||||||
|
activeCollectionId: collection.id,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Relative ref={drop}>
|
<Relative ref={drop}>
|
||||||
@@ -259,18 +263,14 @@ function CollectionLink({
|
|||||||
!isEditing &&
|
!isEditing &&
|
||||||
!isDraggingAnyCollection && (
|
!isDraggingAnyCollection && (
|
||||||
<Fade>
|
<Fade>
|
||||||
{can.update && (
|
<NudeButton
|
||||||
<Tooltip tooltip={t("New doc")} delay={500}>
|
tooltip={{ tooltip: t("New doc"), delay: 500 }}
|
||||||
<NudeButton
|
action={createDocument}
|
||||||
type={undefined}
|
context={context}
|
||||||
aria-label={t("New document")}
|
hideOnActionDisabled
|
||||||
as={Link}
|
>
|
||||||
to={newDocumentPath(collection.id)}
|
<PlusIcon />
|
||||||
>
|
</NudeButton>
|
||||||
<PlusIcon />
|
|
||||||
</NudeButton>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
<CollectionMenu
|
<CollectionMenu
|
||||||
collection={collection}
|
collection={collection}
|
||||||
onOpen={handleMenuOpen}
|
onOpen={handleMenuOpen}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { StarredIcon, UnstarredIcon } from "outline-icons";
|
import { StarredIcon, UnstarredIcon } from "outline-icons";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import styled, { useTheme } from "styled-components";
|
import styled, { useTheme } from "styled-components";
|
||||||
import Document from "~/models/Document";
|
import Document from "~/models/Document";
|
||||||
|
import { starDocument, unstarDocument } from "~/actions/definitions/documents";
|
||||||
|
import useActionContext from "~/hooks/useActionContext";
|
||||||
import { hover } from "~/styles";
|
import { hover } from "~/styles";
|
||||||
import NudeButton from "./NudeButton";
|
import NudeButton from "./NudeButton";
|
||||||
|
|
||||||
@@ -12,22 +13,10 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function Star({ size, document, ...rest }: Props) {
|
function Star({ size, document, ...rest }: Props) {
|
||||||
const { t } = useTranslation();
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const context = useActionContext({
|
||||||
const handleClick = React.useCallback(
|
activeDocumentId: document.id,
|
||||||
(ev: React.MouseEvent<HTMLButtonElement>) => {
|
});
|
||||||
ev.preventDefault();
|
|
||||||
ev.stopPropagation();
|
|
||||||
|
|
||||||
if (document.isStarred) {
|
|
||||||
document.unstar();
|
|
||||||
} else {
|
|
||||||
document.star();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[document]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!document) {
|
if (!document) {
|
||||||
return null;
|
return null;
|
||||||
@@ -35,9 +24,9 @@ function Star({ size, document, ...rest }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<NudeButton
|
<NudeButton
|
||||||
onClick={handleClick}
|
context={context}
|
||||||
|
action={document.isStarred ? unstarDocument : starDocument}
|
||||||
size={size}
|
size={size}
|
||||||
aria-label={document.isStarred ? t("Unstar") : t("Star")}
|
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{document.isStarred ? (
|
{document.isStarred ? (
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { TFunctionResult } from "i18next";
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
type Props = Omit<TippyProps, "content" | "theme"> & {
|
export type Props = Omit<TippyProps, "content" | "theme"> & {
|
||||||
tooltip: React.ReactChild | React.ReactChild[] | TFunctionResult;
|
tooltip: React.ReactChild | React.ReactChild[] | TFunctionResult;
|
||||||
shortcut?: React.ReactNode;
|
shortcut?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export default function useActionContext(
|
|||||||
return {
|
return {
|
||||||
isContextMenu: false,
|
isContextMenu: false,
|
||||||
isCommandBar: false,
|
isCommandBar: false,
|
||||||
|
isButton: false,
|
||||||
activeCollectionId: stores.ui.activeCollectionId,
|
activeCollectionId: stores.ui.activeCollectionId,
|
||||||
activeDocumentId: stores.ui.activeDocumentId,
|
activeDocumentId: stores.ui.activeDocumentId,
|
||||||
currentUserId: stores.auth.user?.id,
|
currentUserId: stores.auth.user?.id,
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ export type MenuItem =
|
|||||||
export type ActionContext = {
|
export type ActionContext = {
|
||||||
isContextMenu: boolean;
|
isContextMenu: boolean;
|
||||||
isCommandBar: boolean;
|
isCommandBar: boolean;
|
||||||
|
isButton: boolean;
|
||||||
activeCollectionId: string | undefined;
|
activeCollectionId: string | undefined;
|
||||||
activeDocumentId: string | undefined;
|
activeDocumentId: string | undefined;
|
||||||
currentUserId: string | undefined;
|
currentUserId: string | undefined;
|
||||||
|
|||||||
Reference in New Issue
Block a user