Skip to content

Commit

Permalink
Add TextArea component (#459)
Browse files Browse the repository at this point in the history
* Upstream TextArea component from Mismatch Finder
* Add TextArea story (#460)

Co-authored-by: Silvan Heintze <59574251+Silvan-WMDE@users.noreply.github.com>
Bug: T289138
Bug: T289141
  • Loading branch information
itamargiv and Silvan-WMDE committed Aug 26, 2021
1 parent 74d7753 commit bf4e40e
Show file tree
Hide file tree
Showing 4 changed files with 372 additions and 0 deletions.
14 changes: 14 additions & 0 deletions vue-components/src/components/ResizeLimit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
enum ResizeLimit {
Horizontal = 'horizontal',
Vertical = 'vertical',
None = 'none'
}

function validateLimit( limit: string ): boolean {
return Object.values( ResizeLimit ).includes( limit as ResizeLimit );
}

export {
ResizeLimit,
validateLimit,
};
201 changes: 201 additions & 0 deletions vue-components/src/components/TextArea.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
<template>
<div class="wikit wikit-TextArea">
<span class="wikit-TextArea__label-wrapper">
<label
:class="[
'wikit-TextArea__label'
]"
:for="id"
>
{{ label }}
</label>
</span>
<textarea
:id="id"
:class="[
'wikit-TextArea__textarea',
`wikit-TextArea__textarea--${resizeType}`
]"
:value="value"
:rows="rows"
:placeholder="placeholder"
label=""
@input="$emit( 'input', $event.target.value )"
/>
</div>
</template>

<script lang="ts">
import Vue from 'vue';
import generateId from '@/components/util/generateUid';
import { ResizeLimit, validateLimit } from '@/components/ResizeLimit';
/**
* Text areas are multi-line, non auto-sizing input fields that allow manual resizing by users.
*/
export default Vue.extend( {
props: {
/**
* An initial value for the textarea
*/
value: {
type: String,
default: '',
},
/**
* The text area label
*/
label: {
type: String,
default: '',
},
/**
* The text area placeholder
*/
placeholder: {
type: String,
default: '',
},
/**
* Defines the amount of lines of text that the text area can take by
* default before scroll is triggered, therefore influencing the height
* of the component.
*/
rows: {
type: Number,
default: 2,
},
/**
* Allows users to expand the component horizontally or vertically
* using the expand handler. It can be used to entirely disable manual
* resizing.
*
* Allowed values: `vertical`, `horizontal`, `none`
*/
resize: {
type: String,
validator( value: ResizeLimit ): boolean {
return validateLimit( value );
},
default: ResizeLimit.Vertical,
},
},
data() {
return {
// https://github.com/vuejs/vue/issues/5886
id: generateId( 'wikit-TextArea' ),
};
},
computed: {
resizeType(): string {
// Unfortunately, the vue prop validator does not throw or falls
// back to default values on validation failure, therefore, we need
// to check for a valid resize limit value
return validateLimit( this.resize ) ? this.resize : 'vertical';
},
},
} );
</script>

<style lang="scss">
.wikit-TextArea {
&__label-wrapper {
display: flex;
align-items: center;
gap: $dimension-spacing-small;
}
&__label {
@include Label('block');
}
}
.wikit-TextArea__textarea {
display: block;
width: 100%;
// The default resizing behaviour should be on the y axis only
resize: vertical;
/**
* Colors
*/
color: $wikit-Input-color;
background-color: $wikit-Input-background-color;
/**
* Typography
*/
font-family: $wikit-Input-font-family;
font-size: $wikit-Input-font-size;
font-weight: $wikit-Input-font-weight;
line-height: $wikit-Input-line-height;
/**
* Spacing
*/
padding-inline: $wikit-Input-desktop-padding-inline;
padding-block: $wikit-Input-desktop-padding-block;
@media (max-width: $width-breakpoint-mobile) {
padding-inline: $wikit-Input-mobile-padding-inline;
padding-block: $wikit-Input-mobile-padding-block;
}
/**
* Borders
*/
border-color: $wikit-Input-border-color;
border-style: $wikit-Input-border-style;
border-width: $wikit-Input-border-width;
border-radius: $wikit-Input-border-radius;
/**
* Animation
*/
// Sets a basis for the inset box-shadow transition which otherwise doesn't work in Firefox.
// https://stackoverflow.com/questions/25410207/css-transition-not-working-on-box-shadow-property/25410897
// TODO: replace by token
box-shadow: inset 0 0 0 1px transparent;
transition-duration: $wikit-Input-transition-duration;
transition-timing-function: $wikit-Input-transition-timing-function;
transition-property: $wikit-Input-transition-property;
/**
* State overrides
*/
&:hover {
border-color: $wikit-Input-hover-border-color;
}
&:focus,
&:active {
border-color: $wikit-Input-active-border-color;
box-shadow: $wikit-Input-active-box-shadow;
}
&:focus {
outline: none;
}
&::placeholder {
font-family: $wikit-Input-placeholder-font-family;
font-size: $wikit-Input-placeholder-font-size;
font-weight: $wikit-Input-placeholder-font-weight;
line-height: $wikit-Input-placeholder-line-height;
color: $wikit-Input-placeholder-color;
}
/**
* Property overrides
*/
&--horizontal {
resize: horizontal;
}
&--none {
resize: none;
}
}
</style>
84 changes: 84 additions & 0 deletions vue-components/stories/TextArea.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import TextArea from '@/components/TextArea';
import { Component } from 'vue';

export default {
component: TextArea,
title: 'TextArea',
};

export function basic( args: object ): Component {
return {
data(): object {
return { args };
},
components: { TextArea },
props: Object.keys( args ),
template: `
<div style="max-width: 75%">
<TextArea
:label="label"
:placeholder="placeholder"
:rows="rows"
:resize="resize"
/>
</div>
`,
};
}

basic.args = {
label: 'Label',
placeholder: 'Placeholder'
};

basic.argTypes = {
value: {
control: false
},
label: {
control: {
type: 'text',
},
},
placeholder: {
control: {
type: 'text',
},
},
rows: {
control: {
type: 'number',
},
},
resize: {
table: {
defaultValue: {
summary: 'vertical'
}
},
control: {
type: 'select',
options: ['vertical', 'horizontal', 'none'],
default: 'vertical'
},
},
input : {
description: 'Emitted on each character input to the textarea, contains the entire string value of the textarea itself.',
table: {
type: {
summary: 'string'
}
}
}
};

export function all(): Component {
return {
components: { TextArea },
template: `
<div style="max-width: 95%">
<TextArea label="Label" placeholder="Placeholder" />
</div>
`,
};
}
73 changes: 73 additions & 0 deletions vue-components/tests/unit/components/TextArea.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { mount } from '@vue/test-utils';
import TextArea from '@/components/TextArea.vue';
import { ResizeLimit } from '@/components/ResizeLimit.ts';

describe( 'TextArea.vue', () => {
it( 'accepts rows property', () => {
const wrapper = mount( TextArea, {
propsData: { rows: 42 },
} );

expect( wrapper.props().rows ).toBe( 42 );
expect( wrapper.find( 'textarea' ).attributes( 'rows' ) ).toBe( '42' );
} );

it( 'accepts resize property', () => {
const wrapper = mount( TextArea, {
propsData: { resize: ResizeLimit.Horizontal },
} );

expect( wrapper.props().resize ).toBe( ResizeLimit.Horizontal );
expect( wrapper.find( 'textarea' ).classes() ).toContain( 'wikit-TextArea__textarea--horizontal' );
} );

it( 'uses default resize value', () => {
const wrapper = mount( TextArea );

expect( wrapper.find( 'textarea' ).classes() ).toContain( 'wikit-TextArea__textarea--vertical' );
} );

it( 'throws on invalid resize values', () => {
expect( () => mount( TextArea, {
propsData: { resize: 'nonsense' },
} ) ).toThrow( 'Invalid prop: custom validator check failed for prop "resize"' );
} );

it( 'accepts a textarea value', () => {
const value = 'Some beautiful value!';
const wrapper = mount( TextArea, {
propsData: { value },
} );
const element = wrapper.find( 'textarea' ).element as HTMLFormElement;

expect( wrapper.props().value ).toBe( value );
expect( element.value ).toBe( value );
} );

it( 'accepts label property', () => {
const label = 'da Label';
const wrapper = mount( TextArea, {
propsData: { label },
} );

expect( wrapper.props().label ).toBe( label );
expect( wrapper.find( 'label' ).text() ).toBe( label );
} );

it( 'accepts placeholder property', () => {
const placeholder = 'This is a placeholder';
const wrapper = mount( TextArea, {
propsData: { placeholder },
} );

expect( wrapper.find( 'textarea' ).attributes( 'placeholder' ) ).toBe( placeholder );
} );

it( 'should emit a change event with textarea value', () => {
const userInput = 'hello';
const wrapper = mount( TextArea );

wrapper.find( 'textarea' ).setValue( userInput );
expect( wrapper.emitted( 'input' )![ 0 ] ).toEqual( [ userInput ] );
} );
} );

0 comments on commit bf4e40e

Please sign in to comment.