@dosgato/dialog 0.0.41 → 0.0.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Dialog.svelte CHANGED
@@ -55,7 +55,9 @@ $: describedby = [title ? labelid : undefined, descid].filter(isNotBlank).join('
55
55
  {#if isNotBlank(cancelText)}
56
56
  <Button cancel {describedby} on:click={() => dispatch('escape')}>{cancelText}</Button>
57
57
  {/if}
58
- <Button class="primary" disabled={disabled || (hasRequired && !ignoreTabs)} {describedby} on:click={() => dispatch('continue')}><Icon icon={continueIcon} inline />{continueText}</Button>
58
+ {#if isNotBlank(continueText)}
59
+ <Button class="primary" disabled={disabled || (hasRequired && !ignoreTabs)} {describedby} on:click={() => dispatch('continue')}><Icon icon={continueIcon} inline />{continueText}</Button>
60
+ {/if}
59
61
  {#if nextTitle && !ignoreTabs}
60
62
  <Button class="next" disabled={!nextTitle} on:click={onNext}>Next<ScreenReaderOnly> Tab ({nextTitle})</ScreenReaderOnly> <Icon width="1.2em" icon={arrowRightLight} inline /></Button>
61
63
  {/if}
@@ -1,7 +1,7 @@
1
1
  <script>import { nullableSerialize, nullableDeserialize } from '@txstate-mws/svelte-forms';
2
2
  import FieldStandard from './FieldStandard.svelte';
3
- import { randomid } from 'txstate-utils';
4
- import { PopupMenu, ScreenReaderOnly } from '@txstate-mws/svelte-components';
3
+ import { isBlank, isNotBlank, randomid } from 'txstate-utils';
4
+ import { modifierKey, PopupMenu, ScreenReaderOnly } from '@txstate-mws/svelte-components';
5
5
  import { getDescribedBy } from './';
6
6
  import { onMount } from 'svelte';
7
7
  export let id = undefined;
@@ -13,47 +13,63 @@ export { className as class };
13
13
  export let notNull = false;
14
14
  export let disabled = false;
15
15
  export let choices;
16
- export let defaultValue = notNull ? choices[0].value : undefined;
16
+ export let defaultValue = undefined;
17
17
  export let conditional = undefined;
18
18
  export let required = false;
19
19
  export let inputelement = undefined;
20
20
  export let helptext = undefined;
21
21
  let inputvalue = '';
22
22
  let popupvalue = undefined;
23
- let savedLabel = '';
24
- let changed = false;
25
23
  let menuid;
26
24
  const liveTextId = randomid();
27
- $: filteredChoices = changed
28
- ? choices.filter((item) => {
29
- return item.label?.toLowerCase().includes(inputvalue.toLowerCase()) || item.value.toLowerCase().includes(inputvalue.toLowerCase());
30
- })
31
- : choices;
25
+ $: finalserialize = !notNull ? nullableSerialize : v => v;
26
+ $: finaldeserialize = !notNull ? nullableDeserialize : v => v;
27
+ $: valueToLabel = Object.fromEntries(choices.map(c => [c.value, c.label || c.value]));
28
+ $: labelToValue = Object.fromEntries(choices.map(c => [c.label || c.value, c.value]));
29
+ $: filteredChoices = choices.filter((item) => {
30
+ return item.label?.toLowerCase().includes(inputvalue.toLowerCase()) || item.value.toLowerCase().includes(inputvalue.toLowerCase());
31
+ });
32
+ let menushown = false;
33
+ let savedVal = defaultValue;
34
+ function onKeyUp(setVal) {
35
+ return (e) => {
36
+ if (!modifierKey(e)) {
37
+ const val = labelToValue[inputvalue.trim()];
38
+ menushown = !val;
39
+ // we need to make sure the form state stays up to date with what's shown in the text field
40
+ // if the text field has only a substring of a valid label the form state value should remain
41
+ // empty, but once the text field has a valid label the form state should be updated
42
+ if (savedVal !== val) {
43
+ savedVal = val;
44
+ setVal(val);
45
+ }
46
+ }
47
+ };
48
+ }
32
49
  function onchangepopup(setVal) {
33
50
  return (e) => {
34
51
  inputvalue = e.detail.label || e.detail.value;
35
- savedLabel = inputvalue;
36
52
  popupvalue = undefined;
53
+ savedVal = e.detail.value;
37
54
  setVal(e.detail.value);
38
- changed = false;
39
55
  };
40
56
  }
41
- async function checkifchanged(e) {
42
- await setTimeout(function () {
43
- if (inputelement.value !== savedLabel)
44
- changed = true;
45
- }, 1);
46
- }
47
- const valueToLabel = {};
48
- for (const choice of choices)
49
- valueToLabel[choice.value] = choice.label || choice.value;
50
- let set = false;
51
- function reactToInitialValue(value) {
52
- if (!value || set)
53
- return;
54
- inputvalue = valueToLabel[value];
55
- savedLabel = inputvalue;
56
- set = true;
57
+ function reactToValue(value, setVal) {
58
+ const dsvalue = finaldeserialize(value);
59
+ if (dsvalue !== savedVal) {
60
+ const label = valueToLabel[dsvalue];
61
+ // if the form state value changes from the outside we need to replace the text field content
62
+ // with the new label
63
+ // if the new form state value is undefined, we should let them keep any half-finished typing they
64
+ // might have in the field, but if they have a fully valid entry in the field, it needs to disappear
65
+ // to reflect the new reality that the form value has gone away
66
+ if (label != null || (savedVal && valueToLabel[savedVal]))
67
+ inputvalue = label ?? '';
68
+ savedVal = dsvalue;
69
+ // if the form state value changes from the outside to an invalid value, we have to clear it out
70
+ if (isNotBlank(dsvalue) && isBlank(label))
71
+ setVal(finaldeserialize(''));
72
+ }
57
73
  }
58
74
  let portal;
59
75
  onMount(() => {
@@ -61,10 +77,10 @@ onMount(() => {
61
77
  });
62
78
  </script>
63
79
 
64
- <FieldStandard bind:id {label} {path} {required} {defaultValue} {conditional} {helptext} serialize={!notNull ? nullableSerialize : undefined} deserialize={!notNull ? nullableDeserialize : undefined} let:value let:setVal let:valid let:invalid let:id let:onBlur let:onChange let:messagesid let:helptextid>
65
- {@const _ = reactToInitialValue(value)}
66
- <input bind:this={inputelement} bind:value={inputvalue} {id} {placeholder} class="dialog-input {className}" class:valid class:invalid aria-invalid={invalid} aria-expanded={false} aria-controls={menuid} on:blur={onBlur} on:change={onChange} autocapitalize="none" type="text" autocomplete="off" aria-autocomplete="list" role="combobox" {disabled} aria-describedby={getDescribedBy([messagesid, helptextid])} on:keydown={checkifchanged}>
67
- <PopupMenu bind:menuid align="bottomleft" usePortal={portal} items={filteredChoices} buttonelement={inputelement} bind:value={popupvalue} on:change={onchangepopup(setVal)} emptyText="No options available"/>
80
+ <FieldStandard bind:id {label} {path} {required} {defaultValue} {conditional} {helptext} serialize={finalserialize} deserialize={finaldeserialize} let:value let:setVal let:valid let:invalid let:id let:onBlur let:messagesid let:helptextid>
81
+ {@const _ = reactToValue(value, setVal)}
82
+ <input bind:this={inputelement} bind:value={inputvalue} {id} {placeholder} class="dialog-input {className}" class:valid class:invalid aria-invalid={invalid} aria-expanded={false} aria-controls={menuid} on:blur={onBlur} on:keyup={onKeyUp(setVal)} autocapitalize="none" type="text" autocomplete="off" aria-autocomplete="list" role="combobox" {disabled} aria-describedby={getDescribedBy([messagesid, helptextid])}>
83
+ <PopupMenu bind:menushown bind:menuid align="bottomleft" usePortal={portal} items={filteredChoices} buttonelement={inputelement} bind:value={popupvalue} on:change={onchangepopup(setVal)} emptyText="No options available"/>
68
84
  <ScreenReaderOnly arialive="polite" ariaatomic={true} id={liveTextId}>
69
85
  {filteredChoices.length} {filteredChoices.length === 1 ? 'option' : 'options'} available.
70
86
  </ScreenReaderOnly>
@@ -13,7 +13,7 @@ declare const __propDef: {
13
13
  value: string;
14
14
  disabled?: boolean | undefined;
15
15
  }[];
16
- defaultValue?: any;
16
+ defaultValue?: string | undefined;
17
17
  conditional?: boolean | undefined;
18
18
  required?: boolean | undefined;
19
19
  inputelement?: HTMLInputElement | undefined;
@@ -11,6 +11,7 @@ export let disabled = false;
11
11
  export let defaultValue = [];
12
12
  export let conditional = undefined;
13
13
  export let required = false;
14
+ export let maxSelections = 0;
14
15
  export let getOptions;
15
16
  // each time we run getOptions we will save the value -> label mappings
16
17
  // that it finds, so that we can display labels on pills
@@ -54,7 +55,7 @@ async function reactToValue(value) {
54
55
  <FieldStandard bind:id {label} {path} {required} {defaultValue} {conditional} let:value let:valid let:invalid let:id let:onBlur let:setVal>
55
56
  {@const _ = reactToValue(value)}
56
57
  <div class:valid class:invalid>
57
- <MultiSelect {id} name={path} usePortal={portal} {disabled} selected={$selectedStore} {placeholder} getOptions={wrapGetOptions} on:change={e => setVal(e.detail.map(itm => itm.value))} on:blur={onBlur}></MultiSelect>
58
+ <MultiSelect {id} name={path} usePortal={portal} {disabled} {maxSelections} selected={$selectedStore} {placeholder} getOptions={wrapGetOptions} on:change={e => setVal(e.detail.map(itm => itm.value))} on:blur={onBlur}></MultiSelect>
58
59
  </div>
59
60
  </FieldStandard>
60
61
  {/if}
@@ -10,6 +10,7 @@ declare const __propDef: {
10
10
  defaultValue?: string[] | undefined;
11
11
  conditional?: boolean | undefined;
12
12
  required?: boolean | undefined;
13
+ maxSelections?: number | undefined;
13
14
  getOptions: (search: string) => Promise<PopupMenuItem[]>;
14
15
  };
15
16
  events: {
package/Tab.svelte CHANGED
@@ -15,12 +15,16 @@ const idx = $store.tabs.findIndex(t => t.name === name);
15
15
  const last = idx === $store.tabs.length - 1;
16
16
  </script>
17
17
 
18
- {#if $accordion}
19
- <div bind:this={tabelements[idx]} id={$tabid} class="tabs-tab" class:last aria-selected={active} aria-controls={$panelid} role="tab" tabindex={0} on:click={onClick(idx)} on:keydown={onKeyDown(idx)}><Icon icon={$store.tabs[idx].icon} inline />{$title}<i class="tabs-accordion-arrow" aria-hidden="true"></i></div>
20
- {/if}
21
- <div id={$panelid} hidden={!active} role="tabpanel" tabindex="-1" aria-labelledby={$tabid} class="tabs-panel" class:accordion={$accordion}>
18
+ {#if $store.tabs.length > 1}
19
+ {#if $accordion}
20
+ <div bind:this={tabelements[idx]} id={$tabid} class="tabs-tab" class:last aria-selected={active} aria-controls={$panelid} role="tab" tabindex={0} on:click={onClick(idx)} on:keydown={onKeyDown(idx)}><Icon icon={$store.tabs[idx].icon} inline />{$title}<i class="tabs-accordion-arrow" aria-hidden="true"></i></div>
21
+ {/if}
22
+ <div id={$panelid} hidden={!active} role="tabpanel" tabindex="-1" aria-labelledby={$tabid} class="tabs-panel" class:accordion={$accordion}>
23
+ <slot />
24
+ </div>
25
+ {:else}
22
26
  <slot />
23
- </div>
27
+ {/if}
24
28
 
25
29
  <style>
26
30
  .tabs-panel {
package/Tabs.svelte CHANGED
@@ -85,20 +85,24 @@ $: reactToCurrent($activeStore);
85
85
  onMount(reactToCurrent);
86
86
  </script>
87
87
 
88
- {#if !$accordion}
89
- <ul use:resize={{ store }} class="tabs-buttons" role="tablist">
90
- {#each $store.tabs as tab, idx (tab.name)}
91
- {@const active = isActive(idx, $store.current)}
92
- {@const left = idx % cols === 0}
93
- <li bind:this={tabelements[idx]} use:offset={{ store: active ? activeStore : undefined }} id={$store.tabids[tab.name]} class="tabs-tab" class:left class:wrapping class:active style:font-size="{scalefactor}em" style:line-height={1.2 / scalefactor} aria-selected={active} aria-controls={$store.panelids[tab.name]} role="tab" tabindex={active ? 0 : -1} on:click={onClick(idx)} on:keydown={onKeyDown(idx)}><span><Icon icon={tab.icon} inline />{tab.title}</span></li>
94
- {/each}
95
- </ul>
96
- <div bind:this={activeelement} class="tabs-active"></div>
97
- <slot current={$store.current} />
98
- {:else}
99
- <div use:resize={{ store }} class="tabs-container">
88
+ {#if $store.tabs.length > 1}
89
+ {#if !$accordion}
90
+ <ul use:resize={{ store }} class="tabs-buttons" role="tablist">
91
+ {#each $store.tabs as tab, idx (tab.name)}
92
+ {@const active = isActive(idx, $store.current)}
93
+ {@const left = idx % cols === 0}
94
+ <li bind:this={tabelements[idx]} use:offset={{ store: active ? activeStore : undefined }} id={$store.tabids[tab.name]} class="tabs-tab" class:left class:wrapping class:active style:font-size="{scalefactor}em" style:line-height={1.2 / scalefactor} aria-selected={active} aria-controls={$store.panelids[tab.name]} role="tab" tabindex={active ? 0 : -1} on:click={onClick(idx)} on:keydown={onKeyDown(idx)}><span><Icon icon={tab.icon} inline />{tab.title}</span></li>
95
+ {/each}
96
+ </ul>
97
+ <div bind:this={activeelement} class="tabs-active"></div>
100
98
  <slot current={$store.current} />
101
- </div>
99
+ {:else}
100
+ <div use:resize={{ store }} class="tabs-container">
101
+ <slot current={$store.current} />
102
+ </div>
103
+ {/if}
104
+ {:else}
105
+ <slot current={$store.current} />
102
106
  {/if}
103
107
 
104
108
  <style>
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@dosgato/dialog",
3
3
  "description": "A component library for building forms that edit a JSON document.",
4
- "version": "0.0.41",
4
+ "version": "0.0.43",
5
5
  "dependencies": {
6
- "@txstate-mws/svelte-components": "^1.3.8",
6
+ "@txstate-mws/svelte-components": "^1.3.9",
7
7
  "@txstate-mws/svelte-forms": "^1.3.4",
8
8
  "@iconify/svelte": "^3.0.0",
9
9
  "@iconify-icons/mdi": "^1.2.22",