Files
WRNexusJS/packages/ui/components/Accordion.wrn
T
Clintchiz 8d044565e3
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s
feat: publish changed packages independently
2026-08-11 15:32:36 +05:30

479 lines
14 KiB
Plaintext

component Accordion {
outputs {
change(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
open(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
close(payload: { sourceEvent?: Event; [key: string]: string | number | boolean | null | object })
}
props {
size: string = "default"
color: string = "primary"
variant: string = "default"
class: string = ""
id: string = "accordion"
items: unknown[] = []
defaultOpen: unknown[] = []
multiple: boolean = false
alwaysOpen: boolean = false
disabled: boolean = false
indicator: string = "plus"
indicatorPosition: string = "start"
showIndicator: boolean = true
bordered: boolean = false
separated: boolean = false
flush: boolean = false
contentItalic: boolean = false
}
state openValues = defaultOpen
functions {
shared function itemValue(item, index) {
return item.value !== undefined && item.value !== ""
? String(item.value)
: String(index)
}
shared function nestedValue(parent, parentIndex, item, index) {
return itemValue(parent, parentIndex) + "." + itemValue(item, index)
}
shared function isOpen(value) {
return openValues.includes(value)
}
shared function allowsMultiple() {
return multiple || alwaysOpen
}
// Named rather than computed: an output is resolved as a property name, so
// output[eventName] would not reach a parent binding.
client function dispatchAccordionEvent(sourceEvent, eventName, value, item, payload) {
payload = {
component: "Accordion",
value: value,
item: item,
open: isOpen(value),
openValues: openValues
}
if (eventName === "open") {
output.open(payload)
} else if (eventName === "close") {
output.close(payload)
} else {
output.change(payload)
}
}
client function toggleItem(sourceEvent, value, item, wasOpen) {
if (disabled || item.disabled) {
return
}
wasOpen = isOpen(value)
if (wasOpen) {
openValues = openValues.filter((entry) => entry !== value)
} else if (allowsMultiple()) {
openValues = openValues.concat([value])
} else {
openValues = [value]
}
dispatchAccordionEvent(
sourceEvent,
wasOpen ? "close" : "open",
value,
item
)
dispatchAccordionEvent(sourceEvent, "change", value, item)
}
}
view {
<div
{...attrs}
id="{id}"
data-wrn-accordion
data-variant="{variant}"
data-indicator="{indicator}"
data-multiple="{allowsMultiple() ? 'true' : 'false'}"
class="wire-component wire-component--color-{color} wire-component--size-{size} wire-next--accordion wire-next--accordion-{variant} {bordered ? 'wire-next--accordion-bordered' : ''} {separated ? 'wire-next--accordion-separated' : ''} {flush ? 'wire-next--accordion-flush' : ''} {disabled ? 'wire-next--disabled' : ''} {class}"
>
{#each items as item, index}
<section
class="wire-next__accordion-item"
data-open="{isOpen(itemValue(item, index)) ? 'true' : 'false'}"
data-disabled="{disabled || item.disabled ? 'true' : 'false'}"
>
<h3 class="wire-next__accordion-heading">
<button
type="button"
id="{id}-trigger-{index}"
aria-expanded="{isOpen(itemValue(item, index)) ? 'true' : 'false'}"
aria-controls="{id}-panel-{index}"
disabled="{disabled || item.disabled}"
@click="toggleItem(event, itemValue(item, index), item)"
>
{#if showIndicator && indicatorPosition === "start"}
<span class="wire-next__accordion-indicator" aria-hidden="true">
{#if indicator === "chevron"}
<span class="icon-[lucide--chevron-down]"></span>
{:else}
<span>+</span>
{/if}
</span>
{/if}
<span>{item.label || item.title}</span>
{#if showIndicator && indicatorPosition === "end"}
<span class="wire-next__accordion-indicator" aria-hidden="true">
{#if indicator === "chevron"}
<span class="icon-[lucide--chevron-down]"></span>
{:else}
<span>+</span>
{/if}
</span>
{/if}
</button>
</h3>
<div
id="{id}-panel-{index}"
class="wire-next__accordion-panel"
role="region"
aria-labelledby="{id}-trigger-{index}"
aria-hidden="{isOpen(itemValue(item, index)) ? 'false' : 'true'}"
>
<div>
<div class="wire-next__accordion-content {contentItalic ? 'wire-next__accordion-content-italic' : ''}">
{#if item.content}<p>{item.content}</p>{/if}
{#if item.children && item.children.length > 0}
<div class="wire-next__accordion-nested">
{#each item.children as child, childIndex}
<section
class="wire-next__accordion-item"
data-open="{isOpen(nestedValue(item, index, child, childIndex)) ? 'true' : 'false'}"
>
<h4 class="wire-next__accordion-heading">
<button
type="button"
id="{id}-trigger-{index}-{childIndex}"
aria-expanded="{isOpen(nestedValue(item, index, child, childIndex)) ? 'true' : 'false'}"
aria-controls="{id}-panel-{index}-{childIndex}"
disabled="{disabled || child.disabled}"
@click="toggleItem(event, nestedValue(item, index, child, childIndex), child)"
>
{#if showIndicator}
<span class="wire-next__accordion-indicator" aria-hidden="true">
{#if indicator === "chevron"}
<span class="icon-[lucide--chevron-down]"></span>
{:else}
<span>+</span>
{/if}
</span>
{/if}
<span>{child.label || child.title}</span>
</button>
</h4>
<div
id="{id}-panel-{index}-{childIndex}"
class="wire-next__accordion-panel"
role="region"
aria-labelledby="{id}-trigger-{index}-{childIndex}"
aria-hidden="{isOpen(nestedValue(item, index, child, childIndex)) ? 'false' : 'true'}"
>
<div>
<div class="wire-next__accordion-content {contentItalic ? 'wire-next__accordion-content-italic' : ''}">
<p>{child.content}</p>
</div>
</div>
</div>
</section>
{/each}
</div>
{/if}
</div>
</div>
</div>
</section>
{/each}
</div>
}
style {
.wire-next--accordion {
--wire-accordion-accent: var(--wire-component-color);
display: grid;
width: 100%;
color: var(--wire-color-text);
}
.wire-next__accordion-item {
min-width: 0;
}
.wire-next__accordion-heading {
margin: 0;
font: inherit;
}
.wire-next__accordion-heading > button {
display: flex;
width: 100%;
align-items: center;
gap: 0.75rem;
padding: 0.85rem 0;
border: 0;
background: transparent;
color: var(--wire-color-text);
font: inherit;
font-size: 0.88rem;
font-weight: 700;
text-align: left;
cursor: pointer;
}
.wire-next__accordion-heading > button > span:not(.wire-next__accordion-indicator) {
min-width: 0;
flex: 1;
}
.wire-next__accordion-heading > button:focus-visible {
border-radius: var(--wire-radius-sm);
outline: 2px solid var(--wire-accordion-accent);
outline-offset: 2px;
}
.wire-next__accordion-heading > button:disabled {
cursor: not-allowed;
opacity: 0.5;
}
.wire-next__accordion-indicator {
display: inline-flex;
width: 1rem;
height: 1rem;
flex: 0 0 1rem;
align-items: center;
justify-content: center;
color: var(--wire-color-muted);
font-size: 1rem;
font-weight: 500;
line-height: 1;
transition:
color var(--wire-motion-fast) var(--wire-ease-standard),
transform var(--wire-motion-fast) var(--wire-ease-standard);
}
.wire-next__accordion-indicator > span {
display: inline-flex;
align-items: center;
justify-content: center;
}
.wire-next__accordion-indicator > [class*="icon-["] {
width: 0.95rem;
height: 0.95rem;
}
.wire-next__accordion-item[data-open="true"]
> .wire-next__accordion-heading
.wire-next__accordion-indicator {
color: var(--wire-accordion-accent);
transform: rotate(45deg);
}
.wire-next--accordion[data-indicator="chevron"]
.wire-next__accordion-item[data-open="true"]
> .wire-next__accordion-heading
.wire-next__accordion-indicator {
transform: rotate(180deg);
}
.wire-next__accordion-item[data-open="true"] > .wire-next__accordion-heading > button {
color: var(--wire-accordion-accent);
}
.wire-next__accordion-panel {
display: grid;
grid-template-rows: 0fr;
opacity: 0;
transition:
grid-template-rows var(--wire-motion-base) var(--wire-ease-standard),
opacity var(--wire-motion-fast) var(--wire-ease-standard);
}
.wire-next__accordion-item[data-open="true"] > .wire-next__accordion-panel {
grid-template-rows: 1fr;
opacity: 1;
}
.wire-next__accordion-panel > div {
overflow: hidden;
}
.wire-next__accordion-content {
padding: 0 0 1rem 1.75rem;
color: var(--wire-color-muted);
font-size: 0.86rem;
line-height: 1.65;
}
.wire-next__accordion-content > p {
color: inherit;
}
.wire-next__accordion-content-italic > p::first-line {
font-style: italic;
}
.wire-next__accordion-nested {
display: grid;
margin-top: 0.5rem;
}
.wire-next__accordion-nested .wire-next__accordion-content {
padding-left: 1.5rem;
}
.wire-next--accordion-bordered {
overflow: hidden;
border: 1px solid var(--wire-color-border);
border-radius: var(--wire-radius-md);
background: var(--wire-color-surface);
}
.wire-next--accordion-bordered > .wire-next__accordion-item + .wire-next__accordion-item {
border-top: 1px solid var(--wire-color-border);
}
.wire-next--accordion-bordered
> .wire-next__accordion-item
> .wire-next__accordion-heading
> button {
padding-inline: 1rem;
}
.wire-next--accordion-bordered
> .wire-next__accordion-item
> .wire-next__accordion-panel
.wire-next__accordion-content {
padding-inline: 1rem;
}
.wire-next--accordion-separated {
gap: 0.5rem;
}
.wire-next--accordion-separated > .wire-next__accordion-item {
overflow: hidden;
border: 1px solid transparent;
border-radius: var(--wire-radius-md);
background: var(--wire-color-surface-2);
}
.wire-next--accordion-separated > .wire-next__accordion-item[data-open="true"] {
border-color: var(--wire-color-border);
}
.wire-next--accordion-separated
> .wire-next__accordion-item
> .wire-next__accordion-heading
> button,
.wire-next--accordion-separated
> .wire-next__accordion-item
> .wire-next__accordion-panel
.wire-next__accordion-content {
padding-inline: 1rem;
}
.wire-next--accordion-flush {
border-radius: 0;
background: transparent;
box-shadow: none;
}
@media (prefers-reduced-motion: reduce) {
.wire-next__accordion-panel,
.wire-next__accordion-indicator {
transition: none;
}
}
/* Shared component foundation. */
.wire-component {
--wire-component-color: var(--wire-color-primary);
--wire-component-scale: 1;
margin: 0;
color: var(--wire-color-text);
font-family: var(--wrn-font-sans, inherit);
}
.wire-component--color-primary {
--wire-component-color: var(--wire-color-primary);
}
.wire-component--color-secondary {
--wire-component-color: var(--wire-color-secondary);
}
.wire-component--color-success {
--wire-component-color: var(--wire-color-success);
}
.wire-component--color-warning {
--wire-component-color: var(--wire-color-warning);
}
.wire-component--color-danger {
--wire-component-color: var(--wire-color-danger);
}
.wire-component--color-info {
--wire-component-color: var(--wire-color-info);
}
.wire-component--size-xs {
--wire-component-scale: 0.78;
}
.wire-component--size-sm {
--wire-component-scale: 0.88;
}
.wire-component--size-default,
.wire-component--size-md {
--wire-component-scale: 1;
}
.wire-component--size-lg {
--wire-component-scale: 1.14;
}
.wire-component--size-xl {
--wire-component-scale: 1.28;
}
.wire-component:not(.wire-btn) {
font-size: calc(1em * var(--wire-component-scale));
}
.wire-component :is(a, button, input, select, textarea):focus-visible {
outline-color: var(--wire-component-color);
}
.wire-component p {
margin: 0;
color: var(--wire-color-muted);
line-height: 1.6;
}
}
}