Skip to content

Commit

Permalink
Merge pull request #2868 from nextcloud/feature/2866/text-field
Browse files Browse the repository at this point in the history
Create TextField component
  • Loading branch information
marcoambrosini authored Aug 4, 2022
2 parents fd41144 + f1be9a1 commit 7348e13
Show file tree
Hide file tree
Showing 7 changed files with 637 additions and 3 deletions.
328 changes: 328 additions & 0 deletions src/components/InputField/InputField.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,328 @@
<!--
- @copyright Copyright (c) 2022 Marco Ambrosini <marcoambrosini@pm.me>
-
- @author Marco Ambrosini <marcoambrosini@pm.me>
-
- @license GNU AGPL version 3 or any later version
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-->

<template>
<div class="input-field">
<label v-if="!labelOutside && label !== undefined"
class="input-field__label"
:class="{ 'input-field__label--hidden': !labelVisible }"
:for="inputName">
{{ label }}
</label>
<div class="input-field__main-wrapper">
<input v-bind="$attrs"
ref="input"
:name="inputName"
class="input-field__input"
:type="type"
:placeholder="computedPlaceholder"
aria-live="polite"
:class="{
'input-field__input--trailing-icon': showTrailingButton || hasTrailingIcon,
'input-field__input--leading-icon': hasLeadingIcon,
'input-field__input--success': success,
'input-field__input--error': error,
}"
:value="value"
v-on="$listeners"
@input="handleInput">

<!-- Leading icon -->
<div class="input-field__icon input-field__icon--leading">
<!-- Leading material design icon in the text field, set the size to 18 -->
<slot />
</div>

<!-- Success and error icons -->
<div v-if="success || error" class="input-field__icon input-field__icon--trailing">
<Check v-if="success" :size="18" />
<AlertCircle v-else-if="error" :size="18" />
</div>

<!-- trailing button -->
<Button v-else-if="showTrailingButton"
type="tertiary-no-background"
class="input-field__clear-button"
@click="handleTrailingButtonClick">
<!-- Populating this slot creates a trailing button within the
input boundaries that emits a `trailing-button-click` event -->
<template slot="icon">
<slot name="trailing-button-icon" />
</template>
</Button>
</div>
</div>
</template>

<script>
import Button from '../Button/index.js'
import Check from 'vue-material-design-icons/Check'
import AlertCircle from 'vue-material-design-icons/AlertCircleOutline'
import GenRandomId from '../../utils/GenRandomId.js'

export default {
name: 'InputField',

components: {
Button,
Check,
AlertCircle,
},

props: {
/**
* The value of the input field
*/
value: {
type: String,
required: true,
},

/**
* The input element type
*/
type: {
type: String,
required: true,
},

/**
* The hidden input label for accessibility purposes. This will also
* be used as a placeholder unless the placeholder prop is populated
* with a different string.
*/
label: {
type: String,
default: undefined,
},

/**
* Pass in true if you want to use an external label. This is useful
* if you need a label that looks different from the one provided by
* this component
*/
labelOutside: {
type: Boolean,
default: false,
},

/**
* We normally have the lable hidden visually and use it for
* accessibility only. If you want to have the label visible just above
* the input field pass in true to this prop.
*/
labelVisible: {
type: Boolean,
default: false,
},

/**
* The placeholder of the input. This defaults as the string that's
* passed into the label prop. In order to remove the placeholder,
* pass in an empty string.
*/
placeholder: {
type: String,
default: undefined,
},

/**
* Controls whether to display the trailing button.
*/
showTrailingButton: {
type: Boolean,
default: false,
},

/**
* Toggles the success state of the component. Adds a checkmark icon.
* this cannot be used together with canClear.
*/
success: {
type: Boolean,
default: false,
},

/**
* Toggles the error state of the component. Adds an error icon.
* this cannot be used together with canClear.
*/
error: {
type: Boolean,
default: false,
},
},

computed: {
inputName() {
return 'input' + GenRandomId()
},

hasLeadingIcon() {
return this.$slots.default
},

hasTrailingIcon() {
return this.success
},

hasPlaceholder() {
return this.placeholder !== '' && this.placeholder !== undefined
},

computedPlaceholder() {
if (this.labelVisible) {
return this.hasPlaceholder ? this.placeholder : ''
} else {
return this.hasPlaceholder ? this.placeholder : this.label
}
},
},

watch: {
label() {
this.validateLabel()
},

labelOutside() {
this.validateLabel()
},
},

methods: {
handleInput(event) {
this.$emit('update:value', event.target.value)
},

handleTrailingButtonClick(event) {
this.$emit('trailing-button-click', event)
},

validateLabel() {
if (this.label && !this.labelOutside) {
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')
}
},
},
}

</script>

<style lang="scss" scoped>

.input-field {
position: relative;
width: 100%;
border-radius: var(--border-radius-large);

&__main-wrapper {
height: 36px;
position: relative;
}

&__input {
margin: 0;
padding: 0 12px;
font-size: var(--default-font-size);
background-color: var(--color-main-background);
color: var(--color-main-text);
border: 2px solid var(--color-border-dark);
height: 36px !important;
border-radius: var(--border-radius-large);
text-overflow: ellipsis;
cursor: pointer;
width: 100%;
-webkit-appearance: textfield !important;
-moz-appearance: textfield !important;

&:hover {
border-color: var(--color-primary-element);
}
&:focus {
cursor: text;
}

&--success {
border-color: var(--color-success) !important; //Override hover border color
}

&--error {
border-color: var(--color-error) !important; //Override hover border color
}

&--leading-icon {
padding-left: 28px;
}

&--trailing-icon {
padding-right: 28px;
}
}

&__label {
padding: 4px 0;
display: block;

&--hidden {
position: absolute;
left: -10000px;
top: auto;
width: 1px;
height: 1px;
overflow: hidden;
}
}

&__icon {
position: absolute;
height: 32px;
width: 32px;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.7;
&--leading {
bottom: 2px;
left: 2px;
}

&--trailing {
bottom: 2px;
right: 2px;
}
}

&__clear-button {
position: absolute;
top: 2px;
right: 1px;
}
}

::v-deep .button-vue {
min-width: unset;
min-height: unset;
height: 32px;
width: 32px !important;
border-radius: var(--border-radius-large);
}

</style>
25 changes: 25 additions & 0 deletions src/components/InputField/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* @copyright Copyright (c) 2022 Marco Ambrosini <marcoambrosini@pm.me>
*
* @author Marco Ambrosini <marcoambrosini@pm.me>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

import InputField from './InputField.vue'

export default InputField
Loading

0 comments on commit 7348e13

Please sign in to comment.