Add a few UX improvements to adding component to allow control using the keyboard

This commit is contained in:
Andrew Kingston 2022-05-10 19:53:22 +01:00
parent bfd9eff7a6
commit 7ea59a521d
1 changed files with 98 additions and 31 deletions

View File

@ -13,21 +13,40 @@
notifications, notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import structure from "./componentStructure.json" import structure from "./componentStructure.json"
import { store } from "builderStore" import { store, selectedComponent } from "builderStore"
import { onMount } from "svelte" import { onMount } from "svelte"
let section = "components" let section = "components"
let searchString let searchString
let searchRef let searchRef
let selectedIndex
let componentList = []
$: currentDefinition = store.actions.components.getDefinition(
$selectedComponent?._component
)
$: enrichedStructure = enrichStructure(structure, $store.components) $: enrichedStructure = enrichStructure(structure, $store.components)
$: filteredStructure = filterStructure( $: filteredStructure = filterStructure(
enrichedStructure, enrichedStructure,
section, section,
currentDefinition,
searchString searchString
) )
$: blocks = enrichedStructure.find(x => x.name === "Blocks").children $: 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) => { const enrichStructure = (structure, definitions) => {
let enrichedStructure = [] let enrichedStructure = []
structure.forEach(item => { structure.forEach(item => {
@ -50,51 +69,86 @@
return enrichedStructure return enrichedStructure
} }
const filterStructure = (structure, section, search) => { const filterStructure = (structure, section, currentDefinition, search) => {
selectedIndex = search ? 0 : null
componentList = []
if (!structure?.length) { if (!structure?.length) {
return [] return []
} }
// Split list into either components or blocks initially
if (section === "components") { if (section === "components") {
structure = structure.filter(category => category.name !== "Blocks") structure = structure.filter(category => category.name !== "Blocks")
} else { } else {
structure = structure.filter(category => category.name === "Blocks") structure = structure.filter(category => category.name === "Blocks")
} }
if (search) {
let filteredStructure = [] // Return only items which match the search string
structure.forEach(category => { let filteredStructure = []
let matchedChildren = category.children.filter(child => { structure.forEach(category => {
return child.name.toLowerCase().includes(search.toLowerCase()) let matchedChildren = category.children.filter(child => {
}) const name = child.name.toLowerCase()
if (matchedChildren.length) {
filteredStructure.push({ // Check if the component matches the search string
...category, if (search && !name.includes(search.toLowerCase())) {
children: matchedChildren, 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 return structure
} }
const isChildAllowed = ({ name }, selectedComponent) => { const addComponent = async component => {
const currentComponent = store.actions.components.getDefinition(
selectedComponent?._component
)
return currentComponent?.illegalChildren?.includes(name.toLowerCase())
}
const addComponent = async item => {
try { try {
await store.actions.components.create(item.component) await store.actions.components.create(component)
$goto("../") $goto("../")
} catch (error) { } catch (error) {
notifications.error("Error creating component") 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(() => { onMount(() => {
searchRef.focus() searchRef.focus()
window.addEventListener("keydown", handleKeyDown)
return () => {
window.removeEventListener("keydown", handleKeyDown)
}
}) })
</script> </script>
@ -135,7 +189,12 @@
<DetailSummary name={category.name} collapsible={false}> <DetailSummary name={category.name} collapsible={false}>
<div class="component-grid"> <div class="component-grid">
{#each category.children as component} {#each category.children as component}
<div class="component" on:click={() => addComponent(component)}> <div
class="component"
class:selected={selectedIndex === orderMap[component.component]}
on:click={() => addComponent(component.component)}
on:mouseover={() => (selectedIndex = null)}
>
<Icon name={component.icon} /> <Icon name={component.icon} />
<Body size="XS">{component.name}</Body> <Body size="XS">{component.name}</Body>
</div> </div>
@ -148,7 +207,10 @@
<Body size="S">Blocks are collections of pre-built components</Body> <Body size="S">Blocks are collections of pre-built components</Body>
<Layout noPadding gap="XS"> <Layout noPadding gap="XS">
{#each blocks as block} {#each blocks as block}
<div class="component block" on:click={() => addComponent(block)}> <div
class="component block"
on:click={() => addComponent(block.component)}
>
<Icon name={block.icon} /> <Icon name={block.icon} />
<Body size="XS">{block.name}</Body> <Body size="XS">{block.name}</Body>
</div> </div>
@ -176,15 +238,20 @@
padding: 0 var(--spacing-s); padding: 0 var(--spacing-s);
gap: var(--spacing-s); gap: var(--spacing-s);
padding-top: 4px; 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) { .component :global(.spectrum-Body) {
line-height: 1.2 !important; line-height: 1.2 !important;
} }
.component:hover {
cursor: pointer;
background: var(--spectrum-alias-background-color-tertiary);
}
.block { .block {
flex-direction: row; flex-direction: row;
justify-content: flex-start; justify-content: flex-start;