@charcuterie/ui 3.26.0 → 3.28.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.
- package/README.md +4 -2
- package/dist/Board/Board.d.ts +20 -7
- package/dist/Board/Board.d.ts.map +1 -1
- package/dist/Board/Board.js +16 -10
- package/dist/Board/Board.js.map +1 -1
- package/dist/Board/BoardCard.d.ts +18 -12
- package/dist/Board/BoardCard.d.ts.map +1 -1
- package/dist/Board/BoardCard.js +12 -12
- package/dist/Board/BoardCard.js.map +1 -1
- package/dist/Board/BoardLaneList.d.ts +2 -1
- package/dist/Board/BoardLaneList.d.ts.map +1 -1
- package/dist/Board/BoardLaneList.js +13 -4
- package/dist/Board/BoardLaneList.js.map +1 -1
- package/dist/MarkdownEditor/MarkdownEditor.d.ts +1 -1
- package/dist/MarkdownEditor/MarkdownEditor.d.ts.map +1 -1
- package/dist/MarkdownEditor/MarkdownEditor.js +130 -74
- package/dist/MarkdownEditor/MarkdownEditor.js.map +1 -1
- package/dist/Stepper/Stepper.d.ts +10 -1
- package/dist/Stepper/Stepper.d.ts.map +1 -1
- package/dist/Stepper/Stepper.js +25 -5
- package/dist/Stepper/Stepper.js.map +1 -1
- package/dist/markdownEditorCodeMirror/MarkdownEditorCodeMirror.d.ts.map +1 -1
- package/dist/markdownEditorCodeMirror/MarkdownEditorCodeMirror.js +135 -85
- package/dist/markdownEditorCodeMirror/MarkdownEditorCodeMirror.js.map +1 -1
- package/package.json +1 -1
- package/src/Board/Board.mdx +12 -0
- package/src/Board/Board.stories.tsx +48 -0
- package/src/Board/Board.test.tsx +43 -0
- package/src/Board/Board.tsx +34 -8
- package/src/Board/BoardCard.tsx +26 -16
- package/src/Board/BoardLaneList.tsx +35 -5
- package/src/MarkdownEditor/MarkdownEditor.mdx +32 -4
- package/src/MarkdownEditor/MarkdownEditor.test.tsx +108 -0
- package/src/MarkdownEditor/MarkdownEditor.tsx +89 -0
- package/src/Stepper/Stepper.stories.tsx +27 -0
- package/src/Stepper/Stepper.test.tsx +17 -2
- package/src/Stepper/Stepper.tsx +51 -8
- package/src/icons.storyHelpers.tsx +11 -0
- package/src/markdownEditorCodeMirror/MarkdownEditorCodeMirror.mdx +3 -1
- package/src/markdownEditorCodeMirror/MarkdownEditorCodeMirror.stories.tsx +31 -0
- package/src/markdownEditorCodeMirror/MarkdownEditorCodeMirror.test.tsx +109 -1
- package/src/markdownEditorCodeMirror/MarkdownEditorCodeMirror.tsx +84 -0
|
@@ -5,9 +5,13 @@ import { test } from "vitest"
|
|
|
5
5
|
import { mountStory } from "../mountStory.testHelpers.ts"
|
|
6
6
|
import * as stories from "./MarkdownEditorCodeMirror.stories.tsx"
|
|
7
7
|
|
|
8
|
-
const { AllStates, CaretReveal, Default } =
|
|
8
|
+
const { AllStates, CaretReveal, Default, Uploading } =
|
|
9
9
|
composeStories(stories)
|
|
10
10
|
|
|
11
|
+
type Canvas = Awaited<
|
|
12
|
+
ReturnType<typeof mountStory>
|
|
13
|
+
>["canvas"]
|
|
14
|
+
|
|
11
15
|
/**
|
|
12
16
|
* The two ways this surface stopped behaving like the `<textarea>`
|
|
13
17
|
* it replaced. Both are invisible to every other gate: the stories
|
|
@@ -183,3 +187,107 @@ test("a document taller than the frame grows it", async () => {
|
|
|
183
187
|
scroller.clientHeight,
|
|
184
188
|
)
|
|
185
189
|
})
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* THE IMAGE BUTTON, on the CodeMirror surface.
|
|
193
|
+
*
|
|
194
|
+
* Same control and same reason as the `<textarea>` sibling's: paste
|
|
195
|
+
* and drop do not exist on a tablet, and a `FileDropZone` beside the
|
|
196
|
+
* editor appends to the END of the document because pressing it
|
|
197
|
+
* moves focus out. Here the insertion goes through
|
|
198
|
+
* `readSelection(view)`, so it lands at the caret — CodeMirror keeps
|
|
199
|
+
* its selection while the file picker is open.
|
|
200
|
+
*
|
|
201
|
+
* ⚠️ Image is the FOURTH action, before the lists, because
|
|
202
|
+
* `Toolbar` overflows from the end and last put it behind the
|
|
203
|
+
* overflow trigger at 900px. It still collapses on a narrow enough
|
|
204
|
+
* surface, so a test cannot assume it is on the bar.
|
|
205
|
+
*/
|
|
206
|
+
const openImageButton = async (
|
|
207
|
+
canvas: Canvas,
|
|
208
|
+
body: Canvas,
|
|
209
|
+
) => {
|
|
210
|
+
const onTheBar = canvas.queryByRole("button", {
|
|
211
|
+
name: "Image",
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
if (onTheBar) {
|
|
215
|
+
return onTheBar
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
await userEvent.click(
|
|
219
|
+
canvas.getByRole("button", { name: "More actions" }),
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
// `overflow="panel"` here rather than `menu`, and the panel is
|
|
223
|
+
// PORTALLED — so it is on `body`, not inside the story canvas.
|
|
224
|
+
return body.getByRole("button", { name: "Image" })
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
test("the toolbar offers Image when uploads are wired", async () => {
|
|
228
|
+
const { body, canvas } = await mountStory(Uploading)
|
|
229
|
+
|
|
230
|
+
await expect(
|
|
231
|
+
await openImageButton(canvas, body),
|
|
232
|
+
).not.toBeNull()
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
/** A button that opens a file picker and then has nowhere to send
|
|
236
|
+
* the file is worse than no button. */
|
|
237
|
+
test("the toolbar hides Image when nothing can take the file", async () => {
|
|
238
|
+
const { body, canvas } = await mountStory(Default)
|
|
239
|
+
|
|
240
|
+
const overflow = canvas.queryByRole("button", {
|
|
241
|
+
name: "More actions",
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
if (overflow) {
|
|
245
|
+
await userEvent.click(overflow)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
await expect(
|
|
249
|
+
body.queryByRole("button", { name: "Image" }),
|
|
250
|
+
).toBeNull()
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
test("a picked file uploads and lands in the document", async () => {
|
|
254
|
+
const { body, canvas } = await mountStory(Uploading)
|
|
255
|
+
|
|
256
|
+
await userEvent.click(await openImageButton(canvas, body))
|
|
257
|
+
|
|
258
|
+
const picker = canvas
|
|
259
|
+
.getByRole("toolbar")
|
|
260
|
+
.parentElement?.querySelector(
|
|
261
|
+
'input[type="file"]',
|
|
262
|
+
) as HTMLInputElement
|
|
263
|
+
|
|
264
|
+
await expect(picker).not.toBeNull()
|
|
265
|
+
|
|
266
|
+
// The picker itself is the browser's; what this drives is the
|
|
267
|
+
// `change` it fires on the way back.
|
|
268
|
+
await userEvent.upload(
|
|
269
|
+
picker,
|
|
270
|
+
new File(["binary"], "jig.png", { type: "image/png" }),
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
/*
|
|
274
|
+
The document is CodeMirror's, so the assertion reads the
|
|
275
|
+
editing surface's text rather than a `value`.
|
|
276
|
+
|
|
277
|
+
NOT `getByRole("img")`, which is what this first tried: live
|
|
278
|
+
preview reveals a construct's markers while the caret is inside
|
|
279
|
+
it, and the caret lands immediately after the insertion — so
|
|
280
|
+
the image is markdown on screen at exactly this moment and
|
|
281
|
+
becomes a picture when the caret leaves. That is the surface
|
|
282
|
+
working, not failing.
|
|
283
|
+
*/
|
|
284
|
+
await waitFor(() => {
|
|
285
|
+
expect(
|
|
286
|
+
canvas.getByRole("textbox", {
|
|
287
|
+
name: "Task description",
|
|
288
|
+
}).textContent,
|
|
289
|
+
).toContain(
|
|
290
|
+
"",
|
|
291
|
+
)
|
|
292
|
+
})
|
|
293
|
+
})
|
|
@@ -300,6 +300,12 @@ export const MarkdownEditorCodeMirror = ({
|
|
|
300
300
|
|
|
301
301
|
const uploadCount = useRef(0)
|
|
302
302
|
|
|
303
|
+
/** The toolbar's Image button presses this. A real
|
|
304
|
+
* `<input type="file">` because nothing else opens a file
|
|
305
|
+
* picker, and it is the only way in on a touch device — there
|
|
306
|
+
* is nothing to paste from and nothing to drag. */
|
|
307
|
+
const imageInputRef = useRef<HTMLInputElement>(null)
|
|
308
|
+
|
|
303
309
|
const editabilityRef = useRef(new Compartment())
|
|
304
310
|
|
|
305
311
|
const [isRawMode, setIsRawMode] = useState(
|
|
@@ -732,6 +738,34 @@ export const MarkdownEditorCodeMirror = ({
|
|
|
732
738
|
)
|
|
733
739
|
},
|
|
734
740
|
},
|
|
741
|
+
/*
|
|
742
|
+
IMAGE, and only when the consumer can actually take
|
|
743
|
+
one. A button that opens a file picker and then has
|
|
744
|
+
nowhere to send the file is worse than no button.
|
|
745
|
+
|
|
746
|
+
Spread rather than a `filter(Boolean)`, so the array
|
|
747
|
+
stays a `ToolbarItem[]` without a cast.
|
|
748
|
+
|
|
749
|
+
⚠️ FOURTH, and the position is load-bearing rather
|
|
750
|
+
than aesthetic — same reason as the textarea
|
|
751
|
+
sibling's. `Toolbar` overflows from the END, so the
|
|
752
|
+
order IS the priority order, and last put Image
|
|
753
|
+
behind "More actions" at 900px. The whole point of
|
|
754
|
+
the button is that adding an image is not buried.
|
|
755
|
+
*/
|
|
756
|
+
...(onUploadImage
|
|
757
|
+
? [
|
|
758
|
+
{
|
|
759
|
+
icon: icons?.image,
|
|
760
|
+
isDisabled: isDisabled || isReadOnly,
|
|
761
|
+
key: "image",
|
|
762
|
+
label: "Image",
|
|
763
|
+
onSelect: () => {
|
|
764
|
+
imageInputRef.current?.click()
|
|
765
|
+
},
|
|
766
|
+
},
|
|
767
|
+
]
|
|
768
|
+
: []),
|
|
735
769
|
{
|
|
736
770
|
icon: icons?.bulletedList,
|
|
737
771
|
isDisabled: isDisabled || isReadOnly,
|
|
@@ -832,6 +866,56 @@ export const MarkdownEditorCodeMirror = ({
|
|
|
832
866
|
overflow="panel"
|
|
833
867
|
overflowIcon={icons?.overflow}
|
|
834
868
|
/>
|
|
869
|
+
|
|
870
|
+
{/*
|
|
871
|
+
The Image button's actual control.
|
|
872
|
+
|
|
873
|
+
`hidden`, not `sr-only`: `display: none` takes it out of
|
|
874
|
+
the accessibility tree, which is what is wanted — the
|
|
875
|
+
toolbar button is the named control, and a second
|
|
876
|
+
unlabelled file input announced beside it would be one
|
|
877
|
+
control too many.
|
|
878
|
+
|
|
879
|
+
The insertion lands at the CARET, not at the end of the
|
|
880
|
+
document, because `readSelection(view)` reads
|
|
881
|
+
CodeMirror's own selection and the view keeps it while
|
|
882
|
+
the picker is open. That is the whole reason this is a
|
|
883
|
+
toolbar button rather than a drop zone under the box.
|
|
884
|
+
*/}
|
|
885
|
+
{onUploadImage ? (
|
|
886
|
+
<input
|
|
887
|
+
accept="image/*"
|
|
888
|
+
aria-hidden="true"
|
|
889
|
+
className="hidden"
|
|
890
|
+
multiple
|
|
891
|
+
onChange={(changeEvent) => {
|
|
892
|
+
const view = viewRef.current
|
|
893
|
+
|
|
894
|
+
if (view) {
|
|
895
|
+
for (const file of Array.from(
|
|
896
|
+
changeEvent.target.files ?? [],
|
|
897
|
+
).filter(isImageFile)) {
|
|
898
|
+
uploadCount.current += 1
|
|
899
|
+
|
|
900
|
+
void uploadImage({
|
|
901
|
+
count: uploadCount.current,
|
|
902
|
+
file,
|
|
903
|
+
handlers: handlersRef.current,
|
|
904
|
+
setMessage: setUploadMessage,
|
|
905
|
+
view,
|
|
906
|
+
})
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// Cleared so choosing the SAME file twice fires
|
|
911
|
+
// `change` the second time.
|
|
912
|
+
changeEvent.target.value = ""
|
|
913
|
+
}}
|
|
914
|
+
ref={imageInputRef}
|
|
915
|
+
tabIndex={-1}
|
|
916
|
+
type="file"
|
|
917
|
+
/>
|
|
918
|
+
) : null}
|
|
835
919
|
</div>
|
|
836
920
|
|
|
837
921
|
{/*
|