1
0
mirror of https://github.com/hasura/graphql-engine.git synced 2024-12-15 01:12:56 +03:00

Con 411: fix code editor accessibility issue

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/5428
GitOrigin-RevId: 558f83fb789087b52b9ea0a442ba1d5950b30c11
This commit is contained in:
Nicolas Inchauspe 2022-08-18 14:53:04 +02:00 committed by hasura-bot
parent 7e279b5c56
commit 31143c5a1e
6 changed files with 163 additions and 44 deletions

View File

@ -1,13 +1,48 @@
import React from 'react';
import clsx from 'clsx';
import get from 'lodash.get';
import AceEditor, { IAceOptions, IEditorProps } from 'react-ace';
import AceEditor, {
IAceOptions,
IAceEditorProps,
ICommandManager,
} from 'react-ace';
import 'ace-builds/src-noconflict/theme-github';
import 'ace-builds/src-noconflict/theme-eclipse';
import 'ace-builds/src-noconflict/ext-language_tools';
import { FieldError, useFormContext, Controller } from 'react-hook-form';
import { FieldWrapper, FieldWrapperPassThroughProps } from './FieldWrapper';
// Allows to integrate the code editor in a form: by default, tab key adds a
// tab character to the editor content, here, we want to disable this behavior
// and allow to navigate with the tab key. This is done by setting the command.
// Solution found here: https://stackoverflow.com/questions/24963246/ace-editor-simply-re-enable-command-after-disabled-it
const setEditorCommandEnabled = (
editor: IAceEditorProps,
name: string,
enabled: boolean
) => {
const commands: ICommandManager =
editor?.commands as unknown as ICommandManager;
const command = commands.byName[name];
if (!command.bindKeyOriginal) {
command.bindKeyOriginal = command.bindKey;
}
command.bindKey = enabled ? command.bindKeyOriginal : null;
commands.addCommand(command);
// Special case for backspace and delete which will be called from
// textarea if not handled by main commandb binding
if (!enabled) {
let key: any = command.bindKeyOriginal;
if (key && typeof key === 'object') {
key = key[commands.platform];
}
if (/backspace|delete/i.test(key) && commands?.bindKey) {
commands.bindKey(key, 'null');
}
}
};
export type CodeEditorFieldProps = FieldWrapperPassThroughProps & {
/**
* The code editor field name
@ -16,7 +51,7 @@ export type CodeEditorFieldProps = FieldWrapperPassThroughProps & {
/**
* The code editor props
*/
editorProps?: IEditorProps;
editorProps?: IAceEditorProps;
/**
* The code editor options
*/
@ -50,40 +85,116 @@ export const CodeEditorField: React.FC<CodeEditorFieldProps> = ({
formState: { errors },
} = useFormContext();
const maybeError = get(errors, name) as FieldError | undefined;
const editorRef = React.useRef<AceEditor>(null);
const [tipState, setTipState] = React.useState<'ANY' | 'ESC' | 'TAB'>('ANY');
return (
<FieldWrapper id={name} {...wrapperProps} error={maybeError}>
<div className={clsx(disabled ? 'cursor-not-allowed' : '')}>
<div
className={clsx(
'relative w-full max-w-xl',
disabled ? 'cursor-not-allowed' : ''
)}
>
<Controller
name={name}
control={control}
render={({
field: { value, name: controllerName, ref, onChange, onBlur },
}) => (
<AceEditor
name={controllerName}
ref={ref}
value={value}
theme={theme}
mode={mode}
onChange={onChange}
onBlur={onBlur}
editorProps={editorProps}
setOptions={editorOptions}
data-test={dataTest}
className={clsx(
'block relative inset-0 !w-inherit max-w-xl h-code input shadow-sm rounded border border-gray-300 hover:border-gray-400 focus:outline-0 focus:ring-2 focus:ring-yellow-200 focus:border-yellow-400 placeholder-gray-500',
maybeError
? 'border-red-600 hover:border-red-700'
: 'border-gray-300',
disabled
? 'bg-gray-100 border-gray-100 pointer-events-none'
: 'hover:border-gray-400'
)}
data-testid={name}
/>
)}
}) => {
return (
<>
{/* Hidden input to proxy focus event from the react-hook-form controller */}
<input
ref={ref}
onFocus={() => {
editorRef?.current?.editor?.focus();
}}
className="h-0 w-0 absolute"
tabIndex={-1}
/>
<AceEditor
name={controllerName}
ref={editorRef}
value={value}
theme={theme}
mode={mode}
onChange={onChange}
onBlur={() => {
setTipState('ANY');
onBlur();
}}
onFocus={() => {
setTipState('ESC');
if (editorRef?.current?.editor) {
setEditorCommandEnabled(
editorRef?.current?.editor,
'indent',
true
);
setEditorCommandEnabled(
editorRef?.current?.editor,
'outdent',
true
);
}
}}
commands={[
{
name: 'Esc',
bindKey: { win: 'Esc', mac: 'Esc' },
exec: () => {
setTipState('TAB');
if (editorRef?.current?.editor) {
setEditorCommandEnabled(
editorRef?.current?.editor,
'indent',
false
);
setEditorCommandEnabled(
editorRef?.current?.editor,
'outdent',
false
);
}
},
},
]}
editorProps={editorProps}
setOptions={editorOptions}
data-test={dataTest}
className={clsx(
'block relative inset-0 !w-inherit w-full max-w-xl h-code input shadow-sm rounded border border-gray-300 hover:border-gray-400 focus-within:outline-0 focus-within:ring-2 focus-within:ring-yellow-200 focus-within:border-yellow-400 placeholder-gray-500',
maybeError
? 'border-red-600 hover:border-red-700'
: 'border-gray-300',
disabled
? 'bg-gray-100 border-gray-100 pointer-events-none'
: 'hover:border-gray-400'
)}
data-testid={name}
/>
</>
);
}}
/>
{tipState === 'ESC' && (
<div className="absolute bg-legacybg top-full left-1 text-gray-600 text-sm mt-1">
Tip:{' '}
<strong>
Press <em>Esc</em> key
</strong>{' '}
then navigate with <em>Tab</em>
</div>
)}
{tipState === 'TAB' && (
<div className="absolute bg-legacybg top-full left-1 text-gray-600 text-sm mt-1">
Tip: Press <em>Esc</em> key then{' '}
<strong>
navigate with <em>Tab</em>
</strong>
</div>
)}
</div>
</FieldWrapper>
);

View File

@ -99,9 +99,10 @@ export const FieldWrapper = (props: FieldWrapperProps) => {
<div
className={clsx(
className,
'max-w-screen-md',
size === 'medium' ? 'w-1/2' : 'w-full',
horizontal && 'flex flex-row flex-wrap w-full justify-between'
horizontal
? 'flex flex-row flex-wrap w-full max-w-screen-md justify-between'
: 'max-w-xl'
)}
>
<label
@ -128,7 +129,7 @@ export const FieldWrapper = (props: FieldWrapperProps) => {
{tooltip ? <IconTooltip message={tooltip} /> : null}
</span>
{description ? (
<span className="text-gray-600 mb-xs font-normal text-sm">
<span className="text-muted mb-xs font-normal text-sm">
{description}
</span>
) : null}

View File

@ -286,7 +286,7 @@ export const TemplateManuallyTriggerFormValidation = args => {
### 💠 Manually focus a field
In this example, the form `inputFieldName` field is automatically focused when `args.focus` is `true` thanks to a
In this example, the form `codeEditorFieldName` field is automatically focused when `args.focus` is `true` thanks to a
`useImperativeHandle` hook:
```tsx
@ -295,7 +295,7 @@ const TemplateManuallyFocusField = args => {
useEffect(() => {
if (args && args.focus) {
// Use useEffect hook to wait for the form to be rendered before focusing field
formRef.current.setFocus('inputFieldName');
formRef.current.setFocus('codeEditorFieldName');
}
});
return (
@ -308,10 +308,10 @@ const TemplateManuallyFocusField = args => {
{({ control }) => (
<div className="space-y-2">
<h1 className="text-xl font-semibold mb-xs">Manually focus field</h1>
<InputField
name="inputFieldName"
label="The input field label"
placeholder="Input field placeholder"
<CodeEditorField
name="codeEditorFieldName"
label="The code editor field label"
placeholder="Code editor field placeholder"
/>
<Button type={'submit'} mode={'primary'}>
Submit
@ -330,7 +330,7 @@ export const TemplateManuallyFocusField = args => {
useEffect(() => {
if (args && args.focus) {
// Use useEffect hook to wait for the form to be rendered before before focusing field
formRef.current.setFocus('inputFieldName');
formRef.current.setFocus('codeEditorFieldName');
}
});
return (
@ -343,10 +343,10 @@ export const TemplateManuallyFocusField = args => {
{({ control }) => (
<div className="space-y-2">
<h1 className="text-xl font-semibold mb-xs">Manually focus field</h1>
<InputField
name="inputFieldName"
label="The input field label"
placeholder="Input field placeholder"
<CodeEditorField
name="codeEditorFieldName"
label="The code editor field label"
placeholder="Code editor field placeholder"
/>
<Button type={'submit'} mode={'primary'}>
Submit

View File

@ -63,7 +63,7 @@ export const InputField: React.FC<InputFieldProps> = ({
const maybeError = get(errors, name) as FieldError | undefined;
return (
<FieldWrapper id={name} {...wrapperProps} error={maybeError}>
<div className={clsx('relative flex')}>
<div className={clsx('relative flex max-w-xl')}>
{prependLabel !== '' ? (
<span className="inline-flex items-center h-input rounded-l text-muted font-semibold px-sm border border-r-0 border-gray-300 bg-gray-50 whitespace-nowrap shadow-sm">
{prependLabel}
@ -83,7 +83,7 @@ export const InputField: React.FC<InputFieldProps> = ({
aria-label={wrapperProps.label}
data-test={dataTest}
className={clsx(
'block w-full max-w-xl h-input shadow-sm rounded border border-gray-300 hover:border-gray-400 focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-yellow-200 focus-visible:border-yellow-400 placeholder-gray-500',
'block w-full h-input shadow-sm rounded border border-gray-300 hover:border-gray-400 focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-yellow-200 focus-visible:border-yellow-400 placeholder-gray-500',
prependLabel !== '' ? 'rounded-l-none' : '',
appendLabel !== '' ? 'rounded-r-none' : '',
maybeError

View File

@ -24,6 +24,7 @@ export const IconTooltip: React.VFC<IconTooltipProps> = ({
tooltipContentChildren={message}
side={side}
defaultOpen={defaultOpen}
className="flex items-center"
>
<FaQuestionCircle
className={`h-4 text-muted cursor-pointer ${className}`}

View File

@ -1,5 +1,6 @@
import React from 'react';
import * as RadixTooltip from '@radix-ui/react-tooltip';
import clsx from 'clsx';
export type TooltipProps = {
/**
@ -10,18 +11,23 @@ export type TooltipProps = {
* The tooltip content children
*/
tooltipContentChildren: React.ReactNode;
/**
* The tooltip classes
*/
className?: string;
} & Pick<RadixTooltip.TooltipContentProps, 'side'> &
Pick<RadixTooltip.TooltipProps, 'defaultOpen'>;
export const Tooltip: React.VFC<TooltipProps> = ({
children,
tooltipContentChildren,
className,
side = 'right',
defaultOpen = false,
}) => (
<RadixTooltip.Root delayDuration={0} defaultOpen={defaultOpen}>
<RadixTooltip.Trigger
className="ml-xs inline"
className={clsx('ml-xs inline', className)}
data-testid="tooltip-trigger"
>
{children}