Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add keyboard shortcuts to modals #2977

Merged
merged 10 commits into from
Mar 28, 2024
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
"emoji-mart": "^5.5.2",
"filesize": "3.6.1",
"firstline": "1.2.1",
"focus-trap-react": "^10.2.3",
"fs-extra": "9.0.0",
"glob": "7.1.2",
"image-type": "^4.1.0",
Expand Down
118 changes: 62 additions & 56 deletions ts/components/SessionWrapperModal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useRef } from 'react';
import classNames from 'classnames';
import FocusTrap from 'focus-trap-react';
import React, { useRef } from 'react';
import useKey from 'react-use/lib/useKey';

import { SessionIconButton } from './icon';
Expand Down Expand Up @@ -62,69 +63,74 @@ export const SessionWrapperModal = (props: SessionWrapperModalType) => {
}
};

const fallbackFocusId = 'session-wrapper-modal';

return (
<div
className={classNames('loki-dialog modal', additionalClassName || null)}
onClick={handleClick}
role="dialog"
>
<div className="session-confirm-wrapper">
<div ref={modalRef} className="session-modal">
{showHeader ? (
<div className={classNames('session-modal__header', headerReverse && 'reverse')}>
<div className="session-modal__header__close">
{showExitIcon ? (
<SessionIconButton
iconType="exit"
iconSize="small"
onClick={props.onClose}
dataTestId="modal-close-button"
/>
) : null}
<FocusTrap focusTrapOptions={{ fallbackFocus: `#${fallbackFocusId}`, allowOutsideClick: true }}>
<div
className={classNames('loki-dialog modal', additionalClassName || null)}
onClick={handleClick}
role="dialog"
id={fallbackFocusId}
>
<div className="session-confirm-wrapper">
<div ref={modalRef} className="session-modal">
{showHeader ? (
<div className={classNames('session-modal__header', headerReverse && 'reverse')}>
<div className="session-modal__header__close">
{showExitIcon ? (
<SessionIconButton
iconType="exit"
iconSize="small"
onClick={props.onClose}
dataTestId="modal-close-button"
/>
) : null}
</div>
<div className="session-modal__header__title">{title}</div>
<div className="session-modal__header__icons">
{headerIconButtons
? headerIconButtons.map((iconItem: any) => {
return (
<SessionIconButton
key={iconItem.iconType}
iconType={iconItem.iconType}
iconSize={'large'}
iconRotation={iconItem.iconRotation}
onClick={iconItem.onClick}
/>
);
})
: null}
</div>
</div>
<div className="session-modal__header__title">{title}</div>
<div className="session-modal__header__icons">
{headerIconButtons
? headerIconButtons.map((iconItem: any) => {
return (
<SessionIconButton
key={iconItem.iconType}
iconType={iconItem.iconType}
iconSize={'large'}
iconRotation={iconItem.iconRotation}
onClick={iconItem.onClick}
/>
);
})
: null}
</div>
</div>
) : null}
) : null}

<div className="session-modal__body">
<div className="session-modal__centered">
{props.children}
<div className="session-modal__body">
<div className="session-modal__centered">
{props.children}

<div className="session-modal__button-group">
{onConfirm ? (
<SessionButton buttonType={SessionButtonType.Simple} onClick={props.onConfirm}>
{confirmText || window.i18n('ok')}
</SessionButton>
) : null}
{onClose && showClose ? (
<SessionButton
buttonType={SessionButtonType.Simple}
buttonColor={SessionButtonColor.Danger}
onClick={props.onClose}
>
{cancelText || window.i18n('close')}
</SessionButton>
) : null}
<div className="session-modal__button-group">
{onConfirm ? (
<SessionButton buttonType={SessionButtonType.Simple} onClick={props.onConfirm}>
{confirmText || window.i18n('ok')}
</SessionButton>
) : null}
{onClose && showClose ? (
<SessionButton
buttonType={SessionButtonType.Simple}
buttonColor={SessionButtonColor.Danger}
onClick={props.onClose}
>
{cancelText || window.i18n('close')}
</SessionButton>
) : null}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</FocusTrap>
);
};
4 changes: 2 additions & 2 deletions ts/components/basic/SessionButton.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { ReactNode } from 'react';
import classNames from 'classnames';
import React, { ReactNode } from 'react';
import styled from 'styled-components';

export enum SessionButtonType {
Expand Down Expand Up @@ -28,7 +28,7 @@ export enum SessionButtonColor {
None = 'transparent',
}

const StyledButton = styled.div<{
const StyledButton = styled.button<{
color: string | undefined;
Bilb marked this conversation as resolved.
Show resolved Hide resolved
buttonType: SessionButtonType;
buttonShape: SessionButtonShape;
Expand Down
77 changes: 46 additions & 31 deletions ts/components/basic/SessionRadio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@ import React, { ChangeEvent } from 'react';
import styled, { CSSProperties } from 'styled-components';
import { Flex } from './Flex';

const StyledButton = styled.button<{ disabled: boolean }>`
cursor: ${props => (props.disabled ? 'not-allowed' : 'pointer')};
min-height: 30px;
`;

const StyledInput = styled.input<{
filledSize: number;
outlineOffset: number;
selectedColor?: string;
}>`
opacity: 0;
position: absolute;
cursor: ${props => (props.disabled ? 'not-allowed' : 'pointer')};
width: ${props => props.filledSize + props.outlineOffset}px;
height: ${props => props.filledSize + props.outlineOffset}px;

Expand Down Expand Up @@ -71,7 +75,7 @@ export const SessionRadio = (props: SessionRadioProps) => {
style,
} = props;

const clickHandler = (e: ChangeEvent<any>) => {
const clickHandler = (e: React.SyntheticEvent<any>) => {
if (!disabled && onClick) {
// let something else catch the event if our click handler is not set
e.stopPropagation();
Expand All @@ -83,37 +87,48 @@ export const SessionRadio = (props: SessionRadioProps) => {
const outlineOffset = 2;

return (
<Flex
container={true}
flexDirection={radioPosition === 'left' ? 'row' : 'row-reverse'}
justifyContent={radioPosition === 'left' ? 'flex-start' : 'flex-end'}
style={{ ...style, position: 'relative' }}
<StyledButton
Bilb marked this conversation as resolved.
Show resolved Hide resolved
onKeyDown={e => {
if (e.code === 'Space') {
clickHandler(e);
}
}}
onClick={clickHandler}
disabled={disabled}
>
<StyledInput
type="radio"
name={inputName || ''}
value={value}
aria-checked={active}
checked={active}
onChange={clickHandler}
filledSize={filledSize * 2}
outlineOffset={outlineOffset}
disabled={disabled}
data-testid={`input-${value.replaceAll(' ', '-')}`} // data-testid cannot have spaces
/>
<StyledLabel
role="button"
onClick={clickHandler}
filledSize={filledSize - 1}
outlineOffset={outlineOffset}
beforeMargins={beforeMargins}
aria-label={label}
disabled={disabled}
data-testid={`label-${value}`}
<Flex
container={true}
flexDirection={radioPosition === 'left' ? 'row' : 'row-reverse'}
justifyContent={radioPosition === 'left' ? 'flex-start' : 'flex-end'}
style={{ ...style, position: 'relative' }}
>
{label}
</StyledLabel>
</Flex>
<StyledInput
type="radio"
name={inputName || ''}
value={value}
aria-checked={active}
checked={active}
onChange={clickHandler}
tabIndex={-1} // clickHandler is on the parent button, so we need to skip this input while pressing Tab
filledSize={filledSize * 2}
outlineOffset={outlineOffset}
disabled={disabled}
data-testid={`input-${value.replaceAll(' ', '-')}`} // data-testid cannot have spaces
/>
<StyledLabel
role="button"
onClick={clickHandler}
filledSize={filledSize - 1}
outlineOffset={outlineOffset}
beforeMargins={beforeMargins}
aria-label={label}
disabled={disabled}
data-testid={`label-${value}`}
>
{label}
</StyledLabel>
</Flex>
</StyledButton>
);
};

Expand Down
1 change: 1 addition & 0 deletions ts/components/basic/YourSessionIDPill.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const StyledYourSessionIDSelectable = styled.p`
font-weight: 300;
font-size: var(--font-size-sm);
color: var(--text-primary-color);
flex-shrink: 0;
`;

export const YourSessionIDSelectable = () => {
Expand Down
2 changes: 1 addition & 1 deletion ts/components/conversation/SessionConversation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ import { SessionSpinner } from '../basic/SessionSpinner';
import { RightPanel, StyledRightPanelContainer } from './right-panel/RightPanel';

const DEFAULT_JPEG_QUALITY = 0.85;

interface State {
isDraggingFile: boolean;
}
Expand Down Expand Up @@ -354,6 +353,7 @@ export class SessionConversation extends React.Component<Props, State> {
}
break;
default:
break;
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions ts/components/conversation/composition/CompositionButtons.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import styled from 'styled-components';
import { SessionIconButton } from '../../icon';
import { Noop } from '../../../types/Util';
import { SessionIconButton } from '../../icon';

const StyledChatButtonContainer = styled.div`
.session-icon-button {
Expand Down Expand Up @@ -50,7 +50,7 @@ export const StartRecordingButton = (props: { onClick: Noop }) => {
};

// eslint-disable-next-line react/display-name
export const ToggleEmojiButton = React.forwardRef<HTMLDivElement, { onClick: Noop }>(
export const ToggleEmojiButton = React.forwardRef<HTMLButtonElement, { onClick: Noop }>(
yougotwill marked this conversation as resolved.
Show resolved Hide resolved
(props, ref) => {
return (
<StyledChatButtonContainer>
Expand Down
Loading
Loading