mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
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:
@@ -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/';
|
||||
|
||||
|
||||
4
src/components/v-textarea/index.ts
Normal file
4
src/components/v-textarea/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import VTextarea from './v-textarea.vue';
|
||||
|
||||
export { VTextarea };
|
||||
export default VTextarea;
|
||||
45
src/components/v-textarea/readme.md
Normal file
45
src/components/v-textarea/readme.md
Normal 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)` |
|
||||
76
src/components/v-textarea/v-textarea.story.ts
Normal file
76
src/components/v-textarea/v-textarea.story.ts
Normal 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>
|
||||
`,
|
||||
});
|
||||
34
src/components/v-textarea/v-textarea.test.ts
Normal file
34
src/components/v-textarea/v-textarea.test.ts
Normal 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']);
|
||||
});
|
||||
});
|
||||
147
src/components/v-textarea/v-textarea.vue
Normal file
147
src/components/v-textarea/v-textarea.vue
Normal 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>
|
||||
@@ -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;
|
||||
|
||||
34
src/interfaces/textarea/index.ts
Normal file
34
src/interfaces/textarea/index.ts
Normal 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,
|
||||
},
|
||||
],
|
||||
}));
|
||||
1
src/interfaces/textarea/readme.md
Normal file
1
src/interfaces/textarea/readme.md
Normal file
@@ -0,0 +1 @@
|
||||
# Textarea
|
||||
39
src/interfaces/textarea/textarea.story.ts
Normal file
39
src/interfaces/textarea/textarea.story.ts
Normal 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"
|
||||
/>
|
||||
`,
|
||||
});
|
||||
27
src/interfaces/textarea/textarea.test.ts
Normal file
27
src/interfaces/textarea/textarea.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
34
src/interfaces/textarea/textarea.vue
Normal file
34
src/interfaces/textarea/textarea.vue
Normal 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>
|
||||
Reference in New Issue
Block a user