@dtdot/lego 2.3.3 → 2.4.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.
@@ -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
- &:hover {
20
- background-color: ${(props) => props.useHover && getThemeStatusColour(props.variant, props.theme).hover};
21
- }
20
+ `;
21
+ const TextSpan = styled.span `
22
+ cursor: default;
22
23
  `;
23
24
  const ActionSpan = styled.span `
24
- vertical-align: middle;
25
- padding: 3px 7px;
26
- margin: -3px -7px -3px 0;
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, useHover: !!actionIcon, "data-testid": 'badge' },
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, { useHover: false, 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)))));
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;
@@ -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';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dtdot/lego",
3
- "version": "2.3.3",
3
+ "version": "2.4.0",
4
4
  "description": "Some reusable components for building my applications",
5
5
  "main": "build/index.js",
6
6
  "scripts": {