Interface icon (#436)

* icon interface first pass

* icon test

* readme lol

* Fix top placement in attached mode

* Finish icon interface

* Add some padding

* Finishing touches

* Fix tests

Co-authored-by: rijkvanzanten <rijkvanzanten@me.com>
This commit is contained in:
Jacob Rienstra
2020-04-30 23:55:11 -04:00
committed by GitHub
parent 27c7c055da
commit 78951da9de
10 changed files with 1340 additions and 8 deletions

View File

@@ -1,4 +1,4 @@
<template functional>
<template>
<div class="v-list-item-content">
<slot></slot>
</div>

View File

@@ -28,7 +28,7 @@
class="v-menu-content"
@click="onContentClick"
>
<slot />
<slot :active="isActive" />
</div>
</div>
</div>
@@ -139,9 +139,9 @@ export default defineComponent({
isActive.value = false;
}
function toggle() {
function toggle(newActive = !isActive.value) {
if (props.disabled === true) return;
isActive.value = !isActive.value;
isActive.value = newActive;
}
}
@@ -335,10 +335,20 @@ body {
}
&.attached {
.v-menu-content {
border-top: none;
border-top-left-radius: 0;
border-top-right-radius: 0;
&[data-placement^='top'] {
.v-menu-content {
border-bottom: none;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
}
&[data-placement^='bottom'] {
.v-menu-content {
border-top: none;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
}
}
}

View File

@@ -0,0 +1,42 @@
import { withKnobs, boolean } from '@storybook/addon-knobs';
import Vue from 'vue';
import InterfaceIcon from './icon.vue';
import RawValue from '../../../.storybook/raw-value.vue';
import { i18n } from '@/lang';
import VueRouter from 'vue-router';
import markdown from './readme.md';
import withPadding from '../../../.storybook/decorators/with-padding';
import { defineComponent, ref } from '@vue/composition-api';
Vue.component('interface-icon', InterfaceIcon);
export default {
title: 'Interfaces / Icon',
decorators: [withKnobs, withPadding],
parameters: {
notes: markdown,
},
};
export const basic = () =>
defineComponent({
i18n,
router: new VueRouter(),
components: { InterfaceIcon, RawValue },
props: {
disabled: {
default: boolean('disabled', false, 'Options'),
},
},
setup() {
const value = ref('');
return { value };
},
template: `
<div style="width: 300px">
<interface-icon v-model="value" :disabled="disabled" />
<raw-value>{{value}}</raw-value>
</div>
`,
});

View File

@@ -0,0 +1,38 @@
import VueCompositionAPI from '@vue/composition-api';
import { mount, createLocalVue } from '@vue/test-utils';
import InterfaceIcon from './icon.vue';
import ClickOutside from '@/directives/click-outside/';
import Tooltip from '@/directives/tooltip/tooltip';
import Focus from '@/directives/focus/focus';
import TransitionExpand from '@/components/transition/expand';
import VInput from '@/components/v-input';
import VIcon from '@/components/v-icon';
import VMenu from '@/components/v-menu';
import VDivider from '@/components/v-divider';
const localVue = createLocalVue();
localVue.use(VueCompositionAPI);
localVue.component('v-icon', VIcon);
localVue.component('v-input', VInput);
localVue.component('v-menu', VMenu);
localVue.directive('click-outside', ClickOutside);
localVue.directive('tooltip', Tooltip);
localVue.directive('focus', Focus);
localVue.component('transition-expand', TransitionExpand);
localVue.component('v-divider', VDivider);
describe('Interfaces / Icon', () => {
it('Renders a v-icon', async () => {
const component = mount(InterfaceIcon, {
localVue,
mocks: {
$t: jest.fn(),
},
});
expect(component.find(VMenu).exists()).toBe(true);
component.find(VInput).find('input').setValue('search');
await component.vm.$nextTick();
expect(component.find(VIcon).text()).toBe('search');
});
});

View File

@@ -0,0 +1,132 @@
<template>
<v-menu attached :disabled="disabled" close-on-content-click>
<template #activator="{ toggle, active }">
<v-input
:disabled="disabled"
:placeholder="value || $t('search_for_icon')"
v-model="searchQuery"
@focus="toggle(true)"
>
<template #prepend>
<v-icon :name="value" :class="{ active: value }" />
</template>
<template #append>
<v-icon name="expand_more" class="open-indicator" :class="{ open: active }" />
</template>
</v-input>
</template>
<div class="content" :class="width">
<template v-for="(group, index) in filteredIcons">
<div :key="'icon-group-' + group.name" class="icons" v-if="group.icons.length > 0">
<v-icon
v-for="icon in group.icons"
:key="icon"
:name="icon"
:class="{ active: icon === value }"
@click="setIcon(icon)"
/>
</div>
<v-divider
:key="'divider-' + group.name"
v-if="group.icons.length > 0 && index !== filteredIcons.length - 1"
/>
</template>
</div>
</v-menu>
</template>
<script lang="ts">
import icons from './icons.json';
import { defineComponent, ref, computed } from '@vue/composition-api';
export default defineComponent({
props: {
value: {
type: String,
default: 'search',
},
disabled: {
type: Boolean,
default: false,
},
width: {
type: String,
default: 'half',
},
},
setup(props, { emit }) {
const searchQuery = ref('');
const filteredIcons = computed(() => {
return icons.map((group) => {
if (searchQuery.value.length === 0) return group;
const icons = group.icons.filter((icon) =>
icon.includes(searchQuery.value.toLowerCase())
);
return {
...group,
icons: icons,
length: icons.length,
};
});
});
return {
icons,
setIcon,
searchQuery,
filteredIcons,
};
function setIcon(icon: string) {
emit('input', icon);
}
},
});
</script>
<style lang="scss" scoped>
.content {
padding: 8px;
.v-icon:hover {
color: var(--foreground-normal);
}
.v-icon.active {
color: var(--primary);
}
.v-divider {
--v-divider-color: var(--background-normal);
margin: 0 22px;
}
}
.icons {
display: grid;
grid-gap: 8px;
grid-template-columns: repeat(8, 1fr);
justify-content: center;
padding: 20px 0;
color: var(--foreground-subdued);
}
.full .icons {
grid-template-columns: repeat(18, 1fr);
}
.open-indicator {
transform: scaleY(1);
transition: transform var(--fast) var(--transition);
}
.open-indicator.open {
transform: scaleY(-1);
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,17 @@
import { defineInterface } from '../define';
import InterfaceIcon from './icon.vue';
export default defineInterface(({ i18n }) => ({
id: 'icon',
name: i18n.t('interfaces.icon.icon'),
icon: 'insert_emoticon',
component: InterfaceIcon,
options: [
{
field: 'iconColor',
name: 'Icon Color',
width: 'half',
interface: 'color',
},
],
}));

View File

@@ -0,0 +1,7 @@
# Icon Interface
## Options
| Option | Description | Default |
| ---------- | ----------- | ------- |
| `readonly` | Readonly | `false` |

View File

@@ -12,6 +12,7 @@ import InterfaceCheckboxes from './checkboxes';
import InterfaceStatus from './status';
import InterfaceDateTime from './datetime';
import InterfaceImage from './image';
import InterfaceIcon from './icon';
export const interfaces = [
InterfaceTextInput,
@@ -28,6 +29,7 @@ export const interfaces = [
InterfaceStatus,
InterfaceDateTime,
InterfaceImage,
InterfaceIcon,
];
export default interfaces;

View File

@@ -295,6 +295,8 @@
"has": "Contains some of these keys"
},
"search_for_icon": "Search for icon...",
"drop_to_upload": "Drop to Upload",
"upload_file_indeterminate": "Uploading File...",
"upload_file_success": "File Uploaded",