@pareto-engineering/design-system 4.0.0-alpha.42 → 4.0.0-alpha.44

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.
Files changed (45) hide show
  1. package/dist/cjs/a/Removable/Removable.js +72 -0
  2. package/dist/cjs/a/Removable/index.js +13 -0
  3. package/dist/cjs/a/Removable/styles.scss +33 -0
  4. package/dist/cjs/a/index.js +8 -1
  5. package/dist/cjs/b/Button/Button.js +2 -1
  6. package/dist/cjs/b/Button/styles.scss +3 -3
  7. package/dist/cjs/f/common/Description/Description.js +5 -4
  8. package/dist/cjs/f/fields/Checkbox/styles.scss +1 -0
  9. package/dist/cjs/f/fields/ChoicesInput/styles.scss +1 -1
  10. package/dist/cjs/f/fields/EditorInput/EditorInput.js +0 -1
  11. package/dist/cjs/f/fields/EditorInput/common/Toolbar.js +64 -0
  12. package/dist/cjs/f/fields/EditorInput/styles.scss +26 -0
  13. package/dist/cjs/f/fields/TextInput/TextInput.js +3 -3
  14. package/dist/es/a/Removable/Removable.js +64 -0
  15. package/dist/es/a/Removable/index.js +2 -0
  16. package/dist/es/a/Removable/styles.scss +33 -0
  17. package/dist/es/a/index.js +2 -1
  18. package/dist/es/b/Button/Button.js +2 -1
  19. package/dist/es/b/Button/styles.scss +3 -3
  20. package/dist/es/f/common/Description/Description.js +5 -4
  21. package/dist/es/f/fields/Checkbox/styles.scss +1 -0
  22. package/dist/es/f/fields/ChoicesInput/styles.scss +1 -1
  23. package/dist/es/f/fields/EditorInput/EditorInput.js +0 -1
  24. package/dist/es/f/fields/EditorInput/common/Toolbar.js +66 -2
  25. package/dist/es/f/fields/EditorInput/styles.scss +26 -0
  26. package/dist/es/f/fields/TextInput/TextInput.js +3 -3
  27. package/package.json +3 -3
  28. package/src/stories/a/AppContext.stories.jsx +2 -2
  29. package/src/stories/a/Removable.stories.jsx +22 -0
  30. package/src/stories/b/SocialMediaButton.stories.jsx +2 -2
  31. package/src/stories/colors.js +2 -0
  32. package/src/ui/a/Removable/Removable.jsx +85 -0
  33. package/src/ui/a/Removable/index.js +2 -0
  34. package/src/ui/a/Removable/styles.scss +33 -0
  35. package/src/ui/a/index.js +1 -0
  36. package/src/ui/b/Button/Button.jsx +2 -0
  37. package/src/ui/b/Button/styles.scss +3 -3
  38. package/src/ui/f/common/Description/Description.jsx +5 -4
  39. package/src/ui/f/fields/Checkbox/styles.scss +1 -0
  40. package/src/ui/f/fields/ChoicesInput/styles.scss +1 -1
  41. package/src/ui/f/fields/EditorInput/EditorInput.jsx +0 -1
  42. package/src/ui/f/fields/EditorInput/common/Toolbar.jsx +103 -1
  43. package/src/ui/f/fields/EditorInput/styles.scss +26 -0
  44. package/src/ui/f/fields/TextInput/TextInput.jsx +2 -1
  45. package/tests/__snapshots__/Storyshots.test.js.snap +1027 -11
@@ -1,12 +1,13 @@
1
1
  /* eslint-disable import/no-extraneous-dependencies -- required here */
2
2
  import * as React from 'react';
3
- import { useEffect, useState, useCallback } from 'react';
4
- import { $getSelection, $isRangeSelection, FORMAT_TEXT_COMMAND, FORMAT_ELEMENT_COMMAND, UNDO_COMMAND, REDO_COMMAND } from 'lexical';
3
+ import { useEffect, useState, useCallback, useRef } from 'react';
4
+ import { $getSelection, $isRangeSelection, FORMAT_TEXT_COMMAND, FORMAT_ELEMENT_COMMAND, UNDO_COMMAND, REDO_COMMAND, COMMAND_PRIORITY_NORMAL, createCommand } from 'lexical';
5
5
  import { INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, REMOVE_LIST_COMMAND, $isListNode, ListNode } from '@lexical/list';
6
6
  import { $isAtNodeEnd } from '@lexical/selection';
7
7
  import { $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link';
8
8
  import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
9
9
  import { mergeRegister, $getNearestNodeOfType } from '@lexical/utils';
10
+ import { Popover } from "../../../..";
10
11
  import styleNames from '@pareto-engineering/bem/exports';
11
12
  const baseClassName = styleNames.base;
12
13
  const componentClassName = 'toolbar';
@@ -26,6 +27,8 @@ const getSelectedNode = selection => {
26
27
  }
27
28
  return $isAtNodeEnd(anchor) ? focusNode : anchorNode;
28
29
  };
30
+ const defaultColor = 'var(--paragraph)';
31
+ const colorOptions = ['red', 'blue', 'green', 'yellow', 'orange', 'purple', 'pink', 'brown'];
29
32
  const Toolbar = () => {
30
33
  const [editor] = useLexicalComposerContext();
31
34
  const [isBold, setIsBold] = useState(false);
@@ -34,6 +37,8 @@ const Toolbar = () => {
34
37
  const [blockType, setBlockType] = useState('paragraph');
35
38
  const [isLink, setIsLink] = useState(false);
36
39
  const [isUnderline, setIsUnderline] = useState(false);
40
+ const [color, setColor] = useState(defaultColor);
41
+ const colorMenuRef = useRef(false);
37
42
  const formatBulletList = () => {
38
43
  if (blockType !== 'ul') {
39
44
  editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND);
@@ -72,6 +77,18 @@ const Toolbar = () => {
72
77
  setBlockType(element);
73
78
  }
74
79
 
80
+ // Check nodes for color
81
+ const nodes = selection.getNodes().filter(node => node.getType() === 'text');
82
+ nodes.forEach(node => {
83
+ const style = node.getStyle();
84
+ const colorProperty = style.match(/color: ([^;]+)/);
85
+ if (colorProperty) {
86
+ setColor(colorProperty[1]);
87
+ return;
88
+ }
89
+ setColor(false);
90
+ });
91
+
75
92
  // Check selection text styles
76
93
  setIsBold(selection.hasFormat('bold'));
77
94
  setIsItalic(selection.hasFormat('italic'));
@@ -89,6 +106,22 @@ const Toolbar = () => {
89
106
  }
90
107
  }
91
108
  }, [editor]);
109
+ const UPDATE_COLOR_COMMAND = createCommand();
110
+ editor.registerCommand(UPDATE_COLOR_COMMAND, payload => {
111
+ const selection = $getSelection();
112
+ const nodes = selection?.extract().filter(node => node.getType() === 'text');
113
+ nodes?.forEach(node => {
114
+ const style = node.getStyle();
115
+ const colorProperty = style?.match(/color: ([^;]+)/);
116
+ if (colorProperty && color !== payload) {
117
+ node.setStyle(style.replace(colorProperty[0], `color: ${payload}`));
118
+ } else if (colorProperty) {
119
+ node.setStyle(`color: ${defaultColor}`);
120
+ } else {
121
+ node.setStyle(`color: ${payload}`);
122
+ }
123
+ });
124
+ }, COMMAND_PRIORITY_NORMAL);
92
125
  useEffect(() => mergeRegister(editor.registerUpdateListener(({
93
126
  editorState
94
127
  }) => {
@@ -96,6 +129,10 @@ const Toolbar = () => {
96
129
  updateToolbar();
97
130
  });
98
131
  })), [updateToolbar, editor]);
132
+ const dispatchUpdateColor = useCallback((e, payload) => {
133
+ e.stopPropagation();
134
+ editor.dispatchCommand(UPDATE_COLOR_COMMAND, payload);
135
+ }, [editor]);
99
136
  return /*#__PURE__*/React.createElement("div", {
100
137
  className: `${baseClassName} ${componentClassName}`
101
138
  }, /*#__PURE__*/React.createElement("div", {
@@ -125,6 +162,33 @@ const Toolbar = () => {
125
162
  }, /*#__PURE__*/React.createElement("span", {
126
163
  className: "icon"
127
164
  }, "?")), /*#__PURE__*/React.createElement("button", {
165
+ type: "button",
166
+ className: color && color !== defaultColor ? 'active color-menu-button' : 'color-menu-button',
167
+ onClick: () => editor.dispatchCommand(UPDATE_COLOR_COMMAND, color !== defaultColor ? defaultColor : color),
168
+ ref: colorMenuRef,
169
+ style: {
170
+ position: 'relative'
171
+ }
172
+ }, /*#__PURE__*/React.createElement("span", {
173
+ className: "icon",
174
+ style: {
175
+ color
176
+ }
177
+ }, "Q"), /*#__PURE__*/React.createElement(Popover, {
178
+ parentRef: colorMenuRef
179
+ }, /*#__PURE__*/React.createElement("div", {
180
+ className: "color-menu"
181
+ }, colorOptions.map(option => /*#__PURE__*/React.createElement("span", {
182
+ role: "button",
183
+ className: "icon color-option",
184
+ style: {
185
+ color: option
186
+ },
187
+ onClick: e => dispatchUpdateColor(e, option),
188
+ onKeyDown: e => dispatchUpdateColor(e, option),
189
+ tabIndex: 0,
190
+ key: option
191
+ }, "o"))))), /*#__PURE__*/React.createElement("button", {
128
192
  type: "button",
129
193
  className: isLink ? 'active' : undefined,
130
194
  onClick: () => formatLink()
@@ -15,6 +15,7 @@ $active-background: var(--hard-background-inputs);
15
15
  $default-background: var(--background-inputs);
16
16
  $default-icon-color: var(--on-background-inputs);
17
17
  $disabled-background: var(--background-inputs-30);
18
+ $default-color-menu-padding: .5em .25em;
18
19
 
19
20
  .#{bem.$base}.editor-input {
20
21
  &.#{bem.$base}.input-wrapper {
@@ -62,6 +63,31 @@ $disabled-background: var(--background-inputs-30);
62
63
  }
63
64
  }
64
65
 
66
+ .color-menu-button {
67
+ &:hover {
68
+ > .#{bem.$base}.popover {
69
+ display: block;
70
+ }
71
+ }
72
+
73
+ > .#{bem.$base}.popover {
74
+ padding: $default-color-menu-padding;
75
+
76
+ .color-menu {
77
+ display: flex;
78
+ flex-wrap: wrap;
79
+ gap: calc($default-gap / 2);
80
+ justify-content: center;
81
+ max-width: 10em;
82
+ min-width: 5em;
83
+ }
84
+
85
+ .color-option:hover {
86
+ opacity: .5;
87
+ }
88
+ }
89
+ }
90
+
65
91
  > .content-editable {
66
92
  background: $default-background;
67
93
  border: $default-border;
@@ -34,8 +34,8 @@ const TextInput = ({
34
34
  desktopLabelSpan,
35
35
  inputSpan,
36
36
  desktopInputSpan,
37
- symbol
38
- // ...otherProps
37
+ symbol,
38
+ ...otherProps
39
39
  }) => {
40
40
  useInsertionEffect(() => {
41
41
  import("./styles.scss");
@@ -70,7 +70,7 @@ const TextInput = ({
70
70
  disabled: disabled,
71
71
  placeholder: placeholder,
72
72
  autoComplete: autoComplete
73
- }, field)), /*#__PURE__*/React.createElement(FormDescription, {
73
+ }, field, otherProps)), /*#__PURE__*/React.createElement(FormDescription, {
74
74
  className: "s-1",
75
75
  description: description,
76
76
  name: name
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pareto-engineering/design-system",
3
- "version": "4.0.0-alpha.42",
3
+ "version": "4.0.0-alpha.44",
4
4
  "description": "",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/es/index.js",
@@ -51,7 +51,7 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "@lexical/react": "^0.11.3",
54
- "@pareto-engineering/assets": "^4.0.0-alpha.40",
54
+ "@pareto-engineering/assets": "^4.0.0-alpha.43",
55
55
  "@pareto-engineering/bem": "^4.0.0-alpha.20",
56
56
  "@pareto-engineering/styles": "^4.0.0-alpha.39",
57
57
  "@pareto-engineering/utils": "^4.0.0-alpha.33",
@@ -70,5 +70,5 @@
70
70
  "relay-test-utils": "^15.0.0"
71
71
  },
72
72
  "browserslist": "> 2%",
73
- "gitHead": "316af25fc0da0320febe6027eb67f4a8288dec3e"
73
+ "gitHead": "02e37ad192a8bffa34cd4b7af353aa3e1c71acc5"
74
74
  }
@@ -22,7 +22,7 @@ export const Base = () => (
22
22
  config={{
23
23
  APP:{
24
24
  NAME :'Pareto',
25
- CANONICAL :'https://hellopareto.com',
25
+ CANONICAL :'https://pareto.ai',
26
26
  SUPPORT_EMAIL:'support@hellopareto.com',
27
27
  TITLE_SUFFIX :'| Pareto',
28
28
  },
@@ -32,7 +32,7 @@ export const Base = () => (
32
32
  TWITTER :'https://www.twitter.com/hellopareto',
33
33
  },
34
34
  EXTRA:{
35
- SURVEY:'https://survey.hellopareto.com',
35
+ SURVEY:'https://survey.pareto.ai',
36
36
  },
37
37
  }}
38
38
 
@@ -0,0 +1,22 @@
1
+ /* @pareto-engineering/generator-front 1.1.1-alpha.1 */
2
+ import * as React from 'react'
3
+
4
+ import { Removable } from 'ui'
5
+
6
+ export default {
7
+ title :'a/Removable',
8
+ component :Removable,
9
+ subcomponents:{
10
+ // Item:Removable.Item
11
+ },
12
+ decorators:[
13
+ // storyfn => <div className="">{ storyfn() }</div>,
14
+ ],
15
+ argTypes:{
16
+ backgroundColor:{ control: 'color' },
17
+ },
18
+ }
19
+
20
+ export const Base = () => (
21
+ <Removable>Sample Removable</Removable>
22
+ )
@@ -33,7 +33,7 @@ Base.args = {
33
33
  config:{
34
34
  SITE:{
35
35
  NAME :'Pareto',
36
- CANONICAL :'https://hellopareto.com',
36
+ CANONICAL :'https://pareto.ai',
37
37
  SUPPORT_EMAIL:'support@hellopareto.com',
38
38
  TITLE_SUFFIX :'| Pareto',
39
39
  },
@@ -44,7 +44,7 @@ Base.args = {
44
44
  LINKEDIN :'https://www.linkedin.com/company/hellopareto',
45
45
  },
46
46
  EXTRA:{
47
- SURVEY:'https://survey.hellopareto.com',
47
+ SURVEY:'https://survey.pareto.ai',
48
48
  },
49
49
  },
50
50
  }
@@ -51,6 +51,8 @@ const UI = [
51
51
  'transparent',
52
52
  'highlighted',
53
53
  'disabled',
54
+ 'ui-main',
55
+ 'ui-main-2',
54
56
  ]
55
57
 
56
58
  const SM = [
@@ -0,0 +1,85 @@
1
+ /* @pareto-engineering/generator-front 1.1.1-alpha.1 */
2
+ import * as React from 'react'
3
+
4
+ import { useInsertionEffect } from 'react'
5
+
6
+ import PropTypes from 'prop-types'
7
+
8
+ import styleNames from '@pareto-engineering/bem/exports'
9
+
10
+ // Local Definitions
11
+
12
+ const baseClassName = styleNames.base
13
+
14
+ const componentClassName = 'removable'
15
+
16
+ /**
17
+ * This is a wrapper component that adds a close button to its children.
18
+ */
19
+ const Removable = ({
20
+ id,
21
+ className:userClassName,
22
+ style,
23
+ handleClose,
24
+ children,
25
+ // ...otherProps
26
+ }) => {
27
+ useInsertionEffect(() => {
28
+ import('./styles.scss')
29
+ }, [])
30
+
31
+ return (
32
+ <div
33
+ id={id}
34
+ className={[
35
+
36
+ baseClassName,
37
+
38
+ componentClassName,
39
+ userClassName,
40
+ ]
41
+ .filter((e) => e)
42
+ .join(' ')}
43
+ style={style}
44
+ // {...otherProps}
45
+ >
46
+ {children}
47
+ <button type="button" className="close-button" onClick={handleClose || (() => null)}>
48
+ <span className="icon">Y</span>
49
+ </button>
50
+ </div>
51
+ )
52
+ }
53
+
54
+ Removable.propTypes = {
55
+ /**
56
+ * The HTML id for this element
57
+ */
58
+ id:PropTypes.string,
59
+
60
+ /**
61
+ * The HTML class names for this element
62
+ */
63
+ className:PropTypes.string,
64
+
65
+ /**
66
+ * The React-written, css properties for this element.
67
+ */
68
+ style:PropTypes.objectOf(PropTypes.string),
69
+
70
+ /**
71
+ * The function to call when the close button is clicked
72
+ */
73
+ handleClose:PropTypes.func,
74
+
75
+ /**
76
+ * The children JSX
77
+ */
78
+ children:PropTypes.node,
79
+ }
80
+
81
+ Removable.defaultProps = {
82
+ // someProp:false
83
+ }
84
+
85
+ export default Removable
@@ -0,0 +1,2 @@
1
+ /* @pareto-engineering/generator-front 1.1.1-alpha.1 */
2
+ export { default as Removable } from './Removable'
@@ -0,0 +1,33 @@
1
+ /* @pareto-engineering/generator-front 1.1.1-alpha.1 */
2
+
3
+ @use "@pareto-engineering/bem";
4
+
5
+ $default-padding: .5rem;
6
+
7
+ .#{bem.$base}.removable {
8
+ align-items: center;
9
+ background-color: var(--background-inputs);
10
+ border: var(--theme-default-border-style) var(--main2);
11
+ border-radius: $default-padding;
12
+ display: flex;
13
+ justify-content: space-between;
14
+ padding: $default-padding;
15
+
16
+ > :first-child {
17
+ flex-grow: 1;
18
+ }
19
+
20
+ .close-button {
21
+ background-color: transparent;
22
+ border: none;
23
+ color: var(--metadata);
24
+ cursor: pointer;
25
+ margin-left: var(--gap);
26
+
27
+ &:hover {
28
+ color: var(--hard-metadata);
29
+ }
30
+ }
31
+ }
32
+
33
+
package/src/ui/a/index.js CHANGED
@@ -23,3 +23,4 @@ export { AnimatedBlobs } from './AnimatedBlobs'
23
23
  export { Tip } from './Tip'
24
24
  export { AnimatedGradient } from './AnimatedGradient'
25
25
  export { TextSteps } from './TextSteps'
26
+ export { Removable } from './Removable'
@@ -20,6 +20,7 @@ const Button = ({
20
20
  children,
21
21
  isLoading,
22
22
  color,
23
+ textColor,
23
24
  isCompact,
24
25
  isGhost,
25
26
  isSimple,
@@ -43,6 +44,7 @@ const Button = ({
43
44
  componentClassName,
44
45
  userClassName,
45
46
  `x-${color}`,
47
+ textColor && `y-${textColor}`,
46
48
  isGhost && styleNames.modifierGhost,
47
49
  isCompact && styleNames.modifierCompact,
48
50
  isSimple && styleNames.modifierSimple,
@@ -92,7 +92,7 @@ $default-animation-time: .31s;
92
92
  &.#{bem.$modifier-ghost} {
93
93
  background: transparent;
94
94
  border: 1px solid var(--x, var(--#{$default-color}));
95
- color: var(--x, var(--#{$default-color}));
95
+ color: var(--y, var(--x, var(--#{$default-color})));
96
96
 
97
97
  &:hover,
98
98
  &:focus,
@@ -103,12 +103,12 @@ $default-animation-time: .31s;
103
103
  &:not(:disabled) {
104
104
  &:hover {
105
105
  border: 1px solid var(--soft-x, var(--soft-#{$default-color}));
106
- color: var(--soft-x, var(--soft-#{$default-color}));
106
+ color: var(--soft-y, var(--soft-x, var(--soft-#{$default-color})));
107
107
  }
108
108
 
109
109
  &:focus {
110
110
  border: 1px solid var(--hard-x, var(--hard-#{$default-color}));
111
- color: var(--hard-x, var(--hard-#{$default-color}));
111
+ color: var(--hard-y, var(--hard-x, var(--hard-#{$default-color})));
112
112
  }
113
113
  }
114
114
 
@@ -31,9 +31,10 @@ const Description = ({
31
31
  import('./styles.scss')
32
32
  }, [])
33
33
 
34
- const [field, meta] = useField(name)
34
+ const [, meta] = useField(name)
35
+ const hasError = meta.touched && meta.error
35
36
 
36
- if (description || ((meta.touched || field.value) && meta.error)) {
37
+ if (hasError || description) {
37
38
  return (
38
39
  <div
39
40
  id={id}
@@ -41,13 +42,13 @@ const Description = ({
41
42
  baseClassName,
42
43
  componentClassName,
43
44
  userClassName,
44
- meta.error ? 'x-error' : `x-${color}`,
45
+ hasError ? 'x-error' : `x-${color}`,
45
46
  ]
46
47
  .filter((e) => e)
47
48
  .join(' ')}
48
49
  style={style}
49
50
  >
50
- { meta.error || description}
51
+ { hasError ? meta.error : description}
51
52
  </div>
52
53
  )
53
54
  }
@@ -9,5 +9,6 @@
9
9
  align-items: flex-start;
10
10
  display: flex;
11
11
  flex-direction: column;
12
+ justify-content: center;
12
13
  }
13
14
  }
@@ -36,7 +36,7 @@ $disabled-background: var(--background-inputs-30);
36
36
 
37
37
  input {
38
38
  opacity: 0;
39
- position: absolute;
39
+ position: fixed;
40
40
  visibility: none;
41
41
  z-index: -1;
42
42
  }
@@ -61,7 +61,6 @@ const EditorInput = ({
61
61
  JSON.parse(value)
62
62
  return value
63
63
  } catch {
64
- // eslint-disable-next-line no-useless-escape
65
64
  const defaultValue = {
66
65
  root:{
67
66
  children:[
@@ -1,7 +1,12 @@
1
1
  /* eslint-disable import/no-extraneous-dependencies -- required here */
2
2
  import * as React from 'react'
3
3
 
4
- import { useEffect, useState, useCallback } from 'react'
4
+ import {
5
+ useEffect,
6
+ useState,
7
+ useCallback,
8
+ useRef,
9
+ } from 'react'
5
10
 
6
11
  import {
7
12
  $getSelection,
@@ -10,6 +15,8 @@ import {
10
15
  FORMAT_ELEMENT_COMMAND,
11
16
  UNDO_COMMAND,
12
17
  REDO_COMMAND,
18
+ COMMAND_PRIORITY_NORMAL,
19
+ createCommand,
13
20
  } from 'lexical'
14
21
  import {
15
22
  INSERT_ORDERED_LIST_COMMAND,
@@ -27,6 +34,9 @@ import {
27
34
  } from '@lexical/link'
28
35
  import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
29
36
  import { mergeRegister, $getNearestNodeOfType } from '@lexical/utils'
37
+
38
+ import { Popover } from 'ui'
39
+
30
40
  import styleNames from '@pareto-engineering/bem/exports'
31
41
 
32
42
  const baseClassName = styleNames.base
@@ -47,6 +57,19 @@ const getSelectedNode = (selection) => {
47
57
  return $isAtNodeEnd(anchor) ? focusNode : anchorNode
48
58
  }
49
59
 
60
+ const defaultColor = 'var(--paragraph)'
61
+
62
+ const colorOptions = [
63
+ 'red',
64
+ 'blue',
65
+ 'green',
66
+ 'yellow',
67
+ 'orange',
68
+ 'purple',
69
+ 'pink',
70
+ 'brown',
71
+ ]
72
+
50
73
  const Toolbar = () => {
51
74
  const [editor] = useLexicalComposerContext()
52
75
  const [isBold, setIsBold] = useState(false)
@@ -55,6 +78,9 @@ const Toolbar = () => {
55
78
  const [blockType, setBlockType] = useState('paragraph')
56
79
  const [isLink, setIsLink] = useState(false)
57
80
  const [isUnderline, setIsUnderline] = useState(false)
81
+ const [color, setColor] = useState(defaultColor)
82
+
83
+ const colorMenuRef = useRef(false)
58
84
 
59
85
  const formatBulletList = () => {
60
86
  if (blockType !== 'ul') {
@@ -99,6 +125,18 @@ const Toolbar = () => {
99
125
  setBlockType(element)
100
126
  }
101
127
 
128
+ // Check nodes for color
129
+ const nodes = selection.getNodes().filter((node) => node.getType() === 'text')
130
+ nodes.forEach((node) => {
131
+ const style = node.getStyle()
132
+ const colorProperty = style.match(/color: ([^;]+)/)
133
+ if (colorProperty) {
134
+ setColor(colorProperty[1])
135
+ return
136
+ }
137
+ setColor(false)
138
+ })
139
+
102
140
  // Check selection text styles
103
141
  setIsBold(selection.hasFormat('bold'))
104
142
  setIsItalic(selection.hasFormat('italic'))
@@ -117,6 +155,24 @@ const Toolbar = () => {
117
155
  }
118
156
  }, [editor])
119
157
 
158
+ const UPDATE_COLOR_COMMAND = createCommand()
159
+
160
+ editor.registerCommand(UPDATE_COLOR_COMMAND, (payload) => {
161
+ const selection = $getSelection()
162
+ const nodes = selection?.extract().filter((node) => node.getType() === 'text')
163
+ nodes?.forEach((node) => {
164
+ const style = node.getStyle()
165
+ const colorProperty = style?.match(/color: ([^;]+)/)
166
+ if (colorProperty && color !== payload) {
167
+ node.setStyle(style.replace(colorProperty[0], `color: ${payload}`))
168
+ } else if (colorProperty) {
169
+ node.setStyle(`color: ${defaultColor}`)
170
+ } else {
171
+ node.setStyle(`color: ${payload}`)
172
+ }
173
+ })
174
+ }, COMMAND_PRIORITY_NORMAL)
175
+
120
176
  useEffect(() => mergeRegister(
121
177
  editor.registerUpdateListener(({ editorState }) => {
122
178
  editorState.read(() => {
@@ -125,6 +181,11 @@ const Toolbar = () => {
125
181
  }),
126
182
  ), [updateToolbar, editor])
127
183
 
184
+ const dispatchUpdateColor = useCallback((e, payload) => {
185
+ e.stopPropagation()
186
+ editor.dispatchCommand(UPDATE_COLOR_COMMAND, payload)
187
+ }, [editor])
188
+
128
189
  return (
129
190
  <div className={`${baseClassName} ${componentClassName}`}>
130
191
  <div className="group">
@@ -164,6 +225,47 @@ const Toolbar = () => {
164
225
  ?
165
226
  </span>
166
227
  </button>
228
+ <button
229
+ type="button"
230
+ className={color && color !== defaultColor ? 'active color-menu-button' : 'color-menu-button'}
231
+ onClick={() => editor.dispatchCommand(UPDATE_COLOR_COMMAND,
232
+ color !== defaultColor ? defaultColor : color)}
233
+ ref={colorMenuRef}
234
+ style={{ position: 'relative' }}
235
+ >
236
+ <span
237
+ className="icon"
238
+ style={{
239
+ color,
240
+ }}
241
+ >
242
+ Q
243
+ </span>
244
+ <Popover
245
+ parentRef={colorMenuRef}
246
+ >
247
+ <div className="color-menu">
248
+ {
249
+ colorOptions.map((option) => (
250
+ <span
251
+ role="button"
252
+ className="icon color-option"
253
+ style={{
254
+ color:option,
255
+ }}
256
+ onClick={(e) => dispatchUpdateColor(e, option)}
257
+ onKeyDown={(e) => dispatchUpdateColor(e, option)}
258
+ tabIndex={0}
259
+ key={option}
260
+ >
261
+ o
262
+ </span>
263
+ ))
264
+ }
265
+ </div>
266
+ </Popover>
267
+ </button>
268
+
167
269
  <button
168
270
  type="button"
169
271
  className={isLink ? 'active' : undefined}