@dtdot/lego 2.4.0 → 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.
@@ -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, setButton } = useContext(FloatingActionButtonContext);
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
- setButton({ id, icon, onClick, variant, dataTestId });
10
+ setPrimaryButton({ id, icon, onClick, variant, label, dataTestId });
11
11
  return () => {
12
- setButton(undefined);
12
+ setPrimaryButton(undefined);
13
13
  };
14
- }, [icon, onClick, variant, dataTestId, setButton]);
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
- /// <reference types="react" />
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
- setButton: (props: FabProps | undefined) => void;
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;
@@ -1,6 +1,7 @@
1
1
  import { createContext } from 'react';
2
2
  const FloatingActionButtonContext = createContext({
3
3
  contextExists: false,
4
- setButton: () => { },
4
+ setPrimaryButton: () => { },
5
+ setSecondaryButtons: () => { },
5
6
  });
6
7
  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: ({ icon, onClick, variant, "data-testid": dataTestId, }: FloatingActionButtonInternalProps) => JSX.Element;
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 FloatingButton = styled(motion.button) `
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) => (props.offsetBottom ? '76px' : '20px')};
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
- width: 56px;
14
- height: 56px;
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: 24px;
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 FloatingActionButtonInternal = ({ icon, onClick, variant = 'primary', 'data-testid': dataTestId, }) => {
33
- const { menuExists, isMobile } = useContext(MinimalMenuContext);
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: { opacity: 1, scale: 1, transition: { duration: 0.3, delay: 0.2 } },
38
- exit: { opacity: 0, scale: 0, transition: { duration: 0.3 } },
39
- hover: { scale: 1.1 },
40
- tap: { scale: 0.95 },
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
- return (React.createElement(FloatingButton, { key: 'floating-button', initial: contextExists ? 'hidden' : 'visible', animate: 'visible', exit: 'exit', whileHover: 'hover', whileTap: 'tap', offsetBottom: menuExists && isMobile, variants: variants, onClick: onClick, variant: variant, "data-testid": dataTestId },
43
- React.createElement(FontAwesomeIcon, { icon: icon })));
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 [fab, setFab] = useState();
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
- setButton: setFab,
10
- }), [setFab]);
12
+ setPrimaryButton: setPrimaryFab,
13
+ setSecondaryButtons: setSecondaryFabs,
14
+ }), []);
11
15
  return (React.createElement(FloatingActionButtonContext.Provider, { value: contextVal },
12
16
  children,
13
- React.createElement(AnimatePresence, null, fab?.icon && (React.createElement(FloatingActionButtonInternal, { key: fab.id, icon: fab.icon, onClick: fab.onClick, variant: fab.variant, "data-testid": fab.dataTestId })))));
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dtdot/lego",
3
- "version": "2.4.0",
3
+ "version": "2.5.0",
4
4
  "description": "Some reusable components for building my applications",
5
5
  "main": "build/index.js",
6
6
  "scripts": {