blob: 8cc5f8434b24ee0d25ce5fa33472a0b4dd277840 [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001// Copyright 2021 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import {LitElement, property, internalProperty} from 'lit-element';
6import React from 'react';
7import ReactDOM from 'react-dom';
8import deepEqual from 'deep-equal';
9
10import {AutocompleteChangeDetails, AutocompleteChangeReason}
11 from '@material-ui/core/Autocomplete';
12import {ThemeProvider, createTheme} from '@material-ui/core/styles';
13
14import {connectStore} from 'reducers/base.js';
15import * as projectV0 from 'reducers/projectV0.js';
16import * as userV0 from 'reducers/userV0.js';
17import {userRefsToDisplayNames} from 'shared/convertersV0.js';
18import {arrayDifference} from 'shared/helpers.js';
19
20import {ReactAutocomplete} from 'react/ReactAutocomplete.tsx';
21
22type Vocabulary = 'component' | 'label' | 'member' | 'owner' | 'project' | '';
23
24
25/**
26 * A normal text input enhanced by a panel of suggested options.
27 * `<mr-react-autocomplete>` wraps a React implementation of autocomplete
28 * in a web component, suitable for embedding in a LitElement component
29 * hierarchy. All parents must not use Shadow DOM. The supported autocomplete
30 * option types are defined in type Vocabulary.
31 */
32export class MrReactAutocomplete extends connectStore(LitElement) {
33 // Required properties passed in from the parent element.
34 /** The `<input id>` attribute. Called "label" to avoid name conflicts. */
35 @property() label: string = '';
36 /** The autocomplete option type. See type Vocabulary for the full list. */
37 @property() vocabularyName: Vocabulary = '';
38
39 // Optional properties passed in from the parent element.
40 /** The value (or values, if `multiple === true`). */
41 @property({
42 hasChanged: (oldVal, newVal) => !deepEqual(oldVal, newVal),
43 }) value?: string | string[] = undefined;
44 /** Values that show up as disabled chips. */
45 @property({
46 hasChanged: (oldVal, newVal) => !deepEqual(oldVal, newVal),
47 }) fixedValues: string[] = [];
48 /** A valid HTML 5 input type for the `input` element. */
49 @property() inputType: string = 'text';
50 /** True for chip input that takes multiple values, false for single input. */
51 @property() multiple: boolean = false;
52 /** Placeholder for the form input. */
53 @property() placeholder?: string = '';
54 /** Callback for input value changes. */
55 @property() onChange: (
56 event: React.SyntheticEvent,
57 newValue: string | string[] | null,
58 reason: AutocompleteChangeReason,
59 details?: AutocompleteChangeDetails
60 ) => void = () => {};
61
62 // Internal state properties from the Redux store.
63 @internalProperty() protected _components:
64 Map<string, ComponentDef> = new Map();
65 @internalProperty() protected _labels: Map<string, LabelDef> = new Map();
66 @internalProperty() protected _members:
67 {userRefs?: UserRef[], groupRefs?: UserRef[]} = {};
68 @internalProperty() protected _projects:
69 {contributorTo?: string[], memberOf?: string[], ownerOf?: string[]} = {};
70
71 /** @override */
72 createRenderRoot(): LitElement {
73 return this;
74 }
75
76 /** @override */
77 updated(changedProperties: Map<string | number | symbol, unknown>): void {
78 super.updated(changedProperties);
79
80 const theme = createTheme({
81 components: {
82 MuiChip: {
83 styleOverrides: {
84 root: {fontSize: 13},
85 },
86 },
87 },
88 palette: {
89 action: {disabledOpacity: 0.6},
90 primary: {
91 // Same as var(--chops-primary-accent-color).
92 main: '#1976d2',
93 },
94 },
95 typography: {fontSize: 11.375},
96 });
97 const element = <ThemeProvider theme={theme}>
98 <ReactAutocomplete
99 label={this.label}
100 options={this._options()}
101 value={this.value}
102 fixedValues={this.fixedValues}
103 inputType={this.inputType}
104 multiple={this.multiple}
105 placeholder={this.placeholder}
106 onChange={this.onChange}
107 getOptionDescription={this._getOptionDescription.bind(this)}
108 getOptionLabel={(option: string) => option}
109 />
110 </ThemeProvider>;
111 ReactDOM.render(element, this);
112 }
113
114 /** @override */
115 stateChanged(state: any): void {
116 super.stateChanged(state);
117
118 this._components = projectV0.componentsMap(state);
119 this._labels = projectV0.labelDefMap(state);
120 this._members = projectV0.viewedVisibleMembers(state);
121 this._projects = userV0.projects(state);
122 }
123
124 /**
125 * Computes which description belongs to given autocomplete option.
126 * Different data is shown depending on the autocomplete vocabulary.
127 * @param option The option to find a description for.
128 * @return The description for the option.
129 */
130 _getOptionDescription(option: string): string {
131 switch (this.vocabularyName) {
132 case 'component': {
133 const component = this._components.get(option);
134 return component && component.docstring || '';
135 } case 'label': {
136 const label = this._labels.get(option.toLowerCase());
137 return label && label.docstring || '';
138 } default: {
139 return '';
140 }
141 }
142 }
143
144 /**
145 * Computes the set of options used by the autocomplete instance.
146 * @return Array of strings that the user can try to match.
147 */
148 _options(): string[] {
149 switch (this.vocabularyName) {
150 case 'component': {
151 return [...this._components.keys()];
152 } case 'label': {
153 // The label map keys are lowercase. Use the LabelDef label name instead.
154 return [...this._labels.values()].map((labelDef: LabelDef) => labelDef.label);
155 } case 'member': {
156 const {userRefs = []} = this._members;
157 const users = userRefsToDisplayNames(userRefs);
158 return users;
159 } case 'owner': {
160 const {userRefs = [], groupRefs = []} = this._members;
161 const users = userRefsToDisplayNames(userRefs);
162 const groups = userRefsToDisplayNames(groupRefs);
163 // Remove groups from the list of all members.
164 return arrayDifference(users, groups);
165 } case 'project': {
166 const {ownerOf = [], memberOf = [], contributorTo = []} = this._projects;
167 return [...ownerOf, ...memberOf, ...contributorTo];
168 } case '': {
169 return [];
170 } default: {
171 throw new Error(`Unknown vocabulary name: ${this.vocabularyName}`);
172 }
173 }
174 }
175}
176customElements.define('mr-react-autocomplete', MrReactAutocomplete);