mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-10-24 11:35:54 +08:00
dbeaver/pro#5241 [CB] Screen reader does not read some fields in the application (#3585)
* dbeaver/pro#5241 fix: input doesn't create label wrapper if label is not provided * dbeaver/pro#5241 fix: link label to input inside InputFieldBase * dbeaver/pro#5241 fix: make required field asterisk invisible for assistive technologies * dbeaver/pro#5241 fix: link FieldLabel to textarea with generated id and add aria-label for accessibility even with linked label, voiceOver doesn't pronounce label when focusing textarea, that's why aria-label is used there * dbeaver/pro#5241 refactor: force aria-label for input field * dbeaver/pro#5241 refactor: change role for snackbar aria-live polite works with aria regions which initially empty and then dynamically populated with some data. If we want assistive technologies read initial content, we need to use alert role. also hide icon from assistive technologies (no additional information from that) * dbeaver/pro#5241 refactor: remove aria-label from input use native htmlFor * dbeaver/pro#5241 refactor: remove aria-label from Textarea component --------- Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com> Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com>
This commit is contained in:
co-authored by
Evgenia
mr-anton-t
parent
ad6973d069
commit
603b94c551
@@ -6,23 +6,49 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { type ComponentPropsWithRef } from 'react';
|
||||
import { type ComponentPropsWithRef, type ReactElement, useId } from 'react';
|
||||
import './Input.css';
|
||||
import clsx from 'clsx';
|
||||
import { componentProviderWrapper } from '../componentProviderWrapper.js';
|
||||
|
||||
export interface InputProps extends Omit<ComponentPropsWithRef<'input'>, 'size'> {
|
||||
size?: 'small' | 'medium' | 'large' | 'xlarge';
|
||||
label?: React.ReactNode;
|
||||
}
|
||||
export type InputProps = Omit<ComponentPropsWithRef<'input'>, 'size'> & {
|
||||
label?: ReactElement | string;
|
||||
size?: 'small' | 'medium' | 'large' | 'xlarge';
|
||||
};
|
||||
|
||||
export const InputBase: React.FC<InputProps> = function Input({ size, className, ...props }) {
|
||||
export const InputBase: React.FC<InputProps> = function Input({
|
||||
size,
|
||||
className,
|
||||
label,
|
||||
'aria-labelledby': ariaLabelledBy,
|
||||
'aria-label': ariaLabel,
|
||||
...props
|
||||
}) {
|
||||
const labelId = useId();
|
||||
const classNameToApply = clsx(`dbv-kit-input`, `dbv-kit-input--${size ?? 'medium'}`, className);
|
||||
|
||||
if (label) {
|
||||
return (
|
||||
<label className="dbv-kit-input-wrapper">
|
||||
<div id={labelId} className={clsx('dbv-kit-input__title', props.required && 'dbv-kit-input__title--required')}>
|
||||
{label}
|
||||
</div>
|
||||
<input
|
||||
className={classNameToApply}
|
||||
aria-labelledby={labelId}
|
||||
{...props}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<label className="dbv-kit-input-wrapper">
|
||||
{props.label && <div className={clsx('dbv-kit-input__title', props.required && 'dbv-kit-input__title--required')}>{props.label}</div>}
|
||||
<input className={classNameToApply} {...props} />
|
||||
</label>
|
||||
<input
|
||||
className={classNameToApply}
|
||||
aria-labelledby={ariaLabelledBy}
|
||||
aria-label={ariaLabel}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -12,3 +12,7 @@
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: var(--theme-negative);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -23,7 +23,12 @@ export const FieldLabel: React.FC<PropsWithChildren<Props>> = observer(function
|
||||
return (
|
||||
<label {...rest} className={s(styles, { fieldLabel: true }, className)}>
|
||||
{children}
|
||||
{required && ' *'}
|
||||
{required && (
|
||||
<span className={s(styles, { required })} aria-hidden>
|
||||
{' '}
|
||||
*
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React, { forwardRef, useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||
import React, { forwardRef, useCallback, useId, useLayoutEffect, useRef, useState } from 'react';
|
||||
|
||||
import { isNotNullDefined } from '@dbeaver/js-helpers';
|
||||
|
||||
@@ -62,6 +62,7 @@ export const InputFieldBase = observer<InputFieldBaseProps, HTMLInputElement>(
|
||||
ref,
|
||||
) {
|
||||
const [uncontrolledInputValue, setUncontrolledInputValue] = useState(value);
|
||||
const inputId = useId();
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
const mergedRef = useCombinedRef(inputRef, ref);
|
||||
const capsLock = useCapsLockTracker();
|
||||
@@ -116,13 +117,14 @@ export const InputFieldBase = observer<InputFieldBaseProps, HTMLInputElement>(
|
||||
|
||||
return (
|
||||
<Field {...layoutProps} className={s(styles, {}, className)}>
|
||||
<FieldLabel title={labelTooltip || rest.title} className={s(styles, { fieldLabel: true })} required={required}>
|
||||
<FieldLabel htmlFor={inputId} title={labelTooltip || rest.title} className={s(styles, { fieldLabel: true })} required={required}>
|
||||
{children}
|
||||
</FieldLabel>
|
||||
<div className={s(styles, { inputContainer: true })}>
|
||||
<Input
|
||||
ref={mergedRef}
|
||||
{...rest}
|
||||
id={inputId}
|
||||
type={passwordRevealed ? 'text' : rest.type}
|
||||
name={name}
|
||||
value={uncontrolled ? undefined : value}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useCallback, useContext, useLayoutEffect, useRef } from 'react';
|
||||
import { useCallback, useContext, useId, useLayoutEffect, useRef } from 'react';
|
||||
|
||||
import { getTextFileReadingProcess } from '@cloudbeaver/core-utils';
|
||||
|
||||
@@ -69,6 +69,7 @@ export const Textarea: TextareaType = observer(function Textarea({
|
||||
...rest
|
||||
}: ControlledProps | ObjectProps<any, any>) {
|
||||
const translate = useTranslate();
|
||||
const inputId = useId();
|
||||
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const layoutProps = getLayoutProps(rest);
|
||||
rest = filterLayoutFakeProps(rest);
|
||||
@@ -102,12 +103,13 @@ export const Textarea: TextareaType = observer(function Textarea({
|
||||
|
||||
return (
|
||||
<Field {...layoutProps} className={s(styles, { field: true, embedded }, className)}>
|
||||
<FieldLabel className={s(styles, { fieldLabel: true })} title={labelTooltip || rest.title} required={required}>
|
||||
<FieldLabel htmlFor={inputId} className={s(styles, { fieldLabel: true })} title={labelTooltip || rest.title} required={required}>
|
||||
{children}
|
||||
</FieldLabel>
|
||||
<textarea
|
||||
{...rest}
|
||||
ref={textareaRef}
|
||||
id={inputId}
|
||||
required={required}
|
||||
className={s(styles, { textarea: true })}
|
||||
value={value ?? ''}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,7 +17,7 @@ type IconProps = HTMLProps<HTMLDivElement> & {
|
||||
|
||||
export const NotificationMark: React.FC<IconProps> = function NotificationMark({ type, ...props }) {
|
||||
return (
|
||||
<div {...props}>
|
||||
<div aria-hidden {...props}>
|
||||
{type === ENotificationType.Info && <IconOrImage icon="/icons/info_icon.svg" />}
|
||||
{type === ENotificationType.Error && <IconOrImage icon="/icons/error_icon.svg" />}
|
||||
{type === ENotificationType.Success && <IconOrImage icon="/icons/success_icon.svg" />}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -36,7 +36,13 @@ export const SnackbarWrapper: React.FC<React.PropsWithChildren<Props>> = functio
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div tabIndex={0} role="status" aria-live="polite" aria-atomic="true" className={s(styles, { notification: true, mounted, closing }, className)}>
|
||||
<div
|
||||
tabIndex={0}
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
aria-atomic="true"
|
||||
className={s(styles, { notification: true, mounted, closing }, className)}
|
||||
>
|
||||
{children}
|
||||
{!persistent && onClose && (
|
||||
<div className={s(styles, { iconButton: true })}>
|
||||
|
||||
Reference in New Issue
Block a user