Add combobox component

This commit is contained in:
Andrew Kingston 2021-04-19 12:05:11 +01:00
parent 9663c62d2e
commit eeac45d3e7
21 changed files with 193 additions and 253 deletions

View File

@ -45,6 +45,7 @@
"@spectrum-css/fieldgroup": "^3.0.2",
"@spectrum-css/fieldlabel": "^3.0.1",
"@spectrum-css/icon": "^3.0.1",
"@spectrum-css/inputgroup": "^3.0.2",
"@spectrum-css/label": "^2.0.9",
"@spectrum-css/link": "^3.1.1",
"@spectrum-css/menu": "^3.0.1",

View File

@ -0,0 +1,39 @@
<script>
import Field from "./Field.svelte"
import Combobox from "./Core/Combobox.svelte"
import { createEventDispatcher } from "svelte"
export let value = null
export let label = undefined
export let disabled = false
export let labelPosition = "above"
export let error = null
export let placeholder = "Choose an option"
export let options = []
export let getOptionLabel = option => extractProperty(option, "label")
export let getOptionValue = option => extractProperty(option, "value")
const dispatch = createEventDispatcher()
const onChange = e => {
dispatch("change", e.detail)
value = e.detail
}
const extractProperty = (value, property) => {
if (value && typeof value === "object") {
return value[property]
}
return value
}
</script>
<Field {label} {labelPosition} {disabled} {error}>
<Combobox
{error}
{disabled}
{value}
{options}
{placeholder}
{getOptionLabel}
{getOptionValue}
on:change={onChange} />
</Field>

View File

@ -0,0 +1,128 @@
<script>
import "@spectrum-css/inputgroup/dist/index-vars.css"
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"
export let value = null
export let id = null
export let placeholder = "Choose an option"
export let disabled = false
export let error = null
export let options = []
export let getOptionLabel = option => option
export let getOptionValue = option => option
const dispatch = createEventDispatcher()
let open = false
let focus = false
$: fieldText = getFieldText(value, options, placeholder)
const getFieldText = (value, options, placeholder) => {
// Always use placeholder if no value
if (value == null || value === "") {
return placeholder || "Choose an option"
}
// 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
}
const selectOption = value => {
dispatch("change", value)
open = false
}
const onChange = e => {
selectOption(e.target.value)
}
</script>
<div class="spectrum-InputGroup" class:is-focused={open || focus}>
<div
class="spectrum-Textfield spectrum-InputGroup-textfield"
class:is-disabled={!!error}
class:is-focused={open || focus}>
<input
type="text"
on:focus={() => (focus = true)}
on:blur={() => (focus = false)}
on:change={onChange}
{value}
{placeholder}
class="spectrum-Textfield-input spectrum-InputGroup-input" />
</div>
<button
class="spectrum-Picker spectrum-Picker--sizeM spectrum-InputGroup-button"
tabindex="-1"
aria-haspopup="true"
disabled={!!error}
on:click={() => (open = true)}>
<svg
class="spectrum-Icon spectrum-UIIcon-ChevronDown100 spectrum-Picker-menuIcon spectrum-InputGroup-icon"
focusable="false"
aria-hidden="true">
<use xlink:href="#spectrum-css-icon-Chevron100" />
</svg>
</button>
{#if open}
<div class="overlay" on:mousedown|self={() => (open = false)} />
<div
transition:fly={{ y: -20, duration: 200 }}
class="spectrum-Popover spectrum-Popover--bottom is-open">
<ul class="spectrum-Menu" role="listbox">
{#if options && Array.isArray(options)}
{#each options as option}
<li
class="spectrum-Menu-item"
class:is-selected={getOptionValue(option) === value}
role="option"
aria-selected="true"
tabindex="0"
on:click={() => selectOption(getOptionValue(option))}>
<span
class="spectrum-Menu-itemLabel">{getOptionLabel(option)}</span>
<svg
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
focusable="false"
aria-hidden="true">
<use xlink:href="#spectrum-css-icon-Checkmark100" />
</svg>
</li>
{/each}
{/if}
</ul>
</div>
{/if}
</div>
<style>
.spectrum-InputGroup {
min-width: 0;
width: 100%;
}
.spectrum-Textfield-input {
width: 0;
}
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 999;
}
.spectrum-Popover {
max-height: 240px;
width: 100%;
z-index: 999;
top: 100%;
}
</style>

View File

@ -105,6 +105,7 @@
max-height: 240px;
width: 100%;
z-index: 999;
top: 100%;
}
.spectrum-Picker {
width: 100%;

View File

@ -10,6 +10,7 @@
export let id = null
const dispatch = createEventDispatcher()
let focus = false
const updateValue = value => {
if (type === "number") {
@ -20,6 +21,7 @@
}
const onBlur = event => {
focus = false
updateValue(event.target.value)
}
@ -33,7 +35,8 @@
<div
class="spectrum-Textfield"
class:is-invalid={!!error}
class:is-disabled={disabled}>
class:is-disabled={disabled}
class:is-focused={focus}>
{#if error}
<svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Textfield-validationIcon"
@ -49,6 +52,7 @@
value={value || ''}
placeholder={placeholder || ''}
on:blur={onBlur}
on:focus={() => (focus = true)}
{type}
class="spectrum-Textfield-input" />
</div>

View File

@ -4,3 +4,5 @@ export { default as CoreMultiselect } from "./Multiselect.svelte"
export { default as CoreCheckbox } from "./Checkbox.svelte"
export { default as CoreRadioGroup } from "./RadioGroup.svelte"
export { default as CoreTextArea } from "./TextArea.svelte"
export { default as CoreCombobox } from "./Combobox.svelte"
export { default as CoreSwitch } from "./Switch.svelte"

View File

@ -1,158 +0,0 @@
<script>
import Icon from "../Icons/Icon.svelte"
import Label from "../Styleguide/Label.svelte"
import { createEventDispatcher } from "svelte"
export let label = undefined
export let value = ""
export let name = undefined
export let thin = false
export let extraThin = false
export let secondary = false
export let outline = false
export let disabled = false
const dispatch = createEventDispatcher()
let focus = false
const updateValue = e => {
value = e.target.value
}
function handleFocus(e) {
focus = true
dispatch("focus", e)
}
function handleBlur(e) {
focus = false
dispatch("blur", e)
}
</script>
{#if label}
<Label extraSmall grey forAttr={name}>{label}</Label>
{/if}
<div class="container" class:disabled class:secondary class:outline class:focus>
<select
{name}
class:thin
class:extraThin
class:secondary
{disabled}
on:change
on:focus={handleFocus}
on:blur={handleBlur}
bind:value>
<slot />
</select>
<slot name="custom-input" />
<input
class:thin
class:extraThin
class:secondary
class:disabled
{disabled}
on:change={updateValue}
on:input={updateValue}
on:focus={handleFocus}
on:blur={e => {
updateValue(e)
handleBlur(e)
}}
value={value || ''}
type="text" />
<div class="pointer editable-pointer">
<Icon name="arrowdown" />
</div>
</div>
<style>
.container {
position: relative !important;
display: block;
border-radius: var(--border-radius-s);
border: var(--border-transparent);
background-color: var(--background);
}
.container.outline {
border: var(--border-dark);
}
.container.focus {
border: var(--border-blue);
}
input,
select {
border-radius: var(--border-radius-s);
font-size: var(--font-size-m);
outline: none;
border: none;
color: var(--ink);
text-align: left;
background-color: transparent;
}
select {
display: block !important;
width: 100% !important;
padding: var(--spacing-m) 2rem var(--spacing-m) var(--spacing-m);
appearance: none !important;
-webkit-appearance: none !important;
-moz-appearance: none !important;
align-items: center;
white-space: pre;
opacity: 0;
}
input {
position: absolute;
top: 0;
left: 0;
width: calc(100% - 30px);
height: 100%;
border: none;
box-sizing: border-box;
padding: var(--spacing-m) 0 var(--spacing-m) var(--spacing-m);
}
select.thin,
input.thin {
font-size: var(--font-size-xs);
}
select.extraThin,
input.extraThin {
font-size: var(--font-size-xs);
padding: var(--spacing-s) 0 var(--spacing-s) var(--spacing-m);
}
.secondary {
background: var(--grey-2);
}
select:disabled,
input:disabled,
.disabled {
background: var(--grey-4);
color: var(--grey-6);
}
.pointer {
right: 0 !important;
top: 0 !important;
bottom: 0 !important;
position: absolute !important;
pointer-events: none !important;
align-items: center !important;
display: flex !important;
box-sizing: border-box;
}
.editable-pointer {
border-style: solid;
border-width: 0 0 0 1px;
border-color: var(--grey-4);
padding-left: var(--spacing-xs);
}
.editable-pointer :global(svg) {
margin-right: var(--spacing-xs);
fill: var(--ink);
}
</style>

View File

@ -4,7 +4,7 @@ import "./bbui.css"
export { default as Input } from "./Form/Input.svelte"
export { default as TextArea } from "./Form/TextArea.svelte"
export { default as Select } from "./Form/Select.svelte"
export { default as DataList } from "./Form/DataList.svelte"
export { default as Combobox } from "./Form/Combobox.svelte"
export { default as Dropzone } from "./Dropzone/Dropzone.svelte"
export { default as Drawer } from "./Drawer/Drawer.svelte"
export { default as ActionButton } from "./ActionButton/ActionButton.svelte"

View File

@ -116,6 +116,11 @@
resolved "https://registry.yarnpkg.com/@spectrum-css/icon/-/icon-3.0.2.tgz#327dcb95ab86368a00eb5a6d898c2c02e4ae04b3"
integrity sha512-BdHoO2ttrbsj1+IfHmSCGNS0oEf8i2UW3agfOVtSlYOD+iGykupWwy8ANLB6p4GvjlR7YjPRGzDRGgmDwVqR0Q==
"@spectrum-css/inputgroup@^3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@spectrum-css/inputgroup/-/inputgroup-3.0.2.tgz#f1b13603832cbd22394f3d898af13203961f8691"
integrity sha512-O0G3Lw9gxsh8gTLQWIAKkN1O8cWhjpEUl+oR1PguIKFni72uNr2ikU9piOwy/r0gJG2Q/TVs6hAshoAAkmsSzw==
"@spectrum-css/label@^2.0.9":
version "2.0.10"
resolved "https://registry.yarnpkg.com/@spectrum-css/label/-/label-2.0.10.tgz#2368651d7636a19385b5d300cdf6272db1916001"

View File

@ -2,4 +2,4 @@
import FormFieldSelect from "./FormFieldSelect.svelte"
</script>
<FormFieldSelect {...$$props} type="attachment" />
<FormFieldSelect {...$$props} on:change type="attachment" />

View File

@ -2,4 +2,4 @@
import FormFieldSelect from "./FormFieldSelect.svelte"
</script>
<FormFieldSelect {...$$props} type="boolean" />
<FormFieldSelect {...$$props} on:change type="boolean" />

View File

@ -2,4 +2,4 @@
import FormFieldSelect from "./FormFieldSelect.svelte"
</script>
<FormFieldSelect {...$$props} type="datetime" />
<FormFieldSelect {...$$props} on:change type="datetime" />

View File

@ -1,55 +0,0 @@
<script>
import { DataList } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import { allScreens } from "builderStore"
const dispatch = createEventDispatcher()
export let value = ""
$: urls = getUrls()
const handleBlur = () => dispatch("change", value)
const getUrls = () => {
return [
...$allScreens
.filter(
screen =>
screen.props._component.endsWith("/rowdetail") ||
screen.routing.route.endsWith(":id")
)
.map(screen => ({
name: screen.props._instanceName,
url: screen.routing.route,
sort: screen.props._component,
})),
]
}
</script>
<div>
<DataList
editable
secondary
extraThin
on:blur={handleBlur}
on:change
bind:value>
<option value="" />
{#each urls as url}
<option value={url.url}>{url.name}</option>
{/each}
</DataList>
</div>
<style>
div {
flex: 1 1 auto;
display: flex;
flex-direction: row;
}
div :global(> div) {
flex: 1 1 auto;
}
</style>

View File

@ -1,5 +1,5 @@
<script>
import { DataList } from "@budibase/bbui"
import { Combobox } from "@budibase/bbui"
import {
getDatasourceForProvider,
getSchemaForDatasource,
@ -28,32 +28,6 @@
}
return entries.map(entry => entry[0])
}
const handleBlur = () => onChange(value)
</script>
<div>
<DataList
editable
secondary
extraThin
on:blur={handleBlur}
on:change
bind:value>
<option value="" />
{#each options as option}
<option value={option}>{option}</option>
{/each}
</DataList>
</div>
<style>
div {
flex: 1 1 auto;
display: flex;
flex-direction: row;
}
div :global(> div) {
flex: 1 1 auto;
}
</style>
<Combobox on:change {value} {options} />

View File

@ -2,4 +2,4 @@
import FormFieldSelect from "./FormFieldSelect.svelte"
</script>
<FormFieldSelect {...$$props} type="longform" />
<FormFieldSelect {...$$props} on:change type="longform" />

View File

@ -2,4 +2,4 @@
import FormFieldSelect from "./FormFieldSelect.svelte"
</script>
<FormFieldSelect {...$$props} type="number" />
<FormFieldSelect {...$$props} on:change type="number" />

View File

@ -2,4 +2,4 @@
import FormFieldSelect from "./FormFieldSelect.svelte"
</script>
<FormFieldSelect {...$$props} type="options" />
<FormFieldSelect {...$$props} on:change type="options" />

View File

@ -134,6 +134,7 @@
flex: 1;
display: inline-block;
padding-left: 2px;
width: 0;
}
.icon {

View File

@ -2,4 +2,4 @@
import FormFieldSelect from "./FormFieldSelect.svelte"
</script>
<FormFieldSelect {...$$props} type="link" />
<FormFieldSelect {...$$props} on:change type="link" />

View File

@ -2,4 +2,4 @@
import FormFieldSelect from "./FormFieldSelect.svelte"
</script>
<FormFieldSelect {...$$props} type="string" />
<FormFieldSelect {...$$props} on:change type="string" />

View File

@ -17,7 +17,6 @@
import SchemaSelect from "./PropertyControls/SchemaSelect.svelte"
import EventsEditor from "./PropertyControls/EventsEditor"
import FilterEditor from "./PropertyControls/FilterEditor.svelte"
import DetailScreenSelect from "./PropertyControls/DetailScreenSelect.svelte"
import { IconSelect } from "./PropertyControls/IconSelect"
import ColorPicker from "./PropertyControls/ColorPicker.svelte"
import StringFieldSelect from "./PropertyControls/StringFieldSelect.svelte"
@ -61,7 +60,6 @@
select: Select,
dataSource: DataSourceSelect,
dataProvider: DataProviderSelect,
detailScreen: DetailScreenSelect,
boolean: Checkbox,
number: Input,
event: EventsEditor,