Skip to content

Commit

Permalink
FEATURE: Improvements for the Node Creation Dialog (#3132)
Browse files Browse the repository at this point in the history
* FEATURE: Improvements to node type insertion screen for keyboard navigation, autofocusing filter input and node type selection, see #3031

* BUGFIX: Fix code style issues

* TASK: More prominent filter input position and styling

* TASK: Fix modal dialogs position from the top to prevent jumping if content size changes

* BUGFIX: Use valid icon size

* BUGFIX: reduce maximum amount of nodetypes

* TASK: make focused nodetype selection more robust

Co-authored-by: Daniel Kestler <>
Co-authored-by: Markus Günther <[email protected]>
  • Loading branch information
danielkestler and markusguenther authored Nov 16, 2022
1 parent fac0495 commit 56206c5
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 29 deletions.
123 changes: 113 additions & 10 deletions packages/neos-ui/src/Containers/Modals/SelectNodeType/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import {$get} from 'plow-js';
import isEqual from 'lodash.isequal';
import escaperegexp from 'lodash.escaperegexp';

import {neos} from '@neos-project/neos-ui-decorators';
import {actions, selectors} from '@neos-project/neos-ui-redux-store';
Expand Down Expand Up @@ -46,6 +47,7 @@ const allowedSiblingsOrChildrenChanged = (previousProps, nextProps) => (
);

@neos(globalRegistry => ({
i18nRegistry: globalRegistry.get('i18n'),
nodeTypesRegistry: globalRegistry.get('@neos-project/neos-ui-contentrepository')
}))
@connect((state, {nodeTypesRegistry}) => {
Expand Down Expand Up @@ -85,11 +87,15 @@ export default class SelectNodeType extends PureComponent {
allowedSiblingNodeTypes: PropTypes.array,
allowedChildNodeTypes: PropTypes.array,
cancel: PropTypes.func.isRequired,
apply: PropTypes.func.isRequired
apply: PropTypes.func.isRequired,
i18nRegistry: PropTypes.object.isRequired
};

state = {
filterSearchTerm: '',
filteredNodeTypesFlat: [],
filteredNodeTypesGrouped: [],
focusedNodeType: null,
insertMode: calculateInitialMode(
this.props.allowedSiblingNodeTypes,
this.props.allowedChildNodeTypes,
Expand Down Expand Up @@ -182,10 +188,105 @@ export default class SelectNodeType extends PureComponent {
);
}

handleNodeTypeFilterChange = filterSearchTerm => this.setState({filterSearchTerm});
updateFilteredNodes() {
const {i18nRegistry} = this.props;
const {filterSearchTerm} = this.state;

const allowedNodeTypes = this.getAllowedNodeTypesByCurrentInsertMode();

const filteredNodeTypesFlat = [];
const filteredNodeTypesGrouped = [];
allowedNodeTypes.map(value => {
const filteredNodeTypes = (value.nodeTypes || [])
.filter(nodeType => {
if (!filterSearchTerm || filterSearchTerm === '') {
return true;
}
const label = i18nRegistry.translate(nodeType.label, nodeType.label);
if (label.toLowerCase().search(escaperegexp(filterSearchTerm.toLowerCase())) !== -1) {
return true;
}
return false;
});

if (filteredNodeTypes.length > 0) {
filteredNodeTypesFlat.push(...filteredNodeTypes);
filteredNodeTypesGrouped.push(value);
}

return true;
});

const getFocusedNodeTypeName = () => {
const [firstNodeTypeItem] = filteredNodeTypesFlat;
if (filterSearchTerm !== '' && firstNodeTypeItem && Object.keys(firstNodeTypeItem).includes('name')) {
return firstNodeTypeItem.name;
}

return null;
};
const focusedNodeType = getFocusedNodeTypeName();

this.setState({
filteredNodeTypesFlat,
filteredNodeTypesGrouped,
focusedNodeType
});
}

handleNodeTypeFilterChange = filterSearchTerm => {
this.setState({filterSearchTerm});
};

handleEnterKey = () => {
const {apply} = this.props;
const {insertMode, focusedNodeType} = this.state;

if (focusedNodeType) {
apply(insertMode, focusedNodeType);

this.setState({
filterSearchTerm: ''
});
this.handleCloseHelpMessage();
}
}

componentDidUpdate(prevProps, prevState) {
if (this.state.filterSearchTerm !== prevState.filterSearchTerm) {
this.updateFilteredNodes();
}
if (this.state.insertMode !== prevState.insertMode) {
this.updateFilteredNodes();
}
}

handleKeyDown = event => {
if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
event.preventDefault();

const {filteredNodeTypesFlat, focusedNodeType} = this.state;
const index = filteredNodeTypesFlat.findIndex(element => element.name === focusedNodeType);

if (event.key === 'ArrowUp') {
if (index - 1 >= 0) {
this.setState({
focusedNodeType: filteredNodeTypesFlat[index - 1].name
});
}
}
if (event.key === 'ArrowDown') {
if (index + 1 <= filteredNodeTypesFlat.length - 1) {
this.setState({
focusedNodeType: filteredNodeTypesFlat[index + 1].name
});
}
}
}
}

render() {
const {insertMode, filterSearchTerm} = this.state;
const {insertMode, filterSearchTerm, filteredNodeTypesGrouped, focusedNodeType} = this.state;
const {isOpen, allowedSiblingNodeTypes, allowedChildNodeTypes} = this.props;

if (!isOpen) {
Expand All @@ -201,23 +302,25 @@ export default class SelectNodeType extends PureComponent {
style="wide"
id="neos-SelectNodeTypeDialog"
>
<div className={style.nodeTypeDialogHeader} key="nodeTypeDialogHeader">
<div onKeyDown={this.handleKeyDown} role="searchbox" className={style.nodeTypeDialogHeader} key="nodeTypeDialogHeader">
<NodeTypeFilter
filterSearchTerm={filterSearchTerm}
onChange={this.handleNodeTypeFilterChange}
onEnterKey={this.handleEnterKey}
/>
<InsertModeSelector
mode={insertMode}
onSelect={this.handleModeChange}
enableAlongsideModes={Boolean(allowedSiblingNodeTypes.length)}
enableIntoMode={Boolean(allowedChildNodeTypes.length)}
/>
<NodeTypeFilter
filterSearchTerm={filterSearchTerm}
onChange={this.handleNodeTypeFilterChange}
/>
</div>
{this.getAllowedNodeTypesByCurrentInsertMode().map((group, key) => (
{filteredNodeTypesGrouped.map((group, key) => (
<div key={key}>
<NodeTypeGroupPanel
group={group}
filterSearchTerm={this.state.filterSearchTerm}
focusedNodeType={focusedNodeType}
filterSearchTerm={filterSearchTerm}
onSelect={this.handleApply}
showHelpMessageFor ={this.state.showHelpMessageFor}
activeHelpMessageGroupPanel ={this.state.activeHelpMessageGroupPanel}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Icon from '@neos-project/react-ui-components/src/Icon/';
import {neos} from '@neos-project/neos-ui-decorators';
import style from './style.css';

const NodeTypeFilter = ({onChange, filterSearchTerm, i18nRegistry}) => {
const NodeTypeFilter = ({onChange, onEnterKey, filterSearchTerm, i18nRegistry}) => {
const handleResetFilter = () => {
onChange('');
};
Expand All @@ -15,19 +15,31 @@ const NodeTypeFilter = ({onChange, filterSearchTerm, i18nRegistry}) => {
onChange(filterSearchTerm);
};

const handleEnterKey = () => {
onEnterKey();
};

const label = i18nRegistry.translate('filter', 'Filter', {}, 'Neos.Neos', 'Main');

return (
<div className={style.nodeTypeDialogHeader__filter}>
{filterSearchTerm ? (
<IconButton icon="times" onClick={handleResetFilter}/>
) : (
<Icon icon="filter" padded="right"/>
) }
<div className={style.nodeTypeDialogHeader__filterIconSearch}>
<Icon icon="search" size="1x"/>
</div>

{filterSearchTerm && (
<div className={style.nodeTypeDialogHeader__filterIconReset}>
<IconButton icon="times" style="brand" onClick={handleResetFilter}/>
</div>
)}

<TextInput
className={style.nodeTypeDialogHeader__filterInput}
containerClassName={style.nodeTypeDialogHeader__filterInputContainer}
value={filterSearchTerm}
onChange={handleValueChange}
onEnterKey={handleEnterKey}
setFocus={true}
placeholder={label}
/>
</div>
Expand All @@ -36,6 +48,7 @@ const NodeTypeFilter = ({onChange, filterSearchTerm, i18nRegistry}) => {

NodeTypeFilter.propTypes = {
onChange: PropTypes.func.isRequired,
onEnterKey: PropTypes.func.isRequired,
filterSearchTerm: PropTypes.string,
i18nRegistry: PropTypes.object.isRequired
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class NodeTypeGroupPanel extends PureComponent {
toggleNodeTypeGroup: PropTypes.func.isRequired,
toggledGroups: PropTypes.array.isRequired,
filterSearchTerm: PropTypes.string,
focusedNodeType: PropTypes.string,

group: PropTypes.shape({
name: PropTypes.string.isRequired,
Expand Down Expand Up @@ -109,6 +110,7 @@ class NodeTypeGroupPanel extends PureComponent {
render() {
const {
group,
focusedNodeType,
toggledGroups,
onSelect,
filterSearchTerm,
Expand Down Expand Up @@ -142,13 +144,7 @@ class NodeTypeGroupPanel extends PureComponent {
<I18n className={style.groupTitle} fallback={label} id={label}/>
</ToggablePanel.Header>
<ToggablePanel.Contents className={style.groupContents}>
{filteredNodeTypes.length > 0 ? (
filteredNodeTypes.map((nodeType, key) => <NodeTypeItem nodeType={nodeType} key={key} onSelect={onSelect} onHelpMessage={onHelpMessage} groupName={group.name} />)
) : (
<div className={style.noMatchesFound}>
<Icon icon="ban" padded="right"/>{i18nRegistry.translate('noMatchesFound')}.
</div>
)}
{filteredNodeTypes.map((nodeType, key) => <NodeTypeItem nodeType={nodeType} focused={focusedNodeType === nodeType.name} key={key} onSelect={onSelect} onHelpMessage={onHelpMessage} groupName={group.name} />)}
{showHelpMessage ? this.renderHelpMessage() : null}
</ToggablePanel.Contents>
</ToggablePanel>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import {$transform, $get} from 'plow-js';
import mergeClassNames from 'classnames';

import {neos} from '@neos-project/neos-ui-decorators';
import Icon from '@neos-project/react-ui-components/src/Icon/';
Expand All @@ -24,6 +25,7 @@ import style from './style.css';
})
class NodeTypeItem extends PureComponent {
static propTypes = {
focused: PropTypes.bool,
onSelect: PropTypes.func.isRequired,
onHelpMessage: PropTypes.func.isRequired,

Expand Down Expand Up @@ -57,15 +59,18 @@ class NodeTypeItem extends PureComponent {
const usePreviewIcon = ('previewIcon' in ui);
const icon = $get(usePreviewIcon ? 'previewIcon' : 'icon', ui);
const size = this.getIconSize();
const {onHelpMessage, groupName, i18nRegistry} = this.props;
const {onHelpMessage, groupName, i18nRegistry, focused} = this.props;
const helpMessage = i18nRegistry.translate($get('help.message', ui));

return (
<div className={style.nodeType}>
<Button
hoverStyle="brand"
style="clean"
className={style.nodeType__item}
className={mergeClassNames({
[style.nodeType__item]: true,
[style['nodeType__item--focused']]: focused
})}
onClick={this.handleNodeTypeClick}
title={helpMessage ? helpMessage : ''}
>
Expand Down
31 changes: 30 additions & 1 deletion packages/neos-ui/src/Containers/Modals/SelectNodeType/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@
border-radius: 2px;
position: relative;
}
.nodeType__item--focused {
background: var(--colors-PrimaryBlue);
}
.nodeType__helpIcon {
display: block;
position: absolute;
Expand Down Expand Up @@ -145,10 +148,36 @@
padding-right: var(--spacing-Full);
padding-bottom: var(--spacing-Full);
}

.nodeTypeDialogHeader__filter {
position: relative;
display: flex;
flex: 1;
align-items: center;
margin-right: var(--spacing-Full);
}
.nodeTypeDialogHeader__filterInput {
padding-left: var(--spacing-GoldenUnit);

&:focus {
background: var(--colors-ContrastDark);
color: var(--colors-ContrastBrightest);
}
}
.nodeTypeDialogHeader__filterInputContainer {
width: 100%;
}
.nodeTypeDialogHeader__filterIconSearch {
position: absolute;
top: 10px;
left: var(--spacing-Full);
width: var(--spacing-Full);
}
.nodeTypeDialogHeader__filterIconReset {
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: var(--spacing-GoldenUnit);
}

.nodeTypeFilter__label {
Expand Down
6 changes: 3 additions & 3 deletions packages/react-ui-components/src/Dialog/style.css
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
@keyframes slideDialogContents {
from {
opacity: 0;
transform: translateX(-50%) translateY(-50%) scale(.9);
transform: translateX(-50%) scale(.9);
}
}

Expand All @@ -18,9 +18,9 @@
.dialog__contentsPosition {
composes: reset from './../reset.css';
position: absolute;
top: 50%;
top: 20vh;
left: 50%;
transform: translateX(-50%) translateY(-50%) scale(1);
transform: translateX(-50%) scale(1);
background: var(--colors-ContrastDarker);
box-shadow: 0 20px 40px rgba(0, 0, 0, .4);
border-radius: 0;
Expand Down

0 comments on commit 56206c5

Please sign in to comment.