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 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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user