diff --git a/src/components/post-form/__tests__/post-form.test.tsx b/src/components/post-form/__tests__/post-form.test.tsx index 1a428fa3..a00529f1 100644 --- a/src/components/post-form/__tests__/post-form.test.tsx +++ b/src/components/post-form/__tests__/post-form.test.tsx @@ -627,6 +627,9 @@ describe('PostForm', () => { await clickByText(table as HTMLTableElement, 'post'); expect(globalThis.alert).not.toHaveBeenCalled(); expect(container.textContent).toContain('error: empty_comment_alert'); + expect(table?.querySelector('tbody tr.rules')?.textContent).toContain('Please read the Rules and FAQ before posting.'); + expect(table?.querySelector('tfoot')?.textContent).toContain('error: empty_comment_alert'); + expect(table?.querySelector('tbody')?.nextElementSibling).toBe(table?.querySelector('tfoot')); const textInputs = table?.querySelectorAll('input[type="text"]') || []; const nameInput = textInputs[0]; @@ -710,8 +713,9 @@ describe('PostForm', () => { expect(linkInput.disabled).toBe(true); expect(container.textContent).toContain('youtube_thumbnail_link_conversion_notice'); expect(container.textContent).toContain('3'); - expect(table.textContent).not.toContain('youtube_thumbnail_link_conversion_notice'); + expect(table.textContent).toContain('youtube_thumbnail_link_conversion_notice'); expect(getConversionNotice()?.className).toContain('formError'); + expect(getConversionNotice()?.closest('tfoot')).toBeTruthy(); await act(async () => { vi.advanceTimersByTime(1000); @@ -1059,6 +1063,7 @@ describe('PostForm', () => { const delayedOptionsError = Array.from(container.querySelectorAll('div')).find((element) => element.textContent === 'Unsupported options: x, y, z.'); expect(delayedOptionsError?.className).toContain('error'); expect(delayedOptionsError?.className).toContain('formError'); + expect(delayedOptionsError?.closest('tfoot')).toBeTruthy(); await dispatchInput(textarea as HTMLTextAreaElement, 'fortune body'); await clickByText(table as HTMLTableElement, 'post'); @@ -1096,6 +1101,7 @@ describe('PostForm', () => { expect(testState.publishPostOptions.content).toBe(longContent); expect(container.textContent).toContain('comment_field_too_long'); + expect(table.querySelector('tfoot')?.textContent).toContain('comment_field_too_long'); expect(container.textContent).not.toContain('[fortune color='); expect(testState.publishPostMock).not.toHaveBeenCalled(); }); @@ -1253,6 +1259,7 @@ describe('PostForm', () => { const moderatorWarning = Array.from(container.querySelectorAll('div')).find((element) => element.textContent === 'warning: posting as moderator'); expect(moderatorWarning?.className).toContain('error'); expect(moderatorWarning?.className).toContain('formError'); + expect(moderatorWarning?.closest('tfoot')).toBeTruthy(); expect(table?.textContent).not.toContain('Mod editor'); expect(table?.textContent).not.toContain('mods only'); expect(table?.querySelector('button[aria-label="Quote"]')).toBeNull(); diff --git a/src/components/post-form/post-form.module.css b/src/components/post-form/post-form.module.css index 92067da8..3cd8b492 100644 --- a/src/components/post-form/post-form.module.css +++ b/src/components/post-form/post-form.module.css @@ -236,9 +236,15 @@ } .error { + display: block; + margin: 3px 0 0; + padding: 3px 5px; + background-color: #e62020; + color: #fff; + font-family: monospace; + font-size: 13px; font-weight: bold; - padding: 5px; - color: red; + line-height: normal; } .error a, @@ -248,8 +254,20 @@ } .formError { - margin: 8px 0; - padding: 6px 5px; + box-sizing: border-box; + width: 468px; + max-width: calc(100vw - 2px); + text-align: start; +} + +.postFormTable .formErrorRow td { + width: auto; + padding: 0; + border: 0; + background-color: transparent; + color: inherit; + font-weight: normal; + text-transform: none; } .status { diff --git a/src/components/post-form/post-form.tsx b/src/components/post-form/post-form.tsx index cd3a7f87..2db00c36 100644 --- a/src/components/post-form/post-form.tsx +++ b/src/components/post-form/post-form.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; +import { type ReactNode, useCallback, useEffect, useRef, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import type { TFunction } from 'i18next'; import { Link, useLocation, useNavigate, useParams } from 'react-router-dom'; @@ -154,7 +154,6 @@ interface PostFormFieldsProps { textRef: React.RefObject; urlRef: React.Ref; url: string; - lengthError: string | null; handleContentChange: (e: React.ChangeEvent) => void; handleContentValueChange: (content: string, options?: string) => void; handleLinkChange: (link: string) => void; @@ -207,7 +206,6 @@ const PostFormFields = ({ textRef, urlRef, url, - lengthError, handleContentChange, handleContentValueChange, handleLinkChange, @@ -336,7 +334,6 @@ const PostFormFields = ({ hidden={showBbcodeToolbar && isBbcodePreviewing} onChange={handleContentChange} /> - {lengthError &&
{lengthError}
} {flagOptions.length > 0 && ( @@ -505,6 +502,16 @@ const PostFormFields = ({ ); +const PostFormErrorRow = ({ ariaLive, children }: { ariaLive?: 'polite'; children: ReactNode }) => ( + + +
+ {children} +
+ + +); + const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid: string }) => { const { t } = useTranslation(); const params = useParams(); @@ -905,7 +912,6 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid: textRef={textRef} urlRef={urlRef} url={url} - lengthError={lengthError} handleContentChange={handleContentChange} handleContentValueChange={handleContentValueChange} handleLinkChange={handleLinkChange} @@ -943,19 +949,20 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid: disableReplyPublish={isResolvingExternalQuotes} /> + + {moderationPostingWarning ? {moderationPostingWarning} : null} + {lengthError ? {lengthError} : null} + {youtubeThumbnailConversionCountdown !== null ? ( + {t('youtube_thumbnail_link_conversion_notice', { count: youtubeThumbnailConversionCountdown })} + ) : formError ? ( + + {isPostOptionsValidationError(formError) ? : formError} + + ) : null} + {publishPostError ? {publishPostError} : null} + {publishReplyError ? {publishReplyError} : null} + - {moderationPostingWarning ?
{moderationPostingWarning}
: null} - {youtubeThumbnailConversionCountdown !== null ? ( -
- {t('youtube_thumbnail_link_conversion_notice', { count: youtubeThumbnailConversionCountdown })} -
- ) : formError ? ( -
- {isPostOptionsValidationError(formError) ? : formError} -
- ) : null} - {publishPostError &&
{publishPostError}
} - {publishReplyError &&
{publishReplyError}
} {publishReplyStateMessage &&
{publishReplyStateMessage}
} );