From dc43c51b0df43294e017f17be71649314ed0d349 Mon Sep 17 00:00:00 2001 From: Jay Sitter <jay@jaysitter.com> Date: Sun, 25 Jun 2023 02:27:07 -0400 Subject: [PATCH] fix(a11y): Change the look and behavior of some file upload fields --- src/assets/css/main.css | 9 ++ .../components/common/image-upload-form.tsx | 50 +++---- src/shared/components/home/emojis-form.tsx | 51 +++---- src/shared/components/home/site-form.tsx | 38 ++--- src/shared/components/post/post-form.tsx | 135 +++++++++--------- 5 files changed, 146 insertions(+), 137 deletions(-) diff --git a/src/assets/css/main.css b/src/assets/css/main.css index cb4a8b8..dde2a50 100644 --- a/src/assets/css/main.css +++ b/src/assets/css/main.css @@ -88,6 +88,15 @@ color: var(--bs-gray) !important; } +input[type="file"]::file-selector-button { + font: inherit; + border: 0; + padding: 0.375em 0.75em; + border-radius: var(--bs-border-radius); + background-color: var(--bs-secondary); + color: var(--bs-white); +} + .icon { display: inline-grid; display: inline-flex; diff --git a/src/shared/components/common/image-upload-form.tsx b/src/shared/components/common/image-upload-form.tsx index e8005cc..107949f 100644 --- a/src/shared/components/common/image-upload-form.tsx +++ b/src/shared/components/common/image-upload-form.tsx @@ -33,38 +33,34 @@ export class ImageUploadForm extends Component< render() { return ( <form className="image-upload-form d-inline"> - <label htmlFor={this.id} className="pointer text-muted small fw-bold"> - {this.props.imageSrc ? ( - <span className="d-inline-block position-relative"> - {/* TODO: Create "Current Iamge" translation for alt text */} - <img - alt="" - src={this.props.imageSrc} - height={this.props.rounded ? 60 : ""} - width={this.props.rounded ? 60 : ""} - className={`img-fluid ${ - this.props.rounded ? "rounded-circle" : "" - }`} - /> - <button - className="position-absolute d-block p-0 end-0 border-0 top-0 bg-transparent text-white" - type="button" - onClick={linkEvent(this, this.handleRemoveImage)} - aria-label={I18NextService.i18n.t("remove")} - > - <Icon icon="x" classes="mini-overlay" /> - </button> - </span> - ) : ( - <span className="btn btn-secondary">{this.props.uploadTitle}</span> - )} - </label> + {this.props.imageSrc && ( + <span className="d-inline-block position-relative"> + {/* TODO: Create "Current Iamge" translation for alt text */} + <img + alt="" + src={this.props.imageSrc} + height={this.props.rounded ? 60 : ""} + width={this.props.rounded ? 60 : ""} + className={`img-fluid ${ + this.props.rounded ? "rounded-circle" : "" + }`} + /> + <button + className="position-absolute d-block p-0 end-0 border-0 top-0 bg-transparent text-white" + type="button" + onClick={linkEvent(this, this.handleRemoveImage)} + aria-label={I18NextService.i18n.t("remove")} + > + <Icon icon="x" classes="mini-overlay" /> + </button> + </span> + )} <input id={this.id} type="file" accept="image/*,video/*" + className="small" name={this.id} - className="d-none" disabled={!UserService.Instance.myUserInfo} onChange={linkEvent(this, this.handleImageUpload)} /> diff --git a/src/shared/components/home/emojis-form.tsx b/src/shared/components/home/emojis-form.tsx index caf8221..930bd55 100644 --- a/src/shared/components/home/emojis-form.tsx +++ b/src/shared/components/home/emojis-form.tsx @@ -87,7 +87,10 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> { </div> )} <div className="table-responsive"> - <table id="emojis_table" className="table table-sm table-hover"> + <table + id="emojis_table" + className="table table-sm table-hover align-middle" + > <thead className="pointer"> <tr> <th>{I18NextService.i18n.t("column_emoji")}</th> @@ -129,30 +132,30 @@ export class EmojiForm extends Component<EmojiFormProps, EmojiFormState> { /> )} {cv.image_url.length === 0 && ( - <form> - <label - className="btn btn-sm btn-secondary pointer" - htmlFor={`file-uploader-${index}`} - data-tippy-content={I18NextService.i18n.t( - "upload_image" - )} - > - {capitalizeFirstLetter( - I18NextService.i18n.t("upload") + <label + // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex + tabIndex={0} + className="btn btn-sm btn-secondary pointer" + htmlFor={`file-uploader-${index}`} + data-tippy-content={I18NextService.i18n.t( + "upload_image" + )} + > + {capitalizeFirstLetter( + I18NextService.i18n.t("upload") + )} + <input + name={`file-uploader-${index}`} + id={`file-uploader-${index}`} + type="file" + accept="image/*" + className="d-none" + onChange={linkEvent( + { form: this, index: index }, + this.handleImageUpload )} - <input - name={`file-uploader-${index}`} - id={`file-uploader-${index}`} - type="file" - accept="image/*" - className="d-none" - onChange={linkEvent( - { form: this, index: index }, - this.handleImageUpload - )} - /> - </label> - </form> + /> + </label> )} </td> <td className="text-right"> diff --git a/src/shared/components/home/site-form.tsx b/src/shared/components/home/site-form.tsx index 382f565..e30d13b 100644 --- a/src/shared/components/home/site-form.tsx +++ b/src/shared/components/home/site-form.tsx @@ -155,28 +155,32 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> { /> </div> </div> - <div className="input-group mb-3"> - <label className="me-2 col-form-label"> + <div className="row mb-3"> + <label className="col-sm-2 col-form-label"> {I18NextService.i18n.t("icon")} </label> - <ImageUploadForm - uploadTitle={I18NextService.i18n.t("upload_icon")} - imageSrc={this.state.siteForm.icon} - onUpload={this.handleIconUpload} - onRemove={this.handleIconRemove} - rounded - /> + <div className="col-sm-10"> + <ImageUploadForm + uploadTitle={I18NextService.i18n.t("upload_icon")} + imageSrc={this.state.siteForm.icon} + onUpload={this.handleIconUpload} + onRemove={this.handleIconRemove} + rounded + /> + </div> </div> - <div className="input-group mb-3"> - <label className="me-2 col-form-label"> + <div className="row mb-3"> + <label className="col-sm-2 col-form-label"> {I18NextService.i18n.t("banner")} </label> - <ImageUploadForm - uploadTitle={I18NextService.i18n.t("upload_banner")} - imageSrc={this.state.siteForm.banner} - onUpload={this.handleBannerUpload} - onRemove={this.handleBannerRemove} - /> + <div className="col-sm-10"> + <ImageUploadForm + uploadTitle={I18NextService.i18n.t("upload_banner")} + imageSrc={this.state.siteForm.banner} + onUpload={this.handleBannerUpload} + onRemove={this.handleBannerRemove} + /> + </div> </div> <div className="mb-3 row"> <label className="col-12 col-form-label" htmlFor="site-desc"> diff --git a/src/shared/components/post/post-form.tsx b/src/shared/components/post/post-form.tsx index 081792a..b71ef54 100644 --- a/src/shared/components/post/post-form.tsx +++ b/src/shared/components/post/post-form.tsx @@ -347,32 +347,12 @@ export class PostForm extends Component<PostFormProps, PostFormState> { <input type="url" id="post-url" - className="form-control" + className="form-control mb-3" value={url} onInput={linkEvent(this, handlePostUrlChange)} onPaste={linkEvent(this, handleImageUploadPaste)} /> {this.renderSuggestedTitleCopy()} - <form> - <label - htmlFor="file-upload" - className={`${ - UserService.Instance.myUserInfo && "pointer" - } d-inline-block float-right text-muted fw-bold`} - data-tippy-content={I18NextService.i18n.t("upload_image")} - > - <Icon icon="image" classes="icon-inline" /> - </label> - <input - id="file-upload" - type="file" - accept="image/*,video/*" - name="file" - className="d-none" - disabled={!UserService.Instance.myUserInfo} - onChange={linkEvent(this, handleImageUpload)} - /> - </form> {url && validURL(url) && ( <div> <a @@ -402,56 +382,73 @@ export class PostForm extends Component<PostFormProps, PostFormState> { </a> </div> )} - {this.state.imageLoading && <Spinner />} - {url && isImage(url) && ( - <img src={url} className="img-fluid" alt="" /> - )} - {this.state.imageDeleteUrl && ( - <button - className="btn btn-danger btn-sm mt-2" - onClick={linkEvent(this, handleImageDelete)} - aria-label={I18NextService.i18n.t("delete")} - data-tippy-content={I18NextService.i18n.t("delete")} - > - <Icon icon="x" classes="icon-inline me-1" /> - {capitalizeFirstLetter(I18NextService.i18n.t("delete"))} - </button> - )} - {this.props.crossPosts && this.props.crossPosts.length > 0 && ( - <> - <div className="my-1 text-muted small fw-bold"> - {I18NextService.i18n.t("cross_posts")} - </div> - <PostListings - showCommunity - posts={this.props.crossPosts} - enableDownvotes={this.props.enableDownvotes} - enableNsfw={this.props.enableNsfw} - allLanguages={this.props.allLanguages} - siteLanguages={this.props.siteLanguages} - viewOnly - // All of these are unused, since its view only - onPostEdit={() => {}} - onPostVote={() => {}} - onPostReport={() => {}} - onBlockPerson={() => {}} - onLockPost={() => {}} - onDeletePost={() => {}} - onRemovePost={() => {}} - onSavePost={() => {}} - onFeaturePost={() => {}} - onPurgePerson={() => {}} - onPurgePost={() => {}} - onBanPersonFromCommunity={() => {}} - onBanPerson={() => {}} - onAddModToCommunity={() => {}} - onAddAdmin={() => {}} - onTransferCommunity={() => {}} - /> - </> - )} </div> </div> + + <div className="mb-3 row"> + <label htmlFor="file-upload" className={"col-sm-2 col-form-label"}> + {capitalizeFirstLetter(I18NextService.i18n.t("image"))} + <Icon icon="image" classes="icon-inline ms-1" /> + </label> + <input + id="file-upload" + type="file" + accept="image/*,video/*" + name="file" + className="small col-sm-10" + disabled={!UserService.Instance.myUserInfo} + onChange={linkEvent(this, handleImageUpload)} + /> + {this.state.imageLoading && <Spinner />} + {url && isImage(url) && ( + <img src={url} className="img-fluid" alt="" /> + )} + {this.state.imageDeleteUrl && ( + <button + className="btn btn-danger btn-sm mt-2" + onClick={linkEvent(this, handleImageDelete)} + aria-label={I18NextService.i18n.t("delete")} + data-tippy-content={I18NextService.i18n.t("delete")} + > + <Icon icon="x" classes="icon-inline me-1" /> + {capitalizeFirstLetter(I18NextService.i18n.t("delete"))} + </button> + )} + {this.props.crossPosts && this.props.crossPosts.length > 0 && ( + <> + <div className="my-1 text-muted small fw-bold"> + {I18NextService.i18n.t("cross_posts")} + </div> + <PostListings + showCommunity + posts={this.props.crossPosts} + enableDownvotes={this.props.enableDownvotes} + enableNsfw={this.props.enableNsfw} + allLanguages={this.props.allLanguages} + siteLanguages={this.props.siteLanguages} + viewOnly + // All of these are unused, since its view only + onPostEdit={() => {}} + onPostVote={() => {}} + onPostReport={() => {}} + onBlockPerson={() => {}} + onLockPost={() => {}} + onDeletePost={() => {}} + onRemovePost={() => {}} + onSavePost={() => {}} + onFeaturePost={() => {}} + onPurgePerson={() => {}} + onPurgePost={() => {}} + onBanPersonFromCommunity={() => {}} + onBanPerson={() => {}} + onAddModToCommunity={() => {}} + onAddAdmin={() => {}} + onTransferCommunity={() => {}} + /> + </> + )} + </div> + <div className="mb-3 row"> <label className="col-sm-2 col-form-label" htmlFor="post-title"> {I18NextService.i18n.t("title")} -- 2.44.1