Textarea updates (#387)

* textarea updates with placeholder, font, and trim

* added wrapper div

* test fix

* font in css var

* Rename readonly to disabled, remove unused div

* Remove unused prop

* Fix code smell

Co-authored-by: rijkvanzanten <rijkvanzanten@me.com>
This commit is contained in:
Jacob Rienstra
2020-04-20 11:19:12 -04:00
committed by GitHub
parent bf79610219
commit b06f19bec5
10 changed files with 115 additions and 58 deletions

View File

@@ -11,35 +11,37 @@ The HTML `<textarea>` element supports a huge amount of attributes and events. I
## Props
| Prop | Description | Default |
|-------------------|--------------------------------------------------------------------------------|---------|
| ----------------- | ------------------------------------------------------------------------------ | ------- |
| `placeholder` | Text to show when no input is entered | `null` |
| `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` |
| `trim` | Trim leading and trailing whitespace | `true` |
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)` |
| Variable | Default |
| -------------------------- | -------------------------- |
| `--v-textarea-min-height` | `none` |
| `--v-textarea-max-height` | `var(--input-height-tall)` |
| `--v-textarea-height` | `var(--input-height-tall)` |
| `--v-textarea-font-family` | `var(--family-sans-serif)` |

View File

@@ -1,4 +1,4 @@
import { withKnobs, boolean } from '@storybook/addon-knobs';
import { withKnobs, boolean, text } from '@storybook/addon-knobs';
import Vue from 'vue';
import readme from './readme.md';
import withPadding from '../../../.storybook/decorators/with-padding';
@@ -22,8 +22,11 @@ export const basic = () =>
disabled: {
default: boolean('Disabled', false),
},
monospace: {
default: boolean('Monospace', false),
trim: {
default: boolean('Trim', false),
},
placeholder: {
default: text('Placeholder', 'Enter a longer value.....'),
},
fullWidth: {
default: boolean('Full Width', false),
@@ -44,9 +47,8 @@ export const basic = () =>
v-model="value"
:disabled="disabled"
:full-width="fullWidth"
:monospace="monospace"
v-bind="{placeholder, trim}"
:expand-on-focus="expandOnFocus"
placeholder="Enter a value..."
/>
<raw-value>{{ value }}</raw-value>
</div>

View File

@@ -17,7 +17,6 @@ describe('Textarea', () => {
it('Sets the correct classes based on props', async () => {
component.setProps({
disabled: true,
monospace: true,
});
await component.vm.$nextTick();

View File

@@ -8,15 +8,15 @@
'has-content': hasContent,
}"
>
<div class="prepend" v-if="$scopedSlots.prepend"><slot name="prepend" /></div>
<textarea
v-bind="$attrs"
v-focus="autofocus"
v-on="_listeners"
:class="{ monospace }"
:placeholder="placeholder"
:disabled="disabled"
:value="value"
/>
<div class="prepend" v-if="$scopedSlots.prepend"><slot name="prepend" /></div>
<div class="append" v-if="$scopedSlots.append"><slot name="append" /></div>
</div>
</template>
@@ -34,10 +34,6 @@ export default defineComponent({
type: Boolean,
default: false,
},
monospace: {
type: Boolean,
default: false,
},
fullWidth: {
type: Boolean,
default: false,
@@ -50,6 +46,14 @@ export default defineComponent({
type: Boolean,
default: false,
},
placeholder: {
type: String,
default: null,
},
trim: {
type: Boolean,
default: true,
},
},
setup(props, { emit, listeners }) {
const _listeners = computed(() => ({
@@ -62,7 +66,11 @@ export default defineComponent({
return { _listeners, hasContent };
function emitValue(event: InputEvent) {
emit('input', (event.target as HTMLInputElement).value);
let value = (event.target as HTMLInputElement).value;
if (props.trim === true) {
value = value.trim();
}
emit('input', value);
}
},
});
@@ -73,6 +81,7 @@ export default defineComponent({
--v-textarea-min-height: none;
--v-textarea-max-height: var(--input-height-tall);
--v-textarea-height: var(--input-height-tall);
--v-textarea-font-family: var(--family-sans-serif);
position: relative;
display: flex;
@@ -141,6 +150,7 @@ export default defineComponent({
height: var(--input-height);
padding: var(--input-padding);
color: var(--foreground-normal);
font-family: var(--v-textarea-font-family);
background-color: transparent;
border: 0;
resize: none;
@@ -148,10 +158,6 @@ export default defineComponent({
&::placeholder {
color: var(--foreground-subdued);
}
&.monospace {
font-family: var(--family-monospace);
}
}
}
</style>

View File

@@ -57,15 +57,15 @@ export default defineComponent({
<style lang="scss" scoped>
.v-input {
.monospace {
&.monospace {
--v-input-font-family: var(--family-monospace);
}
.serif {
&.serif {
--v-input-font-family: var(--family-serif);
}
.sans-serif {
&.sans-serif {
--v-input-font-family: var(--family-sans-serif);
}
}

View File

@@ -7,12 +7,6 @@ export default defineInterface(({ i18n }) => ({
icon: 'box',
component: InterfaceTextarea,
options: [
{
field: 'monospace',
name: 'Monospace',
width: 'half',
interface: 'switch',
},
{
field: 'placeholder',
name: 'Placeholder',
@@ -20,15 +14,23 @@ export default defineInterface(({ i18n }) => ({
interface: 'text-input',
},
{
field: 'rows',
name: 'Rows',
field: 'trim',
name: 'Trim',
width: 'half',
interface: 'numeric',
interface: 'switch',
},
{
field: 'font',
name: 'Font',
width: 'half',
interface: 'dropdown',
options: {
min: 5,
max: 100,
items: [
{ itemText: 'Sans', itemValue: 'sans-serif' },
{ itemText: 'Mono', itemValue: 'monospace' },
{ itemText: 'Serif', itemValue: 'serif' },
],
},
default: 8,
},
],
}));

View File

@@ -1 +1,10 @@
# Textarea
## Options
| Option | Description | Default |
| ------------- | ------------------------------------------------------------------- | ------------ |
| `placeholder` | Text to show when no input is entered | `null` |
| `trim` | Trim leading and trailing whitespace | `true` |
| `font` | Font to render the value in (`sans-serif`, `serif`, or `monospace`) | `sans-serif` |
| `readonly` | Readonly | `false` |

View File

@@ -1,10 +1,11 @@
import { withKnobs, boolean, text } from '@storybook/addon-knobs';
import { withKnobs, boolean, text, optionsKnob } 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';
import { defineComponent, ref } from '@vue/composition-api';
import RawValue from '../../../.storybook/raw-value.vue';
Vue.component('interface-textarea', InterfaceTextarea);
@@ -18,22 +19,40 @@ export default {
export const basic = () =>
defineComponent({
components: { RawValue },
props: {
monospace: {
default: boolean('Monospace', false, 'Options'),
disabled: {
default: boolean('Disabled', false, 'Options'),
},
placeholder: {
default: text('Placeholder', 'Enter a value...', 'Options'),
},
trim: {
default: boolean('Trim', false, 'Options'),
},
font: {
default: optionsKnob(
'Font',
{ Sans: 'sans-serif', Serif: 'serif', Mono: 'monospace' },
'sans',
{ display: 'select' },
'Options'
),
},
},
setup() {
const value = ref('');
const onInput = action('input');
return { onInput };
return { onInput, value };
},
template: `
<div>
<interface-textarea
v-bind="{ monospace, placeholder, rows }"
v-model="value"
v-bind="{ placeholder, trim, font, disabled }"
@input="onInput"
/>
<raw-value>{{ value }}</raw-value>
</div>
`,
});

View File

@@ -13,10 +13,7 @@ describe('Interfaces / Text Input', () => {
const component = shallowMount(InterfaceTextarea, {
localVue,
propsData: {
options: {
monospace: false,
placeholder: 'Enter value...',
},
placeholder: 'Enter value...',
},
listeners: {
input: () => {},

View File

@@ -1,15 +1,16 @@
<template>
<v-textarea
v-model="value"
:placeholder="placeholder"
:monospace="monospace"
:rows="rows"
v-bind="{ placeholder, trim }"
:value="value"
:disabled="disabled"
:class="font"
@input="$listeners.input"
full-width
/>
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
import { defineComponent, PropType } from '@vue/composition-api';
export default defineComponent({
props: {
@@ -25,10 +26,30 @@ export default defineComponent({
type: String,
default: null,
},
monospace: {
trim: {
type: Boolean,
default: false,
default: true,
},
font: {
type: String as PropType<'sans-serif' | 'serif' | 'monospace'>,
default: 'sans-serif',
},
},
});
</script>
<style lang="scss" scoped>
.v-textarea {
&.monospace {
--v-input-font-family: var(--family-monospace);
}
&.serif {
--v-input-font-family: var(--family-serif);
}
&.sans-serif {
--v-input-font-family: var(--family-sans-serif);
}
}
</style>