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:
Tom Moor
2022-03-12 15:46:13 -08:00
committed by GitHub
parent d8104c6cb6
commit b7097654b5
7 changed files with 109 additions and 39 deletions

View 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;

View File

@@ -1,12 +1,18 @@
import styled from "styled-components";
import ActionButton, {
Props as ActionButtonProps,
} from "~/components/ActionButton";
const Button = styled.button.attrs((props) => ({
type: "type" in props ? props.type : "button",
}))<{
type Props = ActionButtonProps & {
width?: number;
height?: 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;
height: ${(props) => props.height || props.size || 24}px;
background: none;
@@ -20,4 +26,4 @@ const Button = styled.button.attrs((props) => ({
color: inherit;
`;
export default Button;
export default StyledNudeButton;

View File

@@ -4,7 +4,7 @@ import { PlusIcon } from "outline-icons";
import * as React from "react";
import { useDrop, useDrag, DropTargetMonitor } from "react-dnd";
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 { sortNavigationNodes } from "@shared/utils/collections";
import Collection from "~/models/Collection";
@@ -14,13 +14,13 @@ import CollectionIcon from "~/components/CollectionIcon";
import Fade from "~/components/Fade";
import Modal from "~/components/Modal";
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 usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import CollectionMenu from "~/menus/CollectionMenu";
import { NavigationNode } from "~/types";
import { newDocumentPath } from "~/utils/routeHelpers";
import DocumentLink from "./DocumentLink";
import DropCursor from "./DropCursor";
import DropToImport from "./DropToImport";
@@ -220,6 +220,10 @@ function CollectionLink({
}
}, [collection.id, ui.activeCollectionId, search]);
const context = useActionContext({
activeCollectionId: collection.id,
});
return (
<>
<Relative ref={drop}>
@@ -259,18 +263,14 @@ function CollectionLink({
!isEditing &&
!isDraggingAnyCollection && (
<Fade>
{can.update && (
<Tooltip tooltip={t("New doc")} delay={500}>
<NudeButton
type={undefined}
aria-label={t("New document")}
as={Link}
to={newDocumentPath(collection.id)}
tooltip={{ tooltip: t("New doc"), delay: 500 }}
action={createDocument}
context={context}
hideOnActionDisabled
>
<PlusIcon />
</NudeButton>
</Tooltip>
)}
<CollectionMenu
collection={collection}
onOpen={handleMenuOpen}

View File

@@ -1,8 +1,9 @@
import { StarredIcon, UnstarredIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled, { useTheme } from "styled-components";
import Document from "~/models/Document";
import { starDocument, unstarDocument } from "~/actions/definitions/documents";
import useActionContext from "~/hooks/useActionContext";
import { hover } from "~/styles";
import NudeButton from "./NudeButton";
@@ -12,22 +13,10 @@ type Props = {
};
function Star({ size, document, ...rest }: Props) {
const { t } = useTranslation();
const theme = useTheme();
const handleClick = React.useCallback(
(ev: React.MouseEvent<HTMLButtonElement>) => {
ev.preventDefault();
ev.stopPropagation();
if (document.isStarred) {
document.unstar();
} else {
document.star();
}
},
[document]
);
const context = useActionContext({
activeDocumentId: document.id,
});
if (!document) {
return null;
@@ -35,9 +24,9 @@ function Star({ size, document, ...rest }: Props) {
return (
<NudeButton
onClick={handleClick}
context={context}
action={document.isStarred ? unstarDocument : starDocument}
size={size}
aria-label={document.isStarred ? t("Unstar") : t("Star")}
{...rest}
>
{document.isStarred ? (

View File

@@ -3,7 +3,7 @@ import { TFunctionResult } from "i18next";
import * as React from "react";
import styled from "styled-components";
type Props = Omit<TippyProps, "content" | "theme"> & {
export type Props = Omit<TippyProps, "content" | "theme"> & {
tooltip: React.ReactChild | React.ReactChild[] | TFunctionResult;
shortcut?: React.ReactNode;
};

View File

@@ -20,6 +20,7 @@ export default function useActionContext(
return {
isContextMenu: false,
isCommandBar: false,
isButton: false,
activeCollectionId: stores.ui.activeCollectionId,
activeDocumentId: stores.ui.activeDocumentId,
currentUserId: stores.auth.user?.id,

View File

@@ -69,6 +69,7 @@ export type MenuItem =
export type ActionContext = {
isContextMenu: boolean;
isCommandBar: boolean;
isButton: boolean;
activeCollectionId: string | undefined;
activeDocumentId: string | undefined;
currentUserId: string | undefined;