@guardian/stand 0.0.2 → 0.0.4

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 (47) hide show
  1. package/README.md +41 -0
  2. package/dist/byline/Byline.cjs +8 -1
  3. package/package.json +42 -35
  4. package/.changeset/README.md +0 -8
  5. package/.changeset/config.json +0 -11
  6. package/.prettierrc +0 -1
  7. package/.storybook/main.ts +0 -12
  8. package/.storybook/preview.tsx +0 -83
  9. package/CHANGELOG.md +0 -7
  10. package/dist/types/.storybook/main.d.ts +0 -3
  11. package/dist/types/.storybook/preview.d.ts +0 -3
  12. package/dist/types/jest-setup-after-env.d.ts +0 -1
  13. package/dist/types/src/byline/Byline.stories.d.ts +0 -206
  14. package/dist/types/src/byline/Byline.test.d.ts +0 -1
  15. package/dist/types/src/mocks/prosemirror-view.d.ts +0 -10
  16. package/eslint.config.js +0 -14
  17. package/jest-setup-after-env.ts +0 -1
  18. package/jest.config.js +0 -12
  19. package/rollup.config.js +0 -49
  20. package/src/byline/Byline.stories.tsx +0 -186
  21. package/src/byline/Byline.test.tsx +0 -450
  22. package/src/byline/Byline.tsx +0 -524
  23. package/src/byline/Preview.tsx +0 -59
  24. package/src/byline/contributors-fixture.ts +0 -1006
  25. package/src/byline/lib.test.ts +0 -179
  26. package/src/byline/lib.ts +0 -426
  27. package/src/byline/placeholder.ts +0 -30
  28. package/src/byline/plugins.ts +0 -186
  29. package/src/byline/schema.ts +0 -62
  30. package/src/byline/styles.ts +0 -246
  31. package/src/byline/theme.ts +0 -45
  32. package/src/byline/util.ts +0 -5
  33. package/src/index.ts +0 -2
  34. package/src/mocks/prosemirror-view.ts +0 -19
  35. package/tsconfig.json +0 -19
  36. /package/dist/types/{src/byline → byline}/Byline.d.ts +0 -0
  37. /package/dist/types/{src/byline → byline}/Preview.d.ts +0 -0
  38. /package/dist/types/{src/byline → byline}/contributors-fixture.d.ts +0 -0
  39. /package/dist/types/{src/byline → byline}/lib.d.ts +0 -0
  40. /package/dist/types/{src/byline → byline}/lib.test.d.ts +0 -0
  41. /package/dist/types/{src/byline → byline}/placeholder.d.ts +0 -0
  42. /package/dist/types/{src/byline → byline}/plugins.d.ts +0 -0
  43. /package/dist/types/{src/byline → byline}/schema.d.ts +0 -0
  44. /package/dist/types/{src/byline → byline}/styles.d.ts +0 -0
  45. /package/dist/types/{src/byline → byline}/theme.d.ts +0 -0
  46. /package/dist/types/{src/byline → byline}/util.d.ts +0 -0
  47. /package/dist/types/{src/index.d.ts → index.d.ts} +0 -0
@@ -1,186 +0,0 @@
1
- import type { Meta, StoryObj } from '@storybook/react-vite';
2
- import { Byline } from './Byline';
3
- import { contributors } from './contributors-fixture';
4
- import type { TaggedContributor } from './lib';
5
-
6
- const searchContributors = (
7
- selectedText: string,
8
- ): Promise<TaggedContributor[]> => {
9
- return new Promise<TaggedContributor[]>((resolve) => {
10
- const results = contributors
11
- .filter((name) =>
12
- name.toLowerCase().includes(selectedText.toLowerCase()),
13
- )
14
- .map((name, index) => ({
15
- path: `profile/${name.toLowerCase().replace(/\s/g, '-')}`,
16
- label: name,
17
- type: 'Contributor',
18
- tagId: `${index + 1}`,
19
- // show internal label for every 5th contributor for testing internalLabel
20
- internalLabel:
21
- index % 5 === 0 ? `${name} (internal)` : undefined,
22
- }))
23
- .slice(0, 20);
24
-
25
- return resolve(results);
26
- });
27
- };
28
-
29
- const disableSnapshot = {
30
- parameters: {
31
- chromatic: { disableSnapshot: true },
32
- },
33
- };
34
-
35
- const meta = {
36
- title: 'Stand/Byline',
37
- component: Byline,
38
- parameters: {},
39
- args: {
40
- handleSave: () => {},
41
- initialValue: [],
42
- searchContributors,
43
- enablePreview: true,
44
- },
45
- } satisfies Meta<typeof Byline>;
46
-
47
- type Story = StoryObj<typeof Byline>;
48
-
49
- export const Default = {} satisfies Story;
50
-
51
- export const WithTheme = {
52
- args: {
53
- allowUntaggedContributors: true,
54
- searchContributors,
55
- theme: {
56
- editor: {
57
- invisibles: {
58
- color: 'lightblue',
59
- },
60
- color: 'rgba(255, 255, 255, 0.87)',
61
- background: 'rgb(51, 51, 51)',
62
- border: '1px solid rgb(173, 216, 230)',
63
- chip: {
64
- color: 'initial',
65
- taggedBackground: 'rgb(173, 216, 230)',
66
- border: '1px solid rgb(173, 216, 230)',
67
- borderRadius: '3px',
68
- padding: '5.5px 7px',
69
- untagged: {
70
- color: 'rgba(255, 255, 255, 0.87)',
71
- },
72
- },
73
- },
74
- dropdown: {
75
- background: 'rgb(36, 36, 36)',
76
- li: {
77
- color: 'rgba(255, 255, 255, 0.87)',
78
- borderBottom: 'none',
79
- selected: {
80
- color: 'rgba(255, 255, 255, 0.87)',
81
- background: 'rgb(51, 51, 51)',
82
- },
83
- },
84
- },
85
- },
86
- },
87
- } satisfies Story;
88
-
89
- export const WithUntaggedContributors = {
90
- args: {
91
- allowUntaggedContributors: true,
92
- },
93
- ...disableSnapshot,
94
- } satisfies Story;
95
-
96
- export const WithInitialValue = {
97
- args: {
98
- allowUntaggedContributors: true,
99
- initialValue: [
100
- {
101
- type: 'contributor',
102
- value: 'Joe Bloggs',
103
- tagId: '1',
104
- path: 'profile/joebloggs',
105
- },
106
- {
107
- type: 'text',
108
- value: ' in London, ',
109
- },
110
- {
111
- type: 'contributor',
112
- value: 'Jane Doe',
113
- },
114
- {
115
- type: 'text',
116
- value: ' in New York',
117
- },
118
- ],
119
- },
120
- } satisfies Story;
121
-
122
- export const WithNoSearch = {
123
- args: {
124
- allowUntaggedContributors: true,
125
- searchContributors: undefined,
126
- },
127
- ...disableSnapshot,
128
- } satisfies Story;
129
-
130
- export const WithNoSearchAndNoUntagged = {
131
- args: {
132
- allowUntaggedContributors: false,
133
- searchContributors: undefined,
134
- },
135
- ...disableSnapshot,
136
- } satisfies Story;
137
-
138
- export const WithCustomPlaceholder = {
139
- args: {
140
- allowUntaggedContributors: true,
141
- placeholder: 'A custom placeholder...',
142
- },
143
- } satisfies Story;
144
-
145
- export const WithContributorLimit = {
146
- args: { contributorLimit: 1 },
147
- ...disableSnapshot,
148
- } satisfies Story;
149
-
150
- export const WithoutPreview = {
151
- args: {
152
- allowUntaggedContributors: true,
153
- enablePreview: false,
154
- },
155
- ...disableSnapshot,
156
- } satisfies Story;
157
-
158
- export const ReadOnly = {
159
- args: {
160
- readOnly: true,
161
- allowUntaggedContributors: true,
162
- enablePreview: true,
163
- initialValue: [
164
- {
165
- type: 'contributor',
166
- value: 'Joe Bloggs',
167
- tagId: '1',
168
- path: 'profile/joebloggs',
169
- },
170
- {
171
- type: 'text',
172
- value: ' in London and ',
173
- },
174
- {
175
- type: 'contributor',
176
- value: 'Jane Doe',
177
- },
178
- {
179
- type: 'text',
180
- value: ' in New York',
181
- },
182
- ],
183
- },
184
- } satisfies Story;
185
-
186
- export default meta;
@@ -1,450 +0,0 @@
1
- import { act, render } from '@testing-library/react';
2
- import { screen } from '@testing-library/react';
3
- import userEvent from '@testing-library/user-event';
4
- import type { EditorView } from 'prosemirror-view';
5
- import { mockEditorViewMethods } from '../mocks/prosemirror-view';
6
- import { Byline } from './Byline';
7
- import type { BylineModel, TaggedContributor } from './lib';
8
-
9
- jest.mock('prosemirror-view', () => {
10
- const actualProsemirrorView = jest.requireActual('prosemirror-view');
11
- const Class = actualProsemirrorView.EditorView as typeof EditorView;
12
- class MockEditorView extends Class {
13
- posAtCoords = mockEditorViewMethods.posAtCoords;
14
- coordsAtPos = mockEditorViewMethods.coordsAtPos;
15
- }
16
- return {
17
- ...actualProsemirrorView,
18
- EditorView: MockEditorView,
19
- };
20
- });
21
-
22
- // Mock the offsetParent used for visibility in the test dom
23
- // See https://github.com/jsdom/jsdom/issues/1261
24
- Object.defineProperty(HTMLElement.prototype, 'offsetParent', {
25
- get() {
26
- return this.parentNode;
27
- },
28
- });
29
-
30
- // Mock scrollIntoView for testing, else we get `TypeError: selectedOption.scrollIntoView is not a function` errors
31
- Element.prototype.scrollIntoView = jest.fn();
32
-
33
- const mockSearchContributors: (
34
- selectedText: string,
35
- ) => Promise<TaggedContributor[]> = (selectedText: string) => {
36
- const allContributors: TaggedContributor[] = [
37
- {
38
- path: 'profile/mahesh-makani',
39
- label: 'Mahesh Makani',
40
- internalLabel: 'Mahesh Makani (Software Engineer)',
41
- tagId: '1',
42
- },
43
- {
44
- path: 'profile/andrew-howe-ely',
45
- label: 'Andrew Howe-Ely',
46
- tagId: '2',
47
- },
48
- {
49
- path: 'profile/jane-smith',
50
- label: 'Jane Smith',
51
- internalLabel: 'Jane Smith (Journalist)',
52
- tagId: '3',
53
- },
54
- {
55
- path: 'profile/john-doe',
56
- label: 'John Doe',
57
- tagId: '4',
58
- },
59
- ];
60
-
61
- const filteredContributors = allContributors.filter((contributor) =>
62
- contributor.label
63
- .toLowerCase()
64
- .startsWith(selectedText.toLowerCase().trim()),
65
- );
66
-
67
- return Promise.resolve(filteredContributors);
68
- };
69
-
70
- describe('Byline editor', () => {
71
- it('shows dropdown with untagged contributor suggestion, hides when click away', async () => {
72
- const user = userEvent.setup();
73
- render(
74
- <Byline allowUntaggedContributors={true} handleSave={() => {}} />,
75
- );
76
-
77
- await act(async () => {
78
- await user.type(screen.getByRole('combobox'), 'Test');
79
- });
80
-
81
- const dropdown = screen.getByRole('listbox');
82
- expect(dropdown).toHaveTextContent(
83
- 'Add "Test" as untagged contributor',
84
- );
85
-
86
- await act(async () => {
87
- await user.click(document.body);
88
- });
89
- expect(dropdown).not.toBeVisible();
90
- });
91
- it('adds a chip from the dropdown', async () => {
92
- const user = userEvent.setup();
93
- render(
94
- <Byline allowUntaggedContributors={true} handleSave={() => {}} />,
95
- );
96
-
97
- const editor = screen.getByRole('combobox');
98
- await act(async () => {
99
- await user.type(screen.getByRole('combobox'), 'Test');
100
- await user.click(screen.getByRole('option'));
101
- });
102
-
103
- const chip = editor.querySelector('chip[data-label="Test"]');
104
- expect(chip).toBeInTheDocument();
105
- expect(chip).toHaveTextContent('Test');
106
- });
107
- it('deletes a chip when clicking x', async () => {
108
- const user = userEvent.setup();
109
- render(
110
- <Byline allowUntaggedContributors={true} handleSave={() => {}} />,
111
- );
112
-
113
- const editor = screen.getByRole('combobox');
114
- await act(async () => {
115
- await user.type(screen.getByRole('combobox'), 'Test');
116
- await user.click(screen.getByRole('option'));
117
- });
118
-
119
- const chip = editor.querySelector('chip[data-label="Test"]');
120
- expect(chip).toBeInTheDocument();
121
-
122
- const deleteHander = screen.getByTitle('Delete Test');
123
-
124
- await act(async () => {
125
- await user.click(deleteHander);
126
- });
127
- expect(chip).not.toBeInTheDocument();
128
- });
129
- it('hides dropdown with escape key', async () => {
130
- const user = userEvent.setup();
131
- render(
132
- <Byline allowUntaggedContributors={true} handleSave={() => {}} />,
133
- );
134
-
135
- await act(async () => {
136
- await user.type(screen.getByRole('combobox'), 'Test');
137
- });
138
-
139
- const dropdown = screen.getByRole('listbox');
140
- expect(dropdown).toHaveTextContent(
141
- 'Add "Test" as untagged contributor',
142
- );
143
-
144
- await act(async () => {
145
- await user.type(screen.getByRole('combobox'), '{Escape}');
146
- });
147
- expect(dropdown).not.toBeVisible();
148
- });
149
- it('displays placeholder text when no content is present', async () => {
150
- const user = userEvent.setup();
151
- render(
152
- <Byline
153
- allowUntaggedContributors={true}
154
- placeholder="Placeholder"
155
- handleSave={() => {}}
156
- />,
157
- );
158
-
159
- const placeholder = await screen.findByText('Placeholder');
160
- expect(placeholder).toBeInTheDocument();
161
-
162
- await act(async () => {
163
- await user.type(screen.getByRole('combobox'), 'Test');
164
- });
165
-
166
- expect(placeholder).not.toBeVisible();
167
- });
168
- it('renders search options in dropdown', async () => {
169
- const user = userEvent.setup();
170
- render(
171
- <Byline
172
- allowUntaggedContributors={true}
173
- placeholder="Placeholder"
174
- handleSave={() => {}}
175
- searchContributors={mockSearchContributors}
176
- />,
177
- );
178
-
179
- await act(async () => {
180
- await user.click(screen.getByRole('combobox'));
181
- await user.type(screen.getByRole('combobox'), 'Ma'); // Type "Ma" to match "Mahesh Makani"
182
- });
183
-
184
- // testing internalLabel
185
- const mahesh = screen.getByText('Mahesh Makani (Software Engineer)');
186
- expect(mahesh).toBeInTheDocument();
187
-
188
- await act(async () => {
189
- await user.clear(screen.getByRole('combobox'));
190
- await user.type(screen.getByRole('combobox'), 'A');
191
- });
192
-
193
- // testing label
194
- const andrew = screen.getByText('Andrew Howe-Ely');
195
- expect(andrew).toBeInTheDocument();
196
- });
197
- it('moves the selected option in dropdown with arrow keys', async () => {
198
- const user = userEvent.setup();
199
- render(
200
- <Byline
201
- allowUntaggedContributors={true}
202
- placeholder="Placeholder"
203
- handleSave={() => {}}
204
- searchContributors={mockSearchContributors}
205
- />,
206
- );
207
-
208
- await act(async () => {
209
- await user.click(screen.getByRole('combobox'));
210
- await user.type(screen.getByRole('combobox'), 'J'); // Type "J" to match both "Jane Smith" and "John Doe"
211
- await user.type(screen.getByRole('combobox'), '{ArrowDown}');
212
- await user.type(screen.getByRole('combobox'), '{ArrowDown}');
213
- });
214
-
215
- const lastOption = screen.getAllByRole('option').pop();
216
- expect(lastOption).toHaveAttribute('aria-selected', 'true');
217
- });
218
- it('adds a chip with the Enter key', async () => {
219
- const user = userEvent.setup();
220
- render(
221
- <Byline
222
- allowUntaggedContributors={true}
223
- placeholder="Placeholder"
224
- handleSave={() => {}}
225
- searchContributors={mockSearchContributors}
226
- />,
227
- );
228
- const editor = screen.getByRole('combobox');
229
-
230
- await act(async () => {
231
- await user.click(editor);
232
- await user.type(screen.getByRole('combobox'), 'Andrew'); // Type full name to ensure it's found
233
- });
234
-
235
- // Wait for the dropdown to appear and verify the option exists
236
- const andrewOption = await screen.findByText('Andrew Howe-Ely');
237
- expect(andrewOption).toBeInTheDocument();
238
-
239
- await act(async () => {
240
- await user.type(screen.getByRole('combobox'), '{Enter}'); // Select the first option
241
- });
242
-
243
- const chip = editor.querySelector('chip[data-label="Andrew Howe-Ely"]');
244
- expect(chip).toBeInTheDocument();
245
- expect(chip).toHaveTextContent('Andrew Howe-Ely');
246
- });
247
- it('executes save function on every keypress', async () => {
248
- const user = userEvent.setup();
249
- const mockHandleSave = jest.fn();
250
- render(
251
- <Byline
252
- allowUntaggedContributors={true}
253
- placeholder="Placeholder"
254
- handleSave={mockHandleSave}
255
- searchContributors={mockSearchContributors}
256
- />,
257
- );
258
- const editor = screen.getByRole('combobox');
259
-
260
- await act(async () => {
261
- await user.type(editor, 'T');
262
- await user.type(editor, 'e');
263
- await user.type(editor, 's');
264
- await user.type(editor, 't');
265
- });
266
-
267
- expect(mockHandleSave).toHaveBeenCalledTimes(4);
268
- });
269
- it('executes save function with correct input at each keypress', async () => {
270
- const user = userEvent.setup();
271
- const saveLog: BylineModel[] = [];
272
- const mockHandleSave = (value: BylineModel) => {
273
- saveLog.push(value);
274
- };
275
- render(
276
- <Byline
277
- allowUntaggedContributors={true}
278
- placeholder="Placeholder"
279
- handleSave={mockHandleSave}
280
- searchContributors={mockSearchContributors}
281
- />,
282
- );
283
- const editor = screen.getByRole('combobox');
284
-
285
- await act(async () => {
286
- await user.type(editor, 'T');
287
- await user.type(editor, 'e');
288
- await user.type(editor, 's');
289
- await user.type(editor, 't');
290
- });
291
-
292
- expect(saveLog.at(0)?.pop()?.value).toBe('T');
293
- expect(saveLog.at(1)?.pop()?.value).toBe('Te');
294
- expect(saveLog.at(2)?.pop()?.value).toBe('Tes');
295
- expect(saveLog.at(3)?.pop()?.value).toBe('Test');
296
- });
297
- it('passes * character to search function', async () => {
298
- const user = userEvent.setup();
299
- const mockSearch = jest.fn().mockImplementation(mockSearchContributors);
300
- render(
301
- <Byline
302
- allowUntaggedContributors={true}
303
- placeholder="Placeholder"
304
- handleSave={() => {}}
305
- searchContributors={mockSearch}
306
- />,
307
- );
308
- const editor = screen.getByRole('combobox');
309
-
310
- await act(async () => {
311
- await user.type(editor, '*Test');
312
- });
313
-
314
- expect(mockSearch).toHaveBeenLastCalledWith('*Test');
315
-
316
- await act(async () => {
317
- await user.clear(editor);
318
- await user.type(editor, 'T*est');
319
- });
320
-
321
- expect(mockSearch).toHaveBeenLastCalledWith('T*est');
322
-
323
- await act(async () => {
324
- await user.clear(editor);
325
- await user.type(editor, 'Te-St*');
326
- });
327
-
328
- expect(mockSearch).toHaveBeenLastCalledWith('Te-St*');
329
- });
330
- });
331
-
332
- describe('trackTypingFromStart behavior', () => {
333
- it('starts tracking when typing from start', async () => {
334
- const user = userEvent.setup();
335
- const mockHandleSave = jest.fn();
336
-
337
- render(
338
- <Byline
339
- allowUntaggedContributors={true}
340
- handleSave={mockHandleSave}
341
- searchContributors={mockSearchContributors}
342
- initialValue={[
343
- { type: 'text', value: 'Existing content and more text' },
344
- ]}
345
- />,
346
- );
347
-
348
- const editor = screen.getByRole('combobox');
349
-
350
- // Start typing from the beginning
351
- await act(async () => {
352
- await user.click(editor);
353
- await user.keyboard('{Home}J');
354
- });
355
-
356
- expect(
357
- screen.queryByText('JExisting content and more text'),
358
- ).toBeInTheDocument();
359
-
360
- expect(screen.getByRole('listbox')).toBeVisible();
361
- expect(screen.getByText('Jane Smith (Journalist)')).toBeInTheDocument();
362
- expect(screen.getByText('John Doe')).toBeInTheDocument();
363
-
364
- await act(async () => {
365
- await user.keyboard('ohn');
366
- });
367
-
368
- expect(
369
- screen.queryByText('JohnExisting content and more text'),
370
- ).toBeInTheDocument();
371
- expect(screen.getByRole('listbox')).toBeVisible();
372
- expect(screen.getByText('John Doe')).toBeInTheDocument();
373
- expect(
374
- screen.queryByText('Jane Smith (Journalist)'),
375
- ).not.toBeInTheDocument();
376
-
377
- await act(async () => {
378
- await user.click(screen.getByText('John Doe'));
379
- });
380
-
381
- const chip = editor.querySelector('chip[data-label="John Doe"]');
382
- expect(chip).toBeInTheDocument();
383
-
384
- expect(
385
- screen.queryByText('Existing content and more text'),
386
- ).toBeInTheDocument();
387
- });
388
-
389
- it('handles backspace correctly within tracked range', async () => {
390
- const user = userEvent.setup();
391
- const mockHandleSave = jest.fn();
392
-
393
- render(
394
- <Byline
395
- allowUntaggedContributors={true}
396
- handleSave={mockHandleSave}
397
- searchContributors={mockSearchContributors}
398
- initialValue={[
399
- { type: 'text', value: 'Existing content and more text' },
400
- ]}
401
- />,
402
- );
403
-
404
- const editor = screen.getByRole('combobox');
405
-
406
- // Type some text
407
- await act(async () => {
408
- await user.click(editor);
409
- await user.keyboard('{Home}John');
410
- });
411
-
412
- expect(screen.getByRole('listbox')).toBeVisible();
413
- expect(screen.getByText('John Doe')).toBeInTheDocument();
414
-
415
- // backspace to fix a typo
416
- await act(async () => {
417
- await user.keyboard('{Backspace}'.repeat(2));
418
- });
419
-
420
- // should still be tracking (within range) and show contributors starting with "Jo"
421
- expect(screen.getByRole('listbox')).toBeVisible();
422
- expect(screen.getByText('John Doe')).toBeInTheDocument();
423
-
424
- // backspace and edit name
425
- await act(async () => {
426
- await user.keyboard('{Backspace}ane');
427
- });
428
-
429
- expect(
430
- screen.queryByText('JaneExisting content and more text'),
431
- ).toBeInTheDocument();
432
-
433
- expect(screen.getByRole('listbox')).toBeVisible();
434
- expect(
435
- screen.queryByText('Jane Smith (Journalist)'),
436
- ).toBeInTheDocument();
437
- expect(screen.queryByText('John Doe')).not.toBeInTheDocument();
438
-
439
- await act(async () => {
440
- await user.click(screen.getByText('Jane Smith (Journalist)'));
441
- });
442
-
443
- const chip = editor.querySelector('chip[data-label="Jane Smith"]');
444
- expect(chip).toBeInTheDocument();
445
-
446
- expect(
447
- screen.queryByText('Existing content and more text'),
448
- ).toBeInTheDocument();
449
- });
450
- });