diff --git a/bun.lock b/bun.lock index 1f27b58f..6c84c3be 100644 --- a/bun.lock +++ b/bun.lock @@ -688,7 +688,7 @@ }, "packages/ui": { "name": "@wrnexus/ui", - "version": "0.8.56", + "version": "0.8.57", "dependencies": { "@wrnexus/core": "workspace:*", }, diff --git a/packages/ui/COMPONENTS.md b/packages/ui/COMPONENTS.md index b82607da..4f65681d 100644 --- a/packages/ui/COMPONENTS.md +++ b/packages/ui/COMPONENTS.md @@ -119,7 +119,7 @@ Theme-aware, responsive badge component. Theme-aware, responsive blockquote component. - Mount: `data-component="Blockquote"` -- Props: `quote: string = "I just wanted to say that I'm very happy with my purchase so far. The documentation is outstanding - clear and detailed."`, `citation: string = ""`, `citationTitle: string = ""`, `citationUrl: string = ""`, `avatarSrc: string = ""`, `avatarAlt: string = ""`, `size: string = "md"`, `color: string = "primary"`, `align: string = "left"`, `variant: string = "default"`, `quoteMark: boolean = true`, `italic: boolean = true`, `class: string = ""` +- Props: `quote: string = "I just wanted to say that I'm very happy with my purchase so far. The documentation is outstanding - clear and detailed."`, `citation: string = ""`, `citationTitle: string = ""`, `citationUrl: string = ""`, `avatarSrc: string = ""`, `avatarAlt: string = ""`, `size: string = "md"`, `color: string = "primary"`, `align: string = "left"`, `variant: string = "default"`, `quoteMark: boolean = true`, `italic: boolean = true`, `maxWidth: string = "xl"`, `class: string = ""` - Slots: `default` - Outputs: None @@ -290,7 +290,7 @@ Theme-aware, responsive styled icon component. Present responsive chronological activity, milestones, or workflow status with rich item metadata. - Mount: `data-component="Timeline"` -- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Timeline"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `class: string = ""` +- Props: `size: string = "default"`, `color: string = "primary"`, `title: string = "Timeline"`, `description: string = ""`, `items: unknown[] = []`, `variant: string = "default"`, `numbered: boolean = false`, `class: string = ""` - Slots: `default` - Outputs: `select({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })` @@ -827,7 +827,7 @@ Theme-aware, responsive navbar component. - Mount: `data-component="Navbar"` - Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Primary navigation"`, `topbarLabel: string = "Utility navigation"`, `brand: Record = {}`, `items: unknown[] = []`, `actions: unknown[] = []`, `active: string = ""`, `sticky: boolean = false`, `floating: boolean = false`, `startFloating: string = "0px"`, `openOnHover: boolean = false`, `maxWidth: string = "full"`, `mobileLabel: string = "Toggle navigation"`, `collapseAt: string = "900px"`, `mobilePanel: string = "content"`, `mobileUtilitiesPosition: string = "header"`, `gutter: string = "md"`, `contentGutter: string = "md"`, `flushMobile: boolean = false`, `translationPrefix: string = ""`, `class: string = ""` -- Slots: `topbar`, `actions`, `utilities` +- Slots: `topbar`, `brand`, `actions`, `utilities` - Outputs: `toggle({ open: boolean })`, `open({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `close({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `select({ item: string | number | boolean | null | object; value: string | number | boolean; level: string })`, `action({ item: string | number | boolean | null | object; value: string | number | boolean })` ### Pagination @@ -853,7 +853,7 @@ Theme-aware, responsive scrollspy component. Theme-aware, responsive sidebar component. - Mount: `data-component="Sidebar"` -- Props: `color: string = "primary"`, `size: string = "default"`, `label: string = "Sidebar"`, `items: unknown[] = []`, `active: string = ""`, `mobileLabel: string = "Open navigation"`, `drawerTitle: string = "Navigation"`, `class: string = ""` +- Props: `color: string = "primary"`, `size: string = "default"`, `label: string = "Sidebar"`, `items: unknown[] = []`, `active: string = ""`, `mobileLabel: string = "Open navigation"`, `drawerTitle: string = "Navigation"`, `mobile: boolean = true`, `drawerPlacement: string = "left"`, `class: string = ""` - Slots: `default`, `drawer` - Outputs: `toggle({ open: boolean })`, `open({ sourceEvent: Event })`, `close({ source: string })`, `select({ item: object; value: string; level: number })` @@ -872,7 +872,7 @@ Switch between related responsive content panels with horizontal or vertical ori - Mount: `data-component="Tabs"` - Props: `color: string = "primary"`, `size: string = "default"`, `items: unknown[] = []`, `active: string = ""`, `orientation: string = "horizontal"`, `mode: string = "client"`, `param: string = "tab"`, `label: string = "Tabs"`, `class: string = ""` -- Slots: `panel-{valueOf(item, index)}`, `default` +- Slots: `default` - Outputs: `change({ value: string; item: object; index: number })`, `select({ value: string; item: object; index: number })` ## Overlays @@ -900,7 +900,7 @@ Present responsive modal side or bottom content with focus management, backdrop Open an accessible anchored menu with keyboard navigation, item selection, actions, and responsive placement. - Mount: `data-component="Dropdown"` -- Props: `items: unknown[] = []`, `open: boolean = false`, `defaultOpen: boolean = false`, `label: string = "Open menu"`, `icon: string = ""`, `showChevron: boolean = true`, `menuLabel: string = "Dropdown menu"`, `placement: string = "bottom-start"`, `width: string = "md"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "raised"`, `closeOnSelect: boolean = true`, `closeOnOutside: boolean = true`, `disabled: boolean = false`, `emptyLabel: string = "No menu items"`, `class: string = ""` +- Props: `items: unknown[] = []`, `open: boolean = false`, `defaultOpen: boolean = false`, `label: string = "Open menu"`, `ariaLabel: string = ""`, `icon: string = ""`, `showChevron: boolean = true`, `menuLabel: string = "Dropdown menu"`, `placement: string = "bottom-start"`, `width: string = "md"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "raised"`, `closeOnSelect: boolean = true`, `closeOnOutside: boolean = true`, `disabled: boolean = false`, `emptyLabel: string = "No menu items"`, `class: string = ""` - Slots: `trigger`, `header`, `default`, `footer` - Outputs: `toggle({ open: boolean; sourceEvent: Event; reason?: object })`, `open({ sourceEvent: Event })`, `close({ reason: string; sourceEvent: Event })`, `select({ item: string | number | boolean | null | object; itemIndex: number; value: string | number | boolean; sourceEvent: Event })`, `action({ item: string | number | boolean | null | object; itemIndex: number; value: string | number | boolean; sourceEvent: Event })` diff --git a/packages/ui/component-reference.json b/packages/ui/component-reference.json index 50f01a16..bfba62ee 100644 --- a/packages/ui/component-reference.json +++ b/packages/ui/component-reference.json @@ -1751,6 +1751,13 @@ "default": "true", "options": [] }, + { + "name": "maxWidth", + "type": "string", + "required": false, + "default": "\"xl\"", + "options": [] + }, { "name": "class", "type": "string", @@ -1791,6 +1798,13 @@ "default": "\"\"", "options": [] }, + { + "name": "activeIcon", + "type": "string", + "required": false, + "default": "\"icon-[lucide--file-text]\"", + "options": [] + }, { "name": "separator", "type": "string", @@ -5461,6 +5475,13 @@ "default": "\"Open menu\"", "options": [] }, + { + "name": "ariaLabel", + "type": "string", + "required": false, + "default": "\"\"", + "options": [] + }, { "name": "icon", "type": "string", @@ -9282,7 +9303,7 @@ "options": [] } ], - "slots": ["topbar", "actions", "utilities"], + "slots": ["topbar", "brand", "actions", "utilities"], "outputs": [ { "name": "toggle", @@ -11528,6 +11549,20 @@ "default": "\"Navigation\"", "options": [] }, + { + "name": "mobile", + "type": "boolean", + "required": false, + "default": "true", + "options": [] + }, + { + "name": "drawerPlacement", + "type": "string", + "required": false, + "default": "\"left\"", + "options": [] + }, { "name": "class", "type": "string", @@ -12727,7 +12762,7 @@ "options": [] } ], - "slots": ["panel-{valueOf(item, index)}", "default"], + "slots": ["default"], "outputs": [ { "name": "change", @@ -13329,6 +13364,13 @@ "default": "\"default\"", "options": [] }, + { + "name": "numbered", + "type": "boolean", + "required": false, + "default": "false", + "options": [] + }, { "name": "class", "type": "string", diff --git a/packages/ui/components/Dropdown.wrn b/packages/ui/components/Dropdown.wrn index a50a4127..4b099e97 100644 --- a/packages/ui/components/Dropdown.wrn +++ b/packages/ui/components/Dropdown.wrn @@ -12,6 +12,7 @@ items: unknown[] = [] open: boolean = false defaultOpen: boolean = false label: string = "Open menu" + ariaLabel: string = "" icon: string = "" showChevron: boolean = true menuLabel: string = "Dropdown menu" @@ -47,6 +48,11 @@ items: unknown[] = [] visible = false output.close({ reason: reason, sourceEvent: sourceEvent }) output.toggle({ open: false, reason: reason, sourceEvent: sourceEvent }) + if (reason === "escape") { + const root = sourceEvent.currentTarget.closest(".wrn-dropdown") || sourceEvent.currentTarget + const trigger = root.querySelector(".wrn-dropdown__trigger") + if (trigger) { trigger.focus() } + } } client function toggleMenu(sourceEvent) { @@ -65,14 +71,14 @@ items: unknown[] = [] output.select({ item: item, itemIndex: itemIndex, - value: item.value || "", + value: item.value == null ? "" : item.value, sourceEvent: sourceEvent }) if (item.action) { output.action({ item: item, itemIndex: itemIndex, - value: item.value || "", + value: item.value == null ? "" : item.value, sourceEvent: sourceEvent }) } @@ -83,18 +89,24 @@ items: unknown[] = [] client function moveFocus(sourceEvent, direction) { const root = sourceEvent.currentTarget.closest(".wrn-dropdown") || sourceEvent.currentTarget - const options = [...root.querySelectorAll("[data-dropdown-item]:not([disabled])")] - if (!options.length) { - return - } - const activeIndex = options.indexOf(document.activeElement) - const nextIndex = (activeIndex + direction + options.length) % options.length - options[nextIndex].focus() + window.setTimeout(function () { + if (!root.isConnected || !isOpen()) { return } + const options = [...root.querySelectorAll('[data-dropdown-item]:not([disabled]):not([aria-disabled="true"])')] + if (!options.length) { + return + } + const activeIndex = options.indexOf(document.activeElement) + const nextIndex = activeIndex < 0 ? (direction > 0 ? 0 : options.length - 1) : (activeIndex + direction + options.length) % options.length + options[nextIndex].focus() + }, 0) } client function handleKeydown(sourceEvent) { + if (disabled) { return } if (sourceEvent.key === "Escape") { + if (!isOpen()) { return } sourceEvent.preventDefault() + sourceEvent.stopPropagation() hideMenu("escape", sourceEvent) } else if (sourceEvent.key === "ArrowDown") { sourceEvent.preventDefault() @@ -111,19 +123,26 @@ items: unknown[] = [] } else if (sourceEvent.key === "Home" && isOpen()) { sourceEvent.preventDefault() const root = sourceEvent.currentTarget.closest(".wrn-dropdown") || sourceEvent.currentTarget - const first = root.querySelector("[data-dropdown-item]:not([disabled])") + const first = root.querySelector('[data-dropdown-item]:not([disabled]):not([aria-disabled="true"])') if (first) { first.focus() } } else if (sourceEvent.key === "End" && isOpen()) { sourceEvent.preventDefault() const root = sourceEvent.currentTarget.closest(".wrn-dropdown") || sourceEvent.currentTarget - const options = [...root.querySelectorAll("[data-dropdown-item]:not([disabled])")] + const options = [...root.querySelectorAll('[data-dropdown-item]:not([disabled]):not([aria-disabled="true"])')] if (options.length) { options[options.length - 1].focus() } } } + + client function handleFocusout(sourceEvent) { + const root = sourceEvent.currentTarget + if (isOpen() && sourceEvent.relatedTarget && !root.contains(sourceEvent.relatedTarget)) { + hideMenu("focusout", sourceEvent) + } + } } view { @@ -138,12 +157,14 @@ items: unknown[] = [] data-variant='{variant}' class='wrn-dropdown {class}' @keydown='handleKeydown(event)' + @focusout='handleFocusout(event)' >