Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ packages/client/src/request-mocks/http-client.env.json
cghooks.lock

WARP.md
.docs/
2,099 changes: 1,067 additions & 1,032 deletions package-lock.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import {
borderRadius,
colors,
shadows,
spacings,
} from '@ff-client/styles/variables';
import styled from 'styled-components';

export const ActionMenuWrapper = styled.div`
position: relative;
`;

export const ActionMenuButton = styled.button`
cursor: pointer;

display: flex;
justify-content: center;
align-items: center;

width: var(--ui-control-height);
height: var(--ui-control-height);
padding: 0;

border: 1px solid ${colors.gray250};
border-radius: ${borderRadius.md};
background: ${colors.white};
color: ${colors.gray700};

svg {
width: 18px;
height: 18px;
stroke: ${colors.gray500};
}

&:hover,
&.open {
background: rgba(96, 125, 159, 0.3);
}
`;

export const ActionMenuDropdown = styled.div`
position: absolute;
right: 0;
top: 100%;
z-index: 100;

min-width: 120px;

background: ${colors.white};
box-shadow: ${shadows.boxSubtle};

border: 1px solid ${colors.gray200};
border-radius: ${borderRadius.md};
`;

export const ActionMenuItem = styled.button<{ $destructive?: boolean }>`
cursor: pointer;

display: flex;
align-items: center;
gap: ${spacings.sm};

width: 100%;
padding: ${spacings.sm} ${spacings.md};

background: transparent;
color: ${({ $destructive }) =>
$destructive ? colors.red600 : colors.gray700};

border: 0;
border-top: 1px solid ${colors.gray200};

font-size: 12px;
text-align: left;

&:first-child {
border-top: 0;
}

&:hover {
background: ${colors.gray050};
}

svg {
width: 16px;
height: 16px;
}
`;
79 changes: 79 additions & 0 deletions packages/client/src/app/components/action-menu/action-menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import type { FC } from 'react';
import React, { useState } from 'react';
import { useClickOutside } from '@ff-client/hooks/use-click-outside';
import { useOnKeypress } from '@ff-client/hooks/use-on-keypress';
import classes from '@ff-client/utils/classes';
import translate from '@ff-client/utils/translations';
import EllipsisIcon from '@ff-icons/actions/ellipsis.svg';

import {
ActionMenuButton,
ActionMenuDropdown,
ActionMenuItem,
ActionMenuWrapper,
} from './action-menu.styles';
import type { ActionMenuChoice } from './action-menu.types';

type Props = {
ariaLabel?: string;
choices: ActionMenuChoice[];
};

export const ActionMenu: FC<Props> = ({
choices,
ariaLabel = translate('Actions'),
}) => {
const [open, setOpen] = useState(false);

// Close menu on Esc key press
useOnKeypress({
callback: (event) => {
if (event.key === 'Escape') {
setOpen(false);
}
},
meetsCondition: open,
type: 'keyup',
});

// Close menu on click outside
const wrapperRef = useClickOutside<HTMLDivElement>({
isEnabled: open,
callback: () => setOpen(false),
});

return (
<ActionMenuWrapper ref={wrapperRef}>
<ActionMenuButton
type="button"
className={classes(open && 'open')}
onClick={() => setOpen((prev) => !prev)}
aria-label={ariaLabel}
aria-expanded={open}
title={ariaLabel}
>
<EllipsisIcon />
</ActionMenuButton>

{open && (
<ActionMenuDropdown>
{choices.map((choice) => (
<ActionMenuItem
key={choice.label}
type="button"
className={choice.className}
$destructive={choice.destructive}
onClick={() => {
setOpen(false);
choice.onClick();
}}
>
{choice.icon}
<span>{choice.label}</span>
</ActionMenuItem>
))}
</ActionMenuDropdown>
)}
</ActionMenuWrapper>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { ReactNode } from 'react';

export type ActionMenuChoice = {
destructive?: boolean;
icon?: ReactNode;
label: string;
className?: string;
onClick: () => void;
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
LabelValueDisplay,
List,
} from './dropdown.options.styles';
import { OptionIcon } from './dropdown.styles';

type Props = DropdownProps & {
focusIndex: number;
Expand Down Expand Up @@ -94,7 +95,7 @@ export const Options: React.FC<Props> = ({
)}

<LabelContainer>
{option.icon && option.icon}
{option.icon && <OptionIcon>{option.icon}</OptionIcon>}
<div>
<span
dangerouslySetInnerHTML={{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,26 @@ export const DropdownWrapper = styled.div`
export const Icon = styled.span`
display: flex;
align-items: center;

width: 16px;
height: 16px;

svg {
width: 16px !important;
height: 16px !important;
}
`;

export const OptionIcon = styled.div`
display: flex;
align-items: center;
justify-content: center;

width: 16px;
height: 16px;

svg {
width: 16px !important;
height: 16px !important;
}
`;
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export type DropdownProps = {
showHints?: boolean;
showSelectedIcon?: boolean;
onChange?: (value: string) => void;
className?: string;
};

export const Dropdown: React.FC<DropdownProps> = ({
Expand All @@ -55,6 +56,7 @@ export const Dropdown: React.FC<DropdownProps> = ({
showHints,
showSelectedIcon,
onChange,
className,
loading = false,
}) => {
const [open, setOpen] = useState(false);
Expand Down Expand Up @@ -157,7 +159,7 @@ export const Dropdown: React.FC<DropdownProps> = ({
return (
<DropdownWrapper
ref={containerRef}
className={classes(open && 'open')}
className={classes(open && 'open', className)}
onClick={toggleOpen}
>
<CurrentValue
Expand All @@ -174,7 +176,6 @@ export const Dropdown: React.FC<DropdownProps> = ({
),
}}
/>

{loading && (
<SpinnerWrapper>
<SpinnerIcon />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ export const EditNotificationModal: React.FC<
</button>
<button className="btn submit" onClick={handleSave}>
<LoadingText
loadingText={translate('Saving')}
loadingText={translate('Saving...')}
loading={mutation.isPending}
spinner
>
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading