@threadlabs/looma 0.14.8 → 0.15.1
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/icons.js +5 -0
- package/components/shared/menu-navigation.js +49 -0
- package/components/shared/overlay.js +33 -60
- package/components/ui-avatar/ui-avatar.html +19 -2
- package/components/ui-avatar/ui-avatar.js +7 -1
- package/components/ui-avatar-group/ui-avatar-group.html +8 -3
- package/components/ui-avatar-group/ui-avatar-group.js +6 -2
- package/components/ui-badge/ui-badge.html +70 -4
- package/components/ui-button/ui-button.html +21 -5
- package/components/ui-button/ui-button.js +10 -0
- package/components/ui-callout/ui-callout.html +13 -5
- package/components/ui-checkbox/ui-checkbox.html +11 -0
- package/components/ui-cluster/ui-cluster.html +7 -0
- package/components/ui-combobox/ui-combobox.html +46 -10
- package/components/ui-combobox/ui-combobox.js +5 -5
- package/components/ui-context-menu/ui-context-menu.html +33 -5
- package/components/ui-context-menu/ui-context-menu.js +26 -17
- package/components/ui-dialog/ui-dialog.html +51 -8
- package/components/ui-dialog/ui-dialog.js +28 -7
- package/components/ui-disclosure/ui-disclosure.html +14 -5
- package/components/ui-disclosure/ui-disclosure.js +51 -4
- package/components/ui-editable/ui-editable.html +17 -3
- package/components/ui-editable/ui-editable.js +7 -0
- package/components/ui-editor-insert-table-grid/ui-editor-insert-table-grid.html +48 -14
- package/components/ui-editor-insert-table-grid/ui-editor-insert-table-grid.js +40 -6
- package/components/ui-editor-mention-menu/ui-editor-mention-menu.html +22 -9
- package/components/ui-editor-mention-menu/ui-editor-mention-menu.js +33 -11
- package/components/ui-editor-mention-menu-item/ui-editor-mention-menu-item.html +67 -0
- package/components/ui-editor-slash-menu/ui-editor-slash-menu.html +33 -30
- package/components/ui-editor-slash-menu/ui-editor-slash-menu.js +42 -8
- package/components/ui-editor-slash-menu-group/ui-editor-slash-menu-group.html +31 -0
- package/components/ui-editor-slash-menu-item/ui-editor-slash-menu-item.html +75 -0
- package/components/ui-editor-table-context-menu/ui-editor-table-context-menu.html +16 -5
- package/components/ui-editor-table-context-menu/ui-editor-table-context-menu.js +10 -2
- package/components/ui-editor-table-overlay/ui-editor-table-overlay.html +5 -5
- package/components/ui-editor-table-overlay/ui-editor-table-overlay.js +5 -2
- package/components/ui-editor-table-toolbar/ui-editor-table-toolbar.html +14 -4
- package/components/ui-editor-table-toolbar/ui-editor-table-toolbar.js +2 -2
- package/components/ui-editor-toolbar/ui-editor-toolbar.html +1 -1
- package/components/ui-editor-toolbar/ui-editor-toolbar.js +46 -0
- package/components/ui-floating-action-button/ui-floating-action-button.html +1 -1
- package/components/ui-form-field/ui-form-field.html +16 -3
- package/components/ui-form-field/ui-form-field.js +1 -1
- package/components/ui-grid/ui-grid.html +17 -2
- package/components/ui-icon/ui-icon.html +5 -0
- package/components/ui-icon-button/ui-icon-button.html +25 -6
- package/components/ui-icon-button/ui-icon-button.js +11 -1
- package/components/ui-input/ui-input.html +3 -3
- package/components/ui-input-group/ui-input-group.html +18 -4
- package/components/ui-listbox/ui-listbox.html +81 -0
- package/components/ui-listbox/ui-listbox.js +27 -0
- package/components/ui-menu/ui-menu.html +33 -5
- package/components/ui-menu/ui-menu.js +18 -12
- package/components/ui-menu-group/ui-menu-group.html +27 -0
- package/components/ui-menu-item/ui-menu-item.html +80 -8
- package/components/ui-menu-item/ui-menu-item.js +47 -0
- package/components/ui-popover/ui-popover.html +33 -5
- package/components/ui-popover/ui-popover.js +9 -1
- package/components/ui-radio-group/ui-radio-group.html +13 -1
- package/components/ui-search-shell/ui-search-shell.html +6 -6
- package/components/ui-search-shell/ui-search-shell.js +21 -5
- package/components/ui-select/ui-select.html +14 -4
- package/components/ui-switch/ui-switch.html +2 -2
- package/components/ui-switcher/ui-switcher.html +8 -1
- package/components/ui-switcher/ui-switcher.js +15 -0
- package/components/ui-tabs/ui-tabs.html +17 -12
- package/components/ui-tabs/ui-tabs.js +43 -7
- package/components/ui-textarea/ui-textarea.html +28 -5
- package/components/ui-toast/ui-toast.html +88 -0
- package/components/ui-toast/ui-toast.js +68 -0
- package/components/ui-toast-region/ui-toast-region.html +102 -19
- package/components/ui-toast-region/ui-toast-region.js +10 -18
- package/components/ui-tooltip/ui-tooltip.html +51 -7
- package/components/ui-tooltip/ui-tooltip.js +29 -3
- package/components/ui-tree/ui-tree.html +12 -0
- package/components/ui-tree/ui-tree.js +191 -12
- package/components/ui-tree-item/ui-tree-item.html +87 -8
- package/components/ui-tree-item/ui-tree-item.js +3 -1
- package/dist/index.js +38 -32
- package/editor/chunks/extensions.js +82 -2
- package/editor/extensions/table-commands.d.ts +8 -0
- package/editor/icons.d.ts +5 -0
- package/editor/index.js +7 -2
- package/editor/insert-table-grid.d.ts +1 -1
- package/editor/mention-menu.d.ts +2 -0
- package/editor/slash-menu.d.ts +3 -0
- package/editor/table-context-menu.d.ts +1 -1
- package/editor/table-overlay.d.ts +2 -2
- package/package.json +1 -1
- package/styles/ui-avatar-group.css +1 -2
- package/styles/ui-avatar.css +11 -1
- package/styles/ui-badge.css +73 -1
- package/styles/ui-button.css +9 -1
- package/styles/ui-callout.css +1 -1
- package/styles/ui-checkbox.css +9 -0
- package/styles/ui-cluster.css +3 -0
- package/styles/ui-combobox.css +38 -7
- package/styles/ui-context-menu.css +32 -4
- package/styles/ui-dialog.css +42 -5
- package/styles/ui-editor-insert-table-grid.css +48 -11
- package/styles/ui-editor-mention-menu-item.css +57 -0
- package/styles/ui-editor-mention-menu.css +11 -1
- package/styles/ui-editor-slash-menu-group.css +21 -0
- package/styles/ui-editor-slash-menu-item.css +62 -0
- package/styles/ui-editor-slash-menu.css +10 -21
- package/styles/ui-editor-table-context-menu.css +1 -1
- package/styles/ui-floating-action-button.css +1 -1
- package/styles/ui-form-field.css +14 -1
- package/styles/ui-grid.css +14 -1
- package/styles/ui-icon-button.css +17 -5
- package/styles/ui-input-group.css +18 -4
- package/styles/ui-input.css +3 -3
- package/styles/ui-listbox.css +69 -0
- package/styles/ui-menu-group.css +19 -0
- package/styles/ui-menu-item.css +48 -9
- package/styles/ui-menu.css +32 -4
- package/styles/ui-popover.css +28 -4
- package/styles/ui-radio-group.css +9 -0
- package/styles/ui-search-shell.css +5 -5
- package/styles/ui-select.css +14 -4
- package/styles/ui-switch.css +2 -2
- package/styles/ui-switcher.css +4 -0
- package/styles/ui-textarea.css +27 -4
- package/styles/ui-toast-region.css +88 -4
- package/styles/ui-toast.css +74 -0
- package/styles/ui-tooltip.css +46 -6
- package/styles/ui-tree-item.css +72 -4
- package/styles/ui-tree.css +8 -0
- package/tokens.css +3 -0
- package/vanilla/UiAvatar.d.ts +2 -0
- package/vanilla/UiAvatar.js +8 -1
- package/vanilla/UiAvatarGroup.d.ts +2 -0
- package/vanilla/UiAvatarGroup.js +1 -1
- package/vanilla/UiBadge.d.ts +1 -1
- package/vanilla/UiBadge.js +1 -1
- package/vanilla/UiButton.d.ts +1 -0
- package/vanilla/UiButton.js +22 -6
- package/vanilla/UiCallout.d.ts +1 -1
- package/vanilla/UiCallout.js +1 -1
- package/vanilla/UiCheckbox.d.ts +1 -0
- package/vanilla/UiCheckbox.js +12 -9
- package/vanilla/UiCluster.d.ts +1 -0
- package/vanilla/UiCluster.js +2 -0
- package/vanilla/UiCombobox.d.ts +3 -0
- package/vanilla/UiCombobox.js +30 -15
- package/vanilla/UiContextMenu.d.ts +1 -1
- package/vanilla/UiContextMenu.js +1 -1
- package/vanilla/UiDialog.d.ts +5 -2
- package/vanilla/UiDialog.js +1 -1
- package/vanilla/UiDisclosure.d.ts +2 -0
- package/vanilla/UiDisclosure.js +32 -18
- package/vanilla/UiEditable.d.ts +5 -0
- package/vanilla/UiEditable.js +7 -3
- package/vanilla/UiEditorInsertTableGrid.js +1 -6
- package/vanilla/UiEditorMentionMenu.d.ts +4 -2
- package/vanilla/UiEditorMentionMenu.js +49 -31
- package/vanilla/UiEditorMentionMenuItem.d.ts +16 -0
- package/vanilla/UiEditorMentionMenuItem.js +56 -0
- package/vanilla/UiEditorSlashMenu.d.ts +4 -3
- package/vanilla/UiEditorSlashMenu.js +59 -46
- package/vanilla/UiEditorSlashMenuGroup.d.ts +14 -0
- package/vanilla/UiEditorSlashMenuGroup.js +48 -0
- package/vanilla/UiEditorSlashMenuItem.d.ts +18 -0
- package/vanilla/UiEditorSlashMenuItem.js +57 -0
- package/vanilla/UiEditorTableContextMenu.d.ts +5 -2
- package/vanilla/UiEditorTableContextMenu.js +1 -2
- package/vanilla/UiEditorTableOverlay.d.ts +1 -1
- package/vanilla/UiEditorTableOverlay.js +5 -5
- package/vanilla/UiEditorTableToolbar.d.ts +4 -2
- package/vanilla/UiEditorTableToolbar.js +1 -2
- package/vanilla/UiEditorToolbar.js +2 -0
- package/vanilla/UiGrid.d.ts +1 -0
- package/vanilla/UiGrid.js +8 -7
- package/vanilla/UiIcon.js +1 -1
- package/vanilla/UiIconButton.d.ts +1 -0
- package/vanilla/UiIconButton.js +11 -2
- package/vanilla/UiListbox.d.ts +20 -0
- package/vanilla/UiListbox.js +45 -0
- package/vanilla/UiMenu.d.ts +1 -1
- package/vanilla/UiMenu.js +1 -1
- package/vanilla/UiMenuGroup.d.ts +14 -0
- package/vanilla/UiMenuGroup.js +35 -0
- package/vanilla/UiMenuItem.d.ts +5 -1
- package/vanilla/UiMenuItem.js +91 -30
- package/vanilla/UiPopover.d.ts +1 -1
- package/vanilla/UiPopover.js +1 -1
- package/vanilla/UiRadioGroup.d.ts +1 -0
- package/vanilla/UiRadioGroup.js +6 -3
- package/vanilla/UiSwitcher.d.ts +1 -0
- package/vanilla/UiSwitcher.js +7 -9
- package/vanilla/UiTabs.d.ts +1 -0
- package/vanilla/UiTabs.js +9 -1
- package/vanilla/UiTextarea.d.ts +1 -0
- package/vanilla/UiTextarea.js +4 -1
- package/vanilla/UiToast.d.ts +16 -0
- package/vanilla/UiToast.js +53 -0
- package/vanilla/UiToastRegion.d.ts +1 -4
- package/vanilla/UiToastRegion.js +3 -11
- package/vanilla/UiTooltip.d.ts +1 -1
- package/vanilla/UiTooltip.js +1 -1
- package/vanilla/UiTree.d.ts +2 -0
- package/vanilla/UiTree.js +1 -1
- package/vanilla/UiTreeItem.d.ts +1 -0
- package/vanilla/UiTreeItem.js +45 -31
- package/vanilla/index.js +6 -0
- package/vue/UiAvatar.d.ts +4 -0
- package/vue/UiAvatar.vue +23 -3
- package/vue/UiAvatarGroup.d.ts +3 -0
- package/vue/UiAvatarGroup.vue +7 -4
- package/vue/UiBadge.d.ts +2 -2
- package/vue/UiBadge.vue +74 -2
- package/vue/UiButton.d.ts +5 -3
- package/vue/UiButton.vue +41 -7
- package/vue/UiCallout.d.ts +2 -2
- package/vue/UiCallout.vue +3 -2
- package/vue/UiCheckbox.d.ts +2 -0
- package/vue/UiCheckbox.vue +16 -0
- package/vue/UiCluster.d.ts +1 -0
- package/vue/UiCluster.vue +5 -0
- package/vue/UiCombobox.d.ts +11 -3
- package/vue/UiCombobox.vue +60 -11
- package/vue/UiContextMenu.d.ts +2 -0
- package/vue/UiContextMenu.vue +47 -6
- package/vue/UiDialog.d.ts +18 -4
- package/vue/UiDialog.vue +86 -11
- package/vue/UiDisclosure.d.ts +8 -2
- package/vue/UiDisclosure.vue +38 -14
- package/vue/UiEditable.d.ts +10 -0
- package/vue/UiEditable.vue +14 -2
- package/vue/UiEditorInsertTableGrid.vue +52 -15
- package/vue/UiEditorMentionMenu.d.ts +24 -2
- package/vue/UiEditorMentionMenu.vue +27 -11
- package/vue/UiEditorMentionMenuItem.d.ts +24 -0
- package/vue/UiEditorMentionMenuItem.js +2 -0
- package/vue/UiEditorMentionMenuItem.vue +88 -0
- package/vue/UiEditorSlashMenu.d.ts +24 -1
- package/vue/UiEditorSlashMenu.vue +33 -33
- package/vue/UiEditorSlashMenuGroup.d.ts +18 -0
- package/vue/UiEditorSlashMenuGroup.js +2 -0
- package/vue/UiEditorSlashMenuGroup.vue +43 -0
- package/vue/UiEditorSlashMenuItem.d.ts +26 -0
- package/vue/UiEditorSlashMenuItem.js +2 -0
- package/vue/UiEditorSlashMenuItem.vue +101 -0
- package/vue/UiEditorTableContextMenu.d.ts +9 -3
- package/vue/UiEditorTableContextMenu.vue +53 -3
- package/vue/UiEditorTableOverlay.d.ts +2 -2
- package/vue/UiEditorTableOverlay.vue +8 -6
- package/vue/UiEditorTableToolbar.d.ts +7 -3
- package/vue/UiEditorTableToolbar.vue +50 -2
- package/vue/UiEditorToolbar.vue +14 -1
- package/vue/UiFloatingActionButton.vue +1 -1
- package/vue/UiFormField.vue +28 -3
- package/vue/UiGrid.d.ts +1 -0
- package/vue/UiGrid.vue +26 -2
- package/vue/UiIcon.vue +16 -0
- package/vue/UiIconButton.d.ts +5 -3
- package/vue/UiIconButton.vue +40 -6
- package/vue/UiInput.vue +10 -3
- package/vue/UiInputGroup.vue +32 -11
- package/vue/UiListbox.d.ts +33 -0
- package/vue/UiListbox.js +2 -0
- package/vue/UiListbox.vue +131 -0
- package/vue/UiMenu.d.ts +2 -0
- package/vue/UiMenu.vue +43 -6
- package/vue/UiMenuGroup.d.ts +18 -0
- package/vue/UiMenuGroup.js +2 -0
- package/vue/UiMenuGroup.vue +39 -0
- package/vue/UiMenuItem.d.ts +34 -2
- package/vue/UiMenuItem.vue +145 -9
- package/vue/UiPopover.d.ts +2 -2
- package/vue/UiPopover.vue +44 -5
- package/vue/UiRadioGroup.d.ts +2 -0
- package/vue/UiRadioGroup.vue +17 -0
- package/vue/UiSearchShell.vue +12 -5
- package/vue/UiSelect.vue +18 -4
- package/vue/UiSwitch.vue +2 -2
- package/vue/UiSwitcher.d.ts +1 -0
- package/vue/UiSwitcher.vue +22 -1
- package/vue/UiTabs.d.ts +6 -2
- package/vue/UiTabs.vue +14 -12
- package/vue/UiTextarea.d.ts +2 -0
- package/vue/UiTextarea.vue +32 -4
- package/vue/UiToast.d.ts +34 -0
- package/vue/UiToast.js +2 -0
- package/vue/UiToast.vue +173 -0
- package/vue/UiToastRegion.d.ts +2 -18
- package/vue/UiToastRegion.vue +123 -48
- package/vue/UiTooltip.d.ts +2 -2
- package/vue/UiTooltip.vue +67 -12
- package/vue/UiTree.d.ts +10 -0
- package/vue/UiTree.vue +27 -0
- package/vue/UiTreeItem.d.ts +2 -0
- package/vue/UiTreeItem.vue +96 -4
- package/vue/chunks/UiAvatar.js +19 -5
- package/vue/chunks/UiAvatarGroup.js +10 -4
- package/vue/chunks/UiBadge.js +1 -1
- package/vue/chunks/UiButton.js +58 -11
- package/vue/chunks/UiCallout.js +2 -2
- package/vue/chunks/UiCheckbox.js +12 -2
- package/vue/chunks/UiCluster.js +5 -3
- package/vue/chunks/UiCombobox.js +29 -13
- package/vue/chunks/UiContextMenu.js +6 -2
- package/vue/chunks/UiDialog.js +25 -8
- package/vue/chunks/UiDisclosure.js +36 -12
- package/vue/chunks/UiEditable.js +22 -8
- package/vue/chunks/UiEditorInsertTableGrid.js +11 -14
- package/vue/chunks/UiEditorMentionMenu.js +30 -19
- package/vue/chunks/UiEditorMentionMenuItem.js +36 -0
- package/vue/chunks/UiEditorSlashMenu.js +33 -35
- package/vue/chunks/UiEditorSlashMenuGroup.js +26 -0
- package/vue/chunks/UiEditorSlashMenuItem.js +51 -0
- package/vue/chunks/UiEditorTableContextMenu.js +39 -5
- package/vue/chunks/UiEditorTableOverlay.js +6 -6
- package/vue/chunks/UiEditorTableToolbar.js +37 -4
- package/vue/chunks/UiEditorToolbar.js +18 -4
- package/vue/chunks/UiFloatingActionButton.js +1 -1
- package/vue/chunks/UiFormField.js +13 -9
- package/vue/chunks/UiGrid.js +6 -2
- package/vue/chunks/UiIcon.js +40 -2
- package/vue/chunks/UiIconButton.js +35 -4
- package/vue/chunks/UiInput.js +1 -1
- package/vue/chunks/UiInputGroup.js +1 -1
- package/vue/chunks/UiListbox.js +70 -0
- package/vue/chunks/UiMenu.js +6 -2
- package/vue/chunks/UiMenuGroup.js +25 -0
- package/vue/chunks/UiMenuItem.js +128 -7
- package/vue/chunks/UiPopover.js +1 -1
- package/vue/chunks/UiRadioGroup.js +8 -2
- package/vue/chunks/UiSearchShell.js +1 -1
- package/vue/chunks/UiSelect.js +1 -1
- package/vue/chunks/UiSwitch.js +1 -1
- package/vue/chunks/UiSwitcher.js +20 -3
- package/vue/chunks/UiTabs.js +4 -3
- package/vue/chunks/UiTextarea.js +7 -2
- package/vue/chunks/UiToast.js +81 -0
- package/vue/chunks/UiToastRegion.js +25 -49
- package/vue/chunks/UiTooltip.js +3 -7
- package/vue/chunks/UiTree.js +8 -4
- package/vue/chunks/UiTreeItem.js +28 -10
- package/vue/components.css +1856 -864
- package/vue/editor/index.js +263 -2
- package/vue/index.d.ts +6 -0
- package/vue/index.js +9 -3
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
function radioScope(item) {
|
|
2
|
+
return item.closest('[role="group"]') ?? item.closest('[role="menu"]');
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export default function controller(host) {
|
|
6
|
+
const item = host.element;
|
|
7
|
+
let external = Boolean(host.state.checked);
|
|
8
|
+
host.state.internalChecked = external;
|
|
9
|
+
|
|
10
|
+
const setChecked = (checked, trigger, announce) => {
|
|
11
|
+
if (Boolean(host.state.internalChecked) === checked) return;
|
|
12
|
+
host.state.internalChecked = checked;
|
|
13
|
+
if (announce) host.dispatch("change", { checked, value: String(host.state.value ?? ""), type: String(host.state.type), trigger });
|
|
14
|
+
};
|
|
15
|
+
const clearOtherRadios = (trigger, announce) => {
|
|
16
|
+
const scope = radioScope(item);
|
|
17
|
+
if (!scope) return;
|
|
18
|
+
for (const other of scope.querySelectorAll('[role="menuitemradio"]')) {
|
|
19
|
+
if (other === item || radioScope(other) !== scope) continue;
|
|
20
|
+
other.dispatchEvent(new CustomEvent("ui-menu-item-clear", { detail: { trigger, announce } }));
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const onActivate = (event) => {
|
|
24
|
+
const type = String(host.state.type);
|
|
25
|
+
if (host.state.disabled || type === "action") return;
|
|
26
|
+
const checked = type === "radio" ? true : !host.state.internalChecked;
|
|
27
|
+
if (type === "radio") clearOtherRadios(event.detail.trigger, true);
|
|
28
|
+
setChecked(checked, event.detail.trigger, true);
|
|
29
|
+
event.detail.checked = checked;
|
|
30
|
+
};
|
|
31
|
+
const onClear = (event) => setChecked(false, event.detail.trigger, event.detail.announce);
|
|
32
|
+
const stop = host.effect(() => {
|
|
33
|
+
const checked = Boolean(host.state.checked);
|
|
34
|
+
if (checked === external) return;
|
|
35
|
+
external = checked;
|
|
36
|
+
if (checked && host.state.type === "radio") clearOtherRadios("programmatic", false);
|
|
37
|
+
setChecked(checked, "programmatic", false);
|
|
38
|
+
});
|
|
39
|
+
item.addEventListener("ui-menu-item-activate", onActivate);
|
|
40
|
+
item.addEventListener("ui-menu-item-clear", onClear);
|
|
41
|
+
if (external && host.state.type === "radio") queueMicrotask(() => clearOtherRadios("programmatic", false));
|
|
42
|
+
return () => {
|
|
43
|
+
stop();
|
|
44
|
+
item.removeEventListener("ui-menu-item-activate", onActivate);
|
|
45
|
+
item.removeEventListener("ui-menu-item-clear", onClear);
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
<defs>
|
|
3
3
|
<prop name="open" type="boolean" default="false">Whether the popover is open initially or when set externally.</prop>
|
|
4
4
|
<prop name="for" type="string" default="">ID of the control that toggles and anchors the popover.</prop>
|
|
5
|
-
<prop
|
|
5
|
+
<prop
|
|
6
|
+
name="placement"
|
|
7
|
+
type="top | top-start | top-end | bottom | bottom-start | bottom-end | left | left-start | left-end | right | right-start | right-end"
|
|
8
|
+
default="bottom-start"
|
|
9
|
+
>Positions the surface around its trigger. Start and end on top or bottom follow the writing direction.</prop>
|
|
6
10
|
<state name="internalOpen" :value="false"></state>
|
|
7
11
|
<event
|
|
8
12
|
name="open"
|
|
@@ -16,12 +20,36 @@
|
|
|
16
20
|
<span><div class="surface"><slot></slot></div></span>
|
|
17
21
|
<style>
|
|
18
22
|
/* This component's hooks style the element they are set on, and never a nested one. */
|
|
23
|
+
@property --ui-popover-surface {
|
|
24
|
+
syntax: "*";
|
|
25
|
+
inherits: false;
|
|
26
|
+
}
|
|
27
|
+
@property --ui-popover-border {
|
|
28
|
+
syntax: "*";
|
|
29
|
+
inherits: false;
|
|
30
|
+
}
|
|
31
|
+
@property --ui-popover-radius {
|
|
32
|
+
syntax: "*";
|
|
33
|
+
inherits: false;
|
|
34
|
+
}
|
|
35
|
+
@property --ui-popover-shadow {
|
|
36
|
+
syntax: "*";
|
|
37
|
+
inherits: false;
|
|
38
|
+
}
|
|
19
39
|
@property --ui-popover-viewport-gap {
|
|
20
40
|
syntax: "*";
|
|
21
41
|
inherits: false;
|
|
22
42
|
}
|
|
43
|
+
@property --ui-popover-offset {
|
|
44
|
+
syntax: "*";
|
|
45
|
+
inherits: false;
|
|
46
|
+
}
|
|
23
47
|
|
|
24
48
|
:host {
|
|
49
|
+
--_ui-popover-surface: var(--ui-popover-surface);
|
|
50
|
+
--_ui-popover-border: var(--ui-popover-border);
|
|
51
|
+
--_ui-popover-radius: var(--ui-popover-radius);
|
|
52
|
+
--_ui-popover-shadow: var(--ui-popover-shadow);
|
|
25
53
|
box-sizing: border-box;
|
|
26
54
|
max-inline-size: min(
|
|
27
55
|
20rem,
|
|
@@ -43,10 +71,10 @@
|
|
|
43
71
|
max-block-size: inherit;
|
|
44
72
|
overflow: auto;
|
|
45
73
|
padding: var(--ui-space-3);
|
|
46
|
-
background: var(--ui-surface-elevated);
|
|
47
|
-
border: 1px solid var(--ui-border-default);
|
|
48
|
-
border-radius: var(--ui-radius-lg);
|
|
49
|
-
box-shadow: var(--ui-elevation-overlay, var(--ui-shadow-lg));
|
|
74
|
+
background: var(--_ui-popover-surface, var(--ui-overlay-surface, var(--ui-surface-elevated)));
|
|
75
|
+
border: 1px solid var(--_ui-popover-border, var(--ui-overlay-border, var(--ui-border-default)));
|
|
76
|
+
border-radius: var(--_ui-popover-radius, var(--ui-overlay-radius, var(--ui-radius-lg)));
|
|
77
|
+
box-shadow: var(--_ui-popover-shadow, var(--ui-overlay-shadow, var(--ui-elevation-overlay, var(--ui-shadow-lg))));
|
|
50
78
|
}
|
|
51
79
|
|
|
52
80
|
:host:not(:host-state([internalOpen])) {
|
|
@@ -3,6 +3,14 @@ import { closeOverlay, createAnchoredSurface, createIdResolver, openOverlay, req
|
|
|
3
3
|
export default function controller(host) {
|
|
4
4
|
const element = host.element;
|
|
5
5
|
const document = element.ownerDocument;
|
|
6
|
+
const offset = () => {
|
|
7
|
+
const value = getComputedStyle(element).getPropertyValue("--ui-popover-offset").trim();
|
|
8
|
+
const amount = Number.parseFloat(value);
|
|
9
|
+
if (!Number.isFinite(amount)) return 4;
|
|
10
|
+
if (value.endsWith("rem")) return amount * Number.parseFloat(getComputedStyle(document.documentElement).fontSize);
|
|
11
|
+
if (value.endsWith("em")) return amount * Number.parseFloat(getComputedStyle(element).fontSize);
|
|
12
|
+
return amount;
|
|
13
|
+
};
|
|
6
14
|
const overlayId = `ui-popover-${Math.random().toString(36).slice(2, 11)}`;
|
|
7
15
|
let anchor = null;
|
|
8
16
|
let surface = null;
|
|
@@ -39,7 +47,7 @@ export default function controller(host) {
|
|
|
39
47
|
anchor?.removeEventListener("click", onAnchorClick);
|
|
40
48
|
anchor = ids.get(nextFor);
|
|
41
49
|
anchor?.addEventListener("click", onAnchorClick);
|
|
42
|
-
surface = anchor ? createAnchoredSurface(element, { anchor, placement: nextPlacement }) : null;
|
|
50
|
+
surface = anchor ? createAnchoredSurface(element, { anchor, placement: nextPlacement, gap: offset }) : null;
|
|
43
51
|
};
|
|
44
52
|
const apply = () => {
|
|
45
53
|
const externalOpen = Boolean(host.state.open);
|
|
@@ -16,16 +16,24 @@
|
|
|
16
16
|
<prop name="required" type="boolean" default="false"
|
|
17
17
|
>Makes a choice required, as a required native radio does for its group: the group states aria-required, every radio in it is marked required, and native
|
|
18
18
|
form validation fails until one is checked.</prop>
|
|
19
|
+
<prop name="invalid" type="boolean" default="false"
|
|
20
|
+
>Exposes an externally validated error on the group and styles its legend with the field danger colour.</prop>
|
|
19
21
|
<state name="internalValue" :value="''"></state>
|
|
20
22
|
<event name="select" type="object({ value: string, previousValue: string, trigger: keyboard | pointer | programmatic })"></event>
|
|
21
23
|
<event name="change" type="object({ checked: boolean, value: string, trigger: keyboard | pointer | programmatic })"></event>
|
|
22
24
|
</defs>
|
|
23
|
-
<fieldset role="radiogroup" :aria-required="required" :disabled="disabled">
|
|
25
|
+
<fieldset role="radiogroup" :aria-required="required" :aria-invalid="invalid" :disabled="disabled">
|
|
24
26
|
<legend><template $value="label"></template></legend>
|
|
25
27
|
<slot></slot>
|
|
26
28
|
</fieldset>
|
|
27
29
|
<style>
|
|
30
|
+
@property --ui-radio-group-invalid-text {
|
|
31
|
+
syntax: "*";
|
|
32
|
+
inherits: false;
|
|
33
|
+
}
|
|
34
|
+
|
|
28
35
|
:host {
|
|
36
|
+
--_ui-radio-group-invalid-text: var(--ui-radio-group-invalid-text, var(--ui-field-danger, var(--ui-danger-solid)));
|
|
29
37
|
display: inline-flex;
|
|
30
38
|
align-items: center;
|
|
31
39
|
gap: var(--ui-space-2);
|
|
@@ -48,6 +56,10 @@
|
|
|
48
56
|
clear: both;
|
|
49
57
|
}
|
|
50
58
|
|
|
59
|
+
:host-state([invalid]) legend {
|
|
60
|
+
color: var(--_ui-radio-group-invalid-text);
|
|
61
|
+
}
|
|
62
|
+
|
|
51
63
|
:host-state([orientation="vertical"]) {
|
|
52
64
|
display: inline-grid;
|
|
53
65
|
justify-items: start;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<defs>
|
|
3
3
|
<prop name="open" type="boolean" default="false">Shows the search surface.</prop>
|
|
4
4
|
<prop name="modal" type="boolean" default="false">Makes the rest of the page inert while the search surface is open.</prop>
|
|
5
|
-
<prop name="dismissible" type="boolean" default="false">Allows
|
|
5
|
+
<prop name="dismissible" type="boolean" default="false">Allows backdrop activation to close the search surface. Escape always closes it.</prop>
|
|
6
6
|
<prop name="label" type="string" default="Search">Provides the native dialog's accessible name.</prop>
|
|
7
7
|
<state name="internalOpen" :value="false"></state>
|
|
8
8
|
<event
|
|
@@ -109,10 +109,10 @@
|
|
|
109
109
|
max-block-size: var(--_ui-search-shell-max-height, 80vh);
|
|
110
110
|
margin-inline: auto;
|
|
111
111
|
overflow: hidden;
|
|
112
|
-
background: var(--_ui-search-shell-surface, var(--ui-surface-elevated));
|
|
113
|
-
border: 1px solid var(--_ui-search-shell-border, var(--ui-border-default));
|
|
114
|
-
border-radius: var(--_ui-search-shell-radius, var(--ui-radius-dialog));
|
|
115
|
-
box-shadow: var(--_ui-search-shell-shadow, var(--ui-elevation-dialog, var(--ui-shadow-xl)));
|
|
112
|
+
background: var(--_ui-search-shell-surface, var(--ui-overlay-surface, var(--ui-surface-elevated)));
|
|
113
|
+
border: 1px solid var(--_ui-search-shell-border, var(--ui-overlay-border, var(--ui-border-default)));
|
|
114
|
+
border-radius: var(--_ui-search-shell-radius, var(--ui-overlay-radius, var(--ui-radius-dialog)));
|
|
115
|
+
box-shadow: var(--_ui-search-shell-shadow, var(--ui-overlay-shadow, var(--ui-elevation-dialog, var(--ui-shadow-xl))));
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
.search {
|
|
@@ -133,7 +133,7 @@
|
|
|
133
133
|
min-block-size: var(--ui-control-min-block-size);
|
|
134
134
|
padding: var(--ui-space-2) var(--ui-space-3);
|
|
135
135
|
border: 1px solid var(--ui-control-border);
|
|
136
|
-
border-radius: var(--ui-radius-md);
|
|
136
|
+
border-radius: var(--ui-field-radius, var(--ui-radius-md));
|
|
137
137
|
background: var(--ui-control-surface);
|
|
138
138
|
color: var(--ui-text-primary);
|
|
139
139
|
font: inherit;
|
|
@@ -38,14 +38,30 @@ export default function controller(host) {
|
|
|
38
38
|
|
|
39
39
|
const onCancel = (event) => {
|
|
40
40
|
event.preventDefault();
|
|
41
|
-
|
|
41
|
+
dispatchClose("escape", "keyboard");
|
|
42
42
|
};
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
const resultRows = () => Array.from(dialog.querySelectorAll(".body button[aria-current]"))
|
|
44
|
+
.filter((row) => !row.disabled && !row.hidden && !row.closest("[hidden]"));
|
|
45
|
+
// A search field can spend Escape clearing itself before the native dialog sees a cancel.
|
|
45
46
|
const onKeydown = (event) => {
|
|
46
|
-
if (event.
|
|
47
|
+
if (event.defaultPrevented) return;
|
|
48
|
+
if (event.key === "Escape") {
|
|
49
|
+
event.preventDefault();
|
|
50
|
+
dispatchClose("escape", "keyboard");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) return;
|
|
54
|
+
const rows = resultRows();
|
|
55
|
+
if (!rows.length) return;
|
|
56
|
+
const target = event.target;
|
|
57
|
+
const fromSearch = target.closest?.(".search") && target.matches?.("input, [role='searchbox'], [role='combobox']");
|
|
58
|
+
const index = rows.indexOf(target.closest?.(".body button[aria-current]"));
|
|
59
|
+
if (!fromSearch && index < 0) return;
|
|
47
60
|
event.preventDefault();
|
|
48
|
-
|
|
61
|
+
if (event.key === "Home") rows[0].focus();
|
|
62
|
+
else if (event.key === "End") rows.at(-1).focus();
|
|
63
|
+
else if (event.key === "ArrowDown") rows[fromSearch ? 0 : (index + 1) % rows.length].focus();
|
|
64
|
+
else rows[fromSearch ? rows.length - 1 : (index - 1 + rows.length) % rows.length].focus();
|
|
49
65
|
};
|
|
50
66
|
const onClick = (event) => {
|
|
51
67
|
if (host.state.dismissible && event.target === dialog) dispatchClose("light-dismiss", trigger());
|
|
@@ -19,6 +19,14 @@
|
|
|
19
19
|
syntax: "*";
|
|
20
20
|
inherits: false;
|
|
21
21
|
}
|
|
22
|
+
@property --ui-select-invalid-border {
|
|
23
|
+
syntax: "*";
|
|
24
|
+
inherits: false;
|
|
25
|
+
}
|
|
26
|
+
@property --ui-select-invalid-focus-shadow {
|
|
27
|
+
syntax: "*";
|
|
28
|
+
inherits: false;
|
|
29
|
+
}
|
|
22
30
|
@property --ui-select-radius {
|
|
23
31
|
syntax: "*";
|
|
24
32
|
inherits: false;
|
|
@@ -37,7 +45,7 @@
|
|
|
37
45
|
margin: 0;
|
|
38
46
|
padding: var(--ui-space-2) var(--ui-space-3);
|
|
39
47
|
border: 1px solid var(--ui-control-border, var(--ui-border-strong));
|
|
40
|
-
border-radius: var(--ui-select-radius, var(--ui-radius-md));
|
|
48
|
+
border-radius: var(--ui-select-radius, var(--ui-field-radius, var(--ui-radius-md)));
|
|
41
49
|
background-color: var(--ui-control-surface, var(--ui-surface-elevated));
|
|
42
50
|
color: var(--ui-text-primary);
|
|
43
51
|
color-scheme: light dark;
|
|
@@ -77,13 +85,15 @@
|
|
|
77
85
|
/* The app's invalid flag, or the browser's own verdict once the user has left an invalid field. */
|
|
78
86
|
:host[aria-invalid="true"],
|
|
79
87
|
:host:user-invalid {
|
|
80
|
-
border-color: var(--ui-danger-solid);
|
|
88
|
+
border-color: var(--ui-select-invalid-border, var(--ui-field-danger, var(--ui-danger-solid)));
|
|
81
89
|
}
|
|
82
90
|
|
|
83
91
|
:host:is([aria-invalid="true"], :user-invalid):focus-visible {
|
|
84
|
-
box-shadow:
|
|
92
|
+
box-shadow: var(
|
|
93
|
+
--ui-select-invalid-focus-shadow,
|
|
85
94
|
var(--ui-control-inset),
|
|
86
|
-
0 0 0 3px color-mix(in srgb, var(--ui-danger-solid) 22%, transparent)
|
|
95
|
+
0 0 0 3px color-mix(in srgb, var(--ui-select-invalid-border, var(--ui-field-danger, var(--ui-danger-solid))) 22%, transparent)
|
|
96
|
+
);
|
|
87
97
|
}
|
|
88
98
|
|
|
89
99
|
:host:disabled {
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
height: var(--_ui-switch-track-height, 1.5rem);
|
|
84
84
|
padding: 0;
|
|
85
85
|
border: 1px solid var(--ui-control-border-hover, var(--ui-border-strong));
|
|
86
|
-
border-radius:
|
|
86
|
+
border-radius: var(--ui-radius-round);
|
|
87
87
|
background: var(--ui-control-surface, var(--ui-surface-default));
|
|
88
88
|
box-shadow: inset 0 1px 2px color-mix(in srgb, var(--ui-text-primary) 10%, transparent);
|
|
89
89
|
cursor: pointer;
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
left: 3px;
|
|
99
99
|
width: 1rem;
|
|
100
100
|
height: 1rem;
|
|
101
|
-
border-radius:
|
|
101
|
+
border-radius: var(--ui-radius-round);
|
|
102
102
|
background: var(--ui-text-secondary);
|
|
103
103
|
box-shadow: var(--ui-shadow-xs);
|
|
104
104
|
transition: transform var(--ui-motion-fast) var(--ui-motion-ease);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<template component="ui-switcher">
|
|
1
|
+
<template component="ui-switcher" controller="./ui-switcher.js">
|
|
2
2
|
<defs>
|
|
3
3
|
<prop name="gap" type="xs | s | m | l | xl"
|
|
4
4
|
>Space between items, on the spacing scale: xs is 0.5rem, s 0.75rem, m 1rem (also the default), l 1.5rem, and xl 2rem. The --ui-switcher-gap custom
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
<prop name="align" type="start | center | end | stretch"
|
|
10
10
|
>How items line up while they sit in a row: stretch (the default) makes them equally tall; start, center, and end align their tops, middles, or
|
|
11
11
|
bottoms.</prop>
|
|
12
|
+
<prop name="limit" type="integer"
|
|
13
|
+
>Most children allowed in a horizontal row. When the count exceeds this value, every child stacks regardless of width. Omit it for no count limit.</prop>
|
|
14
|
+
<state name="overLimit" :value="false"></state>
|
|
12
15
|
</defs>
|
|
13
16
|
<div><slot></slot></div>
|
|
14
17
|
<style>
|
|
@@ -42,6 +45,10 @@
|
|
|
42
45
|
flex-basis: calc((var(--_ui-switcher-threshold, 30rem) - 100%) * 999);
|
|
43
46
|
}
|
|
44
47
|
|
|
48
|
+
:host-state([overLimit]) > :slotted(*) {
|
|
49
|
+
flex-basis: 100%;
|
|
50
|
+
}
|
|
51
|
+
|
|
45
52
|
:host-state([gap="xs"]) {
|
|
46
53
|
--_switcher-gap: var(--ui-space-2);
|
|
47
54
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Stack the group when the authored child count exceeds the optional row limit. */
|
|
2
|
+
export default function controller(host) {
|
|
3
|
+
const element = host.element;
|
|
4
|
+
const update = () => {
|
|
5
|
+
const limit = Number(host.state.limit);
|
|
6
|
+
host.state.overLimit = Number.isFinite(limit) && limit >= 1 && element.children.length > Math.floor(limit);
|
|
7
|
+
};
|
|
8
|
+
const stop = host.effect(update);
|
|
9
|
+
const observer = new MutationObserver(update);
|
|
10
|
+
observer.observe(element, { childList: true });
|
|
11
|
+
return () => {
|
|
12
|
+
stop();
|
|
13
|
+
observer.disconnect();
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
<prop name="orientation" type="horizontal | vertical" default="horizontal"
|
|
10
10
|
>horizontal puts the tabs in a row above the panels, moved between with the Left and Right arrow keys; vertical puts them in a column beside the panels,
|
|
11
11
|
moved between with Up and Down.</prop>
|
|
12
|
+
<prop name="activation" type="auto | manual" default="auto"
|
|
13
|
+
>In auto mode, arrow keys move focus and select the next panel. In manual mode, arrows move focus while Enter or Space selects a panel. Use manual when
|
|
14
|
+
displaying a panel takes time.</prop>
|
|
12
15
|
<prop name="stretch" type="boolean" default="false">Share the full width of the tab list equally between tabs instead of sizing each to its label.</prop>
|
|
13
16
|
<state name="internalValue" :value="''"></state>
|
|
14
17
|
<state name="tabs" type="list(object({ id: string, label: string }))" :value="[]"></state>
|
|
@@ -16,18 +19,20 @@
|
|
|
16
19
|
</defs>
|
|
17
20
|
<div>
|
|
18
21
|
<div class="list" $ref="list" role="tablist" :aria-label="label" :aria-orientation="orientation">
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
22
|
+
<slot name="tab">
|
|
23
|
+
<button
|
|
24
|
+
$each="tab of tabs"
|
|
25
|
+
$key="tab.id"
|
|
26
|
+
type="button"
|
|
27
|
+
role="tab"
|
|
28
|
+
:id="format('%s-tab', tab.id)"
|
|
29
|
+
:value="tab.id"
|
|
30
|
+
:aria-controls="tab.id"
|
|
31
|
+
:aria-selected="tab.id = internalValue"
|
|
32
|
+
$value="tab.label"
|
|
33
|
+
>
|
|
34
|
+
</button>
|
|
35
|
+
</slot>
|
|
31
36
|
</div>
|
|
32
37
|
<div class="panels" $ref="panels"><slot></slot></div>
|
|
33
38
|
</div>
|
|
@@ -7,6 +7,11 @@ export default function controller(host) {
|
|
|
7
7
|
const { list, panels: container } = host.refs;
|
|
8
8
|
const [trigger, stopTracking] = trackTrigger(host);
|
|
9
9
|
const panels = () => Array.from(container.children).filter((child) => child instanceof HTMLElement);
|
|
10
|
+
const tabButtons = () => Array.from(list.querySelectorAll("button"));
|
|
11
|
+
const focusTab = (button) => {
|
|
12
|
+
for (const tab of tabButtons()) tab.tabIndex = tab === button ? 0 : -1;
|
|
13
|
+
button.focus();
|
|
14
|
+
};
|
|
10
15
|
let external = String(host.state.value || "");
|
|
11
16
|
host.state.internalValue = external;
|
|
12
17
|
|
|
@@ -15,12 +20,29 @@ export default function controller(host) {
|
|
|
15
20
|
host.state.tabs = items.map((panel, index) => {
|
|
16
21
|
if (!panel.id) panel.id = `ui-tab-panel-${++panelIds}`;
|
|
17
22
|
panel.setAttribute("role", "tabpanel");
|
|
18
|
-
panel.setAttribute("aria-labelledby", `${panel.id}-tab`);
|
|
19
23
|
return { id: panel.id, label: panel.getAttribute("aria-label") || `Tab ${index + 1}` };
|
|
20
24
|
});
|
|
21
25
|
if (!items.some((panel) => panel.id === host.state.internalValue)) {
|
|
22
26
|
host.state.internalValue = external || items[0]?.id || "";
|
|
23
27
|
}
|
|
28
|
+
syncButtons();
|
|
29
|
+
};
|
|
30
|
+
const syncButtons = () => {
|
|
31
|
+
const buttons = tabButtons();
|
|
32
|
+
const selected = host.state.internalValue;
|
|
33
|
+
const focused = list.contains(host.element.ownerDocument.activeElement) ? host.element.ownerDocument.activeElement : null;
|
|
34
|
+
for (const [index, panel] of panels().entries()) {
|
|
35
|
+
const button = buttons.find((candidate) => candidate.value === panel.id) ?? buttons[index];
|
|
36
|
+
if (!button) continue;
|
|
37
|
+
button.type = "button";
|
|
38
|
+
button.value = panel.id;
|
|
39
|
+
button.setAttribute("role", "tab");
|
|
40
|
+
if (!button.id) button.id = `${panel.id}-tab`;
|
|
41
|
+
button.setAttribute("aria-controls", panel.id);
|
|
42
|
+
button.setAttribute("aria-selected", String(panel.id === selected));
|
|
43
|
+
button.tabIndex = button.disabled ? -1 : button === focused || (!focused && button.value === selected) ? 0 : -1;
|
|
44
|
+
panel.setAttribute("aria-labelledby", button.id);
|
|
45
|
+
}
|
|
24
46
|
};
|
|
25
47
|
const stop = host.effect(() => {
|
|
26
48
|
const value = String(host.state.value || "");
|
|
@@ -31,7 +53,7 @@ export default function controller(host) {
|
|
|
31
53
|
const selected = host.state.internalValue;
|
|
32
54
|
for (const panel of panels()) panel.hidden = panel.id !== selected;
|
|
33
55
|
// Roving focus: only the selected tab is in the tab order.
|
|
34
|
-
|
|
56
|
+
syncButtons();
|
|
35
57
|
});
|
|
36
58
|
|
|
37
59
|
const select = (button, how) => {
|
|
@@ -46,16 +68,25 @@ export default function controller(host) {
|
|
|
46
68
|
const onKeydown = (event) => {
|
|
47
69
|
const button = event.target.closest?.('[role="tab"]');
|
|
48
70
|
if (!button || !list.contains(button)) return;
|
|
49
|
-
const buttons =
|
|
71
|
+
const buttons = tabButtons().filter((candidate) => !candidate.disabled);
|
|
50
72
|
const vertical = host.state.orientation === "vertical";
|
|
51
73
|
const previousKey = vertical ? "ArrowUp" : "ArrowLeft";
|
|
52
74
|
const nextKey = vertical ? "ArrowDown" : "ArrowRight";
|
|
53
|
-
if (event.key
|
|
75
|
+
if (host.state.activation === "manual" && (event.key === "Enter" || event.key === " ")) {
|
|
76
|
+
event.preventDefault();
|
|
77
|
+
select(button, "keyboard");
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (event.key !== previousKey && event.key !== nextKey && event.key !== "Home" && event.key !== "End") return;
|
|
54
81
|
event.preventDefault();
|
|
55
82
|
const index = buttons.indexOf(button);
|
|
56
|
-
const next = buttons[(index + (event.key === nextKey ? 1 : buttons.length - 1)) % buttons.length];
|
|
57
|
-
select(next, "keyboard");
|
|
58
|
-
next
|
|
83
|
+
const next = event.key === "Home" ? buttons[0] : event.key === "End" ? buttons.at(-1) : buttons[(index + (event.key === nextKey ? 1 : buttons.length - 1)) % buttons.length];
|
|
84
|
+
if (host.state.activation !== "manual") select(next, "keyboard");
|
|
85
|
+
focusTab(next);
|
|
86
|
+
};
|
|
87
|
+
const onFocusout = (event) => {
|
|
88
|
+
if (list.contains(event.relatedTarget)) return;
|
|
89
|
+
for (const button of tabButtons()) button.tabIndex = button.value === host.state.internalValue ? 0 : -1;
|
|
59
90
|
};
|
|
60
91
|
// A vertical mouse wheel over an overflowing strip scrolls it sideways instead of the page.
|
|
61
92
|
// Trackpad gestures already carry a horizontal delta and are left to the browser.
|
|
@@ -66,16 +97,21 @@ export default function controller(host) {
|
|
|
66
97
|
};
|
|
67
98
|
list.addEventListener("click", onClick);
|
|
68
99
|
list.addEventListener("keydown", onKeydown);
|
|
100
|
+
list.addEventListener("focusout", onFocusout);
|
|
69
101
|
list.addEventListener("wheel", onWheel, { passive: false });
|
|
70
102
|
const observer = new MutationObserver(readPanels);
|
|
71
103
|
observer.observe(container, { childList: true });
|
|
104
|
+
const tabObserver = new MutationObserver(syncButtons);
|
|
105
|
+
tabObserver.observe(list, { childList: true });
|
|
72
106
|
readPanels();
|
|
73
107
|
return () => {
|
|
74
108
|
stop();
|
|
75
109
|
stopTracking();
|
|
76
110
|
observer.disconnect();
|
|
111
|
+
tabObserver.disconnect();
|
|
77
112
|
list.removeEventListener("click", onClick);
|
|
78
113
|
list.removeEventListener("keydown", onKeydown);
|
|
114
|
+
list.removeEventListener("focusout", onFocusout);
|
|
79
115
|
list.removeEventListener("wheel", onWheel);
|
|
80
116
|
};
|
|
81
117
|
}
|
|
@@ -8,17 +8,28 @@
|
|
|
8
8
|
<prop name="readonly" type="boolean" default="false">Allows selection without allowing edits.</prop>
|
|
9
9
|
<prop name="required" type="boolean" default="false">Marks the control as required for form validation.</prop>
|
|
10
10
|
<prop name="rows" type="integer" default="4">Initial number of visible text rows.</prop>
|
|
11
|
+
<prop name="autosize" type="boolean" default="false"
|
|
12
|
+
>Grow with the text while keeping rows as the minimum visible height. Uses CSS field sizing where supported; other browsers keep the native rows
|
|
13
|
+
behavior.</prop>
|
|
11
14
|
<prop name="size" type="sm | md | lg" default="md"
|
|
12
15
|
>Padding and type size, matching Input's sizes so a textarea sits with fields of the same size; rows sets its height. sm has tighter padding and smaller
|
|
13
16
|
text, for compact forms; lg has roomier padding. Under a coarse pointer, sm keeps body-size text.</prop>
|
|
14
17
|
</defs>
|
|
15
|
-
<textarea :value="value" :disabled="disabled" :readonly="readonly" :required="required" :aria-invalid="invalid" :rows="rows"></textarea>
|
|
18
|
+
<textarea :value="value" :disabled="disabled" :readonly="readonly" :required="required" :aria-invalid="invalid" :rows="rows" style:--_rows="rows"></textarea>
|
|
16
19
|
<style>
|
|
17
20
|
/* This component's hooks style the element they are set on, and never a nested one. */
|
|
18
21
|
@property --ui-textarea-focus-shadow {
|
|
19
22
|
syntax: "*";
|
|
20
23
|
inherits: false;
|
|
21
24
|
}
|
|
25
|
+
@property --ui-textarea-invalid-border {
|
|
26
|
+
syntax: "*";
|
|
27
|
+
inherits: false;
|
|
28
|
+
}
|
|
29
|
+
@property --ui-textarea-invalid-focus-shadow {
|
|
30
|
+
syntax: "*";
|
|
31
|
+
inherits: false;
|
|
32
|
+
}
|
|
22
33
|
@property --ui-textarea-radius {
|
|
23
34
|
syntax: "*";
|
|
24
35
|
inherits: false;
|
|
@@ -35,11 +46,12 @@
|
|
|
35
46
|
min-inline-size: 0;
|
|
36
47
|
min-block-size: calc(3rem * 2);
|
|
37
48
|
margin: 0;
|
|
49
|
+
--_padding-block: var(--ui-space-2);
|
|
38
50
|
padding: var(--ui-space-2) var(--ui-space-3);
|
|
39
51
|
appearance: none;
|
|
40
52
|
resize: vertical;
|
|
41
53
|
border: 1px solid var(--ui-control-border, var(--ui-border-strong));
|
|
42
|
-
border-radius: var(--ui-textarea-radius, var(--ui-radius-md));
|
|
54
|
+
border-radius: var(--ui-textarea-radius, var(--ui-field-radius, var(--ui-radius-md)));
|
|
43
55
|
background: var(--ui-control-surface, var(--ui-surface-elevated));
|
|
44
56
|
color: var(--ui-text-primary);
|
|
45
57
|
font: inherit;
|
|
@@ -75,13 +87,15 @@
|
|
|
75
87
|
/* The app's invalid flag, or the browser's own verdict once the user has left an invalid field. */
|
|
76
88
|
:host[aria-invalid="true"],
|
|
77
89
|
:host:user-invalid {
|
|
78
|
-
border-color: var(--ui-danger-solid);
|
|
90
|
+
border-color: var(--ui-textarea-invalid-border, var(--ui-field-danger, var(--ui-danger-solid)));
|
|
79
91
|
}
|
|
80
92
|
|
|
81
93
|
:host:is([aria-invalid="true"], :user-invalid):focus-visible {
|
|
82
|
-
box-shadow:
|
|
94
|
+
box-shadow: var(
|
|
95
|
+
--ui-textarea-invalid-focus-shadow,
|
|
83
96
|
var(--ui-control-inset),
|
|
84
|
-
0 0 0 3px color-mix(in srgb, var(--ui-danger-solid) 22%, transparent)
|
|
97
|
+
0 0 0 3px color-mix(in srgb, var(--ui-textarea-invalid-border, var(--ui-field-danger, var(--ui-danger-solid))) 22%, transparent)
|
|
98
|
+
);
|
|
85
99
|
}
|
|
86
100
|
|
|
87
101
|
:host:disabled {
|
|
@@ -92,15 +106,24 @@
|
|
|
92
106
|
}
|
|
93
107
|
|
|
94
108
|
:host-state([size="sm"]) {
|
|
109
|
+
--_padding-block: var(--ui-space-1);
|
|
95
110
|
padding: var(--ui-space-1) var(--ui-space-2);
|
|
96
111
|
font-size: var(--ui-font-size-sm);
|
|
97
112
|
}
|
|
98
113
|
|
|
99
114
|
:host-state([size="lg"]) {
|
|
115
|
+
--_padding-block: var(--ui-space-3);
|
|
100
116
|
padding: var(--ui-space-3) var(--ui-space-4);
|
|
101
117
|
font-size: var(--ui-font-size-md);
|
|
102
118
|
}
|
|
103
119
|
|
|
120
|
+
@supports (field-sizing: content) {
|
|
121
|
+
:host-state([autosize]) {
|
|
122
|
+
field-sizing: content;
|
|
123
|
+
min-block-size: calc(var(--_rows) * 1lh + var(--_padding-block) + var(--_padding-block) + 2px);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
104
127
|
/* Below 16px, iOS zooms the page into the focused field, so touch keeps body-size text. */
|
|
105
128
|
@media (pointer: coarse) {
|
|
106
129
|
:host-state([size="sm"]) {
|