Extract autocomplete, people-select
authorMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Sat, 27 Oct 2018 18:18:29 +0000 (20:18 +0200)
committerMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Sat, 27 Oct 2018 18:18:29 +0000 (20:18 +0200)
Feature #14365

Arvados-DCO-1.1-Signed-off-by: Michal Klobukowski <michal.klobukowski@contractors.roche.com>

src/components/autocomplete/autocomplete.tsx [new file with mode: 0644]
src/views-components/sharing-dialog/people-select.tsx [new file with mode: 0644]
src/views-components/sharing-dialog/permission-select.tsx
src/views-components/sharing-dialog/sharing-invitation-form-component.tsx

diff --git a/src/components/autocomplete/autocomplete.tsx b/src/components/autocomplete/autocomplete.tsx
new file mode 100644 (file)
index 0000000..e2f3547
--- /dev/null
@@ -0,0 +1,181 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { Input as MuiInput, Chip as MuiChip, Popper as MuiPopper, Paper, FormControl, InputLabel, StyleRulesCallback, withStyles, RootRef, Menu, MenuItem, ListItemText, ListItem, List } from '@material-ui/core';
+import { PopperProps } from '@material-ui/core/Popper';
+import { WithStyles } from '@material-ui/core/styles';
+import { noop } from 'lodash';
+
+export interface AutocompleteProps<Item, Suggestion> {
+    label?: string;
+    value: string;
+    items: Item[];
+    suggestions?: Suggestion[];
+    onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
+    onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
+    onFocus?: (event: React.FocusEvent<HTMLInputElement>) => void;
+    onCreate?: () => void;
+    onDelete?: (item: Item, index: number) => void;
+    onSelect?: (suggestion: Suggestion) => void;
+    renderChipValue?: (item: Item) => string;
+    renderSuggestion?: (suggestion: Suggestion) => React.ReactNode;
+}
+
+export interface AutocompleteState {
+    suggestionsOpen: boolean;
+}
+export class Autocomplete<Value, Suggestion> extends React.Component<AutocompleteProps<Value, Suggestion>, AutocompleteState> {
+
+    state = {
+        suggestionsOpen: false,
+    };
+
+    containerRef = React.createRef<HTMLDivElement>();
+    inputRef = React.createRef<HTMLInputElement>();
+    render() {
+        return (
+            <RootRef rootRef={this.containerRef}>
+                <FormControl fullWidth>
+                    {this.renderLabel()}
+                    {this.renderInput()}
+                    {this.renderSuggestions()}
+                </FormControl>
+            </RootRef>
+        );
+    }
+
+    renderLabel() {
+        const { label } = this.props;
+        return label && <InputLabel>{label}</InputLabel>;
+    }
+
+    renderInput() {
+        return <Input
+            inputRef={this.inputRef}
+            value={this.props.value}
+            startAdornment={this.renderChips()}
+            onFocus={this.handleFocus}
+            onBlur={this.handleBlur}
+            onChange={this.props.onChange}
+            onKeyPress={this.handleKeyPress}
+        />;
+    }
+
+    handleFocus = (event: React.FocusEvent<HTMLInputElement>) => {
+        const { onFocus = noop } = this.props;
+        this.setState({ suggestionsOpen: true });
+        onFocus(event);
+    }
+
+    handleBlur = (event: React.FocusEvent<HTMLInputElement>) => {
+        const { onBlur = noop } = this.props;
+        this.setState({ suggestionsOpen: true });
+        onBlur(event);
+    }
+
+    handleKeyPress = ({ key }: React.KeyboardEvent<HTMLInputElement>) => {
+        const { onCreate = noop } = this.props;
+        if (key === 'Enter') {
+            onCreate();
+        }
+    }
+
+    renderChips() {
+        const { items, onDelete } = this.props;
+        return items.map(
+            (item, index) =>
+                <Chip
+                    label={this.renderChipValue(item)}
+                    key={index}
+                    onDelete={() => onDelete ? onDelete(item, index) : undefined} />
+        );
+    }
+
+    renderChipValue(value: Value) {
+        const { renderChipValue } = this.props;
+        return renderChipValue ? renderChipValue(value) : JSON.stringify(value);
+    }
+
+    renderSuggestions() {
+        const { suggestions } = this.props;
+        return suggestions && suggestions.length > 0
+            ? <Popper
+                open={this.state.suggestionsOpen}
+                anchorEl={this.containerRef.current}>
+                <Paper>
+                    <List dense style={{ width: this.getSuggestionsWidth() }}>
+                        {suggestions.map(
+                            (suggestion, index) =>
+                                <ListItem button key={index} onClick={this.handleSelect(suggestion)}>
+                                    {this.renderSuggestion(suggestion)}
+                                </ListItem>
+                        )}
+                    </List>
+                </Paper>
+            </Popper>
+            : null;
+    }
+
+    handleSelect(suggestion: Suggestion) {
+        return () => {
+            const { onSelect = noop } = this.props;
+            const { current } = this.inputRef;
+            if (current) {
+                current.focus();
+            }
+            onSelect(suggestion);
+        };
+    }
+
+    renderSuggestion(suggestion: Suggestion) {
+        const { renderSuggestion } = this.props;
+        return renderSuggestion
+            ? renderSuggestion(suggestion)
+            : <ListItemText>{JSON.stringify(suggestion)}</ListItemText>;
+    }
+
+    getSuggestionsWidth() {
+        return this.containerRef.current ? this.containerRef.current.offsetWidth : 'auto';
+    }
+}
+
+type ChipClasses = 'root';
+
+const chipStyles: StyleRulesCallback<ChipClasses> = theme => ({
+    root: {
+        marginRight: theme.spacing.unit / 4,
+        height: theme.spacing.unit * 3,
+    }
+});
+
+const Chip = withStyles(chipStyles)(MuiChip);
+
+type PopperClasses = 'root';
+
+const popperStyles: StyleRulesCallback<ChipClasses> = theme => ({
+    root: {
+        zIndex: theme.zIndex.modal,
+    }
+});
+
+const Popper = withStyles(popperStyles)(
+    ({ classes, ...props }: PopperProps & WithStyles<PopperClasses>) =>
+        <MuiPopper {...props} className={classes.root} />
+);
+
+type InputClasses = 'root';
+
+const inputStyles: StyleRulesCallback<InputClasses> = () => ({
+    root: {
+        display: 'flex',
+        flexWrap: 'wrap',
+    },
+    input: {
+        minWidth: '20%',
+        flex: 1,
+    },
+});
+
+const Input = withStyles(inputStyles)(MuiInput);
diff --git a/src/views-components/sharing-dialog/people-select.tsx b/src/views-components/sharing-dialog/people-select.tsx
new file mode 100644 (file)
index 0000000..2e24560
--- /dev/null
@@ -0,0 +1,51 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { Autocomplete } from '~/components/autocomplete/autocomplete';
+
+
+export interface Person {
+    name: string;
+}
+export interface PeopleSelectProps {
+    suggestedPeople: Person[];
+}
+
+export interface PeopleSelectState {
+    value: string;
+    items: Person[];
+    suggestions: string[];
+}
+export class PeopleSelect extends React.Component<PeopleSelectProps, PeopleSelectState> {
+
+    state = {
+        value: '',
+        items: [{ name: 'Michal Klobukowski' }],
+        suggestions: ['Michal Klobukowski', 'Mateusz Ollik']
+    };
+
+    render() {
+        return (
+            <Autocomplete
+                label='Invite people'
+                value={this.state.value}
+                items={this.state.items}
+                suggestions={this.getSuggestions()}
+                renderChipValue={item => item.name}
+                onChange={this.handleChange} />
+        );
+    }
+
+    getSuggestions() {
+        const { value, suggestions } = this.state;
+        return value
+            ? suggestions.filter(suggestion => suggestion.includes(value))
+            : [];
+    }
+
+    handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+        this.setState({ value: event.target.value });
+    }
+}
index 582f3bcff96e09560fd5c1ab5f97b7c7b7a4f40b..a2d029827a409cf245e3eb898f9046fcdc207e12 100644 (file)
@@ -44,15 +44,12 @@ export const PermissionSelect = withStyles(PermissionSelectStyles)(
 const renderPermissionItem = (value: string) =>
     <PermissionItem {...{ value }} />;
 
-type PermissionItemClasses = 'value' | 'icon';
+type PermissionItemClasses = 'value';
 
 const permissionItemStyles: StyleRulesCallback<PermissionItemClasses> = theme => ({
     value: {
         marginLeft: theme.spacing.unit,
     },
-    icon: {
-       margin: `-${theme.spacing.unit / 2}px 0`,
-    }
 });
 
 const PermissionItem = withStyles(permissionItemStyles)(
@@ -60,7 +57,7 @@ const PermissionItem = withStyles(permissionItemStyles)(
         const Icon = getIcon(value);
         return (
             <Grid container alignItems='center'>
-                <Icon className={classes.icon} />
+                <Icon />
                 <span className={classes.value}>
                     {value}
                 </span>
index 9efbb1be715b3ba7b63cde3c9f8bbc94d6ea47a0..f1eb177becbfd8f33ae57378dbc3effa1ffda1dd 100644 (file)
@@ -4,10 +4,12 @@
 
 import * as React from 'react';
 import { Field, WrappedFieldProps } from 'redux-form';
-import { Grid, Input, FormControl, FormHelperText, FormLabel, InputLabel } from '@material-ui/core';
+import { Grid, Input, FormControl, FormHelperText, FormLabel, InputLabel, Chip } from '@material-ui/core';
 import { ChipsInput } from '~/components/chips-input/chips-input';
 import { identity } from 'lodash';
 import { PermissionSelect } from './permission-select';
+import { PeopleSelect } from './people-select';
+import ChipInput from 'material-ui-chip-input';
 
 export default () =>
     <Grid container spacing={8}>
@@ -26,19 +28,7 @@ const InvitedPeopleField = () =>
 
 
 const InvitedPeopleFieldComponent = (props: WrappedFieldProps) =>
-    <FormControl fullWidth>
-        <FormLabel>
-            Invite people
-        </FormLabel>
-        <ChipsInput
-            {...props.input}
-            value={['Test User']}
-            createNewValue={identity}
-            inputComponent={Input} />
-        <FormHelperText>
-            Helper text
-        </FormHelperText>
-    </FormControl>;
+    <PeopleSelect suggestedPeople={[]} />;
 
 const PermissionSelectField = () =>
     <Field