From 7ea59a521d9272ff528b490b1daf0a75dfa725a7 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 10 May 2022 19:53:22 +0100 Subject: [PATCH] Add a few UX improvements to adding component to allow control using the keyboard --- .../new/_components/NewComponentPanel.svelte | 129 +++++++++++++----- 1 file changed, 98 insertions(+), 31 deletions(-) diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/new/_components/NewComponentPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/new/_components/NewComponentPanel.svelte index 9dce541752..a53ce2fabc 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/new/_components/NewComponentPanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/new/_components/NewComponentPanel.svelte @@ -13,21 +13,40 @@ notifications, } from "@budibase/bbui" import structure from "./componentStructure.json" - import { store } from "builderStore" + import { store, selectedComponent } from "builderStore" import { onMount } from "svelte" let section = "components" let searchString let searchRef + let selectedIndex + let componentList = [] + $: currentDefinition = store.actions.components.getDefinition( + $selectedComponent?._component + ) $: enrichedStructure = enrichStructure(structure, $store.components) $: filteredStructure = filterStructure( enrichedStructure, section, + currentDefinition, searchString ) $: blocks = enrichedStructure.find(x => x.name === "Blocks").children + $: orderMap = createComponentOrderMap(componentList) + // Creates a simple lookup map from an array, so we can find the selected + // component much faster + const createComponentOrderMap = list => { + let map = {} + list.forEach((component, idx) => { + map[component] = idx + }) + return map + } + + // Parses the structure in the manifest and returns an enriched structure with + // explicit categories const enrichStructure = (structure, definitions) => { let enrichedStructure = [] structure.forEach(item => { @@ -50,51 +69,86 @@ return enrichedStructure } - const filterStructure = (structure, section, search) => { + const filterStructure = (structure, section, currentDefinition, search) => { + selectedIndex = search ? 0 : null + componentList = [] if (!structure?.length) { return [] } + + // Split list into either components or blocks initially if (section === "components") { structure = structure.filter(category => category.name !== "Blocks") } else { structure = structure.filter(category => category.name === "Blocks") } - if (search) { - let filteredStructure = [] - structure.forEach(category => { - let matchedChildren = category.children.filter(child => { - return child.name.toLowerCase().includes(search.toLowerCase()) - }) - if (matchedChildren.length) { - filteredStructure.push({ - ...category, - children: matchedChildren, - }) + + // Return only items which match the search string + let filteredStructure = [] + structure.forEach(category => { + let matchedChildren = category.children.filter(child => { + const name = child.name.toLowerCase() + + // Check if the component matches the search string + if (search && !name.includes(search.toLowerCase())) { + return false } + + // Check if the component is allowed as a child + return !currentDefinition?.illegalChildren?.includes(name) }) - structure = filteredStructure - } + if (matchedChildren.length) { + filteredStructure.push({ + ...category, + children: matchedChildren, + }) + + // Create a flat list of all components so that we can reference them by + // order later + componentList = componentList.concat( + matchedChildren.map(x => x.component) + ) + } + }) + structure = filteredStructure return structure } - const isChildAllowed = ({ name }, selectedComponent) => { - const currentComponent = store.actions.components.getDefinition( - selectedComponent?._component - ) - return currentComponent?.illegalChildren?.includes(name.toLowerCase()) - } - - const addComponent = async item => { + const addComponent = async component => { try { - await store.actions.components.create(item.component) + await store.actions.components.create(component) $goto("../") } catch (error) { notifications.error("Error creating component") } } + const handleKeyDown = e => { + if (e.key === "Tab") { + // Cycle selected components on tab press + if (selectedIndex == null) { + selectedIndex = 0 + } else { + selectedIndex = (selectedIndex + 1) % componentList.length + } + e.preventDefault() + e.stopPropagation() + return false + } else if (e.key === "Enter") { + // Add selected component on enter press + if (componentList[selectedIndex]) { + addComponent(componentList[selectedIndex]) + } + } + } + onMount(() => { searchRef.focus() + window.addEventListener("keydown", handleKeyDown) + + return () => { + window.removeEventListener("keydown", handleKeyDown) + } }) @@ -135,7 +189,12 @@
{#each category.children as component} -
addComponent(component)}> +
addComponent(component.component)} + on:mouseover={() => (selectedIndex = null)} + > {component.name}
@@ -148,7 +207,10 @@ Blocks are collections of pre-built components {#each blocks as block} -
addComponent(block)}> +
addComponent(block.component)} + > {block.name}
@@ -176,15 +238,20 @@ padding: 0 var(--spacing-s); gap: var(--spacing-s); padding-top: 4px; - transition: background 130ms ease-out; + border: 1px solid var(--spectrum-global-color-gray-200); + transition: border-color 130ms ease-out; + } + .component.selected, + .component:hover { + border-color: var(--spectrum-global-color-blue-400); + } + .component:hover { + cursor: pointer; } .component :global(.spectrum-Body) { line-height: 1.2 !important; } - .component:hover { - cursor: pointer; - background: var(--spectrum-alias-background-color-tertiary); - } + .block { flex-direction: row; justify-content: flex-start;