@threadlabs/looma 0.9.1 → 0.10.0
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/components/shared/form-reset.js +21 -0
- package/components/ui-avatar/ui-avatar.html +12 -4
- package/components/ui-avatar-group/ui-avatar-group.html +5 -2
- package/components/ui-badge/ui-badge.html +6 -2
- package/components/ui-button/ui-button.html +6 -2
- package/components/ui-callout/ui-callout.html +3 -1
- package/components/ui-checkbox/ui-checkbox.html +24 -6
- package/components/ui-checkbox/ui-checkbox.js +9 -0
- package/components/ui-cluster/ui-cluster.html +6 -2
- package/components/ui-combobox/ui-combobox.html +54 -20
- package/components/ui-combobox/ui-combobox.js +31 -12
- package/components/ui-container/ui-container.html +5 -2
- package/components/ui-disclosure/ui-disclosure.html +6 -3
- package/components/ui-editable/ui-editable.html +3 -1
- package/components/ui-editable/ui-editable.js +7 -6
- package/components/ui-editor-mention-menu/ui-editor-mention-menu.html +3 -1
- package/components/ui-editor-slash-menu/ui-editor-slash-menu.html +3 -1
- package/components/ui-floating-action-button/ui-floating-action-button.html +5 -3
- package/components/ui-form-field/ui-form-field.html +9 -3
- package/components/ui-grid/ui-grid.html +6 -2
- package/components/ui-icon/ui-icon.html +3 -1
- package/components/ui-icon-button/ui-icon-button.html +11 -4
- package/components/ui-input/ui-input.html +3 -1
- package/components/ui-input/ui-input.js +5 -2
- package/components/ui-menu-item/ui-menu-item.html +5 -2
- package/components/ui-radio/ui-radio.html +12 -5
- package/components/ui-radio/ui-radio.js +11 -0
- package/components/ui-radio-group/ui-radio-group.html +17 -7
- package/components/ui-radio-group/ui-radio-group.js +16 -3
- package/components/ui-reel/ui-reel.html +9 -3
- package/components/ui-search-result-row/ui-search-result-row.html +5 -3
- package/components/ui-select/ui-select.html +3 -1
- package/components/ui-select/ui-select.js +5 -2
- package/components/ui-stack/ui-stack.html +9 -3
- package/components/ui-switch/ui-switch.html +10 -5
- package/components/ui-switch/ui-switch.js +9 -0
- package/components/ui-switcher/ui-switcher.html +9 -3
- package/components/ui-tabs/ui-tabs.html +8 -3
- package/components/ui-textarea/ui-textarea.html +3 -1
- package/components/ui-textarea/ui-textarea.js +5 -2
- package/components/ui-tree/ui-tree.html +7 -3
- package/components/ui-tree-item/ui-tree-item.html +32 -11
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/vanilla/UiCheckbox.d.ts +1 -0
- package/vanilla/UiCheckbox.js +7 -4
- package/vanilla/UiCombobox.js +113 -99
- package/vanilla/UiRadioGroup.js +7 -1
- package/vanilla/UiSearchResultRow.js +4 -1
- package/vanilla/UiSwitch.d.ts +1 -0
- package/vanilla/UiSwitch.js +7 -4
- package/vue/UiCheckbox.d.ts +2 -0
- package/vue/UiCheckbox.vue +3 -0
- package/vue/UiCombobox.vue +27 -4
- package/vue/UiRadioGroup.vue +2 -0
- package/vue/UiSearchResultRow.vue +1 -0
- package/vue/UiSwitch.d.ts +2 -0
- package/vue/UiSwitch.vue +3 -0
- package/vue/chunks/UiCheckbox.js +4 -1
- package/vue/chunks/UiCombobox.js +67 -39
- package/vue/chunks/UiRadioGroup.js +8 -2
- package/vue/chunks/UiSearchResultRow.js +7 -2
- package/vue/chunks/UiSwitch.js +4 -1
- package/vue/components.css +89 -89
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calls back once a form reset has restored `control`'s form to its defaults. The browser restores
|
|
3
|
+
* each native control after the reset event has been dispatched, and a reset button's event runs
|
|
4
|
+
* its listeners before that, so the callback waits a task.
|
|
5
|
+
*
|
|
6
|
+
* @ownership A controller that keeps its own copy of a control's state reads it back here; the
|
|
7
|
+
* native defaults (defaultValue, defaultChecked, defaultSelected) are what the reset restores.
|
|
8
|
+
* @lifecycle Returns the function that stops listening.
|
|
9
|
+
*/
|
|
10
|
+
export function afterFormReset(control, callback) {
|
|
11
|
+
const document = control.ownerDocument;
|
|
12
|
+
let timer;
|
|
13
|
+
const onReset = (event) => {
|
|
14
|
+
if (event.target === control.form) timer = setTimeout(callback);
|
|
15
|
+
};
|
|
16
|
+
document.addEventListener("reset", onReset);
|
|
17
|
+
return () => {
|
|
18
|
+
clearTimeout(timer);
|
|
19
|
+
document.removeEventListener("reset", onReset);
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
<template component="ui-avatar" controller="./ui-avatar.js">
|
|
2
2
|
<defs>
|
|
3
|
-
<prop name="src" type="string"
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
<prop name="
|
|
3
|
+
<prop name="src" type="string"
|
|
4
|
+
>The image to show, cropped to fill the circle. Until it loads, or if it fails, the avatar shows initials instead. An img element authored inside the avatar
|
|
5
|
+
takes precedence over src.</prop>
|
|
6
|
+
<prop name="alt" type="string" default=""
|
|
7
|
+
>The avatar's accessible name, announced for it as an image. When empty, name is used, then "Avatar". Initials come from name first, so alt supplies them
|
|
8
|
+
only when name is empty. An authored img carries its own alt instead.</prop>
|
|
9
|
+
<prop name="name" type="string" default=""
|
|
10
|
+
>The person's name. The initials are drawn from it: the first letters of the first two words, or the first two letters of a single word. It is also the
|
|
11
|
+
accessible name when alt is empty.</prop>
|
|
12
|
+
<prop name="fallback" type="string" default=""
|
|
13
|
+
>Text shown instead of the initials derived from name or alt, whenever there is no image. It is uppercased; keep it to a couple of characters, because the
|
|
14
|
+
circle clips anything wider.</prop>
|
|
7
15
|
<state name="hasImage" :value="false"></state>
|
|
8
16
|
<state name="hasAuthoredImage" :value="false"></state>
|
|
9
17
|
<state name="initials" :value="'?'"></state>
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
<template component="ui-avatar-group" controller="./ui-avatar-group.js">
|
|
2
2
|
<defs>
|
|
3
|
-
<prop name="max" type="number" default="5"
|
|
4
|
-
|
|
3
|
+
<prop name="max" type="number" default="5"
|
|
4
|
+
>How many avatars to show. The rest are hidden and counted in a +N badge after the last visible one. Fractions round down, and 0 hides every avatar behind
|
|
5
|
+
the badge.</prop>
|
|
6
|
+
<prop name="label" type="string" default="People"
|
|
7
|
+
>The group's accessible name, announced for the set of avatars as a whole. Name who they are rather than repeating the default.</prop>
|
|
5
8
|
<state name="overflowCount" :value="0"></state>
|
|
6
9
|
</defs>
|
|
7
10
|
<div role="group" :aria-label="label">
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
<template component="ui-badge">
|
|
2
2
|
<defs>
|
|
3
|
-
<prop name="variant" type="solid | subtle" default="subtle"
|
|
4
|
-
|
|
3
|
+
<prop name="variant" type="solid | subtle" default="subtle"
|
|
4
|
+
>Emphasis. subtle sets the text in its tone over a soft wash of it; solid fills with the tone and uses contrasting text, for a status that has to stand out.
|
|
5
|
+
The neutral tone looks the same in both.</prop>
|
|
6
|
+
<prop name="tone" type="neutral | accent | info | success | warning | danger" default="neutral"
|
|
7
|
+
>Which colour the badge speaks in. neutral is a quiet grey for plain labels; accent draws the eye in the accent colour; info, success, warning, and danger
|
|
8
|
+
mark a status, so match the tone to what the badge says.</prop>
|
|
5
9
|
<prop name="shape" type="pill | tag" default="pill"
|
|
6
10
|
>Silhouette. pill is rounded at both ends; tag is a label: flat at the start and coming to a point at the end, for a tag or a chosen chip.</prop>
|
|
7
11
|
</defs>
|
|
@@ -3,11 +3,15 @@
|
|
|
3
3
|
<prop name="variant" type="outline | solid | danger | ghost | link" default="outline"
|
|
4
4
|
>Emphasis: how loudly the button asks. solid fills with its tone, outline draws its tone at the edge over a wash of it, ghost carries only text until it is
|
|
5
5
|
hovered, and link is an inline text action that reads as a link. danger is solid in the danger tone, kept for the call sites that name it.</prop>
|
|
6
|
-
<prop name="size" type="sm | md | lg" default="md"
|
|
6
|
+
<prop name="size" type="sm | md | lg" default="md"
|
|
7
|
+
>Height, padding, and type size. sm is the small control height (2rem by default) with tighter padding and smaller text, md is the standard control height
|
|
8
|
+
(2.5rem), and lg is taller (3rem) with roomier padding.</prop>
|
|
7
9
|
<prop name="tone" type="accent | neutral | danger | success | warning | info" default="accent"
|
|
8
10
|
>Which colour the button speaks in. It applies to every variant, so an outline, a ghost, and a solid in the same tone are the same action at three volumes:
|
|
9
11
|
tone="danger" variant="outline" is a destructive secondary action. neutral is the quiet one, for a button that should not claim a colour.</prop>
|
|
10
|
-
<prop name="disabled" type="boolean" default="false"
|
|
12
|
+
<prop name="disabled" type="boolean" default="false"
|
|
13
|
+
>Makes the button unavailable: it cannot be pressed, its tone fades, and it loses its raised look. A disabled link (as="a" with href) drops its href and
|
|
14
|
+
states aria-disabled="true" instead, because a link has no native disabled state.</prop>
|
|
11
15
|
<prop name="align" type="center | start" default="center"
|
|
12
16
|
>Content alignment. start lays the content out like a list option: from the start edge, with start-aligned text.</prop>
|
|
13
17
|
<prop name="stretch" type="boolean" default="false">Fills the inline size of its container.</prop>
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<template component="ui-callout">
|
|
2
2
|
<defs>
|
|
3
|
-
<prop name="tone" type="info | note | warning | success | danger" default="info"
|
|
3
|
+
<prop name="tone" type="info | note | warning | success | danger" default="info"
|
|
4
|
+
>Which kind of message the callout carries, shown by its icon and colour: info (the default) for neutral information, note for an aside in muted grey,
|
|
5
|
+
warning for a caution, success for a confirmation, and danger for an error or a destructive consequence.</prop>
|
|
4
6
|
</defs>
|
|
5
7
|
<div role="note">
|
|
6
8
|
<span class="icon" aria-hidden="true">
|
|
@@ -1,15 +1,33 @@
|
|
|
1
1
|
<template component="ui-checkbox" controller="./ui-checkbox.js">
|
|
2
2
|
<defs>
|
|
3
|
-
<prop name="checked" type="boolean" default="false"
|
|
4
|
-
|
|
5
|
-
<prop name="
|
|
6
|
-
<prop name="
|
|
7
|
-
|
|
3
|
+
<prop name="checked" type="boolean" default="false"
|
|
4
|
+
>Whether the checkbox is checked initially or when set externally. A form reset returns it to this.</prop>
|
|
5
|
+
<prop name="disabled" type="boolean" default="false">Disables the native checkbox, so it cannot be toggled or focused.</prop>
|
|
6
|
+
<prop name="indeterminate" type="boolean" default="false"
|
|
7
|
+
>Shows the mixed state, for a checkbox that summarises a group whose items are partly checked. It sets the native indeterminate property, which is
|
|
8
|
+
independent of checked, so update it as the group changes.</prop>
|
|
9
|
+
<prop name="required" type="boolean" default="false"
|
|
10
|
+
>Marks the native checkbox required, so native form validation treats it as invalid until it is checked.</prop>
|
|
11
|
+
<prop name="name" type="string" default=""
|
|
12
|
+
>The native name the checkbox submits under: a form that contains it sends name=value while it is checked, and nothing while it is unchecked, as a native
|
|
13
|
+
checkbox does. Left empty, it takes no part in form submission.</prop>
|
|
14
|
+
<prop name="value" type="string" default="on"
|
|
15
|
+
>The value a form submits under name while the checkbox is checked, also reported in the change event's detail so one handler can tell checkboxes apart. on
|
|
16
|
+
by default, as on a native checkbox.</prop>
|
|
8
17
|
<state name="internalChecked" :value="false"></state>
|
|
9
18
|
<event name="change" type="object({ checked: boolean, value: string, trigger: keyboard | pointer | programmatic })"></event>
|
|
10
19
|
</defs>
|
|
11
20
|
<label>
|
|
12
|
-
<input
|
|
21
|
+
<input
|
|
22
|
+
type="checkbox"
|
|
23
|
+
$ref="input"
|
|
24
|
+
.checked="internalChecked"
|
|
25
|
+
.indeterminate="indeterminate"
|
|
26
|
+
:disabled="disabled"
|
|
27
|
+
:required="required"
|
|
28
|
+
:name="name"
|
|
29
|
+
:value="value"
|
|
30
|
+
>
|
|
13
31
|
<span class="label"><slot></slot></span>
|
|
14
32
|
</label>
|
|
15
33
|
<style>
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
+
import { afterFormReset } from "../shared/form-reset.js";
|
|
1
2
|
import { trackTrigger } from "../shared/trigger.js";
|
|
2
3
|
|
|
3
4
|
// `checked` sets the control initially and whenever it changes; the user's changes update the state.
|
|
5
|
+
// A form reset returns the control to `checked`. Vue writes the checked attribute on every render, so
|
|
6
|
+
// the native default alone cannot hold it there.
|
|
4
7
|
export default function controller(host) {
|
|
5
8
|
const input = host.refs.input;
|
|
6
9
|
const [trigger, stopTracking] = trackTrigger(host);
|
|
7
10
|
let external = host.state.checked;
|
|
8
11
|
host.state.internalChecked = Boolean(external);
|
|
9
12
|
const stop = host.effect(() => {
|
|
13
|
+
input.defaultChecked = Boolean(host.state.checked);
|
|
10
14
|
if (host.state.checked === external) return;
|
|
11
15
|
external = host.state.checked;
|
|
12
16
|
host.state.internalChecked = Boolean(external);
|
|
@@ -16,8 +20,13 @@ export default function controller(host) {
|
|
|
16
20
|
host.dispatch("change", { checked: input.checked, value: String(host.state.value ?? "on"), trigger: trigger() });
|
|
17
21
|
};
|
|
18
22
|
input.addEventListener("change", onChange);
|
|
23
|
+
const stopReset = afterFormReset(input, () => {
|
|
24
|
+
host.state.internalChecked = Boolean(external);
|
|
25
|
+
input.checked = Boolean(external);
|
|
26
|
+
});
|
|
19
27
|
return () => {
|
|
20
28
|
stop();
|
|
29
|
+
stopReset();
|
|
21
30
|
stopTracking();
|
|
22
31
|
input.removeEventListener("change", onChange);
|
|
23
32
|
};
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
<template component="ui-cluster">
|
|
2
2
|
<defs>
|
|
3
|
-
<prop name="gap" type="xs | s | m | l | xl"
|
|
4
|
-
|
|
3
|
+
<prop name="gap" type="xs | s | m | l | xl"
|
|
4
|
+
>Space between items, on the spacing scale: xs is 0.5rem, s 0.75rem (also the default), m 1rem, l 1.5rem, and xl 2rem. The --ui-cluster-gap custom property,
|
|
5
|
+
when set, overrides it.</prop>
|
|
6
|
+
<prop name="align" type="start | center | end | stretch"
|
|
7
|
+
>How items line up within each row: center (the default) aligns their middles, start their tops, end their bottoms, and stretch makes every item as tall as
|
|
8
|
+
its row.</prop>
|
|
5
9
|
</defs>
|
|
6
10
|
<div><slot></slot></div>
|
|
7
11
|
<style>
|
|
@@ -2,25 +2,54 @@
|
|
|
2
2
|
<link rel="component" href="../ui-tooltip/ui-tooltip.html">
|
|
3
3
|
<template component="ui-combobox" controller="./ui-combobox.js">
|
|
4
4
|
<defs>
|
|
5
|
-
<prop name="label" type="string" default=""
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
<prop name="
|
|
5
|
+
<prop name="label" type="string" default=""
|
|
6
|
+
>The field's label, tied to its input and shown above it; required adds an asterisk. It also names the suggestion list. Pair it with
|
|
7
|
+
labelVisibility="sr-only" when the context already labels the field visibly.</prop>
|
|
8
|
+
<prop name="placeholder" type="string" default=""
|
|
9
|
+
>Hint text shown in the empty input, as on a native input. It disappears as the user types, so it never replaces label.</prop>
|
|
10
|
+
<prop name="name" type="string" default=""
|
|
11
|
+
>The form field name the combobox submits its value under, never the label shown in the field. Single mode sends one entry: the selected option's value, the
|
|
12
|
+
typed text when allowFreeText is set and no option is selected, or an empty string. Multiple mode sends one entry per chosen item's value, and nothing
|
|
13
|
+
when none is chosen. A disabled combobox sends nothing.</prop>
|
|
14
|
+
<prop name="value" type="string | null"
|
|
15
|
+
>The selected value in single mode, which a named combobox submits with its form. The field shows the matching option's label, and a form reset returns to
|
|
16
|
+
this value.</prop>
|
|
9
17
|
<prop name="items" type="list(object({ id: string, value: string, label: string, group?: string, disabled?: boolean }))"
|
|
10
|
-
>The selected items in multiple mode, as JSON.</prop>
|
|
11
|
-
<prop name="multiple" type="boolean" default="false"
|
|
12
|
-
|
|
13
|
-
|
|
18
|
+
>The selected items in multiple mode, as JSON; a named combobox submits each one's value. A form reset returns to these items, or to none when unset.</prop>
|
|
19
|
+
<prop name="multiple" type="boolean" default="false"
|
|
20
|
+
>Lets the user choose several options. Each choice becomes a removable badge before the input, the list stays open between picks, and choosing a checked
|
|
21
|
+
option removes it. value-change then reports the whole list of items; use items, not value, in this mode.</prop>
|
|
22
|
+
<prop name="tokenSeparators" type="list(string)"
|
|
23
|
+
>Keys that commit the typed text in multiple mode, as KeyboardEvent key values such as "," or ";". The text is added as the option whose label matches it,
|
|
24
|
+
ignoring case, or with allowCreate is reported as create-item; otherwise it stays in the input.</prop>
|
|
25
|
+
<prop name="query" type="string"
|
|
26
|
+
>The input's text, when the consumer controls it. Once set, the consumer owns the text: typing reports query-change, and the input returns to query unless
|
|
27
|
+
query is updated to match. Leave it unset to let the combobox manage its own text, showing the chosen option's label.</prop>
|
|
14
28
|
<prop name="allowFreeText" type="boolean" default="false">Allows values that do not match an authored option.</prop>
|
|
15
29
|
<prop name="allowCreate" type="boolean" default="false">Offers the current query as a new option.</prop>
|
|
16
|
-
<prop name="disabled" type="boolean" default="false"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
<prop name="
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
<prop name="
|
|
23
|
-
|
|
30
|
+
<prop name="disabled" type="boolean" default="false"
|
|
31
|
+
>Disables the input and every button in the field: the chosen-item badges, the clear and disclosure buttons, and the help button. The list closes and cannot
|
|
32
|
+
be opened, the value cannot be cleared, and a named combobox submits nothing with its form.</prop>
|
|
33
|
+
<prop name="readonly" type="boolean" default="false"
|
|
34
|
+
>Shows the current value without letting it change: the text can be selected but not edited, the list does not open, and the clear, disclosure, and badge
|
|
35
|
+
remove buttons are disabled. A named combobox still submits its value.</prop>
|
|
36
|
+
<prop name="required" type="boolean" default="false"
|
|
37
|
+
>Requires a value: adds an asterisk to the label, marks the input required for native form validation, and makes validation report "A value is required."
|
|
38
|
+
when the field is left empty. In multiple mode a chosen item satisfies it, whatever is left in the input.</prop>
|
|
39
|
+
<prop name="size" type="sm | md" default="md"
|
|
40
|
+
>Field height. md is the standard control height; sm is the small control height with tighter padding, for dense forms.</prop>
|
|
41
|
+
<prop name="labelVisibility" type="visible | sr-only" default="visible"
|
|
42
|
+
>visible shows the label above the field; sr-only hides it visually but keeps it as the input's accessible name. Use sr-only only when the surrounding
|
|
43
|
+
layout already makes the field's purpose clear.</prop>
|
|
44
|
+
<prop name="disclosure" type="boolean" default="false"
|
|
45
|
+
>Adds a chevron button that opens and closes the full, unfiltered list of options, for users who would rather browse than type. ArrowDown opens the same
|
|
46
|
+
list with or without it.</prop>
|
|
47
|
+
<prop name="clearable" type="boolean" default="false"
|
|
48
|
+
>Adds a clear button to the field that empties the input and clears the selection, reported as value-change with kind "clear". In multiple mode it clears
|
|
49
|
+
only the typed text; chosen items stay.</prop>
|
|
50
|
+
<prop name="help" type="string" default=""
|
|
51
|
+
>Help text for the field. When set, a help button appears beside the field and opens a tooltip with this text; while it is open, the input is described by
|
|
52
|
+
it.</prop>
|
|
24
53
|
<state name="internalItems" type="list(object({ id: string, value: string, label: string, group?: string, disabled?: boolean }))" :value="[]"></state>
|
|
25
54
|
<state name="raw" :value="''"></state>
|
|
26
55
|
<state name="display" :value="''"></state>
|
|
@@ -41,6 +70,7 @@
|
|
|
41
70
|
:value="{ status: 'pristine', touched: false, dirty: false, issues: [] }"
|
|
42
71
|
></state>
|
|
43
72
|
<state name="uid" :value="''"></state>
|
|
73
|
+
<state name="submitted" :value="''"></state>
|
|
44
74
|
<state
|
|
45
75
|
name="groups"
|
|
46
76
|
type="list(object({ name: string, role: group | presentation, label: string | null, rows: list(object({ id: string, value: string, label: string, group?: string, disabled: boolean, selected: boolean, index: integer })) }))"
|
|
@@ -113,12 +143,15 @@
|
|
|
113
143
|
:aria-busy="validationStatus = 'pending'"
|
|
114
144
|
.value="display"
|
|
115
145
|
:placeholder="placeholder"
|
|
116
|
-
:name="name"
|
|
117
146
|
:disabled="disabled"
|
|
118
147
|
:readonly="readonly"
|
|
119
|
-
:required="required"
|
|
148
|
+
:required="required and not (multiple and internalItems[0])"
|
|
120
149
|
>
|
|
121
|
-
<
|
|
150
|
+
<input $if="name and not multiple" type="hidden" :name="name" :value="submitted" :disabled="disabled">
|
|
151
|
+
<template $if="name and multiple">
|
|
152
|
+
<input $each="item of internalItems" $key="item.id" type="hidden" :name="name" :value="item.value" :disabled="disabled">
|
|
153
|
+
</template>
|
|
154
|
+
<button $if="clearable" type="button" data-combobox-action="clear" aria-label="Clear" :disabled="disabled or readonly">
|
|
122
155
|
<svg class="affordance-icon" aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
|
123
156
|
<path d="M7 7l10 10M17 7 7 17"></path>
|
|
124
157
|
</svg>
|
|
@@ -130,6 +163,7 @@
|
|
|
130
163
|
tabindex="-1"
|
|
131
164
|
aria-label="Show suggestions"
|
|
132
165
|
:aria-controls="format('%s-listbox', uid)"
|
|
166
|
+
:disabled="disabled or readonly"
|
|
133
167
|
>
|
|
134
168
|
<svg
|
|
135
169
|
class="affordance-icon"
|
|
@@ -196,7 +230,7 @@
|
|
|
196
230
|
</div>
|
|
197
231
|
<slot name="footer"></slot>
|
|
198
232
|
</div>
|
|
199
|
-
<div class="message" :id="format('%s-validation', uid)" :hidden="not validation.issues
|
|
233
|
+
<div class="message" :id="format('%s-validation', uid)" :hidden="not validation.issues[0]">
|
|
200
234
|
<div $each="issue of validation.issues" $value="issue.message"></div>
|
|
201
235
|
</div>
|
|
202
236
|
<div class="sr-only" role="status" aria-live="polite" aria-atomic="true" $value="statusText"></div>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { afterFormReset } from "../shared/form-reset.js";
|
|
1
2
|
import { closeOverlay, createAnchoredSurface, openOverlay } from "../shared/overlay.js";
|
|
2
3
|
|
|
3
4
|
const instances = new WeakMap();
|
|
@@ -288,7 +289,8 @@ export default function controller(host) {
|
|
|
288
289
|
// Native constraints and the free-text policy; application validation stays in the form.
|
|
289
290
|
const current = config();
|
|
290
291
|
const result = { output: host.state.raw, issues: [] };
|
|
291
|
-
|
|
292
|
+
const empty = host.state.multiple ? !items().length : !String(host.state.raw).trim();
|
|
293
|
+
if (host.state.required && empty) result.issues = [...result.issues, { message: "A value is required." }];
|
|
292
294
|
else if (host.state.raw && host.state.selected === null && !current.allowFreeText && !current.allowCreate) result.issues = [...result.issues, { message: "Choose a suggestion." }];
|
|
293
295
|
if (result.issues.some((issue) => issue.severity !== "warning")) result.output = undefined;
|
|
294
296
|
if (!run.signal.aborted && alive) setValidation(result);
|
|
@@ -335,6 +337,8 @@ export default function controller(host) {
|
|
|
335
337
|
host.state.groups = groups;
|
|
336
338
|
host.state.creatable = canCreate();
|
|
337
339
|
host.state.createIndex = rows.length;
|
|
340
|
+
// What a named combobox submits in single mode: the value, never the label the field shows.
|
|
341
|
+
host.state.submitted = String(host.state.selected ?? (host.state.allowFreeText ? host.state.raw ?? "" : ""));
|
|
338
342
|
host.state.message = host.state.loading ? "Loading suggestions…"
|
|
339
343
|
: host.state.lookupError || (!rows.length && !host.state.creatable ? "No suggestions." : "");
|
|
340
344
|
const validation = host.state.validation ?? { status: "pristine", issues: [] };
|
|
@@ -388,6 +392,7 @@ export default function controller(host) {
|
|
|
388
392
|
const option = event.target.closest?.('[role="option"][data-index]');
|
|
389
393
|
if (option) { choose(Number(option.dataset.index), "pointer"); return; }
|
|
390
394
|
const action = event.target.closest?.("[data-combobox-action]")?.dataset.comboboxAction;
|
|
395
|
+
if (action && (host.state.disabled || host.state.readonly)) return;
|
|
391
396
|
if (action === "clear") { lastSelection = null; commit(null, "", null, "clear", "pointer"); close(); input.focus(); }
|
|
392
397
|
else if (action === "disclosure") { host.state.expanded ? close() : open("disclosure"); input.focus(); }
|
|
393
398
|
else if (field.contains(event.target) && !event.target.closest?.("button")) input.focus();
|
|
@@ -406,18 +411,22 @@ export default function controller(host) {
|
|
|
406
411
|
const onTooltipOpen = (event) => { if (event.target.closest?.('[data-component~="ui-tooltip"]')) host.state.helpOpen = true; };
|
|
407
412
|
const onTooltipClose = (event) => { if (event.target.closest?.('[data-component~="ui-tooltip"]')) host.state.helpOpen = false; };
|
|
408
413
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
host.state.raw =
|
|
414
|
+
// The selection the props describe: where the combobox starts, and where a form reset returns it.
|
|
415
|
+
const applyDefaults = () => {
|
|
416
|
+
if (host.state.multiple) {
|
|
417
|
+
host.state.selected = null;
|
|
418
|
+
host.state.raw = host.state.query ?? "";
|
|
419
|
+
} else {
|
|
420
|
+
host.state.selected = host.state.value ?? null;
|
|
421
|
+
host.state.raw = host.state.query ?? "";
|
|
422
|
+
if (host.state.query === undefined && !host.state.raw && host.state.selected !== null) {
|
|
423
|
+
host.state.raw = config().options?.find((row) => row.value === host.state.selected)?.label ?? host.state.selected;
|
|
424
|
+
}
|
|
425
|
+
if (host.state.query === undefined && host.state.selected !== null && host.state.raw === host.state.selected) awaitingLabel = host.state.selected;
|
|
417
426
|
}
|
|
418
|
-
|
|
419
|
-
}
|
|
420
|
-
|
|
427
|
+
host.state.display = host.state.raw;
|
|
428
|
+
};
|
|
429
|
+
applyDefaults();
|
|
421
430
|
initialRaw = host.state.raw;
|
|
422
431
|
host.state.validation = { status: "pristine", touched: false, dirty: false, issues: [] };
|
|
423
432
|
surface = createAnchoredSurface(popup, { anchor: field, placement: "bottom-start" });
|
|
@@ -437,6 +446,15 @@ export default function controller(host) {
|
|
|
437
446
|
host.state.display = label;
|
|
438
447
|
awaitingLabel = null;
|
|
439
448
|
};
|
|
449
|
+
// Like a native control, a reset reports no change; the browser has already emptied the input.
|
|
450
|
+
const stopReset = afterFormReset(input, () => {
|
|
451
|
+
close();
|
|
452
|
+
lastSelection = null;
|
|
453
|
+
host.state.internalItems = Array.isArray(host.state.items) ? host.state.items : [];
|
|
454
|
+
applyDefaults();
|
|
455
|
+
input.value = host.state.display;
|
|
456
|
+
resetValidation();
|
|
457
|
+
});
|
|
440
458
|
const observer = new MutationObserver(relabel);
|
|
441
459
|
observer.observe(authored, { childList: true, subtree: true, characterData: true, attributes: true });
|
|
442
460
|
let lastItems = host.state.items;
|
|
@@ -456,6 +474,7 @@ export default function controller(host) {
|
|
|
456
474
|
return () => {
|
|
457
475
|
alive = false;
|
|
458
476
|
stop();
|
|
477
|
+
stopReset();
|
|
459
478
|
observer.disconnect();
|
|
460
479
|
for (const [name, listener] of Object.entries(listeners)) element.removeEventListener(name, listener);
|
|
461
480
|
close();
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
<template component="ui-container">
|
|
2
2
|
<defs>
|
|
3
|
-
<prop name="measure" type="narrow | wide"
|
|
4
|
-
|
|
3
|
+
<prop name="measure" type="narrow | wide"
|
|
4
|
+
>The container's maximum width, in characters of the current font: narrow is 45ch, wide is 80ch, and unset is 65ch, a comfortable reading line. The
|
|
5
|
+
container centres itself when its parent is wider. The --ui-container-measure custom property, when set, overrides it.</prop>
|
|
6
|
+
<prop name="gutters" type="s | m | l"
|
|
7
|
+
>Inline padding on both sides, which keeps content off the edges of a narrow screen: s is 0.75rem, m is 1rem (also the default), and l is 1.5rem.</prop>
|
|
5
8
|
</defs>
|
|
6
9
|
<div><slot></slot></div>
|
|
7
10
|
<style>
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
<template component="ui-disclosure" controller="./ui-disclosure.js">
|
|
2
2
|
<defs>
|
|
3
3
|
<prop name="density" type="comfortable | compact" default="comfortable">Compact trades padding and type size for rows that fit more on screen.</prop>
|
|
4
|
-
<prop name="summary" type="string" default="Details"
|
|
5
|
-
|
|
6
|
-
<prop name="
|
|
4
|
+
<prop name="summary" type="string" default="Details"
|
|
5
|
+
>The text of the trigger row that opens and closes the disclosure. It is the trigger button's accessible name, so name what the content is.</prop>
|
|
6
|
+
<prop name="open" type="boolean" default="false"
|
|
7
|
+
>Whether the disclosure is open initially or when set externally. The trigger toggles it from there, reporting each change as an open or close event.</prop>
|
|
8
|
+
<prop name="disabled" type="boolean" default="false"
|
|
9
|
+
>Disables the trigger and mutes its text, so the disclosure keeps its current state and the user cannot toggle it.</prop>
|
|
7
10
|
<state name="internalOpen" :value="false"></state>
|
|
8
11
|
<state name="contentId" :value="''"></state>
|
|
9
12
|
<event
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<template component="ui-editable" controller="./ui-editable.js">
|
|
2
2
|
<defs>
|
|
3
|
-
<prop name="value" type="string" default=""
|
|
3
|
+
<prop name="value" type="string" default=""
|
|
4
|
+
>The displayed value, and the text the editor starts from. It is not a form field: nothing is submitted with a form, so a form that needs the value copies
|
|
5
|
+
it from the change event into a field of its own.</prop>
|
|
4
6
|
<prop name="label" type="string" default="Edit value">Accessible name for the editor.</prop>
|
|
5
7
|
<prop name="edit" type="boolean" default="false">Whether edit mode is active initially or when set externally.</prop>
|
|
6
8
|
<prop name="disabled" type="boolean" default="false">Prevents entering or changing edit mode.</prop>
|
|
@@ -31,10 +31,11 @@ export default function controller(host) {
|
|
|
31
31
|
input.tabIndex = host.state.internalEdit ? 0 : -1;
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
-
const setEditing = (next, reason, trigger) => {
|
|
34
|
+
const setEditing = (next, reason, trigger, returnFocus = true) => {
|
|
35
35
|
if (host.state.disabled || Boolean(host.state.internalEdit) === next) return;
|
|
36
|
-
// Measured before the input is disabled, which drops focus to the body.
|
|
37
|
-
|
|
36
|
+
// Measured before the input is disabled, which drops focus to the body. A press elsewhere has
|
|
37
|
+
// not moved focus yet, so it says not to return it.
|
|
38
|
+
const focusWasInside = returnFocus && element.contains(document.activeElement);
|
|
38
39
|
if (next) host.state.draft = host.state.internalValue;
|
|
39
40
|
host.state.internalEdit = next;
|
|
40
41
|
apply();
|
|
@@ -51,11 +52,11 @@ export default function controller(host) {
|
|
|
51
52
|
input.select();
|
|
52
53
|
});
|
|
53
54
|
};
|
|
54
|
-
const commit = (trigger) => {
|
|
55
|
+
const commit = (trigger, returnFocus) => {
|
|
55
56
|
const previousValue = String(host.state.internalValue ?? "");
|
|
56
57
|
const value = String(host.state.draft ?? "");
|
|
57
58
|
host.state.internalValue = value;
|
|
58
|
-
setEditing(false, "commit", trigger);
|
|
59
|
+
setEditing(false, "commit", trigger, returnFocus);
|
|
59
60
|
if (value !== previousValue) host.dispatch("change", { value, previousValue, trigger });
|
|
60
61
|
};
|
|
61
62
|
const cancel = (reason, trigger) => {
|
|
@@ -87,7 +88,7 @@ export default function controller(host) {
|
|
|
87
88
|
};
|
|
88
89
|
// Leaving the field saves, as with other in-place editors; Escape is the way to discard.
|
|
89
90
|
const onDocumentPointerdown = (event) => {
|
|
90
|
-
if (host.state.internalEdit && !event.composedPath().includes(element)) commit("pointer");
|
|
91
|
+
if (host.state.internalEdit && !event.composedPath().includes(element)) commit("pointer", false);
|
|
91
92
|
};
|
|
92
93
|
// Only the input's own blur counts: hiding the display button during the swap also moves focus,
|
|
93
94
|
// and must not end the edit. Tabbing to another control saves; clicks away are handled above.
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
<prop name="open" type="boolean" default="false">Whether the menu is open.</prop>
|
|
4
4
|
<prop name="query" type="string" default="">The text typed after the @.</prop>
|
|
5
5
|
<prop name="items" type="list(object({ id: string, label: string, detail?: string, initials?: string }))">The people the menu offers.</prop>
|
|
6
|
-
<prop name="selectedIndex" type="integer" default="0"
|
|
6
|
+
<prop name="selectedIndex" type="integer" default="0"
|
|
7
|
+
>The zero-based index of the highlighted person, capped at the last one shown, and announced through aria-activedescendant. The menu does not handle arrow
|
|
8
|
+
keys itself: the editor sets this as the user moves, while hovering a person highlights them and reports a highlight event.</prop>
|
|
7
9
|
<prop name="loading" type="boolean" default="false">Whether a search is in progress.</prop>
|
|
8
10
|
<prop
|
|
9
11
|
name="anchorRect"
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
<prop name="open" type="boolean" default="false">Whether the menu is open.</prop>
|
|
5
5
|
<prop name="query" type="string" default="">The text typed after the slash.</prop>
|
|
6
6
|
<prop name="items" type="list(object({ title: string, description: string, icon: string }))">The blocks the menu offers.</prop>
|
|
7
|
-
<prop name="selectedIndex" type="integer" default="0"
|
|
7
|
+
<prop name="selectedIndex" type="integer" default="0"
|
|
8
|
+
>The zero-based index of the highlighted item. The menu does not handle arrow keys itself: the editor sets this as the user moves, while hovering an item
|
|
9
|
+
highlights it and reports a highlight event.</prop>
|
|
8
10
|
<prop
|
|
9
11
|
name="anchorRect"
|
|
10
12
|
type="object({ left?: number, top?: number, right?: number, bottom?: number, x?: number, y?: number, width?: number, height?: number }) | null"
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
<template component="ui-floating-action-button">
|
|
2
2
|
<defs>
|
|
3
|
-
<prop name="disabled" type="boolean" default="false">
|
|
4
|
-
<prop name="mobileOnly" type="boolean" default="false"
|
|
5
|
-
|
|
3
|
+
<prop name="disabled" type="boolean" default="false">Disables the button, so it cannot be pressed or focused, and fades it to 60% opacity.</prop>
|
|
4
|
+
<prop name="mobileOnly" type="boolean" default="false"
|
|
5
|
+
>Hides the button on viewports 768px wide and wider, for a shortcut that only phones need. Make sure the action is reachable another way at those
|
|
6
|
+
widths.</prop>
|
|
7
|
+
<prop name="label" type="string" default="">The button's accessible name. The button usually holds only an icon, so name the action it performs.</prop>
|
|
6
8
|
</defs>
|
|
7
9
|
<button type="button" :aria-label="label" :disabled="disabled"><slot></slot></button>
|
|
8
10
|
<style>
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
<template component="ui-form-field" controller="./ui-form-field.js">
|
|
2
2
|
<defs>
|
|
3
|
-
<prop name="invalid" type="boolean" default="false"
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
<prop name="invalid" type="boolean" default="false"
|
|
4
|
+
>Marks the field's control invalid by setting aria-invalid on it. Pair it with content in the error slot, which the field already links to the control as
|
|
5
|
+
its description.</prop>
|
|
6
|
+
<prop name="disabled" type="boolean" default="false"
|
|
7
|
+
>Disables the field's control: the first input, textarea, or select inside it. The field writes this to the control whenever it updates, so set it here
|
|
8
|
+
rather than on the control.</prop>
|
|
9
|
+
<prop name="required" type="boolean" default="false"
|
|
10
|
+
>Marks the field's control required, for native form validation. Like disabled, it is written to the control, so set it here rather than on the control. It
|
|
11
|
+
adds no visual marker; show one in the label if the form needs it.</prop>
|
|
6
12
|
</defs>
|
|
7
13
|
<div>
|
|
8
14
|
<div class="label"><slot name="label"></slot></div>
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
<template component="ui-grid">
|
|
2
2
|
<defs>
|
|
3
|
-
<prop name="gap" type="xs | s | m | l | xl"
|
|
4
|
-
|
|
3
|
+
<prop name="gap" type="xs | s | m | l | xl"
|
|
4
|
+
>Space between cells, on the spacing scale: xs is 0.5rem, s 0.75rem, m 1rem (also the default), l 1.5rem, and xl 2rem. The --ui-grid-gap custom property,
|
|
5
|
+
when set, overrides it.</prop>
|
|
6
|
+
<prop name="min" type="sm | md | lg"
|
|
7
|
+
>The narrowest a column may get before the grid drops to fewer columns: sm is 12rem (also the default), md is 16rem, and lg is 20rem. The grid fits as many
|
|
8
|
+
equal columns as its width allows, and a single column never overflows a narrower container.</prop>
|
|
5
9
|
</defs>
|
|
6
10
|
<div><slot></slot></div>
|
|
7
11
|
<style>
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<template component="ui-icon" controller="./ui-icon.js">
|
|
2
2
|
<defs>
|
|
3
|
-
<prop name="name" type="string" default=""
|
|
3
|
+
<prop name="name" type="string" default=""
|
|
4
|
+
>Which icon to draw, by its name in Looma's icon set (LOOMA_ICONS), such as "align-left". An unknown or empty name draws nothing. The icon is hidden from
|
|
5
|
+
assistive technology, so name the control it sits in.</prop>
|
|
4
6
|
<state
|
|
5
7
|
name="shapes"
|
|
6
8
|
type="list(object({ tag: path | circle | rect, d?: string, cx?: string, cy?: string, r?: string, x?: string, y?: string, width?: string, height?: string, rx?: string }))"
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
<template component="ui-icon-button" controller="./ui-icon-button.js">
|
|
2
2
|
<defs>
|
|
3
|
-
<prop name="disabled" type="boolean" default="false"
|
|
3
|
+
<prop name="disabled" type="boolean" default="false"
|
|
4
|
+
>Disables the button, so it cannot be pressed or focused, and greys the icon; outline and solid also take the disabled surface.</prop>
|
|
4
5
|
<prop name="label" type="string">Accessible name for the icon.</prop>
|
|
5
|
-
<prop name="size" type="sm | md | lg" default="md"
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
<prop name="size" type="sm | md | lg" default="md"
|
|
7
|
+
>Button and icon size together: sm is 1.75rem with a small icon, md is 2rem, and lg is 2.5rem with a large icon. On touch, every size gets an invisible hit
|
|
8
|
+
area of at least 44px.</prop>
|
|
9
|
+
<prop name="variant" type="ghost | outline | solid" default="ghost"
|
|
10
|
+
>Emphasis. ghost shows only the icon until it is hovered, for toolbars and dense rows; outline adds a border on the default surface; solid fills with the
|
|
11
|
+
accent colour, for the one primary action in a group.</prop>
|
|
12
|
+
<prop name="anticipatory" type="boolean" default="false"
|
|
13
|
+
>Keeps the button out of sight until it is wanted: at rest a fine pointer sees only a small guide dot, and the button appears on hover, on focus, or when
|
|
14
|
+
the pointer comes near inside a ui-affordance-scope. Touch devices and the solid variant always show it.</prop>
|
|
8
15
|
<prop name="round" type="boolean" default="false">Use a fully circular shape.</prop>
|
|
9
16
|
</defs>
|
|
10
17
|
<button :disabled="disabled" type="button" :aria-label="label"><slot></slot></button>
|