Skip to content

Commit

Permalink
feat(ui): add filter to BuilderSidebarToolbar. WF-42
Browse files Browse the repository at this point in the history
I introduced a component named `BuilderSidebarTitleSearch` which extract the existing search logic of `BuilderSidebarTree` and use it for `BuilderSidebarToolbar`.

I also took the opportunity to improve the accessibility since we use `<i>` elements as button, but they're not focusable and clickable with the keyboard.

And finally, I introduced an E2E test which cover the new feature.
  • Loading branch information
madeindjs committed Aug 15, 2024
1 parent 8a80db3 commit 1fc79b6
Show file tree
Hide file tree
Showing 4 changed files with 240 additions and 92 deletions.
99 changes: 99 additions & 0 deletions src/ui/src/builder/BuilderSidebarTitleSearch.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<template>
<div v-if="isSearchActive" class="BuilderSidebarTitleSearch">
<input
ref="searchInput"
v-model="searchQuery"
type="text"
placeholder="Search..."
/>
<slot />
<i
:class="{ disabled: disabled }"
class="searchIcon material-symbols-outlined"
title="Close"
tabindex="0"
@keydown.enter="toggleSearch"
@click="toggleSearch"
>
close
</i>
</div>
<div v-else class="BuilderSidebarTitleSearch">
<i class="material-symbols-outlined">{{ icon }}</i>
<h3>{{ title }}</h3>
<i
title="Search"
class="searchIcon material-symbols-outlined"
tabindex="0"
@keydown.enter="toggleSearch"
@click="toggleSearch"
>
search
</i>
</div>
</template>

<script setup lang="ts">
import { nextTick, ref, Ref } from "vue";
defineProps({
icon: { type: String, required: true },
title: { type: String, required: true },
disabled: { type: Boolean, required: false },
});
const searchQuery = defineModel({ type: String, default: "" });
defineEmits({});
const searchInput: Ref<HTMLInputElement> = ref(null);
const isSearchActive: Ref<boolean> = ref(false);
async function toggleSearch() {
isSearchActive.value = !isSearchActive.value;
if (isSearchActive.value) {
await nextTick();
searchInput.value.focus();
} else {
searchQuery.value = "";
}
}
</script>

<style scoped>
@import "./sharedStyles.css";
.BuilderSidebarTitleSearch {
display: flex;
gap: 8px;
align-items: center;
font-size: 1rem;
background: var(--builderBackgroundColor);
padding: 16px;
top: 0;
position: sticky;
font-size: 1rem;
}
.BuilderSidebarTitleSearch h3 {
font-weight: 500;
font-size: 0.875rem;
flex-grow: 1;
}
.BuilderSidebarTitleSearch .searchIcon {
cursor: pointer;
}
.BuilderSidebarTitleSearch .searchIcon.disabled {
color: var(--builderDisabledColor);
}
.BuilderSidebarTitleSearch input {
outline: 0;
border: 0;
flex-grow: 1;
width: 50%;
}
</style>
74 changes: 48 additions & 26 deletions src/ui/src/builder/BuilderSidebarToolbar.vue
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
<template>
<div class="BuilderSidebarToolbar">
<div class="sectionTitle">
<i class="material-symbols-outlined"> handyman </i>
<h3>Toolkit</h3>
</div>
<BuilderSidebarTitleSearch
v-model="searchQuery"
title="Toolkit"
icon="handyman"
/>
<div class="categories">
<template
v-for="(categoryData, category) in categoriesData"
:key="category"
>
<div v-if="categoryData.isVisible !== false" class="category">
<div v-if="shouldDisplayCategory(category)" class="category">
<div class="title">
<i class="material-symbols-outlined">{{
categoryData.icon ?? "question_mark"
}}</i>
<h4>{{ category }}</h4>

<div
<button
class="drop-arrow"
@click="toggleCollapseCategory(category)"
>
Expand All @@ -27,10 +28,14 @@
: "expand_less"
}}
</i>
</div>
</button>
</div>

<div v-show="!categoryData.isCollapsed" class="components">
<div
v-show="!categoryData.isCollapsed"
class="components"
:aria-expanded="!categoryData.isCollapsed"
>
<div
v-for="(
definition, type
Expand Down Expand Up @@ -63,18 +68,23 @@ import { Ref, computed, inject, ref } from "vue";
import { useDragDropComponent } from "./useDragDropComponent";
import injectionKeys from "../injectionKeys";
import { Component, WriterComponentDefinition } from "../writerTypes";
import BuilderSidebarTitleSearch from "./BuilderSidebarTitleSearch.vue";
const wf = inject(injectionKeys.core);
const ssbm = inject(injectionKeys.builderManager);
const { removeInsertionCandidacy } = useDragDropComponent(wf);
type CategoryId = "Root" | "Layout" | "Content" | "Input" | "Embed" | "Other";
type CategoryData = {
isVisible?: boolean;
isCollapsed?: boolean;
icon?: string;
};
const categoriesData: Ref<Record<string, CategoryData>> = ref({
const searchQuery = ref("");
const categoriesData: Ref<Record<CategoryId, CategoryData>> = ref({
Root: {
isVisible: false,
},
Expand All @@ -100,20 +110,37 @@ const categoriesData: Ref<Record<string, CategoryData>> = ref({
},
});
function shouldDisplayCategory(categoryId: CategoryId): boolean {
if (categoriesData.value[categoryId]?.isVisible === false) return false;
return (
Object.keys(definitionsByDisplayCategory.value[categoryId] ?? {})
.length > 0
);
}
function toggleCollapseCategory(categoryId: string) {
const categoryData = categoriesData.value[categoryId];
categoryData.isCollapsed = !categoryData.isCollapsed;
}
const definitionsByDisplayCategory = computed(() => {
const types = wf.getSupportedComponentTypes();
const result: Record<
string,
Record<string, WriterComponentDefinition>
const result: Partial<
Record<CategoryId, Record<string, WriterComponentDefinition>>
> = {};
types.map((type) => {
const definition = wf.getComponentDefinition(type);
const matchingSearch =
searchQuery.value === "" ||
definition.name
.toLocaleLowerCase()
.includes(searchQuery.value.toLocaleLowerCase());
if (!matchingSearch) return;
const isMatch = Object.keys(categoriesData.value).includes(
definition.category,
);
Expand Down Expand Up @@ -149,20 +176,6 @@ const handleDragEnd = (ev: DragEvent) => {
font-size: 0.7rem;
}
.BuilderSidebarToolbar > .sectionTitle {
padding: 16px;
position: sticky;
top: 0;
background: var(--builderBackgroundColor);
font-size: 0.875rem;
height: 40px;
}
h3 {
font-weight: 500;
font-size: 0.875rem;
}
.categories {
padding: 0 12px 12px 12px;
flex: 1 1 auto;
Expand Down Expand Up @@ -201,6 +214,14 @@ h3 {
}
.drop-arrow {
/* reset button default */
border: none;
background-color: unset;
padding: 0;
margin: 0;
height: auto;
width: auto;
border-radius: 50%;
min-width: 24px;
min-height: 24px;
Expand All @@ -212,5 +233,6 @@ h3 {
.drop-arrow:hover {
background: var(--builderSubtleSeparatorColor);
color: unset;
}
</style>
79 changes: 13 additions & 66 deletions src/ui/src/builder/BuilderSidebarTree.vue
Original file line number Diff line number Diff line change
@@ -1,23 +1,11 @@
<template>
<div class="BuilderSidebarTree">
<div v-if="!isSearchActive" class="sectionTitle">
<i class="material-symbols-outlined"> account_tree </i>
<h3>Component Tree</h3>
<i
title="Search"
class="searchIcon material-symbols-outlined"
@click="toggleSearch"
>
search
</i>
</div>
<div v-if="isSearchActive" class="sectionTitle">
<input
ref="searchInput"
v-model="searchQuery"
type="text"
placeholder="Search..."
/>
<BuilderSidebarTitleSearch
v-model="searchQuery"
title="Component Tree"
icon="account_tree"
:disabled="!matchAvailable"
>
<i
:class="{ disabled: !matchAvailable }"
class="searchIcon material-symbols-outlined"
Expand All @@ -26,6 +14,8 @@
? `Go to match ${previousMatchIndex + 1} of ${matchingComponents.length}`
: `Previous match`
"
tabindex="0"
@keydown.enter="goToPreviousMatch"
@click="goToPreviousMatch"
>
navigate_before
Expand All @@ -38,19 +28,13 @@
? `Go to match ${nextMatchIndex + 1} of ${matchingComponents.length}`
: `Next match`
"
tabindex="0"
@keydown.enter="goToNextMatch"
@click="goToNextMatch"
>
navigate_next
</i>
<i
:class="{ disabled: !matchAvailable }"
class="searchIcon material-symbols-outlined"
title="Close"
@click="toggleSearch"
>
close
</i>
</div>
</BuilderSidebarTitleSearch>
<div ref="componentTree" class="components">
<div
v-for="component in rootComponents"
Expand Down Expand Up @@ -79,31 +63,20 @@ import BuilderTreeBranch from "./BuilderTreeBranch.vue";
import injectionKeys from "../injectionKeys";
import { Component } from "../writerTypes";
import { watch } from "vue";
import BuilderSidebarTitleSearch from "./BuilderSidebarTitleSearch.vue";
const wf = inject(injectionKeys.core);
const ssbm = inject(injectionKeys.builderManager);
const { createAndInsertComponent, goToComponentParentPage } =
useComponentActions(wf, ssbm);
const searchInput: Ref<HTMLInputElement> = ref(null);
const isSearchActive: Ref<boolean> = ref(false);
const searchQuery: Ref<string> = ref(null);
const matchIndex: Ref<number> = ref(-1);
const rootComponents = computed(() => {
return wf.getComponents(null, { sortedByPosition: true });
});
async function toggleSearch() {
isSearchActive.value = !isSearchActive.value;
if (isSearchActive.value) {
await nextTick();
searchInput.value.focus();
} else {
searchQuery.value = null;
}
}
function determineMatch(component: Component, query: string): boolean {
if (component.id.toLocaleLowerCase().includes(query)) return true;
if (component.type.toLocaleLowerCase().includes(query)) return true;
Expand Down Expand Up @@ -158,7 +131,6 @@ function goToNextMatch() {
}
const matchingComponents: ComputedRef<Component[]> = computed(() => {
if (!isSearchActive.value) return;
if (!searchQuery.value) return;
const query = searchQuery.value.toLocaleLowerCase();
const components = wf.getComponents();
Expand Down Expand Up @@ -192,35 +164,10 @@ async function addPage() {
flex-direction: column;
}
.sectionTitle {
background: var(--builderBackgroundColor);
padding: 16px;
top: 0;
position: sticky;
font-size: 1rem;
}
.sectionTitle h3 {
font-weight: 500;
font-size: 0.875rem;
flex-grow: 1;
}
.sectionTitle .searchIcon {
cursor: pointer;
}
.sectionTitle .searchIcon.disabled {
.searchIcon.disabled {
color: var(--builderDisabledColor);
}
.sectionTitle input {
outline: 0;
border: 0;
flex-grow: 1;
width: 50%;
}
.components {
padding: 0 12px 12px 12px;
}
Expand Down
Loading

0 comments on commit 1fc79b6

Please sign in to comment.