chore: `CreateDiscussion` a11y improvements (#30583)

pull/30550/head
Douglas Fabris 2 years ago committed by GitHub
parent e666487c3d
commit 0a3355133d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 136
      apps/meteor/client/components/CreateDiscussion/CreateDiscussion.tsx
  2. 38
      apps/meteor/client/components/CreateDiscussion/DefaultParentRoomField.tsx
  3. 2
      apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json

@ -9,14 +9,15 @@ import {
Button,
Icon,
Box,
FieldDescription,
FieldHint,
FieldLabel,
FieldRow,
FieldError,
} from '@rocket.chat/fuselage';
import { useUniqueId } from '@rocket.chat/fuselage-hooks';
import { useTranslation, useEndpoint } from '@rocket.chat/ui-contexts';
import { useMutation } from '@tanstack/react-query';
import type { ComponentProps, ReactElement } from 'react';
import type { ReactElement } from 'react';
import React from 'react';
import { useForm, Controller } from 'react-hook-form';
@ -44,12 +45,12 @@ const CreateDiscussion = ({ onClose, defaultParentRoom, parentMessageId, nameSug
const t = useTranslation();
const {
register,
formState: { isDirty, errors },
formState: { isDirty, isSubmitting, isValidating, errors },
handleSubmit,
control,
watch,
} = useForm({
mode: 'onBlur',
defaultValues: {
name: nameSuggestion || '',
parentRoom: '',
@ -81,22 +82,28 @@ const CreateDiscussion = ({ onClose, defaultParentRoom, parentMessageId, nameSug
});
};
const targetChannelField = useUniqueId();
const encryptedField = useUniqueId();
const discussionField = useUniqueId();
const usersField = useUniqueId();
const firstMessageField = useUniqueId();
return (
<Modal
data-qa='create-discussion-modal'
wrapperFunction={(props: ComponentProps<typeof Box>) => <Box is='form' onSubmit={handleSubmit(handleCreate)} {...props} />}
wrapperFunction={(props) => <Box is='form' onSubmit={handleSubmit(handleCreate)} {...props} />}
>
<Modal.Header>
<Modal.Title>{t('Discussion_title')}</Modal.Title>
<Modal.Close onClick={onClose} />
<Modal.Close tabIndex={-1} onClick={onClose} />
</Modal.Header>
<Modal.Content>
<Box mbe={24}>{t('Discussion_description')}</Box>
<FieldGroup>
<Field>
<FieldDescription>{t('Discussion_description')}</FieldDescription>
</Field>
<Field>
<FieldLabel>{t('Discussion_target_channel')}</FieldLabel>
<FieldLabel htmlFor={targetChannelField} required>
{t('Discussion_target_channel')}
</FieldLabel>
<FieldRow>
{defaultParentRoom && (
<Controller
@ -105,78 +112,119 @@ const CreateDiscussion = ({ onClose, defaultParentRoom, parentMessageId, nameSug
render={() => <DefaultParentRoomField defaultParentRoom={defaultParentRoom} />}
/>
)}
{!defaultParentRoom && (
<Controller
control={control}
name='parentRoom'
rules={{ required: t('Field_required') }}
render={({ field: { onChange, value } }) => (
rules={{ required: t('error-the-field-is-required', { field: t('Discussion_target_channel') }) }}
render={({ field: { name, onBlur, onChange, value } }) => (
<RoomAutoComplete
value={value}
name={name}
onBlur={onBlur}
onChange={onChange}
value={value}
id={targetChannelField}
placeholder={t('Discussion_target_channel_description')}
disabled={Boolean(defaultParentRoom)}
aria-invalid={Boolean(errors.parentRoom)}
aria-required='true'
aria-describedby={`${targetChannelField}-error`}
/>
)}
/>
)}
</FieldRow>
{errors.parentRoom && <FieldError>{errors.parentRoom.message}</FieldError>}
{errors.parentRoom && (
<FieldError aria-live='assertive' id={`${targetChannelField}-error`}>
{errors.parentRoom.message}
</FieldError>
)}
</Field>
<Field display='flex' alignItems='center' flexDirection='row' justifyContent='spaceBetween' flexGrow={1}>
<Box display='flex' flexDirection='column' width='full'>
<FieldLabel>{t('Encrypted')}</FieldLabel>
</Box>
<Controller
control={control}
name='encrypted'
render={({ field: { onChange, value } }) => (
<ToggleSwitch
checked={value}
onChange={onChange}
aria-describedby='Encrypted_discussion_Description'
aria-labelledby='Encrypted_discussion_Label'
<Field>
<Box display='flex' alignItems='center' flexDirection='row' justifyContent='spaceBetween' flexGrow={1}>
<FieldLabel htmlFor={encryptedField}>{t('Encrypted')}</FieldLabel>
<FieldRow>
<Controller
control={control}
name='encrypted'
render={({ field: { value, ...field } }) => <ToggleSwitch id={encryptedField} {...field} checked={value} />}
/>
)}
/>
</FieldRow>
</Box>
</Field>
<Field>
<FieldLabel>{t('Discussion_name')}</FieldLabel>
<FieldLabel htmlFor={discussionField} required>
{t('Discussion_name')}
</FieldLabel>
<FieldRow>
<TextInput
{...register('name', { required: t('Field_required') })}
placeholder={t('New_discussion_name')}
addon={<Icon name='baloons' size='x20' />}
<Controller
name='name'
control={control}
rules={{ required: t('Field_required') }}
render={({ field }) => (
<TextInput
id={discussionField}
{...field}
placeholder={t('New_discussion_name')}
aria-invalid={Boolean(errors.name)}
aria-required='true'
aria-describedby={`${discussionField}-error`}
addon={<Icon name='baloons' size='x20' />}
/>
)}
/>
</FieldRow>
{errors.name && <FieldError>{errors.name.message}</FieldError>}
{errors.name && (
<FieldError aria-live='assertive' id={`${discussionField}-error`}>
{errors.name.message}
</FieldError>
)}
</Field>
<Field>
<FieldLabel>{t('Invite_Users')}</FieldLabel>
<FieldRow w='full' display='flex' flexDirection='column' alignItems='stretch'>
<FieldLabel htmlFor={usersField}>{t('Invite_Users')}</FieldLabel>
<FieldRow>
<Controller
control={control}
name='usernames'
render={({ field: { onChange, value } }) => (
<UserAutoCompleteMultiple value={value} onChange={onChange} placeholder={t('Username_Placeholder')} />
render={({ field: { name, onChange, value, onBlur } }) => (
<UserAutoCompleteMultiple
id={usersField}
name={name}
onChange={onChange}
value={value}
onBlur={onBlur}
placeholder={t('Username_Placeholder')}
/>
)}
/>
</FieldRow>
</Field>
<Field>
<FieldLabel>{t('Discussion_first_message_title')}</FieldLabel>
<FieldLabel htmlFor={firstMessageField}>{t('Discussion_first_message_title')}</FieldLabel>
<FieldRow>
<TextAreaInput {...register('firstMessage')} placeholder={t('New_discussion_first_message')} rows={5} disabled={encrypted} />
<Controller
control={control}
name='firstMessage'
render={({ field }) => (
<TextAreaInput
id={firstMessageField}
{...field}
placeholder={t('New_discussion_first_message')}
rows={5}
disabled={encrypted}
aria-describedby={`${firstMessageField}-hint`}
/>
)}
/>
</FieldRow>
{encrypted && <FieldDescription>{t('Discussion_first_message_disabled_due_to_e2e')}</FieldDescription>}
{encrypted && <FieldHint id={`${firstMessageField}-hint`}>{t('Discussion_first_message_disabled_due_to_e2e')}</FieldHint>}
</Field>
</FieldGroup>
</Modal.Content>
<Modal.Footer>
<Modal.FooterControllers>
<Button onClick={onClose}>{t('Cancel')}</Button>
<Button type='submit' primary disabled={!isDirty || createDiscussionMutation.isLoading}>
<Button type='submit' primary disabled={!isDirty || isSubmitting || isValidating}>
{t('Create')}
</Button>
</Modal.FooterControllers>

@ -1,38 +1,42 @@
import { Skeleton, TextInput, Callout } from '@rocket.chat/fuselage';
import { useTranslation } from '@rocket.chat/ui-contexts';
import { useTranslation, useEndpoint } from '@rocket.chat/ui-contexts';
import { useQuery } from '@tanstack/react-query';
import type { ReactElement } from 'react';
import React, { useMemo } from 'react';
import { AsyncStatePhase } from '../../hooks/useAsyncState';
import { useEndpointData } from '../../hooks/useEndpointData';
import { roomCoordinator } from '../../lib/rooms/roomCoordinator';
const DefaultParentRoomField = ({ defaultParentRoom }: { defaultParentRoom: string }): ReactElement => {
const t = useTranslation();
const { value, phase } = useEndpointData('/v1/rooms.info', {
params: useMemo(
() => ({
roomId: defaultParentRoom,
}),
[defaultParentRoom],
),
const query = useMemo(
() => ({
roomId: defaultParentRoom,
}),
[defaultParentRoom],
);
const roomsInfoEndpoint = useEndpoint('GET', '/v1/rooms.info');
const { data, isLoading, isError } = useQuery(['defaultParentRoomInfo', query], async () => roomsInfoEndpoint(query), {
refetchOnWindowFocus: false,
});
if (phase === AsyncStatePhase.LOADING) {
if (isLoading) {
return <Skeleton width='full' />;
}
if (!value || !value.room) {
if (!data?.room || isError) {
return <Callout type='danger'>{t('Error')}</Callout>;
}
return (
<TextInput
defaultValue={roomCoordinator.getRoomName(value.room.t, {
_id: value.room._id,
fname: value.room.fname,
name: value.room.name,
prid: value.room.prid,
defaultValue={roomCoordinator.getRoomName(data.room.t, {
_id: data.room._id,
fname: data.room.fname,
name: data.room.name,
prid: data.room.prid,
})}
disabled
/>

@ -1680,7 +1680,7 @@
"Discussion_target_channel": "Parent channel or group",
"Discussion_target_channel_description": "Select a channel which is related to what you want to ask",
"Discussion_target_channel_prefix": "You are creating a discussion in",
"Discussion_title": "Create a new discussion",
"Discussion_title": "Create discussion",
"Discussions_unavailable_for_federation": "Discussions are unavailable for Federated rooms",
"discussion-created": "{{message}}",
"Discussions": "Discussions",

Loading…
Cancel
Save