Text-input / v-input fixes (#386)

* textinput fixes including masking trimming icons and fonts

* removed masked attribute from v-input

* added wrapper div

* ugh

* test fix

* fixed all calls to monospace boolean except on textarea (in separate branch)

* readonly

* Remove unused wrapper div and rename readonly to disabled

* Rename readonly to disabled in story

* Prefer style over inline styles

* Fix codesmell

Co-authored-by: rijkvanzanten <rijkvanzanten@me.com>
This commit is contained in:
Jacob Rienstra
2020-04-20 10:23:01 -04:00
committed by GitHub
parent 471c759df7
commit bf79610219
18 changed files with 248 additions and 102 deletions

View File

@@ -43,7 +43,7 @@ export const collection = () =>
hidden_browse: false,
required: false,
options: {
monospace: true,
font: 'monospace',
},
locked: false,
translation: null,

View File

@@ -13,30 +13,33 @@ The HTML `<input>` element supports a huge amount of attributes and events. In o
You can add any custom (text) prefix/suffix to the value in the input using the `prefix` and `suffix` slots.
## 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` |
| `prefix` | Prefix the users value with a value | -- |
| `suffix` | Show a value at the end of the input | -- |
| `slug` | Force the value to be URL safe | `false` |
| `slug-separator` | What character to use as separator in slugs | `-` |
| Prop | Description | Default |
| ---------------- | ------------------------------------------- | ------- |
| `autofocus` | Autofocusses the input on render | `false` |
| `disabled` | Set the disabled state for the input | `false` |
| `full-width` | Render the input with 100% width | `false` |
| `prefix` | Prefix the users value with a value | -- |
| `suffix` | Show a value at the end of the input | -- |
| `slug` | Force the value to be URL safe | `false` |
| `slug-separator` | What character to use as separator in slugs | `-` |
| `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-outer` | Before the input | `{ disabled: boolean, value: string | number; }` |
| `prepend` | In the input, before the value, before the prefix | `{ disabled: boolean, value: string | number; }` |
| `append` | In the input, after the value, after the suffix | `{ disabled: boolean, value: string | number; }` |
| `append-outer` | After the input | `{ disabled: boolean, value: string | number; }` |
## Events
| Events | Description | Value |
|-----------------------|----------------------------------------------|-------|
| --------------------- | -------------------------------------------- | ----- |
| `input` | Updates `v-model` | `any` |
| `click:append` | User clicks on content of inner append slot | -- |
| `click:prepend` | User clicks on content of inner prepend slot | -- |
@@ -46,4 +49,7 @@ Note: all other attached attributes are bound to the input HTMLELement in the co
Note: all other listeners are bound to the input HTMLElement, allowing you to handle everything from `keydown` to `emptied`.
## CSS Variables
n/a
| Variable | Default |
| ----------------------- | -------------------------- |
| `--v-input-font-family` | `var(--family-sans-serif)` |

View File

@@ -1,4 +1,4 @@
import { withKnobs, text } from '@storybook/addon-knobs';
import { withKnobs, text, boolean } from '@storybook/addon-knobs';
import Vue from 'vue';
import VInput from './v-input.vue';
import markdown from './readme.md';
@@ -22,13 +22,21 @@ export default {
export const basic = () =>
defineComponent({
components: { RawValue },
props: {
placeholder: {
default: text('Placeholder', 'Enter a value...', 'Options'),
},
trim: {
default: boolean('Trim', false, 'Options'),
},
},
setup() {
const value = ref(null);
return { value };
},
template: `
<div>
<v-input v-model="value" placeholder="Enter content..." />
<v-input v-model="value" v-bind="{placeholder, trim}" />
<raw-value>{{ value }}</raw-value>
</div>
`,
@@ -42,7 +50,7 @@ export const monospace = () => ({
},
template: `
<div>
<v-input v-model="value" placeholder="Enter content..." monospace />
<v-input v-model="value" placeholder="Enter content..." :style="{'--v-input-font-family': 'var(--family-monospace)'}" />
</div>
`,
});

View File

@@ -54,12 +54,11 @@ describe('Input', () => {
it('Sets the correct classes based on props', async () => {
component.setProps({
disabled: true,
monospace: true,
});
await component.vm.$nextTick();
expect(component.find('.input').classes()).toEqual(['input', 'disabled', 'monospace']);
expect(component.find('.input').classes()).toEqual(['input', 'disabled']);
});
it('Emits just the value for the input event', async () => {

View File

@@ -1,3 +1,4 @@
f
<template>
<div
class="v-input"
@@ -7,7 +8,7 @@
<div v-if="$slots['prepend-outer']" class="prepend-outer">
<slot name="prepend-outer" :value="value" :disabled="disabled" />
</div>
<div class="input" :class="{ disabled, monospace }">
<div class="input" :class="{ disabled }">
<div v-if="$slots.prepend" class="prepend">
<slot name="prepend" :value="value" :disabled="disabled" />
</div>
@@ -53,10 +54,6 @@ export default defineComponent({
type: String,
default: null,
},
monospace: {
type: Boolean,
default: false,
},
fullWidth: {
type: Boolean,
default: false,
@@ -73,6 +70,10 @@ export default defineComponent({
type: String,
default: '-',
},
trim: {
type: Boolean,
default: true,
},
},
setup(props, { emit, listeners }) {
const _listeners = computed(() => ({
@@ -91,6 +92,8 @@ export default defineComponent({
if (props.slug === true) {
value = slugify(value, { separator: props.slugSeparator });
} else if (props.trim === true) {
value = value.trim();
}
emit('input', value);
@@ -101,6 +104,8 @@ export default defineComponent({
<style lang="scss" scoped>
.v-input {
--v-input-font-family: var(--family-sans-serif);
display: flex;
align-items: center;
width: max-content;
@@ -117,6 +122,7 @@ export default defineComponent({
height: 100%;
padding: var(--input-padding);
color: var(--foreground-normal);
font-family: var(--v-input-font-family);
background-color: var(--background-page);
border: var(--border-width) solid var(--border-normal);
border-radius: var(--border-radius);
@@ -158,6 +164,7 @@ export default defineComponent({
flex-grow: 1;
width: 100px; // allows flex to shrink to allow for slots
height: 100%;
font-family: var(--v-input-font-family);
background-color: transparent;
border: none;
appearance: none;
@@ -167,29 +174,6 @@ export default defineComponent({
}
}
&.has-click {
cursor: pointer;
input {
pointer-events: none;
}
}
&.active .input {
color: var(--foreground-normal);
background-color: var(--background-page);
border-color: var(--primary);
}
.input.monospace {
input {
font-family: var(--family-monospace);
}
}
.append-outer {
margin-left: 8px;
}
&.full-width {
width: 100%;
@@ -197,5 +181,26 @@ export default defineComponent({
width: 100%;
}
}
&.has-click {
cursor: pointer;
input {
pointer-events: none;
.prefix,
.suffix {
color: var(--foreground-subdued);
}
}
&.active .input {
color: var(--foreground-normal);
background-color: var(--background-page);
border-color: var(--primary);
}
.append-outer {
margin-left: 8px;
}
}
}
</style>

View File

@@ -23,25 +23,29 @@ Renders a dropdown input.
## Props
| Prop | Description | Default |
|---------------|-----------------------------------------------------|---------|
| `items`* | Items to render in the select | |
| ------------- | --------------------------------------------------- | ------- |
| `items`\* | Items to render in the select | |
| `itemText` | What item value to use for the display text | `text` |
| `itemValue` | What item value to use for the item value | `value` |
| `value` | Currently selected item(s) | |
| `multiple` | Allow multiple items to be selected | `false` |
| `placeholder` | What placeholder to show when no items are selected | |
| `full-width` | Render the select at full width | |
| `monospace` | Render the value and options monospaced | |
| `disabled` | Disable the select | |
## Events
| Event | Description | Value |
|---------|--------------------------|-----------------------------------------|
| ------- | ------------------------ | --------------------------------------- |
| `input` | New value for the select | `(string | number)[] | string | number` |
## Slots
n/a
## CSS Variables
n/a
| Variable | Default |
| ------------------------ | -------------------------- |
| `--v-select-font-family` | `var(--family-sans-serif)` |

View File

@@ -8,7 +8,6 @@
<template #activator="{ toggle }">
<v-input
:full-width="fullWidth"
:monospace="monospace"
readonly
:value="displayValue"
@click="toggle"
@@ -29,7 +28,7 @@
@click="multiple ? null : $emit('input', item.value)"
>
<v-list-item-content>
<span v-if="multiple === false" :class="{ monospace }">{{ item.text }}</span>
<span v-if="multiple === false" class="item-text">{{ item.text }}</span>
<v-checkbox
v-else
:inputValue="value || []"
@@ -86,10 +85,6 @@ export default defineComponent({
type: Boolean,
default: false,
},
monospace: {
type: Boolean,
default: false,
},
allowNull: {
type: Boolean,
default: false,
@@ -147,15 +142,23 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
.monospace {
font-family: var(--family-monospace);
}
.v-select {
--v-select-font-family: var(--family-sans-serif);
.v-input {
cursor: pointer;
font-family: var(--v-select-font-family);
.item-text {
font-family: var(--v-select-font-family);
}
.v-input {
--v-input-font-family: var(--v-select-font-family);
::v-deep input {
cursor: pointer;
::v-deep input {
cursor: pointer;
}
}
}
</style>

View File

@@ -7,17 +7,42 @@ export default defineInterface(({ i18n }) => ({
icon: 'box',
component: InterfaceTextInput,
options: [
{
field: 'monospace',
name: 'Monospace',
width: 'half',
interface: 'switch',
},
{
field: 'placeholder',
name: 'Placeholder',
width: 'half',
interface: 'text-input',
},
{
field: 'iconLeft',
name: 'Icon Left',
width: 'half',
interface: 'icon',
},
{
field: 'iconRight',
name: 'Icon Right',
width: 'half',
interface: 'icon',
},
{
field: 'trim',
name: 'Trim',
width: 'half',
interface: 'switch',
},
{
field: 'font',
name: 'Font',
width: 'half',
interface: 'select-one-dropdown',
options: {
items: [
{ itemText: 'Sans', itemValue: 'sans-serif' },
{ itemText: 'Mono', itemValue: 'monospace' },
{ itemText: 'Serif', itemValue: 'serif' },
],
},
},
],
}));

View File

@@ -1 +1,13 @@
# Text Input
## Options
| Option | Description | Default |
| ------------- | ------------------------------------------------------------------- | ------------ |
| `readonly` | Readonly | `false` |
| `placeholder` | Text to show when no input is entered | `null` |
| `masked` | Mask the value (as in a password field) | `false` |
| `iconLeft` | Icon to appear on the inside left of the input box | `null` |
| `iconRight` | Icon to appear on the inside right of the input box | `null` |
| `trim` | Trim leading and trailing whitespace | `true` |
| `font` | Font to render the value in (`sans-serif`, `serif`, or `monospace`) | `sans-serif` |

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 InterfaceTextInput from './text-input.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-text-input', InterfaceTextInput);
@@ -18,28 +19,49 @@ export default {
export const basic = () =>
defineComponent({
components: { RawValue },
props: {
monospace: {
default: boolean('Monospace', false, 'Options'),
placeholder: {
default: text('Placeholder', 'Enter a value...', 'Options'),
},
masked: {
default: boolean('Masked', false, 'Options'),
},
iconLeft: {
default: text('Icon Left', '', 'Options'),
},
iconRight: {
default: text('Icon Right', '', 'Options'),
},
trim: {
default: boolean('Trim', false, 'Options'),
},
showCharacterCount: {
default: boolean('Show Character Count', false, 'Options'),
font: {
default: optionsKnob(
'Font',
{ Sans: 'sans-serif', Serif: 'serif', Mono: 'monospace' },
'sans',
{ display: 'select' },
'Options'
),
},
placeholder: {
default: text('Placeholder', 'Enter a value...', 'Options'),
disabled: {
default: boolean('Disabled', false, 'Options'),
},
},
setup() {
const onInput = action('input');
return { onInput };
const value = ref(null);
return { onInput, value };
},
template: `
<div>
<interface-text-input
:options="{ monospace, trim, showCharacterCount, placeholder }"
v-model="value"
v-bind="{ placeholder, masked, iconLeft, iconRight, trim, font, disabled }"
@input="onInput"
/>
<raw-value>{{ value }}</raw-value>
</div>
`,
});

View File

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

View File

@@ -1,16 +1,21 @@
<template>
<v-input
:monospace="monospace"
:value="value"
:placeholder="placeholder"
:disabled="disabled"
:trim="trim"
:type="masked ? 'password' : 'text'"
:class="font"
full-width
@input="$listeners.input"
/>
>
<template v-if="iconLeft" #prepend><v-icon :name="iconLeft" /></template>
<template v-if="iconRight" #append><v-icon :name="iconRight" /></template>
</v-input>
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
import { defineComponent, PropType } from '@vue/composition-api';
export default defineComponent({
props: {
@@ -22,14 +27,46 @@ export default defineComponent({
type: Boolean,
default: false,
},
monospace: {
type: Boolean,
default: false,
},
placeholder: {
type: String,
default: null,
},
masked: {
type: Boolean,
default: false,
},
iconLeft: {
type: String,
default: null,
},
iconRight: {
type: String,
default: null,
},
trim: {
type: Boolean,
default: true,
},
font: {
type: String as PropType<'sans-serif' | 'serif' | 'monospace'>,
default: 'sans-serif',
},
},
});
</script>
<style lang="scss" scoped>
.v-input {
.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>

View File

@@ -33,12 +33,12 @@
<v-tab-item value="collection">
<h2 class="type-title">{{ $t('creating_collection_info') }}</h2>
<div class="type-label">{{ $t('name') }}</div>
<v-input full-width monospace v-model="collectionName" />
<v-input full-width class="monospace" v-model="collectionName" />
<v-divider />
<div class="grid">
<div>
<div class="type-label">{{ $t('primary_key_field') }}</div>
<v-input full-width monospace v-model="primaryKeyFieldName" />
<v-input full-width class="monospace" v-model="primaryKeyFieldName" />
</div>
<div>
<div class="type-label">{{ $t('type') }}</div>
@@ -68,7 +68,7 @@
<div class="grid system">
<div class="field" v-for="field in systemFields" :key="field.id">
<div class="type-label">{{ $t(field.label) }}</div>
<v-input v-model="field.name" monospace>
<v-input v-model="field.name" class="monospace">
<template #prepend>
<v-checkbox v-model="field.enabled" />
</template>
@@ -422,4 +422,8 @@ export default defineComponent({
.spacer {
flex-grow: 1;
}
.v-input.monospace {
--v-input-font-family: var(--family-monospace);
}
</style>

View File

@@ -41,10 +41,15 @@
<v-card-title>{{ $t('duplicate_where_to') }}</v-card-title>
<v-card-text>
<span class="label">{{ $tc('collection', 0) }}</span>
<v-select monospace :items="collections" v-model="duplicateTo" full-width />
<v-select
class="monospace"
:items="collections"
v-model="duplicateTo"
full-width
/>
<span class="label">{{ $tc('field', 0) }}</span>
<v-input monospace v-model="duplicateName" full-width />
<v-input class="monospace" v-model="duplicateName" full-width />
</v-card-text>
<v-card-actions>
<v-button secondary @click="duplicateActive = false">
@@ -232,6 +237,10 @@ export default defineComponent({
--background-page: var(--background-subdued);
}
.v-input.monospace {
--v-input-font-family: var(--family-monospace);
}
.v-icon {
--v-icon-color: var(--foreground-subdued);
}

View File

@@ -119,7 +119,7 @@ export default defineComponent({
interface: 'text-input',
width: 'half',
options: {
monospace: true,
font: 'monospace',
},
},
{

View File

@@ -21,7 +21,7 @@
</div>
<div class="field">
<div class="type-label label">{{ $t('db_name') }}</div>
<v-input monospace full-width v-model="_value.db_name" />
<v-input full-width v-model="_value.db_name" class="db" />
</div>
<div class="field">
<div class="type-label label">{{ $t('db_type') }}</div>
@@ -85,3 +85,9 @@ export default defineComponent({
},
});
</script>
<style lang="scss" scoped>
.v-input.db {
--v-input-font-family: var(--family-monospace);
}
</style>

View File

@@ -9,7 +9,7 @@
</div>
<div class="field">
<div class="type-label label">{{ $t('project_key') }}</div>
<v-input slug monospace full-width v-model="_value.project" />
<v-input slug full-width v-model="_value.project" class="key" />
</div>
<div class="field">
<div class="type-label label">{{ $t('admin_email') }}</div>
@@ -88,3 +88,9 @@ export default defineComponent({
},
});
</script>
<style lang="scss" scoped>
.v-input.key {
--v-input-font-family: var(--family-monospace);
}
</style>

View File

@@ -14,8 +14,8 @@
@input="setToken"
:value="token"
:placeholder="$t('super_admin_token')"
monospace
full-width
class="token"
>
<template #append>
<v-progress-circular indeterminate v-if="verifying" />
@@ -94,4 +94,8 @@ export default defineComponent({
.v-input {
margin-top: 32px;
}
.v-input.token {
--v-input-font-family: var(--family-monospace);
}
</style>