fix(ui): repair sidebar drawer, dropdown focus and footer slots
This commit is contained in:
1 parent
f7fe3bb17b
commit
6acf78a72c
8 files changed
+378
-34
No files matched your search
@@ -688,7 +688,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@wrnexus/ui",
|
"name": "@wrnexus/ui",
|
||||||
"version": "0.8.56",
|
"version": "0.8.57",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ Theme-aware, responsive badge component.
|
|||||||
Theme-aware, responsive blockquote component.
|
Theme-aware, responsive blockquote component.
|
||||||
|
|
||||||
- Mount: `data-component="Blockquote"`
|
- 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`
|
- Slots: `default`
|
||||||
- Outputs: None
|
- Outputs: None
|
||||||
|
|
||||||
@@ -290,7 +290,7 @@ Theme-aware, responsive styled icon component.
|
|||||||
Present responsive chronological activity, milestones, or workflow status with rich item metadata.
|
Present responsive chronological activity, milestones, or workflow status with rich item metadata.
|
||||||
|
|
||||||
- Mount: `data-component="Timeline"`
|
- 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`
|
- Slots: `default`
|
||||||
- Outputs: `select({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
|
- Outputs: `select({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
|
||||||
|
|
||||||
@@ -827,7 +827,7 @@ Theme-aware, responsive navbar component.
|
|||||||
|
|
||||||
- Mount: `data-component="Navbar"`
|
- Mount: `data-component="Navbar"`
|
||||||
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Primary navigation"`, `topbarLabel: string = "Utility navigation"`, `brand: Record<string, unknown> = {}`, `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 = ""`
|
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Primary navigation"`, `topbarLabel: string = "Utility navigation"`, `brand: Record<string, unknown> = {}`, `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 })`
|
- 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
|
### Pagination
|
||||||
@@ -853,7 +853,7 @@ Theme-aware, responsive scrollspy component.
|
|||||||
Theme-aware, responsive sidebar component.
|
Theme-aware, responsive sidebar component.
|
||||||
|
|
||||||
- Mount: `data-component="Sidebar"`
|
- 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`
|
- Slots: `default`, `drawer`
|
||||||
- Outputs: `toggle({ open: boolean })`, `open({ sourceEvent: Event })`, `close({ source: string })`, `select({ item: object; value: string; level: number })`
|
- 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"`
|
- 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 = ""`
|
- 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 })`
|
- Outputs: `change({ value: string; item: object; index: number })`, `select({ value: string; item: object; index: number })`
|
||||||
|
|
||||||
## Overlays
|
## 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.
|
Open an accessible anchored menu with keyboard navigation, item selection, actions, and responsive placement.
|
||||||
|
|
||||||
- Mount: `data-component="Dropdown"`
|
- 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`
|
- 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 })`
|
- 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 })`
|
||||||
|
|
||||||
|
|||||||
@@ -1751,6 +1751,13 @@
|
|||||||
"default": "true",
|
"default": "true",
|
||||||
"options": []
|
"options": []
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "maxWidth",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"xl\"",
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "class",
|
"name": "class",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -1791,6 +1798,13 @@
|
|||||||
"default": "\"\"",
|
"default": "\"\"",
|
||||||
"options": []
|
"options": []
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "activeIcon",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"icon-[lucide--file-text]\"",
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "separator",
|
"name": "separator",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -5461,6 +5475,13 @@
|
|||||||
"default": "\"Open menu\"",
|
"default": "\"Open menu\"",
|
||||||
"options": []
|
"options": []
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "ariaLabel",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"\"",
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "icon",
|
"name": "icon",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -9282,7 +9303,7 @@
|
|||||||
"options": []
|
"options": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"slots": ["topbar", "actions", "utilities"],
|
"slots": ["topbar", "brand", "actions", "utilities"],
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
"name": "toggle",
|
"name": "toggle",
|
||||||
@@ -11528,6 +11549,20 @@
|
|||||||
"default": "\"Navigation\"",
|
"default": "\"Navigation\"",
|
||||||
"options": []
|
"options": []
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "mobile",
|
||||||
|
"type": "boolean",
|
||||||
|
"required": false,
|
||||||
|
"default": "true",
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "drawerPlacement",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"left\"",
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "class",
|
"name": "class",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -12727,7 +12762,7 @@
|
|||||||
"options": []
|
"options": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"slots": ["panel-{valueOf(item, index)}", "default"],
|
"slots": ["default"],
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
"name": "change",
|
"name": "change",
|
||||||
@@ -13329,6 +13364,13 @@
|
|||||||
"default": "\"default\"",
|
"default": "\"default\"",
|
||||||
"options": []
|
"options": []
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "numbered",
|
||||||
|
"type": "boolean",
|
||||||
|
"required": false,
|
||||||
|
"default": "false",
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "class",
|
"name": "class",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ items: unknown[] = []
|
|||||||
open: boolean = false
|
open: boolean = false
|
||||||
defaultOpen: boolean = false
|
defaultOpen: boolean = false
|
||||||
label: string = "Open menu"
|
label: string = "Open menu"
|
||||||
|
ariaLabel: string = ""
|
||||||
icon: string = ""
|
icon: string = ""
|
||||||
showChevron: boolean = true
|
showChevron: boolean = true
|
||||||
menuLabel: string = "Dropdown menu"
|
menuLabel: string = "Dropdown menu"
|
||||||
@@ -47,6 +48,11 @@ items: unknown[] = []
|
|||||||
visible = false
|
visible = false
|
||||||
output.close({ reason: reason, sourceEvent: sourceEvent })
|
output.close({ reason: reason, sourceEvent: sourceEvent })
|
||||||
output.toggle({ open: false, 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) {
|
client function toggleMenu(sourceEvent) {
|
||||||
@@ -65,14 +71,14 @@ items: unknown[] = []
|
|||||||
output.select({
|
output.select({
|
||||||
item: item,
|
item: item,
|
||||||
itemIndex: itemIndex,
|
itemIndex: itemIndex,
|
||||||
value: item.value || "",
|
value: item.value == null ? "" : item.value,
|
||||||
sourceEvent: sourceEvent
|
sourceEvent: sourceEvent
|
||||||
})
|
})
|
||||||
if (item.action) {
|
if (item.action) {
|
||||||
output.action({
|
output.action({
|
||||||
item: item,
|
item: item,
|
||||||
itemIndex: itemIndex,
|
itemIndex: itemIndex,
|
||||||
value: item.value || "",
|
value: item.value == null ? "" : item.value,
|
||||||
sourceEvent: sourceEvent
|
sourceEvent: sourceEvent
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -83,18 +89,24 @@ items: unknown[] = []
|
|||||||
|
|
||||||
client function moveFocus(sourceEvent, direction) {
|
client function moveFocus(sourceEvent, direction) {
|
||||||
const root = sourceEvent.currentTarget.closest(".wrn-dropdown") || sourceEvent.currentTarget
|
const root = sourceEvent.currentTarget.closest(".wrn-dropdown") || sourceEvent.currentTarget
|
||||||
const options = [...root.querySelectorAll("[data-dropdown-item]:not([disabled])")]
|
window.setTimeout(function () {
|
||||||
if (!options.length) {
|
if (!root.isConnected || !isOpen()) { return }
|
||||||
return
|
const options = [...root.querySelectorAll('[data-dropdown-item]:not([disabled]):not([aria-disabled="true"])')]
|
||||||
}
|
if (!options.length) {
|
||||||
const activeIndex = options.indexOf(document.activeElement)
|
return
|
||||||
const nextIndex = (activeIndex + direction + options.length) % options.length
|
}
|
||||||
options[nextIndex].focus()
|
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) {
|
client function handleKeydown(sourceEvent) {
|
||||||
|
if (disabled) { return }
|
||||||
if (sourceEvent.key === "Escape") {
|
if (sourceEvent.key === "Escape") {
|
||||||
|
if (!isOpen()) { return }
|
||||||
sourceEvent.preventDefault()
|
sourceEvent.preventDefault()
|
||||||
|
sourceEvent.stopPropagation()
|
||||||
hideMenu("escape", sourceEvent)
|
hideMenu("escape", sourceEvent)
|
||||||
} else if (sourceEvent.key === "ArrowDown") {
|
} else if (sourceEvent.key === "ArrowDown") {
|
||||||
sourceEvent.preventDefault()
|
sourceEvent.preventDefault()
|
||||||
@@ -111,19 +123,26 @@ items: unknown[] = []
|
|||||||
} else if (sourceEvent.key === "Home" && isOpen()) {
|
} else if (sourceEvent.key === "Home" && isOpen()) {
|
||||||
sourceEvent.preventDefault()
|
sourceEvent.preventDefault()
|
||||||
const root = sourceEvent.currentTarget.closest(".wrn-dropdown") || sourceEvent.currentTarget
|
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) {
|
if (first) {
|
||||||
first.focus()
|
first.focus()
|
||||||
}
|
}
|
||||||
} else if (sourceEvent.key === "End" && isOpen()) {
|
} else if (sourceEvent.key === "End" && isOpen()) {
|
||||||
sourceEvent.preventDefault()
|
sourceEvent.preventDefault()
|
||||||
const root = sourceEvent.currentTarget.closest(".wrn-dropdown") || sourceEvent.currentTarget
|
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) {
|
if (options.length) {
|
||||||
options[options.length - 1].focus()
|
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 {
|
view {
|
||||||
@@ -138,12 +157,14 @@ items: unknown[] = []
|
|||||||
data-variant='{variant}'
|
data-variant='{variant}'
|
||||||
class='wrn-dropdown {class}'
|
class='wrn-dropdown {class}'
|
||||||
@keydown='handleKeydown(event)'
|
@keydown='handleKeydown(event)'
|
||||||
|
@focusout='handleFocusout(event)'
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="wrn-dropdown__trigger"
|
class="wrn-dropdown__trigger"
|
||||||
disabled='{disabled}'
|
disabled='{disabled}'
|
||||||
aria-haspopup="menu"
|
aria-haspopup="menu"
|
||||||
|
aria-label='{ariaLabel || label}'
|
||||||
aria-expanded='{open || visible ? "true" : "false"}'
|
aria-expanded='{open || visible ? "true" : "false"}'
|
||||||
@click='toggleMenu(event)'
|
@click='toggleMenu(event)'
|
||||||
>
|
>
|
||||||
@@ -194,6 +215,7 @@ items: unknown[] = []
|
|||||||
data-danger='{item.danger ? "true" : "false"}'
|
data-danger='{item.danger ? "true" : "false"}'
|
||||||
data-selected='{item.selected || item.checked ? "true" : "false"}'
|
data-selected='{item.selected || item.checked ? "true" : "false"}'
|
||||||
aria-disabled='{item.disabled ? "true" : "false"}'
|
aria-disabled='{item.disabled ? "true" : "false"}'
|
||||||
|
tabindex='{item.disabled ? "-1" : "0"}'
|
||||||
class="wrn-dropdown__item"
|
class="wrn-dropdown__item"
|
||||||
@click='chooseItem(item, itemIndex, event)'
|
@click='chooseItem(item, itemIndex, event)'
|
||||||
>
|
>
|
||||||
@@ -460,7 +482,7 @@ items: unknown[] = []
|
|||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 0.72rem;
|
border-radius: 0.72rem;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
text-align: left;
|
text-align: start;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 150ms ease, color 150ms ease, transform 150ms ease;
|
transition: background-color 150ms ease, color 150ms ease, transform 150ms ease;
|
||||||
@@ -550,7 +572,7 @@ items: unknown[] = []
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 639px) {
|
@media (max-width: 639px) {
|
||||||
.wrn-dropdown__panel {
|
.wrn-dropdown[data-placement] .wrn-dropdown__panel {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 0.75rem;
|
right: 0.75rem;
|
||||||
bottom: 0.75rem;
|
bottom: 0.75rem;
|
||||||
|
|||||||
@@ -475,6 +475,7 @@ component Footer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.wrn-footer__copyright {
|
.wrn-footer__copyright {
|
||||||
|
grid-column: 2;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -502,7 +503,12 @@ component Footer {
|
|||||||
text-underline-offset: 0.2em;
|
text-underline-offset: 0.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wrn-footer__copyright-left {
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.wrn-footer__copyright-right {
|
.wrn-footer__copyright-right {
|
||||||
|
grid-column: 3;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
@@ -545,10 +551,12 @@ component Footer {
|
|||||||
}
|
}
|
||||||
.wrn-footer__copyright-left,
|
.wrn-footer__copyright-left,
|
||||||
.wrn-footer__copyright-right {
|
.wrn-footer__copyright-right {
|
||||||
|
grid-column: 1;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.wrn-footer__copyright {
|
.wrn-footer__copyright {
|
||||||
|
grid-column: 1;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ component Sidebar {
|
|||||||
active: string = ""
|
active: string = ""
|
||||||
mobileLabel: string = "Open navigation"
|
mobileLabel: string = "Open navigation"
|
||||||
drawerTitle: string = "Navigation"
|
drawerTitle: string = "Navigation"
|
||||||
|
mobile: boolean = true
|
||||||
|
drawerPlacement: string = "left"
|
||||||
class: string = ""
|
class: string = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,14 +62,16 @@ component Sidebar {
|
|||||||
return Boolean(entry.value) && entry.value === active
|
return Boolean(entry.value) && entry.value === active
|
||||||
}
|
}
|
||||||
|
|
||||||
client function choose(entry, level) {
|
client function choose(entry, level, sourceEvent) {
|
||||||
if (entry.disabled) {
|
if (entry.disabled) {
|
||||||
|
sourceEvent.preventDefault()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
output.select({ item: entry, value: entry.value || "", level: level })
|
output.select({ item: entry, value: entry.value || "", level: level })
|
||||||
if (drawerOpen) {
|
if (drawerOpen) {
|
||||||
drawerOpen = false
|
drawerOpen = false
|
||||||
output.close({ source: "select" })
|
output.close({ source: "select" })
|
||||||
|
output.toggle({ open: false })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,6 +95,7 @@ component Sidebar {
|
|||||||
class='wrn-sidebar {class}'
|
class='wrn-sidebar {class}'
|
||||||
data-color='{color}'
|
data-color='{color}'
|
||||||
data-size='{size}'
|
data-size='{size}'
|
||||||
|
data-mobile='{mobile ? "true" : "false"}'
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -117,7 +122,8 @@ component Sidebar {
|
|||||||
data-active='{isActive(entry)}'
|
data-active='{isActive(entry)}'
|
||||||
aria-current='{isActive(entry) ? "page" : "false"}'
|
aria-current='{isActive(entry) ? "page" : "false"}'
|
||||||
aria-disabled='{entry.disabled ? "true" : "false"}'
|
aria-disabled='{entry.disabled ? "true" : "false"}'
|
||||||
@click='choose(entry, 1)'
|
tabindex='{entry.disabled ? "-1" : "0"}'
|
||||||
|
@click='choose(entry, 1, event)'
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class='wrn-sidebar__icon {entry.icon}'
|
class='wrn-sidebar__icon {entry.icon}'
|
||||||
@@ -137,7 +143,9 @@ component Sidebar {
|
|||||||
data-active='{isActive(child)}'
|
data-active='{isActive(child)}'
|
||||||
aria-current='{isActive(child) ? "page" : "false"}'
|
aria-current='{isActive(child) ? "page" : "false"}'
|
||||||
aria-disabled='{child.disabled ? "true" : "false"}'
|
aria-disabled='{child.disabled ? "true" : "false"}'
|
||||||
@click='choose(child, 2)'
|
data-wrn-roving-item="true"
|
||||||
|
tabindex='{child.disabled ? "-1" : "0"}'
|
||||||
|
@click='choose(child, 2, event)'
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class='wrn-sidebar__icon {child.icon}'
|
class='wrn-sidebar__icon {child.icon}'
|
||||||
@@ -159,7 +167,10 @@ component Sidebar {
|
|||||||
href='{leaf.href || "#"}'
|
href='{leaf.href || "#"}'
|
||||||
data-active='{isActive(leaf)}'
|
data-active='{isActive(leaf)}'
|
||||||
aria-current='{isActive(leaf) ? "page" : "false"}'
|
aria-current='{isActive(leaf) ? "page" : "false"}'
|
||||||
@click='choose(leaf, 3)'
|
aria-disabled='{leaf.disabled ? "true" : "false"}'
|
||||||
|
data-wrn-roving-item="true"
|
||||||
|
tabindex='{leaf.disabled ? "-1" : "0"}'
|
||||||
|
@click='choose(leaf, 3, event)'
|
||||||
>
|
>
|
||||||
<span class="wrn-sidebar__label">{leaf.label}</span>
|
<span class="wrn-sidebar__label">{leaf.label}</span>
|
||||||
</a>
|
</a>
|
||||||
@@ -175,15 +186,94 @@ component Sidebar {
|
|||||||
<slot />
|
<slot />
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
{#if mobile}
|
||||||
<Drawer
|
<Drawer
|
||||||
open={drawerOpen}
|
open={drawerOpen}
|
||||||
placement="left"
|
placement={drawerPlacement}
|
||||||
title={drawerTitle}
|
title={drawerTitle}
|
||||||
label={label}
|
label={label}
|
||||||
@close="closeDrawer()"
|
@close="closeDrawer()"
|
||||||
>
|
>
|
||||||
|
<nav class="wrn-sidebar__drawer-nav" aria-label='{label}'>
|
||||||
|
<ul class="wrn-sidebar__list" data-wrn-roving="vertical">
|
||||||
|
{#each entryList() as entry}
|
||||||
|
<li class="wrn-sidebar__entry" data-group='{isGroup(entry)}'>
|
||||||
|
<p class="wrn-sidebar__heading" data-show="entry.heading">{entry.heading}</p>
|
||||||
|
|
||||||
|
<a
|
||||||
|
class="wrn-sidebar__link"
|
||||||
|
data-show="!entry.heading"
|
||||||
|
href='{entry.href || "#"}'
|
||||||
|
data-wrn-roving-item="true"
|
||||||
|
data-active='{isActive(entry)}'
|
||||||
|
aria-current='{isActive(entry) ? "page" : "false"}'
|
||||||
|
aria-disabled='{entry.disabled ? "true" : "false"}'
|
||||||
|
tabindex='{entry.disabled ? "-1" : "0"}'
|
||||||
|
@click='choose(entry, 1, event)'
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class='wrn-sidebar__icon {entry.icon}'
|
||||||
|
data-show="entry.icon"
|
||||||
|
aria-hidden="true"
|
||||||
|
></span>
|
||||||
|
<span class="wrn-sidebar__label">{entry.label}</span>
|
||||||
|
<span class="wrn-sidebar__badge" data-show="entry.badge">{entry.badge}</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<ul class="wrn-sidebar__sublist" data-show="childrenOf(entry).length > 0">
|
||||||
|
{#each childrenOf(entry) as child}
|
||||||
|
<li class="wrn-sidebar__entry">
|
||||||
|
<a
|
||||||
|
class="wrn-sidebar__link"
|
||||||
|
href='{child.href || "#"}'
|
||||||
|
data-active='{isActive(child)}'
|
||||||
|
aria-current='{isActive(child) ? "page" : "false"}'
|
||||||
|
aria-disabled='{child.disabled ? "true" : "false"}'
|
||||||
|
data-wrn-roving-item="true"
|
||||||
|
tabindex='{child.disabled ? "-1" : "0"}'
|
||||||
|
@click='choose(child, 2, event)'
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class='wrn-sidebar__icon {child.icon}'
|
||||||
|
data-show="child.icon"
|
||||||
|
aria-hidden="true"
|
||||||
|
></span>
|
||||||
|
<span class="wrn-sidebar__label">{child.label}</span>
|
||||||
|
<span class="wrn-sidebar__badge" data-show="child.badge">{child.badge}</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<ul
|
||||||
|
class="wrn-sidebar__sublist wrn-sidebar__sublist--level3"
|
||||||
|
data-show="childrenOf(child).length > 0"
|
||||||
|
>
|
||||||
|
{#each childrenOf(child) as leaf}
|
||||||
|
<li class="wrn-sidebar__entry">
|
||||||
|
<a
|
||||||
|
class="wrn-sidebar__link"
|
||||||
|
href='{leaf.href || "#"}'
|
||||||
|
data-active='{isActive(leaf)}'
|
||||||
|
aria-current='{isActive(leaf) ? "page" : "false"}'
|
||||||
|
aria-disabled='{leaf.disabled ? "true" : "false"}'
|
||||||
|
data-wrn-roving-item="true"
|
||||||
|
tabindex='{leaf.disabled ? "-1" : "0"}'
|
||||||
|
@click='choose(leaf, 3, event)'
|
||||||
|
>
|
||||||
|
<span class="wrn-sidebar__label">{leaf.label}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</nav>
|
||||||
<slot name="drawer"></slot>
|
<slot name="drawer"></slot>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,9 +321,21 @@ component Sidebar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.wrn-sidebar__sublist {
|
.wrn-sidebar__sublist {
|
||||||
margin-left: 0.85rem;
|
margin-inline-start: 0.85rem;
|
||||||
padding-left: 0.5rem;
|
padding-inline-start: 0.5rem;
|
||||||
border-left: 1px solid var(--wrn-color-border);
|
border-inline-start: 1px solid var(--wrn-color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrn-sidebar__entry[data-group="true"] > .wrn-sidebar__sublist {
|
||||||
|
margin-inline-start: 0;
|
||||||
|
padding-inline-start: 0;
|
||||||
|
border-inline-start: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrn-sidebar__icon {
|
||||||
|
width: 1.05rem;
|
||||||
|
height: 1.05rem;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrn-sidebar__entry[data-group="true"] + .wrn-sidebar__entry {
|
.wrn-sidebar__entry[data-group="true"] + .wrn-sidebar__entry {
|
||||||
@@ -286,7 +388,7 @@ component Sidebar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.wrn-sidebar__badge {
|
.wrn-sidebar__badge {
|
||||||
margin-left: auto;
|
margin-inline-start: auto;
|
||||||
padding: 0.05rem 0.4rem;
|
padding: 0.05rem 0.4rem;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: color-mix(in srgb, var(--sidebar-accent) 16%, transparent);
|
background: color-mix(in srgb, var(--sidebar-accent) 16%, transparent);
|
||||||
@@ -323,11 +425,11 @@ component Sidebar {
|
|||||||
* the focus trap and the scroll lock.
|
* the focus trap and the scroll lock.
|
||||||
*/
|
*/
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
.wrn-sidebar__launcher {
|
.wrn-sidebar[data-mobile="true"] > .wrn-sidebar__launcher {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrn-sidebar__rail {
|
.wrn-sidebar[data-mobile="true"] > .wrn-sidebar__rail {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/ui",
|
"name": "@wrnexus/ui",
|
||||||
"version": "0.8.56",
|
"version": "0.8.57",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { renderComponent, mountHtml } from "../../test/src/index.ts";
|
||||||
|
import { uiComponentPath } from "../src/registry.ts";
|
||||||
|
import { parse } from "../../compiler/src/index.ts";
|
||||||
|
|
||||||
|
const source = (name: string) => readFileSync(uiComponentPath(name), "utf8");
|
||||||
|
|
||||||
|
describe("account shell UI regressions", () => {
|
||||||
|
test("Sidebar supplies navigation to its mobile Drawer without an app slot", async () => {
|
||||||
|
const dom = mountHtml(
|
||||||
|
await renderComponent(source("Sidebar"), {
|
||||||
|
items: [
|
||||||
|
{ heading: "Account", items: [{ label: "Profile", href: "/profile", value: "profile" }] },
|
||||||
|
],
|
||||||
|
active: "profile",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const mobileNav = dom.querySelector(".wrn-sidebar__drawer-nav");
|
||||||
|
expect(mobileNav?.querySelector('a[href="/profile"]')?.getAttribute("aria-current")).toBe(
|
||||||
|
"page",
|
||||||
|
);
|
||||||
|
expect(dom.querySelector('.wrn-sidebar__rail a[href="/profile"]')).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Sidebar embedded in another drawer does not generate a second mobile drawer", async () => {
|
||||||
|
const dom = mountHtml(
|
||||||
|
await renderComponent(source("Sidebar"), {
|
||||||
|
mobile: false,
|
||||||
|
items: [{ label: "Profile", href: "/profile" }],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(dom.querySelector('[data-component="Drawer"]')).toBeNull();
|
||||||
|
expect(dom.querySelector(".wrn-sidebar")?.getAttribute("data-mobile")).toBe("false");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Sidebar blocks disabled links at every nesting level", async () => {
|
||||||
|
const dom = mountHtml(
|
||||||
|
await renderComponent(source("Sidebar"), {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: "Parent",
|
||||||
|
disabled: true,
|
||||||
|
href: "/parent",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: "Child",
|
||||||
|
disabled: true,
|
||||||
|
href: "/child",
|
||||||
|
items: [{ label: "Leaf", disabled: true, href: "/leaf" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
for (const href of ["/parent", "/child", "/leaf"]) {
|
||||||
|
const link = dom.querySelector(`.wrn-sidebar__rail a[href="${href}"]`)!;
|
||||||
|
expect(link.getAttribute("aria-disabled")).toBe("true");
|
||||||
|
expect(link.getAttribute("tabindex")).toBe("-1");
|
||||||
|
const click = new (dom.window as { MouseEvent: typeof MouseEvent }).MouseEvent("click", {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
});
|
||||||
|
link.dispatchEvent(click);
|
||||||
|
expect(click.defaultPrevented).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Dropdown keyboard navigation skips disabled links and ArrowUp starts at the last item", async () => {
|
||||||
|
const dom = mountHtml(
|
||||||
|
await renderComponent(source("Dropdown"), {
|
||||||
|
label: "Account",
|
||||||
|
items: [
|
||||||
|
{ label: "Profile", href: "/profile" },
|
||||||
|
{ label: "Disabled", href: "/disabled", disabled: true },
|
||||||
|
{ label: "Security", href: "/security" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const trigger = dom.querySelector(".wrn-dropdown__trigger") as HTMLButtonElement;
|
||||||
|
// Execute the component's real JS functions: the lightweight mount runtime
|
||||||
|
// intentionally does not interpret nested timer callbacks.
|
||||||
|
const functions = parse(source("Dropdown"))
|
||||||
|
.functions.join("\n")
|
||||||
|
.replace(/\b(client|shared) function/g, "function");
|
||||||
|
const handle = new Function(
|
||||||
|
"document",
|
||||||
|
"window",
|
||||||
|
"output",
|
||||||
|
`
|
||||||
|
let open = false, visible = false, disabled = false;
|
||||||
|
${functions}
|
||||||
|
return handleKeydown;
|
||||||
|
`,
|
||||||
|
)(dom.document, dom.window, {
|
||||||
|
open() {},
|
||||||
|
close() {},
|
||||||
|
toggle({ open }: { open: boolean }) {
|
||||||
|
trigger.setAttribute("aria-expanded", String(open));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const key = (_target: Element, value: string) =>
|
||||||
|
handle({
|
||||||
|
key: value,
|
||||||
|
currentTarget: dom.querySelector(".wrn-dropdown"),
|
||||||
|
preventDefault() {},
|
||||||
|
stopPropagation() {},
|
||||||
|
});
|
||||||
|
trigger.focus();
|
||||||
|
key(trigger, "ArrowUp");
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||||
|
expect(dom.document.activeElement?.textContent).toContain("Security");
|
||||||
|
key(dom.document.activeElement!, "ArrowUp");
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||||
|
expect(dom.document.activeElement?.textContent).toContain("Profile");
|
||||||
|
key(dom.document.activeElement!, "Escape");
|
||||||
|
expect(dom.document.activeElement).toBe(trigger);
|
||||||
|
expect(trigger.getAttribute("aria-expanded")).toBe("false");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Dropdown preserves falsy action values and has an explicit accessible trigger name", async () => {
|
||||||
|
const dom = mountHtml(
|
||||||
|
await renderComponent(source("Dropdown"), {
|
||||||
|
label: "",
|
||||||
|
ariaLabel: "Account menu",
|
||||||
|
items: [{ label: "Zero", value: 0, action: true }],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
let value: unknown = undefined;
|
||||||
|
dom.querySelector(".wrn-dropdown")!.addEventListener("action", (event) => {
|
||||||
|
value = (event as CustomEvent).detail.value;
|
||||||
|
});
|
||||||
|
const trigger = dom.querySelector(".wrn-dropdown__trigger") as HTMLButtonElement;
|
||||||
|
expect(trigger.getAttribute("aria-label")).toBe("Account menu");
|
||||||
|
trigger.click();
|
||||||
|
(dom.querySelector("[data-dropdown-item]") as HTMLButtonElement).click();
|
||||||
|
expect(value).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Dropdown closes when keyboard focus leaves the component", async () => {
|
||||||
|
const dom = mountHtml(
|
||||||
|
await renderComponent(source("Dropdown"), {
|
||||||
|
label: "Menu",
|
||||||
|
items: [{ label: "Profile", href: "/profile" }],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const trigger = dom.querySelector(".wrn-dropdown__trigger") as HTMLButtonElement;
|
||||||
|
trigger.click();
|
||||||
|
const outside = dom.document.createElement("button");
|
||||||
|
dom.document.body.append(outside);
|
||||||
|
trigger.dispatchEvent(
|
||||||
|
new (dom.window as { FocusEvent: typeof FocusEvent }).FocusEvent("focusout", {
|
||||||
|
bubbles: true,
|
||||||
|
relatedTarget: outside,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(trigger.getAttribute("aria-expanded")).toBe("false");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("layout fixes belong to components, not consuming applications", () => {
|
||||||
|
expect(source("Sidebar")).toMatch(
|
||||||
|
/\.wrn-sidebar__icon\s*\{[^}]*width: 1.05rem;[^}]*height: 1.05rem;/s,
|
||||||
|
);
|
||||||
|
expect(source("Sidebar")).toContain("margin-inline-start");
|
||||||
|
expect(source("Dropdown")).toContain(".wrn-dropdown[data-placement] .wrn-dropdown__panel");
|
||||||
|
expect(source("Footer")).toMatch(/\.wrn-footer__copyright-left\s*\{\s*grid-column: 1;/);
|
||||||
|
expect(source("Footer")).toMatch(/\.wrn-footer__copyright-right\s*\{\s*grid-column: 3;/);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user