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:
parent
7e279b5c56
commit
31143c5a1e
console/src/new-components
@ -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>
|
||||
);
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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}`}
|
||||
|
@ -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}
|
||||
|
Loading…
Reference in New Issue
Block a user