Skip to content

Commit

Permalink
Create nested example (#10)
Browse files Browse the repository at this point in the history
* Refactor context and types

* Update changelog

* Update demos

* Create nested example
  • Loading branch information
kadiryazici authored Oct 13, 2022
1 parent 94ca726 commit 2d466bb
Show file tree
Hide file tree
Showing 5 changed files with 357 additions and 0 deletions.
2 changes: 2 additions & 0 deletions playground/examples/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { Example } from '../types';
import keyboardExample from './keyboard';
import nestedExample from './nested';

export default [
keyboardExample, //
nestedExample,
] as Example[];
225 changes: 225 additions & 0 deletions playground/examples/nested/ItemRenderer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
<script lang="ts">
import {
computed,
inject,
onMounted,
onUnmounted,
provide,
ref,
shallowRef,
type InjectionKey,
} from 'vue';
import type { Item, ItemGroup, CustomItem } from '../../../src';
import { Wowerlay } from 'wowerlay';
import useKey from '../../composables/useKey';
import { SelectableItems, type Context, filterSelectableItems } from '../../../src/';
import IconChevronRight from 'virtual:icons/carbon/chevron-right';
import ItemRenderer from './ItemRenderer.vue';
export type ItemMetaWithChildren = {
children?: (Item<ItemMetaWithChildren> | ItemGroup | CustomItem<ItemMetaWithChildren>)[];
text: string;
};
export const InjectKey: InjectionKey<boolean> = Symbol();
const allowedInputTypes = ['email', 'password', 'text', 'number', 'url', 'time', 'tel'];
export const isInputing = () =>
document.activeElement instanceof HTMLTextAreaElement ||
document.activeElement instanceof HTMLSelectElement ||
(document.activeElement instanceof HTMLInputElement &&
allowedInputTypes.includes(document.activeElement.type));
export const isFocusedOnBlackListedElement = () =>
document.activeElement?.getAttribute('role') === 'button' ||
document.activeElement instanceof HTMLButtonElement ||
document.activeElement instanceof HTMLAnchorElement ||
document.activeElement instanceof HTMLTextAreaElement ||
document.activeElement instanceof HTMLSelectElement ||
(document.activeElement instanceof HTMLInputElement &&
!allowedInputTypes.includes(document.activeElement.type));
export default {
inheritAttrs: false,
};
</script>

<script lang="ts" setup>
const props = withDefaults(
defineProps<{
items?: (Item<ItemMetaWithChildren> | ItemGroup | CustomItem<ItemMetaWithChildren>)[];
preventCloseOnSelect?: boolean;
}>(),
{
items: () => [],
preventCloseOnSelect: false,
},
);
const child = inject(InjectKey, false);
provide(InjectKey, true);
const emit = defineEmits<{
(e: 'close'): void;
(e: 'closeSelf'): void;
}>();
const focusedElement = ref<null | HTMLElement>(null);
const expand = ref(false);
const focusedItem = shallowRef<Item<ItemMetaWithChildren> | undefined>();
const focusedItemHasChildren = computed(() => {
if (focusedItem.value == null) return false;
const meta = focusedItem.value.meta;
return meta && Array.isArray(meta.children) && meta.children.length > 0;
});
function setupHandler(ctx: Context) {
useKey('esc', () => emit('close'));
useKey('up', ctx.focusPrevious, { input: true, repeat: true, prevent: true });
useKey('down', ctx.focusNext, { input: true, repeat: true, prevent: true });
useKey(
'left',
(event) => {
if (child) {
event.preventDefault();
emit('closeSelf');
}
},
{ input: true },
);
useKey(
'right',
(event) => {
const item = ctx.getFocusedItem<ItemMetaWithChildren>();
if (item && Array.isArray(item.meta?.children)) {
event.preventDefault();
expand.value = true;
}
},
{ input: true },
);
useKey(
'enter',
() => {
if (isFocusedOnBlackListedElement()) {
ctx.setFocusByKey();
return;
}
// We return if focused item is selectable item because by pressing enter
// users fire @click event and it triggeres selection
// so it selects twice.
if (document.activeElement?.hasAttribute('data-vue-selectable-items-item')) return;
ctx.selectFocusedElement();
},
{ input: true },
);
ctx.onFocus((_meta, item, el) => {
focusedElement.value = el;
focusedItem.value = item;
if (!isInputing()) {
el.focus();
} else {
el.scrollIntoView({
block: 'center',
inline: 'center',
});
}
});
const expandIfHasChildren = (meta: ItemMetaWithChildren) => {
if (meta && Array.isArray(meta.children) && filterSelectableItems(meta.children).length > 0) {
expand.value = true;
}
};
ctx.onHover((meta) => {
expandIfHasChildren(meta);
});
ctx.onSelect((meta: ItemMetaWithChildren) => {
expandIfHasChildren(meta);
if (!props.preventCloseOnSelect && !meta.children) emit('close');
});
ctx.onUnfocus(() => {
focusedElement.value = null;
focusedItem.value = undefined;
expand.value = false;
});
ctx.onDOMFocus((meta, item, el) => {
ctx.setFocusByKey(item.key);
});
}
const ctx = shallowRef<Context | null>(null);
function focusInHandler() {
const el = document.activeElement;
if (el == null) {
ctx.value?.clearFocus();
return;
}
if (
el.matches('[data-vue-selectable-items-item]') ||
el.querySelector('[data-vue-selectable-items-item]') ||
isInputing()
) {
return;
}
ctx.value?.clearFocus();
}
onMounted(() => window.addEventListener('focusin', focusInHandler));
onUnmounted(() => window.removeEventListener('focusin', focusInHandler));
</script>

<template>
<SelectableItems
v-bind="$attrs"
:setup="setupHandler"
:items="props.items"
ref="ctx"
>
<template #render="{ text, children }: ItemMetaWithChildren">
{{ text }}
<IconChevronRight
style="transform: translateX(50%); margin-left: 10px; font-size: 10px"
v-if="!!children"
/>
</template>
</SelectableItems>

<Wowerlay
v-if="focusedItemHasChildren"
:target="focusedElement"
v-model:visible="expand"
noBackground
position="right-start"
:horizontalGap="-3"
:transition="false"
>
<ItemRenderer
@closeSelf="expand = false"
:items="focusedItem!.meta!.children!"
@close="
() => {
expand = false;
$emit('close');
}
"
/>
</Wowerlay>
</template>
13 changes: 13 additions & 0 deletions playground/examples/nested/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { Example } from '../../types';
import { nanoid } from 'nanoid';
import { defineAsyncComponent } from 'vue';
import code from './index.vue?raw';

const nestedExample: Example = {
id: nanoid(),
title: 'Nested Items',
component: defineAsyncComponent(() => import('./index.vue')),
code,
};

export default nestedExample;
116 changes: 116 additions & 0 deletions playground/examples/nested/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<script lang="ts" setup>
import { item } from '../../../src';
import ItemRenderer, { type ItemMetaWithChildren } from './ItemRenderer.vue';
import { Wowerlay } from 'wowerlay';
import { ref } from 'vue';
const itemOptions = {
elementTag: 'button',
elementAttrs: {
tabindex: 0,
style: {
outline: 'none',
},
},
};
const items = [
item<ItemMetaWithChildren>({
meta: { text: 'Washington' },
key: 'washington',
...itemOptions,
}),
item<ItemMetaWithChildren>({
meta: {
text: 'New York City',
children: [
item<ItemMetaWithChildren>({
meta: { text: 'Momentos' },
key: 'inner-0',
...itemOptions,
}),
item<ItemMetaWithChildren>({
meta: {
text: 'Momentos',
children: [
item<ItemMetaWithChildren>({
meta: { text: 'Momentos' },
key: 'inner-0',
...itemOptions,
}),
item<ItemMetaWithChildren>({
meta: { text: 'Momentos' },
key: 'inner-1',
...itemOptions,
}),
item<ItemMetaWithChildren>({
meta: { text: 'Momentos' },
key: 'inner-2',
...itemOptions,
}),
],
},
key: 'inner-1',
...itemOptions,
}),
item<ItemMetaWithChildren>({
meta: { text: 'Momentos' },
key: 'inner-2',
...itemOptions,
}),
],
},
key: 'nwc',
...itemOptions,
}),
item<ItemMetaWithChildren>({
meta: { text: 'Istanbul' },
key: 'Istanbul',
...itemOptions,
}),
item<ItemMetaWithChildren>({
meta: { text: 'BMW' },
key: 'bmw',
onSelect: () => console.log('Bremın how are you'),
...itemOptions,
}),
];
const target = ref<HTMLElement>();
</script>

<template>
<p style="margin-bottom: 20px; color: var(--txt-2)">
Navigate with ArrowUp and ArrowDown and select with Enter
</p>

<div style="text-align: center">
<input
class="input"
type="text"
placeholder="test-input"
/>

<button
style="margin-left: 10px"
class="btn"
>
Clicko
</button>
</div>

<br /><br />

<div ref="target"></div>
<Wowerlay
noBackground
:visible="true"
position="bottom"
:target="target"
:transition="false"
>
<ItemRenderer :items="items" />
</Wowerlay>
</template>

<style lang="scss" scoped></style>
1 change: 1 addition & 0 deletions playground/main.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createApp } from 'vue';
import './styles/index.scss';
import 'wowerlay/dist/style.css';
import App from './App.vue';
import examples from './examples';

Expand Down

0 comments on commit 2d466bb

Please sign in to comment.