@dtdot/lego 2.3.3 → 2.5.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/build/components/Badge/Badge.component.d.ts +1 -2
- package/build/components/Badge/Badge.component.js +19 -9
- package/build/components/BadgeSelector/BadgeSelector.component.js +1 -1
- package/build/components/FloatingActionButton/FloatingActionButton.component.d.ts +10 -1
- package/build/components/FloatingActionButton/FloatingActionButton.component.js +21 -6
- package/build/components/FloatingActionButton/_FloatingActionButton.context.d.ts +12 -2
- package/build/components/FloatingActionButton/_FloatingActionButton.context.js +2 -1
- package/build/components/FloatingActionButton/_FloatingActionButton.internal.d.ts +13 -1
- package/build/components/FloatingActionButton/_FloatingActionButton.internal.js +89 -15
- package/build/components/FloatingActionButton/_FloatingActionButton.provider.js +18 -6
- package/build/components/TagSelect/TagSelect.component.d.ts +22 -0
- package/build/components/TagSelect/TagSelect.component.js +172 -0
- package/build/index.d.ts +1 -0
- package/build/index.js +1 -0
- package/package.json +1 -1
|
@@ -3,7 +3,6 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core';
|
|
|
3
3
|
import { Status } from '../../theme/theme.types';
|
|
4
4
|
interface BadgeSpanProps {
|
|
5
5
|
variant: BadgeVariant;
|
|
6
|
-
useHover: boolean;
|
|
7
6
|
}
|
|
8
7
|
export declare const BadgeSpan: import("styled-components").StyledComponent<"span", import("styled-components").DefaultTheme, BadgeSpanProps, never>;
|
|
9
8
|
export type BadgeVariant = Status;
|
|
@@ -11,7 +10,7 @@ export interface BadgeProps {
|
|
|
11
10
|
children: React.ReactNode;
|
|
12
11
|
variant: BadgeVariant;
|
|
13
12
|
actionIcon?: IconProp;
|
|
14
|
-
onAction?: () => void;
|
|
13
|
+
onAction?: (event: React.MouseEvent) => void;
|
|
15
14
|
}
|
|
16
15
|
declare const Badge: ({ children, variant, actionIcon, onAction }: BadgeProps) => JSX.Element;
|
|
17
16
|
export default Badge;
|
|
@@ -5,6 +5,8 @@ import getThemeStatusColour from '../../theme/helpers/getThemeStatusColour';
|
|
|
5
5
|
export const BadgeSpan = styled.span `
|
|
6
6
|
padding: 4px 8px;
|
|
7
7
|
border-radius: 2px;
|
|
8
|
+
display: inline-flex;
|
|
9
|
+
align-items: center;
|
|
8
10
|
|
|
9
11
|
background-color: ${(props) => getThemeStatusColour(props.variant, props.theme).main};
|
|
10
12
|
color: ${(props) => getThemeStatusColour(props.variant, props.theme).contrast};
|
|
@@ -15,23 +17,31 @@ export const BadgeSpan = styled.span `
|
|
|
15
17
|
line-height: ${(props) => props.theme.fonts.default.size};
|
|
16
18
|
|
|
17
19
|
text-transform: lowercase;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
20
|
+
`;
|
|
21
|
+
const TextSpan = styled.span `
|
|
22
|
+
cursor: default;
|
|
22
23
|
`;
|
|
23
24
|
const ActionSpan = styled.span `
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
display: inline-flex;
|
|
26
|
+
align-items: center;
|
|
27
|
+
justify-content: center;
|
|
28
|
+
padding: 4px 6px;
|
|
29
|
+
margin: -4px -8px -4px 6px;
|
|
27
30
|
cursor: pointer;
|
|
28
31
|
user-select: none;
|
|
32
|
+
border-top-right-radius: 2px;
|
|
33
|
+
border-bottom-right-radius: 2px;
|
|
29
34
|
|
|
35
|
+
background-color: rgba(0, 0, 0, 0.15);
|
|
30
36
|
font-size: ${(props) => props.theme.fonts.default.size};
|
|
37
|
+
|
|
38
|
+
&:hover {
|
|
39
|
+
background-color: rgba(0, 0, 0, 0.25);
|
|
40
|
+
}
|
|
31
41
|
`;
|
|
32
42
|
const Badge = ({ children, variant, actionIcon, onAction }) => {
|
|
33
|
-
return (React.createElement(BadgeSpan, { variant: variant,
|
|
34
|
-
children,
|
|
43
|
+
return (React.createElement(BadgeSpan, { variant: variant, "data-testid": 'badge' },
|
|
44
|
+
React.createElement(TextSpan, null, children),
|
|
35
45
|
actionIcon && (React.createElement(ActionSpan, { onClick: onAction },
|
|
36
46
|
React.createElement(FontAwesomeIcon, { icon: actionIcon })))));
|
|
37
47
|
};
|
|
@@ -24,6 +24,6 @@ const BadgeSelector = ({ options, value, onChange }) => {
|
|
|
24
24
|
const handleClick = (_value) => {
|
|
25
25
|
onChange([_value]);
|
|
26
26
|
};
|
|
27
|
-
return (React.createElement(BadgeSelectorOuter, { "data-testid": 'badge-selector' }, options.map((option) => (React.createElement(InteractiveBadge, {
|
|
27
|
+
return (React.createElement(BadgeSelectorOuter, { "data-testid": 'badge-selector' }, options.map((option) => (React.createElement(InteractiveBadge, { key: option.value, variant: option.variant, inactive: !value.includes(option.value), onClick: () => handleClick(option.value), "data-testid": value.includes(option.value) ? 'badge-selected' : 'badge' }, option.name)))));
|
|
28
28
|
};
|
|
29
29
|
export default BadgeSelector;
|
|
@@ -5,10 +5,19 @@ interface FloatingActionButtonProps {
|
|
|
5
5
|
'icon': IconDefinition;
|
|
6
6
|
'onClick': () => void;
|
|
7
7
|
'variant'?: ColourVariant;
|
|
8
|
+
'label'?: string;
|
|
8
9
|
'data-testid'?: string;
|
|
9
10
|
}
|
|
10
11
|
declare const FloatingActionButton: {
|
|
11
|
-
({ icon, onClick, variant, "data-testid": dataTestId, }: FloatingActionButtonProps): JSX.Element | null;
|
|
12
|
+
({ icon, onClick, variant, label, "data-testid": dataTestId, }: FloatingActionButtonProps): JSX.Element | null;
|
|
12
13
|
Provider: ({ children }: import("./_FloatingActionButton.provider").FloatingActionButtonProviderProps) => JSX.Element;
|
|
14
|
+
Secondary: ({ icon, onClick, variant, label, "data-testid": dataTestId, }: SecondaryFloatingActionButtonProps) => null;
|
|
13
15
|
};
|
|
16
|
+
interface SecondaryFloatingActionButtonProps {
|
|
17
|
+
'icon': IconDefinition;
|
|
18
|
+
'onClick': () => void;
|
|
19
|
+
'variant'?: ColourVariant;
|
|
20
|
+
'label'?: string;
|
|
21
|
+
'data-testid'?: string;
|
|
22
|
+
}
|
|
14
23
|
export default FloatingActionButton;
|
|
@@ -3,19 +3,34 @@ import FloatingActionButtonContext from './_FloatingActionButton.context';
|
|
|
3
3
|
import FloatingActionButtonProvider from './_FloatingActionButton.provider';
|
|
4
4
|
import { v4 } from 'uuid';
|
|
5
5
|
import FloatingActionButtonInternal from './_FloatingActionButton.internal';
|
|
6
|
-
const FloatingActionButton = ({ icon, onClick, variant = 'primary', 'data-testid': dataTestId, }) => {
|
|
7
|
-
const { contextExists,
|
|
6
|
+
const FloatingActionButton = ({ icon, onClick, variant = 'primary', label, 'data-testid': dataTestId, }) => {
|
|
7
|
+
const { contextExists, setPrimaryButton } = useContext(FloatingActionButtonContext);
|
|
8
8
|
const id = useMemo(() => v4(), []);
|
|
9
9
|
useEffect(() => {
|
|
10
|
-
|
|
10
|
+
setPrimaryButton({ id, icon, onClick, variant, label, dataTestId });
|
|
11
11
|
return () => {
|
|
12
|
-
|
|
12
|
+
setPrimaryButton(undefined);
|
|
13
13
|
};
|
|
14
|
-
}, [icon, onClick, variant, dataTestId,
|
|
14
|
+
}, [icon, onClick, variant, label, dataTestId, setPrimaryButton, id]);
|
|
15
15
|
if (!contextExists) {
|
|
16
|
-
return React.createElement(FloatingActionButtonInternal, { icon: icon, onClick: onClick, variant: variant, "data-testid": dataTestId });
|
|
16
|
+
return (React.createElement(FloatingActionButtonInternal, { icon: icon, onClick: onClick, variant: variant, label: label, "data-testid": dataTestId }));
|
|
17
17
|
}
|
|
18
18
|
return null;
|
|
19
19
|
};
|
|
20
|
+
const SecondaryFloatingActionButton = ({ icon, onClick, variant = 'primary', label, 'data-testid': dataTestId, }) => {
|
|
21
|
+
const { setSecondaryButtons } = useContext(FloatingActionButtonContext);
|
|
22
|
+
const id = useMemo(() => v4(), []);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
// Register this secondary FAB
|
|
25
|
+
setSecondaryButtons((prev) => [...prev, { id, icon, onClick, variant, label, dataTestId }]);
|
|
26
|
+
return () => {
|
|
27
|
+
// Unregister this secondary FAB
|
|
28
|
+
setSecondaryButtons((prev) => prev.filter((fab) => fab.id !== id));
|
|
29
|
+
};
|
|
30
|
+
}, [icon, onClick, variant, label, dataTestId, setSecondaryButtons, id]);
|
|
31
|
+
// Secondary FABs always return null - rendered by provider
|
|
32
|
+
return null;
|
|
33
|
+
};
|
|
20
34
|
FloatingActionButton.Provider = FloatingActionButtonProvider;
|
|
35
|
+
FloatingActionButton.Secondary = SecondaryFloatingActionButton;
|
|
21
36
|
export default FloatingActionButton;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import { Dispatch, SetStateAction } from 'react';
|
|
2
2
|
import { IconDefinition } from '@fortawesome/fontawesome-svg-core';
|
|
3
3
|
import { ColourVariant } from '../../theme/theme.types';
|
|
4
4
|
export interface FabProps {
|
|
@@ -6,11 +6,21 @@ export interface FabProps {
|
|
|
6
6
|
icon: IconDefinition | null;
|
|
7
7
|
onClick: () => void;
|
|
8
8
|
variant?: ColourVariant;
|
|
9
|
+
label?: string;
|
|
10
|
+
dataTestId?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface SecondaryFabProps {
|
|
13
|
+
id: string;
|
|
14
|
+
icon: IconDefinition;
|
|
15
|
+
onClick: () => void;
|
|
16
|
+
variant?: ColourVariant;
|
|
17
|
+
label?: string;
|
|
9
18
|
dataTestId?: string;
|
|
10
19
|
}
|
|
11
20
|
interface FloatingActionButtonContextProps {
|
|
12
21
|
contextExists: boolean;
|
|
13
|
-
|
|
22
|
+
setPrimaryButton: Dispatch<SetStateAction<FabProps | undefined>>;
|
|
23
|
+
setSecondaryButtons: Dispatch<SetStateAction<SecondaryFabProps[]>>;
|
|
14
24
|
}
|
|
15
25
|
declare const FloatingActionButtonContext: import("react").Context<FloatingActionButtonContextProps>;
|
|
16
26
|
export default FloatingActionButtonContext;
|
|
@@ -6,6 +6,18 @@ interface FloatingActionButtonInternalProps {
|
|
|
6
6
|
'onClick': () => void;
|
|
7
7
|
'variant'?: ColourVariant;
|
|
8
8
|
'data-testid'?: string;
|
|
9
|
+
'label'?: string;
|
|
9
10
|
}
|
|
10
|
-
declare const FloatingActionButtonInternal: (
|
|
11
|
+
declare const FloatingActionButtonInternal: (props: FloatingActionButtonInternalProps) => JSX.Element;
|
|
12
|
+
interface MiniFabInternalProps {
|
|
13
|
+
'icon': IconDefinition;
|
|
14
|
+
'onClick': () => void;
|
|
15
|
+
'variant'?: ColourVariant;
|
|
16
|
+
'label'?: string;
|
|
17
|
+
'bottom': number;
|
|
18
|
+
'staggerDelay': number;
|
|
19
|
+
'data-testid'?: string;
|
|
20
|
+
}
|
|
21
|
+
declare const MiniFabInternal: ({ bottom, staggerDelay, ...rest }: MiniFabInternalProps) => JSX.Element;
|
|
11
22
|
export default FloatingActionButtonInternal;
|
|
23
|
+
export { MiniFabInternal };
|
|
@@ -1,17 +1,42 @@
|
|
|
1
1
|
import React, { useContext } from 'react';
|
|
2
2
|
import styled from 'styled-components';
|
|
3
|
-
import { motion } from 'framer-motion';
|
|
3
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
4
4
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
5
5
|
import getThemeVariantColours from '../../theme/helpers/getThemeVariantColours';
|
|
6
6
|
import MinimalMenuContext from '../MinimalMenu/MinimalMenu.context';
|
|
7
7
|
import zIndexConstants from '../../constants/zIndex.constants';
|
|
8
8
|
import FloatingActionButtonContext from './_FloatingActionButton.context';
|
|
9
|
-
const
|
|
9
|
+
const FAB_SIZES = {
|
|
10
|
+
primary: {
|
|
11
|
+
width: 56,
|
|
12
|
+
height: 56,
|
|
13
|
+
fontSize: 24,
|
|
14
|
+
labelOffset: 68,
|
|
15
|
+
hoverScale: 1.1,
|
|
16
|
+
tapScale: 0.95,
|
|
17
|
+
},
|
|
18
|
+
mini: {
|
|
19
|
+
width: 40,
|
|
20
|
+
height: 40,
|
|
21
|
+
fontSize: 18,
|
|
22
|
+
labelOffset: 52,
|
|
23
|
+
hoverScale: 1.15,
|
|
24
|
+
tapScale: 0.9,
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
const FabContainer = styled(motion.div) `
|
|
10
28
|
position: fixed;
|
|
11
|
-
bottom: ${(props) =>
|
|
29
|
+
bottom: ${(props) => {
|
|
30
|
+
if (props.bottom !== undefined)
|
|
31
|
+
return `${props.bottom}px`;
|
|
32
|
+
return props.offsetBottom ? '76px' : '20px';
|
|
33
|
+
}};
|
|
12
34
|
right: 20px;
|
|
13
|
-
|
|
14
|
-
|
|
35
|
+
z-index: ${zIndexConstants.floatingActionButton};
|
|
36
|
+
`;
|
|
37
|
+
const FabButton = styled(motion.button) `
|
|
38
|
+
width: ${(props) => FAB_SIZES[props.size].width}px;
|
|
39
|
+
height: ${(props) => FAB_SIZES[props.size].height}px;
|
|
15
40
|
border-radius: 50%;
|
|
16
41
|
background-color: ${(props) => getThemeVariantColours(props.variant, props.theme).main};
|
|
17
42
|
color: ${(props) => getThemeVariantColours(props.variant, props.theme).contrastText};
|
|
@@ -21,25 +46,74 @@ const FloatingButton = styled(motion.button) `
|
|
|
21
46
|
display: flex;
|
|
22
47
|
justify-content: center;
|
|
23
48
|
align-items: center;
|
|
24
|
-
font-size:
|
|
49
|
+
font-size: ${(props) => FAB_SIZES[props.size].fontSize}px;
|
|
25
50
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
|
|
26
|
-
z-index: ${zIndexConstants.floatingActionButton};
|
|
27
51
|
|
|
28
52
|
&:hover {
|
|
29
53
|
background-color: ${(props) => getThemeVariantColours(props.variant, props.theme).darker};
|
|
30
54
|
}
|
|
31
55
|
`;
|
|
32
|
-
const
|
|
33
|
-
|
|
56
|
+
const FabLabel = styled(motion.div) `
|
|
57
|
+
position: absolute;
|
|
58
|
+
right: ${(props) => FAB_SIZES[props.size].labelOffset}px;
|
|
59
|
+
top: 50%;
|
|
60
|
+
transform: translateY(-50%);
|
|
61
|
+
|
|
62
|
+
padding: 6px 12px;
|
|
63
|
+
border-radius: 4px;
|
|
64
|
+
|
|
65
|
+
background-color: ${(props) => props.theme.colours.tertiary.main};
|
|
66
|
+
color: ${(props) => props.theme.colours.defaultFont};
|
|
67
|
+
|
|
68
|
+
font-family: ${(props) => props.theme.fonts.default.family};
|
|
69
|
+
font-size: ${(props) => props.theme.fonts.default.size};
|
|
70
|
+
font-weight: ${(props) => props.theme.fonts.default.weight};
|
|
71
|
+
|
|
72
|
+
white-space: nowrap;
|
|
73
|
+
pointer-events: none;
|
|
74
|
+
box-shadow: ${(props) => props.theme.shadows.medium};
|
|
75
|
+
`;
|
|
76
|
+
const BaseFab = ({ icon, onClick, variant = 'primary', label, size, bottom, offsetBottom, staggerDelay = 0, 'data-testid': dataTestId, }) => {
|
|
77
|
+
const [showLabel, setShowLabel] = React.useState(false);
|
|
34
78
|
const { contextExists } = useContext(FloatingActionButtonContext);
|
|
79
|
+
const sizeConfig = FAB_SIZES[size];
|
|
35
80
|
const variants = {
|
|
36
81
|
hidden: { opacity: 0, scale: 0 },
|
|
37
|
-
visible: {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
82
|
+
visible: {
|
|
83
|
+
opacity: 1,
|
|
84
|
+
scale: 1,
|
|
85
|
+
transition: {
|
|
86
|
+
duration: 0.3,
|
|
87
|
+
delay: staggerDelay,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
exit: {
|
|
91
|
+
opacity: 0,
|
|
92
|
+
scale: 0,
|
|
93
|
+
transition: {
|
|
94
|
+
duration: 0.3,
|
|
95
|
+
delay: staggerDelay,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
hover: { scale: sizeConfig.hoverScale },
|
|
99
|
+
tap: { scale: sizeConfig.tapScale },
|
|
41
100
|
};
|
|
42
|
-
|
|
43
|
-
|
|
101
|
+
const labelVariants = {
|
|
102
|
+
hidden: { opacity: 0, x: 10 },
|
|
103
|
+
visible: { opacity: 1, x: 0 },
|
|
104
|
+
};
|
|
105
|
+
return (React.createElement(FabContainer, { bottom: bottom, offsetBottom: offsetBottom, initial: contextExists ? 'hidden' : 'visible', animate: 'visible', exit: 'exit', variants: variants, onHoverStart: () => setShowLabel(true), onHoverEnd: () => setShowLabel(false) },
|
|
106
|
+
React.createElement(FabButton, { animate: 'visible', whileHover: 'hover', whileTap: 'tap', variants: variants, onClick: onClick, variant: variant, size: size, "data-testid": dataTestId },
|
|
107
|
+
React.createElement(FontAwesomeIcon, { icon: icon })),
|
|
108
|
+
label && (React.createElement(AnimatePresence, null, showLabel && (React.createElement(FabLabel, { size: size, initial: 'hidden', animate: 'visible', exit: 'hidden', variants: labelVariants, transition: { type: 'spring', duration: 0.3 } }, label))))));
|
|
109
|
+
};
|
|
110
|
+
const FloatingActionButtonInternal = (props) => {
|
|
111
|
+
const { menuExists, isMobile } = useContext(MinimalMenuContext);
|
|
112
|
+
const { contextExists } = useContext(FloatingActionButtonContext);
|
|
113
|
+
return (React.createElement(BaseFab, { ...props, size: 'primary', offsetBottom: menuExists && isMobile, staggerDelay: contextExists ? 0.2 : 0 }));
|
|
114
|
+
};
|
|
115
|
+
const MiniFabInternal = ({ bottom, staggerDelay, ...rest }) => {
|
|
116
|
+
return React.createElement(BaseFab, { ...rest, size: 'mini', bottom: bottom, staggerDelay: staggerDelay });
|
|
44
117
|
};
|
|
45
118
|
export default FloatingActionButtonInternal;
|
|
119
|
+
export { MiniFabInternal };
|
|
@@ -1,15 +1,27 @@
|
|
|
1
|
-
import React, { useMemo, useState } from 'react';
|
|
1
|
+
import React, { useContext, useMemo, useState } from 'react';
|
|
2
2
|
import { AnimatePresence } from 'framer-motion';
|
|
3
3
|
import FloatingActionButtonContext from './_FloatingActionButton.context';
|
|
4
|
-
import FloatingActionButtonInternal from './_FloatingActionButton.internal';
|
|
4
|
+
import FloatingActionButtonInternal, { MiniFabInternal } from './_FloatingActionButton.internal';
|
|
5
|
+
import MinimalMenuContext from '../MinimalMenu/MinimalMenu.context';
|
|
5
6
|
const FloatingActionButtonProvider = ({ children }) => {
|
|
6
|
-
const [
|
|
7
|
+
const [primaryFab, setPrimaryFab] = useState();
|
|
8
|
+
const [secondaryFabs, setSecondaryFabs] = useState([]);
|
|
9
|
+
const { menuExists, isMobile } = useContext(MinimalMenuContext);
|
|
7
10
|
const contextVal = useMemo(() => ({
|
|
8
11
|
contextExists: true,
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
setPrimaryButton: setPrimaryFab,
|
|
13
|
+
setSecondaryButtons: setSecondaryFabs,
|
|
14
|
+
}), []);
|
|
11
15
|
return (React.createElement(FloatingActionButtonContext.Provider, { value: contextVal },
|
|
12
16
|
children,
|
|
13
|
-
React.createElement(AnimatePresence, null,
|
|
17
|
+
React.createElement(AnimatePresence, null,
|
|
18
|
+
primaryFab?.icon && (React.createElement(FloatingActionButtonInternal, { key: primaryFab.id, icon: primaryFab.icon, onClick: primaryFab.onClick, variant: primaryFab.variant, label: primaryFab.label, "data-testid": primaryFab.dataTestId })),
|
|
19
|
+
secondaryFabs.map((fab, index) => {
|
|
20
|
+
const offsetBottom = menuExists && isMobile;
|
|
21
|
+
const baseOffset = offsetBottom ? 76 : 20;
|
|
22
|
+
const bottom = baseOffset + 56 + 8 + index * (40 + 10);
|
|
23
|
+
const staggerDelay = 0.2 + 0.05 * (index + 1);
|
|
24
|
+
return (React.createElement(MiniFabInternal, { key: fab.id, icon: fab.icon, onClick: fab.onClick, variant: fab.variant, label: fab.label, bottom: bottom, staggerDelay: staggerDelay, "data-testid": fab.dataTestId }));
|
|
25
|
+
}))));
|
|
14
26
|
};
|
|
15
27
|
export default FloatingActionButtonProvider;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import { Status } from '../../theme/theme.types';
|
|
3
|
+
export interface TagOption {
|
|
4
|
+
value: string;
|
|
5
|
+
label: string;
|
|
6
|
+
variant: Status;
|
|
7
|
+
}
|
|
8
|
+
export interface ITagSelectProps {
|
|
9
|
+
'name'?: string;
|
|
10
|
+
'label'?: string;
|
|
11
|
+
'description'?: string;
|
|
12
|
+
'placeholder'?: string;
|
|
13
|
+
'value'?: string[];
|
|
14
|
+
'onChange'?: (value: string[]) => void;
|
|
15
|
+
'data-testid'?: string;
|
|
16
|
+
'options': TagOption[];
|
|
17
|
+
'className'?: string;
|
|
18
|
+
'allowNewTags'?: boolean;
|
|
19
|
+
'newTagVariant'?: Status;
|
|
20
|
+
}
|
|
21
|
+
declare const TagSelect: (props: ITagSelectProps) => JSX.Element;
|
|
22
|
+
export default TagSelect;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
2
|
+
import React, { Fragment, useState, useEffect, useCallback, useRef } from 'react';
|
|
3
|
+
import { faChevronDown, faChevronUp, faTimes, faPlus } from '@fortawesome/free-solid-svg-icons';
|
|
4
|
+
import styled from 'styled-components';
|
|
5
|
+
import ControlDescription from '../../shared/ControlDescription';
|
|
6
|
+
import ControlLabel from '../../shared/ControlLabel';
|
|
7
|
+
import { ControlStyles } from '../../shared/ControlStyles';
|
|
8
|
+
import getThemeControlColours from '../../theme/helpers/getThemeControlColours';
|
|
9
|
+
import useFormNode, { getValue } from '../Form/useFormNode.hook';
|
|
10
|
+
import Badge from '../Badge/Badge.component';
|
|
11
|
+
const ControlOuter = styled.div `
|
|
12
|
+
position: relative;
|
|
13
|
+
`;
|
|
14
|
+
const TagSelectControl = styled.div `
|
|
15
|
+
${ControlStyles}
|
|
16
|
+
cursor: pointer;
|
|
17
|
+
min-height: 48px;
|
|
18
|
+
height: auto;
|
|
19
|
+
`;
|
|
20
|
+
const TagsContainer = styled.div `
|
|
21
|
+
display: flex;
|
|
22
|
+
flex-wrap: wrap;
|
|
23
|
+
gap: 8px;
|
|
24
|
+
align-items: center;
|
|
25
|
+
min-height: 48px;
|
|
26
|
+
`;
|
|
27
|
+
const IconContainer = styled.div `
|
|
28
|
+
position: absolute;
|
|
29
|
+
right: 0;
|
|
30
|
+
top: 0;
|
|
31
|
+
height: 48px;
|
|
32
|
+
padding: 0 16px;
|
|
33
|
+
display: flex;
|
|
34
|
+
align-items: center;
|
|
35
|
+
`;
|
|
36
|
+
const PlaceholderText = styled.div `
|
|
37
|
+
color: ${(props) => getThemeControlColours(props.theme).placeholder};
|
|
38
|
+
padding: 12px 0;
|
|
39
|
+
`;
|
|
40
|
+
const OptionsContainer = styled.div `
|
|
41
|
+
position: absolute;
|
|
42
|
+
width: 100%;
|
|
43
|
+
background-color: ${(props) => props.theme.colours.controlBackground};
|
|
44
|
+
z-index: 10000;
|
|
45
|
+
box-shadow: ${(props) => props.theme.shadows.small};
|
|
46
|
+
max-height: 270px;
|
|
47
|
+
overflow-y: auto;
|
|
48
|
+
margin-top: 4px;
|
|
49
|
+
`;
|
|
50
|
+
const Option = styled.div `
|
|
51
|
+
color: ${(props) => getThemeControlColours(props.theme).font};
|
|
52
|
+
background-color: ${(props) => props.selected ? props.theme.colours.controlBorder : props.theme.colours.controlBackgroundDisabled};
|
|
53
|
+
display: flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
cursor: pointer;
|
|
56
|
+
min-height: 36px;
|
|
57
|
+
padding: 6px 12px;
|
|
58
|
+
|
|
59
|
+
&:hover {
|
|
60
|
+
background-color: ${(props) => props.theme.colours.controlBorder};
|
|
61
|
+
}
|
|
62
|
+
`;
|
|
63
|
+
const NewTagInput = styled.input `
|
|
64
|
+
border: none;
|
|
65
|
+
background: transparent;
|
|
66
|
+
color: ${(props) => getThemeControlColours(props.theme).font};
|
|
67
|
+
font-family: ${(props) => props.theme.fonts.default.family};
|
|
68
|
+
font-size: ${(props) => props.theme.fonts.default.size};
|
|
69
|
+
padding: 12px 12px;
|
|
70
|
+
width: 100%;
|
|
71
|
+
outline: none;
|
|
72
|
+
|
|
73
|
+
&::placeholder {
|
|
74
|
+
color: ${(props) => getThemeControlColours(props.theme).placeholder};
|
|
75
|
+
}
|
|
76
|
+
`;
|
|
77
|
+
const NewTagOption = styled.div `
|
|
78
|
+
background-color: ${(props) => props.theme.colours.controlBackgroundDisabled};
|
|
79
|
+
border-top: 1px solid ${(props) => props.theme.colours.controlBorder};
|
|
80
|
+
`;
|
|
81
|
+
const TagSelect = (props) => {
|
|
82
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
83
|
+
const [newTagValue, setNewTagValue] = useState('');
|
|
84
|
+
const controlRef = useRef(null);
|
|
85
|
+
const optionsRef = useRef(null);
|
|
86
|
+
const { label, name, description, placeholder, 'value': propsValue, 'data-testid': dataTestId, options, className, allowNewTags = true, newTagVariant = 'info', } = props;
|
|
87
|
+
const { value: contextValue, onChange: contextOnChange } = useFormNode(name);
|
|
88
|
+
const value = getValue(propsValue, contextValue) || [];
|
|
89
|
+
const splitDescription = description ? description.split('\\n').map((str) => str.trim()) : undefined;
|
|
90
|
+
const handleGlobalClick = useCallback((event) => {
|
|
91
|
+
if (!optionsRef.current?.contains(event.target) && !controlRef.current?.contains(event.target)) {
|
|
92
|
+
setIsOpen(false);
|
|
93
|
+
}
|
|
94
|
+
}, []);
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (isOpen) {
|
|
97
|
+
document.addEventListener('mouseup', handleGlobalClick);
|
|
98
|
+
}
|
|
99
|
+
return () => {
|
|
100
|
+
document.removeEventListener('mouseup', handleGlobalClick);
|
|
101
|
+
};
|
|
102
|
+
}, [handleGlobalClick, isOpen]);
|
|
103
|
+
const handleToggleTag = (tagValue) => {
|
|
104
|
+
const newValue = value.includes(tagValue) ? value.filter((v) => v !== tagValue) : [...value, tagValue];
|
|
105
|
+
if (contextOnChange) {
|
|
106
|
+
contextOnChange(newValue);
|
|
107
|
+
}
|
|
108
|
+
if (props.onChange) {
|
|
109
|
+
props.onChange(newValue);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
const handleRemoveTag = (tagValue, e) => {
|
|
113
|
+
e.stopPropagation();
|
|
114
|
+
handleToggleTag(tagValue);
|
|
115
|
+
};
|
|
116
|
+
const handleAddNewTag = () => {
|
|
117
|
+
if (!newTagValue.trim() || !allowNewTags)
|
|
118
|
+
return;
|
|
119
|
+
const trimmedValue = newTagValue.trim();
|
|
120
|
+
if (value.includes(trimmedValue)) {
|
|
121
|
+
setNewTagValue('');
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const newValue = [...value, trimmedValue];
|
|
125
|
+
if (contextOnChange) {
|
|
126
|
+
contextOnChange(newValue);
|
|
127
|
+
}
|
|
128
|
+
if (props.onChange) {
|
|
129
|
+
props.onChange(newValue);
|
|
130
|
+
}
|
|
131
|
+
setNewTagValue('');
|
|
132
|
+
};
|
|
133
|
+
const handleKeyDown = (e) => {
|
|
134
|
+
if (e.key === 'Enter') {
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
handleAddNewTag();
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
const getTagOption = (tagValue) => {
|
|
140
|
+
return options.find((o) => o.value === tagValue);
|
|
141
|
+
};
|
|
142
|
+
const selectedTags = value
|
|
143
|
+
.map((v) => getTagOption(v) || { value: v, label: v, variant: newTagVariant })
|
|
144
|
+
.filter(Boolean);
|
|
145
|
+
return (React.createElement("div", { className: className },
|
|
146
|
+
label && React.createElement(ControlLabel, { htmlFor: name }, label),
|
|
147
|
+
React.createElement(ControlOuter, null,
|
|
148
|
+
React.createElement(TagSelectControl, { ref: controlRef, "data-testid": dataTestId, onClick: () => setIsOpen(!isOpen) },
|
|
149
|
+
selectedTags.length === 0 && placeholder && React.createElement(PlaceholderText, null, placeholder),
|
|
150
|
+
selectedTags.length > 0 && (React.createElement(TagsContainer, null, selectedTags.map((tag) => (React.createElement("span", { key: tag.value, onClick: (e) => e.stopPropagation() },
|
|
151
|
+
React.createElement(Badge, { variant: tag.variant, actionIcon: faTimes, onAction: (e) => handleRemoveTag(tag.value, e) }, tag.label)))))),
|
|
152
|
+
React.createElement(IconContainer, null,
|
|
153
|
+
React.createElement(FontAwesomeIcon, { icon: isOpen ? faChevronUp : faChevronDown }))),
|
|
154
|
+
isOpen && (React.createElement(OptionsContainer, { ref: optionsRef },
|
|
155
|
+
options.map((option) => {
|
|
156
|
+
const isSelected = value.includes(option.value);
|
|
157
|
+
return (React.createElement(Option, { key: option.value, "data-testid": `option-${option.value}`, selected: isSelected, onClick: () => handleToggleTag(option.value) },
|
|
158
|
+
React.createElement(Badge, { variant: option.variant }, option.label)));
|
|
159
|
+
}),
|
|
160
|
+
allowNewTags && (React.createElement(NewTagOption, null,
|
|
161
|
+
React.createElement(NewTagInput, { placeholder: 'Add new tag...', value: newTagValue, onChange: (e) => setNewTagValue(e.target.value), onKeyDown: handleKeyDown, onClick: (e) => e.stopPropagation() }),
|
|
162
|
+
newTagValue && (React.createElement(Option, { selected: false, onClick: handleAddNewTag },
|
|
163
|
+
React.createElement(Badge, { variant: newTagVariant },
|
|
164
|
+
React.createElement(FontAwesomeIcon, { icon: faPlus }),
|
|
165
|
+
" Add \"",
|
|
166
|
+
newTagValue,
|
|
167
|
+
"\"")))))))),
|
|
168
|
+
splitDescription && (React.createElement(ControlDescription, null, splitDescription.map((line, index) => (React.createElement(Fragment, { key: index },
|
|
169
|
+
index !== 0 && React.createElement("br", null),
|
|
170
|
+
line)))))));
|
|
171
|
+
};
|
|
172
|
+
export default TagSelect;
|
package/build/index.d.ts
CHANGED
|
@@ -35,6 +35,7 @@ export { default as ProfileImage } from './components/ProfileImage/ProfileImage.
|
|
|
35
35
|
export { default as QrCode } from './components/QrCode/QrCode.component';
|
|
36
36
|
export { default as Select } from './components/Select/Select.component';
|
|
37
37
|
export { default as Spacer } from './components/Spacer/Spacer.component';
|
|
38
|
+
export { default as TagSelect } from './components/TagSelect/TagSelect.component';
|
|
38
39
|
export { default as SquareButton } from './components/SquareButton/SquareButton.component';
|
|
39
40
|
export { default as Swimlane } from './components/Swimlane/Swimlane.component';
|
|
40
41
|
export { default as Table } from './components/Table/Table.component';
|
package/build/index.js
CHANGED
|
@@ -35,6 +35,7 @@ export { default as ProfileImage } from './components/ProfileImage/ProfileImage.
|
|
|
35
35
|
export { default as QrCode } from './components/QrCode/QrCode.component';
|
|
36
36
|
export { default as Select } from './components/Select/Select.component';
|
|
37
37
|
export { default as Spacer } from './components/Spacer/Spacer.component';
|
|
38
|
+
export { default as TagSelect } from './components/TagSelect/TagSelect.component';
|
|
38
39
|
export { default as SquareButton } from './components/SquareButton/SquareButton.component';
|
|
39
40
|
export { default as Swimlane } from './components/Swimlane/Swimlane.component';
|
|
40
41
|
export { default as Table } from './components/Table/Table.component';
|