@linagora/twake-mui 9.6.0 → 9.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/dist/components/Avatar/helpers.d.ts +9 -0
- package/dist/components/Avatar/helpers.js +21 -0
- package/dist/components/ContactPopover/AttendeeInfo.d.ts +9 -0
- package/dist/components/ContactPopover/AttendeeInfo.js +35 -0
- package/dist/components/ContactPopover/ContactPopover.stories.d.ts +6 -0
- package/dist/components/ContactPopover/ContactPopover.stories.js +73 -0
- package/dist/components/ContactPopover/ContactPopoverActions.d.ts +6 -0
- package/dist/components/ContactPopover/ContactPopoverActions.js +3 -0
- package/dist/components/ContactPopover/ContactPopoverCalendarAction.d.ts +5 -0
- package/dist/components/ContactPopover/ContactPopoverCalendarAction.js +9 -0
- package/dist/components/ContactPopover/ContactPopoverChatAction.d.ts +5 -0
- package/dist/components/ContactPopover/ContactPopoverChatAction.js +9 -0
- package/dist/components/ContactPopover/ContactPopoverEmailAction.d.ts +5 -0
- package/dist/components/ContactPopover/ContactPopoverEmailAction.js +12 -0
- package/dist/components/ContactPopover/ContactPopoverVideoAction.d.ts +5 -0
- package/dist/components/ContactPopover/ContactPopoverVideoAction.js +9 -0
- package/dist/components/ContactPopover/index.d.ts +30 -0
- package/dist/components/ContactPopover/index.js +134 -0
- package/dist/components/ContactPopover/locales/en.json +7 -0
- package/dist/components/ContactPopover/locales/fr.json +7 -0
- package/dist/components/ContactPopover/locales/ru.json +7 -0
- package/dist/components/ContactPopover/locales/vi.json +7 -0
- package/dist/components/DropdownText/DropdownText.stories.d.ts +7 -0
- package/dist/components/DropdownText/DropdownText.stories.js +74 -0
- package/dist/components/DropdownText/index.d.ts +29 -0
- package/dist/components/DropdownText/index.js +34 -0
- package/dist/components/NavDesktopDropdown/index.js +7 -7
- package/dist/components/SearchBar/DisableHighlight.d.ts +1 -0
- package/dist/components/SearchBar/DisableHighlight.js +5 -0
- package/dist/components/SearchBar/FocusHighlight.d.ts +1 -0
- package/dist/components/SearchBar/FocusHighlight.js +6 -0
- package/dist/components/SearchBar/Highlight.d.ts +2 -0
- package/dist/components/SearchBar/Highlight.js +14 -0
- package/dist/components/SearchBar/SearchBar.stories.d.ts +7 -0
- package/dist/components/SearchBar/SearchBar.stories.js +60 -0
- package/dist/components/SearchBar/SearchBarButtonBase.d.ts +3 -0
- package/dist/components/SearchBar/SearchBarButtonBase.js +7 -0
- package/dist/components/SearchBar/SearchBarIconWrapper.d.ts +2 -0
- package/dist/components/SearchBar/SearchBarIconWrapper.js +6 -0
- package/dist/components/SearchBar/SearchBarInputBase.d.ts +5 -0
- package/dist/components/SearchBar/SearchBarInputBase.js +11 -0
- package/dist/components/SearchBar/SearchBarRoot.d.ts +7 -0
- package/dist/components/SearchBar/SearchBarRoot.js +39 -0
- package/dist/components/SearchBar/SearchBarTypography.d.ts +5 -0
- package/dist/components/SearchBar/SearchBarTypography.js +8 -0
- package/dist/components/SearchBar/index.d.ts +4 -0
- package/dist/components/SearchBar/index.js +87 -0
- package/dist/components/SearchBar/types.d.ts +23 -0
- package/dist/components/SearchBar/types.js +1 -0
- package/dist/index.d.ts +13 -1
- package/dist/index.js +5 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
# @linagora/twake-mui [9.8.0](https://github.com/linagora/twake-ui/compare/@linagora/twake-mui@9.7.0...@linagora/twake-mui@9.8.0) (2026-09-24)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **twake-mui:** Migrate DropdownText from cozy-ui ([777b07c](https://github.com/linagora/twake-ui/commit/777b07c26e72cc21ac278dd33b1e0a8210dc19e9))
|
|
7
|
+
|
|
8
|
+
# @linagora/twake-mui [9.7.0](https://github.com/linagora/twake-ui/compare/@linagora/twake-mui@9.6.0...@linagora/twake-mui@9.7.0) (2026-09-24)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **twake-mui:** Added ContactPopover component ([5332ae2](https://github.com/linagora/twake-ui/commit/5332ae2051c0863ce8269a32954c5dbb3a5a5694))
|
|
14
|
+
* **twake-mui:** Migrate SearchBar from cozy-ui ([0b03e38](https://github.com/linagora/twake-ui/commit/0b03e38cee859c52d913acb3aca155eeca0c9665))
|
|
15
|
+
|
|
1
16
|
# @linagora/twake-mui [9.6.0](https://github.com/linagora/twake-ui/compare/@linagora/twake-mui@9.5.1...@linagora/twake-mui@9.6.0) (2026-09-24)
|
|
2
17
|
|
|
3
18
|
|
|
@@ -6,3 +6,12 @@ export declare const colorMapping: Record<string, string>;
|
|
|
6
6
|
* @returns Color key from supportedColors
|
|
7
7
|
*/
|
|
8
8
|
export declare const nameToColor: (name?: string) => string | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* Extract initials from a name (first letter of first name + first letter of last name)
|
|
11
|
+
* If only one name is provided, returns first 2 letters
|
|
12
|
+
* Falls back to first 2 letters of email local-part if no name provided
|
|
13
|
+
* @param name - Display name
|
|
14
|
+
* @param email - Email address (fallback source for initials)
|
|
15
|
+
* @returns Uppercase initials string
|
|
16
|
+
*/
|
|
17
|
+
export declare const getInitials: (name: string, email: string) => string;
|
|
@@ -41,3 +41,24 @@ export const nameToColor = (name = '') => {
|
|
|
41
41
|
const key = makeKey(colors, name);
|
|
42
42
|
return colors[key];
|
|
43
43
|
};
|
|
44
|
+
/**
|
|
45
|
+
* Extract initials from a name (first letter of first name + first letter of last name)
|
|
46
|
+
* If only one name is provided, returns first 2 letters
|
|
47
|
+
* Falls back to first 2 letters of email local-part if no name provided
|
|
48
|
+
* @param name - Display name
|
|
49
|
+
* @param email - Email address (fallback source for initials)
|
|
50
|
+
* @returns Uppercase initials string
|
|
51
|
+
*/
|
|
52
|
+
export const getInitials = (name, email) => {
|
|
53
|
+
if (!name || name === email) {
|
|
54
|
+
const emailPart = email.split('@')[0];
|
|
55
|
+
return emailPart.slice(0, 2).toUpperCase();
|
|
56
|
+
}
|
|
57
|
+
const parts = name.trim().split(/\s+/);
|
|
58
|
+
if (parts.length === 1) {
|
|
59
|
+
return parts[0].slice(0, 2).toUpperCase();
|
|
60
|
+
}
|
|
61
|
+
const firstName = parts[0];
|
|
62
|
+
const lastName = parts[parts.length - 1];
|
|
63
|
+
return (firstName[0] + lastName[0]).toUpperCase();
|
|
64
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface AttendeeInfoProps {
|
|
2
|
+
/** Attendee display name */
|
|
3
|
+
name: string;
|
|
4
|
+
/** Attendee email address */
|
|
5
|
+
email: string;
|
|
6
|
+
/** Callback when email is copied to clipboard */
|
|
7
|
+
onEmailCopy?: (email: string) => void;
|
|
8
|
+
}
|
|
9
|
+
export declare const AttendeeInfo: ({ name, email, onEmailCopy }: AttendeeInfoProps) => JSX.Element;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Icon } from '@linagora/twake-icons';
|
|
2
|
+
import { Copy } from '@linagora/twake-icons';
|
|
3
|
+
import { Box, IconButton, Typography, useTheme, alpha } from '@mui/material';
|
|
4
|
+
import React, { useState } from 'react';
|
|
5
|
+
import { useI18n } from 'twake-i18n';
|
|
6
|
+
import { Avatar } from '../Avatar';
|
|
7
|
+
import { nameToColor, getInitials } from '../Avatar/helpers';
|
|
8
|
+
export const AttendeeInfo = ({ name, email, onEmailCopy }) => {
|
|
9
|
+
const theme = useTheme();
|
|
10
|
+
const { t } = useI18n();
|
|
11
|
+
const [showCopied, setShowCopied] = useState(false);
|
|
12
|
+
const handleCopy = async () => {
|
|
13
|
+
try {
|
|
14
|
+
await navigator.clipboard.writeText(email);
|
|
15
|
+
onEmailCopy?.(email);
|
|
16
|
+
setShowCopied(true);
|
|
17
|
+
setTimeout(() => setShowCopied(false), 2000);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// Clipboard write failed — silently ignore
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const avatarColor = nameToColor(name || email);
|
|
24
|
+
const initials = getInitials(name, email);
|
|
25
|
+
return (React.createElement(Box, { sx: { display: 'flex', alignItems: 'center', gap: 2 } },
|
|
26
|
+
React.createElement(Avatar, { color: avatarColor, size: 48, sx: { width: 48, height: 48, fontSize: '1.25rem' } }, initials),
|
|
27
|
+
React.createElement(Box, { sx: { minWidth: 0 } },
|
|
28
|
+
name && name !== email ? (React.createElement(Typography, { variant: "body1", sx: { fontWeight: 500 }, noWrap: true }, name)) : null,
|
|
29
|
+
React.createElement(Box, { sx: { display: 'flex', alignItems: 'center', gap: 1 } },
|
|
30
|
+
React.createElement(Typography, { variant: "body2", color: "text.secondary", noWrap: true }, email),
|
|
31
|
+
React.createElement(IconButton, { size: "small", onClick: () => void handleCopy(), sx: { p: 0.5, color: alpha(theme.palette.grey[900], 0.9) }, title: showCopied
|
|
32
|
+
? t('ContactPopover.copiedTooltip')
|
|
33
|
+
: t('ContactPopover.copyTooltip') },
|
|
34
|
+
React.createElement(Icon, { icon: Copy, size: 16, color: theme.palette.text.icon }))))));
|
|
35
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Chip, Box, Typography } from '@mui/material';
|
|
2
|
+
import React, { useEffect, useRef } from 'react';
|
|
3
|
+
import { ContactPopover } from './index';
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'Components/ContactPopover',
|
|
6
|
+
component: ContactPopover,
|
|
7
|
+
parameters: {
|
|
8
|
+
layout: 'centered'
|
|
9
|
+
},
|
|
10
|
+
tags: ['autodocs', 'argos'],
|
|
11
|
+
argTypes: {
|
|
12
|
+
name: {
|
|
13
|
+
control: 'text',
|
|
14
|
+
description: 'Attendee display name'
|
|
15
|
+
},
|
|
16
|
+
email: {
|
|
17
|
+
control: 'text',
|
|
18
|
+
description: 'Attendee email address'
|
|
19
|
+
},
|
|
20
|
+
disabled: {
|
|
21
|
+
control: 'boolean',
|
|
22
|
+
description: 'Disable the popover'
|
|
23
|
+
},
|
|
24
|
+
onEmailCopy: {
|
|
25
|
+
action: 'email copied'
|
|
26
|
+
},
|
|
27
|
+
onClose: {
|
|
28
|
+
action: 'popover closed'
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
export default meta;
|
|
33
|
+
export const Default = {
|
|
34
|
+
render: () => {
|
|
35
|
+
const containerRef = useRef(null);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const timer = setTimeout(() => {
|
|
38
|
+
containerRef.current
|
|
39
|
+
?.querySelectorAll('[role="button"]')[0]
|
|
40
|
+
.click();
|
|
41
|
+
}, 100);
|
|
42
|
+
return () => clearTimeout(timer);
|
|
43
|
+
}, []);
|
|
44
|
+
return (React.createElement(Box, { ref: containerRef, sx: { display: 'flex', flexDirection: 'column', gap: 3 } },
|
|
45
|
+
React.createElement(Box, null,
|
|
46
|
+
React.createElement(Typography, { variant: "subtitle2", gutterBottom: true }, "Default \u2014 all actions"),
|
|
47
|
+
React.createElement(ContactPopover, { name: "John Doe", email: "john.doe@example.com" },
|
|
48
|
+
React.createElement(Chip, { label: "John Doe", clickable: true }),
|
|
49
|
+
React.createElement(ContactPopover.Actions, null,
|
|
50
|
+
React.createElement(ContactPopover.EmailAction, { url: "https://mail.example.com/john.doe" }),
|
|
51
|
+
React.createElement(ContactPopover.CalendarAction, { url: "https://calendar.example.com/john.doe" }),
|
|
52
|
+
React.createElement(ContactPopover.ChatAction, { url: "https://chat.example.com/john.doe" }),
|
|
53
|
+
React.createElement(ContactPopover.VideoAction, { url: "https://meet.example.com/john.doe" })))),
|
|
54
|
+
React.createElement(Box, null,
|
|
55
|
+
React.createElement(Typography, { variant: "subtitle2", gutterBottom: true }, "Email only"),
|
|
56
|
+
React.createElement(ContactPopover, { name: "", email: "jane.smith@example.com" },
|
|
57
|
+
React.createElement(Chip, { label: "jane.smith@example.com", clickable: true }),
|
|
58
|
+
React.createElement(ContactPopover.Actions, null,
|
|
59
|
+
React.createElement(ContactPopover.EmailAction, { url: "mailto:jane.smith@example.com" })))),
|
|
60
|
+
React.createElement(Box, null,
|
|
61
|
+
React.createElement(Typography, { variant: "subtitle2", gutterBottom: true }, "Without actions"),
|
|
62
|
+
React.createElement(ContactPopover, { name: "Alice Brown", email: "alice.brown@example.com" },
|
|
63
|
+
React.createElement(Chip, { label: "Alice Brown", clickable: true }))),
|
|
64
|
+
React.createElement(Box, null,
|
|
65
|
+
React.createElement(Typography, { variant: "subtitle2", gutterBottom: true }, "Long name / email"),
|
|
66
|
+
React.createElement(ContactPopover, { name: "Christopher Alexander Montgomery", email: "christopher.montgomery@verylongdomain.example.com" },
|
|
67
|
+
React.createElement(Chip, { label: "Christopher Alexander Montgomery", clickable: true }),
|
|
68
|
+
React.createElement(ContactPopover.Actions, null,
|
|
69
|
+
React.createElement(ContactPopover.ChatAction, { url: "https://chat.example.com/christopher" }),
|
|
70
|
+
React.createElement(ContactPopover.VideoAction, { url: "https://meet.example.com/christopher" }),
|
|
71
|
+
React.createElement(ContactPopover.CalendarAction, { url: "https://calendar.example.com/christopher" }))))));
|
|
72
|
+
}
|
|
73
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Icon } from '@linagora/twake-icons';
|
|
2
|
+
import { CalendarToday } from '@linagora/twake-icons';
|
|
3
|
+
import { IconButton, useTheme } from '@mui/material';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
export const ContactPopoverCalendarAction = ({ url }) => {
|
|
6
|
+
const theme = useTheme();
|
|
7
|
+
return (React.createElement(IconButton, { href: url, size: "small", sx: { border: `1px solid ${theme.palette.divider}` } },
|
|
8
|
+
React.createElement(Icon, { icon: CalendarToday, size: 20, color: theme.palette.text.icon })));
|
|
9
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Icon } from '@linagora/twake-icons';
|
|
2
|
+
import { Discuss } from '@linagora/twake-icons';
|
|
3
|
+
import { IconButton, useTheme } from '@mui/material';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
export const ContactPopoverChatAction = ({ url }) => {
|
|
6
|
+
const theme = useTheme();
|
|
7
|
+
return (React.createElement(IconButton, { href: url, size: "small", sx: { border: `1px solid ${theme.palette.divider}` } },
|
|
8
|
+
React.createElement(Icon, { icon: Discuss, size: 20, color: theme.palette.text.icon })));
|
|
9
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Button, useTheme } from '@mui/material';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { useI18n } from 'twake-i18n';
|
|
4
|
+
export const ContactPopoverEmailAction = ({ url }) => {
|
|
5
|
+
const theme = useTheme();
|
|
6
|
+
const { t } = useI18n();
|
|
7
|
+
return (React.createElement(Button, { href: url, variant: "outlined", size: "small", fullWidth: true, sx: {
|
|
8
|
+
justifyContent: 'center',
|
|
9
|
+
color: theme.palette.text.primary,
|
|
10
|
+
borderColor: theme.palette.divider
|
|
11
|
+
} }, t('ContactPopover.emailButton')));
|
|
12
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Icon } from '@linagora/twake-icons';
|
|
2
|
+
import { Camera } from '@linagora/twake-icons';
|
|
3
|
+
import { IconButton, useTheme } from '@mui/material';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
export const ContactPopoverVideoAction = ({ url }) => {
|
|
6
|
+
const theme = useTheme();
|
|
7
|
+
return (React.createElement(IconButton, { href: url, size: "small", sx: { border: `1px solid ${theme.palette.divider}` } },
|
|
8
|
+
React.createElement(Icon, { icon: Camera, size: 20, color: theme.palette.text.icon })));
|
|
9
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ContactPopoverActions } from './ContactPopoverActions';
|
|
3
|
+
import { ContactPopoverCalendarAction } from './ContactPopoverCalendarAction';
|
|
4
|
+
import { ContactPopoverChatAction } from './ContactPopoverChatAction';
|
|
5
|
+
import { ContactPopoverEmailAction } from './ContactPopoverEmailAction';
|
|
6
|
+
import { ContactPopoverVideoAction } from './ContactPopoverVideoAction';
|
|
7
|
+
export interface ContactPopoverProps {
|
|
8
|
+
/** Attendee display name */
|
|
9
|
+
name: string;
|
|
10
|
+
/** Attendee email address */
|
|
11
|
+
email: string;
|
|
12
|
+
/** Children element that triggers the popover, plus optional ContactPopover.Actions */
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
/** Callback when email is copied to clipboard */
|
|
15
|
+
onEmailCopy?: (email: string) => void;
|
|
16
|
+
/** Callback when the popover closes */
|
|
17
|
+
onClose?: () => void;
|
|
18
|
+
/** Disable the popover (render children only) */
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
}
|
|
21
|
+
interface ContactPopoverComponent extends React.FC<ContactPopoverProps> {
|
|
22
|
+
Actions: typeof ContactPopoverActions;
|
|
23
|
+
ChatAction: typeof ContactPopoverChatAction;
|
|
24
|
+
VideoAction: typeof ContactPopoverVideoAction;
|
|
25
|
+
CalendarAction: typeof ContactPopoverCalendarAction;
|
|
26
|
+
EmailAction: typeof ContactPopoverEmailAction;
|
|
27
|
+
}
|
|
28
|
+
export declare const ContactPopover: ContactPopoverComponent;
|
|
29
|
+
export { ContactPopoverActions, ContactPopoverChatAction, ContactPopoverVideoAction, ContactPopoverCalendarAction, ContactPopoverEmailAction };
|
|
30
|
+
export default ContactPopover;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { Icon } from '@linagora/twake-icons';
|
|
2
|
+
import { Cross } from '@linagora/twake-icons';
|
|
3
|
+
import { Box, IconButton, Popover, styled } from '@mui/material';
|
|
4
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
5
|
+
import { useExtendI18n } from 'twake-i18n';
|
|
6
|
+
import { AttendeeInfo } from './AttendeeInfo';
|
|
7
|
+
import { ContactPopoverActions } from './ContactPopoverActions';
|
|
8
|
+
import { ContactPopoverCalendarAction } from './ContactPopoverCalendarAction';
|
|
9
|
+
import { ContactPopoverChatAction } from './ContactPopoverChatAction';
|
|
10
|
+
import { ContactPopoverEmailAction } from './ContactPopoverEmailAction';
|
|
11
|
+
import { ContactPopoverVideoAction } from './ContactPopoverVideoAction';
|
|
12
|
+
import en from './locales/en.json';
|
|
13
|
+
import fr from './locales/fr.json';
|
|
14
|
+
import ru from './locales/ru.json';
|
|
15
|
+
import vi from './locales/vi.json';
|
|
16
|
+
const locales = { en, fr, ru, vi };
|
|
17
|
+
const StyledPopoverContent = styled(Box)(({ theme }) => ({
|
|
18
|
+
padding: theme.spacing(3),
|
|
19
|
+
display: 'flex',
|
|
20
|
+
flexDirection: 'column',
|
|
21
|
+
gap: theme.spacing(2),
|
|
22
|
+
position: 'relative',
|
|
23
|
+
minWidth: 320
|
|
24
|
+
}));
|
|
25
|
+
const CloseButton = styled(IconButton)(() => ({
|
|
26
|
+
position: 'absolute',
|
|
27
|
+
top: 8,
|
|
28
|
+
right: 8
|
|
29
|
+
}));
|
|
30
|
+
export const ContactPopover = ({ name, email, children, onEmailCopy, onClose, disabled = false }) => {
|
|
31
|
+
useExtendI18n(locales);
|
|
32
|
+
const [anchorEl, setAnchorEl] = useState(null);
|
|
33
|
+
const openTimeoutRef = useRef(null);
|
|
34
|
+
const closeTimeoutRef = useRef(null);
|
|
35
|
+
const clearOpenTimeout = () => {
|
|
36
|
+
if (openTimeoutRef.current) {
|
|
37
|
+
clearTimeout(openTimeoutRef.current);
|
|
38
|
+
openTimeoutRef.current = null;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const clearCloseTimeout = () => {
|
|
42
|
+
if (closeTimeoutRef.current) {
|
|
43
|
+
clearTimeout(closeTimeoutRef.current);
|
|
44
|
+
closeTimeoutRef.current = null;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const handleTriggerMouseEnter = (event) => {
|
|
48
|
+
clearCloseTimeout();
|
|
49
|
+
clearOpenTimeout();
|
|
50
|
+
const target = event.currentTarget;
|
|
51
|
+
if (anchorEl) {
|
|
52
|
+
setAnchorEl(target);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
openTimeoutRef.current = setTimeout(() => {
|
|
56
|
+
setAnchorEl(target);
|
|
57
|
+
}, 500);
|
|
58
|
+
};
|
|
59
|
+
const handleTriggerMouseLeave = () => {
|
|
60
|
+
clearOpenTimeout();
|
|
61
|
+
if (anchorEl)
|
|
62
|
+
return;
|
|
63
|
+
clearCloseTimeout();
|
|
64
|
+
closeTimeoutRef.current = setTimeout(() => {
|
|
65
|
+
setAnchorEl(null);
|
|
66
|
+
}, 200);
|
|
67
|
+
};
|
|
68
|
+
const handlePopoverMouseEnter = () => {
|
|
69
|
+
clearCloseTimeout();
|
|
70
|
+
clearOpenTimeout();
|
|
71
|
+
};
|
|
72
|
+
const handleMouseLeave = () => {
|
|
73
|
+
clearOpenTimeout();
|
|
74
|
+
clearCloseTimeout();
|
|
75
|
+
closeTimeoutRef.current = setTimeout(() => {
|
|
76
|
+
setAnchorEl(null);
|
|
77
|
+
}, 200);
|
|
78
|
+
};
|
|
79
|
+
const handleClick = (event) => {
|
|
80
|
+
clearOpenTimeout();
|
|
81
|
+
clearCloseTimeout();
|
|
82
|
+
setAnchorEl(anchorEl ? null : event.currentTarget);
|
|
83
|
+
};
|
|
84
|
+
const handleClose = () => {
|
|
85
|
+
clearOpenTimeout();
|
|
86
|
+
clearCloseTimeout();
|
|
87
|
+
setAnchorEl(null);
|
|
88
|
+
onClose?.();
|
|
89
|
+
};
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
return () => {
|
|
92
|
+
clearOpenTimeout();
|
|
93
|
+
clearCloseTimeout();
|
|
94
|
+
};
|
|
95
|
+
}, []);
|
|
96
|
+
const childrenArray = React.Children.toArray(children);
|
|
97
|
+
const actionsChild = childrenArray.find((child) => React.isValidElement(child) && child.type === ContactPopoverActions);
|
|
98
|
+
const triggerChildren = childrenArray.filter(child => child !== actionsChild);
|
|
99
|
+
if (disabled) {
|
|
100
|
+
return React.createElement(React.Fragment, null, triggerChildren);
|
|
101
|
+
}
|
|
102
|
+
return (React.createElement("div", null,
|
|
103
|
+
React.createElement("div", { onClick: handleClick, onMouseEnter: handleTriggerMouseEnter, onMouseLeave: handleTriggerMouseLeave, onKeyDown: event => {
|
|
104
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
105
|
+
handleClick(event);
|
|
106
|
+
}
|
|
107
|
+
}, role: "button", tabIndex: 0, style: {
|
|
108
|
+
display: 'inline-block',
|
|
109
|
+
width: 'fit-content',
|
|
110
|
+
cursor: 'pointer'
|
|
111
|
+
} }, triggerChildren),
|
|
112
|
+
React.createElement(Popover, { open: Boolean(anchorEl), anchorEl: anchorEl, onClose: handleClose, anchorOrigin: { vertical: 'center', horizontal: 'right' }, transformOrigin: { vertical: 'center', horizontal: 'left' }, slotProps: {
|
|
113
|
+
paper: {
|
|
114
|
+
elevation: 4,
|
|
115
|
+
sx: {
|
|
116
|
+
borderRadius: 2
|
|
117
|
+
},
|
|
118
|
+
onMouseEnter: handlePopoverMouseEnter,
|
|
119
|
+
onMouseLeave: handleMouseLeave
|
|
120
|
+
}
|
|
121
|
+
} },
|
|
122
|
+
React.createElement(StyledPopoverContent, null,
|
|
123
|
+
React.createElement(CloseButton, { size: "small", onClick: handleClose },
|
|
124
|
+
React.createElement(Icon, { icon: Cross, size: 16 })),
|
|
125
|
+
React.createElement(AttendeeInfo, { name: name, email: email, onEmailCopy: onEmailCopy }),
|
|
126
|
+
actionsChild))));
|
|
127
|
+
};
|
|
128
|
+
ContactPopover.Actions = ContactPopoverActions;
|
|
129
|
+
ContactPopover.ChatAction = ContactPopoverChatAction;
|
|
130
|
+
ContactPopover.VideoAction = ContactPopoverVideoAction;
|
|
131
|
+
ContactPopover.CalendarAction = ContactPopoverCalendarAction;
|
|
132
|
+
ContactPopover.EmailAction = ContactPopoverEmailAction;
|
|
133
|
+
export { ContactPopoverActions, ContactPopoverChatAction, ContactPopoverVideoAction, ContactPopoverCalendarAction, ContactPopoverEmailAction };
|
|
134
|
+
export default ContactPopover;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import DropdownText from './index';
|
|
3
|
+
declare const meta: Meta<typeof DropdownText>;
|
|
4
|
+
export default meta;
|
|
5
|
+
type Story = StoryObj<typeof meta>;
|
|
6
|
+
export declare const Default: Story;
|
|
7
|
+
export declare const Screenshot: Story;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Box, Grid, Stack } from '@mui/material';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import DropdownText from './index';
|
|
4
|
+
const variants = [
|
|
5
|
+
'h1',
|
|
6
|
+
'h2',
|
|
7
|
+
'h3',
|
|
8
|
+
'h4',
|
|
9
|
+
'h5',
|
|
10
|
+
'h6',
|
|
11
|
+
'subtitle1',
|
|
12
|
+
'subtitle2',
|
|
13
|
+
'body1',
|
|
14
|
+
'body2',
|
|
15
|
+
'caption'
|
|
16
|
+
];
|
|
17
|
+
const columns = [
|
|
18
|
+
{ title: 'Default', props: {} },
|
|
19
|
+
{ title: 'Disabled', props: { disabled: true } },
|
|
20
|
+
{ title: 'Color primary', props: { color: 'primary' } },
|
|
21
|
+
{ title: 'Color error', props: { color: 'error' } }
|
|
22
|
+
];
|
|
23
|
+
const meta = {
|
|
24
|
+
title: 'DropdownText',
|
|
25
|
+
component: DropdownText,
|
|
26
|
+
parameters: { layout: 'centered' },
|
|
27
|
+
tags: ['autodocs'],
|
|
28
|
+
argTypes: {
|
|
29
|
+
variant: { control: 'select', options: variants },
|
|
30
|
+
color: {
|
|
31
|
+
control: 'select',
|
|
32
|
+
options: ['inherit', 'primary', 'secondary', 'error', 'textSecondary']
|
|
33
|
+
},
|
|
34
|
+
disabled: { control: 'boolean' },
|
|
35
|
+
noWrap: { control: 'boolean' },
|
|
36
|
+
spaceBetween: { control: 'boolean' }
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
export default meta;
|
|
40
|
+
export const Default = {
|
|
41
|
+
args: {
|
|
42
|
+
children: 'Dropdown'
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const Frame = ({ width, children }) => (React.createElement(Box, { sx: { width, border: 1, borderStyle: 'dashed', borderColor: 'divider' } }, children));
|
|
46
|
+
// Visual Regression - Combined view for Argos testing
|
|
47
|
+
export const Screenshot = {
|
|
48
|
+
tags: ['argos'],
|
|
49
|
+
render: () => (React.createElement(Stack, { spacing: 4 },
|
|
50
|
+
React.createElement("section", null,
|
|
51
|
+
React.createElement("h3", { style: { marginBottom: '12px' } }, "Variants"),
|
|
52
|
+
React.createElement(Grid, { container: true, spacing: 2 }, columns.map(({ title, props }) => (React.createElement(Grid, { key: title, size: { xs: 6, sm: 3 } },
|
|
53
|
+
React.createElement(Stack, { spacing: 2 },
|
|
54
|
+
React.createElement("div", null, title),
|
|
55
|
+
variants.map(variant => (React.createElement(DropdownText, { key: variant, variant: variant, ...props }, variant))))))))),
|
|
56
|
+
React.createElement("section", null,
|
|
57
|
+
React.createElement("h3", { style: { marginBottom: '12px' } }, "Containers"),
|
|
58
|
+
React.createElement(Stack, { spacing: 2 },
|
|
59
|
+
React.createElement(Frame, null,
|
|
60
|
+
React.createElement(DropdownText, null, "This is a long text without ellipsis without restrictive container")),
|
|
61
|
+
React.createElement(Frame, { width: "150px" },
|
|
62
|
+
React.createElement(DropdownText, null, "This is a long text without ellipsis inside a restrictive container")),
|
|
63
|
+
React.createElement(Frame, { width: "150px" },
|
|
64
|
+
React.createElement(DropdownText, { noWrap: true }, "This is a long text with ellipsis inside a container")),
|
|
65
|
+
React.createElement(Frame, null,
|
|
66
|
+
React.createElement(DropdownText, null,
|
|
67
|
+
"Text with",
|
|
68
|
+
React.createElement("br", null),
|
|
69
|
+
"breaking spaces",
|
|
70
|
+
React.createElement("br", null),
|
|
71
|
+
"inside content")),
|
|
72
|
+
React.createElement(Frame, null,
|
|
73
|
+
React.createElement(DropdownText, { spaceBetween: true }, "Space between text and icon"))))))
|
|
74
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { IconProps } from '@linagora/twake-icons';
|
|
2
|
+
import { TypographyProps } from '@mui/material';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
declare const iconSizeByVariant: {
|
|
5
|
+
readonly h1: 24;
|
|
6
|
+
readonly h2: 17;
|
|
7
|
+
readonly h3: 15;
|
|
8
|
+
readonly h4: 14;
|
|
9
|
+
readonly h5: 13;
|
|
10
|
+
readonly h6: 12;
|
|
11
|
+
readonly body1: 12;
|
|
12
|
+
readonly body2: 11;
|
|
13
|
+
readonly caption: 10;
|
|
14
|
+
readonly subtitle1: 11;
|
|
15
|
+
readonly subtitle2: 10;
|
|
16
|
+
};
|
|
17
|
+
export type DropdownTextVariant = keyof typeof iconSizeByVariant;
|
|
18
|
+
export interface DropdownTextProps extends Omit<React.ComponentPropsWithoutRef<'div'>, 'color'> {
|
|
19
|
+
variant?: DropdownTextVariant;
|
|
20
|
+
color?: TypographyProps['color'];
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
noWrap?: boolean;
|
|
23
|
+
spaceBetween?: boolean;
|
|
24
|
+
innerTextProps?: TypographyProps;
|
|
25
|
+
innerIconContainerProps?: TypographyProps;
|
|
26
|
+
innerIconProps?: Omit<IconProps, 'icon'>;
|
|
27
|
+
}
|
|
28
|
+
declare const DropdownText: React.ForwardRefExoticComponent<DropdownTextProps & React.RefAttributes<HTMLDivElement>>;
|
|
29
|
+
export default DropdownText;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Icon, Bottom } from '@linagora/twake-icons';
|
|
2
|
+
import { Box, Typography } from '@mui/material';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
const iconSizeByVariant = {
|
|
5
|
+
h1: 24,
|
|
6
|
+
h2: 17,
|
|
7
|
+
h3: 15,
|
|
8
|
+
h4: 14,
|
|
9
|
+
h5: 13,
|
|
10
|
+
h6: 12,
|
|
11
|
+
body1: 12,
|
|
12
|
+
body2: 11,
|
|
13
|
+
caption: 10,
|
|
14
|
+
subtitle1: 11,
|
|
15
|
+
subtitle2: 10
|
|
16
|
+
};
|
|
17
|
+
const DropdownText = React.forwardRef(({ variant = 'body1', color = 'inherit', disabled = false, noWrap = false, spaceBetween = false, innerTextProps, innerIconContainerProps, innerIconProps, children, ...props }, ref) => {
|
|
18
|
+
const textColor = disabled ? 'textDisabled' : color;
|
|
19
|
+
return (React.createElement(Box, { ref: ref, sx: {
|
|
20
|
+
display: 'flex',
|
|
21
|
+
width: '100%',
|
|
22
|
+
alignItems: 'center',
|
|
23
|
+
justifyContent: spaceBetween ? 'space-between' : undefined
|
|
24
|
+
}, ...props },
|
|
25
|
+
React.createElement(Typography, { variant: variant, color: textColor, noWrap: noWrap, ...innerTextProps }, children),
|
|
26
|
+
React.createElement(Typography, { component: "span", color: textColor, sx: {
|
|
27
|
+
display: 'flex',
|
|
28
|
+
ml: '5px',
|
|
29
|
+
mt: variant === 'body1' ? '3px' : undefined
|
|
30
|
+
}, ...innerIconContainerProps },
|
|
31
|
+
React.createElement(Icon, { icon: Bottom, size: iconSizeByVariant[variant], ...innerIconProps }))));
|
|
32
|
+
});
|
|
33
|
+
DropdownText.displayName = 'DropdownText';
|
|
34
|
+
export default DropdownText;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ListItem, Typography } from '@mui/material';
|
|
1
|
+
import { ListItem } from '@mui/material';
|
|
3
2
|
import React, { Children, isValidElement, useState } from 'react';
|
|
4
3
|
import { useBreakpoints } from '../../hooks/useBreakpoints';
|
|
4
|
+
import DropdownText from '../DropdownText';
|
|
5
5
|
export const NavDesktopDropdown = ({ label, children, defaultOpen = true, limit = 5 }) => {
|
|
6
6
|
const { isDesktop } = useBreakpoints();
|
|
7
7
|
const [open, setOpen] = useState(defaultOpen);
|
|
@@ -10,15 +10,15 @@ export const NavDesktopDropdown = ({ label, children, defaultOpen = true, limit
|
|
|
10
10
|
if (!isDesktop)
|
|
11
11
|
return null;
|
|
12
12
|
return (React.createElement(React.Fragment, null,
|
|
13
|
-
React.createElement(ListItem, {
|
|
13
|
+
React.createElement(ListItem, { sx: {
|
|
14
14
|
minHeight: 48,
|
|
15
15
|
py: 1,
|
|
16
16
|
px: 2,
|
|
17
|
-
justifyContent: 'space-between',
|
|
18
17
|
cursor: isActivated ? 'pointer' : undefined
|
|
19
18
|
} },
|
|
20
|
-
React.createElement(
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
React.createElement(DropdownText, { variant: "caption", color: "textSecondary", spaceBetween: true, onClick: isActivated ? onToggle : undefined, innerIconProps: {
|
|
20
|
+
rotate: open ? 0 : -90,
|
|
21
|
+
display: isActivated ? undefined : 'none'
|
|
22
|
+
} }, label)),
|
|
23
23
|
open && children));
|
|
24
24
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const DisableHighlight: import("@emotion/styled").StyledComponent<any, {}, {}>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const FocusHighlight: import("@emotion/styled").StyledComponent<any, {}, {}>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { styled } from '@mui/material';
|
|
2
|
+
export const Highlight = styled('span')(({ theme }) => ({
|
|
3
|
+
overflow: 'hidden',
|
|
4
|
+
position: 'absolute',
|
|
5
|
+
top: -1,
|
|
6
|
+
right: -1,
|
|
7
|
+
bottom: -1,
|
|
8
|
+
left: -1,
|
|
9
|
+
borderRadius: 'inherit',
|
|
10
|
+
opacity: 0,
|
|
11
|
+
transition: theme.transitions.create('opacity', {
|
|
12
|
+
duration: theme.transitions.duration.short
|
|
13
|
+
})
|
|
14
|
+
}));
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { SearchBar } from './index';
|
|
3
|
+
declare const meta: Meta<typeof SearchBar>;
|
|
4
|
+
export default meta;
|
|
5
|
+
type Story = StoryObj<typeof meta>;
|
|
6
|
+
export declare const Default: Story;
|
|
7
|
+
export declare const Screenshot: Story;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Cloud } from '@linagora/twake-icons';
|
|
2
|
+
import { Stack, Typography } from '@mui/material';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { SearchBar } from './index';
|
|
5
|
+
const sizes = ['small', 'medium', 'large', 'auto'];
|
|
6
|
+
const meta = {
|
|
7
|
+
title: 'SearchBar',
|
|
8
|
+
component: SearchBar,
|
|
9
|
+
parameters: { layout: 'padded' },
|
|
10
|
+
tags: ['autodocs'],
|
|
11
|
+
argTypes: {
|
|
12
|
+
size: {
|
|
13
|
+
control: 'select',
|
|
14
|
+
options: sizes
|
|
15
|
+
},
|
|
16
|
+
type: {
|
|
17
|
+
control: 'select',
|
|
18
|
+
options: ['search', 'button']
|
|
19
|
+
},
|
|
20
|
+
elevation: { control: 'number' },
|
|
21
|
+
disabled: { control: 'boolean' },
|
|
22
|
+
disabledClear: { control: 'boolean' },
|
|
23
|
+
disabledFocus: { control: 'boolean' },
|
|
24
|
+
disabledHover: { control: 'boolean' },
|
|
25
|
+
placeholder: { control: 'text' },
|
|
26
|
+
defaultValue: { control: 'text' },
|
|
27
|
+
value: { control: 'text' }
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
export default meta;
|
|
31
|
+
export const Default = {
|
|
32
|
+
args: {
|
|
33
|
+
size: 'small',
|
|
34
|
+
elevation: 1
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const Row = ({ title, children }) => (React.createElement("section", { style: { paddingBottom: '24px' } },
|
|
38
|
+
React.createElement("h3", { style: { marginBottom: '12px', marginTop: 0 } }, title),
|
|
39
|
+
React.createElement(Stack, { spacing: 2 }, children)));
|
|
40
|
+
export const Screenshot = {
|
|
41
|
+
tags: ['argos'],
|
|
42
|
+
render: () => (React.createElement(Stack, { spacing: 4, sx: { width: '100%', maxWidth: 800 } },
|
|
43
|
+
React.createElement(Row, { title: "Sizes" }, sizes.map(size => (React.createElement(SearchBar, { key: size, size: size, placeholder: `Size: ${size}` })))),
|
|
44
|
+
React.createElement(Row, { title: "Normal" },
|
|
45
|
+
React.createElement(SearchBar, { size: "small" }),
|
|
46
|
+
React.createElement(SearchBar, { size: "medium", defaultValue: "Filled search" })),
|
|
47
|
+
React.createElement(Row, { title: "With elevation" },
|
|
48
|
+
React.createElement(SearchBar, { size: "small", elevation: 10 })),
|
|
49
|
+
React.createElement(Row, { title: "Disabled" },
|
|
50
|
+
React.createElement(SearchBar, { disabled: true, size: "small" }),
|
|
51
|
+
React.createElement(SearchBar, { disabled: true, size: "small", defaultValue: "Disabled filled search" })),
|
|
52
|
+
React.createElement(Row, { title: "Button type" },
|
|
53
|
+
React.createElement(SearchBar, { type: "button", size: "small", label: React.createElement(Typography, { color: "primary" }, "This is a custom label") }),
|
|
54
|
+
React.createElement(SearchBar, { type: "button", size: "medium", icon: Cloud, label: "Button with string label" })),
|
|
55
|
+
React.createElement(Row, { title: "Options" },
|
|
56
|
+
React.createElement(SearchBar, { size: "small", defaultValue: "Without clear button", disabledClear: true }),
|
|
57
|
+
React.createElement(SearchBar, { size: "small", disabledHover: true, placeholder: "Disabled hover" }),
|
|
58
|
+
React.createElement(SearchBar, { size: "small", disabledFocus: true, placeholder: "Disabled focus" }),
|
|
59
|
+
React.createElement(SearchBar, { size: "small", icon: Cloud, placeholder: "Custom icon" }))))
|
|
60
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare const SearchBarButtonBase: import("@emotion/styled").StyledComponent<import("@mui/material").ButtonBaseOwnProps & Omit<import("@mui/material").ButtonBaseOwnProps, keyof import("@mui/material").ButtonBaseOwnProps> & import("@mui/material/OverridableComponent").CommonProps & Omit<Omit<import("react").DetailedHTMLProps<import("react").ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, "ref"> & {
|
|
2
|
+
ref?: ((instance: HTMLButtonElement | null) => void) | import("react").RefObject<HTMLButtonElement> | null | undefined;
|
|
3
|
+
}, "disabled" | "style" | "className" | "type" | "tabIndex" | "children" | "classes" | "action" | "centerRipple" | "disableRipple" | "disableTouchRipple" | "focusRipple" | "focusVisibleClassName" | "LinkComponent" | "nativeButton" | "onFocusVisible" | "sx" | "TouchRippleProps" | "touchRippleRef"> & import("@mui/system").MUIStyledCommonProps<import("@mui/material").Theme>, {}, {}>;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { SearchBarProps } from './types';
|
|
2
|
+
export declare const SearchBarInputBase: import("@emotion/styled").StyledComponent<import("@mui/material").InputBaseProps & import("@mui/system").MUIStyledCommonProps<import("@mui/material").Theme> & {
|
|
3
|
+
searchSize: NonNullable<SearchBarProps["size"]>;
|
|
4
|
+
hasIcon: boolean;
|
|
5
|
+
}, {}, {}>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { InputBase, styled } from '@mui/material';
|
|
2
|
+
const fontSizeToPixel = { small: 14, medium: 16, large: 16, auto: 16 };
|
|
3
|
+
export const SearchBarInputBase = styled(InputBase, {
|
|
4
|
+
shouldForwardProp: prop => prop !== 'searchSize' && prop !== 'hasIcon'
|
|
5
|
+
})(({ searchSize, hasIcon }) => {
|
|
6
|
+
return {
|
|
7
|
+
flex: 1,
|
|
8
|
+
fontSize: fontSizeToPixel[searchSize],
|
|
9
|
+
paddingLeft: !hasIcon ? '1rem' : undefined
|
|
10
|
+
};
|
|
11
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { SearchBarProps } from './types';
|
|
2
|
+
export declare const SearchBarRoot: import("@emotion/styled").StyledComponent<import("@mui/material").PaperOwnProps & import("@mui/material/OverridableComponent").CommonProps & Omit<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & {
|
|
3
|
+
ref?: ((instance: HTMLDivElement | null) => void) | import("react").RefObject<HTMLDivElement> | null | undefined;
|
|
4
|
+
}, "style" | "square" | "className" | "elevation" | "children" | "classes" | "sx" | "variant"> & import("@mui/system").MUIStyledCommonProps<import("@mui/material").Theme> & {
|
|
5
|
+
searchSize: NonNullable<SearchBarProps["size"]>;
|
|
6
|
+
isFocused: boolean;
|
|
7
|
+
}, {}, {}>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Paper, styled } from '@mui/material';
|
|
2
|
+
const sizeToPixel = { small: 40, medium: 48, large: 56, auto: 'auto' };
|
|
3
|
+
const radiusBySize = { small: 20, medium: 24, large: 28, auto: 24 };
|
|
4
|
+
export const SearchBarRoot = styled(Paper, {
|
|
5
|
+
shouldForwardProp: prop => prop !== 'searchSize' && prop !== 'isFocused'
|
|
6
|
+
})(({ theme, searchSize, isFocused }) => {
|
|
7
|
+
return {
|
|
8
|
+
display: 'flex',
|
|
9
|
+
boxSizing: 'border-box',
|
|
10
|
+
position: 'relative',
|
|
11
|
+
alignItems: 'center',
|
|
12
|
+
height: sizeToPixel[searchSize],
|
|
13
|
+
flex: 'auto',
|
|
14
|
+
borderRadius: radiusBySize[searchSize],
|
|
15
|
+
borderStyle: 'solid',
|
|
16
|
+
borderWidth: 1,
|
|
17
|
+
paddingRight: '0.5rem',
|
|
18
|
+
borderColor: isFocused ? theme.palette.primary.main : 'transparent',
|
|
19
|
+
backgroundColor: isFocused
|
|
20
|
+
? theme.palette.background.paper
|
|
21
|
+
: theme.palette.background.default,
|
|
22
|
+
transition: theme.transitions.create(['border-color', 'background-color']),
|
|
23
|
+
'&:hover': {
|
|
24
|
+
'&:not(.Mui-disabled):not(.SearchBar-focused)': {
|
|
25
|
+
'& .SearchBar-focusHighlight': {
|
|
26
|
+
opacity: 1
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
'&.Mui-disabled': {
|
|
31
|
+
'& .SearchBar-icon': {
|
|
32
|
+
color: theme.palette.text.disabled
|
|
33
|
+
},
|
|
34
|
+
'& .SearchBar-disableHighlight': {
|
|
35
|
+
opacity: 1
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const SearchBarTypography: import("@emotion/styled").StyledComponent<import("@mui/material").TypographyOwnProps & import("@mui/material/OverridableComponent").CommonProps & Omit<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "ref"> & {
|
|
2
|
+
ref?: ((instance: HTMLSpanElement | null) => void) | import("react").RefObject<HTMLSpanElement> | null | undefined;
|
|
3
|
+
}, "style" | "color" | "className" | "children" | "classes" | "sx" | "variant" | "align" | "noWrap" | "gutterBottom" | "variantMapping"> & import("@mui/system").MUIStyledCommonProps<import("@mui/material").Theme> & {
|
|
4
|
+
hasIcon: boolean;
|
|
5
|
+
}, {}, {}>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Typography, styled } from '@mui/material';
|
|
2
|
+
export const SearchBarTypography = styled(Typography, {
|
|
3
|
+
shouldForwardProp: prop => prop !== 'hasIcon'
|
|
4
|
+
})(({ hasIcon }) => ({
|
|
5
|
+
color: 'currentColor',
|
|
6
|
+
opacity: 0.42,
|
|
7
|
+
paddingLeft: !hasIcon ? '1rem' : undefined
|
|
8
|
+
}));
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Cross, Icon, Magnifier } from '@linagora/twake-icons';
|
|
2
|
+
import { IconButton } from '@mui/material';
|
|
3
|
+
import debounce from '@mui/utils/debounce';
|
|
4
|
+
import cx from 'classnames';
|
|
5
|
+
import React, { forwardRef, useState, useMemo, useRef, useEffect } from 'react';
|
|
6
|
+
import { DisableHighlight } from './DisableHighlight';
|
|
7
|
+
import { FocusHighlight } from './FocusHighlight';
|
|
8
|
+
import { SearchBarButtonBase } from './SearchBarButtonBase';
|
|
9
|
+
import { SearchBarIconWrapper } from './SearchBarIconWrapper';
|
|
10
|
+
import { SearchBarInputBase } from './SearchBarInputBase';
|
|
11
|
+
import { SearchBarRoot } from './SearchBarRoot';
|
|
12
|
+
import { SearchBarTypography } from './SearchBarTypography';
|
|
13
|
+
export const SearchBar = forwardRef(({ placeholder, icon = Magnifier, size = 'small', type = 'search', label, componentsProps, disabledClear = false, disabledFocus = false, disabledHover = false, className, defaultValue = '', value, elevation = 1, disabled = false, onChange = () => { }, onClear = () => { }, onFocus = () => { }, onBlur = () => { }, ...props }, ref) => {
|
|
14
|
+
const [currentValue, setCurrentValue] = useState(defaultValue);
|
|
15
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
16
|
+
const spreadValue = typeof value !== 'undefined' ? value : currentValue;
|
|
17
|
+
const isSelfControlledComp = typeof value === 'undefined';
|
|
18
|
+
const onChangeRef = useRef(onChange);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
onChangeRef.current = onChange;
|
|
21
|
+
}, [onChange]);
|
|
22
|
+
const delayedOnChangeRef = useRef();
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
delayedOnChangeRef.current = debounce((ev) => {
|
|
25
|
+
onChangeRef.current(ev);
|
|
26
|
+
}, 375);
|
|
27
|
+
return () => {
|
|
28
|
+
delayedOnChangeRef.current?.clear();
|
|
29
|
+
};
|
|
30
|
+
}, []);
|
|
31
|
+
const delayedOnChange = useMemo(() => {
|
|
32
|
+
const fn = (ev) => {
|
|
33
|
+
delayedOnChangeRef.current?.(ev);
|
|
34
|
+
};
|
|
35
|
+
fn.clear = () => {
|
|
36
|
+
delayedOnChangeRef.current?.clear();
|
|
37
|
+
};
|
|
38
|
+
return fn;
|
|
39
|
+
}, []);
|
|
40
|
+
const handleChange = (ev) => {
|
|
41
|
+
if (!isSelfControlledComp)
|
|
42
|
+
return onChange(ev);
|
|
43
|
+
const _value = ev.target.value;
|
|
44
|
+
delayedOnChange(ev);
|
|
45
|
+
setCurrentValue(_value);
|
|
46
|
+
};
|
|
47
|
+
const handleClear = (ev) => {
|
|
48
|
+
delayedOnChange.clear();
|
|
49
|
+
const mockEvent = {
|
|
50
|
+
...ev,
|
|
51
|
+
target: { ...ev.target, value: '' }
|
|
52
|
+
};
|
|
53
|
+
onChange(mockEvent);
|
|
54
|
+
onClear(ev);
|
|
55
|
+
if (isSelfControlledComp)
|
|
56
|
+
setCurrentValue('');
|
|
57
|
+
};
|
|
58
|
+
const handleFocus = (ev) => {
|
|
59
|
+
onFocus(ev);
|
|
60
|
+
setIsFocused(true);
|
|
61
|
+
};
|
|
62
|
+
const handleBlur = (ev) => {
|
|
63
|
+
onBlur(ev);
|
|
64
|
+
setIsFocused(false);
|
|
65
|
+
};
|
|
66
|
+
const effectivelyFocused = isFocused && !disabledFocus;
|
|
67
|
+
return (React.createElement(SearchBarRoot, { component: "form", elevation: elevation, searchSize: size, isFocused: effectivelyFocused, className: cx(className, {
|
|
68
|
+
'SearchBar-focused': effectivelyFocused,
|
|
69
|
+
'Mui-disabled': disabled
|
|
70
|
+
}), ref: ref, onSubmit: (ev) => {
|
|
71
|
+
ev.preventDefault();
|
|
72
|
+
props.onSubmit?.(ev);
|
|
73
|
+
}, ...props },
|
|
74
|
+
type === 'button' ? (React.createElement(SearchBarButtonBase, { disabled: disabled, "aria-label": typeof label === 'string' ? label : 'Search' },
|
|
75
|
+
icon && (React.createElement(SearchBarIconWrapper, { className: "SearchBar-icon" },
|
|
76
|
+
React.createElement(Icon, { icon: icon }))),
|
|
77
|
+
typeof label === 'string' ? (React.createElement(SearchBarTypography, { hasIcon: !!icon }, label)) : (label))) : (React.createElement(React.Fragment, null,
|
|
78
|
+
icon && (React.createElement(SearchBarIconWrapper, { className: "SearchBar-icon" },
|
|
79
|
+
React.createElement(Icon, { icon: icon }))),
|
|
80
|
+
React.createElement(SearchBarInputBase, { ...componentsProps?.inputBase, className: cx(componentsProps?.inputBase?.className), searchSize: size, hasIcon: !!icon, placeholder: disabled ? undefined : placeholder, value: disabled ? placeholder : spreadValue, disabled: disabled, "aria-label": placeholder || 'Search', onChange: handleChange, onFocus: handleFocus, onBlur: handleBlur }))),
|
|
81
|
+
spreadValue && !disabledClear && type === 'search' && !disabled && (React.createElement(IconButton, { size: "small", onClick: handleClear, "aria-label": "Clear search" },
|
|
82
|
+
React.createElement(Icon, { icon: Cross }))),
|
|
83
|
+
!disabledHover && !disabled && (React.createElement(FocusHighlight, { className: "SearchBar-focusHighlight" })),
|
|
84
|
+
disabled && (React.createElement(DisableHighlight, { className: "SearchBar-disableHighlight" }))));
|
|
85
|
+
});
|
|
86
|
+
SearchBar.displayName = 'SearchBar';
|
|
87
|
+
export default SearchBar;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { InputBaseProps, PaperProps } from '@mui/material';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
export interface SearchBarProps extends Omit<PaperProps, 'onChange' | 'onBlur' | 'onFocus'> {
|
|
4
|
+
type?: 'button' | 'search';
|
|
5
|
+
size?: 'small' | 'medium' | 'large' | 'auto';
|
|
6
|
+
icon?: React.ElementType;
|
|
7
|
+
componentsProps?: {
|
|
8
|
+
inputBase?: InputBaseProps;
|
|
9
|
+
};
|
|
10
|
+
value?: string;
|
|
11
|
+
defaultValue?: string;
|
|
12
|
+
disabledClear?: boolean;
|
|
13
|
+
disabledFocus?: boolean;
|
|
14
|
+
disabledHover?: boolean;
|
|
15
|
+
elevation?: number;
|
|
16
|
+
placeholder?: string;
|
|
17
|
+
label?: React.ReactNode;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
|
20
|
+
onFocus?: React.FocusEventHandler<HTMLInputElement>;
|
|
21
|
+
onClear?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
|
22
|
+
onBlur?: React.FocusEventHandler<HTMLInputElement>;
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -10,8 +10,11 @@ export { Avatar, default as AvatarDefault } from './components/Avatar';
|
|
|
10
10
|
export { Button, default as ButtonDefault } from './components/Button';
|
|
11
11
|
export { default as Chip } from './components/Chip';
|
|
12
12
|
export { Dialog, default as DialogDefault } from './components/Dialog';
|
|
13
|
+
export { ContactPopover, default as ContactPopoverDefault } from './components/ContactPopover';
|
|
14
|
+
export { default as DropdownText } from './components/DropdownText';
|
|
13
15
|
export { Tabs, default as TabsDefault } from './components/Tabs';
|
|
14
16
|
export { Switch, default as SwitchDefault } from './components/Switch';
|
|
17
|
+
export { SearchBar, default as SearchBarDefault } from './components/SearchBar';
|
|
15
18
|
export { Sidebar, SIDEBAR_MOBILE_HEIGHT } from './components/Sidebar';
|
|
16
19
|
export { Layout, Main, Content } from './components/Layout';
|
|
17
20
|
export { Nav } from './components/Nav';
|
|
@@ -21,7 +24,7 @@ export { NavIcon } from './components/NavIcon';
|
|
|
21
24
|
export { NavText } from './components/NavText';
|
|
22
25
|
export { NavDesktopLimiter } from './components/NavDesktopLimiter';
|
|
23
26
|
export { NavDesktopDropdown } from './components/NavDesktopDropdown';
|
|
24
|
-
export { nameToColor, supportedColors } from './components/Avatar/helpers';
|
|
27
|
+
export { nameToColor, supportedColors, getInitials } from './components/Avatar/helpers';
|
|
25
28
|
export { default as AccordionExpandIcon } from './components/AccordionExpandIcon';
|
|
26
29
|
export { default as VirtualizedTable } from './components/VirtualizedTable';
|
|
27
30
|
export { VirtuosoMockContext } from 'react-virtuoso';
|
|
@@ -30,10 +33,19 @@ export type { TwakeTheme } from './lib/theme';
|
|
|
30
33
|
export type { PaletteJson } from './lib/types';
|
|
31
34
|
export type { AvatarProps } from './components/Avatar';
|
|
32
35
|
export type { ButtonProps } from './components/Button';
|
|
36
|
+
export type { ContactPopoverProps } from './components/ContactPopover';
|
|
37
|
+
export { ContactPopoverActions, ContactPopoverChatAction, ContactPopoverVideoAction, ContactPopoverCalendarAction, ContactPopoverEmailAction } from './components/ContactPopover';
|
|
38
|
+
export type { ContactPopoverActionsProps } from './components/ContactPopover/ContactPopoverActions';
|
|
39
|
+
export type { ContactPopoverChatActionProps } from './components/ContactPopover/ContactPopoverChatAction';
|
|
40
|
+
export type { ContactPopoverVideoActionProps } from './components/ContactPopover/ContactPopoverVideoAction';
|
|
41
|
+
export type { ContactPopoverCalendarActionProps } from './components/ContactPopover/ContactPopoverCalendarAction';
|
|
42
|
+
export type { ContactPopoverEmailActionProps } from './components/ContactPopover/ContactPopoverEmailAction';
|
|
33
43
|
export type { ChipProps } from './components/Chip';
|
|
34
44
|
export type { DialogProps, DialogSize } from './components/Dialog';
|
|
45
|
+
export type { DropdownTextProps, DropdownTextVariant } from './components/DropdownText';
|
|
35
46
|
export type { TabsProps } from './components/Tabs';
|
|
36
47
|
export type { SwitchProps } from './components/Switch';
|
|
48
|
+
export type { SearchBarProps } from './components/SearchBar/types';
|
|
37
49
|
export type { SidebarProps } from './components/Sidebar';
|
|
38
50
|
export type { LayoutProps, MainProps, ContentProps } from './components/Layout';
|
|
39
51
|
export type { NavProps } from './components/Nav';
|
package/dist/index.js
CHANGED
|
@@ -13,8 +13,11 @@ export { Avatar, default as AvatarDefault } from './components/Avatar';
|
|
|
13
13
|
export { Button, default as ButtonDefault } from './components/Button';
|
|
14
14
|
export { default as Chip } from './components/Chip';
|
|
15
15
|
export { Dialog, default as DialogDefault } from './components/Dialog';
|
|
16
|
+
export { ContactPopover, default as ContactPopoverDefault } from './components/ContactPopover';
|
|
17
|
+
export { default as DropdownText } from './components/DropdownText';
|
|
16
18
|
export { Tabs, default as TabsDefault } from './components/Tabs';
|
|
17
19
|
export { Switch, default as SwitchDefault } from './components/Switch';
|
|
20
|
+
export { SearchBar, default as SearchBarDefault } from './components/SearchBar';
|
|
18
21
|
export { Sidebar, SIDEBAR_MOBILE_HEIGHT } from './components/Sidebar';
|
|
19
22
|
export { Layout, Main, Content } from './components/Layout';
|
|
20
23
|
export { Nav } from './components/Nav';
|
|
@@ -24,8 +27,9 @@ export { NavIcon } from './components/NavIcon';
|
|
|
24
27
|
export { NavText } from './components/NavText';
|
|
25
28
|
export { NavDesktopLimiter } from './components/NavDesktopLimiter';
|
|
26
29
|
export { NavDesktopDropdown } from './components/NavDesktopDropdown';
|
|
27
|
-
export { nameToColor, supportedColors } from './components/Avatar/helpers';
|
|
30
|
+
export { nameToColor, supportedColors, getInitials } from './components/Avatar/helpers';
|
|
28
31
|
export { default as AccordionExpandIcon } from './components/AccordionExpandIcon';
|
|
29
32
|
export { default as VirtualizedTable } from './components/VirtualizedTable';
|
|
30
33
|
export { VirtuosoMockContext } from 'react-virtuoso';
|
|
31
34
|
export { useBreakpoints } from './hooks/useBreakpoints';
|
|
35
|
+
export { ContactPopoverActions, ContactPopoverChatAction, ContactPopoverVideoAction, ContactPopoverCalendarAction, ContactPopoverEmailAction } from './components/ContactPopover';
|