Refactor UI folder (#2016)
* Added Overview page * Revised Getting Started page * Minor revision * Edited readme, minor modifications to docs * Removed sweep.yaml, .devcontainer, .ergomake * Moved security.md to .github, added contributing.md * changes as per code review * updated contributing.md * fixed broken links & added missing links in doc, improved structure * fixed link in wsl setup * fixed server link, added https cloning in yarn-setup * removed package-lock.json * added doc card, admonitions * removed underline from nav buttons * refactoring modules/ui * refactoring modules/ui * Change folder case * Fix theme location * Fix case 2 * Fix storybook --------- Co-authored-by: Nimra Ahmed <nimra1408@gmail.com> Co-authored-by: Nimra Ahmed <50912134+nimraahmed@users.noreply.github.com>
This commit is contained in:
@ -0,0 +1,132 @@
|
||||
import { ReactNode, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { AnimatePresence, LayoutGroup } from 'framer-motion';
|
||||
import debounce from 'lodash.debounce';
|
||||
|
||||
import {
|
||||
H1Title,
|
||||
H1TitleFontColor,
|
||||
} from '@/ui/display/typography/components/H1Title';
|
||||
import { Button } from '@/ui/input/button/components/Button';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { Modal } from '@/ui/layout/modal/components/Modal';
|
||||
import {
|
||||
Section,
|
||||
SectionAlignment,
|
||||
SectionFontColor,
|
||||
} from '@/ui/layout/section/components/Section';
|
||||
|
||||
export type ConfirmationModalProps = {
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
subtitle: ReactNode;
|
||||
setIsOpen: (val: boolean) => void;
|
||||
onConfirmClick: () => void;
|
||||
deleteButtonText?: string;
|
||||
confirmationPlaceholder?: string;
|
||||
confirmationValue?: string;
|
||||
};
|
||||
|
||||
const StyledConfirmationModal = styled(Modal)`
|
||||
border-radius: ${({ theme }) => theme.spacing(1)};
|
||||
padding: ${({ theme }) => theme.spacing(6)};
|
||||
width: calc(400px - ${({ theme }) => theme.spacing(32)});
|
||||
`;
|
||||
|
||||
const StyledCenteredButton = styled(Button)`
|
||||
justify-content: center;
|
||||
margin-top: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledCenteredTitle = styled.div`
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
export const StyledConfirmationButton = styled(StyledCenteredButton)`
|
||||
border-color: ${({ theme }) => theme.color.red20};
|
||||
box-shadow: none;
|
||||
color: ${({ theme }) => theme.color.red};
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
line-height: ${({ theme }) => theme.text.lineHeight.lg};
|
||||
:hover {
|
||||
background-color: ${({ theme }) => theme.color.red10};
|
||||
}
|
||||
`;
|
||||
|
||||
export const ConfirmationModal = ({
|
||||
isOpen = false,
|
||||
title,
|
||||
subtitle,
|
||||
setIsOpen,
|
||||
onConfirmClick,
|
||||
deleteButtonText = 'Delete',
|
||||
confirmationValue,
|
||||
confirmationPlaceholder,
|
||||
}: ConfirmationModalProps) => {
|
||||
const [inputConfirmationValue, setInputConfirmationValue] =
|
||||
useState<string>('');
|
||||
const [isValidValue, setIsValidValue] = useState(!confirmationValue);
|
||||
|
||||
const handleInputConfimrationValueChange = (value: string) => {
|
||||
setInputConfirmationValue(value);
|
||||
isValueMatchingInput(confirmationValue, value);
|
||||
};
|
||||
|
||||
const isValueMatchingInput = debounce(
|
||||
(value?: string, inputValue?: string) => {
|
||||
setIsValidValue(Boolean(value && inputValue && value === inputValue));
|
||||
},
|
||||
250,
|
||||
);
|
||||
|
||||
return (
|
||||
<AnimatePresence mode="wait">
|
||||
<LayoutGroup>
|
||||
<StyledConfirmationModal
|
||||
isOpen={isOpen}
|
||||
onClose={() => {
|
||||
if (isOpen) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}}
|
||||
onEnter={onConfirmClick}
|
||||
>
|
||||
<StyledCenteredTitle>
|
||||
<H1Title title={title} fontColor={H1TitleFontColor.Primary} />
|
||||
</StyledCenteredTitle>
|
||||
<Section
|
||||
alignment={SectionAlignment.Center}
|
||||
fontColor={SectionFontColor.Primary}
|
||||
>
|
||||
{subtitle}
|
||||
</Section>
|
||||
{confirmationValue && (
|
||||
<Section>
|
||||
<TextInput
|
||||
value={inputConfirmationValue}
|
||||
onChange={handleInputConfimrationValueChange}
|
||||
placeholder={confirmationPlaceholder}
|
||||
fullWidth
|
||||
key={'input-' + confirmationValue}
|
||||
/>
|
||||
</Section>
|
||||
)}
|
||||
<StyledCenteredButton
|
||||
onClick={onConfirmClick}
|
||||
variant="secondary"
|
||||
accent="danger"
|
||||
title={deleteButtonText}
|
||||
disabled={!isValidValue}
|
||||
fullWidth
|
||||
/>
|
||||
<StyledCenteredButton
|
||||
onClick={() => setIsOpen(false)}
|
||||
variant="secondary"
|
||||
title="Cancel"
|
||||
fullWidth
|
||||
/>
|
||||
</StyledConfirmationModal>
|
||||
</LayoutGroup>
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
216
front/src/modules/ui/layout/modal/components/Modal.tsx
Normal file
216
front/src/modules/ui/layout/modal/components/Modal.tsx
Normal file
@ -0,0 +1,216 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import {
|
||||
ClickOutsideMode,
|
||||
useListenClickOutside,
|
||||
} from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
|
||||
import { ModalHotkeyScope } from './types/ModalHotkeyScope';
|
||||
|
||||
const StyledModalDiv = styled(motion.div)<{
|
||||
size?: ModalSize;
|
||||
padding?: ModalPadding;
|
||||
}>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: ${({ theme }) => theme.background.primary};
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
overflow: hidden;
|
||||
max-height: 90vh;
|
||||
z-index: 10000; // should be higher than Backdrop's z-index
|
||||
|
||||
width: ${({ size, theme }) => {
|
||||
switch (size) {
|
||||
case 'small':
|
||||
return theme.modal.size.sm;
|
||||
case 'medium':
|
||||
return theme.modal.size.md;
|
||||
case 'large':
|
||||
return theme.modal.size.lg;
|
||||
default:
|
||||
return 'auto';
|
||||
}
|
||||
}};
|
||||
|
||||
padding: ${({ padding, theme }) => {
|
||||
switch (padding) {
|
||||
case 'none':
|
||||
return theme.spacing(0);
|
||||
case 'small':
|
||||
return theme.spacing(2);
|
||||
case 'medium':
|
||||
return theme.spacing(4);
|
||||
case 'large':
|
||||
return theme.spacing(6);
|
||||
default:
|
||||
return 'auto';
|
||||
}
|
||||
}};
|
||||
`;
|
||||
|
||||
const StyledHeader = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 60px;
|
||||
overflow: hidden;
|
||||
padding: ${({ theme }) => theme.spacing(5)};
|
||||
`;
|
||||
|
||||
const StyledContent = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
padding: ${({ theme }) => theme.spacing(10)};
|
||||
`;
|
||||
|
||||
const StyledFooter = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 60px;
|
||||
overflow: hidden;
|
||||
padding: ${({ theme }) => theme.spacing(5)};
|
||||
`;
|
||||
|
||||
const StyledBackDrop = styled(motion.div)`
|
||||
align-items: center;
|
||||
background: ${({ theme }) => theme.background.overlay};
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 9999;
|
||||
`;
|
||||
|
||||
/**
|
||||
* Modal components
|
||||
*/
|
||||
type ModalHeaderProps = React.PropsWithChildren & React.ComponentProps<'div'>;
|
||||
|
||||
const ModalHeader = ({ children }: ModalHeaderProps) => (
|
||||
<StyledHeader>{children}</StyledHeader>
|
||||
);
|
||||
|
||||
type ModalContentProps = React.PropsWithChildren & React.ComponentProps<'div'>;
|
||||
|
||||
const ModalContent = ({ children, className }: ModalContentProps) => (
|
||||
<StyledContent className={className}>{children}</StyledContent>
|
||||
);
|
||||
|
||||
type ModalFooterProps = React.PropsWithChildren & React.ComponentProps<'div'>;
|
||||
|
||||
const ModalFooter = ({ children }: ModalFooterProps) => (
|
||||
<StyledFooter>{children}</StyledFooter>
|
||||
);
|
||||
|
||||
/**
|
||||
* Modal
|
||||
*/
|
||||
export type ModalSize = 'small' | 'medium' | 'large';
|
||||
export type ModalPadding = 'none' | 'small' | 'medium' | 'large';
|
||||
|
||||
type ModalProps = React.PropsWithChildren &
|
||||
React.ComponentProps<'div'> & {
|
||||
isOpen?: boolean;
|
||||
onClose?: () => void;
|
||||
hotkeyScope?: ModalHotkeyScope;
|
||||
onEnter?: () => void;
|
||||
size?: ModalSize;
|
||||
padding?: ModalPadding;
|
||||
};
|
||||
|
||||
const modalVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: { opacity: 1 },
|
||||
exit: { opacity: 0 },
|
||||
};
|
||||
|
||||
export const Modal = ({
|
||||
isOpen = false,
|
||||
children,
|
||||
onClose,
|
||||
hotkeyScope = ModalHotkeyScope.Default,
|
||||
onEnter,
|
||||
size = 'medium',
|
||||
padding = 'medium',
|
||||
}: ModalProps) => {
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useListenClickOutside({
|
||||
refs: [modalRef],
|
||||
callback: () => onClose?.(),
|
||||
mode: ClickOutsideMode.absolute,
|
||||
});
|
||||
|
||||
const {
|
||||
goBackToPreviousHotkeyScope,
|
||||
setHotkeyScopeAndMemorizePreviousScope,
|
||||
} = usePreviousHotkeyScope();
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.Escape],
|
||||
() => {
|
||||
onClose?.();
|
||||
},
|
||||
hotkeyScope,
|
||||
[onClose],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.Enter],
|
||||
() => {
|
||||
onEnter?.();
|
||||
},
|
||||
hotkeyScope,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setHotkeyScopeAndMemorizePreviousScope(hotkeyScope);
|
||||
} else {
|
||||
goBackToPreviousHotkeyScope();
|
||||
}
|
||||
}, [
|
||||
goBackToPreviousHotkeyScope,
|
||||
hotkeyScope,
|
||||
isOpen,
|
||||
setHotkeyScopeAndMemorizePreviousScope,
|
||||
]);
|
||||
|
||||
return isOpen ? (
|
||||
<StyledBackDrop>
|
||||
<StyledModalDiv
|
||||
// framer-motion seems to have typing problems with refs
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
ref={modalRef}
|
||||
size={size}
|
||||
padding={padding}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
layout
|
||||
variants={modalVariants}
|
||||
>
|
||||
{children}
|
||||
</StyledModalDiv>
|
||||
</StyledBackDrop>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
};
|
||||
|
||||
Modal.Header = ModalHeader;
|
||||
Modal.Content = ModalContent;
|
||||
Modal.Footer = ModalFooter;
|
||||
@ -0,0 +1,33 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
|
||||
import { ConfirmationModal } from '../ConfirmationModal';
|
||||
|
||||
const meta: Meta<typeof ConfirmationModal> = {
|
||||
title: 'UI/Modal/ConfirmationModal',
|
||||
component: ConfirmationModal,
|
||||
decorators: [ComponentDecorator],
|
||||
};
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof ConfirmationModal>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
isOpen: true,
|
||||
title: 'Pariatur labore.',
|
||||
subtitle: 'Velit dolore aliquip laborum occaecat fugiat.',
|
||||
deleteButtonText: 'Delete',
|
||||
},
|
||||
decorators: [ComponentDecorator],
|
||||
};
|
||||
|
||||
export const InputConfirmation: Story = {
|
||||
args: {
|
||||
confirmationValue: 'email@test.dev',
|
||||
confirmationPlaceholder: 'email@test.dev',
|
||||
...Default.args,
|
||||
},
|
||||
decorators: Default.decorators,
|
||||
};
|
||||
@ -0,0 +1,40 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
|
||||
import { Modal } from '../Modal';
|
||||
import { ModalHotkeyScope } from '../types/ModalHotkeyScope';
|
||||
|
||||
const meta: Meta<typeof Modal> = {
|
||||
title: 'UI/Modal/Modal',
|
||||
component: Modal,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Modal>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
isOpen: true,
|
||||
size: 'medium',
|
||||
padding: 'medium',
|
||||
hotkeyScope: ModalHotkeyScope.Default,
|
||||
children: (
|
||||
<>
|
||||
<Modal.Header>Stay in touch</Modal.Header>
|
||||
<Modal.Content>
|
||||
This is a dummy newletter form so don't bother trying to test it. Not
|
||||
that I expect you to, anyways. :)
|
||||
</Modal.Content>
|
||||
<Modal.Footer>
|
||||
By using Twenty, you're opting for the finest CRM experience you'll
|
||||
ever encounter.
|
||||
</Modal.Footer>
|
||||
</>
|
||||
),
|
||||
},
|
||||
decorators: [ComponentDecorator],
|
||||
argTypes: {
|
||||
children: { control: false },
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,3 @@
|
||||
export enum ModalHotkeyScope {
|
||||
Default = 'default',
|
||||
}
|
||||
Reference in New Issue
Block a user