Skip to content

Commit 151d067

Browse files
Merge pull request #2936 from nextcloud/backport/2868/stable5
[stable5] Create TextField component
2 parents d8b792f + 8f37a15 commit 151d067

7 files changed

Lines changed: 637 additions & 3 deletions

File tree

Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
<!--
2+
- @copyright Copyright (c) 2022 Marco Ambrosini <marcoambrosini@pm.me>
3+
-
4+
- @author Marco Ambrosini <marcoambrosini@pm.me>
5+
-
6+
- @license GNU AGPL version 3 or any later version
7+
-
8+
- This program is free software: you can redistribute it and/or modify
9+
- it under the terms of the GNU Affero General Public License as
10+
- published by the Free Software Foundation, either version 3 of the
11+
- License, or (at your option) any later version.
12+
-
13+
- This program is distributed in the hope that it will be useful,
14+
- but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
- GNU Affero General Public License for more details.
17+
-
18+
- You should have received a copy of the GNU Affero General Public License
19+
- along with this program. If not, see <http://www.gnu.org/licenses/>.
20+
-->
21+
22+
<template>
23+
<div class="input-field">
24+
<label v-if="!labelOutside && label !== undefined"
25+
class="input-field__label"
26+
:class="{ 'input-field__label--hidden': !labelVisible }"
27+
:for="inputName">
28+
{{ label }}
29+
</label>
30+
<div class="input-field__main-wrapper">
31+
<input v-bind="$attrs"
32+
ref="input"
33+
:name="inputName"
34+
class="input-field__input"
35+
:type="type"
36+
:placeholder="computedPlaceholder"
37+
aria-live="polite"
38+
:class="{
39+
'input-field__input--trailing-icon': showTrailingButton || hasTrailingIcon,
40+
'input-field__input--leading-icon': hasLeadingIcon,
41+
'input-field__input--success': success,
42+
'input-field__input--error': error,
43+
}"
44+
:value="value"
45+
v-on="$listeners"
46+
@input="handleInput">
47+
48+
<!-- Leading icon -->
49+
<div class="input-field__icon input-field__icon--leading">
50+
<!-- Leading material design icon in the text field, set the size to 18 -->
51+
<slot />
52+
</div>
53+
54+
<!-- Success and error icons -->
55+
<div v-if="success || error" class="input-field__icon input-field__icon--trailing">
56+
<Check v-if="success" :size="18" />
57+
<AlertCircle v-else-if="error" :size="18" />
58+
</div>
59+
60+
<!-- trailing button -->
61+
<Button v-else-if="showTrailingButton"
62+
type="tertiary-no-background"
63+
class="input-field__clear-button"
64+
@click="handleTrailingButtonClick">
65+
<!-- Populating this slot creates a trailing button within the
66+
input boundaries that emits a `trailing-button-click` event -->
67+
<template slot="icon">
68+
<slot name="trailing-button-icon" />
69+
</template>
70+
</Button>
71+
</div>
72+
</div>
73+
</template>
74+
75+
<script>
76+
import Button from '../Button/index.js'
77+
import Check from 'vue-material-design-icons/Check'
78+
import AlertCircle from 'vue-material-design-icons/AlertCircleOutline'
79+
import GenRandomId from '../../utils/GenRandomId.js'
80+
81+
export default {
82+
name: 'InputField',
83+
84+
components: {
85+
Button,
86+
Check,
87+
AlertCircle,
88+
},
89+
90+
props: {
91+
/**
92+
* The value of the input field
93+
*/
94+
value: {
95+
type: String,
96+
required: true,
97+
},
98+
99+
/**
100+
* The input element type
101+
*/
102+
type: {
103+
type: String,
104+
required: true,
105+
},
106+
107+
/**
108+
* The hidden input label for accessibility purposes. This will also
109+
* be used as a placeholder unless the placeholder prop is populated
110+
* with a different string.
111+
*/
112+
label: {
113+
type: String,
114+
default: undefined,
115+
},
116+
117+
/**
118+
* Pass in true if you want to use an external label. This is useful
119+
* if you need a label that looks different from the one provided by
120+
* this component
121+
*/
122+
labelOutside: {
123+
type: Boolean,
124+
default: false,
125+
},
126+
127+
/**
128+
* We normally have the lable hidden visually and use it for
129+
* accessibility only. If you want to have the label visible just above
130+
* the input field pass in true to this prop.
131+
*/
132+
labelVisible: {
133+
type: Boolean,
134+
default: false,
135+
},
136+
137+
/**
138+
* The placeholder of the input. This defaults as the string that's
139+
* passed into the label prop. In order to remove the placeholder,
140+
* pass in an empty string.
141+
*/
142+
placeholder: {
143+
type: String,
144+
default: undefined,
145+
},
146+
147+
/**
148+
* Controls whether to display the trailing button.
149+
*/
150+
showTrailingButton: {
151+
type: Boolean,
152+
default: false,
153+
},
154+
155+
/**
156+
* Toggles the success state of the component. Adds a checkmark icon.
157+
* this cannot be used together with canClear.
158+
*/
159+
success: {
160+
type: Boolean,
161+
default: false,
162+
},
163+
164+
/**
165+
* Toggles the error state of the component. Adds an error icon.
166+
* this cannot be used together with canClear.
167+
*/
168+
error: {
169+
type: Boolean,
170+
default: false,
171+
},
172+
},
173+
174+
computed: {
175+
inputName() {
176+
return 'input' + GenRandomId()
177+
},
178+
179+
hasLeadingIcon() {
180+
return this.$slots.default
181+
},
182+
183+
hasTrailingIcon() {
184+
return this.success
185+
},
186+
187+
hasPlaceholder() {
188+
return this.placeholder !== '' && this.placeholder !== undefined
189+
},
190+
191+
computedPlaceholder() {
192+
if (this.labelVisible) {
193+
return this.hasPlaceholder ? this.placeholder : ''
194+
} else {
195+
return this.hasPlaceholder ? this.placeholder : this.label
196+
}
197+
},
198+
},
199+
200+
watch: {
201+
label() {
202+
this.validateLabel()
203+
},
204+
205+
labelOutside() {
206+
this.validateLabel()
207+
},
208+
},
209+
210+
methods: {
211+
handleInput(event) {
212+
this.$emit('update:value', event.target.value)
213+
},
214+
215+
handleTrailingButtonClick(event) {
216+
this.$emit('trailing-button-click', event)
217+
},
218+
219+
validateLabel() {
220+
if (this.label && !this.labelOutside) {
221+
throw new Error('You need to add a label to the textField component. Either use the prop label or use an external one, as per the example in the documentation')
222+
}
223+
},
224+
},
225+
}
226+
227+
</script>
228+
229+
<style lang="scss" scoped>
230+
231+
.input-field {
232+
position: relative;
233+
width: 100%;
234+
border-radius: var(--border-radius-large);
235+
236+
&__main-wrapper {
237+
height: 36px;
238+
position: relative;
239+
}
240+
241+
&__input {
242+
margin: 0;
243+
padding: 0 12px;
244+
font-size: var(--default-font-size);
245+
background-color: var(--color-main-background);
246+
color: var(--color-main-text);
247+
border: 2px solid var(--color-border-dark);
248+
height: 36px !important;
249+
border-radius: var(--border-radius-large);
250+
text-overflow: ellipsis;
251+
cursor: pointer;
252+
width: 100%;
253+
-webkit-appearance: textfield !important;
254+
-moz-appearance: textfield !important;
255+
256+
&:hover {
257+
border-color: var(--color-primary-element);
258+
}
259+
&:focus {
260+
cursor: text;
261+
}
262+
263+
&--success {
264+
border-color: var(--color-success) !important; //Override hover border color
265+
}
266+
267+
&--error {
268+
border-color: var(--color-error) !important; //Override hover border color
269+
}
270+
271+
&--leading-icon {
272+
padding-left: 28px;
273+
}
274+
275+
&--trailing-icon {
276+
padding-right: 28px;
277+
}
278+
}
279+
280+
&__label {
281+
padding: 4px 0;
282+
display: block;
283+
284+
&--hidden {
285+
position: absolute;
286+
left: -10000px;
287+
top: auto;
288+
width: 1px;
289+
height: 1px;
290+
overflow: hidden;
291+
}
292+
}
293+
294+
&__icon {
295+
position: absolute;
296+
height: 32px;
297+
width: 32px;
298+
display: flex;
299+
align-items: center;
300+
justify-content: center;
301+
opacity: 0.7;
302+
&--leading {
303+
bottom: 2px;
304+
left: 2px;
305+
}
306+
307+
&--trailing {
308+
bottom: 2px;
309+
right: 2px;
310+
}
311+
}
312+
313+
&__clear-button {
314+
position: absolute;
315+
top: 2px;
316+
right: 1px;
317+
}
318+
}
319+
320+
::v-deep .button-vue {
321+
min-width: unset;
322+
min-height: unset;
323+
height: 32px;
324+
width: 32px !important;
325+
border-radius: var(--border-radius-large);
326+
}
327+
328+
</style>

src/components/InputField/index.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* @copyright Copyright (c) 2022 Marco Ambrosini <marcoambrosini@pm.me>
3+
*
4+
* @author Marco Ambrosini <marcoambrosini@pm.me>
5+
*
6+
* @license AGPL-3.0-or-later
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU Affero General Public License as
10+
* published by the Free Software Foundation, either version 3 of the
11+
* License, or (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* GNU Affero General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU Affero General Public License
19+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
20+
*
21+
*/
22+
23+
import InputField from './InputField.vue'
24+
25+
export default InputField

0 commit comments

Comments
 (0)