@dtdot/lego 0.17.0 → 0.17.1

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.
@@ -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,6 +1,7 @@
1
1
  /// <reference types="react" />
2
2
  interface FormStateContextProps {
3
3
  value: Record<string, any>;
4
+ errors: Record<string, any>;
4
5
  onChange?: (key: string, value: any) => void;
5
6
  }
6
7
  declare const FormStateContext: import("react").Context<FormStateContextProps>;
@@ -1,6 +1,7 @@
1
1
  import { createContext } from 'react';
2
2
  const FormStateContext = createContext({
3
3
  value: {},
4
+ errors: {},
4
5
  onChange: undefined,
5
6
  });
6
7
  export default FormStateContext;
@@ -1,5 +1,10 @@
1
- declare function useFormNode<T>(key: string): {
2
- value: T;
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;
3
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,14 +1,16 @@
1
- /// <reference types="react" />
2
- export declare const StyledInput: import("styled-components").StyledComponent<"input", import("styled-components").DefaultTheme, {}, never>;
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>>;
3
4
  export interface IInputProps {
4
- name: string;
5
+ name?: string;
5
6
  label?: string;
6
7
  placeholder?: string;
7
8
  type?: string;
8
9
  value?: string;
10
+ error?: string;
9
11
  onChange?: (value: any) => void;
10
12
  onFocus?: () => void;
11
13
  onBlur?: () => void;
12
14
  }
13
- declare const Input: ({ label, name, placeholder, type, value, onChange, onFocus, onBlur }: IInputProps) => JSX.Element;
15
+ declare const Input: React.ForwardRefExoticComponent<IInputProps & React.RefAttributes<HTMLInputElement>>;
14
16
  export default Input;
@@ -1,8 +1,18 @@
1
1
  // eslint-disable-next-line @typescript-eslint/no-use-before-define
2
- import React from 'react';
3
- import styled from 'styled-components';
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,12 +21,12 @@ 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
- export const StyledInput = styled.input `
24
+ export const InputStyles = css `
15
25
  outline: none;
16
26
  box-shadow: none;
17
27
 
18
28
  width: 100%;
19
- height: 48px;
29
+ height: ${INPUT_HEIGHT}px;
20
30
  padding: 0 12px;
21
31
 
22
32
  font-family: ${(props) => props.theme.fonts.default.family};
@@ -40,8 +50,52 @@ export const StyledInput = styled.input `
40
50
  color: ${(props) => getThemeControlColours(props.theme).placeholder};
41
51
  }
42
52
  `;
43
- const Input = ({ label, name, placeholder, type = 'text', value, onChange, onFocus, onBlur }) => {
44
- const { value: contextValue, onChange: contextOnChange } = useFormNode(name);
53
+ const StyledInput = styled(motion.input) `
54
+ ${InputStyles}
55
+ `;
56
+ const ErrorMessage = styled(motion.div) `
57
+ position: absolute;
58
+ left: 38px;
59
+ bottom: 0;
60
+
61
+ font-family: ${(props) => props.theme.fonts.default.family};
62
+ font-size: ${(props) => props.theme.fonts.default.size};
63
+ color: ${(props) => props.theme.colours.statusDanger.main};
64
+ `;
65
+ const ErrorContainer = styled(motion.div) `
66
+ position: absolute;
67
+ left: 0;
68
+ top: 0;
69
+ height: 100%;
70
+ display: flex;
71
+ align-items: center;
72
+
73
+ color: ${(props) => props.theme.colours.statusDanger.main};
74
+ `;
75
+ const ErrorInner = styled.div `
76
+ width: 24px;
77
+ height: 24px;
78
+
79
+ display: flex;
80
+ justify-content: center;
81
+ align-items: center;
82
+ cursor: pointer;
83
+ `;
84
+ const errorVariants = {
85
+ show: { opacity: 1, x: 10 },
86
+ };
87
+ const inputVariants = {
88
+ error: { paddingLeft: '38px' },
89
+ errorFocus: { paddingLeft: '38px', paddingBottom: '18px' },
90
+ };
91
+ const messageVariants = {
92
+ errorFocus: { opacity: 1, y: -4 },
93
+ };
94
+ const Input = React.forwardRef(function ForwardRefInput(props, ref) {
95
+ const { label, name, placeholder, type = 'text', value, error: propsError, onChange, onFocus, onBlur } = props;
96
+ const [isFocused, setIsFocused] = useState(false);
97
+ const { value: contextValue, error: contextError, onChange: contextOnChange } = useFormNode(name);
98
+ const error = contextError || propsError;
45
99
  const handleChange = (e) => {
46
100
  if (onChange) {
47
101
  onChange(e.target.value);
@@ -50,8 +104,25 @@ const Input = ({ label, name, placeholder, type = 'text', value, onChange, onFoc
50
104
  contextOnChange(e.target.value);
51
105
  }
52
106
  };
107
+ const handleFocus = () => {
108
+ setIsFocused(true);
109
+ if (onFocus) {
110
+ onFocus();
111
+ }
112
+ };
113
+ const handleBlur = () => {
114
+ setIsFocused(false);
115
+ if (onBlur) {
116
+ onBlur();
117
+ }
118
+ };
53
119
  return (React.createElement("div", null,
54
120
  label && React.createElement(InputLabel, { htmlFor: name }, label),
55
- React.createElement(StyledInput, { type: type, name: name, placeholder: placeholder, value: value || contextValue, onChange: handleChange, onFocus: onFocus, onBlur: onBlur })));
56
- };
121
+ React.createElement(InputContainer, { animate: error ? (isFocused ? 'errorFocus' : 'error') : undefined },
122
+ 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 }),
123
+ React.createElement(ErrorContainer, { animate: error ? 'show' : undefined, style: { opacity: 0 }, variants: errorVariants, transition: { type: 'spring', duration: 0.3 } },
124
+ React.createElement(ErrorInner, null,
125
+ React.createElement(FontAwesomeIcon, { icon: faExclamationCircle }))),
126
+ error && (React.createElement(ErrorMessage, { style: { opacity: 0, y: 0 }, variants: messageVariants, transition: { type: 'spring', duration: 0.3 } }, error)))));
127
+ });
57
128
  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,6 +1,7 @@
1
1
  import { faPlusCircle } from '@fortawesome/free-solid-svg-icons';
2
2
  import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3
- import React, { useContext, useEffect } from 'react';
3
+ import { motion } from 'framer-motion';
4
+ import React, { useCallback, useContext, useEffect } from 'react';
4
5
  import styled from 'styled-components';
5
6
  import { v4 as uuidv4 } from 'uuid';
6
7
  import useKeypress from '../../hooks/useKeyPress';
@@ -14,11 +15,12 @@ const AddRow = styled.div `
14
15
 
15
16
  margin-top: 4px;
16
17
  `;
17
- const AddRowInner = styled.div `
18
+ const AddRowInner = styled(motion.div) `
18
19
  padding: 4px;
19
20
  display: flex;
20
21
  align-items: center;
21
22
  cursor: pointer;
23
+ overflow: hidden;
22
24
 
23
25
  color: ${(props) => props.theme.colours.defaultFont};
24
26
  font-family: ${(props) => props.theme.fonts.default.family};
@@ -27,23 +29,34 @@ const AddRowInner = styled.div `
27
29
  const IconContainer = styled.div `
28
30
  padding-right: 4px;
29
31
  `;
30
- const LiveList = ({ value: inputValue, name, onChange: inputOnChange }) => {
32
+ const addVariants = {
33
+ hover: { scale: 1.05 },
34
+ };
35
+ const defaultValue = [{ id: uuidv4(), value: '' }];
36
+ const LiveList = ({ value: inputValue, name, onChange: propsOnChange }) => {
31
37
  const { getFocused, requestFocus } = useContext(FocusContext);
32
- const { value: contextValue, onChange: contextOnChange } = useFormNode(name);
33
- const value = contextValue || inputValue;
34
- const onChange = contextOnChange || inputOnChange;
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]);
35
48
  useEffect(() => {
36
49
  if (!(value === null || value === void 0 ? void 0 : value.length)) {
37
- onChange([{ id: uuidv4(), value: '' }]);
50
+ wrappedOnChange([{ id: uuidv4(), value: '' }]);
38
51
  }
39
- }, [value, onChange]);
52
+ }, [value, wrappedOnChange]);
40
53
  useKeypress('Enter', () => {
41
54
  const focusedId = getFocused();
42
55
  const focusedIndex = value.findIndex((val) => val.id === focusedId);
43
56
  const newItem = { id: uuidv4(), value: '' };
44
57
  const newValue = [...value];
45
58
  newValue.splice(focusedIndex + 1, 0, newItem);
46
- onChange(newValue);
59
+ wrappedOnChange(newValue);
47
60
  requestFocus(newItem.id);
48
61
  });
49
62
  useKeypress('Backspace', () => {
@@ -57,7 +70,7 @@ const LiveList = ({ value: inputValue, name, onChange: inputOnChange }) => {
57
70
  }
58
71
  const focusedIndex = value.findIndex((val) => val.id === focusedId);
59
72
  const prevId = focusedIndex > 0 ? value[focusedIndex - 1].id : undefined;
60
- onChange(value.filter((val) => val.id !== focusedId));
73
+ wrappedOnChange(value.filter((val) => val.id !== focusedId));
61
74
  if (prevId) {
62
75
  // Timeout to prevent deleting the last char of the previous row
63
76
  setTimeout(() => {
@@ -66,28 +79,28 @@ const LiveList = ({ value: inputValue, name, onChange: inputOnChange }) => {
66
79
  }
67
80
  });
68
81
  const handleRowChange = (updateId, updateValue) => {
69
- onChange(value.map((v) => (v.id === updateId ? { ...v, value: updateValue } : v)));
82
+ wrappedOnChange(value.map((v) => (v.id === updateId ? { ...v, value: updateValue } : v)));
70
83
  };
71
84
  const handleRowRemove = (id) => {
72
85
  if (value.length === 1) {
73
86
  const withValueCleared = [...value];
74
87
  value[0].value = '';
75
- onChange(withValueCleared);
88
+ wrappedOnChange(withValueCleared);
76
89
  return;
77
90
  }
78
- onChange(value.filter((val) => val.id !== id));
91
+ wrappedOnChange(value.filter((val) => val.id !== id));
79
92
  };
80
93
  const handleRowAdd = () => {
81
94
  const newItem = { id: uuidv4(), value: '' };
82
- onChange([...value, newItem]);
95
+ wrappedOnChange([...value, newItem]);
83
96
  };
84
97
  if (!(value === null || value === void 0 ? void 0 : value.length)) {
85
98
  return null;
86
99
  }
87
100
  return (React.createElement("div", null,
88
- value.map((val) => (React.createElement(LiveListRow, { key: val.id, id: val.id, value: val.value, onChange: (newVal) => handleRowChange(val.id, newVal), onRemove: () => handleRowRemove(val.id) }))),
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) }))),
89
102
  React.createElement(AddRow, null,
90
- React.createElement(AddRowInner, { onClick: handleRowAdd },
103
+ React.createElement(AddRowInner, { style: { scale: 1 }, whileHover: 'hover', variants: addVariants, onClick: handleRowAdd },
91
104
  React.createElement(IconContainer, null,
92
105
  React.createElement(FontAwesomeIcon, { icon: faPlusCircle })),
93
106
  "add"))));
@@ -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 InForm: () => JSX.Element;
5
+ export declare const WithValidation: () => JSX.Element;
5
6
  declare const _default: Meta<import("@storybook/react/types-6-0").Args>;
6
7
  export default _default;
@@ -1,10 +1,14 @@
1
1
  import React, { useState } from 'react';
2
- import { ControlGroup, Form, Heading, ImageUpload, Input, LiveList, Spacer } from '../..';
2
+ import { Button, ButtonGroup, ControlGroup, Form, Heading, ImageUpload, Input, LiveList, Spacer, Text } from '../..';
3
3
  const exampleLiveListValue = [
4
4
  { id: '6f974464-d592-4271-b3ae-2821fffce258', value: '500g chicken' },
5
5
  { id: 'a273d33a-2643-4991-b81d-2beff51d42a8', value: '1/2 cup rice' },
6
6
  { id: '2ecef56f-f725-4f8a-ac84-bd3117e7d50b', value: '1 tbs olive oil' },
7
7
  ];
8
+ const exampleListErrors = {
9
+ '6f974464-d592-4271-b3ae-2821fffce258': 'Ingredient not found',
10
+ 'a273d33a-2643-4991-b81d-2beff51d42a8': 'Ingredient not found',
11
+ };
8
12
  export const Standard = () => {
9
13
  const [value, setValue] = useState(exampleLiveListValue);
10
14
  return (React.createElement(React.Fragment, null,
@@ -19,7 +23,26 @@ export const InForm = () => {
19
23
  React.createElement(ImageUpload, { name: 'image' }),
20
24
  React.createElement(Input, { name: 'name', placeholder: 'Something tasty..' }),
21
25
  React.createElement(Heading.FormHeading, null, "Ingredients"),
22
- React.createElement(LiveList, { name: 'ingredients', value: value, onChange: setValue }))));
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"))));
23
46
  };
24
47
  export default {
25
48
  title: 'Components/LiveList',
@@ -2,8 +2,9 @@
2
2
  interface LiveListRowProps {
3
3
  id: string;
4
4
  value: string;
5
+ error?: string;
5
6
  onChange: (value: string) => void;
6
7
  onRemove: () => void;
7
8
  }
8
- declare const LiveListRow: ({ id, value, onChange, onRemove }: LiveListRowProps) => JSX.Element;
9
+ declare const LiveListRow: ({ id, value, error, onChange, onRemove }: LiveListRowProps) => JSX.Element;
9
10
  export default LiveListRow;
@@ -3,11 +3,13 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3
3
  import { motion } from 'framer-motion';
4
4
  import React, { useContext, useEffect, useRef, useState } from 'react';
5
5
  import styled from 'styled-components';
6
- import { StyledInput } from '../Input/Input.component';
7
6
  import { FocusContext } from './_FocusContext';
7
+ import Input from '../Input/Input.component';
8
8
  const InputContainer = styled(motion.div) `
9
9
  position: relative;
10
- padding: 2px 0;
10
+ margin: 2px 0;
11
+
12
+ background-color: ${(props) => props.theme.colours.controlBackground};
11
13
  `;
12
14
  const RemoveContainer = styled(motion.div) `
13
15
  position: absolute;
@@ -32,7 +34,7 @@ const removeVariants = {
32
34
  hover: { opacity: 1, x: -10 },
33
35
  focus: { opacity: 1, x: -10 },
34
36
  };
35
- const LiveListRow = ({ id, value, onChange, onRemove }) => {
37
+ const LiveListRow = ({ id, value, error, onChange, onRemove }) => {
36
38
  const { onFocus, onBlur, registerFocusable, deregisterFocusable } = useContext(FocusContext);
37
39
  const inputRef = useRef(null);
38
40
  const [isFocused, setIsFocused] = useState(false);
@@ -47,7 +49,7 @@ const LiveListRow = ({ id, value, onChange, onRemove }) => {
47
49
  });
48
50
  }, [id, registerFocusable, deregisterFocusable]);
49
51
  const handleChange = (e) => {
50
- onChange(e.target.value);
52
+ onChange(e);
51
53
  };
52
54
  const handleFocus = () => {
53
55
  setIsFocused(true);
@@ -58,8 +60,8 @@ const LiveListRow = ({ id, value, onChange, onRemove }) => {
58
60
  onBlur(id);
59
61
  };
60
62
  return (React.createElement(InputContainer, { whileHover: 'hover' },
61
- React.createElement(StyledInput, { onFocus: handleFocus, onBlur: handleBlur, ref: inputRef, value: value, onChange: handleChange }),
62
- React.createElement(RemoveContainer, { animate: isFocused ? 'focus' : undefined, style: { opacity: 0 }, variants: removeVariants, transition: { duration: 0.05 } },
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 } },
63
65
  React.createElement(RemoveInner, { onClick: onRemove },
64
66
  React.createElement(FontAwesomeIcon, { icon: faTimes })))));
65
67
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dtdot/lego",
3
- "version": "0.17.0",
3
+ "version": "0.17.1",
4
4
  "description": "Some reusable components for building my applications",
5
5
  "main": "build/index.js",
6
6
  "scripts": {