mirror of
https://github.com/hunvreus/basecoat.git
synced 2026-11-12 13:18:22 +00:00
458 lines
18 KiB
Plaintext
458 lines
18 KiB
Plaintext
---
|
|
layout: layouts/page.njk
|
|
title: Select
|
|
description: Displays a list of options for the user to pick from—triggered by a button.
|
|
toc:
|
|
- label: Usage
|
|
id: usage
|
|
children:
|
|
- label: HTML
|
|
id: usage-html
|
|
- label: HTML + JavaScript
|
|
id: usage-html-js
|
|
children:
|
|
- label: "Step 1: Include the JavaScript files"
|
|
id: usage-html-js-1
|
|
- label: "Step 2: Add your select HTML"
|
|
id: usage-html-js-2
|
|
- label: HTML structure
|
|
id: usage-html-js-3
|
|
- label: JavaScript events
|
|
id: usage-html-js-4
|
|
- label: JavaScript methods
|
|
id: usage-html-js-5
|
|
- label: Jinja and Nunjucks
|
|
id: usage-macro
|
|
- label: Examples
|
|
id: examples
|
|
children:
|
|
- label: Scrollable
|
|
id: example-scrollable
|
|
- label: Disabled
|
|
id: example-disabled
|
|
- label: With icon
|
|
id: example-with-icon
|
|
- label: Multiple
|
|
id: example-multiple
|
|
---
|
|
|
|
{% from "macros/code_preview.njk" import code_preview %}
|
|
{% from "macros/code_block.njk" import code_block %}
|
|
{% from "select.njk" import select %}
|
|
|
|
{% set code_native %}<select class="select w-[180px]">
|
|
<optgroup label="Fruits">
|
|
<option>Apple</option>
|
|
<option>Banana</option>
|
|
<option>Blueberry</option>
|
|
<option>Grapes</option>
|
|
<option>Pineapple</option>
|
|
</optgroup>
|
|
</select>{% endset %}
|
|
{{ code_preview("select-native", code_native) }}
|
|
|
|
{% set code_html %}
|
|
{{ select(
|
|
trigger_attrs={"class": "w-[180px]"},
|
|
is_combobox=true,
|
|
multiple=true,
|
|
items=[
|
|
{
|
|
type: "group",
|
|
label: "Fruits",
|
|
items: [
|
|
{ type: "item", label: "Apple", attrs: {"data-force": "true"} },
|
|
{ type: "item", label: "Banana", attrs: {"data-keywords": "bread"} },
|
|
{ type: "item", label: "Blueberry" },
|
|
{ type: "item", value: "pineapple", label: "Grapes" },
|
|
{ type: "item", value: "pineapple", label: "Pineapple" }
|
|
]
|
|
}
|
|
]
|
|
) }}
|
|
{% endset %}
|
|
|
|
{% set code_script %}
|
|
{% endset %}
|
|
|
|
{{ code_preview("select", code_script | prettyHtml ~ code_html | prettyHtml) }}
|
|
|
|
<h2 id="usage"><a href="#usage">Usage</a></h2>
|
|
|
|
<h3 id="usage-html"><a href="#usage-html">HTML</a></h3>
|
|
|
|
<section class="prose">
|
|
<p>If you use a <code><select></code> element, just add the <code>select</code> class to it or have a parent with the <code>form</code> class (<a href="/components/form">read more about form</a>).</p>
|
|
</section>
|
|
|
|
{{ code_block(code_native) }}
|
|
|
|
<h3 id="usage-html-js"><a href="#usage-html-js">HTML + JavaScript</a></h3>
|
|
|
|
<h4 id="usage-html-js-1"><a href="#usage-html-js-1">Step 1: Include the JavaScript files</a></h4>
|
|
|
|
<section class="prose">
|
|
<p>You can either <a href="/installation/#install-cdn-all">include the JavaScript file for all the components</a>, or just the one for this component by adding this to the <code><head></code> of your page:</p>
|
|
</section>
|
|
|
|
{% set code_script %}<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/basecoat.min.js" defer></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@{{ pkg.version }}/dist/js/select.min.js" defer></script>{% endset %}
|
|
{{ code_block(code_script | prettyHtml, "html") }}
|
|
|
|
<div class="flex flex-wrap gap-2 my-6">
|
|
<a class="badge-outline" href="/installation/#install-js">
|
|
Components with JavaScript
|
|
{% lucide "arrow-right" %}
|
|
</a>
|
|
<a class="badge-outline" href="/installation/#install-cli">
|
|
Use the CLI
|
|
{% lucide "arrow-right" %}
|
|
</a>
|
|
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/js/select.js" target="_blank">
|
|
select.js
|
|
{% lucide "arrow-right" %}
|
|
</a>
|
|
</div>
|
|
|
|
<h4 id="usage-html-js-2"><a href="#usage-html-js-2">Step 2: Add your select HTML</a></h4>
|
|
|
|
{{ code_block(code_html | prettyHtml, "html") }}
|
|
|
|
<h4 id="usage-html-js-3"><a href="#usage-html-js-3">HTML structure</a></h4>
|
|
|
|
<section class="prose">
|
|
<dl>
|
|
<dt><code class="highlight language-html"><div class="select"></code></dt>
|
|
<dd>
|
|
<p>Wraps around the entire component. Can have the following attributes:</p>
|
|
<ul>
|
|
<li><code>data-placeholder="{ TEXT }"</code> <span class="badge-secondary">Optional</span>: placeholder text shown when no options are selected (multiselect only).</li>
|
|
</ul>
|
|
<dl>
|
|
<dt><code class="highlight language-html"><button type="button"></code></dt>
|
|
<dd>
|
|
<p>The trigger to open the popover. Should have the following attributes:</p>
|
|
<ul>
|
|
<li><code>id="{BUTTON_ID}"</code>: linked to by the <code>aria-labelledby</code> attribute of the listbox.</li>
|
|
<li><code>aria-haspopup="listbox"</code>: indicates that the button opens a listbox.</li>
|
|
<li><code>aria-controls="{ LISTBOX_ID }"</code>: points to the listbox's id.</li>
|
|
<li><code>aria-expanded="false"</code>: tracks the popover's state.</li>
|
|
<li><code>aria-activedescendant="{ OPTION_ID }"</code> <span class="badge-secondary">Optional</span>: points to the active option's id.</li>
|
|
</ul>
|
|
</dd>
|
|
<dt><code class="highlight language-html"><div data-popover aria-hidden="true" id="{ POPOVER_ID }"></code></dt>
|
|
<dd>The popover content. You can set up the side and alignment using the <code>data-side</code> and <code>data-align</code> attributes (see <a href="/components/popover">Popover</a> component).
|
|
<dl>
|
|
<dt><code class="highlight language-html"><div role="listbox"></code></dt>
|
|
<dd>
|
|
<p>The listbox containing the options. Should have the following attributes:</p>
|
|
<ul>
|
|
<li><code>id="{ LISTBOX_ID }"</code>: refered to by the <code>aria-controls</code> attribute of the trigger.</li>
|
|
<li><code>aria-labelledby="{ BUTTON_ID }"</code>: linked to by the button's <code>id</code> attribute.</li>
|
|
<li><code>aria-multiselectable="true"</code> <span class="badge-secondary">Optional</span>: enables multiple selection mode.</li>
|
|
</ul>
|
|
<dl>
|
|
<dt><code class="highlight language-html"><div role="option" data-value="{ VALUE }"></code></dt>
|
|
<dd>
|
|
<p>Option that can be selected. Should have the following attributes:</p>
|
|
<ul>
|
|
<li><code>id="{ OPTION_ID }"</code> <span class="badge-secondary">Optional</span>: unique id for this option (needed if you use <code>aria-activedescendant</code> on the trigger).</li>
|
|
<li><code>data-value="{ VALUE }"</code>: the value for this option.</li>
|
|
<li><code>data-label="{ LABEL }"</code> <span class="badge-secondary">Optional</span>: the text label to use for this option when selected in multiple mode. If not provided, the option's text content will be used (HTML stripped).</li>
|
|
<li><code>aria-selected="true"</code> <span class="badge-secondary">Optional</span>: indicates this option is selected.</li>
|
|
</ul>
|
|
</dd>
|
|
<dt><code class="highlight language-html"><hr role="separator"></code> <span class="badge-secondary">Optional</span></dt>
|
|
<dd>Separator between groups/options.</dd>
|
|
<dt><code class="highlight language-html"><div role="group"></code> <span class="badge-secondary">Optional</span></dt>
|
|
<dd>Group of options, can have a <code>aria-labelledby</code> attribute to link to a heading.</dd>
|
|
<dt><code class="highlight language-html"><span role="heading"></code> <span class="badge-secondary">Optional</span></dt>
|
|
<dd>Group heading, must have an <code>id</code> attribute if you use the <code>aria-labelledby</code> attribute on the group.</dd>
|
|
</dl>
|
|
</dd>
|
|
</dl>
|
|
</dl>
|
|
</dd>
|
|
<dt><code class="highlight language-html"><input type="hidden" name="{ NAME }" value="{ VALUE }"></code></dt>
|
|
<dd>
|
|
<p>The hidden input that holds the selected value.</p>
|
|
<p>For single-select: contains the selected option's value as a string.</p>
|
|
<p>For multiselect: contains a <strong>JSON array</strong> of selected values (e.g., <code>["apple","banana"]</code>). When no options are selected, contains an empty array (<code>[]</code>).</p>
|
|
<p><strong>Backend handling:</strong> Parse the JSON value on the server side. For example:</p>
|
|
<ul>
|
|
<li><strong>Python/Flask:</strong> <code>values = json.loads(request.form.get('field', '[]'))</code></li>
|
|
<li><strong>PHP:</strong> <code>$values = json_decode($_POST['field'] ?? '[]', true);</code></li>
|
|
<li><strong>Node.js:</strong> <code>const values = JSON.parse(req.body.field || '[]');</code></li>
|
|
<li><strong>Ruby/Rails:</strong> <code>values = JSON.parse(params[:field] || '[]')</code></li>
|
|
</ul>
|
|
</dd>
|
|
</dl>
|
|
</section>
|
|
|
|
<h4 id="usage-html-js-4"><a href="#usage-html-js-4">JavaScript events</a></h4>
|
|
|
|
<section class="prose">
|
|
<dl>
|
|
<dt><code>basecoat:initialized</code></dt>
|
|
<dd>Once the component is fully initialized, it dispatches a custom (non-bubbling) <code>basecoat:initialized</code> event on itself.</dd>
|
|
<dt><code>basecoat:popover</code></dt>
|
|
<dd>When the popover opens, the component dispatches a custom (non-bubbling) <code>basecoat:popover</code> event on <code>document</code>. Other popover components (Combobox, Dropdown Menu, Popover and Select) listen for this to close any open popovers.</dd>
|
|
<dt><code>change</code></dt>
|
|
<dd>
|
|
<p>When the selected value changes, the component dispatches a custom (bubbling) <code>change</code> event on itself, with the selected value in <code>event.detail.value</code>:</p>
|
|
<ul>
|
|
<li>Single select: <code>{ detail: { value: "something" }}</code> (string)</li>
|
|
<li>Multiple select: <code>{ detail: { value: ["item1", "item2"] }}</code> (array)</li>
|
|
</ul>
|
|
</dd>
|
|
</dl>
|
|
</section>
|
|
|
|
<h4 id="usage-html-js-5"><a href="#usage-html-js-5">JavaScript methods and properties</a></h4>
|
|
|
|
<section class="prose">
|
|
<dl>
|
|
<dt><code>value</code> (property)</dt>
|
|
<dd>
|
|
<p>Get or set the current value. For single-select, this is a string. For multiselect, this is an array. The multiselect setter accepts both strings and arrays.</p>
|
|
|
|
{% set code_value %}<script>
|
|
const selectComponent = document.querySelector('#my-select');
|
|
selectComponent.addEventListener('basecoat:initialized', () => {
|
|
// Get value
|
|
console.log(selectComponent.value); // 'apple'
|
|
|
|
// Set value (single-select)
|
|
selectComponent.value = 'banana';
|
|
|
|
// Set value (multiselect - accepts string or array)
|
|
selectComponent.value = ['apple', 'banana'];
|
|
selectComponent.value = 'apple'; // normalized to ['apple']
|
|
});
|
|
</script>{% endset %}
|
|
{{ code_block(code_value | prettyHtml, "html") }}
|
|
</dd>
|
|
<dt><code>select(value)</code></dt>
|
|
<dd>
|
|
<p>Selects an option by value (i.e. the option with the matching <code>data-value</code> attribute). For single-select, this will close the popover. For multiselect, this adds the value to the selection if not already selected.</p>
|
|
|
|
{% set code_select %}<script>
|
|
const selectComponent = document.querySelector('#my-select');
|
|
selectComponent.addEventListener('basecoat:initialized', () => {
|
|
selectComponent.select('apple');
|
|
});
|
|
</script>{% endset %}
|
|
{{ code_block(code_select | prettyHtml, "html") }}
|
|
</dd>
|
|
<dt><code>deselect(value)</code> <span class="badge-secondary">Multiselect only</span></dt>
|
|
<dd>
|
|
<p>Removes a specific value from the selection. Only available when the component is in multiselect mode (<code>aria-multiselectable="true"</code>).</p>
|
|
|
|
{% set code_deselect %}<script>
|
|
const selectComponent = document.querySelector('#my-multiselect');
|
|
selectComponent.addEventListener('basecoat:initialized', () => {
|
|
selectComponent.deselect('apple');
|
|
});
|
|
</script>{% endset %}
|
|
{{ code_block(code_deselect | prettyHtml, "html") }}
|
|
</dd>
|
|
<dt><code>toggle(value)</code> <span class="badge-secondary">Multiselect only</span></dt>
|
|
<dd>
|
|
<p>Toggles a specific value in the selection (adds if not present, removes if present). Only available when the component is in multiselect mode (<code>aria-multiselectable="true"</code>).</p>
|
|
|
|
{% set code_toggle %}<script>
|
|
const selectComponent = document.querySelector('#my-multiselect');
|
|
selectComponent.addEventListener('basecoat:initialized', () => {
|
|
selectComponent.toggle('apple'); // adds if not selected, removes if selected
|
|
});
|
|
</script>{% endset %}
|
|
{{ code_block(code_toggle | prettyHtml, "html") }}
|
|
</dd>
|
|
<dt><code>selectAll()</code> <span class="badge-secondary">Multiselect only</span></dt>
|
|
<dd>
|
|
<p>Selects all available options. Only available when the component is in multiselect mode (<code>aria-multiselectable="true"</code>).</p>
|
|
|
|
{% set code_script_all %}<script>
|
|
const selectComponent = document.querySelector('#my-multiselect');
|
|
selectComponent.addEventListener('basecoat:initialized', () => {
|
|
selectComponent.selectAll();
|
|
});
|
|
</script>{% endset %}
|
|
{{ code_block(code_script_all | prettyHtml, "html") }}
|
|
</dd>
|
|
<dt><code>selectNone()</code> <span class="badge-secondary">Multiselect only</span></dt>
|
|
<dd>
|
|
<p>Deselects all options. Only available when the component is in multiselect mode (<code>aria-multiselectable="true"</code>).</p>
|
|
|
|
{% set code_script_none %}<script>
|
|
const selectComponent = document.querySelector('#my-multiselect');
|
|
selectComponent.addEventListener('basecoat:initialized', () => {
|
|
selectComponent.selectNone();
|
|
});
|
|
</script>{% endset %}
|
|
{{ code_block(code_script_none | prettyHtml, "html") }}
|
|
</dd>
|
|
<dt><code>selectByValue(value)</code></dt>
|
|
<dd>
|
|
<p><strong>Deprecated:</strong> Alias for <code>select(value)</code>. Kept for backward compatibility.</p>
|
|
</dd>
|
|
</dl>
|
|
</section>
|
|
|
|
<h3 id="usage-macro"><a href="#usage-macro">Jinja and Nunjucks</a></h3>
|
|
|
|
<div class="prose">
|
|
<p>You can use the <code class="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs">select()</code> Nunjucks or Jinja macro for this component.</p>
|
|
</div>
|
|
|
|
<div class="flex flex-wrap gap-2 my-6">
|
|
<a class="badge-outline" href="/installation/#install-macros" target="_blank">
|
|
Use Nunjucks or Jinja macros
|
|
{% lucide "arrow-right" %}
|
|
</a>
|
|
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/jinja/select.html.jinja" target="_blank">
|
|
Jinja macro
|
|
{% lucide "arrow-right" %}
|
|
</a>
|
|
<a class="badge-outline" href="https://github.com/hunvreus/basecoat/blob/main/src/nunjucks/select.njk" target="_blank">
|
|
Nunjucks macro
|
|
{% lucide "arrow-right" %}
|
|
</a>
|
|
</div>
|
|
|
|
{% set raw_code %}{% raw %}{{ select(
|
|
items=[
|
|
{
|
|
type: "group",
|
|
label: "Fruits",
|
|
items: [
|
|
{ type: "item", value: "apple", label: "Apple" },
|
|
{ type: "item", value: "banana", label: "Banana" },
|
|
{ type: "item", value: "blueberry", label: "Blueberry" },
|
|
{ type: "item", value: "grapes", label: "Grapes" },
|
|
{ type: "item", value: "pineapple", label: "Pineapple" }
|
|
]
|
|
}
|
|
]
|
|
) }}{% endraw %}{% endset %}
|
|
{{ code_block(raw_code, "jinja") }}
|
|
|
|
<h2 id="examples"><a href="#examples">Examples</a></h2>
|
|
|
|
<h3 id="example-scrollable"><a href="#example-scrollable">Scrollable</a></h3>
|
|
|
|
{% set code %}
|
|
{% call select(
|
|
listbox_attrs={"class": "scrollbar overflow-y-auto max-h-64"}
|
|
) %}
|
|
{% for i in range(0, 99) %}
|
|
<div role="option" {% if i > 0 %}data-value="item-{{ i }}"{% endif %}>Item {{ i }}</div>
|
|
{% endfor %}
|
|
{% endcall %}
|
|
{% endset %}
|
|
|
|
{{ code_preview("select-scrollable", code | prettyHtml) }}
|
|
|
|
<h3 id="example-disabled"><a href="#example-disabled">Disabled</a></h3>
|
|
|
|
{% set code %}
|
|
{% call select(
|
|
trigger_attrs={"disabled": "disabled"}
|
|
) %}
|
|
<div role="option" data-value="disabled">Disabled</div>
|
|
{% endcall %}
|
|
{% endset %}
|
|
|
|
{{ code_preview("select-disabled", code | prettyHtml) }}
|
|
|
|
<h3 id="example-invalid-state"><a href="#example-invalid-state">Invalid State</a></h3>
|
|
|
|
{% set code %}
|
|
{{ select(
|
|
trigger_attrs={"aria-invalid": "true"},
|
|
items=[
|
|
{ value: "", label: "Select a role" },
|
|
{ value: "admin", label: "Admin" },
|
|
{ value: "editor", label: "Editor" },
|
|
{ value: "viewer", label: "Viewer" },
|
|
{ value: "guest", label: "Guest" }
|
|
]
|
|
) }}
|
|
{% endset %}
|
|
|
|
{{ code_preview("select-disabled", code | prettyHtml) }}
|
|
|
|
<h3 id="example-with-icon"><a href="#example-with-icon">With icon</a></h3>
|
|
|
|
{% set code %}
|
|
{% call select(
|
|
id="select-with-icons",
|
|
trigger_attrs={'class': 'w-[180px]'}
|
|
) %}
|
|
<div type="button" role="option" data-value="bar">
|
|
<span class="flex items-center gap-2">
|
|
{% lucide "chart-bar", {"class": "text-muted-foreground"} %}
|
|
Bar
|
|
</span>
|
|
</div>
|
|
<div type="button" role="option" data-value="line">
|
|
<span class="flex items-center gap-2">
|
|
{% lucide "chart-line", {"class": "text-muted-foreground"} %}
|
|
Line
|
|
</span>
|
|
</div>
|
|
<div type="button" role="option" data-value="pie">
|
|
<span class="flex items-center gap-2">
|
|
{% lucide "chart-pie", {"class": "text-muted-foreground"} %}
|
|
Pie
|
|
</span>
|
|
</div>
|
|
{% endcall %}
|
|
{% endset %}
|
|
|
|
{{ code_preview("select-with-icons", code | prettyHtml) }}
|
|
|
|
<h3 id="example-multiple"><a href="#example-multiple">Multiple</a></h3>
|
|
|
|
<section class="prose">
|
|
<p>Enable multiple selection by setting adding <code>aria-multiselectable="true"</code> to the <code>listbox</code>. The selected options are displayed as a comma-separated list in the trigger button. Use <code>data-label</code> to specify clean text labels for options that contain HTML (like icons), otherwise the text content will be extracted automatically.</p>
|
|
</section>
|
|
|
|
{% set code %}
|
|
{% call select(
|
|
id="select-multiple",
|
|
name="chart-types",
|
|
multiple=true,
|
|
placeholder="Select chart types...",
|
|
selected=["bar", "line"],
|
|
trigger_attrs={"class": "w-[220px]"}
|
|
) %}
|
|
<div role="option" data-value="bar" data-label="Bar">
|
|
<span class="flex items-center gap-2">
|
|
{% lucide "chart-bar", {"class": "text-muted-foreground"} %}
|
|
Bar
|
|
</span>
|
|
</div>
|
|
<div role="option" data-value="line" data-label="Line">
|
|
<span class="flex items-center gap-2">
|
|
{% lucide "chart-line", {"class": "text-muted-foreground"} %}
|
|
Line
|
|
</span>
|
|
</div>
|
|
<div role="option" data-value="pie" data-label="Pie">
|
|
<span class="flex items-center gap-2">
|
|
{% lucide "chart-pie", {"class": "text-muted-foreground"} %}
|
|
Pie
|
|
</span>
|
|
</div>
|
|
<div role="option" data-value="area" data-label="Area">
|
|
<span class="flex items-center gap-2">
|
|
{% lucide "chart-area", {"class": "text-muted-foreground"} %}
|
|
Area
|
|
</span>
|
|
</div>
|
|
{% endcall %}
|
|
{% endset %}
|
|
|
|
{{ code_preview("select-multiple", code | prettyHtml) }}
|