mirror of
https://github.com/directus/directus.git
synced 2026-02-11 14:05:14 -05:00
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:
@@ -1,4 +1,4 @@
|
||||
<template functional>
|
||||
<template>
|
||||
<div class="v-list-item-content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
42
src/interfaces/icon/icon.story.ts
Normal file
42
src/interfaces/icon/icon.story.ts
Normal 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>
|
||||
`,
|
||||
});
|
||||
38
src/interfaces/icon/icon.test.ts
Normal file
38
src/interfaces/icon/icon.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
132
src/interfaces/icon/icon.vue
Normal file
132
src/interfaces/icon/icon.vue
Normal 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>
|
||||
1082
src/interfaces/icon/icons.json
Normal file
1082
src/interfaces/icon/icons.json
Normal file
File diff suppressed because it is too large
Load Diff
17
src/interfaces/icon/index.ts
Normal file
17
src/interfaces/icon/index.ts
Normal 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',
|
||||
},
|
||||
],
|
||||
}));
|
||||
7
src/interfaces/icon/readme.md
Normal file
7
src/interfaces/icon/readme.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Icon Interface
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Description | Default |
|
||||
| ---------- | ----------- | ------- |
|
||||
| `readonly` | Readonly | `false` |
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user