Interface textarea (#310)

* textarea component

* textarea component files

* textarea interface

* Update v-textarea

* Fix test

* Remove resize option in textarea

Co-authored-by: rijkvanzanten <rijkvanzanten@me.com>
This commit is contained in:
Jacob Rienstra
2020-04-06 14:07:47 -04:00
committed by GitHub
parent 30ec0c02b6
commit 417fb4371d
12 changed files with 445 additions and 1 deletions

View File

@@ -38,6 +38,7 @@ import VSlider from './v-slider/';
import VSwitch from './v-switch/';
import VTable from './v-table/';
import VTabs, { VTab, VTabsItems, VTabItem } from './v-tabs/';
import VTextarea from './v-textarea';
Vue.component('v-avatar', VAvatar);
Vue.component('v-button', VButton);
@@ -84,6 +85,7 @@ Vue.component('v-tabs', VTabs);
Vue.component('v-tab', VTab);
Vue.component('v-tabs-items', VTabsItems);
Vue.component('v-tab-item', VTabItem);
Vue.component('v-textarea', VTextarea);
import DrawerDetail from '@/views/private/components/drawer-detail/';

View File

@@ -0,0 +1,4 @@
import VTextarea from './v-textarea.vue';
export { VTextarea };
export default VTextarea;

View File

@@ -0,0 +1,45 @@
# Input
```html
<v-textarea v-model="value" />
```
## Attributes & Events
The HTML `<textarea>` element supports a huge amount of attributes and events. In order to support all of these, all props that aren't specially handled (see list below) will be passed on to the `<textarea>` element directly. This allows you to change anything you want on the input.
## Props
| Prop | Description | Default |
|-------------------|--------------------------------------------------------------------------------|---------|
| `autofocus` | Autofocusses the input on render | `false` |
| `disabled` | Set the disabled state for the input | `false` |
| `monospace` | Render the entered value in the monospace font | `false` |
| `full-width` | Render the input with 100% width | `false` |
| `value` | Current value. Syncs with `v-model` | |
| `expand-on-focus` | Renders the textarea at regular input size, and expands to max-height on focus | `false` |
Note: all other attached attributes are bound to the input HTMLELement in the component. This allows you to attach any of the standard HTML attributes like `min`, `length`, or `pattern`.
## Slots
| Slot | Description | Data |
|-----------|----------------------------------------------------------|------|
| `prepend` | Prepend elements before the text content in the textarea | |
| `append` | Append elements after the text content | |
## Events
| Events | Description | Value |
|---------|-------------------|-------|
| `input` | Updates `v-model` | `any` |
Note: all other listeners are bound to the input HTMLElement, allowing you to handle everything from `keydown` to `emptied`.
## CSS Variables
| Variable | Default |
|---------------------------|----------------------------|
| `--v-textarea-min-height` | `none` |
| `--v-textarea-max-height` | `var(--input-height-tall)` |
| `--v-textarea-height` | `var(--input-height-tall)` |

View File

@@ -0,0 +1,76 @@
import { withKnobs, boolean } from '@storybook/addon-knobs';
import Vue from 'vue';
import readme from './readme.md';
import withPadding from '../../../.storybook/decorators/with-padding';
import { defineComponent, ref } from '@vue/composition-api';
import RawValue from '../../../.storybook/raw-value.vue';
Vue.directive('focus', {});
export default {
title: 'Components / Textarea',
decorators: [withKnobs, withPadding],
parameters: {
notes: readme,
},
};
export const basic = () =>
defineComponent({
components: { RawValue },
props: {
disabled: {
default: boolean('Disabled', false),
},
monospace: {
default: boolean('Monospace', false),
},
fullWidth: {
default: boolean('Full Width', false),
},
expandOnFocus: {
default: boolean('Expand on Focus', false),
},
},
setup() {
const value = ref('');
return {
value,
};
},
template: `
<div>
<v-textarea
v-model="value"
:disabled="disabled"
:full-width="fullWidth"
:monospace="monospace"
:expand-on-focus="expandOnFocus"
placeholder="Enter a value..."
/>
<raw-value>{{ value }}</raw-value>
</div>
`,
});
export const withSlots = () =>
defineComponent({
template: `
<v-textarea v-model="value">
<template #prepend>
<v-sheet
style="
--v-sheet-background-color: var(--primary-alt);
--v-sheet-color: var(--primary);
">Prepend</v-sheet>
</template>
<template #append>
<v-sheet
style="
--v-sheet-background-color: var(--secondary-alt);
--v-sheet-color: var(--secondary);
">Append</v-sheet>
</template>
</v-textarea>
`,
});

View File

@@ -0,0 +1,34 @@
import { mount, createLocalVue, Wrapper } from '@vue/test-utils';
import VueCompositionAPI from '@vue/composition-api';
const localVue = createLocalVue();
localVue.use(VueCompositionAPI);
localVue.directive('focus', {});
import VTextarea from './v-textarea.vue';
describe('Textarea', () => {
let component: Wrapper<Vue>;
beforeEach(() => {
component = mount(VTextarea, { localVue });
});
it('Sets the correct classes based on props', async () => {
component.setProps({
disabled: true,
monospace: true,
});
await component.vm.$nextTick();
expect(component.find('.v-textarea').classes()).toEqual(['v-textarea', 'disabled']);
});
it('Emits just the value for the input event', async () => {
const input = component.find('textarea');
(input.element as HTMLInputElement).value = 'The value';
input.trigger('input');
expect(component.emitted('input')?.[0]).toEqual(['The value']);
});
});

View File

@@ -0,0 +1,147 @@
<template>
<div
class="v-textarea"
:class="{
disabled,
'expand-on-focus': expandOnFocus,
'full-width': fullWidth,
}"
>
<div class="prepend" v-if="$scopedSlots.prepend"><slot name="prepend" /></div>
<textarea
v-bind="$attrs"
v-focus="autofocus"
v-on="_listeners"
:class="{
monospace,
'allow-resize-x': !allowResizeY && allowResizeX,
'allow-resize-y': !allowResizeX && allowResizeY,
'allow-resize-both': allowResizeX && allowResizeY,
}"
:disabled="disabled"
:value="value"
/>
<div class="append" v-if="$scopedSlots.append"><slot name="append" /></div>
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from '@vue/composition-api';
export default defineComponent({
props: {
disabled: {
type: Boolean,
default: false,
},
autofocus: {
type: Boolean,
default: false,
},
monospace: {
type: Boolean,
default: false,
},
fullWidth: {
type: Boolean,
default: false,
},
value: {
type: [String, Number],
default: null,
},
expandOnFocus: {
type: Boolean,
default: false,
},
},
setup(props, { emit, listeners }) {
const _listeners = computed(() => ({
...listeners,
input: emitValue,
}));
return { _listeners };
function emitValue(event: InputEvent) {
emit('input', (event.target as HTMLInputElement).value);
}
},
});
</script>
<style lang="scss" scoped>
.v-textarea {
--v-textarea-min-height: none;
--v-textarea-max-height: var(--input-height-tall);
--v-textarea-height: var(--input-height-tall);
position: relative;
display: flex;
flex-direction: column;
width: max-content;
height: var(--v-textarea-height);
min-height: var(--v-textarea-min-height);
max-height: var(--v-textarea-max-height);
background-color: var(--background-page);
border: var(--border-width) solid var(--border-normal);
border-radius: var(--border-radius);
transition: border-color var(--fast) var(--transition);
.append,
.prepend {
flex-shrink: 0;
}
&.expand-on-focus {
height: var(--input-height);
transition: height var(--medium) var(--transition);
&:focus,
&:focus-within {
height: var(--v-textarea-max-height);
}
}
&:hover {
border-color: var(--border-normal);
}
&:focus,
&:focus-within {
border-color: var(--primary);
}
&.full-width {
width: 100%;
}
&.disabled {
color: var(--foreground-subdued);
background-color: var(--background-subdued);
border-color: var(--border-subdued);
cursor: not-allowed;
}
textarea {
position: relative;
display: block;
flex-grow: 1;
width: 100%;
height: var(--input-height);
padding: var(--input-padding);
color: var(--foreground-normal);
background-color: transparent;
border: 0;
resize: none;
&::placeholder {
color: var(--foreground-subdued);
}
&.monospace {
font-family: var(--family-monospace);
}
}
}
</style>

View File

@@ -1,4 +1,5 @@
import InterfaceTextInput from './text-input/';
import InterfaceTextarea from './textarea/';
export const interfaces = [InterfaceTextInput];
export const interfaces = [InterfaceTextInput, InterfaceTextarea];
export default interfaces;

View File

@@ -0,0 +1,34 @@
import InterfaceTextarea from './textarea.vue';
import { defineInterface } from '@/interfaces/define';
export default defineInterface(({ i18n }) => ({
id: 'textarea',
name: i18n.t('interfaces.textarea.textarea'),
icon: 'box',
component: InterfaceTextarea,
options: [
{
field: 'monospace',
name: 'Monospace',
width: 'half',
interface: 'switch',
},
{
field: 'placeholder',
name: 'Placeholder',
width: 'half',
interface: 'text-input',
},
{
field: 'rows',
name: 'Rows',
width: 'half',
interface: 'numeric',
options: {
min: 5,
max: 100,
},
default: 8,
},
],
}));

View File

@@ -0,0 +1 @@
# Textarea

View File

@@ -0,0 +1,39 @@
import { withKnobs, boolean, text } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';
import Vue from 'vue';
import InterfaceTextarea from './textarea.vue';
import markdown from './readme.md';
import withPadding from '../../../.storybook/decorators/with-padding';
import { defineComponent } from '@vue/composition-api';
Vue.component('interface-textarea', InterfaceTextarea);
export default {
title: 'Interfaces / Textarea',
decorators: [withKnobs, withPadding],
parameters: {
notes: markdown,
},
};
export const basic = () =>
defineComponent({
props: {
monospace: {
default: boolean('Monospace', false, 'Options'),
},
placeholder: {
default: text('Placeholder', 'Enter a value...', 'Options'),
},
},
setup() {
const onInput = action('input');
return { onInput };
},
template: `
<interface-textarea
v-bind="{ monospace, placeholder, rows }"
@input="onInput"
/>
`,
});

View File

@@ -0,0 +1,27 @@
import VueCompositionAPI from '@vue/composition-api';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import InterfaceTextarea from './textarea.vue';
import VTextarea from '@/components/v-textarea';
const localVue = createLocalVue();
localVue.use(VueCompositionAPI);
localVue.component('v-textarea', VTextarea);
describe('Interfaces / Text Input', () => {
it('Renders a v-textarea', () => {
const component = shallowMount(InterfaceTextarea, {
localVue,
propsData: {
options: {
monospace: false,
placeholder: 'Enter value...',
},
},
listeners: {
input: () => {},
},
});
expect(component.find(VTextarea).exists()).toBe(true);
});
});

View File

@@ -0,0 +1,34 @@
<template>
<v-textarea
v-model="value"
:placeholder="placeholder"
:monospace="monospace"
:rows="rows"
full-width
/>
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
export default defineComponent({
props: {
value: {
type: String,
default: null,
},
disabled: {
type: Boolean,
default: false,
},
placeholder: {
type: String,
default: null,
},
monospace: {
type: Boolean,
default: false,
},
},
});
</script>