Once the component is fully initialized, it dispatches a custom (non-bubbling) basecoat:initialized event on itself.
-
command:select
-
When a command is selected (via click or Enter key), the component dispatches a custom (bubbling) command:select event on itself, with the selected value in event.detail (e.g. {detail: {value: "calendar"}}).
+ {% if items and items|length > 0 %}
+ {{ render_command_items(items, id ~ "-items" if id else "items") }}
+ {% else %}
+ {{ caller() if caller }}
+ {% endif %}
+
+
+{% endmacro %}
+
+{#
+ Renders a command dialog (modal command palette).
+
+ @param id {string} [optional] - Unique identifier for the command dialog.
+ @param items {array} [optional] - Array of items to render (alternative to using caller).
+ @param placeholder {string} [optional] [default="Type a command or search..."] - Placeholder text for the search input.
+ @param empty_text {string} [optional] [default="No results found."] - Text displayed when no results match the search.
+ @param dialog_attrs {object} [optional] - Additional HTML attributes for the dialog element.
+ @param input_attrs {object} [optional] - Additional HTML attributes for the search input.
+ @param listbox_attrs {object} [optional] - Additional HTML attributes for the listbox div.
+ @param open {boolean} [optional] [default=False] - Whether the command dialog should be open initially.
+#}
+{% macro command_dialog(
+ id=None,
+ items=None,
+ placeholder="Type a command or search...",
+ empty_text="No results found.",
dialog_attrs={},
input_attrs={},
listbox_attrs={},
open=False
) %}
-{% set id = id or ("command-" ~ range(100000, 999999) | random | string) %}
+{% set id = id or ("command-dialog-" ~ range(100000, 999999) | random | string) %}
{% endmacro %}
@@ -107,6 +173,7 @@
data-value="{{ item.value }}"
{% if item.keywords %}data-keywords="{{ item.keywords }}"{% endif %}
{% if item.label %}data-label="{{ item.label }}"{% endif %}
+ {% if item.disabled %}aria-disabled="true"{% endif %}
{% if item.attrs %}
{% for key, value in item.attrs.items() %}
{{ key }}="{{ value }}"
@@ -118,4 +185,3 @@
{% endif %}
{% endfor %}
{% endmacro %}
-
diff --git a/src/js/command.js b/src/js/command.js
index 793a412..21633ee 100644
--- a/src/js/command.js
+++ b/src/js/command.js
@@ -1,13 +1,13 @@
(() => {
- const initCommand = (dialog) => {
- const input = dialog.querySelector('header input');
- const listbox = dialog.querySelector('[role="listbox"]');
+ const initCommand = (container) => {
+ const input = container.querySelector('header input');
+ const listbox = container.querySelector('[role="listbox"]');
if (!input || !listbox) {
const missing = [];
if (!input) missing.push('input');
if (!listbox) missing.push('listbox');
- console.error(`Command component initialization failed. Missing element(s): ${missing.join(', ')}`, dialog);
+ console.error(`Command component initialization failed. Missing element(s): ${missing.join(', ')}`, container);
return;
}
@@ -51,7 +51,10 @@
}
});
- // empty state handled via CSS on [role="listbox"][data-empty]
+ if (visibleOptions.length > 0) {
+ setActiveOption(options.indexOf(visibleOptions[0]));
+ visibleOptions[0].scrollIntoView({ block: 'nearest' });
+ }
};
const selectOption = (option) => {
@@ -61,9 +64,7 @@
detail: { value: option.dataset.value },
bubbles: true
});
- dialog.dispatchEvent(event);
-
- dialog.close();
+ container.dispatchEvent(event);
};
input.addEventListener('input', filterOptions);
@@ -92,13 +93,13 @@
case 'ArrowDown':
if (currentVisibleIndex < visibleOptions.length - 1) {
nextVisibleIndex = currentVisibleIndex + 1;
- } else if (currentVisibleIndex === -1) {
- nextVisibleIndex = 0;
}
break;
case 'ArrowUp':
if (currentVisibleIndex > 0) {
nextVisibleIndex = currentVisibleIndex - 1;
+ } else if (currentVisibleIndex === -1) {
+ nextVisibleIndex = 0;
}
break;
case 'Home':
@@ -109,7 +110,7 @@
break;
}
- if (nextVisibleIndex !== currentVisibleIndex && nextVisibleIndex >= 0) {
+ if (nextVisibleIndex !== currentVisibleIndex) {
const newActiveOption = visibleOptions[nextVisibleIndex];
setActiveOption(options.indexOf(newActiveOption));
newActiveOption.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
@@ -127,7 +128,11 @@
});
listbox.addEventListener('mouseleave', () => {
- setActiveOption(-1);
+ if (visibleOptions.length > 0) {
+ setActiveOption(options.indexOf(visibleOptions[0]));
+ } else {
+ setActiveOption(-1);
+ }
});
listbox.addEventListener('click', (event) => {
@@ -139,32 +144,14 @@
input.addEventListener('keydown', handleKeyNavigation);
- dialog.addEventListener('close', () => {
- input.value = '';
- visibleOptions = [...options];
- options.forEach(opt => opt.setAttribute('aria-hidden', 'false'));
- setActiveOption(-1);
- });
-
- dialog.dataset.commandInitialized = true;
- dialog.dispatchEvent(new CustomEvent('basecoat:initialized'));
- };
-
- document.addEventListener('keydown', (event) => {
- if ((event.metaKey || event.ctrlKey) && event.key === 'k') {
- event.preventDefault();
- const firstCommand = document.querySelector('.command');
- if (firstCommand) {
- if (firstCommand.open) {
- firstCommand.close();
- } else {
- firstCommand.showModal();
- const input = firstCommand.querySelector('header input');
- if (input) input.focus();
- }
- }
+ if (visibleOptions.length > 0) {
+ setActiveOption(options.indexOf(visibleOptions[0]));
+ visibleOptions[0].scrollIntoView({ block: 'nearest' });
}
- });
+
+ container.dataset.commandInitialized = true;
+ container.dispatchEvent(new CustomEvent('basecoat:initialized'));
+ };
if (window.basecoat) {
window.basecoat.register('command', '.command:not([data-command-initialized])', initCommand);
diff --git a/src/nunjucks/command.njk b/src/nunjucks/command.njk
index edeff42..46b8e50 100644
--- a/src/nunjucks/command.njk
+++ b/src/nunjucks/command.njk
@@ -1,15 +1,13 @@
{#
- Renders a command menu component with search and keyboard navigation.
+ Renders a standalone command menu component with search and keyboard navigation.
@param id {string} [optional] - Unique identifier for the command component.
@param items {array} [optional] - Array of items to render (alternative to using caller).
@param placeholder {string} [optional] [default="Type a command or search..."] - Placeholder text for the search input.
@param empty_text {string} [optional] [default="No results found."] - Text displayed when no results match the search.
@param main_attrs {object} [optional] - Additional HTML attributes for the main container div.
- @param dialog_attrs {object} [optional] - Additional HTML attributes for the dialog element.
@param input_attrs {object} [optional] - Additional HTML attributes for the search input.
@param listbox_attrs {object} [optional] - Additional HTML attributes for the listbox div.
- @param open {boolean} [optional] [default=false] - Whether the command menu should be open initially.
#}
{% macro command(
id=None,
@@ -17,60 +15,128 @@
placeholder="Type a command or search...",
empty_text="No results found.",
main_attrs={},
+ input_attrs={},
+ listbox_attrs={}
+) %}
+{% set id = id or ("command-" + (range(100000, 999999) | random | string)) %}
+
+
+
+
+
+
+ {% if items and items.length > 0 %}
+ {{ render_command_items(items, id ~ "-items" if id else "items") }}
+ {% else %}
+ {{ caller() if caller }}
+ {% endif %}
+
+
+{% endmacro %}
+
+{#
+ Renders a command dialog (modal command palette).
+
+ @param id {string} [optional] - Unique identifier for the command dialog.
+ @param items {array} [optional] - Array of items to render (alternative to using caller).
+ @param placeholder {string} [optional] [default="Type a command or search..."] - Placeholder text for the search input.
+ @param empty_text {string} [optional] [default="No results found."] - Text displayed when no results match the search.
+ @param dialog_attrs {object} [optional] - Additional HTML attributes for the dialog element.
+ @param input_attrs {object} [optional] - Additional HTML attributes for the search input.
+ @param listbox_attrs {object} [optional] - Additional HTML attributes for the listbox div.
+ @param open {boolean} [optional] [default=false] - Whether the command dialog should be open initially.
+#}
+{% macro command_dialog(
+ id=None,
+ items=None,
+ placeholder="Type a command or search...",
+ empty_text="No results found.",
dialog_attrs={},
input_attrs={},
listbox_attrs={},
open=false
) %}
-{% set id = id or ("command-" + (range(100000, 999999) | random | string)) %}
+{% set id = id or ("command-dialog-" + (range(100000, 999999) | random | string)) %}
{% endmacro %}
@@ -107,6 +173,7 @@
data-value="{{ item.value }}"
{% if item.keywords %}data-keywords="{{ item.keywords }}"{% endif %}
{% if item.label %}data-label="{{ item.label }}"{% endif %}
+ {% if item.disabled %}aria-disabled="true"{% endif %}
{% if item.attrs %}
{% for key, value in item.attrs %}
{{ key }}="{{ value }}"
@@ -118,4 +185,3 @@
{% endif %}
{% endfor %}
{% endmacro %}
-