Chore(front): Create Storybook tests for the DropdownMenu component (#2157)
* Chore(front): Create Storybook tests for the DropdownMenu component Co-authored-by: Benjamin Mayanja V <vibenjamin6@gmail.com> Co-authored-by: FellipeMTX <fellipefacdir@gmail.com> * Fix the tests Co-authored-by: Benjamin Mayanja V <vibenjamin6@gmail.com> Co-authored-by: FellipeMTX <fellipefacdir@gmail.com> * Simplify Dropdown * Remove console.log --------- Co-authored-by: Benjamin Mayanja V <vibenjamin6@gmail.com> Co-authored-by: FellipeMTX <fellipefacdir@gmail.com> Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -4,7 +4,7 @@ import { useRecoilState } from 'recoil';
|
||||
|
||||
import { IconTrash } from '@/ui/display/icon';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { StyledDropdownMenuSeparator } from '@/ui/layout/dropdown/components/StyledDropdownMenuSeparator';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||
import { MenuItemSelectColor } from '@/ui/navigation/menu-item/components/MenuItemSelectColor';
|
||||
import { ThemeColor } from '@/ui/theme/constants/colors';
|
||||
@ -123,7 +123,7 @@ export const BoardColumnEditTitleMenu = ({
|
||||
autoFocus
|
||||
/>
|
||||
</StyledEditTitleContainer>
|
||||
<StyledDropdownMenuSeparator />
|
||||
<DropdownMenuSeparator />
|
||||
{COLUMN_COLOR_OPTIONS.map((colorOption) => (
|
||||
<MenuItemSelectColor
|
||||
key={colorOption.name}
|
||||
@ -135,7 +135,7 @@ export const BoardColumnEditTitleMenu = ({
|
||||
text={colorOption.name}
|
||||
/>
|
||||
))}
|
||||
<StyledDropdownMenuSeparator />
|
||||
<DropdownMenuSeparator />
|
||||
<MenuItem
|
||||
onClick={handleDelete}
|
||||
LeftIcon={IconTrash}
|
||||
|
||||
@ -15,8 +15,8 @@ import { SingleEntitySelect } from '@/ui/input/relation-picker/components/Single
|
||||
import { relationPickerSearchFilterScopedState } from '@/ui/input/relation-picker/states/relationPickerSearchFilterScopedState';
|
||||
import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect';
|
||||
import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope';
|
||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { StyledDropdownMenu } from '@/ui/layout/dropdown/components/StyledDropdownMenu';
|
||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
@ -138,7 +138,7 @@ export const BoardColumnMenu = ({
|
||||
|
||||
return (
|
||||
<StyledMenuContainer ref={boardColumnMenuRef}>
|
||||
<StyledDropdownMenu data-select-disable>
|
||||
<DropdownMenu data-select-disable>
|
||||
{currentMenu === 'actions' && (
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
@ -183,7 +183,7 @@ export const BoardColumnMenu = ({
|
||||
selectedEntity={companies.selectedEntities[0]}
|
||||
/>
|
||||
)}
|
||||
</StyledDropdownMenu>
|
||||
</DropdownMenu>
|
||||
</StyledMenuContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { useResetRecoilState } from 'recoil';
|
||||
|
||||
import { ViewBarDropdownButton } from '@/ui/data/view-bar/components/ViewBarDropdownButton';
|
||||
import { viewEditModeState } from '@/ui/data/view-bar/states/viewEditModeState';
|
||||
|
||||
import { Dropdown } from '../../dropdown/components/Dropdown';
|
||||
import { DropdownScope } from '../../dropdown/scopes/DropdownScope';
|
||||
import { BoardScopeIds } from '../types/enums/BoardScopeIds';
|
||||
|
||||
import { BoardOptionsDropdownButton } from './BoardOptionsDropdownButton';
|
||||
@ -23,17 +24,18 @@ export const BoardOptionsDropdown = ({
|
||||
const resetViewEditMode = useResetRecoilState(viewEditModeState);
|
||||
|
||||
return (
|
||||
<ViewBarDropdownButton
|
||||
buttonComponent={<BoardOptionsDropdownButton />}
|
||||
dropdownComponents={
|
||||
<BoardOptionsDropdownContent
|
||||
customHotkeyScope={customHotkeyScope}
|
||||
onStageAdd={onStageAdd}
|
||||
/>
|
||||
}
|
||||
dropdownHotkeyScope={customHotkeyScope}
|
||||
dropdownId={BoardScopeIds.OptionsDropdown}
|
||||
onClickOutside={resetViewEditMode}
|
||||
/>
|
||||
<DropdownScope dropdownScopeId={BoardScopeIds.OptionsDropdown}>
|
||||
<Dropdown
|
||||
clickableComponent={<BoardOptionsDropdownButton />}
|
||||
dropdownComponents={
|
||||
<BoardOptionsDropdownContent
|
||||
customHotkeyScope={customHotkeyScope}
|
||||
onStageAdd={onStageAdd}
|
||||
/>
|
||||
}
|
||||
dropdownHotkeyScope={customHotkeyScope}
|
||||
onClickOutside={resetViewEditMode}
|
||||
/>
|
||||
</DropdownScope>
|
||||
);
|
||||
};
|
||||
|
||||
@ -22,11 +22,9 @@ import {
|
||||
} from '@/ui/display/icon';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
|
||||
import { DropdownMenuInput } from '@/ui/layout/dropdown/components/DropdownMenuInput';
|
||||
import { DropdownMenuInputContainer } from '@/ui/layout/dropdown/components/DropdownMenuInputContainer';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { StyledDropdownMenu } from '@/ui/layout/dropdown/components/StyledDropdownMenu';
|
||||
import { StyledDropdownMenuSeparator } from '@/ui/layout/dropdown/components/StyledDropdownMenuSeparator';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||
import { MenuItemNavigate } from '@/ui/navigation/menu-item/components/MenuItemNavigate';
|
||||
@ -166,28 +164,24 @@ export const BoardOptionsDropdownContent = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledDropdownMenu>
|
||||
<>
|
||||
{!currentMenu && (
|
||||
<>
|
||||
<DropdownMenuInputContainer>
|
||||
<DropdownMenuInput
|
||||
ref={viewEditInputRef}
|
||||
autoFocus={
|
||||
viewEditMode.mode === 'create' || !!viewEditMode.viewId
|
||||
}
|
||||
placeholder={
|
||||
viewEditMode.mode === 'create' ? 'New view' : 'View name'
|
||||
}
|
||||
defaultValue={
|
||||
viewEditMode.mode === 'create'
|
||||
? ''
|
||||
: viewEditMode.viewId
|
||||
? viewsById[viewEditMode.viewId]?.name
|
||||
: currentView?.name
|
||||
}
|
||||
/>
|
||||
</DropdownMenuInputContainer>
|
||||
<StyledDropdownMenuSeparator />
|
||||
<DropdownMenuInput
|
||||
ref={viewEditInputRef}
|
||||
autoFocus={viewEditMode.mode === 'create' || !!viewEditMode.viewId}
|
||||
placeholder={
|
||||
viewEditMode.mode === 'create' ? 'New view' : 'View name'
|
||||
}
|
||||
defaultValue={
|
||||
viewEditMode.mode === 'create'
|
||||
? ''
|
||||
: viewEditMode.viewId
|
||||
? viewsById[viewEditMode.viewId]?.name
|
||||
: currentView?.name
|
||||
}
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItemNavigate
|
||||
onClick={() => handleMenuNavigate('fields')}
|
||||
@ -207,7 +201,7 @@ export const BoardOptionsDropdownContent = ({
|
||||
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetMenu}>
|
||||
Stages
|
||||
</DropdownMenuHeader>
|
||||
<StyledDropdownMenuSeparator />
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
onClick={() => setCurrentMenu('stage-creation')}
|
||||
@ -229,7 +223,7 @@ export const BoardOptionsDropdownContent = ({
|
||||
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetMenu}>
|
||||
Fields
|
||||
</DropdownMenuHeader>
|
||||
<StyledDropdownMenuSeparator />
|
||||
<DropdownMenuSeparator />
|
||||
{hasVisibleFields && (
|
||||
<ViewFieldsVisibilityDropdownSection
|
||||
title="Visible"
|
||||
@ -238,9 +232,7 @@ export const BoardOptionsDropdownContent = ({
|
||||
isDraggable={true}
|
||||
/>
|
||||
)}
|
||||
{hasVisibleFields && hasHiddenFields && (
|
||||
<StyledDropdownMenuSeparator />
|
||||
)}
|
||||
{hasVisibleFields && hasHiddenFields && <DropdownMenuSeparator />}
|
||||
{hasHiddenFields && (
|
||||
<ViewFieldsVisibilityDropdownSection
|
||||
title="Hidden"
|
||||
@ -251,6 +243,6 @@ export const BoardOptionsDropdownContent = ({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</StyledDropdownMenu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
117
front/src/modules/ui/layout/dropdown/components/Dropdown.tsx
Normal file
117
front/src/modules/ui/layout/dropdown/components/Dropdown.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
import { useRef } from 'react';
|
||||
import { Keys } from 'react-hotkeys-hook';
|
||||
import { flip, offset, Placement, useFloating } from '@floating-ui/react';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
import { HotkeyEffect } from '@/ui/utilities/hotkey/components/HotkeyEffect';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
|
||||
import { useDropdown } from '../hooks/useDropdown';
|
||||
import { useInternalHotkeyScopeManagement } from '../hooks/useInternalHotkeyScopeManagement';
|
||||
|
||||
import { DropdownMenu } from './DropdownMenu';
|
||||
import { DropdownToggleEffect } from './DropdownToggleEffect';
|
||||
|
||||
type DropdownProps = {
|
||||
clickableComponent?: JSX.Element | JSX.Element[];
|
||||
dropdownComponents: JSX.Element | JSX.Element[];
|
||||
hotkey?: {
|
||||
key: Keys;
|
||||
scope: string;
|
||||
};
|
||||
dropdownHotkeyScope: HotkeyScope;
|
||||
dropdownPlacement?: Placement;
|
||||
dropdownMenuWidth?: number;
|
||||
dropdownOffset?: { x?: number; y?: number };
|
||||
onClickOutside?: () => void;
|
||||
onClose?: () => void;
|
||||
onOpen?: () => void;
|
||||
};
|
||||
|
||||
export const Dropdown = ({
|
||||
clickableComponent,
|
||||
dropdownComponents,
|
||||
dropdownMenuWidth = 160,
|
||||
hotkey,
|
||||
dropdownHotkeyScope,
|
||||
dropdownPlacement = 'bottom-end',
|
||||
dropdownOffset = { x: 0, y: 0 },
|
||||
onClickOutside,
|
||||
onClose,
|
||||
onOpen,
|
||||
}: DropdownProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { isDropdownOpen, toggleDropdown, closeDropdown } = useDropdown();
|
||||
|
||||
const offsetMiddlewares = [];
|
||||
if (dropdownOffset.x) {
|
||||
offsetMiddlewares.push(offset({ crossAxis: dropdownOffset.x }));
|
||||
}
|
||||
|
||||
if (dropdownOffset.y) {
|
||||
offsetMiddlewares.push(offset({ mainAxis: dropdownOffset.y }));
|
||||
}
|
||||
|
||||
const { refs, floatingStyles } = useFloating({
|
||||
placement: dropdownPlacement,
|
||||
middleware: [flip(), ...offsetMiddlewares],
|
||||
});
|
||||
|
||||
const handleHotkeyTriggered = () => {
|
||||
toggleDropdown();
|
||||
};
|
||||
|
||||
useListenClickOutside({
|
||||
refs: [containerRef],
|
||||
callback: () => {
|
||||
onClickOutside?.();
|
||||
|
||||
if (isDropdownOpen) {
|
||||
closeDropdown();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
useInternalHotkeyScopeManagement({
|
||||
dropdownHotkeyScopeFromParent: dropdownHotkeyScope,
|
||||
});
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.Escape,
|
||||
() => {
|
||||
closeDropdown();
|
||||
},
|
||||
dropdownHotkeyScope.scope,
|
||||
[closeDropdown],
|
||||
);
|
||||
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
{clickableComponent && (
|
||||
<div ref={refs.setReference} onClick={toggleDropdown}>
|
||||
{clickableComponent}
|
||||
</div>
|
||||
)}
|
||||
{hotkey && (
|
||||
<HotkeyEffect
|
||||
hotkey={hotkey}
|
||||
onHotkeyTriggered={handleHotkeyTriggered}
|
||||
/>
|
||||
)}
|
||||
{isDropdownOpen && (
|
||||
<DropdownMenu
|
||||
width={dropdownMenuWidth}
|
||||
data-select-disable
|
||||
ref={refs.setFloating}
|
||||
style={floatingStyles}
|
||||
>
|
||||
{dropdownComponents}
|
||||
</DropdownMenu>
|
||||
)}
|
||||
<DropdownToggleEffect onDropdownClose={onClose} onDropdownOpen={onOpen} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,103 +1,25 @@
|
||||
import { useRef } from 'react';
|
||||
import { Keys } from 'react-hotkeys-hook';
|
||||
import { flip, offset, Placement, useFloating } from '@floating-ui/react';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { HotkeyEffect } from '@/ui/utilities/hotkey/components/HotkeyEffect';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
const StyledDropdownMenu = styled.div<{
|
||||
disableBlur?: boolean;
|
||||
width?: `${string}px` | 'auto' | number;
|
||||
}>`
|
||||
backdrop-filter: ${({ disableBlur }) =>
|
||||
disableBlur ? 'none' : 'blur(20px)'};
|
||||
|
||||
import { useDropdown } from '../hooks/useDropdown';
|
||||
import { useInternalHotkeyScopeManagement } from '../hooks/useInternalHotkeyScopeManagement';
|
||||
background: ${({ theme }) => theme.background.secondary};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
box-shadow: ${({ theme }) => theme.boxShadow.strong};
|
||||
|
||||
import { DropdownToggleEffect } from './DropdownToggleEffect';
|
||||
display: flex;
|
||||
|
||||
type DropdownMenuProps = {
|
||||
clickableComponent?: JSX.Element | JSX.Element[];
|
||||
dropdownComponents: JSX.Element | JSX.Element[];
|
||||
hotkey?: {
|
||||
key: Keys;
|
||||
scope: string;
|
||||
};
|
||||
dropdownHotkeyScope: HotkeyScope;
|
||||
dropdownPlacement?: Placement;
|
||||
dropdownOffset?: { x: number; y: number };
|
||||
onClickOutside?: () => void;
|
||||
onClose?: () => void;
|
||||
onOpen?: () => void;
|
||||
};
|
||||
flex-direction: column;
|
||||
|
||||
export const DropdownMenu = ({
|
||||
clickableComponent,
|
||||
dropdownComponents,
|
||||
hotkey,
|
||||
dropdownHotkeyScope,
|
||||
dropdownPlacement = 'bottom-end',
|
||||
dropdownOffset = { x: 0, y: 0 },
|
||||
onClickOutside,
|
||||
onClose,
|
||||
onOpen,
|
||||
}: DropdownMenuProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
overflow: hidden;
|
||||
|
||||
const { isDropdownOpen, toggleDropdown, closeDropdown } = useDropdown();
|
||||
width: ${({ width }) =>
|
||||
width ? `${typeof width === 'number' ? `${width}px` : width}` : '160px'};
|
||||
`;
|
||||
|
||||
const { refs, floatingStyles } = useFloating({
|
||||
placement: dropdownPlacement,
|
||||
middleware: [
|
||||
flip(),
|
||||
offset({ mainAxis: dropdownOffset.y, crossAxis: dropdownOffset.x }),
|
||||
],
|
||||
});
|
||||
|
||||
const handleHotkeyTriggered = () => {
|
||||
toggleDropdown();
|
||||
};
|
||||
|
||||
useListenClickOutside({
|
||||
refs: [containerRef],
|
||||
callback: () => {
|
||||
onClickOutside?.();
|
||||
|
||||
if (isDropdownOpen) {
|
||||
closeDropdown();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
useInternalHotkeyScopeManagement({
|
||||
dropdownHotkeyScopeFromParent: dropdownHotkeyScope,
|
||||
});
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.Escape,
|
||||
() => {
|
||||
closeDropdown();
|
||||
},
|
||||
dropdownHotkeyScope.scope,
|
||||
[closeDropdown],
|
||||
);
|
||||
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
{clickableComponent && (
|
||||
<div ref={refs.setReference} onClick={toggleDropdown}>
|
||||
{clickableComponent}
|
||||
</div>
|
||||
)}
|
||||
{hotkey && (
|
||||
<HotkeyEffect
|
||||
hotkey={hotkey}
|
||||
onHotkeyTriggered={handleHotkeyTriggered}
|
||||
/>
|
||||
)}
|
||||
{isDropdownOpen && (
|
||||
<div data-select-disable ref={refs.setFloating} style={floatingStyles}>
|
||||
{dropdownComponents}
|
||||
</div>
|
||||
)}
|
||||
<DropdownToggleEffect onDropdownClose={onClose} onDropdownOpen={onOpen} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const DropdownMenu = StyledDropdownMenu;
|
||||
|
||||
@ -1,47 +0,0 @@
|
||||
import { HTMLAttributes, useRef } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { StyledDropdownMenu } from '@/ui/layout/dropdown/components/StyledDropdownMenu';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
|
||||
const StyledDropdownMenuContainer = styled.ul<{
|
||||
anchor: 'left' | 'right';
|
||||
}>`
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
${({ anchor }) => {
|
||||
if (anchor === 'right') return 'right: 0';
|
||||
}};
|
||||
top: 14px;
|
||||
`;
|
||||
|
||||
export type DropdownMenuContainerProps = {
|
||||
anchor?: 'left' | 'right';
|
||||
children: React.ReactNode;
|
||||
onClose?: () => void;
|
||||
width?: `${string}px` | 'auto' | number;
|
||||
} & HTMLAttributes<HTMLUListElement>;
|
||||
|
||||
export const DropdownMenuContainer = ({
|
||||
anchor = 'right',
|
||||
children,
|
||||
onClose,
|
||||
width,
|
||||
}: DropdownMenuContainerProps) => {
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useListenClickOutside({
|
||||
refs: [dropdownRef],
|
||||
callback: () => {
|
||||
onClose?.();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledDropdownMenuContainer data-select-disable anchor={anchor}>
|
||||
<StyledDropdownMenu ref={dropdownRef} width={width}>
|
||||
{children}
|
||||
</StyledDropdownMenu>
|
||||
</StyledDropdownMenuContainer>
|
||||
);
|
||||
};
|
||||
@ -1,9 +1,10 @@
|
||||
import { forwardRef, InputHTMLAttributes } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { rgba } from '@/ui/theme/constants/colors';
|
||||
import { textInputStyle } from '@/ui/theme/constants/effects';
|
||||
|
||||
const StyledViewNameInput = styled.input`
|
||||
const StyledInput = styled.input`
|
||||
${textInputStyle}
|
||||
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
@ -20,4 +21,24 @@ const StyledViewNameInput = styled.input`
|
||||
}
|
||||
`;
|
||||
|
||||
export { StyledViewNameInput as DropdownMenuInput };
|
||||
const StyledInputContainer = styled.div`
|
||||
box-sizing: border-box;
|
||||
padding: ${({ theme }) => theme.spacing(1)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const DropdownMenuInput = forwardRef<
|
||||
HTMLInputElement,
|
||||
InputHTMLAttributes<HTMLInputElement>
|
||||
>(({ autoFocus, defaultValue, placeholder }, ref) => {
|
||||
return (
|
||||
<StyledInputContainer>
|
||||
<StyledInput
|
||||
autoFocus={autoFocus}
|
||||
defaultValue={defaultValue}
|
||||
placeholder={placeholder}
|
||||
ref={ref}
|
||||
/>
|
||||
</StyledInputContainer>
|
||||
);
|
||||
});
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledInputContainer = styled.div`
|
||||
box-sizing: border-box;
|
||||
padding: ${({ theme }) => theme.spacing(1)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export { StyledInputContainer as DropdownMenuInputContainer };
|
||||
@ -1,8 +1,10 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
export const StyledDropdownMenuSeparator = styled.div`
|
||||
const StyledDropdownMenuSeparator = styled.div`
|
||||
background-color: ${({ theme }) => theme.border.color.light};
|
||||
height: 1px;
|
||||
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const DropdownMenuSeparator = StyledDropdownMenuSeparator;
|
||||
@ -1,23 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
export const StyledDropdownMenu = styled.div<{
|
||||
disableBlur?: boolean;
|
||||
width?: `${string}px` | 'auto' | number;
|
||||
}>`
|
||||
backdrop-filter: ${({ disableBlur }) =>
|
||||
disableBlur ? 'none' : 'blur(20px)'};
|
||||
|
||||
background: ${({ theme }) => theme.background.secondary};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
box-shadow: ${({ theme }) => theme.boxShadow.strong};
|
||||
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
width: ${({ width }) =>
|
||||
width ? `${typeof width === 'number' ? `${width}px` : width}` : '160px'};
|
||||
`;
|
||||
@ -1,7 +1,8 @@
|
||||
import { useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { expect } from '@storybook/jest';
|
||||
import { Decorator, Meta, StoryObj } from '@storybook/react';
|
||||
import { userEvent, within } from '@storybook/testing-library';
|
||||
import { userEvent, waitFor, within } from '@storybook/testing-library';
|
||||
import { PlayFunction } from '@storybook/types';
|
||||
|
||||
import { Button } from '@/ui/input/button/components/Button';
|
||||
@ -13,19 +14,17 @@ import { Avatar } from '@/users/components/Avatar';
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
|
||||
import { DropdownScope } from '../../scopes/DropdownScope';
|
||||
import { DropdownMenu } from '../DropdownMenu';
|
||||
import { Dropdown } from '../Dropdown';
|
||||
import { DropdownMenuHeader } from '../DropdownMenuHeader';
|
||||
import { DropdownMenuInput } from '../DropdownMenuInput';
|
||||
import { DropdownMenuInputContainer } from '../DropdownMenuInputContainer';
|
||||
import { DropdownMenuItemsContainer } from '../DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearchInput } from '../DropdownMenuSearchInput';
|
||||
import { StyledDropdownMenu } from '../StyledDropdownMenu';
|
||||
import { StyledDropdownMenuSeparator } from '../StyledDropdownMenuSeparator';
|
||||
import { DropdownMenuSeparator } from '../DropdownMenuSeparator';
|
||||
import { StyledDropdownMenuSubheader } from '../StyledDropdownMenuSubheader';
|
||||
|
||||
const meta: Meta<typeof DropdownMenu> = {
|
||||
title: 'UI/Layout/Dropdown/DropdownMenu',
|
||||
component: DropdownMenu,
|
||||
const meta: Meta<typeof Dropdown> = {
|
||||
title: 'UI/Layout/Dropdown/Dropdown',
|
||||
component: Dropdown,
|
||||
|
||||
decorators: [
|
||||
ComponentDecorator,
|
||||
@ -38,7 +37,7 @@ const meta: Meta<typeof DropdownMenu> = {
|
||||
args: {
|
||||
clickableComponent: <Button title="Open Dropdown" />,
|
||||
dropdownHotkeyScope: { scope: 'testDropdownMenu' },
|
||||
dropdownOffset: { x: 0, y: -8 },
|
||||
dropdownOffset: { x: 0, y: 8 },
|
||||
},
|
||||
argTypes: {
|
||||
clickableComponent: { control: false },
|
||||
@ -49,26 +48,9 @@ const meta: Meta<typeof DropdownMenu> = {
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof DropdownMenu>;
|
||||
type Story = StoryObj<typeof Dropdown>;
|
||||
|
||||
const FakeContentBelow = () => (
|
||||
<div style={{ position: 'absolute' }}>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
|
||||
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
|
||||
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
|
||||
consequat.
|
||||
</div>
|
||||
);
|
||||
|
||||
const avatarUrl =
|
||||
'https://s3-alpha-sig.figma.com/img/bbb5/4905/f0a52cc2b9aaeb0a82a360d478dae8bf?Expires=1687132800&Signature=iVBr0BADa3LHoFVGbwqO-wxC51n1o~ZyFD-w7nyTyFP4yB-Y6zFawL-igewaFf6PrlumCyMJThDLAAc-s-Cu35SBL8BjzLQ6HymzCXbrblUADMB208PnMAvc1EEUDq8TyryFjRO~GggLBk5yR0EXzZ3zenqnDEGEoQZR~TRqS~uDF-GwQB3eX~VdnuiU2iittWJkajIDmZtpN3yWtl4H630A3opQvBnVHZjXAL5YPkdh87-a-H~6FusWvvfJxfNC2ZzbrARzXofo8dUFtH7zUXGCC~eUk~hIuLbLuz024lFQOjiWq2VKyB7dQQuGFpM-OZQEV8tSfkViP8uzDLTaCg__&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4';
|
||||
|
||||
const StyledFakeMenuContent = styled.div`
|
||||
height: 400px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledFakeBelowContainer = styled.div`
|
||||
const StyledContainer = styled.div`
|
||||
height: 600px;
|
||||
position: relative;
|
||||
|
||||
@ -77,12 +59,62 @@ const StyledFakeBelowContainer = styled.div`
|
||||
|
||||
const StyledMenuAbsolutePositionWrapper = styled.div`
|
||||
height: fit-content;
|
||||
position: absolute;
|
||||
|
||||
width: fit-content;
|
||||
`;
|
||||
|
||||
const mockSelectArray = [
|
||||
const WithContentBelowDecorator: Decorator = (Story) => (
|
||||
<StyledContainer>
|
||||
<StyledMenuAbsolutePositionWrapper>
|
||||
<Story />
|
||||
</StyledMenuAbsolutePositionWrapper>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
|
||||
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
|
||||
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
|
||||
consequat.
|
||||
</StyledContainer>
|
||||
);
|
||||
|
||||
const StyledEmptyDropdownContent = styled.div`
|
||||
height: 400px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const Empty: Story = {
|
||||
args: {
|
||||
dropdownComponents: (
|
||||
<StyledEmptyDropdownContent data-testid="dropdown-content" />
|
||||
),
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const button = await canvas.findByRole('button');
|
||||
userEvent.click(button);
|
||||
|
||||
await waitFor(async () => {
|
||||
const fakeMenu = await canvas.findByTestId('dropdown-content');
|
||||
expect(fakeMenu).toBeInTheDocument();
|
||||
});
|
||||
|
||||
userEvent.click(button);
|
||||
|
||||
await waitFor(async () => {
|
||||
const fakeMenu = await canvas.findByTestId('dropdown-content');
|
||||
expect(fakeMenu).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
userEvent.click(button);
|
||||
await waitFor(async () => {
|
||||
const fakeMenu = await canvas.findByTestId('dropdown-content');
|
||||
expect(fakeMenu).toBeInTheDocument();
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const avatarUrl =
|
||||
'https://s3-alpha-sig.figma.com/img/bbb5/4905/f0a52cc2b9aaeb0a82a360d478dae8bf?Expires=1687132800&Signature=iVBr0BADa3LHoFVGbwqO-wxC51n1o~ZyFD-w7nyTyFP4yB-Y6zFawL-igewaFf6PrlumCyMJThDLAAc-s-Cu35SBL8BjzLQ6HymzCXbrblUADMB208PnMAvc1EEUDq8TyryFjRO~GggLBk5yR0EXzZ3zenqnDEGEoQZR~TRqS~uDF-GwQB3eX~VdnuiU2iittWJkajIDmZtpN3yWtl4H630A3opQvBnVHZjXAL5YPkdh87-a-H~6FusWvvfJxfNC2ZzbrARzXofo8dUFtH7zUXGCC~eUk~hIuLbLuz024lFQOjiWq2VKyB7dQQuGFpM-OZQEV8tSfkViP8uzDLTaCg__&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4';
|
||||
|
||||
const optionsMock = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Company A',
|
||||
@ -120,7 +152,7 @@ const FakeSelectableMenuItemList = ({ hasAvatar }: { hasAvatar?: boolean }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{mockSelectArray.map((item) => (
|
||||
{optionsMock.map((item) => (
|
||||
<MenuItemSelectAvatar
|
||||
key={item.id}
|
||||
selected={selectedItem === item.id}
|
||||
@ -149,7 +181,7 @@ const FakeCheckableMenuItemList = ({ hasAvatar }: { hasAvatar?: boolean }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{mockSelectArray.map((item) => (
|
||||
{optionsMock.map((item) => (
|
||||
<MenuItemMultiSelectAvatar
|
||||
key={item.id}
|
||||
selected={selectedItemsById[item.id]}
|
||||
@ -176,55 +208,40 @@ const FakeCheckableMenuItemList = ({ hasAvatar }: { hasAvatar?: boolean }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const WithContentBelowDecorator: Decorator = (Story) => (
|
||||
<StyledFakeBelowContainer>
|
||||
<FakeContentBelow />
|
||||
<StyledMenuAbsolutePositionWrapper>
|
||||
<Story />
|
||||
</StyledMenuAbsolutePositionWrapper>
|
||||
</StyledFakeBelowContainer>
|
||||
);
|
||||
|
||||
const playInteraction: PlayFunction<any, any> = async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const button = await canvas.findByRole('button');
|
||||
|
||||
userEvent.click(button);
|
||||
};
|
||||
|
||||
export const Empty: Story = {
|
||||
args: {
|
||||
dropdownComponents: (
|
||||
<StyledDropdownMenu>
|
||||
<StyledFakeMenuContent />
|
||||
</StyledDropdownMenu>
|
||||
),
|
||||
},
|
||||
play: playInteraction,
|
||||
await waitFor(async () => {
|
||||
expect(canvas.getByText('Company A')).toBeInTheDocument();
|
||||
});
|
||||
};
|
||||
|
||||
export const WithHeaders: Story = {
|
||||
decorators: [WithContentBelowDecorator],
|
||||
args: {
|
||||
dropdownComponents: (
|
||||
<StyledDropdownMenu>
|
||||
<>
|
||||
<DropdownMenuHeader>Header</DropdownMenuHeader>
|
||||
<StyledDropdownMenuSeparator />
|
||||
<DropdownMenuSeparator />
|
||||
<StyledDropdownMenuSubheader>Subheader 1</StyledDropdownMenuSubheader>
|
||||
<DropdownMenuItemsContainer>
|
||||
{mockSelectArray.slice(0, 3).map(({ name }) => (
|
||||
<MenuItem text={name} />
|
||||
))}
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
<>
|
||||
{optionsMock.slice(0, 3).map(({ name }) => (
|
||||
<MenuItem text={name} />
|
||||
))}
|
||||
</>
|
||||
</DropdownMenuItemsContainer>
|
||||
<StyledDropdownMenuSeparator />
|
||||
<DropdownMenuSeparator />
|
||||
<StyledDropdownMenuSubheader>Subheader 2</StyledDropdownMenuSubheader>
|
||||
<DropdownMenuItemsContainer>
|
||||
{mockSelectArray.slice(3).map(({ name }) => (
|
||||
{optionsMock.slice(3).map(({ name }) => (
|
||||
<MenuItem text={name} />
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</StyledDropdownMenu>
|
||||
</>
|
||||
),
|
||||
},
|
||||
play: playInteraction,
|
||||
@ -234,33 +251,45 @@ export const SearchWithLoadingMenu: Story = {
|
||||
decorators: [WithContentBelowDecorator],
|
||||
args: {
|
||||
dropdownComponents: (
|
||||
<StyledDropdownMenu>
|
||||
<>
|
||||
<DropdownMenuSearchInput value="query" autoFocus />
|
||||
<StyledDropdownMenuSeparator />
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
<DropdownMenuSkeletonItem />
|
||||
</DropdownMenuItemsContainer>
|
||||
</StyledDropdownMenu>
|
||||
</>
|
||||
),
|
||||
},
|
||||
play: playInteraction,
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const button = await canvas.findByRole('button');
|
||||
|
||||
await waitFor(() => {
|
||||
userEvent.click(button);
|
||||
expect(canvas.getByDisplayValue('query')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
userEvent.click(button);
|
||||
expect(canvas.queryByDisplayValue('query')).not.toBeInTheDocument();
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const WithInput: Story = {
|
||||
decorators: [WithContentBelowDecorator],
|
||||
args: {
|
||||
dropdownComponents: (
|
||||
<StyledDropdownMenu>
|
||||
<DropdownMenuInputContainer>
|
||||
<DropdownMenuInput defaultValue="Lorem ipsum" autoFocus />
|
||||
</DropdownMenuInputContainer>
|
||||
<StyledDropdownMenuSeparator />
|
||||
<>
|
||||
<DropdownMenuInput defaultValue="Lorem ipsum" autoFocus />
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{mockSelectArray.map(({ name }) => (
|
||||
{optionsMock.map(({ name }) => (
|
||||
<MenuItem text={name} />
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</StyledDropdownMenu>
|
||||
</>
|
||||
),
|
||||
},
|
||||
play: playInteraction,
|
||||
@ -270,11 +299,9 @@ export const SelectableMenuItemWithAvatar: Story = {
|
||||
decorators: [WithContentBelowDecorator],
|
||||
args: {
|
||||
dropdownComponents: (
|
||||
<StyledDropdownMenu>
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
<FakeSelectableMenuItemList hasAvatar />
|
||||
</DropdownMenuItemsContainer>
|
||||
</StyledDropdownMenu>
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
<FakeSelectableMenuItemList hasAvatar />
|
||||
</DropdownMenuItemsContainer>
|
||||
),
|
||||
},
|
||||
play: playInteraction,
|
||||
@ -284,11 +311,9 @@ export const CheckableMenuItemWithAvatar: Story = {
|
||||
decorators: [WithContentBelowDecorator],
|
||||
args: {
|
||||
dropdownComponents: (
|
||||
<StyledDropdownMenu>
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
<FakeCheckableMenuItemList hasAvatar />
|
||||
</DropdownMenuItemsContainer>
|
||||
</StyledDropdownMenu>
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
<FakeCheckableMenuItemList hasAvatar />
|
||||
</DropdownMenuItemsContainer>
|
||||
),
|
||||
},
|
||||
play: playInteraction,
|
||||
|
||||
@ -9,10 +9,6 @@ const meta: Meta<typeof DropdownMenuInput> = {
|
||||
component: DropdownMenuInput,
|
||||
decorators: [ComponentDecorator],
|
||||
args: { defaultValue: 'Lorem ipsum' },
|
||||
argTypes: {
|
||||
as: { table: { disable: true } },
|
||||
theme: { table: { disable: true } },
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
@ -3,15 +3,17 @@ import styled from '@emotion/styled';
|
||||
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
|
||||
import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
|
||||
import { ViewBarDropdownButton } from '@/ui/data/view-bar/components/ViewBarDropdownButton';
|
||||
import { IconCheckbox, IconNotes, IconPlus } from '@/ui/display/icon/index';
|
||||
import { IconButton } from '@/ui/input/button/components/IconButton';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { StyledDropdownMenu } from '@/ui/layout/dropdown/components/StyledDropdownMenu';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||
import { ActivityType } from '~/generated/graphql';
|
||||
|
||||
import { Dropdown } from '../../dropdown/components/Dropdown';
|
||||
import { DropdownMenu } from '../../dropdown/components/DropdownMenu';
|
||||
import { DropdownScope } from '../../dropdown/scopes/DropdownScope';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
z-index: 1;
|
||||
`;
|
||||
@ -33,40 +35,41 @@ export const ShowPageAddButton = ({
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<ViewBarDropdownButton
|
||||
dropdownId="add-show-page"
|
||||
buttonComponent={
|
||||
<IconButton
|
||||
Icon={IconPlus}
|
||||
size="medium"
|
||||
dataTestId="add-showpage-button"
|
||||
accent="default"
|
||||
variant="secondary"
|
||||
onClick={toggleDropdown}
|
||||
/>
|
||||
}
|
||||
dropdownComponents={
|
||||
<StyledDropdownMenu>
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
onClick={() => handleSelect(ActivityType.Note)}
|
||||
accent="default"
|
||||
LeftIcon={IconNotes}
|
||||
text="Note"
|
||||
/>
|
||||
<MenuItem
|
||||
onClick={() => handleSelect(ActivityType.Task)}
|
||||
accent="default"
|
||||
LeftIcon={IconCheckbox}
|
||||
text="Task"
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
</StyledDropdownMenu>
|
||||
}
|
||||
dropdownHotkeyScope={{
|
||||
scope: PageHotkeyScope.ShowPage,
|
||||
}}
|
||||
/>
|
||||
<DropdownScope dropdownScopeId="add-show-page">
|
||||
<Dropdown
|
||||
clickableComponent={
|
||||
<IconButton
|
||||
Icon={IconPlus}
|
||||
size="medium"
|
||||
dataTestId="add-showpage-button"
|
||||
accent="default"
|
||||
variant="secondary"
|
||||
onClick={toggleDropdown}
|
||||
/>
|
||||
}
|
||||
dropdownComponents={
|
||||
<DropdownMenu>
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
onClick={() => handleSelect(ActivityType.Note)}
|
||||
accent="default"
|
||||
LeftIcon={IconNotes}
|
||||
text="Note"
|
||||
/>
|
||||
<MenuItem
|
||||
onClick={() => handleSelect(ActivityType.Task)}
|
||||
accent="default"
|
||||
LeftIcon={IconCheckbox}
|
||||
text="Task"
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownMenu>
|
||||
}
|
||||
dropdownHotkeyScope={{
|
||||
scope: PageHotkeyScope.ShowPage,
|
||||
}}
|
||||
/>
|
||||
</DropdownScope>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user