@dtdot/lego 0.16.3 → 0.17.2
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/Form/Form.component.d.ts +3 -2
- package/build/components/Form/Form.component.js +2 -1
- package/build/components/Form/FormState.context.d.ts +1 -0
- package/build/components/Form/FormState.context.js +1 -0
- package/build/components/Form/useFormNode.hook.d.ts +8 -3
- package/build/components/Form/useFormNode.hook.js +10 -1
- package/build/components/Input/Input.component.d.ts +8 -3
- package/build/components/Input/Input.component.js +80 -8
- package/build/components/Input/Input.stories.d.ts +1 -0
- package/build/components/Input/Input.stories.js +18 -2
- package/build/components/LiveInput/LiveInput.component.d.ts +2 -2
- package/build/components/LiveInput/LiveInput.component.js +9 -1
- package/build/components/LiveList/LiveList.component.d.ts +12 -0
- package/build/components/LiveList/LiveList.component.js +108 -0
- package/build/components/LiveList/LiveList.stories.d.ts +7 -0
- package/build/components/LiveList/LiveList.stories.js +50 -0
- package/build/components/LiveList/_FocusContext.d.ts +16 -0
- package/build/components/LiveList/_FocusContext.js +64 -0
- package/build/components/LiveList/_LiveListRow.d.ts +10 -0
- package/build/components/LiveList/_LiveListRow.js +68 -0
- package/build/components/Modal/Modal.component.js +1 -1
- package/build/hooks/useKeyPress.d.ts +2 -0
- package/build/hooks/useKeyPress.js +22 -0
- package/build/index.d.ts +1 -0
- package/build/index.js +1 -0
- package/package.json +4 -2
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
interface FormProps {
|
|
3
|
-
value: any
|
|
3
|
+
value: Record<string, any>;
|
|
4
|
+
errors?: Record<string, any>;
|
|
4
5
|
onChange: (value: any) => void;
|
|
5
6
|
onSubmit?: () => void;
|
|
6
7
|
children: React.ReactNode;
|
|
7
8
|
}
|
|
8
|
-
declare const Form: ({ value, onChange, onSubmit, children }: FormProps) => JSX.Element;
|
|
9
|
+
declare const Form: ({ value, errors, onChange, onSubmit, children }: FormProps) => JSX.Element;
|
|
9
10
|
export default Form;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import FormStateContext from './FormState.context';
|
|
4
|
-
const Form = ({ value, onChange, onSubmit, children }) => {
|
|
4
|
+
const Form = ({ value, errors = {}, onChange, onSubmit, children }) => {
|
|
5
5
|
const onChangeFn = (key, fieldValue) => {
|
|
6
6
|
onChange({
|
|
7
7
|
...value,
|
|
@@ -16,6 +16,7 @@ const Form = ({ value, onChange, onSubmit, children }) => {
|
|
|
16
16
|
};
|
|
17
17
|
const contextValue = {
|
|
18
18
|
value,
|
|
19
|
+
errors,
|
|
19
20
|
onChange: onChangeFn,
|
|
20
21
|
};
|
|
21
22
|
return (React.createElement(FormStateContext.Provider, { value: contextValue },
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
declare function useFormNode(key
|
|
2
|
-
value:
|
|
3
|
-
|
|
1
|
+
declare function useFormNode<T = string, K = string>(key?: string): {
|
|
2
|
+
value: undefined;
|
|
3
|
+
error: undefined;
|
|
4
|
+
onChange: undefined;
|
|
5
|
+
} | {
|
|
6
|
+
value: T | undefined;
|
|
7
|
+
error: K | undefined;
|
|
8
|
+
onChange: (_value: T) => void;
|
|
4
9
|
};
|
|
5
10
|
export default useFormNode;
|
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import { useContext } from 'react';
|
|
2
2
|
import FormStateContext from './FormState.context';
|
|
3
3
|
function useFormNode(key) {
|
|
4
|
-
const { value, onChange } = useContext(FormStateContext);
|
|
4
|
+
const { value, errors, onChange } = useContext(FormStateContext);
|
|
5
|
+
if (!key) {
|
|
6
|
+
return {
|
|
7
|
+
value: undefined,
|
|
8
|
+
error: undefined,
|
|
9
|
+
onChange: undefined,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
5
12
|
const internalOnChange = (_value) => {
|
|
6
13
|
if (onChange) {
|
|
7
14
|
onChange(key, _value);
|
|
8
15
|
}
|
|
9
16
|
};
|
|
10
17
|
const internalValue = value[key];
|
|
18
|
+
const internalError = errors[key];
|
|
11
19
|
return {
|
|
12
20
|
value: internalValue,
|
|
21
|
+
error: internalError,
|
|
13
22
|
onChange: internalOnChange,
|
|
14
23
|
};
|
|
15
24
|
}
|
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export declare const INPUT_HEIGHT = 48;
|
|
3
|
+
export declare const InputStyles: import("styled-components").FlattenInterpolation<import("styled-components").ThemeProps<import("styled-components").DefaultTheme>>;
|
|
2
4
|
export interface IInputProps {
|
|
3
|
-
name
|
|
5
|
+
name?: string;
|
|
4
6
|
label?: string;
|
|
5
7
|
placeholder?: string;
|
|
6
8
|
type?: string;
|
|
7
9
|
value?: string;
|
|
10
|
+
error?: string;
|
|
8
11
|
onChange?: (value: any) => void;
|
|
12
|
+
onFocus?: () => void;
|
|
13
|
+
onBlur?: () => void;
|
|
9
14
|
}
|
|
10
|
-
declare const Input:
|
|
15
|
+
declare const Input: React.ForwardRefExoticComponent<IInputProps & React.RefAttributes<HTMLInputElement>>;
|
|
11
16
|
export default Input;
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import { faExclamationCircle } from '@fortawesome/free-solid-svg-icons';
|
|
3
|
+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
4
|
+
import { motion } from 'framer-motion';
|
|
5
|
+
import React, { useState } from 'react';
|
|
6
|
+
import styled, { css } from 'styled-components';
|
|
4
7
|
import getThemeControlColours from '../../theme/helpers/getThemeControlColours';
|
|
5
8
|
import useFormNode from '../Form/useFormNode.hook';
|
|
9
|
+
export const INPUT_HEIGHT = 48;
|
|
10
|
+
const InputContainer = styled(motion.div) `
|
|
11
|
+
position: relative;
|
|
12
|
+
margin: 2px 0;
|
|
13
|
+
|
|
14
|
+
background-color: ${(props) => props.theme.colours.controlBackground};
|
|
15
|
+
`;
|
|
6
16
|
const InputLabel = styled.label `
|
|
7
17
|
display: block;
|
|
8
18
|
padding-bottom: 8px;
|
|
@@ -11,13 +21,14 @@ const InputLabel = styled.label `
|
|
|
11
21
|
font-family: ${(props) => props.theme.fonts.default.family};
|
|
12
22
|
font-size: ${(props) => props.theme.fonts.default.size};
|
|
13
23
|
`;
|
|
14
|
-
const
|
|
24
|
+
export const InputStyles = css `
|
|
15
25
|
outline: none;
|
|
16
26
|
box-shadow: none;
|
|
17
27
|
|
|
18
28
|
width: 100%;
|
|
19
|
-
height:
|
|
29
|
+
height: ${INPUT_HEIGHT}px;
|
|
20
30
|
padding: 0 12px;
|
|
31
|
+
scroll-margin-bottom: 100px;
|
|
21
32
|
|
|
22
33
|
font-family: ${(props) => props.theme.fonts.default.family};
|
|
23
34
|
font-size: ${(props) => props.theme.fonts.default.size};
|
|
@@ -40,8 +51,52 @@ const StyledInput = styled.input `
|
|
|
40
51
|
color: ${(props) => getThemeControlColours(props.theme).placeholder};
|
|
41
52
|
}
|
|
42
53
|
`;
|
|
43
|
-
const
|
|
44
|
-
|
|
54
|
+
const StyledInput = styled(motion.input) `
|
|
55
|
+
${InputStyles}
|
|
56
|
+
`;
|
|
57
|
+
const ErrorMessage = styled(motion.div) `
|
|
58
|
+
position: absolute;
|
|
59
|
+
left: 38px;
|
|
60
|
+
bottom: 0;
|
|
61
|
+
|
|
62
|
+
font-family: ${(props) => props.theme.fonts.default.family};
|
|
63
|
+
font-size: ${(props) => props.theme.fonts.default.size};
|
|
64
|
+
color: ${(props) => props.theme.colours.statusDanger.main};
|
|
65
|
+
`;
|
|
66
|
+
const ErrorContainer = styled(motion.div) `
|
|
67
|
+
position: absolute;
|
|
68
|
+
left: 0;
|
|
69
|
+
top: 0;
|
|
70
|
+
height: 100%;
|
|
71
|
+
display: flex;
|
|
72
|
+
align-items: center;
|
|
73
|
+
|
|
74
|
+
color: ${(props) => props.theme.colours.statusDanger.main};
|
|
75
|
+
`;
|
|
76
|
+
const ErrorInner = styled.div `
|
|
77
|
+
width: 24px;
|
|
78
|
+
height: 24px;
|
|
79
|
+
|
|
80
|
+
display: flex;
|
|
81
|
+
justify-content: center;
|
|
82
|
+
align-items: center;
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
`;
|
|
85
|
+
const errorVariants = {
|
|
86
|
+
show: { opacity: 1, x: 10 },
|
|
87
|
+
};
|
|
88
|
+
const inputVariants = {
|
|
89
|
+
error: { paddingLeft: '38px' },
|
|
90
|
+
errorFocus: { paddingLeft: '38px', paddingBottom: '18px' },
|
|
91
|
+
};
|
|
92
|
+
const messageVariants = {
|
|
93
|
+
errorFocus: { opacity: 1, y: -4 },
|
|
94
|
+
};
|
|
95
|
+
const Input = React.forwardRef(function ForwardRefInput(props, ref) {
|
|
96
|
+
const { label, name, placeholder, type = 'text', value, error: propsError, onChange, onFocus, onBlur } = props;
|
|
97
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
98
|
+
const { value: contextValue, error: contextError, onChange: contextOnChange } = useFormNode(name);
|
|
99
|
+
const error = contextError || propsError;
|
|
45
100
|
const handleChange = (e) => {
|
|
46
101
|
if (onChange) {
|
|
47
102
|
onChange(e.target.value);
|
|
@@ -50,8 +105,25 @@ const Input = ({ label, name, placeholder, type = 'text', value, onChange }) =>
|
|
|
50
105
|
contextOnChange(e.target.value);
|
|
51
106
|
}
|
|
52
107
|
};
|
|
108
|
+
const handleFocus = () => {
|
|
109
|
+
setIsFocused(true);
|
|
110
|
+
if (onFocus) {
|
|
111
|
+
onFocus();
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const handleBlur = () => {
|
|
115
|
+
setIsFocused(false);
|
|
116
|
+
if (onBlur) {
|
|
117
|
+
onBlur();
|
|
118
|
+
}
|
|
119
|
+
};
|
|
53
120
|
return (React.createElement("div", null,
|
|
54
121
|
label && React.createElement(InputLabel, { htmlFor: name }, label),
|
|
55
|
-
React.createElement(
|
|
56
|
-
}
|
|
122
|
+
React.createElement(InputContainer, { animate: error ? (isFocused ? 'errorFocus' : 'error') : undefined },
|
|
123
|
+
React.createElement(StyledInput, { ref: ref, variants: inputVariants, transition: { type: 'spring', duration: 0.3 }, type: type, name: name, placeholder: placeholder, value: value || contextValue, onChange: handleChange, onFocus: handleFocus, onBlur: handleBlur }),
|
|
124
|
+
React.createElement(ErrorContainer, { animate: error ? 'show' : undefined, style: { opacity: 0 }, variants: errorVariants, transition: { type: 'spring', duration: 0.3 } },
|
|
125
|
+
React.createElement(ErrorInner, null,
|
|
126
|
+
React.createElement(FontAwesomeIcon, { icon: faExclamationCircle }))),
|
|
127
|
+
error && (React.createElement(ErrorMessage, { style: { opacity: 0, y: 0 }, variants: messageVariants, transition: { type: 'spring', duration: 0.3 } }, error)))));
|
|
128
|
+
});
|
|
57
129
|
export default Input;
|
|
@@ -2,5 +2,6 @@
|
|
|
2
2
|
import { Meta } from '@storybook/react/types-6-0';
|
|
3
3
|
export declare const Standard: () => JSX.Element;
|
|
4
4
|
export declare const WithoutLabels: () => JSX.Element;
|
|
5
|
+
export declare const WithError: () => JSX.Element;
|
|
5
6
|
declare const _default: Meta<import("@storybook/react/types-6-0").Args>;
|
|
6
7
|
export default _default;
|
|
@@ -1,11 +1,27 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Input, ControlGroup } from '../..';
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Button, ButtonGroup, Input, ControlGroup, Spacer } from '../..';
|
|
3
3
|
export const Standard = () => (React.createElement(ControlGroup, null,
|
|
4
4
|
React.createElement(Input, { name: 'one', label: 'A standard input' }),
|
|
5
5
|
React.createElement(Input, { name: 'two', label: 'Another input' })));
|
|
6
6
|
export const WithoutLabels = () => (React.createElement(ControlGroup, null,
|
|
7
7
|
React.createElement(Input, { name: 'one', placeholder: 'A standard input' }),
|
|
8
8
|
React.createElement(Input, { name: 'two', placeholder: 'Another input' })));
|
|
9
|
+
export const WithError = () => {
|
|
10
|
+
const [error, setError] = useState('Input has an error!');
|
|
11
|
+
const clear = () => {
|
|
12
|
+
setError(undefined);
|
|
13
|
+
};
|
|
14
|
+
const validate = () => {
|
|
15
|
+
setError('Input has an error!');
|
|
16
|
+
};
|
|
17
|
+
return (React.createElement(React.Fragment, null,
|
|
18
|
+
React.createElement(ControlGroup, null,
|
|
19
|
+
React.createElement(Input, { name: 'one', error: error, placeholder: 'A standard input' })),
|
|
20
|
+
React.createElement(Spacer, { size: '2x' }),
|
|
21
|
+
React.createElement(ButtonGroup, null,
|
|
22
|
+
React.createElement(Button, { onClick: clear }, "Clear"),
|
|
23
|
+
React.createElement(Button, { onClick: validate }, "Set Errors"))));
|
|
24
|
+
};
|
|
9
25
|
export default {
|
|
10
26
|
title: 'Components/Input',
|
|
11
27
|
component: Input,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
/// <reference types="react" />
|
|
2
2
|
interface LiveInputProps {
|
|
3
3
|
name: string;
|
|
4
4
|
placeholder: string;
|
|
5
5
|
type?: string;
|
|
6
6
|
value?: string;
|
|
7
|
-
onChange?: (
|
|
7
|
+
onChange?: (value: string) => void;
|
|
8
8
|
}
|
|
9
9
|
declare const LiveInput: ({ name, type, placeholder, value, onChange }: LiveInputProps) => JSX.Element;
|
|
10
10
|
export default LiveInput;
|
|
@@ -16,7 +16,15 @@ const StyledInput = styled.input `
|
|
|
16
16
|
`;
|
|
17
17
|
const LiveInput = ({ name, type = 'text', placeholder, value, onChange }) => {
|
|
18
18
|
const { value: contextValue, onChange: contextOnChange } = useFormNode(name);
|
|
19
|
+
const handleChange = (e) => {
|
|
20
|
+
if (onChange) {
|
|
21
|
+
onChange(e.target.value);
|
|
22
|
+
}
|
|
23
|
+
if (contextOnChange) {
|
|
24
|
+
contextOnChange(e.target.value);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
19
27
|
return (React.createElement("div", null,
|
|
20
|
-
React.createElement(StyledInput, { name: name, type: type, value: value || contextValue, placeholder: placeholder, onChange:
|
|
28
|
+
React.createElement(StyledInput, { name: name, type: type, value: value || contextValue, placeholder: placeholder, onChange: handleChange })));
|
|
21
29
|
};
|
|
22
30
|
export default LiveInput;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
interface LiveListValue {
|
|
3
|
+
id: string;
|
|
4
|
+
value: string;
|
|
5
|
+
}
|
|
6
|
+
export interface LiveListProps {
|
|
7
|
+
name: string;
|
|
8
|
+
value?: LiveListValue[];
|
|
9
|
+
onChange?: (value: LiveListValue[]) => void;
|
|
10
|
+
}
|
|
11
|
+
declare const _default: (props: any) => JSX.Element;
|
|
12
|
+
export default _default;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { faPlusCircle } from '@fortawesome/free-solid-svg-icons';
|
|
2
|
+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
3
|
+
import { motion } from 'framer-motion';
|
|
4
|
+
import React, { useCallback, useContext, useEffect } from 'react';
|
|
5
|
+
import styled from 'styled-components';
|
|
6
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
7
|
+
import useKeypress from '../../hooks/useKeyPress';
|
|
8
|
+
import useFormNode from '../Form/useFormNode.hook';
|
|
9
|
+
import { FocusContext, FocusContextProviderHOC } from './_FocusContext';
|
|
10
|
+
import LiveListRow from './_LiveListRow';
|
|
11
|
+
const AddRow = styled.div `
|
|
12
|
+
width: 100%;
|
|
13
|
+
display: flex;
|
|
14
|
+
justify-content: center;
|
|
15
|
+
|
|
16
|
+
margin-top: 4px;
|
|
17
|
+
`;
|
|
18
|
+
const AddRowInner = styled(motion.div) `
|
|
19
|
+
padding: 4px;
|
|
20
|
+
display: flex;
|
|
21
|
+
align-items: center;
|
|
22
|
+
cursor: pointer;
|
|
23
|
+
overflow: hidden;
|
|
24
|
+
|
|
25
|
+
color: ${(props) => props.theme.colours.defaultFont};
|
|
26
|
+
font-family: ${(props) => props.theme.fonts.default.family};
|
|
27
|
+
font-size: ${(props) => props.theme.fonts.default.size};
|
|
28
|
+
`;
|
|
29
|
+
const IconContainer = styled.div `
|
|
30
|
+
padding-right: 4px;
|
|
31
|
+
`;
|
|
32
|
+
const addVariants = {
|
|
33
|
+
hover: { scale: 1.05 },
|
|
34
|
+
};
|
|
35
|
+
const defaultValue = [{ id: uuidv4(), value: '' }];
|
|
36
|
+
const LiveList = ({ value: inputValue, name, onChange: propsOnChange }) => {
|
|
37
|
+
const { getFocused, requestFocus } = useContext(FocusContext);
|
|
38
|
+
const { value: contextValue, error: contextError, onChange: contextOnChange } = useFormNode(name);
|
|
39
|
+
const value = contextValue || inputValue || defaultValue;
|
|
40
|
+
const wrappedOnChange = useCallback((val) => {
|
|
41
|
+
if (propsOnChange) {
|
|
42
|
+
propsOnChange(val);
|
|
43
|
+
}
|
|
44
|
+
if (contextOnChange) {
|
|
45
|
+
contextOnChange(val);
|
|
46
|
+
}
|
|
47
|
+
}, [propsOnChange, contextOnChange]);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!(value === null || value === void 0 ? void 0 : value.length)) {
|
|
50
|
+
wrappedOnChange([{ id: uuidv4(), value: '' }]);
|
|
51
|
+
}
|
|
52
|
+
}, [value, wrappedOnChange]);
|
|
53
|
+
useKeypress('Enter', () => {
|
|
54
|
+
const focusedId = getFocused();
|
|
55
|
+
const focusedIndex = value.findIndex((val) => val.id === focusedId);
|
|
56
|
+
const newItem = { id: uuidv4(), value: '' };
|
|
57
|
+
const newValue = [...value];
|
|
58
|
+
newValue.splice(focusedIndex + 1, 0, newItem);
|
|
59
|
+
wrappedOnChange(newValue);
|
|
60
|
+
requestFocus(newItem.id);
|
|
61
|
+
});
|
|
62
|
+
useKeypress('Backspace', () => {
|
|
63
|
+
const focusedId = getFocused();
|
|
64
|
+
if (!focusedId) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const focusedValue = value.find((val) => val.id === focusedId);
|
|
68
|
+
if (focusedValue === null || focusedValue === void 0 ? void 0 : focusedValue.value) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const focusedIndex = value.findIndex((val) => val.id === focusedId);
|
|
72
|
+
const prevId = focusedIndex > 0 ? value[focusedIndex - 1].id : undefined;
|
|
73
|
+
wrappedOnChange(value.filter((val) => val.id !== focusedId));
|
|
74
|
+
if (prevId) {
|
|
75
|
+
// Timeout to prevent deleting the last char of the previous row
|
|
76
|
+
setTimeout(() => {
|
|
77
|
+
requestFocus(prevId);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
const handleRowChange = (updateId, updateValue) => {
|
|
82
|
+
wrappedOnChange(value.map((v) => (v.id === updateId ? { ...v, value: updateValue } : v)));
|
|
83
|
+
};
|
|
84
|
+
const handleRowRemove = (id) => {
|
|
85
|
+
if (value.length === 1) {
|
|
86
|
+
const withValueCleared = [...value];
|
|
87
|
+
value[0].value = '';
|
|
88
|
+
wrappedOnChange(withValueCleared);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
wrappedOnChange(value.filter((val) => val.id !== id));
|
|
92
|
+
};
|
|
93
|
+
const handleRowAdd = () => {
|
|
94
|
+
const newItem = { id: uuidv4(), value: '' };
|
|
95
|
+
wrappedOnChange([...value, newItem]);
|
|
96
|
+
};
|
|
97
|
+
if (!(value === null || value === void 0 ? void 0 : value.length)) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return (React.createElement("div", null,
|
|
101
|
+
value.map((val) => (React.createElement(LiveListRow, { key: val.id, id: val.id, value: val.value, error: contextError ? contextError[val.id] : undefined, onChange: (newVal) => handleRowChange(val.id, newVal), onRemove: () => handleRowRemove(val.id) }))),
|
|
102
|
+
React.createElement(AddRow, null,
|
|
103
|
+
React.createElement(AddRowInner, { style: { scale: 1 }, whileHover: 'hover', variants: addVariants, onClick: handleRowAdd },
|
|
104
|
+
React.createElement(IconContainer, null,
|
|
105
|
+
React.createElement(FontAwesomeIcon, { icon: faPlusCircle })),
|
|
106
|
+
"add"))));
|
|
107
|
+
};
|
|
108
|
+
export default FocusContextProviderHOC(LiveList);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import { Meta } from '@storybook/react/types-6-0';
|
|
3
|
+
export declare const Standard: () => JSX.Element;
|
|
4
|
+
export declare const InForm: () => JSX.Element;
|
|
5
|
+
export declare const WithValidation: () => JSX.Element;
|
|
6
|
+
declare const _default: Meta<import("@storybook/react/types-6-0").Args>;
|
|
7
|
+
export default _default;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Button, ButtonGroup, ControlGroup, Form, Heading, ImageUpload, Input, LiveList, Spacer, Text } from '../..';
|
|
3
|
+
const exampleLiveListValue = [
|
|
4
|
+
{ id: '6f974464-d592-4271-b3ae-2821fffce258', value: '500g chicken' },
|
|
5
|
+
{ id: 'a273d33a-2643-4991-b81d-2beff51d42a8', value: '1/2 cup rice' },
|
|
6
|
+
{ id: '2ecef56f-f725-4f8a-ac84-bd3117e7d50b', value: '1 tbs olive oil' },
|
|
7
|
+
];
|
|
8
|
+
const exampleListErrors = {
|
|
9
|
+
'6f974464-d592-4271-b3ae-2821fffce258': 'Ingredient not found',
|
|
10
|
+
'a273d33a-2643-4991-b81d-2beff51d42a8': 'Ingredient not found',
|
|
11
|
+
};
|
|
12
|
+
export const Standard = () => {
|
|
13
|
+
const [value, setValue] = useState(exampleLiveListValue);
|
|
14
|
+
return (React.createElement(React.Fragment, null,
|
|
15
|
+
React.createElement(Heading.FormHeading, null, "Ingredients"),
|
|
16
|
+
React.createElement(Spacer, { size: '1x' }),
|
|
17
|
+
React.createElement(LiveList, { value: value, onChange: setValue })));
|
|
18
|
+
};
|
|
19
|
+
export const InForm = () => {
|
|
20
|
+
const [value, setValue] = useState({});
|
|
21
|
+
return (React.createElement(Form, { value: value, onChange: setValue },
|
|
22
|
+
React.createElement(ControlGroup, { variation: 'comfortable' },
|
|
23
|
+
React.createElement(ImageUpload, { name: 'image' }),
|
|
24
|
+
React.createElement(Input, { name: 'name', placeholder: 'Something tasty..' }),
|
|
25
|
+
React.createElement(Heading.FormHeading, null, "Ingredients"),
|
|
26
|
+
React.createElement(LiveList, { name: 'ingredients' }))));
|
|
27
|
+
};
|
|
28
|
+
export const WithValidation = () => {
|
|
29
|
+
const value = { ingredients: exampleLiveListValue };
|
|
30
|
+
const [errors, setErrors] = useState({ ingredients: exampleListErrors });
|
|
31
|
+
const validate = () => {
|
|
32
|
+
setErrors({ ingredients: exampleListErrors });
|
|
33
|
+
};
|
|
34
|
+
const clear = () => {
|
|
35
|
+
setErrors({ ingredients: undefined });
|
|
36
|
+
};
|
|
37
|
+
return (React.createElement(React.Fragment, null,
|
|
38
|
+
React.createElement(Form, { value: value, errors: errors, onChange: () => undefined },
|
|
39
|
+
React.createElement(ControlGroup, { variation: 'comfortable' },
|
|
40
|
+
React.createElement(Text, { variant: 'secondary' }, "Max length 20 chars"),
|
|
41
|
+
React.createElement(LiveList, { name: 'ingredients' }))),
|
|
42
|
+
React.createElement(Spacer, { size: '2x' }),
|
|
43
|
+
React.createElement(ButtonGroup, null,
|
|
44
|
+
React.createElement(Button, { onClick: clear }, "Clear"),
|
|
45
|
+
React.createElement(Button, { onClick: validate }, "Set Errors"))));
|
|
46
|
+
};
|
|
47
|
+
export default {
|
|
48
|
+
title: 'Components/LiveList',
|
|
49
|
+
component: LiveList,
|
|
50
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface FocusContextProps {
|
|
3
|
+
getFocused: () => string | undefined;
|
|
4
|
+
requestFocus: (id: string) => void;
|
|
5
|
+
onFocus: (id: string) => void;
|
|
6
|
+
onBlur: (id: string) => void;
|
|
7
|
+
registerFocusable: (id: string, focus: () => void) => void;
|
|
8
|
+
deregisterFocusable: (id: string) => void;
|
|
9
|
+
}
|
|
10
|
+
declare const FocusContext: React.Context<FocusContextProps>;
|
|
11
|
+
export interface FocusContextProviderProps {
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
}
|
|
14
|
+
declare const FocusContextProvider: ({ children }: FocusContextProviderProps) => JSX.Element;
|
|
15
|
+
declare function FocusContextProviderHOC(WrappedComponent: any): (props: any) => JSX.Element;
|
|
16
|
+
export { FocusContext, FocusContextProvider, FocusContextProviderHOC };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React, { createContext, useCallback, useRef } from 'react';
|
|
2
|
+
const FocusContext = createContext({
|
|
3
|
+
// eslint-disable-next-line
|
|
4
|
+
getFocused: () => undefined,
|
|
5
|
+
// eslint-disable-next-line
|
|
6
|
+
requestFocus: (id) => { },
|
|
7
|
+
// eslint-disable-next-line
|
|
8
|
+
onFocus: (id) => { },
|
|
9
|
+
// eslint-disable-next-line
|
|
10
|
+
onBlur: (id) => { },
|
|
11
|
+
// eslint-disable-next-line
|
|
12
|
+
registerFocusable: (id, focus) => { },
|
|
13
|
+
// eslint-disable-next-line
|
|
14
|
+
deregisterFocusable: (id) => { },
|
|
15
|
+
});
|
|
16
|
+
const FocusContextProvider = ({ children }) => {
|
|
17
|
+
const focusRef = useRef();
|
|
18
|
+
const focusRequest = useRef();
|
|
19
|
+
const subscribersRef = useRef({});
|
|
20
|
+
const getFocused = () => {
|
|
21
|
+
return focusRef.current || undefined;
|
|
22
|
+
};
|
|
23
|
+
const requestFocus = useCallback((id) => {
|
|
24
|
+
const subscribers = subscribersRef.current;
|
|
25
|
+
const matchingSubscriber = subscribers[id];
|
|
26
|
+
focusRequest.current = id;
|
|
27
|
+
if (matchingSubscriber) {
|
|
28
|
+
matchingSubscriber();
|
|
29
|
+
}
|
|
30
|
+
}, []);
|
|
31
|
+
const onFocus = (id) => {
|
|
32
|
+
focusRef.current = id;
|
|
33
|
+
};
|
|
34
|
+
const onBlur = useCallback((id) => {
|
|
35
|
+
if (focusRef.current === id) {
|
|
36
|
+
focusRef.current = undefined;
|
|
37
|
+
}
|
|
38
|
+
}, []);
|
|
39
|
+
const registerFocusable = useCallback((id, focus) => {
|
|
40
|
+
subscribersRef.current[id] = focus;
|
|
41
|
+
if (focusRequest.current === id) {
|
|
42
|
+
focus();
|
|
43
|
+
}
|
|
44
|
+
}, []);
|
|
45
|
+
const deregisterFocusable = useCallback((id) => {
|
|
46
|
+
delete subscribersRef.current[id];
|
|
47
|
+
}, []);
|
|
48
|
+
return (React.createElement(FocusContext.Provider, { value: {
|
|
49
|
+
getFocused,
|
|
50
|
+
requestFocus,
|
|
51
|
+
onFocus,
|
|
52
|
+
onBlur,
|
|
53
|
+
registerFocusable,
|
|
54
|
+
deregisterFocusable,
|
|
55
|
+
} }, children));
|
|
56
|
+
};
|
|
57
|
+
function FocusContextProviderHOC(WrappedComponent) {
|
|
58
|
+
const HocComponent = (props) => {
|
|
59
|
+
return (React.createElement(FocusContextProvider, null,
|
|
60
|
+
React.createElement(WrappedComponent, Object.assign({}, props))));
|
|
61
|
+
};
|
|
62
|
+
return HocComponent;
|
|
63
|
+
}
|
|
64
|
+
export { FocusContext, FocusContextProvider, FocusContextProviderHOC };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
interface LiveListRowProps {
|
|
3
|
+
id: string;
|
|
4
|
+
value: string;
|
|
5
|
+
error?: string;
|
|
6
|
+
onChange: (value: string) => void;
|
|
7
|
+
onRemove: () => void;
|
|
8
|
+
}
|
|
9
|
+
declare const LiveListRow: ({ id, value, error, onChange, onRemove }: LiveListRowProps) => JSX.Element;
|
|
10
|
+
export default LiveListRow;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { faTimes } from '@fortawesome/free-solid-svg-icons';
|
|
2
|
+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
3
|
+
import { motion } from 'framer-motion';
|
|
4
|
+
import React, { useContext, useEffect, useRef, useState } from 'react';
|
|
5
|
+
import styled from 'styled-components';
|
|
6
|
+
import { FocusContext } from './_FocusContext';
|
|
7
|
+
import Input from '../Input/Input.component';
|
|
8
|
+
const InputContainer = styled(motion.div) `
|
|
9
|
+
position: relative;
|
|
10
|
+
margin: 2px 0;
|
|
11
|
+
|
|
12
|
+
background-color: ${(props) => props.theme.colours.controlBackground};
|
|
13
|
+
`;
|
|
14
|
+
const RemoveContainer = styled(motion.div) `
|
|
15
|
+
position: absolute;
|
|
16
|
+
right: 0;
|
|
17
|
+
top: 0;
|
|
18
|
+
height: 100%;
|
|
19
|
+
display: flex;
|
|
20
|
+
align-items: center;
|
|
21
|
+
|
|
22
|
+
color: ${(props) => props.theme.colours.defaultFont};
|
|
23
|
+
`;
|
|
24
|
+
const RemoveInner = styled.div `
|
|
25
|
+
width: 24px;
|
|
26
|
+
height: 24px;
|
|
27
|
+
|
|
28
|
+
display: flex;
|
|
29
|
+
justify-content: center;
|
|
30
|
+
align-items: center;
|
|
31
|
+
cursor: pointer;
|
|
32
|
+
`;
|
|
33
|
+
const removeVariants = {
|
|
34
|
+
hover: { opacity: 1, x: -10 },
|
|
35
|
+
focus: { opacity: 1, x: -10 },
|
|
36
|
+
};
|
|
37
|
+
const LiveListRow = ({ id, value, error, onChange, onRemove }) => {
|
|
38
|
+
const { onFocus, onBlur, registerFocusable, deregisterFocusable } = useContext(FocusContext);
|
|
39
|
+
const inputRef = useRef(null);
|
|
40
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
registerFocusable(id, () => {
|
|
43
|
+
if (inputRef.current) {
|
|
44
|
+
inputRef.current.focus();
|
|
45
|
+
}
|
|
46
|
+
return () => {
|
|
47
|
+
deregisterFocusable(id);
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
}, [id, registerFocusable, deregisterFocusable]);
|
|
51
|
+
const handleChange = (e) => {
|
|
52
|
+
onChange(e);
|
|
53
|
+
};
|
|
54
|
+
const handleFocus = () => {
|
|
55
|
+
setIsFocused(true);
|
|
56
|
+
onFocus(id);
|
|
57
|
+
};
|
|
58
|
+
const handleBlur = () => {
|
|
59
|
+
setIsFocused(false);
|
|
60
|
+
onBlur(id);
|
|
61
|
+
};
|
|
62
|
+
return (React.createElement(InputContainer, { whileHover: 'hover' },
|
|
63
|
+
React.createElement(Input, { onFocus: handleFocus, onBlur: handleBlur, ref: inputRef, value: value, error: error, onChange: handleChange }),
|
|
64
|
+
React.createElement(RemoveContainer, { animate: isFocused ? 'focus' : undefined, style: { opacity: 0 }, variants: removeVariants, transition: { type: 'spring', duration: 0.3 } },
|
|
65
|
+
React.createElement(RemoveInner, { onClick: onRemove },
|
|
66
|
+
React.createElement(FontAwesomeIcon, { icon: faTimes })))));
|
|
67
|
+
};
|
|
68
|
+
export default LiveListRow;
|
|
@@ -51,7 +51,7 @@ const ModalWrapper = styled.div `
|
|
|
51
51
|
`)}
|
|
52
52
|
`;
|
|
53
53
|
const ModalOuter = styled(motion.div) `
|
|
54
|
-
background-color: ${(props) => props.theme.colours.
|
|
54
|
+
background-color: ${(props) => props.theme.colours.background};
|
|
55
55
|
border-radius: 4px;
|
|
56
56
|
margin: 16px 0 128px 0;
|
|
57
57
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
const useKeypress = (key, handler) => {
|
|
3
|
+
const eventListenerRef = useRef();
|
|
4
|
+
useEffect(() => {
|
|
5
|
+
eventListenerRef.current = (event) => {
|
|
6
|
+
if (key === event.key) {
|
|
7
|
+
handler === null || handler === void 0 ? void 0 : handler();
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
}, [key, handler]);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const eventListener = (event) => {
|
|
13
|
+
var _a;
|
|
14
|
+
(_a = eventListenerRef.current) === null || _a === void 0 ? void 0 : _a.call(eventListenerRef, event);
|
|
15
|
+
};
|
|
16
|
+
window.addEventListener('keydown', eventListener);
|
|
17
|
+
return () => {
|
|
18
|
+
window.removeEventListener('keydown', eventListener);
|
|
19
|
+
};
|
|
20
|
+
}, []);
|
|
21
|
+
};
|
|
22
|
+
export default useKeypress;
|
package/build/index.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export { default as InlineCardGroup } from './components/InlineCard/InlineCardGr
|
|
|
17
17
|
export { default as Input } from './components/Input/Input.component';
|
|
18
18
|
export { default as Heading } from './components/Heading/Heading.component';
|
|
19
19
|
export { default as LiveInput } from './components/LiveInput/LiveInput.component';
|
|
20
|
+
export { default as LiveList } from './components/LiveList/LiveList.component';
|
|
20
21
|
export { default as Loader } from './components/Loader/Loader.component';
|
|
21
22
|
export { default as Menu } from './components/Menu/Menu.component';
|
|
22
23
|
export { default as Modal } from './components/Modal/Modal.component';
|
package/build/index.js
CHANGED
|
@@ -17,6 +17,7 @@ export { default as InlineCardGroup } from './components/InlineCard/InlineCardGr
|
|
|
17
17
|
export { default as Input } from './components/Input/Input.component';
|
|
18
18
|
export { default as Heading } from './components/Heading/Heading.component';
|
|
19
19
|
export { default as LiveInput } from './components/LiveInput/LiveInput.component';
|
|
20
|
+
export { default as LiveList } from './components/LiveList/LiveList.component';
|
|
20
21
|
export { default as Loader } from './components/Loader/Loader.component';
|
|
21
22
|
export { default as Menu } from './components/Menu/Menu.component';
|
|
22
23
|
export { default as Modal } from './components/Modal/Modal.component';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dtdot/lego",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.2",
|
|
4
4
|
"description": "Some reusable components for building my applications",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"@types/react-dom": "^17.0.9",
|
|
32
32
|
"@types/spark-md5": "^3.0.2",
|
|
33
33
|
"@types/styled-components": "^5.1.4",
|
|
34
|
+
"@types/uuid": "^8.3.4",
|
|
34
35
|
"@typescript-eslint/eslint-plugin": "^4.9.1",
|
|
35
36
|
"@typescript-eslint/parser": "^4.9.1",
|
|
36
37
|
"babel-loader": "^8.2.2",
|
|
@@ -60,6 +61,7 @@
|
|
|
60
61
|
"identicon.js": "^2.3.3",
|
|
61
62
|
"qrcode": "^1.4.4",
|
|
62
63
|
"react-use-measure": "^2.1.1",
|
|
63
|
-
"spark-md5": "^3.0.1"
|
|
64
|
+
"spark-md5": "^3.0.1",
|
|
65
|
+
"uuid": "^8.3.2"
|
|
64
66
|
}
|
|
65
67
|
}
|