@kaizen/components 1.48.0 → 1.49.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.
Files changed (72) hide show
  1. package/dist/cjs/DateInput/DateInput/DateInput.cjs +1 -0
  2. package/dist/cjs/DatePicker/DatePicker.cjs +7 -3
  3. package/dist/cjs/DateRangePicker/DateRangePicker.cjs +7 -2
  4. package/dist/cjs/EmptyState/EmptyState.cjs +0 -1
  5. package/dist/cjs/RichTextEditor/EditableRichTextContent/EditableRichTextContent.cjs +1 -0
  6. package/dist/cjs/RichTextEditor/EditableRichTextContent/EditableRichTextContent.module.scss.cjs +1 -0
  7. package/dist/cjs/RichTextEditor/RichTextEditor/RichTextEditor.cjs +1 -0
  8. package/dist/cjs/RichTextEditor/RichTextEditor/RichTextEditor.module.scss.cjs +1 -0
  9. package/dist/cjs/RichTextEditor/utils/plugins/LinkManager/components/LinkModal/LinkModal.cjs +5 -1
  10. package/dist/cjs/RichTextEditor/utils/plugins/LinkManager/components/LinkModal/LinkModal.module.scss.cjs +6 -0
  11. package/dist/cjs/RichTextEditor/utils/plugins/LinkManager/validation.cjs +8 -1
  12. package/dist/cjs/Select/Select.cjs +6 -2
  13. package/dist/cjs/__future__/Select/Select.cjs +12 -11
  14. package/dist/esm/DateInput/DateInput/DateInput.mjs +1 -0
  15. package/dist/esm/DatePicker/DatePicker.mjs +7 -3
  16. package/dist/esm/DateRangePicker/DateRangePicker.mjs +7 -2
  17. package/dist/esm/EmptyState/EmptyState.mjs +0 -1
  18. package/dist/esm/RichTextEditor/EditableRichTextContent/EditableRichTextContent.mjs +3 -2
  19. package/dist/esm/RichTextEditor/EditableRichTextContent/EditableRichTextContent.module.scss.mjs +1 -0
  20. package/dist/esm/RichTextEditor/RichTextEditor/RichTextEditor.mjs +3 -2
  21. package/dist/esm/RichTextEditor/RichTextEditor/RichTextEditor.module.scss.mjs +1 -0
  22. package/dist/esm/RichTextEditor/utils/plugins/LinkManager/components/LinkModal/LinkModal.mjs +5 -1
  23. package/dist/esm/RichTextEditor/utils/plugins/LinkManager/components/LinkModal/LinkModal.module.scss.mjs +4 -0
  24. package/dist/esm/RichTextEditor/utils/plugins/LinkManager/validation.mjs +2 -1
  25. package/dist/esm/Select/Select.mjs +6 -2
  26. package/dist/esm/__future__/Select/Select.mjs +12 -11
  27. package/dist/styles.css +9 -8
  28. package/dist/types/Input/Input/Input.d.ts +5 -0
  29. package/dist/types/RichTextEditor/utils/plugins/LinkManager/validation.d.ts +1 -0
  30. package/dist/types/Select/Select.d.ts +10 -0
  31. package/dist/types/TextArea/TextArea.d.ts +5 -0
  32. package/dist/types/__future__/Select/Select.d.ts +5 -0
  33. package/dist/types/__future__/Select/subcomponents/SelectToggle/SelectToggle.d.ts +8 -0
  34. package/package.json +6 -6
  35. package/src/DateInput/DateInput/DateInput.tsx +1 -0
  36. package/src/DatePicker/DatePicker.spec.tsx +14 -14
  37. package/src/DatePicker/DatePicker.tsx +20 -11
  38. package/src/DateRangePicker/DateRangePicker.tsx +14 -2
  39. package/src/DateRangePicker/_docs/DateRangePicker.mdx +5 -1
  40. package/src/DateRangePicker/_docs/DateRangePicker.stories.tsx +99 -3
  41. package/src/EmptyState/EmptyState.tsx +1 -4
  42. package/src/FieldGroup/_docs/FieldGroup.stickersheet.stories.tsx +2 -12
  43. package/src/FieldGroup/_docs/FieldGroup.stories.tsx +4 -9
  44. package/src/Input/Input/Input.tsx +5 -0
  45. package/src/Input/InputSearch/InputSearch.spec.tsx +10 -7
  46. package/src/Notification/ToastNotification/ToastNotificationsList/ToastNotificationsList.module.scss +1 -1
  47. package/src/RichTextEditor/EditableRichTextContent/EditableRichTextContent.module.scss +25 -7
  48. package/src/RichTextEditor/EditableRichTextContent/EditableRichTextContent.tsx +3 -1
  49. package/src/RichTextEditor/EditableRichTextContent/_docs/EditableRichTextContent.mdx +6 -4
  50. package/src/RichTextEditor/EditableRichTextContent/_docs/EditableRichTextContent.stickersheet.stories.tsx +98 -0
  51. package/src/RichTextEditor/EditableRichTextContent/_docs/EditableRichTextContent.stories.tsx +8 -5
  52. package/src/RichTextEditor/EditableRichTextContent/_docs/defaultContent.json +11 -0
  53. package/src/RichTextEditor/EditableRichTextContent/_docs/dummyContent.json +44 -39
  54. package/src/RichTextEditor/RichTextContent/_docs/RichTextContent.mdx +11 -1
  55. package/src/RichTextEditor/RichTextContent/_docs/RichTextContent.stories.tsx +47 -2
  56. package/src/RichTextEditor/RichTextEditor/RichTextEditor.module.scss +6 -1
  57. package/src/RichTextEditor/RichTextEditor/RichTextEditor.spec.stories.tsx +48 -0
  58. package/src/RichTextEditor/RichTextEditor/RichTextEditor.tsx +7 -2
  59. package/src/RichTextEditor/RichTextEditor/_docs/RichTextEditor.mdx +66 -7
  60. package/src/RichTextEditor/RichTextEditor/_docs/RichTextEditor.stories.tsx +60 -7
  61. package/src/RichTextEditor/_mixins.scss +1 -0
  62. package/src/RichTextEditor/utils/plugins/LinkManager/components/LinkModal/LinkModal.module.scss +5 -0
  63. package/src/RichTextEditor/utils/plugins/LinkManager/components/LinkModal/LinkModal.spec.stories.tsx +37 -0
  64. package/src/RichTextEditor/utils/plugins/LinkManager/components/LinkModal/LinkModal.stories.tsx +33 -0
  65. package/src/RichTextEditor/utils/plugins/LinkManager/components/LinkModal/LinkModal.tsx +9 -1
  66. package/src/RichTextEditor/utils/plugins/LinkManager/{validation.ts → validation.tsx} +11 -1
  67. package/src/Select/Select.tsx +9 -1
  68. package/src/Select/_docs/Select.stories.tsx +0 -3
  69. package/src/TextArea/TextArea.tsx +5 -0
  70. package/src/__future__/Select/Select.tsx +6 -1
  71. package/src/__future__/Select/_docs/Select.stickersheet.stories.tsx +0 -9
  72. package/src/__future__/Select/subcomponents/SelectToggle/SelectToggle.tsx +4 -0
@@ -1,11 +1,50 @@
1
1
  [
2
+ {
3
+ "type": "paragraph",
4
+ "content": [
5
+ {
6
+ "type": "text",
7
+ "text": "User text goes here"
8
+ }
9
+ ]
10
+ },
2
11
  {
3
12
  "type": "paragraph",
4
13
  "content": [
5
14
  {
6
15
  "type": "text",
7
16
  "marks": [{ "type": "strong" }],
8
- "text": "Some notes I'd like to share in bold text:"
17
+ "text": "bold"
18
+ }
19
+ ]
20
+ },
21
+ {
22
+ "type": "paragraph",
23
+ "content": [
24
+ {
25
+ "type": "text",
26
+ "marks": [{ "type": "em" }],
27
+ "text": "underline"
28
+ }
29
+ ]
30
+ },
31
+ {
32
+ "type": "paragraph",
33
+ "content": [
34
+ {
35
+ "type": "text",
36
+ "text": "link",
37
+ "marks": [
38
+ {
39
+ "type": "link",
40
+ "attrs": {
41
+ "href": "https://cultureamp.design",
42
+ "_metadata": null,
43
+ "target": "_blank",
44
+ "rel": "noreferrer"
45
+ }
46
+ }
47
+ ]
9
48
  }
10
49
  ]
11
50
  },
@@ -17,24 +56,7 @@
17
56
  "content": [
18
57
  {
19
58
  "type": "paragraph",
20
- "content": [
21
- { "type": "text", "text": "Note 1 " },
22
- {
23
- "type": "text",
24
- "marks": [
25
- {
26
- "type": "link",
27
- "attrs": {
28
- "href": "https://cultureamp.design",
29
- "_metadata": null,
30
- "target": "_blank",
31
- "rel": "noreferrer"
32
- }
33
- }
34
- ],
35
- "text": "with link"
36
- }
37
- ]
59
+ "content": [{ "type": "text", "text": "Bullet list" }]
38
60
  },
39
61
  {
40
62
  "type": "bulletList",
@@ -73,29 +95,12 @@
73
95
  "content": [
74
96
  {
75
97
  "type": "paragraph",
76
- "content": [
77
- { "type": "text", "text": "Another really " },
78
- {
79
- "type": "text",
80
- "marks": [{ "type": "underline" }],
81
- "text": "important note"
82
- },
83
- { "type": "text", "text": " " },
84
- {
85
- "type": "text",
86
- "marks": [{ "type": "em" }],
87
- "text": "(that I'd like to emphasise)"
88
- }
89
- ]
98
+ "content": [{ "type": "text", "text": "List item" }]
90
99
  }
91
100
  ]
92
101
  }
93
102
  ]
94
103
  },
95
- {
96
- "type": "paragraph",
97
- "content": [{ "type": "text", "text": "Additionally:" }]
98
- },
99
104
  {
100
105
  "type": "orderedList",
101
106
  "attrs": { "order": 1 },
@@ -105,7 +110,7 @@
105
110
  "content": [
106
111
  {
107
112
  "type": "paragraph",
108
- "content": [{ "type": "text", "text": "Point one" }]
113
+ "content": [{ "type": "text", "text": "Numbered list" }]
109
114
  },
110
115
  {
111
116
  "type": "orderedList",
@@ -149,7 +154,7 @@
149
154
  "content": [
150
155
  {
151
156
  "type": "paragraph",
152
- "content": [{ "type": "text", "text": "Point two" }]
157
+ "content": [{ "type": "text", "text": "List item" }]
153
158
  }
154
159
  ]
155
160
  }
@@ -15,7 +15,17 @@ import * as RichTextContentStories from "./RichTextContent.stories"
15
15
 
16
16
  ## Overview
17
17
 
18
- To render your rich text content data structure as read-only text.
18
+ To render rich content as it appears in the [RichTextEditor](/docs/components-richtexteditor-richtexteditor--docs) in read-only format.
19
19
 
20
20
  <Canvas of={RichTextContentStories.Playground} />
21
21
  <Controls of={RichTextContentStories.Playground} />
22
+
23
+
24
+ ## Usage
25
+
26
+ A common use case of `RichTextContent` is to display user generated output as read-only text.
27
+
28
+ <Canvas of={RichTextContentStories.ReadOnly} />
29
+
30
+ This should not be used out of the box to toggle between active and inactive states of the `RichTextEditor`. Instead we recommend using the [EditableRichTextContent](/docs/components-richtexteditor-editablerichtextcontent--docs#usage) pattern, which indicates interactivity to the user and ensures compliance with WCAG guidelines.
31
+
@@ -1,12 +1,23 @@
1
+ import React from "react"
1
2
  import { Meta, StoryObj } from "@storybook/react"
3
+ import { Text } from "~components/Text"
2
4
  import { RichTextContent } from "../index"
3
- import dummyContent from "./dummyContent.json"
4
5
 
5
6
  const meta = {
6
7
  title: "Components/RichTextEditor/RichTextContent",
7
8
  component: RichTextContent,
8
9
  args: {
9
- content: dummyContent,
10
+ content: [
11
+ {
12
+ type: "paragraph",
13
+ content: [
14
+ {
15
+ type: "text",
16
+ text: "User text goes here",
17
+ },
18
+ ],
19
+ },
20
+ ],
10
21
  },
11
22
  argTypes: {
12
23
  content: { control: false },
@@ -22,3 +33,37 @@ export const Playground: Story = {
22
33
  chromatic: { disable: false },
23
34
  },
24
35
  }
36
+
37
+ export const ReadOnly: Story = {
38
+ parameters: {
39
+ chromatic: { disable: false },
40
+ },
41
+ args: {
42
+ content: [
43
+ {
44
+ type: "paragraph",
45
+ content: [
46
+ {
47
+ type: "text",
48
+ text: "User text goes here",
49
+ },
50
+ ],
51
+ },
52
+ ],
53
+ },
54
+ render: args => (
55
+ <>
56
+ {/* Note that since RichTextContent is not content editable, it is essentially just a div. This is why we haven't used the Label component */}
57
+ <Text
58
+ variant="small"
59
+ tag="div"
60
+ classNameOverride="block mb-6 leading-paragraph font-weight-heading"
61
+ >
62
+ Read only state
63
+ </Text>
64
+ <div className="p-12 bg-gray-200 rounded-default">
65
+ <RichTextContent {...args} />
66
+ </div>
67
+ </>
68
+ ),
69
+ }
@@ -16,7 +16,7 @@
16
16
  border-color $animation-duration-immediate;
17
17
 
18
18
  &:hover,
19
- &:focus {
19
+ &:focus-visible {
20
20
  border-color: $color-gray-600;
21
21
  background: $color-gray-200;
22
22
  }
@@ -46,6 +46,11 @@
46
46
  border-top-right-radius: 0;
47
47
  }
48
48
 
49
+ .editorLabel {
50
+ margin-bottom: $spacing-6;
51
+ display: inline-block;
52
+ }
53
+
49
54
  /* stylelint-disable no-descending-specificity */
50
55
  .editorWrapper {
51
56
  position: relative;
@@ -147,3 +147,51 @@ export const IncreaseIndent: Story = {
147
147
  })
148
148
  },
149
149
  }
150
+
151
+ export const CreateALink: Story = {
152
+ ...TestBase,
153
+ name: "Create a link",
154
+ play: async context => {
155
+ const { canvasElement, step } = context
156
+ const { getByRole, getByText } = within(canvasElement)
157
+ const editor = getByRole("textbox")
158
+ await step("Focus on editor", async () => {
159
+ await userEvent.click(editor)
160
+ expect(editor).toHaveFocus()
161
+ })
162
+
163
+ await step("Input text and select the first word", async () => {
164
+ await userEvent.keyboard("Link me")
165
+ await userEvent.pointer([
166
+ {
167
+ target: getByText("Link me"),
168
+ offset: 0,
169
+ keys: "[MouseLeft>]",
170
+ },
171
+ { offset: 4 },
172
+ ])
173
+ })
174
+
175
+ await step("click the link button", async () => {
176
+ await userEvent.click(getByRole("button", { name: "Link" }))
177
+ })
178
+
179
+ // wait for the transition to end and focus to shift
180
+ await new Promise(resolve => setTimeout(resolve, 500))
181
+
182
+ await step("Enter text", async () => {
183
+ await userEvent.keyboard("https://www.google.com")
184
+ })
185
+
186
+ await new Promise(resolve => setTimeout(resolve, 500))
187
+
188
+ await step("Tab and save", async () => {
189
+ await userEvent.keyboard("{Tab}{Enter}")
190
+ })
191
+
192
+ await step("Link exists in the RTE", async () => {
193
+ const link = getByRole("link", { name: "Link" })
194
+ expect(link).toBeInTheDocument()
195
+ })
196
+ },
197
+ }
@@ -159,8 +159,13 @@ export const RichTextEditor = ({
159
159
 
160
160
  return (
161
161
  <>
162
- {!labelledBy && labelText && <Label id={labelId} labelText={labelText} />}
163
- {/* TODO: add a bit of margin here once we have a classNameOverride on Label */}
162
+ {!labelledBy && labelText && (
163
+ <Label
164
+ classNameOverride={styles.editorLabel}
165
+ id={labelId}
166
+ labelText={labelText}
167
+ />
168
+ )}
164
169
  <div className={classnames(styles.editorWrapper, styles[status])}>
165
170
  {controls && (
166
171
  <Toolbar
@@ -1,5 +1,7 @@
1
1
  import { Canvas, Controls, DocsStory, Meta } from "@storybook/blocks"
2
2
  import { ResourceLinks, KAIOInstallation } from "~storybook/components"
3
+ import * as EditableRichTextContentStories from "../../EditableRichTextContent/_docs/EditableRichTextContent.stories"
4
+ import * as RichTextContentStories from "../../RichTextContent/_docs/RichTextContent.stories"
3
5
  import * as RichTextEditorStories from "./RichTextEditor.stories"
4
6
 
5
7
  <Meta of={RichTextEditorStories} />
@@ -24,21 +26,78 @@ A text area with additional capabilities for users to format the input text.
24
26
 
25
27
  ## API
26
28
 
27
- <DocsStory of={RichTextEditorStories.Controls} />
29
+ ### Controls
28
30
 
29
- ### Default Value
30
- Note: In order to provide the RichTextEditor with a default value that contains rich text you must also apply the corresponding controls.
31
- eg. if your default text contains Bold text, you must add the Bold control
31
+ `controls` accepts an array of `ToolbarItems` that are used to create the `RichTextEditor` schema and build out its core functionality. It also offers the ability to group the items in the toolbar.
32
+
33
+ <Canvas of={RichTextEditorStories.Controls} />
34
+
35
+ As the schema is defined by the `controls`, removing an item will remove its functionality from the `RichTextEditor`.
36
+
37
+ <Canvas of={RichTextEditorStories.ControlsWithoutBold} />
38
+
39
+ With all controls, the Kaizen `RichTextEditor` can create and render formatted text, lists, and links.
40
+
41
+ <Canvas of={RichTextEditorStories.AllAvailableContent} />
42
+
43
+
44
+ ### The EditorContentArray and defaultValue
45
+
46
+ The `defaultValue` is the initial content of the `RichTextEditor`. It accepts an array of objects in the [ProseMirror's rich text format](https://cultureamp.atlassian.net/wiki/spaces/TV/pages/3380543671/ProseMirror+rich+formatted+text+data+format).
32
47
 
33
48
  <Canvas of={RichTextEditorStories.DefaultValue} />
34
49
 
35
- #### Malformed Content
36
- If your default value does not conform correctly, the RTE will throw an error.
50
+ As mentioned in the previous section, the data (`EditorContentArray`) that is passed to the `defaultValue` must be able to map to the `controls` provided.
51
+
52
+ For example: if your `defaultValue` contains bolded text, you must pass bold into your `controls`.
53
+
54
+ ```
55
+ <RichTextEditor
56
+ labelText="Rich text"
57
+ defaultValue={rteData}
58
+ onChange={handleOnChange}
59
+ controls: [
60
+ { name: "bold", group: "inline" },
61
+ {/* other controls */}
62
+ ]
63
+ />
64
+ ```
65
+
66
+ ### Data errors and onDataError
67
+ When content is passed to the `defaultValue` that does not match to the `RichTextEditor`'s [schema](https://github.com/cultureamp/kaizen-design-system/blob/main/packages/components/src/RichTextEditor/RichTextEditor/schema.ts), the component will throw and render a generic error.
37
68
 
38
69
  <Canvas of={RichTextEditorStories.MalformedContent} />
39
70
 
40
- <DocsStory of={RichTextEditorStories.Rows} />
71
+ This will also throw if you have passed in an `EditorContentArray` that contains data that cannot map to the `controls` provided to the component.
72
+
73
+ <Canvas of={RichTextEditorStories.IncorrectDataForControls}/>
74
+
75
+ The `dataError` React Node and `onDataError` callback also allows you to handle these edge cases without breaking the page.
76
+
77
+ ### Rows
78
+
79
+ Sets the minimum height for the editable area of the RichTextEditor.
80
+
81
+ <Canvas of={RichTextEditorStories.Rows} />
41
82
 
42
83
  <DocsStory of={RichTextEditorStories.Description} />
43
84
 
44
85
  <DocsStory of={RichTextEditorStories.Validation} />
86
+
87
+
88
+ ### Inactive states and read-only text
89
+
90
+ In addition to the `RichTextEditor`, there are two additional Kaizen components that support rendering data in the RTE format.
91
+
92
+ #### EditableRichTextContent
93
+
94
+ For rendering editable content that can toggle between an active and inactive state we recommend the [EditableRichTextContent](/docs/components-richtexteditor-editablerichtextcontent--docs).
95
+
96
+ <Canvas of={EditableRichTextContentStories.UsageExample} />
97
+
98
+ #### RichTextContent
99
+
100
+ For rendering content as read-only text we recommend using the [RichTextContent](/docs/components-richtexteditor-richtextcontent--docs).
101
+
102
+ <Canvas of={RichTextContentStories.Playground} />
103
+
@@ -1,5 +1,7 @@
1
1
  import React, { useState } from "react"
2
2
  import { Meta, StoryObj } from "@storybook/react"
3
+ import { fn } from "@storybook/test"
4
+ import dummyContent from "../../EditableRichTextContent/_docs/dummyContent.json"
3
5
  import { EditorContentArray } from "../../index"
4
6
  import { RichTextEditor, RichTextEditorProps } from "../index"
5
7
 
@@ -77,9 +79,26 @@ export const Controls: Story = {
77
79
  { name: "bold", group: "inline" },
78
80
  { name: "italic", group: "inline" },
79
81
  { name: "underline", group: "inline" },
80
- { name: "orderedList", group: "list" },
81
- { name: "bulletList", group: "list" },
82
- { name: "link", group: "link" },
82
+ ],
83
+ },
84
+ }
85
+
86
+ export const ControlsWithoutBold: Story = {
87
+ args: {
88
+ controls: [
89
+ { name: "italic", group: "inline" },
90
+ { name: "underline", group: "inline" },
91
+ ],
92
+ defaultValue: [
93
+ {
94
+ type: "paragraph",
95
+ content: [
96
+ {
97
+ type: "text",
98
+ text: "This user text cannot be bolded",
99
+ },
100
+ ],
101
+ },
83
102
  ],
84
103
  },
85
104
  }
@@ -92,7 +111,7 @@ export const DefaultValue: Story = {
92
111
  content: [
93
112
  {
94
113
  type: "text",
95
- text: "Some notes I'd like to share",
114
+ text: "User text goes here",
96
115
  },
97
116
  ],
98
117
  },
@@ -100,6 +119,20 @@ export const DefaultValue: Story = {
100
119
  },
101
120
  }
102
121
 
122
+ export const AllAvailableContent: Story = {
123
+ args: {
124
+ defaultValue: dummyContent,
125
+ controls: [
126
+ { name: "bold", group: "inline" },
127
+ { name: "italic", group: "inline" },
128
+ { name: "underline", group: "inline" },
129
+ { name: "orderedList", group: "list" },
130
+ { name: "bulletList", group: "list" },
131
+ { name: "link", group: "link" },
132
+ ],
133
+ },
134
+ }
135
+
103
136
  export const Rows: Story = {
104
137
  args: {
105
138
  labelText: "1 Row",
@@ -109,13 +142,13 @@ export const Rows: Story = {
109
142
 
110
143
  export const Description: Story = {
111
144
  args: {
112
- description: "I am a rich text editor",
145
+ description: "Description text",
113
146
  },
114
147
  }
115
148
 
116
149
  export const Validation: Story = {
117
150
  args: {
118
- validationMessage: "something went wrong",
151
+ validationMessage: "Error message",
119
152
  status: "error",
120
153
  },
121
154
  }
@@ -129,10 +162,30 @@ export const MalformedContent: Story = {
129
162
  {
130
163
  type: "text",
131
164
  marks: [{ type: "strong" }],
132
- text: "Some notes I'd like to share in bold text:",
165
+ text: "User text goes here in bold text",
166
+ },
167
+ ],
168
+ },
169
+ ],
170
+ },
171
+ }
172
+
173
+ export const IncorrectDataForControls: Story = {
174
+ args: {
175
+ defaultValue: [
176
+ {
177
+ type: "paragraph",
178
+ content: [
179
+ {
180
+ type: "text",
181
+ marks: [{ type: "strong" }],
182
+ text: "User text goes here in bold text",
133
183
  },
134
184
  ],
135
185
  },
136
186
  ],
187
+ controls: [{ name: "italic", group: "inline" }],
188
+ dataError: <>Cannot bold text without a bold control</>,
189
+ onDataError: () => fn(),
137
190
  },
138
191
  }
@@ -11,6 +11,7 @@
11
11
  position: relative;
12
12
  white-space: pre-wrap;
13
13
  box-sizing: content-box;
14
+ color: $color-purple-800;
14
15
 
15
16
  > p {
16
17
  margin: 0 0 $spacing-16;
@@ -0,0 +1,5 @@
1
+ .validationErrorMessage {
2
+ ul {
3
+ margin-bottom: 0;
4
+ }
5
+ }
@@ -0,0 +1,37 @@
1
+ import { Meta, StoryObj } from "@storybook/react"
2
+ import { userEvent, waitFor, fn, expect } from "@storybook/test"
3
+ import { LinkModal } from "./LinkModal"
4
+
5
+ const meta = {
6
+ title: "Components/RichTextEditor/Subcomponents/LinkModal/Tests",
7
+ component: LinkModal,
8
+ args: {
9
+ onSubmit: fn(),
10
+ onDismiss: fn(),
11
+ onAfterLeave: fn(),
12
+ isOpen: true,
13
+ },
14
+ } satisfies Meta<typeof LinkModal>
15
+
16
+ export default meta
17
+
18
+ type Story = StoryObj<typeof meta>
19
+
20
+ export const InvalidLink: Story = {
21
+ parameters: {
22
+ chromatic: { disable: false },
23
+ },
24
+ args: {
25
+ defaultHref: "google.com",
26
+ },
27
+ play: async () => {
28
+ await new Promise(resolve => setTimeout(resolve, 500))
29
+ await userEvent.keyboard("{Tab}{Enter}")
30
+
31
+ await waitFor(() => {
32
+ expect(document.activeElement).toHaveAccessibleDescription(
33
+ /Empty or invalid link\. Links must start with http or https/
34
+ )
35
+ })
36
+ },
37
+ }
@@ -0,0 +1,33 @@
1
+ import { Meta, StoryObj } from "@storybook/react"
2
+ import { fn } from "@storybook/test"
3
+ import { LinkModal } from "./LinkModal"
4
+
5
+ const meta = {
6
+ title: "Components/RichTextEditor/Subcomponents/LinkModal",
7
+ component: LinkModal,
8
+ args: {
9
+ onSubmit: fn(),
10
+ onDismiss: fn(),
11
+ onAfterLeave: fn(),
12
+ isOpen: true,
13
+ },
14
+ } satisfies Meta<typeof LinkModal>
15
+
16
+ export default meta
17
+
18
+ type Story = StoryObj<typeof meta>
19
+
20
+ export const AddLink: Story = {
21
+ parameters: {
22
+ chromatic: { disable: false },
23
+ },
24
+ }
25
+
26
+ export const EditLink: Story = {
27
+ parameters: {
28
+ chromatic: { disable: false },
29
+ },
30
+ args: {
31
+ defaultHref: "http://google.com",
32
+ },
33
+ }
@@ -2,6 +2,7 @@ import React, { useRef, useState } from "react"
2
2
  import { InputEditModal } from "~components/Modal"
3
3
  import { TextField } from "~components/TextField"
4
4
  import { ValidationResponse, validateLink } from "../../validation"
5
+ import styles from "./LinkModal.module.scss"
5
6
 
6
7
  type LinkModalProps = {
7
8
  onSubmit: (href: string) => void
@@ -51,8 +52,15 @@ export const LinkModal = ({
51
52
  type="text"
52
53
  defaultValue={href ?? ""}
53
54
  labelText="Link URL"
55
+ description="Must start with http:// or https://"
54
56
  inputRef={inputRef}
55
- validationMessage={validationStatus.message}
57
+ validationMessage={
58
+ validationStatus?.message && (
59
+ <div className={styles.validationErrorMessage}>
60
+ {validationStatus.message}
61
+ </div>
62
+ )
63
+ }
56
64
  status={validationStatus.status}
57
65
  onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
58
66
  setHref(e.target.value)
@@ -1,3 +1,5 @@
1
+ import React from "react"
2
+
1
3
  export type ValidationResponse = {
2
4
  status: "success" | "error" | "default"
3
5
  message?: React.ReactNode
@@ -9,7 +11,15 @@ export const validateLink = (href: string): ValidationResponse => {
9
11
  if (!isValidLink) {
10
12
  return {
11
13
  status: "error",
12
- message: "Please enter a valid URL",
14
+ message: (
15
+ <>
16
+ Empty or invalid link. Links must start with http or https, e.g:
17
+ <ul>
18
+ <li>https://google.com</li>
19
+ <li>http://www.google.com</li>
20
+ </ul>
21
+ </>
22
+ ),
13
23
  }
14
24
  }
15
25
 
@@ -35,6 +35,11 @@ export type SelectProps = {
35
35
  * @default false
36
36
  */
37
37
  fullWidth?: boolean
38
+ /**
39
+ * @deprecated Use of placeholder text goes against our a11y standards.
40
+ * Use the `labelText` prop to provide a concise name, and the `description` prop for any help text.
41
+ */
42
+ placeholder?: string
38
43
  } & ReactSelectProps<any, boolean>
39
44
 
40
45
  /**
@@ -53,6 +58,7 @@ export const Select = React.forwardRef<any, SelectProps>(
53
58
  fullWidth: propsFullWidth,
54
59
  className: propsClassName,
55
60
  isDisabled,
61
+ placeholder,
56
62
  ...props
57
63
  },
58
64
  ref
@@ -92,6 +98,7 @@ export const Select = React.forwardRef<any, SelectProps>(
92
98
  {...props}
93
99
  ref={ref}
94
100
  aria-labelledby={labelId}
101
+ placeholder={placeholder || ""}
95
102
  components={{
96
103
  Control,
97
104
  Placeholder,
@@ -126,12 +133,13 @@ interface AsyncProps
126
133
 
127
134
  export const AsyncSelect = React.forwardRef(
128
135
  (
129
- { className: propsClassName, ...props }: AsyncProps,
136
+ { className: propsClassName, placeholder, ...props }: AsyncProps,
130
137
  ref: React.Ref<any>
131
138
  ) => (
132
139
  <Async
133
140
  {...props}
134
141
  ref={ref}
142
+ placeholder={placeholder || ""}
135
143
  components={{
136
144
  Control,
137
145
  Placeholder,