@dtdot/lego 0.16.4 → 0.17.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.
@@ -1,5 +1,5 @@
1
- declare function useFormNode(key: string): {
2
- value: any;
3
- onChange: (_value: any) => void;
1
+ declare function useFormNode<T>(key: string): {
2
+ value: T;
3
+ onChange: (_value: T) => void;
4
4
  };
5
5
  export default useFormNode;
@@ -1,4 +1,5 @@
1
1
  /// <reference types="react" />
2
+ export declare const StyledInput: import("styled-components").StyledComponent<"input", import("styled-components").DefaultTheme, {}, never>;
2
3
  export interface IInputProps {
3
4
  name: string;
4
5
  label?: string;
@@ -6,6 +7,8 @@ export interface IInputProps {
6
7
  type?: string;
7
8
  value?: string;
8
9
  onChange?: (value: any) => void;
10
+ onFocus?: () => void;
11
+ onBlur?: () => void;
9
12
  }
10
- declare const Input: ({ label, name, placeholder, type, value, onChange }: IInputProps) => JSX.Element;
13
+ declare const Input: ({ label, name, placeholder, type, value, onChange, onFocus, onBlur }: IInputProps) => JSX.Element;
11
14
  export default Input;
@@ -11,7 +11,7 @@ const InputLabel = styled.label `
11
11
  font-family: ${(props) => props.theme.fonts.default.family};
12
12
  font-size: ${(props) => props.theme.fonts.default.size};
13
13
  `;
14
- const StyledInput = styled.input `
14
+ export const StyledInput = styled.input `
15
15
  outline: none;
16
16
  box-shadow: none;
17
17
 
@@ -40,7 +40,7 @@ const StyledInput = styled.input `
40
40
  color: ${(props) => getThemeControlColours(props.theme).placeholder};
41
41
  }
42
42
  `;
43
- const Input = ({ label, name, placeholder, type = 'text', value, onChange }) => {
43
+ const Input = ({ label, name, placeholder, type = 'text', value, onChange, onFocus, onBlur }) => {
44
44
  const { value: contextValue, onChange: contextOnChange } = useFormNode(name);
45
45
  const handleChange = (e) => {
46
46
  if (onChange) {
@@ -52,6 +52,6 @@ const Input = ({ label, name, placeholder, type = 'text', value, onChange }) =>
52
52
  };
53
53
  return (React.createElement("div", null,
54
54
  label && React.createElement(InputLabel, { htmlFor: name }, label),
55
- React.createElement(StyledInput, { type: type, name: name, placeholder: placeholder, value: value || contextValue, onChange: handleChange })));
55
+ React.createElement(StyledInput, { type: type, name: name, placeholder: placeholder, value: value || contextValue, onChange: handleChange, onFocus: onFocus, onBlur: onBlur })));
56
56
  };
57
57
  export default Input;
@@ -1,10 +1,10 @@
1
- import React from 'react';
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?: (event: React.ChangeEvent<HTMLInputElement>) => void;
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: onChange || contextOnChange })));
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,95 @@
1
+ import { faPlusCircle } from '@fortawesome/free-solid-svg-icons';
2
+ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3
+ import React, { useContext, useEffect } from 'react';
4
+ import styled from 'styled-components';
5
+ import { v4 as uuidv4 } from 'uuid';
6
+ import useKeypress from '../../hooks/useKeyPress';
7
+ import useFormNode from '../Form/useFormNode.hook';
8
+ import { FocusContext, FocusContextProviderHOC } from './_FocusContext';
9
+ import LiveListRow from './_LiveListRow';
10
+ const AddRow = styled.div `
11
+ width: 100%;
12
+ display: flex;
13
+ justify-content: center;
14
+
15
+ margin-top: 4px;
16
+ `;
17
+ const AddRowInner = styled.div `
18
+ padding: 4px;
19
+ display: flex;
20
+ align-items: center;
21
+ cursor: pointer;
22
+
23
+ color: ${(props) => props.theme.colours.defaultFont};
24
+ font-family: ${(props) => props.theme.fonts.default.family};
25
+ font-size: ${(props) => props.theme.fonts.default.size};
26
+ `;
27
+ const IconContainer = styled.div `
28
+ padding-right: 4px;
29
+ `;
30
+ const LiveList = ({ value: inputValue, name, onChange: inputOnChange }) => {
31
+ const { getFocused, requestFocus } = useContext(FocusContext);
32
+ const { value: contextValue, onChange: contextOnChange } = useFormNode(name);
33
+ const value = contextValue || inputValue;
34
+ const onChange = contextOnChange || inputOnChange;
35
+ useEffect(() => {
36
+ if (!(value === null || value === void 0 ? void 0 : value.length)) {
37
+ onChange([{ id: uuidv4(), value: '' }]);
38
+ }
39
+ }, [value, onChange]);
40
+ useKeypress('Enter', () => {
41
+ const focusedId = getFocused();
42
+ const focusedIndex = value.findIndex((val) => val.id === focusedId);
43
+ const newItem = { id: uuidv4(), value: '' };
44
+ const newValue = [...value];
45
+ newValue.splice(focusedIndex + 1, 0, newItem);
46
+ onChange(newValue);
47
+ requestFocus(newItem.id);
48
+ });
49
+ useKeypress('Backspace', () => {
50
+ const focusedId = getFocused();
51
+ if (!focusedId) {
52
+ return;
53
+ }
54
+ const focusedValue = value.find((val) => val.id === focusedId);
55
+ if (focusedValue === null || focusedValue === void 0 ? void 0 : focusedValue.value) {
56
+ return;
57
+ }
58
+ const focusedIndex = value.findIndex((val) => val.id === focusedId);
59
+ const prevId = focusedIndex > 0 ? value[focusedIndex - 1].id : undefined;
60
+ onChange(value.filter((val) => val.id !== focusedId));
61
+ if (prevId) {
62
+ // Timeout to prevent deleting the last char of the previous row
63
+ setTimeout(() => {
64
+ requestFocus(prevId);
65
+ });
66
+ }
67
+ });
68
+ const handleRowChange = (updateId, updateValue) => {
69
+ onChange(value.map((v) => (v.id === updateId ? { ...v, value: updateValue } : v)));
70
+ };
71
+ const handleRowRemove = (id) => {
72
+ if (value.length === 1) {
73
+ const withValueCleared = [...value];
74
+ value[0].value = '';
75
+ onChange(withValueCleared);
76
+ return;
77
+ }
78
+ onChange(value.filter((val) => val.id !== id));
79
+ };
80
+ const handleRowAdd = () => {
81
+ const newItem = { id: uuidv4(), value: '' };
82
+ onChange([...value, newItem]);
83
+ };
84
+ if (!(value === null || value === void 0 ? void 0 : value.length)) {
85
+ return null;
86
+ }
87
+ 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) }))),
89
+ React.createElement(AddRow, null,
90
+ React.createElement(AddRowInner, { onClick: handleRowAdd },
91
+ React.createElement(IconContainer, null,
92
+ React.createElement(FontAwesomeIcon, { icon: faPlusCircle })),
93
+ "add"))));
94
+ };
95
+ export default FocusContextProviderHOC(LiveList);
@@ -0,0 +1,6 @@
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
+ declare const _default: Meta<import("@storybook/react/types-6-0").Args>;
6
+ export default _default;
@@ -0,0 +1,27 @@
1
+ import React, { useState } from 'react';
2
+ import { ControlGroup, Form, Heading, ImageUpload, Input, LiveList, Spacer } 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
+ export const Standard = () => {
9
+ const [value, setValue] = useState(exampleLiveListValue);
10
+ return (React.createElement(React.Fragment, null,
11
+ React.createElement(Heading.FormHeading, null, "Ingredients"),
12
+ React.createElement(Spacer, { size: '1x' }),
13
+ React.createElement(LiveList, { value: value, onChange: setValue })));
14
+ };
15
+ export const InForm = () => {
16
+ const [value, setValue] = useState({});
17
+ return (React.createElement(Form, { value: value, onChange: setValue },
18
+ React.createElement(ControlGroup, { variation: 'comfortable' },
19
+ React.createElement(ImageUpload, { name: 'image' }),
20
+ React.createElement(Input, { name: 'name', placeholder: 'Something tasty..' }),
21
+ React.createElement(Heading.FormHeading, null, "Ingredients"),
22
+ React.createElement(LiveList, { name: 'ingredients', value: value, onChange: setValue }))));
23
+ };
24
+ export default {
25
+ title: 'Components/LiveList',
26
+ component: LiveList,
27
+ };
@@ -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,9 @@
1
+ /// <reference types="react" />
2
+ interface LiveListRowProps {
3
+ id: string;
4
+ value: string;
5
+ onChange: (value: string) => void;
6
+ onRemove: () => void;
7
+ }
8
+ declare const LiveListRow: ({ id, value, onChange, onRemove }: LiveListRowProps) => JSX.Element;
9
+ export default LiveListRow;
@@ -0,0 +1,66 @@
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 { StyledInput } from '../Input/Input.component';
7
+ import { FocusContext } from './_FocusContext';
8
+ const InputContainer = styled(motion.div) `
9
+ position: relative;
10
+ padding: 2px 0;
11
+ `;
12
+ const RemoveContainer = styled(motion.div) `
13
+ position: absolute;
14
+ right: 0;
15
+ top: 0;
16
+ height: 100%;
17
+ display: flex;
18
+ align-items: center;
19
+
20
+ color: ${(props) => props.theme.colours.defaultFont};
21
+ `;
22
+ const RemoveInner = styled.div `
23
+ width: 24px;
24
+ height: 24px;
25
+
26
+ display: flex;
27
+ justify-content: center;
28
+ align-items: center;
29
+ cursor: pointer;
30
+ `;
31
+ const removeVariants = {
32
+ hover: { opacity: 1, x: -10 },
33
+ focus: { opacity: 1, x: -10 },
34
+ };
35
+ const LiveListRow = ({ id, value, onChange, onRemove }) => {
36
+ const { onFocus, onBlur, registerFocusable, deregisterFocusable } = useContext(FocusContext);
37
+ const inputRef = useRef(null);
38
+ const [isFocused, setIsFocused] = useState(false);
39
+ useEffect(() => {
40
+ registerFocusable(id, () => {
41
+ if (inputRef.current) {
42
+ inputRef.current.focus();
43
+ }
44
+ return () => {
45
+ deregisterFocusable(id);
46
+ };
47
+ });
48
+ }, [id, registerFocusable, deregisterFocusable]);
49
+ const handleChange = (e) => {
50
+ onChange(e.target.value);
51
+ };
52
+ const handleFocus = () => {
53
+ setIsFocused(true);
54
+ onFocus(id);
55
+ };
56
+ const handleBlur = () => {
57
+ setIsFocused(false);
58
+ onBlur(id);
59
+ };
60
+ 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(RemoveInner, { onClick: onRemove },
64
+ React.createElement(FontAwesomeIcon, { icon: faTimes })))));
65
+ };
66
+ export default LiveListRow;
@@ -0,0 +1,2 @@
1
+ declare const useKeypress: (key: string, handler: () => void) => void;
2
+ export default useKeypress;
@@ -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.16.4",
3
+ "version": "0.17.0",
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
  }