(frontend) enhance dropdown components and add new LoadMoreText feature

- Updated DropButton and DropdownMenu components to include new props
for accessibility and improved layout.
- Introduced LoadMoreText component for better user experience in
loading additional content.
- Added SearchUserRow and UserAvatar components for improved user search
functionality.
- Cleaned up unused imports and adjusted styles for better consistency
across components.
This commit is contained in:
Nathan Panchout
2024-12-16 10:17:12 +01:00
committed by Anthony LC
parent eb35fdc7a9
commit 8456f47260
11 changed files with 210 additions and 45 deletions

View File

@@ -1,7 +1,7 @@
import { PropsWithChildren, useState } from 'react';
import { css } from 'styled-components';
import { Box, BoxButton, BoxProps, DropButton, Icon } from '@/components';
import { Box, BoxButton, BoxProps, DropButton, Icon, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
export type DropdownMenuOption = {
@@ -10,6 +10,7 @@ export type DropdownMenuOption = {
testId?: string;
callback?: () => void | Promise<unknown>;
danger?: boolean;
isSelected?: boolean;
disabled?: boolean;
show?: boolean;
};
@@ -17,7 +18,9 @@ export type DropdownMenuOption = {
export type DropdownMenuProps = {
options: DropdownMenuOption[];
showArrow?: boolean;
label?: string;
arrowCss?: BoxProps['$css'];
topMessage?: string;
};
export const DropdownMenu = ({
@@ -25,6 +28,8 @@ export const DropdownMenu = ({
children,
showArrow = false,
arrowCss,
label,
topMessage,
}: PropsWithChildren<DropdownMenuProps>) => {
const theme = useCunninghamTheme();
const spacings = theme.spacingsTokens();
@@ -39,9 +44,10 @@ export const DropdownMenu = ({
<DropButton
isOpen={isOpen}
onOpenChange={onOpenChange}
label={label}
button={
showArrow ? (
<Box>
<Box $direction="row" $align="center">
<div>{children}</div>
<Icon
$css={
@@ -58,15 +64,26 @@ export const DropdownMenu = ({
)
}
>
<Box>
{options.map((option, index) => {
<Box $maxWidth="320px">
{topMessage && (
<Text
$variation="1000"
$wrap="wrap"
$size="xs"
$weight="bold"
$padding={{ vertical: 'xs', horizontal: 'base' }}
>
{topMessage}
</Text>
)}
{options.map((option) => {
if (option.show !== undefined && !option.show) {
return;
}
const isDisabled = option.disabled !== undefined && option.disabled;
return (
<BoxButton
aria-label={option.label}
data-testid={option.testId}
$direction="row"
disabled={isDisabled}
@@ -78,6 +95,7 @@ export const DropdownMenu = ({
}}
key={option.label}
$align="center"
$justify="space-between"
$background={colors['greyscale-000']}
$color={colors['primary-600']}
$padding={{ vertical: 'xs', horizontal: 'base' }}
@@ -86,28 +104,35 @@ export const DropdownMenu = ({
$css={css`
border: none;
font-size: var(--c--theme--font--sizes--sm);
color: var(--c--theme--colors--primary-600);
color: var(--c--theme--colors--greyscale-1000);
font-weight: 500;
cursor: ${isDisabled ? 'not-allowed' : 'pointer'};
user-select: none;
border-bottom: ${index !== options.length - 1
? `1px solid var(--c--theme--colors--greyscale-200)`
: 'none'};
&:hover {
background-color: var(--c--theme--colors--greyscale-050);
}
`}
>
{option.icon && (
<Icon
$size="20px"
$theme={!isDisabled ? 'primary' : 'greyscale'}
$variation={!isDisabled ? '600' : '400'}
iconName={option.icon}
/>
<Box $direction="row" $align="center" $gap={spacings['base']}>
{option.icon && (
<Icon
$size="20px"
$theme="greyscale"
$variation={isDisabled ? '400' : '1000'}
iconName={option.icon}
/>
)}
<Text
$margin={{ top: '-3px' }}
$variation={isDisabled ? '400' : '1000'}
>
{option.label}
</Text>
</Box>
{option.isSelected && (
<Icon iconName="check" $size="20px" $theme="greyscale" />
)}
{option.label}
</BoxButton>
);
})}

View File

@@ -0,0 +1,35 @@
import { useTranslation } from 'react-i18next';
import { Box } from './Box';
import { Icon } from './Icon';
import { Text } from './Text';
type LoadMoreTextProps = {
['data-testid']?: string;
};
export const LoadMoreText = ({
'data-testid': dataTestId,
}: LoadMoreTextProps) => {
const { t } = useTranslation();
return (
<Box
data-testid={dataTestId}
$direction="row"
$align="center"
$gap="0.4rem"
$padding={{ horizontal: '2xs', vertical: 'sm' }}
>
<Icon
$theme="primary"
$variation="800"
iconName="arrow_downward"
$size="md"
/>
<Text $theme="primary" $variation="800">
{t('Load more')}
</Text>
</Box>
);
};

View File

@@ -22,7 +22,8 @@ import { BlockNoteToolbar } from './BlockNoteToolbar';
const cssEditor = (readonly: boolean) => `
&, & > .bn-container, & .ProseMirror {
height:100%
height:100%;
};
& .bn-inline-content code {
@@ -32,8 +33,7 @@ const cssEditor = (readonly: boolean) => `
}
@media screen and (width <= 560px) {
& .bn-editor {
padding-left: 40px;
padding-right: 10px;
${readonly && `padding-left: 10px;`}
};
.bn-side-menu[data-block-type=heading][data-level="1"] {

View File

@@ -1,7 +1,6 @@
import { Alert, Loader, VariantType } from '@openfun/cunningham-react';
import { Loader } from '@openfun/cunningham-react';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import * as Y from 'yjs';
@@ -25,7 +24,6 @@ interface DocEditorProps {
}
export const DocEditor = ({ doc, versionId }: DocEditorProps) => {
const { t } = useTranslation();
const { isDesktop } = useResponsiveStore();
const isVersion = !!versionId && typeof versionId === 'string';
@@ -60,14 +58,6 @@ export const DocEditor = ({ doc, versionId }: DocEditorProps) => {
)}
</Box>
{!doc.abilities.partial_update && (
<Box $width="100%" $margin={{ all: 'small', top: 'none' }}>
<Alert type={VariantType.WARNING}>
{t(`Read only, you cannot edit this document.`)}
</Alert>
</Box>
)}
<Box
$background={colorsTokens()['primary-bg']}
$direction="row"

View File

@@ -92,7 +92,7 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
<DocToolBox doc={doc} />
</Box>
</Box>
<HorizontalSeparator $withPadding={false} />
<HorizontalSeparator />
</Box>
</>
);

View File

@@ -21,14 +21,12 @@ import {
useEditorStore,
usePanelEditorStore,
} from '@/features/docs/doc-editor/';
import {
Doc,
ModalRemoveDoc,
ModalShare,
} from '@/features/docs/doc-management';
import { Doc, ModalRemoveDoc } from '@/features/docs/doc-management';
import { ModalSelectVersion } from '@/features/docs/doc-versioning';
import { useResponsiveStore } from '@/stores';
import { DocShareModal } from '../../doc-share/component/DocShareModal';
import { ModalPDF } from './ModalExport';
interface DocToolBoxProps {
@@ -42,10 +40,10 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
const spacings = spacingsTokens();
const colors = colorsTokens();
const [isModalShareOpen, setIsModalShareOpen] = useState(false);
const [isModalRemoveOpen, setIsModalRemoveOpen] = useState(false);
const [isModalPDFOpen, setIsModalPDFOpen] = useState(false);
const selectHistoryModal = useModal();
const modalShare = useModal();
const { setIsPanelOpen, setIsPanelTableContentOpen } = usePanelEditorStore();
const { isSmallMobile, isDesktop } = useResponsiveStore();
@@ -60,7 +58,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
label: t('Share'),
icon: 'upload',
callback: () => {
setIsModalShareOpen(true);
modalShare.open();
},
},
{
@@ -72,6 +70,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
},
]
: []),
{
label: t('Version history'),
icon: 'history',
@@ -149,7 +148,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
<Button
color="primary-text"
onClick={() => {
setIsModalShareOpen(true);
modalShare.open();
}}
size={isSmallMobile ? 'small' : 'medium'}
>
@@ -185,8 +184,9 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
/>
</DropdownMenu>
</Box>
{isModalShareOpen && (
<ModalShare onClose={() => setIsModalShareOpen(false)} doc={doc} />
{modalShare.isOpen && (
<DocShareModal onClose={() => modalShare.close()} doc={doc} />
)}
{isModalPDFOpen && (
<ModalPDF onClose={() => setIsModalPDFOpen(false)} doc={doc} />

View File

@@ -9,12 +9,12 @@ import {
} from '@/components';
import { User } from '@/core';
import { Doc, Role } from '@/features/docs/doc-management';
import { SearchUserRow } from '@/features/docs/doc-share/component/SearchUserRow';
import {
useDeleteDocInvitation,
useUpdateDocInvitation,
} from '@/features/docs/members/invitation-list';
import { Invitation } from '@/features/docs/members/invitation-list/types';
import { SearchUserRow } from '@/features/users/components/SearchUserRow';
import { DocRoleDropdown } from './DocRoleDropdown';

View File

@@ -7,12 +7,12 @@ import {
DropdownMenuOption,
IconOptions,
} from '@/components';
import { SearchUserRow } from '@/features/docs/doc-share/component/SearchUserRow';
import {
useDeleteDocAccess,
useUpdateDocAccess,
} from '@/features/docs/members/members-list';
import { useWhoAmI } from '@/features/docs/members/members-list/hooks/useWhoAmI';
import { SearchUserRow } from '@/features/users/components/SearchUserRow';
import { useResponsiveStore } from '@/stores';
import { Access, Doc, Role } from '../../doc-management/types';

View File

@@ -3,7 +3,7 @@ import { css } from 'styled-components';
import { Box, Icon, Text } from '@/components';
import { User } from '@/core';
import { SearchUserRow } from '@/features/users/components/SearchUserRow';
import { SearchUserRow } from '@/features/docs/doc-share/component/SearchUserRow';
type Props = {
user: User;

View File

@@ -0,0 +1,47 @@
import { Box, Text } from '@/components';
import {
QuickSearchItemContent,
QuickSearchItemContentProps,
} from '@/components/quick-search/QuickSearchItemContent';
import { User } from '@/core';
import { useCunninghamTheme } from '@/cunningham';
import { UserAvatar } from './UserAvatar';
type Props = {
user: User;
alwaysShowRight?: boolean;
right?: QuickSearchItemContentProps['right'];
};
export const SearchUserRow = ({
user,
right,
alwaysShowRight = false,
}: Props) => {
const hasFullName = user.full_name != null && user.full_name !== '';
const { spacingsTokens } = useCunninghamTheme();
const spacings = spacingsTokens();
return (
<QuickSearchItemContent
right={right}
alwaysShowRight={alwaysShowRight}
left={
<Box $direction="row" $align="center" $gap={spacings['2xs']}>
<UserAvatar user={user} />
<Box $direction="column">
<Text $size="sm" $weight="500" $variation="1000">
{hasFullName ? user.full_name : user.email}
</Text>
{hasFullName && (
<Text $size="xs" $variation="600">
{user.email}
</Text>
)}
</Box>
</Box>
}
/>
);
};

View File

@@ -0,0 +1,68 @@
import { css } from 'styled-components';
import { Box } from '@/components';
import { User } from '@/core';
import { tokens } from '@/cunningham';
const colors = tokens.themes.default.theme.colors;
const avatarsColors = [
colors['blue-500'],
colors['brown-500'],
colors['cyan-500'],
colors['gold-500'],
colors['green-500'],
colors['olive-500'],
colors['orange-500'],
colors['pink-500'],
colors['purple-500'],
colors['yellow-500'],
];
const getColorFromName = (name: string) => {
let hash = 0;
for (let i = 0; i < name.length; i++) {
hash = name.charCodeAt(i) + ((hash << 5) - hash);
}
return avatarsColors[Math.abs(hash) % avatarsColors.length];
};
type Props = {
user: User;
};
export const UserAvatar = ({ user }: Props) => {
const name = user.full_name || user.email || '?';
const splitName = name?.split(' ');
return (
<Box
$background={getColorFromName(name)}
$width="24px"
$height="24px"
$direction="row"
$align="center"
$justify="center"
$radius="50%"
$css={css`
color: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(255, 255, 255, 0.5);
`}
>
<Box
$direction="row"
$css={css`
text-align: center;
font-style: normal;
font-weight: 600;
font-family: Arial, Helvetica, sans-serif; // Can't use marianne font because it's impossible to center with this font
font-size: 10px;
text-transform: uppercase;
`}
>
{splitName[0]?.charAt(0)}
{splitName?.[1]?.charAt(0)}
</Box>
</Box>
);
};