Skip to content

Commit

Permalink
Improve UX and accessiblity of the modal dialogs. #303
Browse files Browse the repository at this point in the history
- Add "Select All"/"Clear Selection" buttons to the multiple items dialog
- Add modal footers and use scroll container
- Improve layout of search results modal
- Improve accessiblity in all dialogs
- Fix few missing i18n strings
  • Loading branch information
tnajdek committed Oct 23, 2024
1 parent 4ee16dc commit 0e0b088
Show file tree
Hide file tree
Showing 10 changed files with 235 additions and 100 deletions.
2 changes: 1 addition & 1 deletion modules/web-common
72 changes: 40 additions & 32 deletions src/js/components/confirm-add-dialog.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { memo, useCallback, useState } from 'react';
import { memo, useCallback, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { FormattedMessage, useIntl } from 'react-intl';
import { Button, Icon, Tabs, Tab, TabPane } from 'web-common/components';
import { isTriggerEvent } from 'web-common/utils';

Expand All @@ -12,6 +12,8 @@ const ConfirmAddDialog = props => {
const isReady = itemToConfirm && activeDialog === 'CONFIRM_ADD_DIALOG';

const [activeTab, setActiveTab] = useState('current-style-content');
const minModalBodyHeight = useRef(null); // To avoid modal dialog shrinking when switching tabs, store the height of the body content when it's first rendered
const intl = useIntl();

const currentStyleHtml = useCallback(() => {
if (!itemToConfirm?.inCurrentStyle) {
Expand Down Expand Up @@ -42,27 +44,34 @@ const ConfirmAddDialog = props => {
setActiveTab(ev.getAttribute('aria-controls'));
}, []);

const title = intl.formatMessage({ id: 'zbib.dialog.confirmAddThisCitation', defaultMessage: 'Add this citation to your bibliography?' });

return isReady ? (
<Modal
isOpen={ activeDialog === 'CONFIRM_ADD_DIALOG' }
contentLabel="Confirm Add Citation"
className="confirm-add-dialog modal modal-lg"
contentLabel={title}
className="confirm-add-dialog modal modal-lg modal-with-footer"
onRequestClose={ onConfirmAddCancel }
>
<div className="modal-content" tabIndex={ -1 }>
<div className="modal-header">
<h4 className="modal-title text-truncate">
<FormattedMessage id="zbib.dialog.confirmAddThisCitation" defaultMessage="Add this citation to your bibliography?" />
{ title }
</h4>
<Button
title={intl.formatMessage({ id: 'zbib.modal.closeDialog', defaultMessage: 'Close Dialog' })}
icon
className="close"
onClick={ onConfirmAddCancel }
>
<Icon type={ '24/remove' } width="24" height="24" />
<Icon type={ '24/remove' } role="presentation" width="24" height="24" />
</Button>
</div>
<div className="modal-body">
<div
ref={ ref => minModalBodyHeight.current = ref?.offsetHeight }
style={ minModalBodyHeight.current ? { minHeight: minModalBodyHeight.current } : {} }
className="modal-body"
>

{ itemToConfirm.inIncomingStyle ? (
<>
Expand Down Expand Up @@ -100,32 +109,31 @@ const ConfirmAddDialog = props => {
<div dangerouslySetInnerHTML={{ __html: currentStyleHtml() }} />
</div>
) }

<div className="more-items-action">
<Button
autoFocus
className="btn-outline-secondary btn-min-width"
onClick={ handleConfirm }
onKeyDown = { handleConfirm }
>
{ activeTab === "incoming-style-content" ? (
(incomingStyle.titleShort ?? incomingStyle.title).length > 40 ? (
<FormattedMessage
id="zbib.addAndSwitchShort"
defaultMessage={"Add and switch to this style"}
/>
) : (
<FormattedMessage
id="zbib.addAndSwitch"
defaultMessage={ "Add and switch to \"{style}\"" }
values={ { style: incomingStyle.titleShort ?? incomingStyle.title } }
/>
)
</div>
<div className="modal-footer">
<Button
autoFocus
className="btn-outline-secondary btn-min-width"
onClick={handleConfirm}
onKeyDown={handleConfirm}
>
{activeTab === "incoming-style-content" ? (
(incomingStyle.titleShort ?? incomingStyle.title).length > 40 ? (
<FormattedMessage
id="zbib.addAndSwitchShort"
defaultMessage={"Add and switch to this style"}
/>
) : (
<FormattedMessage id="zbib.general.add" defaultMessage="Add" />
) }
</Button>
</div>
<FormattedMessage
id="zbib.addAndSwitch"
defaultMessage={"Add and switch to \"{style}\""}
values={{ style: incomingStyle.titleShort ?? incomingStyle.title }}
/>
)
) : (
<FormattedMessage id="zbib.general.add" defaultMessage="Add" />
)}
</Button>
</div>
</div>
</Modal>
Expand Down
65 changes: 38 additions & 27 deletions src/js/components/multiple-choice-dialog.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import PropTypes from 'prop-types';
import { useCallback, useId, memo } from 'react';
import { FormattedMessage } from 'react-intl';
import cx from 'classnames';
import { useCallback, useId, memo, useRef } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import { Button, Icon, Spinner } from 'web-common/components';
import { isTriggerEvent } from 'web-common/utils';

Expand Down Expand Up @@ -40,18 +41,16 @@ const ChoiceItem = memo(({ item, onItemSelect }) => {
onClick={ onItemSelect }
tabIndex={ 0 }
>
{ badge && <span className="badge badge-light d-sm-none">{ badge }</span> }
{/* { badge && <span className="badge badge-light d-sm-none">{ badge }</span> } */}
<h5 id={ id } className="title">
<span className="title-container">
{ title }
</span>
{ badge && <span className="badge badge-light d-xs-none d-sm-inline-block">{ badge }</span> }
</h5>
{ item.value.description && (
<p className="description">
{item.value.description}
</p>
)}
<p className="description">
{item.value.description}
</p>
</li>
);
});
Expand All @@ -68,6 +67,9 @@ const getItem = (ev, items) => items.find(item => item.signature === ev.currentT
const MultipleChoiceDialog = props => {
const { activeDialog, isTranslatingMore, moreItemsLink, multipleChoiceItems,
onMultipleChoiceCancel, onMultipleChoiceMore, onMultipleChoiceSelect } = props;
const persistBtnWidth = useRef(null); // To avoid button size change when spinner is shown, store the button width in a ref when it is firts rendered
const intl = useIntl();
const useDescriptionColumn = !!multipleChoiceItems?.some(item => item.value.description);

const handleItemSelect = useCallback(ev => {
if(isTriggerEvent(ev)) {
Expand All @@ -76,30 +78,33 @@ const MultipleChoiceDialog = props => {
}
}, [multipleChoiceItems, onMultipleChoiceSelect]);

const title = intl.formatMessage({ id: 'zbib.multipleChoice.prompt', defaultMessage: 'Please select a citation from the list' });

return (multipleChoiceItems && activeDialog === 'MULTIPLE_CHOICE_DIALOG') ? (
<Modal
isOpen={ activeDialog === 'MULTIPLE_CHOICE_DIALOG' }
contentLabel="Select a Search Result to Add"
className="multiple-choice-dialog modal modal-lg"
contentLabel={ title }
className={cx("multiple-choice-dialog modal modal-lg", { 'modal-with-footer': moreItemsLink })}
onRequestClose={ onMultipleChoiceCancel }
>
<div className="modal-content" tabIndex={ -1 }>
<div className="modal-header">
<h4 className="modal-title text-truncate">
<FormattedMessage id="zbib.multipleChoice.prompt" defaultMessage="Please select a citation from the list" />
{ title }
</h4>
<Button
title={intl.formatMessage({ id: 'zbib.modal.closeDialog', defaultMessage: 'Close Dialog' })}
icon
className="close"
onClick={ onMultipleChoiceCancel }
>
<Icon type={ '24/remove' } width="24" height="24" />
<Icon type={'24/remove'} role="presentation" width="24" height="24" />
</Button>
</div>
<div className="modal-body">
<ul
aria-label="Results"
className="results"
className={ cx("results", { 'single-column': !useDescriptionColumn }) }
>
{ multipleChoiceItems.map(item => (
<ChoiceItem
Expand All @@ -109,21 +114,27 @@ const MultipleChoiceDialog = props => {
/>
)) }
</ul>
{ moreItemsLink && (
<div className="more-items-action">
{ isTranslatingMore ? <Spinner /> : (
moreItemsLink !== null && (
<Button
className="btn-outline-secondary btn-min-width"
onClick={ onMultipleChoiceMore }
>
<FormattedMessage id="zbib.multipleChoice.more" defaultMessage="More…" />
</Button>
)
) }
</div>
) }
</div>
{moreItemsLink && (
<div className="modal-footer">
<div className="selection-count">
<FormattedMessage
id="zbib.multipleChoice.selectionCount"
defaultMessage="{count, plural, one {# item found} other {# items found}}"
values={{ count: multipleChoiceItems.length }}
/>
</div>
<Button
ref={ ref => persistBtnWidth.current = ref?.offsetWidth }
style={ isTranslatingMore ? { width: persistBtnWidth.current } : {} }
disabled={ isTranslatingMore }
className="btn-outline-secondary btn-min-width btn-flex"
onClick={onMultipleChoiceMore}
>
{ isTranslatingMore ? <Spinner /> : <FormattedMessage id="zbib.multipleChoice.more" defaultMessage="More…" /> }
</Button>
</div>
)}
</div>
</Modal>
) : null;
Expand Down
69 changes: 50 additions & 19 deletions src/js/components/multiple-items-dialog.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import PropTypes from 'prop-types';
import { memo, useEffect, useMemo, useCallback, useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { FormattedMessage, useIntl } from 'react-intl';
import { Button, Icon } from 'web-common/components';
import { isTriggerEvent } from 'web-common/utils';
import { usePrevious } from 'web-common/hooks';
Expand All @@ -12,6 +12,7 @@ const MultipleItemsDialog = props => {
const { activeDialog, multipleItems, onMultipleItemsSelect, onMultipleItemsCancel, } = props;
const [selectedItems, setSelectedItems] = useState([]);
const prevActiveDialog = usePrevious(activeDialog);
const intl = useIntl();
const isOpen = multipleItems && activeDialog === 'MULTIPLE_ITEMS_DIALOG';

const bibliographyRendered = useMemo(() => {
Expand Down Expand Up @@ -58,30 +59,41 @@ const MultipleItemsDialog = props => {
onMultipleItemsCancel();
}, [onMultipleItemsCancel])

const handleSelectAll = useCallback(() => {
setSelectedItems(multipleItems.bibliographyItems.map(item => item.id));
}, [multipleItems]);

const handleClearSelection = useCallback(() => {
setSelectedItems([]);
}, []);

useEffect(() => {
if(prevActiveDialog !== activeDialog && activeDialog === 'MULTIPLE_ITEMS_DIALOG') {
setSelectedItems([]);
}
}, [activeDialog, prevActiveDialog]);

const title = intl.formatMessage({ id: 'zbib.multipleItems.prompt', defaultMessage: 'Please select citations from the list' });

return (
<Modal
isOpen={ !!isOpen }
contentLabel="Select Entries to Add"
className="multiple-choice-dialog modal modal-lg"
contentLabel={ title }
className="multiple-items-dialog modal modal-lg modal-with-footer"
onRequestClose={ handleCancel }
>
<div className="modal-content" tabIndex={ -1 }>
<div className="modal-header">
<h4 className="modal-title text-truncate">
<FormattedMessage id="zbib.multipleItems.prompt" defaultMessage="Please select a citation from the list" />
{ title }
</h4>
<Button
title={ intl.formatMessage({ id: 'zbib.modal.closeDialog', defaultMessage: 'Close Dialog' }) }
icon
className="close"
onClick={ handleCancel }
>
<Icon type={ '24/remove' } width="24" height="24" />
<Icon type={ '24/remove' } role="presentation" width="24" height="24" />
</Button>
</div>
<div className="modal-body">
Expand All @@ -106,26 +118,45 @@ const MultipleItemsDialog = props => {
dangerouslySetInnerHTML={
{ __html: bibliographyRenderedNodes[index]?.innerHTML || item.value }
} />
<input
checked={ selectedItems.includes(item.id) }
className="checkbox"
onChange={ handleItemSelectionChange }
tabIndex={ -1 }
type="checkbox"
/>
<div>
<input
aria-labelledby={`citation-${item.id}`}
checked={ selectedItems.includes(item.id) }
className="checkbox"
onChange={ handleItemSelectionChange }
tabIndex={ -1 }
type="checkbox"
/>
</div>
</li>
))
}
</ul>
<div className="more-items-action">
<Button
disabled={ selectedItems.length === 0 }
className="btn-outline-secondary btn-min-width"
onClick={ handleAddSelected }
>
<FormattedMessage id="zbib.multipleItems.confirm" defaultMessage="Add Selected" />
</div>
<div className="modal-footer">
<div className="selection-count">
<Button className="btn btn-link" onClick={ handleSelectAll }>
<FormattedMessage id="zbib.multipleItems.selectAll" defaultMessage="Select All" />
</Button>
<Button className="btn btn-link" onClick={ handleClearSelection }>
<FormattedMessage id="zbib.multipleItems.clearSelection" defaultMessage="Clear Selection" />
</Button>
</div>
<Button
disabled={selectedItems.length === 0}
className="btn-outline-secondary btn-min-width"
onClick={handleAddSelected}
>
{ selectedItems.length > 0 ? (
<FormattedMessage
id="zbib.multipleItems.confirmWithCount"
defaultMessage="Add {count, plural, one {# Item} other {# Items}}"
values={{ count: selectedItems.length }}
/>
) : (
<FormattedMessage id="zbib.multipleItems.confirm" defaultMessage="Add Selected" />
) }
</Button>
</div>
</div>
</Modal>
Expand Down
6 changes: 4 additions & 2 deletions src/js/components/style-installer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,18 +171,20 @@ const StyleInstaller = props => {
}
}, []);

const title = intl.formatMessage({ id: 'zbib.styleInstaller.title', defaultMessage: 'Add a Citation Style' });

return (
<Modal
isOpen={ isOpen }
contentLabel="Citation Style Picker"
contentLabel={ title }
className={ cx('style-installer', 'modal', 'modal-lg', { loading: !state.isReady }) }
onRequestClose={ handleCancel }
>
{ state.isReady ? (
<div className="modal-content" tabIndex={ -1 }>
<div className="modal-header">
<h4 className="modal-title text-truncate">
<FormattedMessage id="zbib.styleInstaller.title" defaultMessage="Add a Citation Style" />
{ title }
</h4>
<Button
icon
Expand Down
Loading

0 comments on commit 0e0b088

Please sign in to comment.