import { html, type TemplateResult } from 'lit';
import type { ListboxOption } from '../../components/nui-listbox/types.js';
import {
formatUsageFromDefaults,
type PlaygroundControl,
type PlaygroundDefinition,
type PlaygroundPreviewHandlers,
type PlaygroundProps,
whenBoolean,
whenString,
} from '../types.js';
const DEFAULT_OPTIONS: ListboxOption[] = [
{ label: 'New York', value: 'NY' },
{ label: 'Rome', value: 'RM' },
{ label: 'London', value: 'LDN' },
{ label: 'Istanbul', value: 'IST' },
{ label: 'Paris', value: 'PRS' },
];
const ATTRIBUTE_NAMES: Record = {
optionLabel: 'option-label',
optionValue: 'option-value',
optionDisabled: 'option-disabled',
filterPlaceholder: 'filter-placeholder',
listScrollHeight: 'scroll-height',
highlightOnSelect: 'highlight-on-select',
emptyMessage: 'empty-message',
emptyFilterMessage: 'empty-filter-message',
ariaLabel: 'aria-label',
ariaLabelledby: 'aria-labelledby',
nuiType: 'nui-type',
listboxClass: 'listbox-class',
};
export const NUI_LISTBOX_DEFAULTS: PlaygroundProps = {
value: 'NY',
options: JSON.stringify(DEFAULT_OPTIONS, null, 2),
optionLabel: 'label',
optionValue: 'value',
optionDisabled: 'disabled',
multiple: false,
disabled: false,
invalid: false,
readonly: false,
filter: false,
filterPlaceholder: '',
listScrollHeight: '14rem',
striped: false,
checkmark: false,
highlightOnSelect: true,
emptyMessage: 'No results found',
emptyFilterMessage: 'No results found',
fluid: false,
tabindex: '0',
unstyled: false,
nuiType: '',
listboxClass: '',
ariaLabel: '',
ariaLabelledby: '',
};
const CONTROLS: PlaygroundControl[] = [
{
key: 'value',
label: 'value',
type: 'text',
section: 'Content',
placeholder: 'NY',
},
{
key: 'options',
label: 'options (JSON)',
type: 'textarea',
section: 'Content',
rows: 8,
fullWidth: true,
placeholder: '[{"label":"New York","value":"NY"}]',
},
{
key: 'optionLabel',
label: 'option-label',
type: 'text',
section: 'Content',
placeholder: 'label',
},
{
key: 'optionValue',
label: 'option-value',
type: 'text',
section: 'Content',
placeholder: 'value',
},
{
key: 'optionDisabled',
label: 'option-disabled',
type: 'text',
section: 'Content',
placeholder: 'disabled',
},
{
key: 'emptyMessage',
label: 'empty-message',
type: 'text',
section: 'Content',
},
{
key: 'emptyFilterMessage',
label: 'empty-filter-message',
type: 'text',
section: 'Content',
},
{
key: 'listScrollHeight',
label: 'scroll-height',
type: 'text',
section: 'Appearance',
placeholder: '14rem',
},
{ key: 'nuiType', label: 'nui-type', type: 'text', section: 'Appearance' },
{
key: 'listboxClass',
label: 'listbox-class',
type: 'text',
section: 'Appearance',
},
{
key: 'striped',
label: 'striped',
type: 'boolean',
section: 'Appearance',
},
{
key: 'checkmark',
label: 'checkmark',
type: 'boolean',
section: 'Appearance',
},
{
key: 'highlightOnSelect',
label: 'highlight-on-select',
type: 'boolean',
section: 'Appearance',
},
{ key: 'filter', label: 'filter', type: 'boolean', section: 'Modifiers' },
{
key: 'filterPlaceholder',
label: 'filter-placeholder',
type: 'text',
section: 'Modifiers',
placeholder: 'Search',
},
{
key: 'unstyled',
label: 'unstyled',
type: 'boolean',
section: 'Modifiers',
},
{ key: 'fluid', label: 'fluid', type: 'boolean', section: 'Layout' },
{
key: 'tabindex',
label: 'tabindex',
type: 'text',
section: 'Layout',
placeholder: '0',
},
{ key: 'multiple', label: 'multiple', type: 'boolean', section: 'State' },
{ key: 'disabled', label: 'disabled', type: 'boolean', section: 'State' },
{ key: 'readonly', label: 'readonly', type: 'boolean', section: 'State' },
{ key: 'invalid', label: 'invalid', type: 'boolean', section: 'State' },
{
key: 'ariaLabel',
label: 'aria-label',
type: 'text',
section: 'HTML',
},
{
key: 'ariaLabelledby',
label: 'aria-labelledby',
type: 'text',
section: 'HTML',
},
];
function parseOptions(value: unknown): ListboxOption[] {
if (typeof value !== 'string' || !value.trim()) {
return [];
}
try {
const parsed = JSON.parse(value) as unknown;
return Array.isArray(parsed) ? (parsed as ListboxOption[]) : [];
} catch {
return [];
}
}
function parsePlaygroundValue(
value: unknown,
multiple: boolean,
): string | number | (string | number)[] | null {
if (multiple) {
if (typeof value !== 'string' || !value.trim()) {
return [];
}
try {
const parsed = JSON.parse(value) as unknown;
return Array.isArray(parsed) ? (parsed as (string | number)[]) : [];
} catch {
return [];
}
}
if (value === undefined || value === null || value === '') {
return null;
}
const asNumber = Number(value);
return Number.isFinite(asNumber) && String(asNumber) === String(value)
? asNumber
: String(value);
}
function handleListboxChange(
event: Event,
props: PlaygroundProps,
handlers?: PlaygroundPreviewHandlers,
): void {
if (!handlers) {
return;
}
const value = (event as CustomEvent<{ value: unknown }>).detail.value;
handlers.onPropChange(
'value',
props.multiple ? JSON.stringify(value) : String(value ?? ''),
);
}
function renderPreview(
props: PlaygroundProps,
handlers?: PlaygroundPreviewHandlers,
): TemplateResult {
const multiple = Boolean(props.multiple);
return html`
handleListboxChange(event, props, handlers)}
>
`;
}
export const nuiListboxPlayground: PlaygroundDefinition = {
tag: 'nui-listbox',
label: 'Listbox',
description:
'List of selectable options with optional filter, multiple selection, and keyboard navigation.',
defaults: NUI_LISTBOX_DEFAULTS,
controls: CONTROLS,
renderPreview,
getPreviewClass: (props) => (props.fluid ? 'is-fluid' : ''),
formatUsage: (props) => {
const { options: _options, value: _value, ...usageProps } = props;
return formatUsageFromDefaults(
'nui-listbox',
usageProps,
NUI_LISTBOX_DEFAULTS,
ATTRIBUTE_NAMES,
);
},
};