mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(post-form): style errors like 4chan and move into tfoot
Use red banner styling for form errors and render them in the table footer so messages stay aligned with the post form layout.
This commit is contained in:
@@ -627,6 +627,9 @@ describe('PostForm', () => {
|
|||||||
await clickByText(table as HTMLTableElement, 'post');
|
await clickByText(table as HTMLTableElement, 'post');
|
||||||
expect(globalThis.alert).not.toHaveBeenCalled();
|
expect(globalThis.alert).not.toHaveBeenCalled();
|
||||||
expect(container.textContent).toContain('error: empty_comment_alert');
|
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<HTMLInputElement>('input[type="text"]') || [];
|
const textInputs = table?.querySelectorAll<HTMLInputElement>('input[type="text"]') || [];
|
||||||
const nameInput = textInputs[0];
|
const nameInput = textInputs[0];
|
||||||
@@ -710,8 +713,9 @@ describe('PostForm', () => {
|
|||||||
expect(linkInput.disabled).toBe(true);
|
expect(linkInput.disabled).toBe(true);
|
||||||
expect(container.textContent).toContain('youtube_thumbnail_link_conversion_notice');
|
expect(container.textContent).toContain('youtube_thumbnail_link_conversion_notice');
|
||||||
expect(container.textContent).toContain('3');
|
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()?.className).toContain('formError');
|
||||||
|
expect(getConversionNotice()?.closest('tfoot')).toBeTruthy();
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
vi.advanceTimersByTime(1000);
|
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.');
|
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('error');
|
||||||
expect(delayedOptionsError?.className).toContain('formError');
|
expect(delayedOptionsError?.className).toContain('formError');
|
||||||
|
expect(delayedOptionsError?.closest('tfoot')).toBeTruthy();
|
||||||
|
|
||||||
await dispatchInput(textarea as HTMLTextAreaElement, 'fortune body');
|
await dispatchInput(textarea as HTMLTextAreaElement, 'fortune body');
|
||||||
await clickByText(table as HTMLTableElement, 'post');
|
await clickByText(table as HTMLTableElement, 'post');
|
||||||
@@ -1096,6 +1101,7 @@ describe('PostForm', () => {
|
|||||||
|
|
||||||
expect(testState.publishPostOptions.content).toBe(longContent);
|
expect(testState.publishPostOptions.content).toBe(longContent);
|
||||||
expect(container.textContent).toContain('comment_field_too_long');
|
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(container.textContent).not.toContain('[fortune color=');
|
||||||
expect(testState.publishPostMock).not.toHaveBeenCalled();
|
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');
|
const moderatorWarning = Array.from(container.querySelectorAll('div')).find((element) => element.textContent === 'warning: posting as moderator');
|
||||||
expect(moderatorWarning?.className).toContain('error');
|
expect(moderatorWarning?.className).toContain('error');
|
||||||
expect(moderatorWarning?.className).toContain('formError');
|
expect(moderatorWarning?.className).toContain('formError');
|
||||||
|
expect(moderatorWarning?.closest('tfoot')).toBeTruthy();
|
||||||
expect(table?.textContent).not.toContain('Mod editor');
|
expect(table?.textContent).not.toContain('Mod editor');
|
||||||
expect(table?.textContent).not.toContain('mods only');
|
expect(table?.textContent).not.toContain('mods only');
|
||||||
expect(table?.querySelector('button[aria-label="Quote"]')).toBeNull();
|
expect(table?.querySelector('button[aria-label="Quote"]')).toBeNull();
|
||||||
|
|||||||
@@ -236,9 +236,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
|
display: block;
|
||||||
|
margin: 3px 0 0;
|
||||||
|
padding: 3px 5px;
|
||||||
|
background-color: #e62020;
|
||||||
|
color: #fff;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 13px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
padding: 5px;
|
line-height: normal;
|
||||||
color: red;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.error a,
|
.error a,
|
||||||
@@ -248,8 +254,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.formError {
|
.formError {
|
||||||
margin: 8px 0;
|
box-sizing: border-box;
|
||||||
padding: 6px 5px;
|
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 {
|
.status {
|
||||||
|
|||||||
@@ -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 { Trans, useTranslation } from 'react-i18next';
|
||||||
import type { TFunction } from 'i18next';
|
import type { TFunction } from 'i18next';
|
||||||
import { Link, useLocation, useNavigate, useParams } from 'react-router-dom';
|
import { Link, useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||||
@@ -154,7 +154,6 @@ interface PostFormFieldsProps {
|
|||||||
textRef: React.RefObject<HTMLTextAreaElement | null>;
|
textRef: React.RefObject<HTMLTextAreaElement | null>;
|
||||||
urlRef: React.Ref<HTMLInputElement>;
|
urlRef: React.Ref<HTMLInputElement>;
|
||||||
url: string;
|
url: string;
|
||||||
lengthError: string | null;
|
|
||||||
handleContentChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
handleContentChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
|
||||||
handleContentValueChange: (content: string, options?: string) => void;
|
handleContentValueChange: (content: string, options?: string) => void;
|
||||||
handleLinkChange: (link: string) => void;
|
handleLinkChange: (link: string) => void;
|
||||||
@@ -207,7 +206,6 @@ const PostFormFields = ({
|
|||||||
textRef,
|
textRef,
|
||||||
urlRef,
|
urlRef,
|
||||||
url,
|
url,
|
||||||
lengthError,
|
|
||||||
handleContentChange,
|
handleContentChange,
|
||||||
handleContentValueChange,
|
handleContentValueChange,
|
||||||
handleLinkChange,
|
handleLinkChange,
|
||||||
@@ -336,7 +334,6 @@ const PostFormFields = ({
|
|||||||
hidden={showBbcodeToolbar && isBbcodePreviewing}
|
hidden={showBbcodeToolbar && isBbcodePreviewing}
|
||||||
onChange={handleContentChange}
|
onChange={handleContentChange}
|
||||||
/>
|
/>
|
||||||
{lengthError && <div className={styles.error}>{lengthError}</div>}
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{flagOptions.length > 0 && (
|
{flagOptions.length > 0 && (
|
||||||
@@ -505,6 +502,16 @@ const PostFormFields = ({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const PostFormErrorRow = ({ ariaLive, children }: { ariaLive?: 'polite'; children: ReactNode }) => (
|
||||||
|
<tr className={styles.formErrorRow}>
|
||||||
|
<td colSpan={2}>
|
||||||
|
<div className={`${styles.error} ${styles.formError}`} aria-live={ariaLive}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
|
||||||
const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid: string }) => {
|
const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid: string }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@@ -905,7 +912,6 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
|||||||
textRef={textRef}
|
textRef={textRef}
|
||||||
urlRef={urlRef}
|
urlRef={urlRef}
|
||||||
url={url}
|
url={url}
|
||||||
lengthError={lengthError}
|
|
||||||
handleContentChange={handleContentChange}
|
handleContentChange={handleContentChange}
|
||||||
handleContentValueChange={handleContentValueChange}
|
handleContentValueChange={handleContentValueChange}
|
||||||
handleLinkChange={handleLinkChange}
|
handleLinkChange={handleLinkChange}
|
||||||
@@ -943,19 +949,20 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
|||||||
disableReplyPublish={isResolvingExternalQuotes}
|
disableReplyPublish={isResolvingExternalQuotes}
|
||||||
/>
|
/>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
{moderationPostingWarning ? <PostFormErrorRow>{moderationPostingWarning}</PostFormErrorRow> : null}
|
||||||
|
{lengthError ? <PostFormErrorRow>{lengthError}</PostFormErrorRow> : null}
|
||||||
|
{youtubeThumbnailConversionCountdown !== null ? (
|
||||||
|
<PostFormErrorRow ariaLive='polite'>{t('youtube_thumbnail_link_conversion_notice', { count: youtubeThumbnailConversionCountdown })}</PostFormErrorRow>
|
||||||
|
) : formError ? (
|
||||||
|
<PostFormErrorRow>
|
||||||
|
{isPostOptionsValidationError(formError) ? <PostOptionsErrorMessage error={formError} directories={directories} /> : formError}
|
||||||
|
</PostFormErrorRow>
|
||||||
|
) : null}
|
||||||
|
{publishPostError ? <PostFormErrorRow>{publishPostError}</PostFormErrorRow> : null}
|
||||||
|
{publishReplyError ? <PostFormErrorRow>{publishReplyError}</PostFormErrorRow> : null}
|
||||||
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
{moderationPostingWarning ? <div className={`${styles.error} ${styles.formError}`}>{moderationPostingWarning}</div> : null}
|
|
||||||
{youtubeThumbnailConversionCountdown !== null ? (
|
|
||||||
<div className={`${styles.error} ${styles.formError}`} aria-live='polite'>
|
|
||||||
{t('youtube_thumbnail_link_conversion_notice', { count: youtubeThumbnailConversionCountdown })}
|
|
||||||
</div>
|
|
||||||
) : formError ? (
|
|
||||||
<div className={`${styles.error} ${styles.formError}`}>
|
|
||||||
{isPostOptionsValidationError(formError) ? <PostOptionsErrorMessage error={formError} directories={directories} /> : formError}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{publishPostError && <div className={`${styles.error} ${styles.formError}`}>{publishPostError}</div>}
|
|
||||||
{publishReplyError && <div className={`${styles.error} ${styles.formError}`}>{publishReplyError}</div>}
|
|
||||||
{publishReplyStateMessage && <div className={styles.status}>{publishReplyStateMessage}</div>}
|
{publishReplyStateMessage && <div className={styles.status}>{publishReplyStateMessage}</div>}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user