From b454811698c7ea0cee944ed50030c13163cf72c9 Mon Sep 17 00:00:00 2001 From: Jarrod Flesch Date: Fri, 23 Sep 2022 13:11:24 -0400 Subject: [PATCH 1/4] fix: threads readOnly to ReactSelect --- .../forms/field-types/Select/index.tsx | 1 + test/fields/collections/Select/index.ts | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/admin/components/forms/field-types/Select/index.tsx b/src/admin/components/forms/field-types/Select/index.tsx index 246066eb0..6a8946d21 100644 --- a/src/admin/components/forms/field-types/Select/index.tsx +++ b/src/admin/components/forms/field-types/Select/index.tsx @@ -95,6 +95,7 @@ const Select: React.FC = (props) => { showError={showError} errorMessage={errorMessage} required={required} + readOnly={readOnly} description={description} style={style} className={className} diff --git a/test/fields/collections/Select/index.ts b/test/fields/collections/Select/index.ts index abf04f599..aeb0919f9 100644 --- a/test/fields/collections/Select/index.ts +++ b/test/fields/collections/Select/index.ts @@ -24,6 +24,27 @@ const SelectFields: CollectionConfig = { }, ], }, + { + name: 'selectReadOnly', + type: 'select', + admin: { + readOnly: true, + }, + options: [ + { + label: 'Value One', + value: 'one', + }, + { + label: 'Value Two', + value: 'two', + }, + { + label: 'Value Three', + value: 'three', + }, + ], + }, { name: 'selectHasMany', hasMany: true, From 918130486e1e38a3d57fb993f466207209c5c0bb Mon Sep 17 00:00:00 2001 From: Jarrod Flesch Date: Fri, 23 Sep 2022 14:34:02 -0400 Subject: [PATCH 2/4] fix: styles readOnly RichTextEditor, removes interactivity within when readOnly --- .../forms/field-types/RichText/RichText.tsx | 18 +++++-- .../forms/field-types/RichText/index.scss | 20 ++++++++ test/fields/collections/RichText/index.ts | 48 +++++++++++++++++-- test/fields/config.ts | 1 + 4 files changed, 80 insertions(+), 7 deletions(-) diff --git a/src/admin/components/forms/field-types/RichText/RichText.tsx b/src/admin/components/forms/field-types/RichText/RichText.tsx index cadd9f4be..e3fed8b81 100644 --- a/src/admin/components/forms/field-types/RichText/RichText.tsx +++ b/src/admin/components/forms/field-types/RichText/RichText.tsx @@ -70,6 +70,7 @@ const RichText: React.FC = (props) => { const [enabledLeaves, setEnabledLeaves] = useState({}); const [initialValueKey, setInitialValueKey] = useState(''); const editorRef = useRef(null); + const toolbarRef = useRef(null); const renderElement = useCallback(({ attributes, children, element }) => { const matchedElement = enabledElements[element?.type]; @@ -176,6 +177,17 @@ const RichText: React.FC = (props) => { setInitialValueKey(JSON.stringify(initialValue || '')); }, [initialValue]); + useEffect(() => { + if (loaded && readOnly) { + // disable interactions on all editor elements when read only + Array.from(editorRef.current.children).forEach((child) => { + const childAsElement = child as HTMLElement; + childAsElement.tabIndex = -1; + childAsElement.style.pointerEvents = 'none'; + }); + } + }, [loaded, readOnly]); + if (!loaded) { return null; } @@ -215,14 +227,14 @@ const RichText: React.FC = (props) => { editor={editor} value={valueToRender as any[]} onChange={(val) => { - if (val !== defaultValue && val !== value) { + if (!readOnly && val !== defaultValue && val !== value) { setValue(val); } }} >
-
-
+
+
{elements.map((element, i) => { let elementName: string; if (typeof element === 'object' && element?.name) elementName = element.name; diff --git a/src/admin/components/forms/field-types/RichText/index.scss b/src/admin/components/forms/field-types/RichText/index.scss index 730fbe195..01bb21ce9 100644 --- a/src/admin/components/forms/field-types/RichText/index.scss +++ b/src/admin/components/forms/field-types/RichText/index.scss @@ -3,6 +3,7 @@ .rich-text { margin-bottom: base(2); display: flex; + isolation: isolate; &__toolbar { @include blur-bg(var(--theme-elevation-0)); @@ -104,6 +105,25 @@ background-color: var(--theme-elevation-150); padding: base(.5); } + + .rich-text__toolbar { + pointer-events: none; + position: relative; + top: 0; + + &::after { + content: ' '; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: var(--theme-elevation-150); + opacity: .85; + z-index: 2; + backdrop-filter: unset; + } + } } &__button { diff --git a/test/fields/collections/RichText/index.ts b/test/fields/collections/RichText/index.ts index 579d3ea86..5c0d04b71 100644 --- a/test/fields/collections/RichText/index.ts +++ b/test/fields/collections/RichText/index.ts @@ -73,12 +73,46 @@ const RichTextFields: CollectionConfig = { }, }, }, + { + name: 'richTextReadOnly', + type: 'richText', + admin: { + readOnly: true, + link: { + fields: [ + { + name: 'rel', + label: 'Rel Attribute', + type: 'select', + hasMany: true, + options: [ + 'noopener', 'noreferrer', 'nofollow', + ], + admin: { + description: 'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.', + }, + }, + ], + }, + upload: { + collections: { + uploads: { + fields: [ + { + name: 'caption', + type: 'richText', + }, + ], + }, + }, + }, + }, + }, ], }; -export const richTextDoc = { - selectHasMany: ['one', 'five'], - richText: [ +function generateRichText() { + return [ { children: [ { @@ -220,7 +254,13 @@ export const richTextDoc = { ], }; }), - ], + ]; +} + +export const richTextDoc = { + selectHasMany: ['one', 'five'], + richText: generateRichText(), + richTextReadOnly: generateRichText(), }; export default RichTextFields; diff --git a/test/fields/config.ts b/test/fields/config.ts index f0d6b3ae7..b014ebac1 100644 --- a/test/fields/config.ts +++ b/test/fields/config.ts @@ -92,6 +92,7 @@ export default buildConfig({ const richTextUploadIndex = richTextDocWithRelationship.richText.findIndex(({ type }) => type === 'upload'); richTextDocWithRelationship.richText[richTextUploadIndex].value = { id: createdUploadDoc.id }; + richTextDocWithRelationship.richTextReadOnly[richTextUploadIndex].value = { id: createdUploadDoc.id }; await payload.create({ collection: 'rich-text-fields', data: richTextDocWithRelationship }); From 00ef1700ae41e68ff0831a587bf3f09fe6c2c966 Mon Sep 17 00:00:00 2001 From: Jarrod Flesch Date: Thu, 29 Sep 2022 11:49:25 -0400 Subject: [PATCH 3/4] fix: ajusts how disabled states are being set on anchors and buttons --- .../forms/field-types/RichText/RichText.tsx | 33 ++++++++++++++----- .../forms/field-types/RichText/index.scss | 4 +++ 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/admin/components/forms/field-types/RichText/RichText.tsx b/src/admin/components/forms/field-types/RichText/RichText.tsx index e3fed8b81..12f18f365 100644 --- a/src/admin/components/forms/field-types/RichText/RichText.tsx +++ b/src/admin/components/forms/field-types/RichText/RichText.tsx @@ -178,14 +178,28 @@ const RichText: React.FC = (props) => { }, [initialValue]); useEffect(() => { - if (loaded && readOnly) { - // disable interactions on all editor elements when read only - Array.from(editorRef.current.children).forEach((child) => { - const childAsElement = child as HTMLElement; - childAsElement.tabIndex = -1; - childAsElement.style.pointerEvents = 'none'; + function setClickableState(clickState: 'disabled' | 'enabled') { + const selectors = 'button, a, [role="button"]'; + const toolbarButtons: (HTMLButtonElement | HTMLAnchorElement)[] = toolbarRef.current.querySelectorAll(selectors); + const editorButtons: (HTMLButtonElement | HTMLAnchorElement)[] = editorRef.current.querySelectorAll(selectors); + + [...(toolbarButtons || []), ...(editorButtons || [])].forEach((child) => { + const isButton = child.tagName === 'BUTTON'; + const isDisabling = clickState === 'disabled'; + child.setAttribute('tabIndex', isDisabling ? '-1' : '0'); + if (isButton) child.setAttribute('disabled', isDisabling ? 'disabled' : null); }); } + + if (loaded && readOnly) { + setClickableState('disabled'); + } + + return () => { + if (loaded && readOnly) { + setClickableState('enabled'); + } + }; }, [loaded, readOnly]); if (!loaded) { @@ -233,8 +247,11 @@ const RichText: React.FC = (props) => { }} >
-
-
+
+
{elements.map((element, i) => { let elementName: string; if (typeof element === 'object' && element?.name) elementName = element.name; diff --git a/src/admin/components/forms/field-types/RichText/index.scss b/src/admin/components/forms/field-types/RichText/index.scss index 01bb21ce9..8b72848c5 100644 --- a/src/admin/components/forms/field-types/RichText/index.scss +++ b/src/admin/components/forms/field-types/RichText/index.scss @@ -104,6 +104,10 @@ .rich-text__editor { background-color: var(--theme-elevation-150); padding: base(.5); + + .popup button { + display: none; + } } .rich-text__toolbar { From 09a8144f3cc63f7ec15fd75f51b8ac8d0cf3f1b5 Mon Sep 17 00:00:00 2001 From: Jarrod Flesch Date: Thu, 29 Sep 2022 12:53:16 -0400 Subject: [PATCH 4/4] fix: richText e2e test, specific selectors --- test/fields/e2e.spec.ts | 113 ++++++++++++++++++++++------------------ 1 file changed, 62 insertions(+), 51 deletions(-) diff --git a/test/fields/e2e.spec.ts b/test/fields/e2e.spec.ts index e05a1efa1..0cc7865d9 100644 --- a/test/fields/e2e.spec.ts +++ b/test/fields/e2e.spec.ts @@ -143,73 +143,84 @@ describe('fields', () => { }); describe('fields - richText', () => { - test('should create new url link', async () => { + async function navigateToRichTextFields() { const url: AdminUrlUtil = new AdminUrlUtil(serverURL, 'rich-text-fields'); await page.goto(url.list); await page.locator('.row-1 .cell-id').click(); + } - // Open link popup - await page.locator('.rich-text__toolbar .link').click(); + describe('toolbar', () => { + test('should create new url link', async () => { + await navigateToRichTextFields(); - const editLinkModal = page.locator('.rich-text-link-edit-modal__template'); - await expect(editLinkModal).toBeVisible(); + // Open link popup + await page.locator('.rich-text__toolbar button:not([disabled]) .link').click(); - // Fill values and click Confirm - await editLinkModal.locator('#field-text').fill('link text'); - await editLinkModal.locator('label[for="field-linkType-custom"]').click(); - await editLinkModal.locator('#field-url').fill('https://payloadcms.com'); - await wait(200); - await editLinkModal.locator('button[type="submit"]').click(); + const editLinkModal = page.locator('.rich-text-link-edit-modal__template'); + await expect(editLinkModal).toBeVisible(); - // Remove link - await page.locator('span >> text="link text"').click(); - const popup = page.locator('.popup--active .rich-text-link__popup'); - await expect(popup.locator('.rich-text-link__link-label')).toBeVisible(); - await popup.locator('.rich-text-link__link-close').click(); - await expect(page.locator('span >> text="link text"')).toHaveCount(0); + // Fill values and click Confirm + await editLinkModal.locator('#field-text').fill('link text'); + await editLinkModal.locator('label[for="field-linkType-custom"]').click(); + await editLinkModal.locator('#field-url').fill('https://payloadcms.com'); + await wait(200); + await editLinkModal.locator('button[type="submit"]').click(); + + // Remove link from editor body + await page.locator('span >> text="link text"').click(); + const popup = page.locator('.popup--active .rich-text-link__popup'); + await expect(popup.locator('.rich-text-link__link-label')).toBeVisible(); + await popup.locator('.rich-text-link__link-close').click(); + await expect(page.locator('span >> text="link text"')).toHaveCount(0); + }); + + test('should not create new url link when read only', async () => { + await navigateToRichTextFields(); + + // Attempt to open link popup + const modalTrigger = page.locator('.rich-text--read-only .rich-text__toolbar button .link'); + await expect(modalTrigger).toBeDisabled(); + }); }); - test('should populate url link', async () => { - const url: AdminUrlUtil = new AdminUrlUtil(serverURL, 'rich-text-fields'); - await page.goto(url.list); - await page.locator('.row-1 .cell-id').click(); + describe('editor', () => { + test('should populate url link', async () => { + navigateToRichTextFields(); - // Open link popup - await page.locator('span >> text="render links"').click(); - const popup = page.locator('.popup--active .rich-text-link__popup'); - await expect(popup).toBeVisible(); - await expect(popup.locator('a')).toHaveAttribute('href', 'https://payloadcms.com'); + // Open link popup + await page.locator('#field-richText span >> text="render links"').click(); + const popup = page.locator('.popup--active .rich-text-link__popup'); + await expect(popup).toBeVisible(); + await expect(popup.locator('a')).toHaveAttribute('href', 'https://payloadcms.com'); - // Open link edit modal - await popup.locator('.rich-text-link__link-edit').click(); - const editLinkModal = page.locator('.rich-text-link-edit-modal__template'); - await expect(editLinkModal).toBeVisible(); + // Open link edit modal + await popup.locator('.rich-text-link__link-edit').click(); + const editLinkModal = page.locator('.rich-text-link-edit-modal__template'); + await expect(editLinkModal).toBeVisible(); - // Close link edit modal - await editLinkModal.locator('button[type="submit"]').click(); - await expect(editLinkModal).not.toBeVisible(); - }); + // Close link edit modal + await editLinkModal.locator('button[type="submit"]').click(); + await expect(editLinkModal).not.toBeVisible(); + }); - test('should populate relationship link', async () => { - const url: AdminUrlUtil = new AdminUrlUtil(serverURL, 'rich-text-fields'); - await page.goto(url.list); - await page.locator('.row-1 .cell-id').click(); + test('should populate relationship link', async () => { + navigateToRichTextFields(); - // Open link popup - await page.locator('span >> text="link to relationships"').click(); - const popup = page.locator('.popup--active .rich-text-link__popup'); - await expect(popup).toBeVisible(); - await expect(popup.locator('a')).toHaveAttribute('href', /\/admin\/collections\/array-fields\/.*/); + // Open link popup + await page.locator('#field-richText span >> text="link to relationships"').click(); + const popup = page.locator('.popup--active .rich-text-link__popup'); + await expect(popup).toBeVisible(); + await expect(popup.locator('a')).toHaveAttribute('href', /\/admin\/collections\/array-fields\/.*/); - // Open link edit modal - await popup.locator('.rich-text-link__link-edit').click(); - const editLinkModal = page.locator('.rich-text-link-edit-modal__template'); - await expect(editLinkModal).toBeVisible(); + // Open link edit modal + await popup.locator('.rich-text-link__link-edit').click(); + const editLinkModal = page.locator('.rich-text-link-edit-modal__template'); + await expect(editLinkModal).toBeVisible(); - // Close link edit modal - await editLinkModal.locator('button[type="submit"]').click(); - await expect(editLinkModal).not.toBeVisible(); - // await page.locator('span >> text="render links"').click(); + // Close link edit modal + await editLinkModal.locator('button[type="submit"]').click(); + await expect(editLinkModal).not.toBeVisible(); + }); }); }); });