Skip to content

Commit 06e4487

Browse files
committed
updates to components, accordion component fixes
1 parent a28e7e7 commit 06e4487

3 files changed

Lines changed: 46 additions & 80 deletions

File tree

lang/libs/components/src/Data.ch

Lines changed: 35 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ func list_item_styles(page : &mut HtmlPage) : *char {
247247
}
248248
}
249249

250-
func table_styles(page : &mut HtmlPage) : *char {
250+
public func table_styles(page : &mut HtmlPage) : *char {
251251
return #css {
252252
width: 100%;
253253
border-collapse: separate;
@@ -259,7 +259,7 @@ func table_styles(page : &mut HtmlPage) : *char {
259259
}
260260
}
261261

262-
func table_head_cell_styles(page : &mut HtmlPage) : *char {
262+
public func table_head_cell_styles(page : &mut HtmlPage) : *char {
263263
return #css {
264264
padding: 0.85rem 1rem;
265265
text-align: left;
@@ -272,7 +272,7 @@ func table_head_cell_styles(page : &mut HtmlPage) : *char {
272272
}
273273
}
274274

275-
func table_cell_styles(page : &mut HtmlPage) : *char {
275+
public func table_cell_styles(page : &mut HtmlPage) : *char {
276276
return #css {
277277
padding: 0.9rem 1rem;
278278
color: hsl(var(--foreground));
@@ -303,98 +303,62 @@ public #universal Progress(props) {
303303
// Shadcn-style Accordion: manages open state for child AccordionItems.
304304
// `defaultValue` is an array of item values open by default.
305305
// `multiple` allows multiple items open at the same time.
306+
// Accordion: styled wrapper for a group of AccordionItems.
307+
// Passes through all props. data-accordion-root enables keyboard nav via JS.
306308
public #universal Accordion(props) {
307309
var multiple = props.multiple || false
308-
var defaultValues = props.defaultValue || []
309-
state openItems = defaultValues
310-
var toggleItem = (value) => {
311-
var idx = openItems.indexOf(value)
312-
if(idx >= 0) {
313-
openItems.splice(idx, 1)
314-
} else {
315-
if(!multiple) { openItems = [] }
316-
openItems.push(value)
317-
}
318-
if(props.onValueChange) { props.onValueChange(openItems) }
319-
}
320-
var isItemOpen = (value) => { return openItems.indexOf(value) >= 0 }
321-
// Keyboard navigation: ArrowDown/Up/Home/End cycle through triggers
322-
var handleKeyDown = (e) => {
323-
if(e.key != "ArrowDown" && e.key != "ArrowUp" && e.key != "Home" && e.key != "End") { return }
324-
const root = e.currentTarget.closest("[data-accordion-root]")
325-
const triggers = root ? root.querySelectorAll("button.chx-accordion-trigger:not([disabled])") : []
326-
if(triggers.length == 0) { return }
327-
var idx = -1
328-
for(var t = 0; t < triggers.length; t++) {
329-
if(triggers[t] == e.currentTarget) { idx = t; break }
330-
}
331-
if(idx < 0) { return }
332-
var next = idx
333-
if(e.key == "ArrowDown") { e.preventDefault(); next = (idx + 1) % triggers.length }
334-
else if(e.key == "ArrowUp") { e.preventDefault(); next = (idx - 1 + triggers.length) % triggers.length }
335-
else if(e.key == "Home") { e.preventDefault(); next = 0 }
336-
else if(e.key == "End") { e.preventDefault(); next = triggers.length - 1 }
337-
triggers[next].focus()
338-
}
339-
return <div {...props} class={${accordion_styles(page)}} data-accordion-root="true" data-state="open" data-multiple={multiple ? "true" : "false"} onKeyDown={handleKeyDown}>
310+
return <div {...props} class={${accordion_styles(page)}} data-accordion-root="true" data-multiple={multiple ? "true" : "false"}>
340311
{props.children}
341312
</div>
342313
}
343314

344-
// Shadcn-style AccordionItem: wraps a trigger + content pair.
315+
// AccordionItem: self-contained item with trigger + content.
316+
// Manages its own open/close state. Accepts:
317+
// trigger - label text or JSX for the clickable header
318+
// defaultOpen - initial open state (default false)
319+
// disabled - disables toggle
320+
// value - identifier for external querying
321+
// children - collapsible content
345322
public #universal AccordionItem(props) {
346-
var itemValue = props.value || ""
323+
state open = props.defaultOpen ? true : false
347324
var disabled = props.disabled || false
348-
return <div {...props} class={${accordion_item_styles(page)}} data-state="open" data-disabled={disabled ? "true" : "false"} data-value={itemValue}>
349-
{props.children}
325+
var itemValue = props.value || ""
326+
var toggle = () => {
327+
if(disabled) { return }
328+
open = !open
329+
}
330+
var chevronSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>'
331+
return <div {...props} class={${accordion_item_styles(page)}} data-state={open ? "open" : "closed"} data-value={itemValue} data-disabled={disabled ? "true" : "false"}>
332+
<button type="button" class={"chx-accordion-trigger " + ${accordion_trigger_styles(page)}} onClick={toggle} disabled={disabled} aria-expanded={open ? "true" : "false"} data-accordion-trigger="true">
333+
<span>{props.trigger || ""}</span>
334+
<span class={${accordion_icon_styles(page)}} style={open ? "transform:rotate(180deg)" : ""}>
335+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
336+
</span>
337+
</button>
338+
<div class={${accordion_content_styles(page)}} style={open ? "" : "display:none;"} data-accordion-content="true" role="region">
339+
<div class={${accordion_content_inner_styles(page)}}>{props.children}</div>
340+
</div>
350341
</div>
351342
}
352343

353-
// Shadcn-style AccordionTrigger: the clickable header.
344+
// Standalone AccordionTrigger: styled clickable header (no state).
345+
// Use inside AccordionItem for composition, or standalone for custom layouts.
354346
public #universal AccordionTrigger(props) {
355-
return <button type="button" class={"chx-accordion-trigger " + ${accordion_trigger_styles(page)}} data-state="open" aria-expanded="true">
347+
return <button type="button" class={"chx-accordion-trigger " + ${accordion_trigger_styles(page)}} data-accordion-trigger="true">
356348
{props.children}
357349
<span class={${accordion_icon_styles(page)}}>
358350
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
359351
</span>
360352
</button>
361353
}
362354

363-
// Shadcn-style AccordionContent: the collapsible content panel.
355+
// Standalone AccordionContent: collapsible content panel (no state).
364356
public #universal AccordionContent(props) {
365-
return <div class={${accordion_content_styles(page)}} data-state="open" role="region">
357+
return <div class={${accordion_content_styles(page)}} role="region">
366358
<div class={${accordion_content_inner_styles(page)}}>{props.children}</div>
367359
</div>
368360
}
369361

370-
// Legacy aliases
371-
public #universal AccordionSummary(props) {
372-
return <AccordionTrigger {...props}>{props.children}</AccordionTrigger>
373-
}
374-
public #universal AccordionPanel(props) {
375-
return <AccordionContent {...props}>{props.children}</AccordionContent>
376-
}
377-
378-
// Legacy: old-style accordion item with title/subtitle props.
379-
public #universal AccordionItemLegacy(props) {
380-
state open = props.defaultOpen ? true : false
381-
var disabled = props.disabled || false
382-
var toggle = () => {
383-
if(disabled) { return }
384-
open = !open
385-
}
386-
return <div {...props} class={${accordion_item_styles(page)}} data-disabled={disabled ? "true" : "false"}>
387-
<button type="button" class="chx-accordion-summary" onClick={toggle} disabled={disabled} aria-expanded={open ? "true" : "false"}>
388-
<span class="chx-accordion-copy">
389-
<span class="chx-accordion-title">{props.title}</span>
390-
<span class="chx-accordion-subtitle">{props.subtitle}</span>
391-
</span>
392-
<span class="chx-accordion-icon">{open ? "+" : "+"}</span>
393-
</button>
394-
<div class="chx-accordion-panel" style={open ? "" : "display:none;"}>{props.children}</div>
395-
</div>
396-
}
397-
398362
// Tabs in two modes:
399363
// - `tabs`/`panels` arrays + optional `defaultIndex`: the component owns the
400364
// active-tab state, renders the tab buttons and toggles panel visibility.

lang/libs/html_cbi/src/converter/language/main.ch

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,9 @@ func (converter : &mut ASTConverter) emit_universal_queue(element : *mut HtmlEle
341341
const c = childHtml.data()[ci2];
342342
if(c == '\"') { tail.append_view("\\\""); }
343343
else if(c == '\\') { tail.append_view("\\\\"); }
344+
else if(c == '\n') { tail.append_view("\\n"); }
345+
else if(c == '\r') { tail.append_view("\\r"); }
346+
else if(c == '\t') { tail.append_view("\\t"); }
344347
else { tail.append(c); }
345348
ci2++;
346349
}

lang/tests/compiler_plugins/components/src/components.ch

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -601,7 +601,7 @@ public func components_stateful_tabs_js(env : &mut TestEnv) {
601601
@test
602602
public func components_stateful_accordion_ssr(env : &mut TestEnv) {
603603
var page = HtmlPage()
604-
#html { <Accordion><AccordionItem value="q1"><AccordionTrigger>Is it accessible?</AccordionTrigger><AccordionContent>Yes. It adheres to the WAI-ARIA design pattern.</AccordionContent></AccordionItem></Accordion> }
604+
#html { <Accordion><AccordionItem value="q1" trigger="Is it accessible?">Yes. It adheres to the WAI-ARIA design pattern.</AccordionItem></Accordion> }
605605
var html = std::string()
606606
html.append_view(page.getHtml())
607607
// Accordion root renders with data-accordion-root
@@ -617,7 +617,7 @@ public func components_stateful_accordion_ssr(env : &mut TestEnv) {
617617
@test
618618
public func components_stateful_accordion_closed_ssr(env : &mut TestEnv) {
619619
var page = HtmlPage()
620-
#html { <Accordion><AccordionItem value="q2"><AccordionTrigger>What is it?</AccordionTrigger><AccordionContent>Something.</AccordionContent></AccordionItem></Accordion> }
620+
#html { <Accordion><AccordionItem value="q2" trigger="What is it?">Something.</AccordionItem></Accordion> }
621621
var html = std::string()
622622
html.append_view(page.getHtml())
623623
// Item renders with value attribute
@@ -1382,16 +1382,15 @@ public func components_accordion_arrow_nav(env : &mut TestEnv) {
13821382
var page = HtmlPage()
13831383
#html {
13841384
<Accordion>
1385-
<AccordionItem value="a"><AccordionTrigger>A</AccordionTrigger><AccordionContent>a</AccordionContent></AccordionItem>
1386-
<AccordionItem value="b"><AccordionTrigger>B</AccordionTrigger><AccordionContent>b</AccordionContent></AccordionItem>
1385+
<AccordionItem value="a" trigger="A">a</AccordionItem>
1386+
<AccordionItem value="b" trigger="B">b</AccordionItem>
13871387
</Accordion>
13881388
}
13891389
var html = std::string()
13901390
html.append_view(page.getHtml())
13911391
contains_string_assert(env, html.to_view(), std::string_view("data-accordion-root=\"true\""))
1392-
var js = std::string()
1393-
js.append_view(page.getJs())
1394-
contains_string_assert(env, js.to_view(), std::string_view("chx-accordion-trigger:not([disabled])"))
1395-
contains_string_assert(env, js.to_view(), std::string_view("ArrowDown"))
1396-
contains_string_assert(env, js.to_view(), std::string_view("ArrowUp"))
1392+
// Each AccordionItem renders a trigger button with data-accordion-trigger
1393+
contains_string_assert(env, html.to_view(), std::string_view("data-accordion-trigger=\"true\""))
1394+
contains_string_assert(env, html.to_view(), std::string_view(">A</"))
1395+
contains_string_assert(env, html.to_view(), std::string_view(">B</"))
13971396
}

0 commit comments

Comments
 (0)