From 93f90405cf2ce8ad160c3e69fe0d95009ef0c593 Mon Sep 17 00:00:00 2001 From: Dean Date: Tue, 17 May 2022 15:09:13 +0100 Subject: [PATCH] Builder focus behaviour added to guide users when adding form elements. Refactored the component tests --- packages/bbui/src/Form/Combobox.svelte | 2 + packages/bbui/src/Form/Core/Combobox.svelte | 29 +++-- .../integration/createComponents.spec.js | 105 ++++++++++++++---- packages/builder/cypress/support/commands.js | 4 +- .../src/builderStore/store/frontend.js | 35 ++++++ .../AppPreview/CurrentItemPreview.svelte | 14 +++ .../ComponentSettingsSection.svelte | 9 ++ .../PropertyControls/FormFieldSelect.svelte | 16 ++- .../PropertyControls/PropertyControl.svelte | 2 + .../src/components/app/Placeholder.svelte | 5 +- .../src/components/app/forms/Field.svelte | 41 ++++++- 11 files changed, 216 insertions(+), 46 deletions(-) diff --git a/packages/bbui/src/Form/Combobox.svelte b/packages/bbui/src/Form/Combobox.svelte index 83927b05db..9b7cab1a08 100644 --- a/packages/bbui/src/Form/Combobox.svelte +++ b/packages/bbui/src/Form/Combobox.svelte @@ -13,6 +13,7 @@ export let options = [] export let getOptionLabel = option => extractProperty(option, "label") export let getOptionValue = option => extractProperty(option, "value") + export let autofocus = false const dispatch = createEventDispatcher() const onChange = e => { @@ -35,6 +36,7 @@ {options} {placeholder} {readonly} + {autofocus} {getOptionLabel} {getOptionValue} on:change={onChange} diff --git a/packages/bbui/src/Form/Core/Combobox.svelte b/packages/bbui/src/Form/Core/Combobox.svelte index 6a6423cccf..1c590ea395 100644 --- a/packages/bbui/src/Form/Core/Combobox.svelte +++ b/packages/bbui/src/Form/Core/Combobox.svelte @@ -3,13 +3,14 @@ import "@spectrum-css/popover/dist/index-vars.css" import "@spectrum-css/menu/dist/index-vars.css" import { fly } from "svelte/transition" - import { createEventDispatcher } from "svelte" + import { createEventDispatcher, getContext } from "svelte" export let value = null export let id = null export let placeholder = "Choose an option or type" export let disabled = false export let readonly = false + export let autofocus = false export let error = null export let options = [] export let getOptionLabel = option => option @@ -18,22 +19,12 @@ const dispatch = createEventDispatcher() let open = false let focus = false - $: fieldText = getFieldText(value, options, placeholder) + let comboInput - const getFieldText = (value, options, placeholder) => { - // Always use placeholder if no value - if (value == null || value === "") { - return placeholder || "Choose an option or type" - } + let builderFocus = getContext("field_focus") - // Wait for options to load if there is a value but no options - if (!options?.length) { - return "" - } - - // Render the label if the selected option is found, otherwise raw value - const selected = options.find(option => getOptionValue(option) === value) - return selected ? getOptionLabel(selected) : value + $: if (autofocus && comboInput) { + comboInput.focus() } const selectOption = value => { @@ -66,10 +57,16 @@ class:is-focused={open || focus} > (focus = true)} - on:blur={() => (focus = false)} + on:blur={() => { + if (builderFocus) { + builderFocus.clear() + } + focus = false + }} on:change={onType} value={value || ""} placeholder={placeholder || ""} diff --git a/packages/builder/cypress/integration/createComponents.spec.js b/packages/builder/cypress/integration/createComponents.spec.js index e13439d9c6..3525e66fb4 100644 --- a/packages/builder/cypress/integration/createComponents.spec.js +++ b/packages/builder/cypress/integration/createComponents.spec.js @@ -1,9 +1,7 @@ -// TODO for now components are skipped, might not be good to keep doing this - import filterTests from "../support/filterTests" filterTests(['all'], () => { - xcontext("Create Components", () => { + context("Create Components", () => { let headlineId before(() => { @@ -12,12 +10,13 @@ filterTests(['all'], () => { cy.createTable("dog") cy.addColumn("dog", "name", "Text") cy.addColumn("dog", "age", "Number") - cy.addColumn("dog", "type", "Options") + cy.addColumn("dog", "breed", "Options") cy.navigateToFrontend() + cy.wait(1000) //allow the iframe some wiggle room }) it("should add a container", () => { - cy.addComponent(null, "Container").then(componentId => { + cy.addComponent("Layout", "Container").then(componentId => { cy.getComponent(componentId).should("exist") }) }) @@ -31,7 +30,6 @@ filterTests(['all'], () => { it("should change the text of the headline", () => { const text = "Lorem ipsum dolor sit amet." - cy.get("[data-cy=Settings]").click() cy.get("[data-cy=setting-text] input") .type(text) .blur() @@ -39,32 +37,34 @@ filterTests(['all'], () => { }) it("should change the size of the headline", () => { - cy.get("[data-cy=Design]").click() - cy.contains("Typography").click() - cy.get("[data-cy=font-size-prop-control]").click() - cy.contains("60px").click() - cy.getComponent(headlineId).should("have.css", "font-size", "60px") + cy.get("[data-cy=setting-size]").scrollIntoView().click() + cy.get("[data-cy=setting-size]").within(() => { + cy.get(".spectrum-Form-item li.spectrum-Menu-item").contains("3XL").click() + }) + + cy.getComponent(headlineId).within(() => { + cy.get(".spectrum-Heading").should("have.css", "font-size", "60px") + }) }) it("should create a form and reset to match schema", () => { cy.addComponent("Form", "Form").then(() => { - cy.get("[data-cy=Settings]").click() cy.get("[data-cy=setting-dataSource]") - .contains("Choose option") + .contains("Custom") .click() cy.get(".dropdown") .contains("dog") .click() cy.addComponent("Form", "Field Group").then(fieldGroupId => { - cy.get("[data-cy=Settings]").click() - cy.contains("Update Form Fields").click() - cy.get(".modal") - .get("button.primary") + cy.contains("Update form fields").click() + cy.get(".spectrum-Modal") + .get(".confirm-wrap .spectrum-Button") .click() + cy.wait(500) cy.getComponent(fieldGroupId).within(() => { cy.contains("name").should("exist") cy.contains("age").should("exist") - cy.contains("type").should("exist") + cy.contains("breed").should("exist") }) cy.getComponent(fieldGroupId) .find("input") @@ -81,17 +81,78 @@ filterTests(['all'], () => { cy.get("[data-cy=setting-_instanceName] input") .type(componentId) .blur() - cy.get(".ui-nav ul .nav-item.selected .ri-more-line").click({ + cy.get(".nav-items-container .nav-item.selected .actions > div > .icon").click({ force: true, }) - cy.get(".dropdown-container") + cy.get(".spectrum-Popover.is-open li") .contains("Delete") .click() - cy.get(".modal") + cy.get(".spectrum-Modal button") .contains("Delete Component") - .click() + .click({ + force: true, + }) cy.getComponent(componentId).should("not.exist") }) }) + + it("should set focus to the field setting when fields are added to a form", () => { + cy.addComponent("Form", "Form").then(() => { + cy.get("[data-cy=setting-dataSource]") + .contains("Custom") + .click() + cy.get(".dropdown") + .contains("dog") + .click() + + const componentTypeLabels = ["Text Field", "Number Field", "Password Field", + "Options Picker", "Checkbox", "Long Form Field", "Date Picker", "Attachment", + "JSON Field", "Multi-select Picker", "Relationship Picker"] + + const refocusTest = (componentId) => { + let inputClasses + + cy.getComponent(componentId) + .find(".showMe").should("exist").click({ force: true }) + + cy.get("[data-cy=setting-field] .spectrum-InputGroup") + .should("have.class", "is-focused").within(() => { + cy.get("input").should(($input) => { + expect($input).to.have.length(1) + inputClasses = Cypress.$($input).attr('class') + }) + }) + + cy.focused().then(($focused) => { + const focusedClasses = Cypress.$($focused).attr('class') + expect(inputClasses).to.equal(focusedClasses) + }) + } + + const testFieldFocusOnCreate = (componentLabel) => { + let inputClasses + + cy.addComponent("Form", componentLabel).then((componentId) => { + + refocusTest(componentId) + + cy.get("[data-cy=setting-field] .spectrum-InputGroup") + .should("have.class", "is-focused").within(() => { + cy.get("input").should(($input) => { + expect($input).to.have.length(1) + inputClasses = Cypress.$($input).attr('class') + }) + }) + }) + cy.focused().then(($focused) => { + const focusedClasses = Cypress.$($focused).attr('class') + expect(inputClasses).to.equal(focusedClasses) + }) + } + + componentTypeLabels.forEach( testFieldFocusOnCreate ) + + }) + }) }) }) diff --git a/packages/builder/cypress/support/commands.js b/packages/builder/cypress/support/commands.js index fe355441b9..3baa148ae8 100644 --- a/packages/builder/cypress/support/commands.js +++ b/packages/builder/cypress/support/commands.js @@ -334,9 +334,9 @@ Cypress.Commands.add("getComponent", componentId => { .its("0.contentDocument") .should("exist") .its("body") - .should("not.be.null") + .should("not.be.undefined") .then(cy.wrap) - .find(`[data-id=${componentId}]`) + .find(`[data-id='${componentId}']`) }) Cypress.Commands.add("navigateToFrontend", () => { diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index 3ffc890c7d..48e5982eae 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -380,6 +380,21 @@ export const getFrontendStore = () => { const selected = get(selectedComponent) const asset = get(currentAsset) + const formComponents = [ + "stringfield", + "optionsfield", + "numberfield", + "datetimefield", + "booleanfield", + "passwordfield", + "longformfield", + "attachmentfield", + "jsonfield", + "relationshipfield", + "multifieldselect", + "s3upload", + ] + // Create new component const componentInstance = store.actions.components.createInstance( componentName, @@ -417,11 +432,31 @@ export const getFrontendStore = () => { } parentComponent._children.push(componentInstance) + let isFormComponent = false + let componentPrefix = "@budibase/standard-components/" + if (parentComponent._component === componentPrefix + "form") { + const mappedComponentTypes = formComponents.map(cmp => { + return componentPrefix + cmp + }) + if (mappedComponentTypes.indexOf(componentInstance._component) > -1) { + isFormComponent = true + } + } + // Save components and update UI await store.actions.preview.saveSelected() store.update(state => { state.currentView = "component" state.selectedComponentId = componentInstance._id + + if (isFormComponent) { + //A field component added to a form. + state.builderFocus = { + key: "field", + target: state.selectedComponentId, + location: "component_settings", + } + } return state }) diff --git a/packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte b/packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte index 28bc50d15a..9bab224807 100644 --- a/packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte +++ b/packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte @@ -157,6 +157,14 @@ try { if (type === "select-component" && data.id) { store.actions.components.select({ _id: data.id }) + //Clear focus + if(data.id !== $store.builderFocus?.target){ + store.update(state => { + delete state.builderFocus + return state + }) + } + //check if the builder-focus matches? } else if (type === "update-prop") { await store.actions.components.updateProp(data.prop, data.value) } else if (type === "delete-component" && data.id) { @@ -190,6 +198,12 @@ store.actions.components.copy(source, true) await store.actions.components.paste(destination, data.mode) } + } else if(type == "builder-focus") { + store.update(state => ({ + ...state, + builderFocus : + { ...data } + })) } else { console.warn(`Client sent unknown event type: ${type}`) } diff --git a/packages/builder/src/components/design/PropertiesPanel/ComponentSettingsSection.svelte b/packages/builder/src/components/design/PropertiesPanel/ComponentSettingsSection.svelte index a043cca619..a36ce52379 100644 --- a/packages/builder/src/components/design/PropertiesPanel/ComponentSettingsSection.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/ComponentSettingsSection.svelte @@ -86,6 +86,14 @@ return true } + + const isFocused = setting => { + return ( + componentInstance._id === $store.builderFocus?.target && + setting.key === $store.builderFocus?.key && + "component_settings" === $store.builderFocus?.location + ) + } {#each sections as section, idx (section.name)} @@ -120,6 +128,7 @@ {componentBindings} {componentInstance} {componentDefinition} + autofocus={isFocused(setting)} /> {/if} {/each} diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FormFieldSelect.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FormFieldSelect.svelte index 1f08c56ff5..6fd904bb56 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FormFieldSelect.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FormFieldSelect.svelte @@ -4,12 +4,24 @@ getDatasourceForProvider, getSchemaForDatasource, } from "builderStore/dataBinding" - import { currentAsset } from "builderStore" + import { currentAsset, store } from "builderStore" import { findClosestMatchingComponent } from "builderStore/componentUtils" + import { setContext } from "svelte" + + setContext("field_focus", { + clear: () => { + store.update(state => { + delete state.builderFocus + return state + }) + }, + test: $store.builderFocus?.target, + }) export let componentInstance export let value export let type + export let autofocus = false $: form = findClosestMatchingComponent( $currentAsset?.props, @@ -40,4 +52,4 @@ } - + diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/PropertyControl.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/PropertyControl.svelte index 617b1c83ab..026150f957 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/PropertyControl.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/PropertyControl.svelte @@ -16,6 +16,7 @@ export let bindings = [] export let componentBindings = [] export let nested = false + export let autofocus = false $: allBindings = getAllBindings(bindings, componentBindings, nested) $: safeValue = getSafeValue(value, props.defaultValue, allBindings) @@ -82,6 +83,7 @@ {key} {type} {...props} + {autofocus} /> diff --git a/packages/client/src/components/app/Placeholder.svelte b/packages/client/src/components/app/Placeholder.svelte index 203071e0b1..31a7819cae 100644 --- a/packages/client/src/components/app/Placeholder.svelte +++ b/packages/client/src/components/app/Placeholder.svelte @@ -9,7 +9,10 @@ {#if $builderStore.inBuilder}
- {text || $component.name || "Placeholder"} + {#if !$$slots.content} + {text || $component.name || "Placeholder"} + {/if} +
{/if} diff --git a/packages/client/src/components/app/forms/Field.svelte b/packages/client/src/components/app/forms/Field.svelte index b267f6caff..aaa2ec11b2 100644 --- a/packages/client/src/components/app/forms/Field.svelte +++ b/packages/client/src/components/app/forms/Field.svelte @@ -3,6 +3,10 @@ import FieldGroupFallback from "./FieldGroupFallback.svelte" import { getContext, onDestroy } from "svelte" + const dispatchEvent = (type, data = {}) => { + window.parent.postMessage({ type, data }) + } + export let label export let field export let fieldState @@ -76,9 +80,28 @@ {#if !formContext} {:else if !fieldState} - + {#if $builderStore.inBuilder} +
+ +
+ Add the Field setting to start using your + component  + { + dispatchEvent("builder-focus", { + location: "component_settings", + key: "field", + target: $component.id, + }) + }} + > + Show me + +
+
+
+ {/if} {:else if schemaType && schemaType !== type && type !== "options"}