@bgub/fig-headless 0.0.0-stage → 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +21 -0
  3. package/README.md +595 -2
  4. package/dist/accordion.d.ts +46 -0
  5. package/dist/accordion.js +2 -0
  6. package/dist/accordion.js.map +1 -0
  7. package/dist/changes--uvVfvy8.d.ts +11 -0
  8. package/dist/changes-DglRuvQa.js +2 -0
  9. package/dist/changes-DglRuvQa.js.map +1 -0
  10. package/dist/checkbox.d.ts +24 -0
  11. package/dist/checkbox.js +2 -0
  12. package/dist/checkbox.js.map +1 -0
  13. package/dist/combobox.d.ts +53 -0
  14. package/dist/combobox.js +2 -0
  15. package/dist/combobox.js.map +1 -0
  16. package/dist/command.d.ts +42 -0
  17. package/dist/command.js +2 -0
  18. package/dist/command.js.map +1 -0
  19. package/dist/composite-CG4JghHi.d.ts +4 -0
  20. package/dist/composite-CiKLX-Ti.js +2 -0
  21. package/dist/composite-CiKLX-Ti.js.map +1 -0
  22. package/dist/controllable-value-CDbRzlId.js +2 -0
  23. package/dist/controllable-value-CDbRzlId.js.map +1 -0
  24. package/dist/diagnostics-CoURcwqb.js +2 -0
  25. package/dist/diagnostics-CoURcwqb.js.map +1 -0
  26. package/dist/dialog.d.ts +44 -0
  27. package/dist/dialog.js +2 -0
  28. package/dist/dialog.js.map +1 -0
  29. package/dist/field.d.ts +35 -0
  30. package/dist/field.js +2 -0
  31. package/dist/field.js.map +1 -0
  32. package/dist/form-reset-B7G3-0OT.js +2 -0
  33. package/dist/form-reset-B7G3-0OT.js.map +1 -0
  34. package/dist/hover-HF7JXkR2.js +2 -0
  35. package/dist/hover-HF7JXkR2.js.map +1 -0
  36. package/dist/ids-Cvu36mFL.js +2 -0
  37. package/dist/ids-Cvu36mFL.js.map +1 -0
  38. package/dist/listbox-DcD6dHSg.js +2 -0
  39. package/dist/listbox-DcD6dHSg.js.map +1 -0
  40. package/dist/listbox.d.ts +32 -0
  41. package/dist/listbox.js +2 -0
  42. package/dist/listbox.js.map +1 -0
  43. package/dist/menu/context-menu.d.ts +15 -0
  44. package/dist/menu/context-menu.js +2 -0
  45. package/dist/menu/context-menu.js.map +1 -0
  46. package/dist/menu/submenu.d.ts +20 -0
  47. package/dist/menu/submenu.js +2 -0
  48. package/dist/menu/submenu.js.map +1 -0
  49. package/dist/menu-B4pKuF3z.js +2 -0
  50. package/dist/menu-B4pKuF3z.js.map +1 -0
  51. package/dist/menu.d.ts +52 -0
  52. package/dist/menu.js +1 -0
  53. package/dist/open-state-BmP__rp4.d.ts +6 -0
  54. package/dist/open-state-G79p9WiE.js +2 -0
  55. package/dist/open-state-G79p9WiE.js.map +1 -0
  56. package/dist/part-reference-B7zINPUK.js +2 -0
  57. package/dist/part-reference-B7zINPUK.js.map +1 -0
  58. package/dist/parts-I0jdefyp.js +2 -0
  59. package/dist/parts-I0jdefyp.js.map +1 -0
  60. package/dist/popover-CSPJNmHy.d.ts +37 -0
  61. package/dist/popover-UfJHN1h2.js +2 -0
  62. package/dist/popover-UfJHN1h2.js.map +1 -0
  63. package/dist/popover.d.ts +2 -0
  64. package/dist/popover.js +1 -0
  65. package/dist/popup/position.d.ts +26 -0
  66. package/dist/popup/position.js +2 -0
  67. package/dist/popup/position.js.map +1 -0
  68. package/dist/popup-state-CFSmDEwK.js +2 -0
  69. package/dist/popup-state-CFSmDEwK.js.map +1 -0
  70. package/dist/radio-group.d.ts +52 -0
  71. package/dist/radio-group.js +2 -0
  72. package/dist/radio-group.js.map +1 -0
  73. package/dist/relations-uMHtgW03.js +2 -0
  74. package/dist/relations-uMHtgW03.js.map +1 -0
  75. package/dist/select.d.ts +45 -0
  76. package/dist/select.js +2 -0
  77. package/dist/select.js.map +1 -0
  78. package/dist/slider.d.ts +35 -0
  79. package/dist/slider.js +2 -0
  80. package/dist/slider.js.map +1 -0
  81. package/dist/switch.d.ts +24 -0
  82. package/dist/switch.js +2 -0
  83. package/dist/switch.js.map +1 -0
  84. package/dist/tabs/indicator.d.ts +20 -0
  85. package/dist/tabs/indicator.js +2 -0
  86. package/dist/tabs/indicator.js.map +1 -0
  87. package/dist/tabs.d.ts +49 -0
  88. package/dist/tabs.js +2 -0
  89. package/dist/tabs.js.map +1 -0
  90. package/dist/toast.d.ts +34 -0
  91. package/dist/toast.js +2 -0
  92. package/dist/toast.js.map +1 -0
  93. package/dist/toggle-B0hOxshF.js +2 -0
  94. package/dist/toggle-B0hOxshF.js.map +1 -0
  95. package/dist/toggle-q6B-U4Nq.d.ts +29 -0
  96. package/dist/toolbar.d.ts +25 -0
  97. package/dist/toolbar.js +2 -0
  98. package/dist/toolbar.js.map +1 -0
  99. package/dist/tooltip.d.ts +31 -0
  100. package/dist/tooltip.js +2 -0
  101. package/dist/tooltip.js.map +1 -0
  102. package/dist-development/accordion.js +2 -0
  103. package/dist-development/accordion.js.map +1 -0
  104. package/dist-development/changes-DglRuvQa.js +2 -0
  105. package/dist-development/changes-DglRuvQa.js.map +1 -0
  106. package/dist-development/checkbox.js +2 -0
  107. package/dist-development/checkbox.js.map +1 -0
  108. package/dist-development/combobox.js +2 -0
  109. package/dist-development/combobox.js.map +1 -0
  110. package/dist-development/command.js +2 -0
  111. package/dist-development/command.js.map +1 -0
  112. package/dist-development/composite-DoFLgUjR.js +2 -0
  113. package/dist-development/composite-DoFLgUjR.js.map +1 -0
  114. package/dist-development/controllable-value-CDbRzlId.js +2 -0
  115. package/dist-development/controllable-value-CDbRzlId.js.map +1 -0
  116. package/dist-development/diagnostics-BkQV0MaT.js +2 -0
  117. package/dist-development/diagnostics-BkQV0MaT.js.map +1 -0
  118. package/dist-development/dialog.js +2 -0
  119. package/dist-development/dialog.js.map +1 -0
  120. package/dist-development/field.js +2 -0
  121. package/dist-development/field.js.map +1 -0
  122. package/dist-development/form-reset-B7G3-0OT.js +2 -0
  123. package/dist-development/form-reset-B7G3-0OT.js.map +1 -0
  124. package/dist-development/hover-HF7JXkR2.js +2 -0
  125. package/dist-development/hover-HF7JXkR2.js.map +1 -0
  126. package/dist-development/ids-Cvu36mFL.js +2 -0
  127. package/dist-development/ids-Cvu36mFL.js.map +1 -0
  128. package/dist-development/listbox-B-oe_0c0.js +2 -0
  129. package/dist-development/listbox-B-oe_0c0.js.map +1 -0
  130. package/dist-development/listbox.js +2 -0
  131. package/dist-development/listbox.js.map +1 -0
  132. package/dist-development/menu/context-menu.js +2 -0
  133. package/dist-development/menu/context-menu.js.map +1 -0
  134. package/dist-development/menu/submenu.js +2 -0
  135. package/dist-development/menu/submenu.js.map +1 -0
  136. package/dist-development/menu-DBxBFdYm.js +2 -0
  137. package/dist-development/menu-DBxBFdYm.js.map +1 -0
  138. package/dist-development/menu.js +1 -0
  139. package/dist-development/open-state-G79p9WiE.js +2 -0
  140. package/dist-development/open-state-G79p9WiE.js.map +1 -0
  141. package/dist-development/part-reference-B7zINPUK.js +2 -0
  142. package/dist-development/part-reference-B7zINPUK.js.map +1 -0
  143. package/dist-development/parts-I0jdefyp.js +2 -0
  144. package/dist-development/parts-I0jdefyp.js.map +1 -0
  145. package/dist-development/popover-DXxGM3tO.js +2 -0
  146. package/dist-development/popover-DXxGM3tO.js.map +1 -0
  147. package/dist-development/popover.js +1 -0
  148. package/dist-development/popup/position.js +2 -0
  149. package/dist-development/popup/position.js.map +1 -0
  150. package/dist-development/popup-state-BUYfC2nG.js +2 -0
  151. package/dist-development/popup-state-BUYfC2nG.js.map +1 -0
  152. package/dist-development/radio-group.js +2 -0
  153. package/dist-development/radio-group.js.map +1 -0
  154. package/dist-development/relations-CZpjLINW.js +2 -0
  155. package/dist-development/relations-CZpjLINW.js.map +1 -0
  156. package/dist-development/select.js +2 -0
  157. package/dist-development/select.js.map +1 -0
  158. package/dist-development/slider.js +2 -0
  159. package/dist-development/slider.js.map +1 -0
  160. package/dist-development/switch.js +2 -0
  161. package/dist-development/switch.js.map +1 -0
  162. package/dist-development/tabs/indicator.js +2 -0
  163. package/dist-development/tabs/indicator.js.map +1 -0
  164. package/dist-development/tabs.js +2 -0
  165. package/dist-development/tabs.js.map +1 -0
  166. package/dist-development/toast.js +2 -0
  167. package/dist-development/toast.js.map +1 -0
  168. package/dist-development/toggle-CIbzprg5.js +2 -0
  169. package/dist-development/toggle-CIbzprg5.js.map +1 -0
  170. package/dist-development/toolbar.js +2 -0
  171. package/dist-development/toolbar.js.map +1 -0
  172. package/dist-development/tooltip.js +2 -0
  173. package/dist-development/tooltip.js.map +1 -0
  174. package/package.json +177 -3
@@ -0,0 +1 @@
1
+ {"version":3,"file":"listbox-DcD6dHSg.js","names":[],"sources":["../src/internal/listbox.ts"],"sourcesContent":["import { createPartCollection } from \"./registration.ts\";\nimport { createComposite, sameValue } from \"./composite.ts\";\n\nexport interface ListboxOptionConfig {\n readonly disabled: boolean;\n readonly textValue: string | undefined;\n readonly value: unknown;\n}\n\nexport interface ListboxOption extends ListboxOptionConfig {\n readonly node: HTMLElement;\n}\n\n/** Ordered option registration and active-descendant movement. */\nexport function createListbox(name: string, registrationChanged: () => void) {\n const composite = createComposite({\n container: '[role=\"listbox\"]',\n item: '[role=\"option\"]',\n name,\n registrationChanged,\n });\n const registrations = createPartCollection<ListboxOption>();\n let search = \"\";\n let searchTimer: ReturnType<typeof setTimeout> | undefined;\n\n function bindContainer(node: HTMLElement, signal: AbortSignal): void {\n if (!composite.bindContainer(node, signal)) return;\n signal.addEventListener(\n \"abort\",\n () => {\n if (searchTimer !== undefined) clearTimeout(searchTimer);\n searchTimer = undefined;\n search = \"\";\n },\n { once: true },\n );\n }\n\n function bindOption(\n node: HTMLElement,\n signal: AbortSignal,\n config: ListboxOptionConfig,\n ): void {\n const option = { ...config, node };\n registrations.bind(node, signal, option);\n composite.bindItem(node, signal, config.value, config.disabled);\n }\n\n function options(): readonly ListboxOption[] {\n return composite\n .items()\n .flatMap((item) => registrations.get(item.node) ?? []);\n }\n\n function option(value: unknown): ListboxOption | undefined {\n return options().find((entry) => sameValue(entry.value, value));\n }\n\n function optionAt(target: EventTarget | null): ListboxOption | undefined {\n const item = composite.itemAt(target);\n return item === undefined ? undefined : registrations.get(item.node);\n }\n\n function move(current: unknown, key: string): ListboxOption | undefined {\n const enabled = options().filter((entry) => !entry.disabled);\n if (enabled.length === 0) return undefined;\n if (key === \"Home\") return enabled[0];\n if (key === \"End\") return enabled.at(-1);\n if (key !== \"ArrowDown\" && key !== \"ArrowUp\") return undefined;\n const currentIndex = enabled.findIndex((entry) =>\n sameValue(entry.value, current),\n );\n if (currentIndex === -1) {\n return key === \"ArrowDown\" ? enabled[0] : enabled.at(-1);\n }\n const delta = key === \"ArrowDown\" ? 1 : -1;\n return enabled[(currentIndex + delta + enabled.length) % enabled.length];\n }\n\n function typeahead(current: unknown, key: string): ListboxOption | undefined {\n if (key.length !== 1 || key === \" \") return undefined;\n if (searchTimer !== undefined) clearTimeout(searchTimer);\n searchTimer = setTimeout(() => {\n search = \"\";\n }, 500);\n const normalized = key.toLocaleLowerCase();\n const repeated = search.length === 1 && search === normalized;\n search = repeated ? search : search + normalized;\n const enabled = options().filter((entry) => !entry.disabled);\n const currentIndex = enabled.findIndex((entry) =>\n sameValue(entry.value, current),\n );\n const start = currentIndex === -1 ? 0 : currentIndex + (repeated ? 1 : 0);\n for (let offset = 0; offset < enabled.length; offset += 1) {\n const candidate = enabled[(start + offset) % enabled.length];\n const text =\n candidate?.textValue ?? candidate?.node.textContent?.trim() ?? \"\";\n if (\n candidate !== undefined &&\n text.toLocaleLowerCase().startsWith(search)\n ) {\n return candidate;\n }\n }\n return undefined;\n }\n\n return {\n ...composite,\n bindContainer,\n bindOption,\n move,\n option,\n optionAt,\n options,\n typeahead,\n };\n}\n"],"mappings":"2FAcA,SAAgB,EAAc,EAAc,EAAiC,CAC3E,IAAM,EAAY,EAAgB,CAChC,UAAW,mBACX,KAAM,kBACN,OACA,qBACF,CAAC,EACK,EAAgB,EAAoC,EACtD,EAAS,GACT,EAEJ,SAAS,EAAc,EAAmB,EAA2B,CAC9D,EAAU,cAAc,EAAM,CAAM,GACzC,EAAO,iBACL,YACM,CACA,IAAgB,IAAA,IAAW,aAAa,CAAW,EACvD,EAAc,IAAA,GACd,EAAS,EACX,EACA,CAAE,KAAM,EAAK,CACf,CACF,CAEA,SAAS,EACP,EACA,EACA,EACM,CACN,IAAM,EAAS,CAAE,GAAG,EAAQ,MAAK,EACjC,EAAc,KAAK,EAAM,EAAQ,CAAM,EACvC,EAAU,SAAS,EAAM,EAAQ,EAAO,MAAO,EAAO,QAAQ,CAChE,CAEA,SAAS,GAAoC,CAC3C,OAAO,EACJ,MAAM,CAAC,CACP,QAAS,GAAS,EAAc,IAAI,EAAK,IAAI,GAAK,CAAC,CAAC,CACzD,CAEA,SAAS,EAAO,EAA2C,CACzD,OAAO,EAAQ,CAAC,CAAC,KAAM,GAAU,EAAU,EAAM,MAAO,CAAK,CAAC,CAChE,CAEA,SAAS,EAAS,EAAuD,CACvE,IAAM,EAAO,EAAU,OAAO,CAAM,EACpC,OAAO,IAAS,IAAA,GAAY,IAAA,GAAY,EAAc,IAAI,EAAK,IAAI,CACrE,CAEA,SAAS,EAAK,EAAkB,EAAwC,CACtE,IAAM,EAAU,EAAQ,CAAC,CAAC,OAAQ,GAAU,CAAC,EAAM,QAAQ,EAC3D,GAAI,EAAQ,SAAW,EAAG,OAC1B,GAAI,IAAQ,OAAQ,OAAO,EAAQ,GACnC,GAAI,IAAQ,MAAO,OAAO,EAAQ,GAAG,EAAE,EACvC,GAAI,IAAQ,aAAe,IAAQ,UAAW,OAC9C,IAAM,EAAe,EAAQ,UAAW,GACtC,EAAU,EAAM,MAAO,CAAO,CAChC,EAKA,OAJI,IAAiB,GACZ,IAAQ,YAAc,EAAQ,GAAK,EAAQ,GAAG,EAAE,EAGlD,GAAS,GADF,IAAQ,YAAc,EAAI,IACD,EAAQ,QAAU,EAAQ,OACnE,CAEA,SAAS,EAAU,EAAkB,EAAwC,CAC3E,GAAI,EAAI,SAAW,GAAK,IAAQ,IAAK,OACjC,IAAgB,IAAA,IAAW,aAAa,CAAW,EACvD,EAAc,eAAiB,CAC7B,EAAS,EACX,EAAG,GAAG,EACN,IAAM,EAAa,EAAI,kBAAkB,EACnC,EAAW,EAAO,SAAW,GAAK,IAAW,EACnD,EAAS,EAAW,EAAS,EAAS,EACtC,IAAM,EAAU,EAAQ,CAAC,CAAC,OAAQ,GAAU,CAAC,EAAM,QAAQ,EACrD,EAAe,EAAQ,UAAW,GACtC,EAAU,EAAM,MAAO,CAAO,CAChC,EACM,EAAQ,IAAiB,GAAK,EAAI,GAAgB,KACxD,IAAK,IAAI,EAAS,EAAG,EAAS,EAAQ,OAAQ,GAAU,EAAG,CACzD,IAAM,EAAY,GAAS,EAAQ,GAAU,EAAQ,QAC/C,EACJ,GAAW,WAAa,GAAW,KAAK,aAAa,KAAK,GAAK,GACjE,GACE,IAAc,IAAA,IACd,EAAK,kBAAkB,CAAC,CAAC,WAAW,CAAM,EAE1C,OAAO,CAEX,CAEF,CAEA,MAAO,CACL,GAAG,EACH,gBACA,aACA,OACA,SACA,WACA,UACA,WACF,CACF"}
@@ -0,0 +1,32 @@
1
+ import { t as ChangeDetails } from "./changes--uvVfvy8.js";
2
+ import { FigNode, MixinDescriptor } from "@bgub/fig";
3
+ //#region src/listbox/listbox.d.ts
4
+ type ListboxValueChangeDetails = ChangeDetails;
5
+ type ListboxValueChangeHandler<Value = unknown> = (values: readonly Value[], details: ListboxValueChangeDetails, signal: AbortSignal) => void;
6
+ interface ListboxOptionOptions {
7
+ disabled?: boolean;
8
+ /** Text used for typeahead when it differs from the host's text content. */
9
+ textValue?: string;
10
+ }
11
+ interface ListboxOptions<Value = unknown> {
12
+ defaultValue?: readonly Value[];
13
+ disabled?: boolean;
14
+ multiple?: boolean;
15
+ onValueChange?: ListboxValueChangeHandler<Value>;
16
+ readOnly?: boolean;
17
+ value?: readonly Value[];
18
+ }
19
+ interface ListboxParts<Value = unknown> {
20
+ readonly values: readonly Value[];
21
+ option(value: Value, options?: ListboxOptionOptions): MixinDescriptor;
22
+ root(): MixinDescriptor;
23
+ }
24
+ interface ListboxProps<Value = unknown> extends ListboxOptions<Value> {
25
+ children: (listbox: ListboxParts<Value>) => FigNode;
26
+ }
27
+ /** Coordinates selection and active-descendant navigation for one listbox. */
28
+ declare function useListbox<Value = unknown>(options?: ListboxOptions<Value>): ListboxParts<Value>;
29
+ /** {@link useListbox} as a render-callback component. */
30
+ declare function Listbox<Value = unknown>(props: ListboxProps<Value>): FigNode;
31
+ //#endregion
32
+ export { Listbox, ListboxOptionOptions, ListboxOptions, ListboxParts, ListboxProps, ListboxValueChangeDetails, ListboxValueChangeHandler, useListbox };
@@ -0,0 +1,2 @@
1
+ import{t as e}from"./changes-DglRuvQa.js";import{a as t,n,r}from"./parts-I0jdefyp.js";import"./diagnostics-CoURcwqb.js";import{i}from"./composite-CiKLX-Ti.js";import{n as a}from"./controllable-value-CDbRzlId.js";import{t as o}from"./ids-Cvu36mFL.js";import{t as s}from"./listbox-DcD6dHSg.js";import{createMixin as c,useBeforePaint as l,useMemo as u,useStableEvent as d,useState as f}from"@bgub/fig";import{on as p}from"@bgub/fig-dom";const m=c((e,t)=>({"aria-activedescendant":t.highlighted===null?void 0:t.registry.option(t.highlighted)?.node.id??t.idFor(t.highlighted,`option`),"aria-disabled":t.disabled?`true`:void 0,"aria-multiselectable":t.multiple?`true`:void 0,"aria-readonly":t.readOnly?`true`:void 0,bind:n(e,t.registry,t.registry.bindContainer),"data-disabled":t.disabled?``:void 0,"data-readonly":t.readOnly?``:void 0,mix:[p(`click`,e=>{if(e.defaultPrevented)return;let n=t.registry.optionAt(e.target);n!==void 0&&(t.disabled||n.disabled?e.preventDefault():e.button===0&&(t.setHighlighted(n.value),t.readOnly?e.preventDefault():t.select(n,e)))}),p(`pointerdown`,e=>{t.registry.optionAt(e.target)!==void 0&&(e.preventDefault(),!t.disabled&&e.currentTarget instanceof HTMLElement&&e.currentTarget.focus())}),p(`pointermove`,e=>{if(e.pointerType===`touch`||t.disabled)return;let n=t.registry.optionAt(e.target);n!==void 0&&!n.disabled&&t.setHighlighted(n.value)}),p(`keydown`,e=>{if(e.defaultPrevented||e.target instanceof Element&&e.target.closest(`[role="listbox"]`)!==e.currentTarget||t.disabled||e.isComposing||e.altKey||e.ctrlKey||e.metaKey||e.shiftKey)return;let n=t.registry.move(t.highlighted,e.key);if(n!==void 0){e.preventDefault(),t.setHighlighted(n.value),t.multiple||t.select(n,e);return}if(e.key===`Enter`||e.key===` `){let n=t.registry.option(t.highlighted);n!==void 0&&(e.preventDefault(),t.select(n,e));return}let r=t.registry.typeahead(t.highlighted,e.key);r!==void 0&&(t.setHighlighted(r.value),t.multiple||t.select(r,e))})],role:`listbox`,tabindex:t.disabled?-1:e.props.tabindex??0})),h=c((e,t,r)=>{let a=r.disabled||e.props.disabled===!0;return{"aria-disabled":a?`true`:void 0,"aria-selected":r.selected?`true`:`false`,bind:n(e,t.registry,(e,n)=>t.registry.bindOption(e,n,{disabled:a,textValue:r.textValue,value:r.value})),"data-disabled":a?``:void 0,"data-highlighted":i(t.highlighted,r.value)?``:void 0,"data-selected":r.selected?``:void 0,id:e.props.id??t.idFor(r.value,`option`),role:`option`,type:e.type===`button`?e.props.type??`button`:void 0,tabindex:-1}});function g(n={}){let{disabled:c=!1,multiple:p=!1,readOnly:g=!1}=n,_=t(),y=a({value:n.value,defaultValue:n.defaultValue??[],onChange:n.onValueChange,equal:v,reconcile:_}),b=y.value,[x,S]=f(()=>({value:b[0]??null})),C=u(()=>s(`listbox`,_),[]),w=o(),T=d((t,n)=>{c||g||t.disabled||y.request(e=>{let n=e.some(e=>i(e,t.value));return p?n?e.filter(e=>!i(e,t.value)):[...e,t.value]:[t.value]},e(n,t.node))}),E=d(e=>{i(x.value,e)||S({value:e})});l(()=>{let e=C.options(),t=C.option(x.value)?.disabled===!1?x.value:b.find(e=>C.option(e)?.disabled===!1)??e.find(e=>!e.disabled)?.value??null;E(t);let n=C.containerNode();if(n!==null){let e=t===null?void 0:C.option(t)?.node.id??w(t,`option`);r(n,`aria-activedescendant`,e)}});let D={disabled:c,highlighted:x.value,idFor:w,multiple:p,readOnly:g,registry:C,select:T,setHighlighted:E};return{option:(e,t={})=>h(D,{disabled:t.disabled===!0,selected:b.some(t=>i(t,e)),textValue:t.textValue,value:e}),root:()=>m(D),values:b}}function _(e){return e.children(g(e))}function v(e,t){return e.length===t.length&&e.every((e,n)=>i(e,t[n]))}export{_ as Listbox,g as useListbox};
2
+ //# sourceMappingURL=listbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"listbox.js","names":["createListboxRegistry"],"sources":["../src/listbox/listbox.tsx"],"sourcesContent":["import {\n createMixin,\n type FigNode,\n type MixinContext,\n type MixinDescriptor,\n useBeforePaint,\n useMemo,\n useStableEvent,\n useState,\n} from \"@bgub/fig\";\nimport { on } from \"@bgub/fig-dom\";\nimport {\n type ChangeDetails,\n createChangeDetails,\n} from \"../internal/changes.ts\";\nimport { useControllableValue } from \"../internal/controllable-value.ts\";\nimport { sameValue } from \"../internal/composite.ts\";\nimport {\n assertAccessibleName,\n assertSingleSelection,\n} from \"../internal/diagnostics.ts\";\nimport { usePartIds } from \"../internal/ids.ts\";\nimport {\n createListbox as createListboxRegistry,\n type ListboxOption,\n} from \"../internal/listbox.ts\";\nimport { bindPart, setIdReference } from \"../internal/parts.ts\";\nimport { useRegistrationReconcile } from \"../internal/reconcile.ts\";\n\nexport type ListboxValueChangeDetails = ChangeDetails;\n\nexport type ListboxValueChangeHandler<Value = unknown> = (\n values: readonly Value[],\n details: ListboxValueChangeDetails,\n signal: AbortSignal,\n) => void;\n\nexport interface ListboxOptionOptions {\n disabled?: boolean;\n /** Text used for typeahead when it differs from the host's text content. */\n textValue?: string;\n}\n\nexport interface ListboxOptions<Value = unknown> {\n defaultValue?: readonly Value[];\n disabled?: boolean;\n multiple?: boolean;\n onValueChange?: ListboxValueChangeHandler<Value>;\n readOnly?: boolean;\n value?: readonly Value[];\n}\n\nexport interface ListboxParts<Value = unknown> {\n readonly values: readonly Value[];\n option(value: Value, options?: ListboxOptionOptions): MixinDescriptor;\n root(): MixinDescriptor;\n}\n\nexport interface ListboxProps<Value = unknown> extends ListboxOptions<Value> {\n children: (listbox: ListboxParts<Value>) => FigNode;\n}\n\ntype ListboxRegistry = ReturnType<typeof createListboxRegistry>;\n\ninterface ListboxState {\n readonly disabled: boolean;\n readonly highlighted: unknown;\n readonly idFor: (value: unknown, part: string) => string;\n readonly multiple: boolean;\n readonly readOnly: boolean;\n readonly registry: ListboxRegistry;\n readonly select: (option: ListboxOption, event: Event) => void;\n readonly setHighlighted: (value: unknown) => void;\n}\n\ninterface ListboxOptionState {\n readonly disabled: boolean;\n readonly selected: boolean;\n readonly textValue: string | undefined;\n readonly value: unknown;\n}\n\nconst listboxRootMixin = /* @__PURE__ */ createMixin(\n (context: MixinContext, state: ListboxState) => ({\n \"aria-activedescendant\":\n state.highlighted === null\n ? undefined\n : (state.registry.option(state.highlighted)?.node.id ??\n state.idFor(state.highlighted, \"option\")),\n \"aria-disabled\": state.disabled ? \"true\" : undefined,\n \"aria-multiselectable\": state.multiple ? \"true\" : undefined,\n \"aria-readonly\": state.readOnly ? \"true\" : undefined,\n bind: bindPart(context, state.registry, state.registry.bindContainer),\n \"data-disabled\": state.disabled ? \"\" : undefined,\n \"data-readonly\": state.readOnly ? \"\" : undefined,\n mix: [\n on(\"click\", (event) => {\n if (event.defaultPrevented) return;\n const option = state.registry.optionAt(event.target);\n if (option === undefined) return;\n if (state.disabled || option.disabled) {\n event.preventDefault();\n } else if (event.button === 0) {\n state.setHighlighted(option.value);\n if (state.readOnly) event.preventDefault();\n else state.select(option, event);\n }\n }),\n on(\"pointerdown\", (event) => {\n if (state.registry.optionAt(event.target) === undefined) return;\n event.preventDefault();\n if (!state.disabled && event.currentTarget instanceof HTMLElement) {\n event.currentTarget.focus();\n }\n }),\n on(\"pointermove\", (event) => {\n if (event.pointerType === \"touch\") return;\n if (state.disabled) return;\n const option = state.registry.optionAt(event.target);\n if (option !== undefined && !option.disabled) {\n state.setHighlighted(option.value);\n }\n }),\n on(\"keydown\", (event) => {\n if (event.defaultPrevented) return;\n if (\n event.target instanceof Element &&\n event.target.closest('[role=\"listbox\"]') !== event.currentTarget\n )\n return;\n if (\n state.disabled ||\n event.isComposing ||\n event.altKey ||\n event.ctrlKey ||\n event.metaKey ||\n event.shiftKey\n ) {\n return;\n }\n const moved = state.registry.move(state.highlighted, event.key);\n if (moved !== undefined) {\n event.preventDefault();\n state.setHighlighted(moved.value);\n if (!state.multiple) state.select(moved, event);\n return;\n }\n if (event.key === \"Enter\" || event.key === \" \") {\n const option = state.registry.option(state.highlighted);\n if (option !== undefined) {\n event.preventDefault();\n state.select(option, event);\n }\n return;\n }\n const match = state.registry.typeahead(state.highlighted, event.key);\n if (match === undefined) return;\n state.setHighlighted(match.value);\n if (!state.multiple) state.select(match, event);\n }),\n ],\n role: \"listbox\",\n tabindex: state.disabled ? -1 : (context.props.tabindex ?? 0),\n }),\n);\n\nconst listboxOptionMixin = /* @__PURE__ */ createMixin(\n (context: MixinContext, state: ListboxState, own: ListboxOptionState) => {\n const disabled = own.disabled || context.props.disabled === true;\n return {\n \"aria-disabled\": disabled ? \"true\" : undefined,\n \"aria-selected\": own.selected ? \"true\" : \"false\",\n bind: bindPart(context, state.registry, (node, signal) =>\n state.registry.bindOption(node, signal, {\n disabled,\n textValue: own.textValue,\n value: own.value,\n }),\n ),\n \"data-disabled\": disabled ? \"\" : undefined,\n \"data-highlighted\": sameValue(state.highlighted, own.value)\n ? \"\"\n : undefined,\n \"data-selected\": own.selected ? \"\" : undefined,\n id: context.props.id ?? state.idFor(own.value, \"option\"),\n role: \"option\",\n type:\n context.type === \"button\"\n ? (context.props.type ?? \"button\")\n : undefined,\n tabindex: -1,\n };\n },\n);\n\n/** Coordinates selection and active-descendant navigation for one listbox. */\nexport function useListbox<Value = unknown>(\n options: ListboxOptions<Value> = {},\n): ListboxParts<Value> {\n const { disabled = false, multiple = false, readOnly = false } = options;\n const requestReconcile = useRegistrationReconcile();\n const selection = useControllableValue<readonly Value[]>({\n value: options.value,\n defaultValue: options.defaultValue ?? [],\n onChange: options.onValueChange,\n equal: sameValues,\n reconcile: requestReconcile,\n });\n const values = selection.value;\n if (!multiple) assertSingleSelection(values, \"single-select listbox\");\n\n const [highlighted, setHighlightedState] = useState<{\n readonly value: unknown;\n }>(() => ({ value: values[0] ?? null }));\n const registry = useMemo(\n () => createListboxRegistry(\"listbox\", requestReconcile),\n [],\n );\n const idFor = usePartIds();\n const select = useStableEvent((option: ListboxOption, event: Event) => {\n if (disabled || readOnly || option.disabled) return;\n selection.request(\n (current) => {\n const selected = current.some((value) =>\n sameValue(value, option.value),\n );\n return multiple\n ? selected\n ? current.filter((value) => !sameValue(value, option.value))\n : [...current, option.value as Value]\n : [option.value as Value];\n },\n createChangeDetails(event, option.node),\n );\n });\n const setHighlighted = useStableEvent((next: unknown) => {\n if (!sameValue(highlighted.value, next)) {\n setHighlightedState({ value: next });\n }\n });\n\n useBeforePaint(() => {\n const mounted = registry.options();\n const nextHighlight =\n registry.option(highlighted.value)?.disabled === false\n ? highlighted.value\n : (values.find((value) => registry.option(value)?.disabled === false) ??\n mounted.find((option) => !option.disabled)?.value ??\n null);\n setHighlighted(nextHighlight);\n\n const root = registry.containerNode();\n if (root !== null) {\n assertAccessibleName(root, \"listbox\");\n const optionId =\n nextHighlight === null\n ? undefined\n : (registry.option(nextHighlight)?.node.id ??\n idFor(nextHighlight, \"option\"));\n setIdReference(root, \"aria-activedescendant\", optionId);\n }\n });\n\n const state: ListboxState = {\n disabled,\n highlighted: highlighted.value,\n idFor,\n multiple,\n readOnly,\n registry,\n select,\n setHighlighted,\n };\n\n return {\n option: (value, optionOptions = {}) =>\n listboxOptionMixin(state, {\n disabled: optionOptions.disabled === true,\n selected: values.some((selected) => sameValue(selected, value)),\n textValue: optionOptions.textValue,\n value,\n }),\n root: () => listboxRootMixin(state),\n values,\n };\n}\n\n/** {@link useListbox} as a render-callback component. */\nexport function Listbox<Value = unknown>(props: ListboxProps<Value>): FigNode {\n return props.children(useListbox(props));\n}\n\nfunction sameValues(\n left: readonly unknown[],\n right: readonly unknown[],\n): boolean {\n return (\n left.length === right.length &&\n left.every((value, index) => sameValue(value, right[index]))\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import { MenuOptions, MenuParts } from "../menu.js";
2
+ import { FigNode } from "@bgub/fig";
3
+ //#region src/menu/context-menu.d.ts
4
+ interface ContextMenuOptions<Value = unknown> extends MenuOptions<Value> {
5
+ disabled?: boolean;
6
+ }
7
+ interface ContextMenuProps<Value = unknown> extends ContextMenuOptions<Value> {
8
+ children: (menu: MenuParts<Value>) => FigNode;
9
+ }
10
+ /** Optional point-positioned menu; ordinary clicks retain their native action. */
11
+ declare function useContextMenu<Value = unknown>(options?: ContextMenuOptions<Value>): MenuParts<Value>;
12
+ /** {@link useContextMenu} as a render-callback component. */
13
+ declare function ContextMenu<Value = unknown>(props: ContextMenuProps<Value>): FigNode;
14
+ //#endregion
15
+ export { ContextMenu, ContextMenuOptions, ContextMenuProps, useContextMenu };
@@ -0,0 +1,2 @@
1
+ import{t as e}from"../changes-DglRuvQa.js";import{n as t,s as n}from"../parts-I0jdefyp.js";import{a as r,i,n as a}from"../menu-B4pKuF3z.js";import{createMixin as o,useBeforePaint as s,useId as c,useMemo as l,useStableEvent as u,useState as d}from"@bgub/fig";import{on as f}from"@bgub/fig-dom";const p=o((e,n)=>{let r=n.disabled||e.props.disabled===!0;return{"aria-disabled":r?`true`:void 0,"aria-controls":n.popupId,"aria-expanded":n.open?`true`:`false`,"aria-haspopup":`menu`,bind:t(e,n.owner,n.bind),"data-disabled":r?``:void 0,"data-open":n.open?``:void 0,id:e.props.id??n.id,tabindex:e.props.tabindex??0,type:e.type===`button`?e.props.type??`button`:void 0,mix:[f(`contextmenu`,e=>{r||e.defaultPrevented||n.show(e)}),f(`keydown`,e=>{r||e.defaultPrevented||e.isComposing||e.keyCode===229||e.altKey||e.ctrlKey||e.metaKey||(e.key===`ContextMenu`||e.key===`F10`&&e.shiftKey)&&n.show(e)})]}});function m(t={}){let o=c(),f=t.id??`${o}-context-menu`,m=l(()=>({trigger:n(()=>{}),activation:null,positioner:g()}),[]),[h,_]=d(null),v=a({...t,id:f,onOpenChange:(n,r,i)=>{if(m.activation===null){t.onOpenChange?.(n,r,i);return}let a=e(m.activation,m.trigger.node()??void 0);t.onOpenChange?.(n,a,i),a.isCanceled&&r.cancel()}}),y=i(v),b=u((e,t)=>{m.trigger.bind(e,t),y.bindTrigger(e,t)}),x=u(e=>{let t=m.trigger.node();if(!(t===null||t.matches(`:disabled`))){e.preventDefault(),e instanceof KeyboardEvent?_(null):_({x:e.clientX,y:e.clientY}),m.activation=e;try{y.setOpen(!0,`first`)}finally{m.activation=null}}});s(e=>{e.addEventListener(`abort`,m.positioner.stop,{once:!0})},[]),s(()=>{m.positioner.sync(v.open?y.popup():null,m.trigger.node(),h)});let S={...v,trigger:()=>p({owner:m.trigger,bind:b,disabled:t.disabled===!0,id:`${o}-trigger`,open:v.open,popupId:f,show:x})};return r(S,y),S}function h(e){return e.children(m(e))}function g(){let e=null,t=null,n=null,r,i=()=>{};function a(){r?.(),r=void 0,e=null,t=null,i=()=>{}}function o(o,c,l){n=l,(o!==e||c!==t)&&(a(),o!==null&&c!==null&&(e=o,t=c,r=s(o,c))),i()}function s(e,t){let r=e.ownerDocument.defaultView;if(r===null)return()=>{};let a=[`position`,`right`,`bottom`,`margin`,`position-area`,`left`,`top`,`max-width`,`max-height`,`box-sizing`,`overflow`],o=new Map,s=new Map,c=()=>{for(let t of a){let n=o.get(t);if(n===void 0||e.style.getPropertyValue(t)!==s.get(t))continue;let[r,i]=n;r?e.style.setProperty(t,r,i):e.style.removeProperty(t)}s.clear()},l=(t,n)=>{e.style.setProperty(t,n),s.set(t,e.style.getPropertyValue(t))},u=``,d=``,f=()=>{let i=t.getBoundingClientRect(),a=r.getComputedStyle(t).direction,o=a?a===`rtl`:t.closest(`[dir]`)?.getAttribute(`dir`)===`rtl`,s=n??{x:o?i.right:i.left,y:i.bottom},c=r.visualViewport,f=c?.offsetLeft??0,p=c?.offsetTop??0,m=c?.width??r.innerWidth,h=c?.height??r.innerHeight,g=_(e),v=`${Math.max(0,m-16)/g}px`,y=`${Math.max(0,h-16)/g}px`;l(`max-width`,!u||u===`none`?v:`min(${u}, ${v})`),l(`max-height`,!d||d===`none`?y:`min(${d}, ${y})`);let b=e.getBoundingClientRect(),x=o?s.x-b.width:s.x,S=s.y+b.height>p+h-8?s.y-b.height:s.y;l(`left`,`${Math.max(f+8,Math.min(x,f+m-b.width-8))/g}px`),l(`top`,`${Math.max(p+8,Math.min(S,p+h-b.height-8))/g}px`)};i=()=>{c();for(let t of a)o.set(t,[e.style.getPropertyValue(t),e.style.getPropertyPriority(t)]);let t=r.getComputedStyle(e);u=t.maxWidth,d=t.maxHeight,l(`position`,`fixed`),l(`right`,`auto`),l(`bottom`,`auto`),l(`margin`,`0`),l(`position-area`,`none`),l(`box-sizing`,`border-box`),l(`overflow`,`auto`),f()};let p,m=()=>{p===void 0&&(p=r.requestAnimationFrame(()=>{p=void 0,f()}))};r.addEventListener(`resize`,m),r.addEventListener(`scroll`,m,!0),r.visualViewport?.addEventListener(`resize`,m),r.visualViewport?.addEventListener(`scroll`,m);let h=typeof ResizeObserver>`u`?null:new ResizeObserver(m);return h?.observe(e),h?.observe(t),()=>{p!==void 0&&r.cancelAnimationFrame(p),r.removeEventListener(`resize`,m),r.removeEventListener(`scroll`,m,!0),r.visualViewport?.removeEventListener(`resize`,m),r.visualViewport?.removeEventListener(`scroll`,m),h?.disconnect(),c()}}return{sync:o,stop:a}}function _(e){let t=e.currentCSSZoom;if(t!==void 0&&Number.isFinite(t)&&t>0)return t;let n=e.ownerDocument.defaultView;if(n===null)return 1;let r=1,i=e;for(;i!==null;){let e=n.getComputedStyle(i).getPropertyValue(`zoom`),t=parseFloat(e)/(e.endsWith(`%`)?100:1);if(Number.isFinite(t)&&t>0&&(r*=t),i.parentElement!==null)i=i.parentElement;else{let e=i.getRootNode();i=e instanceof ShadowRoot?e.host:null}}return Number.isFinite(r)&&r>0?r:1}export{h as ContextMenu,m as useContextMenu};
2
+ //# sourceMappingURL=context-menu.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-menu.js","names":[],"sources":["../../src/menu/context-menu.tsx"],"sourcesContent":["import {\n createMixin,\n type FigNode,\n type MixinContext,\n type MixinDescriptor,\n useBeforePaint,\n useId,\n useMemo,\n useStableEvent,\n useState,\n} from \"@bgub/fig\";\nimport { on } from \"@bgub/fig-dom\";\nimport { createChangeDetails } from \"../internal/changes.ts\";\nimport { bindPart } from \"../internal/parts.ts\";\nimport { createPartSlot } from \"../internal/registration.ts\";\nimport { menuController, registerMenuController } from \"./controller.ts\";\nimport { type MenuOptions, type MenuParts, useMenu } from \"./menu.tsx\";\n\nexport interface ContextMenuOptions<\n Value = unknown,\n> extends MenuOptions<Value> {\n disabled?: boolean;\n}\n\nexport interface ContextMenuProps<\n Value = unknown,\n> extends ContextMenuOptions<Value> {\n children: (menu: MenuParts<Value>) => FigNode;\n}\n\ninterface Point {\n readonly x: number;\n readonly y: number;\n}\n\ninterface TriggerState {\n readonly owner: object;\n readonly bind: (node: HTMLElement, signal: AbortSignal) => void;\n readonly disabled: boolean;\n readonly id: string;\n readonly open: boolean;\n readonly popupId: string;\n readonly show: (event: MouseEvent | KeyboardEvent) => void;\n}\n\nconst contextTriggerMixin = /* @__PURE__ */ createMixin(\n (context: MixinContext, state: TriggerState) => {\n const disabled = state.disabled || context.props.disabled === true;\n return {\n \"aria-disabled\": disabled ? \"true\" : undefined,\n \"aria-controls\": state.popupId,\n \"aria-expanded\": state.open ? \"true\" : \"false\",\n \"aria-haspopup\": \"menu\",\n bind: bindPart(context, state.owner, state.bind),\n \"data-disabled\": disabled ? \"\" : undefined,\n \"data-open\": state.open ? \"\" : undefined,\n id: context.props.id ?? state.id,\n // A context area may be an ordinary div. It must be reachable by the\n // keyboard, and capable of receiving focus back when the menu closes.\n tabindex: context.props.tabindex ?? 0,\n type:\n context.type === \"button\"\n ? (context.props.type ?? \"button\")\n : undefined,\n mix: [\n on(\"contextmenu\", (event) => {\n if (disabled || event.defaultPrevented) return;\n state.show(event);\n }),\n on(\"keydown\", (event) => {\n if (\n disabled ||\n event.defaultPrevented ||\n event.isComposing ||\n event.keyCode === 229 ||\n event.altKey ||\n event.ctrlKey ||\n event.metaKey\n )\n return;\n if (\n event.key === \"ContextMenu\" ||\n (event.key === \"F10\" && event.shiftKey)\n )\n state.show(event);\n }),\n ],\n };\n },\n);\n\n/** Optional point-positioned menu; ordinary clicks retain their native action. */\nexport function useContextMenu<Value = unknown>(\n options: ContextMenuOptions<Value> = {},\n): MenuParts<Value> {\n const id = useId();\n const popupId = options.id ?? `${id}-context-menu`;\n const tracker = useMemo(\n () => ({\n trigger: createPartSlot(() => {}),\n activation: null as MouseEvent | KeyboardEvent | null,\n positioner: createContextPositioner(),\n }),\n [],\n );\n const [point, setPoint] = useState<Point | null>(null);\n const menu = useMenu<Value>({\n ...options,\n id: popupId,\n onOpenChange: (open, details, signal) => {\n if (tracker.activation === null) {\n options.onOpenChange?.(open, details, signal);\n return;\n }\n const activation = createChangeDetails(\n tracker.activation,\n tracker.trigger.node() ?? undefined,\n );\n options.onOpenChange?.(open, activation, signal);\n if (activation.isCanceled) details.cancel();\n },\n });\n const controller = menuController(menu);\n const bind = useStableEvent((node: HTMLElement, signal: AbortSignal) => {\n tracker.trigger.bind(node, signal);\n controller.bindTrigger(node, signal);\n });\n const show = useStableEvent((event: MouseEvent | KeyboardEvent) => {\n const trigger = tracker.trigger.node();\n if (trigger === null || trigger.matches(\":disabled\")) return;\n event.preventDefault();\n if (event instanceof KeyboardEvent) {\n // Recompute from the element on resize/scroll for keyboard invocation.\n setPoint(null);\n } else setPoint({ x: event.clientX, y: event.clientY });\n tracker.activation = event;\n try {\n controller.setOpen(true, \"first\");\n } finally {\n tracker.activation = null;\n }\n });\n\n useBeforePaint((signal) => {\n signal.addEventListener(\"abort\", tracker.positioner.stop, { once: true });\n }, []);\n useBeforePaint(() => {\n tracker.positioner.sync(\n menu.open ? controller.popup() : null,\n tracker.trigger.node(),\n point,\n );\n });\n\n const parts: MenuParts<Value> = {\n ...menu,\n trigger: (): MixinDescriptor =>\n contextTriggerMixin({\n owner: tracker.trigger,\n bind,\n disabled: options.disabled === true,\n id: `${id}-trigger`,\n open: menu.open,\n popupId,\n show,\n }),\n };\n registerMenuController(parts, controller);\n return parts;\n}\n\n/** {@link useContextMenu} as a render-callback component. */\nexport function ContextMenu<Value = unknown>(\n props: ContextMenuProps<Value>,\n): FigNode {\n return props.children(useContextMenu(props));\n}\n\n/** Retains observation across renders; only changed hosts replace listeners. */\nfunction createContextPositioner() {\n let currentPopup: HTMLElement | null = null;\n let currentTrigger: HTMLElement | null = null;\n let currentPoint: Point | null = null;\n let cleanup: (() => void) | undefined;\n let reposition = () => {};\n\n function stop() {\n cleanup?.();\n cleanup = undefined;\n currentPopup = null;\n currentTrigger = null;\n reposition = () => {};\n }\n\n function sync(\n popup: HTMLElement | null,\n trigger: HTMLElement | null,\n point: Point | null,\n ) {\n currentPoint = point;\n if (popup !== currentPopup || trigger !== currentTrigger) {\n stop();\n if (popup !== null && trigger !== null) {\n currentPopup = popup;\n currentTrigger = trigger;\n cleanup = observe(popup, trigger);\n }\n }\n reposition();\n }\n\n function observe(popup: HTMLElement, trigger: HTMLElement): () => void {\n const view = popup.ownerDocument.defaultView;\n if (view === null) return () => {};\n // Point positioning deliberately replaces CSS anchor placement only for\n // this optional widget. Keep authored inline styles intact on teardown.\n const properties = [\n \"position\",\n \"right\",\n \"bottom\",\n \"margin\",\n \"position-area\",\n \"left\",\n \"top\",\n \"max-width\",\n \"max-height\",\n \"box-sizing\",\n \"overflow\",\n ] as const;\n const previous = new Map<string, readonly [string, string]>();\n const applied = new Map<string, string>();\n const restore = () => {\n for (const property of properties) {\n const original = previous.get(property);\n if (\n original === undefined ||\n popup.style.getPropertyValue(property) !== applied.get(property)\n )\n continue;\n const [value, priority] = original;\n if (value) popup.style.setProperty(property, value, priority);\n else popup.style.removeProperty(property);\n }\n applied.clear();\n };\n const write = (property: string, value: string) => {\n popup.style.setProperty(property, value);\n applied.set(property, popup.style.getPropertyValue(property));\n };\n let maxWidth = \"\";\n let maxHeight = \"\";\n const position = () => {\n const triggerRect = trigger.getBoundingClientRect();\n const direction = view.getComputedStyle(trigger).direction;\n const rtl = direction\n ? direction === \"rtl\"\n : trigger.closest(\"[dir]\")?.getAttribute(\"dir\") === \"rtl\";\n const origin = currentPoint ?? {\n x: rtl ? triggerRect.right : triggerRect.left,\n y: triggerRect.bottom,\n };\n const viewport = view.visualViewport;\n const left = viewport?.offsetLeft ?? 0;\n const top = viewport?.offsetTop ?? 0;\n const width = viewport?.width ?? view.innerWidth;\n const height = viewport?.height ?? view.innerHeight;\n const scale = effectiveZoom(popup);\n const availableWidth = `${Math.max(0, width - 16) / scale}px`;\n const availableHeight = `${Math.max(0, height - 16) / scale}px`;\n write(\n \"max-width\",\n !maxWidth || maxWidth === \"none\"\n ? availableWidth\n : `min(${maxWidth}, ${availableWidth})`,\n );\n write(\n \"max-height\",\n !maxHeight || maxHeight === \"none\"\n ? availableHeight\n : `min(${maxHeight}, ${availableHeight})`,\n );\n const rect = popup.getBoundingClientRect();\n const x = rtl ? origin.x - rect.width : origin.x;\n const y =\n origin.y + rect.height > top + height - 8\n ? origin.y - rect.height\n : origin.y;\n write(\n \"left\",\n `${Math.max(left + 8, Math.min(x, left + width - rect.width - 8)) / scale}px`,\n );\n write(\n \"top\",\n `${Math.max(top + 8, Math.min(y, top + height - rect.height - 8)) / scale}px`,\n );\n };\n reposition = () => {\n // New authored props/classes may change while open. Remove only values\n // still owned by us before reading the author's current constraints.\n restore();\n for (const property of properties)\n previous.set(property, [\n popup.style.getPropertyValue(property),\n popup.style.getPropertyPriority(property),\n ]);\n const authored = view.getComputedStyle(popup);\n maxWidth = authored.maxWidth;\n maxHeight = authored.maxHeight;\n write(\"position\", \"fixed\");\n write(\"right\", \"auto\");\n write(\"bottom\", \"auto\");\n write(\"margin\", \"0\");\n write(\"position-area\", \"none\");\n write(\"box-sizing\", \"border-box\");\n write(\"overflow\", \"auto\");\n position();\n };\n let frame: number | undefined;\n const schedule = () => {\n if (frame !== undefined) return;\n frame = view.requestAnimationFrame(() => {\n frame = undefined;\n position();\n });\n };\n view.addEventListener(\"resize\", schedule);\n view.addEventListener(\"scroll\", schedule, true);\n view.visualViewport?.addEventListener(\"resize\", schedule);\n view.visualViewport?.addEventListener(\"scroll\", schedule);\n const observer =\n typeof ResizeObserver === \"undefined\"\n ? null\n : new ResizeObserver(schedule);\n observer?.observe(popup);\n observer?.observe(trigger);\n return () => {\n if (frame !== undefined) view.cancelAnimationFrame(frame);\n view.removeEventListener(\"resize\", schedule);\n view.removeEventListener(\"scroll\", schedule, true);\n view.visualViewport?.removeEventListener(\"resize\", schedule);\n view.visualViewport?.removeEventListener(\"scroll\", schedule);\n observer?.disconnect();\n restore();\n };\n }\n return { sync, stop };\n}\n\n/** CSS zoom changes the coordinate space of fixed top-layer descendants. */\nfunction effectiveZoom(node: HTMLElement): number {\n const native = (node as HTMLElement & { currentCSSZoom?: number })\n .currentCSSZoom;\n if (native !== undefined && Number.isFinite(native) && native > 0)\n return native;\n const view = node.ownerDocument.defaultView;\n if (view === null) return 1;\n let zoom = 1;\n let current: Element | null = node;\n while (current !== null) {\n const value = view.getComputedStyle(current).getPropertyValue(\"zoom\");\n const factor = parseFloat(value) / (value.endsWith(\"%\") ? 100 : 1);\n if (Number.isFinite(factor) && factor > 0) zoom *= factor;\n if (current.parentElement !== null) current = current.parentElement;\n else {\n const root = current.getRootNode();\n current = root instanceof ShadowRoot ? root.host : null;\n }\n }\n return Number.isFinite(zoom) && zoom > 0 ? zoom : 1;\n}\n"],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import { MenuOptions, MenuParts } from "../menu.js";
2
+ import { FigNode } from "@bgub/fig";
3
+ //#region src/menu/submenu.d.ts
4
+ interface MenuSubmenuOptions<Value = unknown> extends MenuOptions<Value> {
5
+ /** Delay for mouse opening and closing. Defaults to 100ms. */
6
+ delay?: number;
7
+ /** Disables the trigger in its parent menu. */
8
+ disabled?: boolean;
9
+ }
10
+ interface MenuSubmenuProps<ParentValue, Value = unknown> extends MenuSubmenuOptions<Value> {
11
+ children: (submenu: MenuParts<Value>) => FigNode;
12
+ parent: MenuParts<ParentValue>;
13
+ value: ParentValue;
14
+ }
15
+ /** A nested menu whose trigger remains an item in its parent menu. */
16
+ declare function useMenuSubmenu<ParentValue, Value = unknown>(parent: MenuParts<ParentValue>, value: ParentValue, options?: MenuSubmenuOptions<Value>): MenuParts<Value>;
17
+ /** {@link useMenuSubmenu} as a render-callback component. */
18
+ declare function MenuSubmenu<ParentValue, Value = unknown>(props: MenuSubmenuProps<ParentValue, Value>): FigNode;
19
+ //#endregion
20
+ export { MenuSubmenu, MenuSubmenuOptions, MenuSubmenuProps, useMenuSubmenu };
@@ -0,0 +1,2 @@
1
+ import{n as e,s as t}from"../parts-I0jdefyp.js";import{a as n,i as r,n as i,r as a}from"../menu-B4pKuF3z.js";import{t as o}from"../hover-HF7JXkR2.js";import{createMixin as s,useBeforePaint as c,useMemo as l,useStableEvent as u}from"@bgub/fig";import{on as d}from"@bgub/fig-dom";function f(e,t,n,r,i){if(i.aborted)return;let a=o(),s=t.ownerDocument,c={x:e.clientX,y:e.clientY};function l(){a.cancel(),s.removeEventListener(`pointermove`,d,!0),i.removeEventListener(`abort`,l)}function u(){l(),n()}function d(e){if(e.pointerType!==`mouse`)return;let o=t.getBoundingClientRect(),s={x:e.clientX,y:e.clientY};if(t.isConnected&&s.x>=o.left&&s.x<=o.right&&s.y>=o.top&&s.y<=o.bottom){l();return}if(!t.isConnected||!p(c,s,o)){l(),a.schedule(!1,n,r,i);return}a.schedule(!1,u,Math.max(r,300),i)}s.addEventListener(`pointermove`,d,!0),i.addEventListener(`abort`,l,{once:!0}),a.schedule(!1,u,Math.max(r,300),i)}function p(e,t,n){let r,i;if(Math.max(n.left-e.x,0,e.x-n.right)>Math.max(n.top-e.y,0,e.y-n.bottom)){let t=e.x<n.left?n.left:n.right;r={x:t,y:n.top-4},i={x:t,y:n.bottom+4}}else{let t=e.y<n.top?n.top:n.bottom;r={x:n.left-4,y:t},i={x:n.right+4,y:t}}let a=m(e,r,t),o=m(r,i,t),s=m(i,e,t);return!(Math.min(a,o,s)<0&&Math.max(a,o,s)>0)}function m(e,t,n){return(t.x-e.x)*(n.y-e.y)-(t.y-e.y)*(n.x-e.x)}const h=s((t,n)=>({bind:e(t,n.bindTrigger,(e,t)=>{n.bindTrigger(e,t)&&t.addEventListener(`abort`,()=>n.hover(),{once:!0}),n.disabled&&n.hover()}),"aria-disabled":n.disabled?`true`:void 0,"data-disabled":n.disabled?``:void 0,mix:[d(`focusout`,e=>{e.relatedTarget instanceof Node&&n.popup()?.contains(e.relatedTarget)||(n.hover(),n.setOpen(!1,!1))}),d(`keydown`,e=>{n.hover(),!(e.defaultPrevented||e.isComposing||e.keyCode===229||n.disabled)&&e.key===x(e.currentTarget)&&(e.preventDefault(),n.setOpen(!0,`first`))}),d(`pointerenter`,e=>{n.disabled||e.pointerType!==`mouse`||n.hover(!n.open||void 0,e)}),d(`pointerleave`,e=>{e.pointerType===`mouse`&&n.hover(!1,e)})]})),g=s((e,t)=>({mix:[d(`keydown`,e=>{if(t.hover(),!(e.defaultPrevented||e.isComposing||e.keyCode===229)){if(e.key===`Tab`){t.closeTree();return}e.key===S(e.currentTarget)&&(e.preventDefault(),t.setOpen(!1))}}),d(`pointerenter`,e=>{e.pointerType===`mouse`&&t.hover()}),d(`pointerleave`,e=>{e.pointerType===`mouse`&&t.hover(!1,e)})]})),_=s((e,t,n,r,i)=>{let a=i.disabled||e.props.disabled===!0;return[t.submenuTrigger(r,a),d(`pointerdown`,e=>i.notePointer(e.pointerType)),d(`click`,e=>{e.defaultPrevented||e.button!==0||(e.preventDefault(),!a&&i.activate(e))}),d(`keydown`,e=>{e.defaultPrevented||e.isComposing||e.keyCode===229||e.key!==`Enter`||(e.preventDefault(),a||i.activate())}),n.trigger(!1,a),h({...i,disabled:a})]}),v=s((e,t,n)=>[t,g(n)]);function y(e,s,d={}){let{delay:p=100,disabled:m=!1,...h}=d,g=r(e),y=i({...h,onSelect:(e,t,n)=>{h.onSelect?.(e,t,n),!t.isCanceled&&a(t)&&g.closeTree(T())}}),b=r(y),x=l(o,[]),S=l(()=>t(()=>{}),[]),C=l(()=>({openedOnHover:!1,pointerType:``}),[]),w=u((e,t,n)=>{if(x.cancel(),e===void 0)return;let r=S.node(),i=()=>{S.node()!==r||!r?.isConnected||(C.openedOnHover=e,b.setOpen(e,!1))},a=b.popup();!e&&y.open&&t!==void 0&&a!==null&&r!==null?f(t,t.currentTarget===r?a:r,i,p,n):x.schedule(e,i,p,n)});c(()=>{(!e.open||m)&&w(void 0,void 0)}),c(()=>{y.open||(C.openedOnHover=!1,w(void 0,void 0))},[y.open]);function T(){let e=b.popup(),t=e?.ownerDocument.activeElement;return t!=null&&e?.contains(t)?t:void 0}let E=u(e=>{let t=e??T();y.setOpen(!1),g.closeTree(t)}),D={notePointer:e=>{C.pointerType=e},activate:e=>{w(void 0,void 0);let t=e!==void 0&&e.detail>0&&C.pointerType===`mouse`;C.pointerType=``;let n=!y.open||C.openedOnHover||t;C.openedOnHover=!1,b.setOpen(n,`first`)},closeTree:E,disabled:m,hover:(e,t)=>w(e,t),bindTrigger:S.bind,open:y.open,popup:()=>b.popup(),setOpen:(e,t)=>{C.openedOnHover=!1,b.setOpen(e,t)}},O={...y,menu:()=>v(y.menu(),D),trigger:()=>_(g,b,s,D)};return n(O,{...b,closeTree:E}),O}function b(e){return e.children(y(e.parent,e.value,e))}function x(e){return C(e)?`ArrowLeft`:`ArrowRight`}function S(e){return C(e)?`ArrowRight`:`ArrowLeft`}function C(e){if(!(e instanceof Element))return!1;let t=getComputedStyle(e).direction;return t?t===`rtl`:e.closest(`[dir]`)?.getAttribute(`dir`)===`rtl`}export{b as MenuSubmenu,y as useMenuSubmenu};
2
+ //# sourceMappingURL=submenu.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"submenu.js","names":[],"sources":["../../src/internal/hover-transit.ts","../../src/menu/submenu.ts"],"sourcesContent":["import { createHoverIntent } from \"./hover.ts\";\n\n/** Keep a popup reachable across a gap, using its placed bounds, not its CSS side. */\nexport function trackHoverTransit(\n leave: PointerEvent,\n target: HTMLElement,\n close: () => void,\n delay: number,\n signal: AbortSignal,\n): void {\n if (signal.aborted) return;\n const intent = createHoverIntent();\n const document = target.ownerDocument;\n const start = { x: leave.clientX, y: leave.clientY };\n function stop(): void {\n intent.cancel();\n document.removeEventListener(\"pointermove\", move, true);\n signal.removeEventListener(\"abort\", stop);\n }\n function finish(): void {\n stop();\n close();\n }\n function move(event: PointerEvent): void {\n if (event.pointerType !== \"mouse\") return;\n const rect = target.getBoundingClientRect();\n const point = { x: event.clientX, y: event.clientY };\n if (\n target.isConnected &&\n point.x >= rect.left &&\n point.x <= rect.right &&\n point.y >= rect.top &&\n point.y <= rect.bottom\n ) {\n stop();\n return;\n }\n if (!target.isConnected || !inCorridor(start, point, rect)) {\n stop();\n intent.schedule(false, close, delay, signal);\n return;\n }\n // A stationary pointer must not leave the popup stuck open indefinitely.\n intent.schedule(false, finish, Math.max(delay, 300), signal);\n }\n document.addEventListener(\"pointermove\", move, true);\n signal.addEventListener(\"abort\", stop, { once: true });\n intent.schedule(false, finish, Math.max(delay, 300), signal);\n}\n\ninterface Point {\n readonly x: number;\n readonly y: number;\n}\n\nfunction inCorridor(start: Point, point: Point, rect: DOMRect): boolean {\n // Choose the facing edge in either axis. This also covers RTL and CSS flips.\n let a: Point;\n let b: Point;\n const dx = Math.max(rect.left - start.x, 0, start.x - rect.right);\n const dy = Math.max(rect.top - start.y, 0, start.y - rect.bottom);\n if (dx > dy) {\n const x = start.x < rect.left ? rect.left : rect.right;\n a = { x, y: rect.top - 4 };\n b = { x, y: rect.bottom + 4 };\n } else {\n const y = start.y < rect.top ? rect.top : rect.bottom;\n a = { x: rect.left - 4, y };\n b = { x: rect.right + 4, y };\n }\n const first = cross(start, a, point);\n const second = cross(a, b, point);\n const third = cross(b, start, point);\n return !(\n Math.min(first, second, third) < 0 && Math.max(first, second, third) > 0\n );\n}\n\nfunction cross(a: Point, b: Point, p: Point): number {\n return (b.x - a.x) * (p.y - a.y) - (b.y - a.y) * (p.x - a.x);\n}\n","import {\n createMixin,\n type FigNode,\n type MixinContext,\n type MixinDescriptor,\n useBeforePaint,\n useMemo,\n useStableEvent,\n} from \"@bgub/fig\";\nimport { on } from \"@bgub/fig-dom\";\nimport { createHoverIntent } from \"../internal/hover.ts\";\nimport { trackHoverTransit } from \"../internal/hover-transit.ts\";\nimport { createPartSlot } from \"../internal/registration.ts\";\nimport { bindPart } from \"../internal/parts.ts\";\nimport { type MenuOptions, type MenuParts, useMenu } from \"./menu.tsx\";\nimport {\n type MenuFocusTarget,\n type MenuController,\n menuClosesOnSelect,\n menuController,\n registerMenuController,\n} from \"./controller.ts\";\n\nexport interface MenuSubmenuOptions<\n Value = unknown,\n> extends MenuOptions<Value> {\n /** Delay for mouse opening and closing. Defaults to 100ms. */\n delay?: number;\n /** Disables the trigger in its parent menu. */\n disabled?: boolean;\n}\n\nexport interface MenuSubmenuProps<\n ParentValue,\n Value = unknown,\n> extends MenuSubmenuOptions<Value> {\n children: (submenu: MenuParts<Value>) => FigNode;\n parent: MenuParts<ParentValue>;\n value: ParentValue;\n}\n\ninterface SubmenuState {\n readonly activate: (event?: MouseEvent) => void;\n readonly notePointer: (type: string) => void;\n readonly closeTree: () => void;\n readonly disabled: boolean;\n readonly hover: (open?: boolean, event?: PointerEvent) => void;\n readonly bindTrigger: (node: HTMLElement, signal: AbortSignal) => boolean;\n readonly open: boolean;\n readonly popup: () => HTMLElement | null;\n readonly setOpen: (open: boolean, focus?: MenuFocusTarget) => void;\n}\n\nconst submenuTriggerBehavior = /* @__PURE__ */ createMixin(\n (context: MixinContext, state: SubmenuState) => ({\n bind: bindPart(context, state.bindTrigger, (node, signal) => {\n if (state.bindTrigger(node, signal))\n signal.addEventListener(\"abort\", () => state.hover(), { once: true });\n if (state.disabled) state.hover();\n }),\n \"aria-disabled\": state.disabled ? \"true\" : undefined,\n \"data-disabled\": state.disabled ? \"\" : undefined,\n mix: [\n on(\"focusout\", (event) => {\n if (\n event.relatedTarget instanceof Node &&\n state.popup()?.contains(event.relatedTarget)\n )\n return;\n state.hover();\n state.setOpen(false, false);\n }),\n on(\"keydown\", (event) => {\n state.hover();\n if (\n event.defaultPrevented ||\n event.isComposing ||\n event.keyCode === 229 ||\n state.disabled\n )\n return;\n if (event.key !== submenuOpenKey(event.currentTarget)) return;\n event.preventDefault();\n state.setOpen(true, \"first\");\n }),\n on(\"pointerenter\", (event) => {\n if (state.disabled || event.pointerType !== \"mouse\") return;\n state.hover(state.open ? undefined : true, event);\n }),\n on(\"pointerleave\", (event) => {\n if (event.pointerType === \"mouse\") state.hover(false, event);\n }),\n ],\n }),\n);\n\nconst submenuMenuBehavior = /* @__PURE__ */ createMixin(\n (_context: MixinContext, state: SubmenuState) => ({\n mix: [\n on(\"keydown\", (event) => {\n state.hover();\n if (\n event.defaultPrevented ||\n event.isComposing ||\n event.keyCode === 229\n )\n return;\n if (event.key === \"Tab\") {\n state.closeTree();\n return;\n }\n if (event.key !== submenuCloseKey(event.currentTarget)) return;\n event.preventDefault();\n state.setOpen(false);\n }),\n on(\"pointerenter\", (event) => {\n if (event.pointerType === \"mouse\") state.hover();\n }),\n on(\"pointerleave\", (event) => {\n if (event.pointerType === \"mouse\") state.hover(false, event);\n }),\n ],\n }),\n);\n\nconst submenuTriggerMixin = /* @__PURE__ */ createMixin(\n (\n context: MixinContext,\n parent: MenuController,\n child: MenuController,\n value: unknown,\n state: SubmenuState,\n ) => {\n const disabled = state.disabled || context.props.disabled === true;\n return [\n parent.submenuTrigger(value, disabled),\n on(\"pointerdown\", (event) => state.notePointer(event.pointerType)),\n on(\"click\", (event) => {\n if (event.defaultPrevented || event.button !== 0) return;\n event.preventDefault();\n if (disabled) return;\n // Mouse presses enter a revealed submenu. Touch and virtual clicks\n // toggle it so assistive users can return to the parent menu.\n state.activate(event);\n }),\n on(\"keydown\", (event) => {\n if (\n event.defaultPrevented ||\n event.isComposing ||\n event.keyCode === 229 ||\n event.key !== \"Enter\"\n )\n return;\n event.preventDefault();\n if (!disabled) state.activate();\n }),\n child.trigger(false, disabled),\n submenuTriggerBehavior({ ...state, disabled }),\n ];\n },\n);\n\nconst submenuMenuMixin = /* @__PURE__ */ createMixin(\n (_context: MixinContext, menu: MixinDescriptor, state: SubmenuState) => [\n menu,\n submenuMenuBehavior(state),\n ],\n);\n\n/** A nested menu whose trigger remains an item in its parent menu. */\nexport function useMenuSubmenu<ParentValue, Value = unknown>(\n parent: MenuParts<ParentValue>,\n value: ParentValue,\n options: MenuSubmenuOptions<Value> = {},\n): MenuParts<Value> {\n const { delay = 100, disabled = false, ...menuOptions } = options;\n const parentController = menuController(parent);\n const menu = useMenu<Value>({\n ...menuOptions,\n onSelect: (selected, details, signal) => {\n menuOptions.onSelect?.(selected, details, signal);\n if (!details.isCanceled && menuClosesOnSelect(details)) {\n parentController.closeTree(focusedItem());\n }\n },\n });\n const controller = menuController(menu);\n const intent = useMemo(createHoverIntent, []);\n const trigger = useMemo(() => createPartSlot(() => {}), []);\n const activation = useMemo(\n () => ({ openedOnHover: false, pointerType: \"\" }),\n [],\n );\n const hover = useStableEvent(\n (\n next: boolean | undefined,\n event: PointerEvent | undefined,\n signal: AbortSignal,\n ) => {\n intent.cancel();\n if (next === undefined) return;\n const anchor = trigger.node();\n const commit = () => {\n if (trigger.node() !== anchor || !anchor?.isConnected) return;\n activation.openedOnHover = next;\n controller.setOpen(next, false);\n };\n const popup = controller.popup();\n if (\n !next &&\n menu.open &&\n event !== undefined &&\n popup !== null &&\n anchor !== null\n ) {\n trackHoverTransit(\n event,\n event.currentTarget === anchor ? popup : anchor,\n commit,\n delay,\n signal,\n );\n } else intent.schedule(next, commit, delay, signal);\n },\n );\n useBeforePaint(() => {\n if (!parent.open || disabled) hover(undefined, undefined);\n });\n useBeforePaint(() => {\n if (!menu.open) {\n activation.openedOnHover = false;\n hover(undefined, undefined);\n }\n }, [menu.open]);\n function focusedItem(): Element | undefined {\n const popup = controller.popup();\n const active = popup?.ownerDocument.activeElement;\n return active !== null && active !== undefined && popup?.contains(active)\n ? active\n : undefined;\n }\n const closeTree = useStableEvent((focusOrigin?: Element) => {\n const origin = focusOrigin ?? focusedItem();\n menu.setOpen(false);\n parentController.closeTree(origin);\n });\n const state = {\n notePointer: (type: string) => {\n activation.pointerType = type;\n },\n activate: (event?: MouseEvent) => {\n hover(undefined, undefined);\n // WebKit reports mouse clicks after touch pointer events. Use the\n // pointerdown source; zero-detail virtual clicks still toggle.\n const mouse =\n event !== undefined &&\n event.detail > 0 &&\n activation.pointerType === \"mouse\";\n activation.pointerType = \"\";\n const next = !menu.open || activation.openedOnHover || mouse;\n activation.openedOnHover = false;\n controller.setOpen(next, \"first\");\n },\n closeTree,\n disabled,\n hover: (next?: boolean, event?: PointerEvent) => hover(next, event),\n bindTrigger: trigger.bind,\n open: menu.open,\n popup: () => controller.popup(),\n setOpen: (open: boolean, focus?: MenuFocusTarget) => {\n activation.openedOnHover = false;\n controller.setOpen(open, focus);\n },\n };\n\n const parts: MenuParts<Value> = {\n ...menu,\n menu: () => submenuMenuMixin(menu.menu(), state),\n trigger: () =>\n submenuTriggerMixin(parentController, controller, value, state),\n };\n registerMenuController(parts, { ...controller, closeTree });\n return parts;\n}\n\n/** {@link useMenuSubmenu} as a render-callback component. */\nexport function MenuSubmenu<ParentValue, Value = unknown>(\n props: MenuSubmenuProps<ParentValue, Value>,\n): FigNode {\n return props.children(useMenuSubmenu(props.parent, props.value, props));\n}\n\nfunction submenuOpenKey(target: EventTarget | null): string {\n return rightToLeft(target) ? \"ArrowLeft\" : \"ArrowRight\";\n}\n\nfunction submenuCloseKey(target: EventTarget | null): string {\n return rightToLeft(target) ? \"ArrowRight\" : \"ArrowLeft\";\n}\n\nfunction rightToLeft(target: EventTarget | null): boolean {\n if (!(target instanceof Element)) return false;\n const direction = getComputedStyle(target).direction;\n return direction\n ? direction === \"rtl\"\n : target.closest(\"[dir]\")?.getAttribute(\"dir\") === \"rtl\";\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import{t as e}from"./changes-DglRuvQa.js";import{i as t,n,o as r,s as i}from"./parts-I0jdefyp.js";import"./diagnostics-CoURcwqb.js";import{n as a,t as o}from"./composite-CiKLX-Ti.js";import{t as s}from"./part-reference-B7zINPUK.js";import{r as c}from"./popup-state-CFSmDEwK.js";import{n as l,r as u}from"./popover-UfJHN1h2.js";import{createMixin as d,useBeforePaint as f,useId as p,useMemo as m,useStableEvent as h}from"@bgub/fig";import{on as g}from"@bgub/fig-dom";function _(e,t){let n=!1,r,i=!1;function a(){let t=e();return t?.contains(t.ownerDocument.activeElement)===!0}return{closingFrom(e){r=e},beforeToggle:e=>{e.target===e.currentTarget&&(i=c(e)===!1&&a())},sync(e,o){if(e===n)return;if(n=e,e){r=void 0,i=!1,o();return}let s=t(),c=s?.ownerDocument,l=c?.activeElement;(a()||i&&l===c?.body||r!==void 0&&r===l)&&s?.focus(),r=void 0,i=!1}}}const v=d((e,t,r)=>({"aria-haspopup":`menu`,bind:n(e,t.registry,t.noteTrigger),id:e.props.id??t.triggerId,mix:g(`keydown`,e=>{if(!(e.defaultPrevented||e.isComposing||e.keyCode===229)){if(r.disabled){(e.key===`Enter`||e.key===` `)&&e.preventDefault();return}e.key===`Enter`||r.openWithArrows&&e.key===`ArrowDown`?(e.preventDefault(),t.open(`first`)):r.openWithArrows&&e.key===`ArrowUp`&&(e.preventDefault(),t.open(`last`))}})})),y=d((e,t)=>{let r=s(e,`aria-labelledby`,t.triggerId);return{...r.props,bind:n(e,t.registry,(e,n)=>t.registry.bindMenu(e,n,r)),mix:g(`keydown`,e=>{if(e.defaultPrevented||e.isComposing||e.keyCode===229)return;if(e.key===`Tab`){t.close();return}let n=t.registry.menuItemAt(e.target);if(n!==void 0&&(e.key===`Enter`||e.key===` `)){if(n.kind===`submenu`)return;e.preventDefault(),!n.disabled&&!(n.kind===`radio`&&n.checked)&&t.activate(n,e);return}if(n===void 0){if(e.target!==e.currentTarget||e.altKey||e.ctrlKey||e.metaKey||e.shiftKey)return;e.key===`ArrowDown`||e.key===`Home`?(e.preventDefault(),t.open(`first`)):e.key===`ArrowUp`||e.key===`End`?(e.preventDefault(),t.open(`last`)):t.registry.focusByTypeahead(e.key);return}t.registry.moveFocus(e,{loop:!0,orientation:`vertical`})===void 0&&!e.altKey&&!e.ctrlKey&&!e.metaKey&&t.registry.focusByTypeahead(e.key)}),role:`menu`,tabindex:e.props.tabindex??-1}}),b=d((e,t,n,r)=>[n,v(t,r)]),x=d((e,t,n)=>[g(`beforetoggle`,t.noteNativeToggle,{capture:!0}),n,y(t)]),S=d((e,r,i)=>{let a=i.disabled||e.props.disabled===!0;return{...t(e,{disabled:a}),"aria-checked":i.kind===`checkbox`||i.kind===`radio`?i.checked?`true`:`false`:void 0,bind:n(e,r.registry,(e,t)=>r.registry.bindMenuItem(e,t,{...i,disabled:a})),"data-checked":i.checked?``:void 0,mix:g(`click`,e=>{if(e.defaultPrevented)return;if(a){e.preventDefault();return}if(e.button!==0||i.kind===`submenu`)return;let t=r.registry.menuItemAt(e.currentTarget);t!==void 0&&!(t.kind===`radio`&&t.checked)&&r.activate(t,e)}),role:i.kind===`checkbox`?`menuitemcheckbox`:i.kind===`radio`?`menuitemradio`:`menuitem`,tabindex:-1}}),C=new WeakMap,w=new WeakSet;function T(e,t){C.set(e,t)}function E(e){let t=C.get(e);if(t===void 0)throw Error(`Fig Headless submenu parent must come from useMenu().`);return t}function D(e,t){t&&w.add(e)}function O(e){return w.has(e)}function k(){let e=o({container:`[role="menu"]`,item:`[role^="menuitem"]`,name:`menu`}),t=new WeakMap,n=r();function i(t,r,i){let a={...i,node:t};n.bind(t,r,a),e.bindItem(t,r,i.value,i.disabled)}function a(t){let r=e.itemAt(t);return r===void 0?void 0:n.get(r.node)}function s(n,r,i){t.set(n,i),e.bindContainer(n,r)}return{...e,bindMenu:s,bindMenuItem:i,menuItemAt:a,syncLabel:(e,n)=>t.get(e)?.sync(e,n)}}function A(t={}){let n=`${p()}-trigger`,r=l(t),o=m(()=>k(),[]),s=m(()=>i(()=>{}),[]),c=m(()=>({pending:null}),[]),d=m(()=>_(o.containerNode,s.node),[]),g=h((e,t)=>{u(r,e,t),s.bind(e,t)}),v=h((e,n,r)=>{t.onSelect?.(e,n,r)}),y=h(e=>{E(!0,e)}),C=h(e=>{d.closingFrom(e),r.setOpen(!1)}),w=h((t,n)=>{if(t.kind===`submenu`)return;let i={...e(n,t.node),kind:t.kind};D(i,t.closeOnSelect),v(t.value,i),!i.isCanceled&&t.closeOnSelect&&r.setOpen(!1)}),E=h((e,t=`first`)=>{e&&(r.open?(c.pending=null,O(t)):c.pending=t),r.setOpen(e)});f(()=>{let e=o.containerNode(),t=s.node();e!==null&&o.syncLabel(e,t?.id),d.sync(r.open,()=>{let e=c.pending;c.pending=null,O(e)})});function O(e){if(e===!1)return;let t=o.items().filter(a);((e===`last`?t[t.length-1]:t[0])?.node??o.containerNode())?.focus()}let A={noteNativeToggle:d.beforeToggle,activate:w,close:C,noteTrigger:g,open:y,registry:o,triggerId:n},j={checkboxItem:(e,t)=>S(A,{checked:t.checked,closeOnSelect:t.closeOnSelect===!0,disabled:t.disabled===!0,kind:`checkbox`,value:e}),item:(e,t={})=>S(A,{checked:void 0,closeOnSelect:t.closeOnSelect!==!1,disabled:t.disabled===!0,kind:`item`,value:e}),menu:()=>x(A,r.popover()),open:r.open,radioItem:(e,t)=>S(A,{checked:t.checked,closeOnSelect:t.closeOnSelect===!0,disabled:t.disabled===!0,kind:`radio`,value:e}),setOpen:e=>E(e),trigger:()=>b(A,r.trigger(),{openWithArrows:!0})};return T(j,{closeTree:C,bindTrigger:g,popup:o.containerNode,setOpen:E,submenuTrigger:(e,t)=>S(A,{checked:void 0,closeOnSelect:!1,disabled:t,kind:`submenu`,value:e}),trigger:(e,t=!1)=>b(A,r.trigger(),{openWithArrows:e,disabled:t})}),j}function j(e){return e.children(A(e))}export{T as a,E as i,A as n,O as r,j as t};
2
+ //# sourceMappingURL=menu-B4pKuF3z.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"menu-B4pKuF3z.js","names":[],"sources":["../src/internal/popup-focus.ts","../src/menu/parts.ts","../src/menu/controller.ts","../src/menu/registry.ts","../src/menu/menu.tsx"],"sourcesContent":["import { toggledOpen } from \"./anchored-popup.ts\";\n\n/** Focus ownership for popups that move DOM focus into their content. */\nexport function createPopupFocus(\n popup: () => HTMLElement | null,\n trigger: () => HTMLElement | null,\n) {\n let wasOpen = false;\n let closingFocus: Element | undefined;\n let nativeLoss = false;\n function containsFocus(): boolean {\n const node = popup();\n return node?.contains(node.ownerDocument.activeElement) === true;\n }\n return {\n closingFrom(origin?: Element): void {\n closingFocus = origin;\n },\n beforeToggle: (event: Event): void => {\n if (event.target === event.currentTarget) {\n nativeLoss = toggledOpen(event) === false && containsFocus();\n }\n },\n sync(open: boolean, initialFocus: () => void): void {\n if (open === wasOpen) return;\n wasOpen = open;\n if (open) {\n closingFocus = undefined;\n nativeLoss = false;\n initialFocus();\n return;\n }\n const node = trigger();\n const document = node?.ownerDocument;\n const active = document?.activeElement;\n // Firefox can clear focus during native dismissal. Never override an\n // intentional move to another control, including an onSelect callback.\n if (\n containsFocus() ||\n (nativeLoss && active === document?.body) ||\n (closingFocus !== undefined && closingFocus === active)\n )\n node?.focus();\n closingFocus = undefined;\n nativeLoss = false;\n },\n };\n}\n","import { createPartReference } from \"../internal/part-reference.ts\";\nimport {\n createMixin,\n type MixinContext,\n type MixinDescriptor,\n} from \"@bgub/fig\";\nimport { on } from \"@bgub/fig-dom\";\nimport { bindPart, triggerProps } from \"../internal/parts.ts\";\nimport type {\n MenuItemConfig,\n MenuItemRegistration,\n MenuRegistry,\n} from \"./registry.ts\";\n\ninterface MenuTriggerOwnState {\n readonly disabled?: boolean;\n readonly openWithArrows: boolean;\n}\n\n/** Widget-level state every part reads. Built once per root render. */\nexport interface MenuPartState {\n readonly noteNativeToggle: (event: Event) => void;\n readonly activate: (item: MenuItemRegistration, event: Event) => void;\n readonly close: () => void;\n readonly noteTrigger: (node: HTMLElement, signal: AbortSignal) => void;\n readonly open: (at: \"first\" | \"last\") => void;\n readonly registry: MenuRegistry;\n readonly triggerId: string;\n}\n\nconst menuTriggerBehavior = /* @__PURE__ */ createMixin(\n (context: MixinContext, state: MenuPartState, own: MenuTriggerOwnState) => ({\n \"aria-haspopup\": \"menu\",\n bind: bindPart(context, state.registry, state.noteTrigger),\n id: context.props.id ?? state.triggerId,\n // Click already opens through popovertarget; these are the keys the\n // pattern adds, and each one says where focus should land.\n mix: on(\"keydown\", (event) => {\n if (event.defaultPrevented || event.isComposing || event.keyCode === 229)\n return;\n if (own.disabled) {\n if (event.key === \"Enter\" || event.key === \" \") event.preventDefault();\n return;\n }\n if (\n event.key === \"Enter\" ||\n (own.openWithArrows && event.key === \"ArrowDown\")\n ) {\n event.preventDefault();\n state.open(\"first\");\n } else if (own.openWithArrows && event.key === \"ArrowUp\") {\n event.preventDefault();\n state.open(\"last\");\n }\n }),\n }),\n);\n\nconst menuBehavior = /* @__PURE__ */ createMixin(\n (context: MixinContext, state: MenuPartState) => {\n const label = createPartReference(\n context,\n \"aria-labelledby\",\n state.triggerId,\n );\n return {\n ...label.props,\n bind: bindPart(context, state.registry, (node, signal) =>\n state.registry.bindMenu(node, signal, label),\n ),\n mix: on(\"keydown\", (event) => {\n if (\n event.defaultPrevented ||\n event.isComposing ||\n event.keyCode === 229\n )\n return;\n // Escape and light dismiss belong to the platform; everything that\n // moves or commits inside the menu belongs here.\n if (event.key === \"Tab\") {\n state.close();\n return;\n }\n const item = state.registry.menuItemAt(event.target);\n if (\n item !== undefined &&\n (event.key === \"Enter\" || event.key === \" \")\n ) {\n if (item.kind === \"submenu\") return;\n event.preventDefault();\n if (!item.disabled && !(item.kind === \"radio\" && item.checked)) {\n state.activate(item, event);\n }\n return;\n }\n if (item === undefined) {\n // Empty/loading menus focus the container. Once items arrive, a\n // navigation key enters the list without requiring a reopen.\n if (\n event.target !== event.currentTarget ||\n event.altKey ||\n event.ctrlKey ||\n event.metaKey ||\n event.shiftKey\n )\n return;\n if (event.key === \"ArrowDown\" || event.key === \"Home\") {\n event.preventDefault();\n state.open(\"first\");\n } else if (event.key === \"ArrowUp\" || event.key === \"End\") {\n event.preventDefault();\n state.open(\"last\");\n } else state.registry.focusByTypeahead(event.key);\n return;\n }\n const moved = state.registry.moveFocus(event, {\n loop: true,\n orientation: \"vertical\",\n });\n if (\n moved === undefined &&\n !event.altKey &&\n !event.ctrlKey &&\n !event.metaKey\n ) {\n state.registry.focusByTypeahead(event.key);\n }\n }),\n role: \"menu\",\n tabindex: context.props.tabindex ?? -1,\n };\n },\n);\n\n/**\n * Layers menu semantics over the popover parts the menu is built on. Both are\n * descriptors, so they compose the way any two authored mixins would.\n */\nexport const menuTriggerMixin = /* @__PURE__ */ createMixin(\n (\n _context: MixinContext,\n state: MenuPartState,\n popoverTrigger: MixinDescriptor,\n own: MenuTriggerOwnState,\n ) => [popoverTrigger, menuTriggerBehavior(state, own)],\n);\n\nexport const menuMixin = /* @__PURE__ */ createMixin(\n (\n _context: MixinContext,\n state: MenuPartState,\n popoverMenu: MixinDescriptor,\n ) => [\n on(\"beforetoggle\", state.noteNativeToggle, { capture: true }),\n popoverMenu,\n menuBehavior(state),\n ],\n);\n\nexport const menuItemMixin = /* @__PURE__ */ createMixin(\n (context: MixinContext, state: MenuPartState, own: MenuItemConfig) => {\n const disabled = own.disabled || context.props.disabled === true;\n return {\n ...triggerProps(context, { disabled }),\n \"aria-checked\":\n own.kind === \"checkbox\" || own.kind === \"radio\"\n ? own.checked\n ? \"true\"\n : \"false\"\n : undefined,\n bind: bindPart(context, state.registry, (node, signal) =>\n state.registry.bindMenuItem(node, signal, { ...own, disabled }),\n ),\n \"data-checked\": own.checked ? \"\" : undefined,\n mix: on(\"click\", (event) => {\n if (event.defaultPrevented) return;\n if (disabled) {\n event.preventDefault();\n return;\n }\n if (event.button !== 0 || own.kind === \"submenu\") return;\n const item = state.registry.menuItemAt(event.currentTarget);\n if (item !== undefined && !(item.kind === \"radio\" && item.checked)) {\n state.activate(item, event);\n }\n }),\n role:\n own.kind === \"checkbox\"\n ? \"menuitemcheckbox\"\n : own.kind === \"radio\"\n ? \"menuitemradio\"\n : \"menuitem\",\n // Focus moves between items directly, so none of them is a tab stop:\n // Tab leaves the menu rather than walking it.\n tabindex: -1,\n };\n },\n);\n","import type { MixinDescriptor } from \"@bgub/fig\";\n\nexport type MenuFocusTarget = \"first\" | \"last\" | false;\n\nexport interface MenuController {\n closeTree(focusOrigin?: Element): void;\n bindTrigger(node: HTMLElement, signal: AbortSignal): void;\n popup(): HTMLElement | null;\n setOpen(open: boolean, focus?: MenuFocusTarget): void;\n submenuTrigger(value: unknown, disabled: boolean): MixinDescriptor;\n trigger(openWithArrows: boolean, disabled?: boolean): MixinDescriptor;\n}\n\nconst controllers = new WeakMap<object, MenuController>();\nconst closingSelections = new WeakSet<object>();\n\nexport function registerMenuController(\n parts: object,\n controller: MenuController,\n): void {\n controllers.set(parts, controller);\n}\n\nexport function menuController(parts: object): MenuController {\n const controller = controllers.get(parts);\n if (controller === undefined) {\n throw new Error(\"Fig Headless submenu parent must come from useMenu().\");\n }\n return controller;\n}\n\nexport function markMenuClose(details: object, close: boolean): void {\n if (close) closingSelections.add(details);\n}\n\nexport function menuClosesOnSelect(details: object): boolean {\n return closingSelections.has(details);\n}\n","import type { PartReference } from \"../internal/part-reference.ts\";\nimport { createPartCollection } from \"../internal/registration.ts\";\nimport { createComposite } from \"../internal/composite.ts\";\n\nexport type MenuItemKind = \"checkbox\" | \"item\" | \"radio\" | \"submenu\";\n\nexport interface MenuItemConfig {\n readonly checked: boolean | undefined;\n readonly closeOnSelect: boolean;\n readonly disabled: boolean;\n readonly kind: MenuItemKind;\n readonly value: unknown;\n}\n\nexport interface MenuItemRegistration extends MenuItemConfig {\n readonly node: HTMLElement;\n}\n\nexport type MenuRegistry = ReturnType<typeof createMenuRegistry>;\n\n/** Adds menu activation metadata to the shared focus composite. */\nexport function createMenuRegistry() {\n const composite = createComposite({\n container: '[role=\"menu\"]',\n item: '[role^=\"menuitem\"]',\n name: \"menu\",\n });\n const labels = new WeakMap<HTMLElement, PartReference>();\n const registrations = createPartCollection<MenuItemRegistration>();\n\n function bindMenuItem(\n node: HTMLElement,\n signal: AbortSignal,\n config: MenuItemConfig,\n ): void {\n const registration = { ...config, node };\n registrations.bind(node, signal, registration);\n composite.bindItem(node, signal, config.value, config.disabled);\n }\n\n function menuItemAt(target: EventTarget | null) {\n const item = composite.itemAt(target);\n return item === undefined ? undefined : registrations.get(item.node);\n }\n\n function bindMenu(\n node: HTMLElement,\n signal: AbortSignal,\n label: PartReference,\n ): void {\n labels.set(node, label);\n composite.bindContainer(node, signal);\n }\n\n return {\n ...composite,\n bindMenu,\n bindMenuItem,\n menuItemAt,\n syncLabel: (node: HTMLElement, id: string | undefined) =>\n labels.get(node)?.sync(node, id),\n };\n}\n","import {\n type FigNode,\n type MixinDescriptor,\n useBeforePaint,\n useId,\n useMemo,\n useStableEvent,\n} from \"@bgub/fig\";\nimport type { ChangeDetails } from \"../internal/changes.ts\";\nimport { createChangeDetails } from \"../internal/changes.ts\";\nimport { createPopupFocus } from \"../internal/popup-focus.ts\";\nimport { isNativeEnabled } from \"../internal/composite.ts\";\nimport { assertAccessibleName } from \"../internal/diagnostics.ts\";\nimport { createPartSlot } from \"../internal/registration.ts\";\nimport type {\n OpenChangeDetails,\n OpenChangeHandler,\n} from \"../internal/open-state.ts\";\nimport { type PopoverOptions, usePopover } from \"../popover/popover.tsx\";\nimport { bindPopoverSource } from \"../popover/registry.ts\";\nimport { menuItemMixin, menuMixin, menuTriggerMixin } from \"./parts.ts\";\nimport {\n markMenuClose,\n registerMenuController,\n type MenuFocusTarget,\n} from \"./controller.ts\";\nimport { createMenuRegistry, type MenuItemRegistration } from \"./registry.ts\";\n\nexport type MenuOpenChangeDetails = OpenChangeDetails;\n\nexport type MenuOpenChangeHandler = OpenChangeHandler;\n\nexport interface MenuSelectDetails extends ChangeDetails {\n readonly kind: \"checkbox\" | \"item\" | \"radio\";\n}\n\nexport type MenuSelectHandler<Value = unknown> = (\n value: Value,\n details: MenuSelectDetails,\n signal: AbortSignal,\n) => void;\n\nexport interface MenuItemOptions {\n /** Defaults to `true` for actions and `false` for checked items. */\n closeOnSelect?: boolean;\n disabled?: boolean;\n}\n\nexport interface MenuCheckedItemOptions extends MenuItemOptions {\n checked: boolean;\n}\n\nexport interface MenuOptions<Value = unknown> extends PopoverOptions {\n /** Runs when an item is chosen, before the menu closes. */\n onSelect?: MenuSelectHandler<Value>;\n}\n\nexport interface MenuParts<Value = unknown> {\n readonly open: boolean;\n checkboxItem(value: Value, options: MenuCheckedItemOptions): MixinDescriptor;\n /** Applies to the caller's own popover element. */\n menu(): MixinDescriptor;\n item(value: Value, options?: MenuItemOptions): MixinDescriptor;\n radioItem(value: Value, options: MenuCheckedItemOptions): MixinDescriptor;\n setOpen(open: boolean): void;\n trigger(): MixinDescriptor;\n}\n\nexport interface MenuProps<Value = unknown> extends MenuOptions<Value> {\n children: (menu: MenuParts<Value>) => FigNode;\n}\n\n/**\n * Coordinates one menu button.\n *\n * The popover underneath supplies the top layer, light dismiss, and Escape,\n * and the composite supplies ordering and movement. What the menu itself owns\n * is focus: `showPopover()` deliberately leaves focus where it was, so the\n * menu moves it to an item on open and returns it to the trigger on close.\n */\nexport function useMenu<Value = unknown>(\n options: MenuOptions<Value> = {},\n): MenuParts<Value> {\n const id = useId();\n const triggerId = `${id}-trigger`;\n const popover = usePopover(options);\n const registry = useMemo(() => createMenuRegistry(), []);\n const trigger = useMemo(() => createPartSlot(() => {}), []);\n const tracker = useMemo<{ pending: MenuFocusTarget | null }>(\n () => ({ pending: null }),\n [],\n );\n const focus = useMemo(\n () => createPopupFocus(registry.containerNode, trigger.node),\n [],\n );\n\n const noteTrigger = useStableEvent(\n (node: HTMLElement, signal: AbortSignal) => {\n bindPopoverSource(popover, node, signal);\n trigger.bind(node, signal);\n },\n );\n\n const emitSelect = useStableEvent(\n (value: unknown, details: MenuSelectDetails, signal: AbortSignal) => {\n options.onSelect?.(value as Value, details, signal);\n },\n );\n\n const open = useStableEvent((at: \"first\" | \"last\") => {\n setOpen(true, at);\n });\n\n const close = useStableEvent((focusOrigin?: Element) => {\n focus.closingFrom(focusOrigin);\n popover.setOpen(false);\n });\n\n const activate = useStableEvent(\n (item: MenuItemRegistration, event: Event) => {\n if (item.kind === \"submenu\") return;\n const details: MenuSelectDetails = {\n ...createChangeDetails(event, item.node),\n kind: item.kind,\n };\n markMenuClose(details, item.closeOnSelect);\n emitSelect(item.value, details);\n if (details.isCanceled) return;\n if (item.closeOnSelect) popover.setOpen(false);\n },\n );\n\n const setOpen = useStableEvent(\n (next: boolean, focus: MenuFocusTarget = \"first\") => {\n if (next) {\n if (popover.open) {\n tracker.pending = null;\n focusItem(focus);\n } else tracker.pending = focus;\n }\n popover.setOpen(next);\n },\n );\n\n // Focus is the menu's own work. It runs after the popover's own pass, which\n // is what puts the element in the top layer, so the items are reachable.\n useBeforePaint(() => {\n const menu = registry.containerNode();\n const triggerNode = trigger.node();\n if (menu !== null) {\n registry.syncLabel(menu, triggerNode?.id);\n assertAccessibleName(menu, \"menu\");\n }\n focus.sync(popover.open, () => {\n const pending = tracker.pending;\n tracker.pending = null;\n focusItem(pending);\n });\n });\n\n function focusItem(focus: MenuFocusTarget | null): void {\n if (focus === false) return;\n const items = registry.items().filter(isNativeEnabled);\n const target = focus === \"last\" ? items[items.length - 1] : items[0];\n (target?.node ?? registry.containerNode())?.focus();\n }\n\n const state = {\n noteNativeToggle: focus.beforeToggle,\n activate,\n close,\n noteTrigger,\n open,\n registry,\n triggerId,\n };\n\n const parts: MenuParts<Value> = {\n checkboxItem: (value, itemOptions) =>\n menuItemMixin(state, {\n checked: itemOptions.checked,\n closeOnSelect: itemOptions.closeOnSelect === true,\n disabled: itemOptions.disabled === true,\n kind: \"checkbox\",\n value,\n }),\n item: (value, itemOptions = {}) =>\n menuItemMixin(state, {\n checked: undefined,\n closeOnSelect: itemOptions.closeOnSelect !== false,\n disabled: itemOptions.disabled === true,\n kind: \"item\",\n value,\n }),\n menu: () => menuMixin(state, popover.popover()),\n open: popover.open,\n radioItem: (value, itemOptions) =>\n menuItemMixin(state, {\n checked: itemOptions.checked,\n closeOnSelect: itemOptions.closeOnSelect === true,\n disabled: itemOptions.disabled === true,\n kind: \"radio\",\n value,\n }),\n setOpen: (next) => setOpen(next),\n trigger: () =>\n menuTriggerMixin(state, popover.trigger(), {\n openWithArrows: true,\n }),\n };\n registerMenuController(parts, {\n closeTree: close,\n bindTrigger: noteTrigger,\n popup: registry.containerNode,\n setOpen,\n submenuTrigger: (value, itemDisabled) =>\n menuItemMixin(state, {\n checked: undefined,\n closeOnSelect: false,\n disabled: itemDisabled,\n kind: \"submenu\",\n value,\n }),\n trigger: (openWithArrows, disabled = false) =>\n menuTriggerMixin(state, popover.trigger(), { openWithArrows, disabled }),\n });\n return parts;\n}\n\n/**\n * {@link useMenu} as a component, for a menu that is not already a component\n * of its own. Opening re-renders this root rather than the caller.\n */\nexport function Menu<Value = unknown>(props: MenuProps<Value>): FigNode {\n return props.children(useMenu(props));\n}\n"],"mappings":"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"}
package/dist/menu.d.ts ADDED
@@ -0,0 +1,52 @@
1
+ import { t as ChangeDetails } from "./changes--uvVfvy8.js";
2
+ import { n as OpenChangeHandler, t as OpenChangeDetails } from "./open-state-BmP__rp4.js";
3
+ import { i as PopoverOptions } from "./popover-CSPJNmHy.js";
4
+ import { FigNode, MixinDescriptor } from "@bgub/fig";
5
+ //#region src/menu/menu.d.ts
6
+ type MenuOpenChangeDetails = OpenChangeDetails;
7
+ type MenuOpenChangeHandler = OpenChangeHandler;
8
+ interface MenuSelectDetails extends ChangeDetails {
9
+ readonly kind: "checkbox" | "item" | "radio";
10
+ }
11
+ type MenuSelectHandler<Value = unknown> = (value: Value, details: MenuSelectDetails, signal: AbortSignal) => void;
12
+ interface MenuItemOptions {
13
+ /** Defaults to `true` for actions and `false` for checked items. */
14
+ closeOnSelect?: boolean;
15
+ disabled?: boolean;
16
+ }
17
+ interface MenuCheckedItemOptions extends MenuItemOptions {
18
+ checked: boolean;
19
+ }
20
+ interface MenuOptions<Value = unknown> extends PopoverOptions {
21
+ /** Runs when an item is chosen, before the menu closes. */
22
+ onSelect?: MenuSelectHandler<Value>;
23
+ }
24
+ interface MenuParts<Value = unknown> {
25
+ readonly open: boolean;
26
+ checkboxItem(value: Value, options: MenuCheckedItemOptions): MixinDescriptor;
27
+ /** Applies to the caller's own popover element. */
28
+ menu(): MixinDescriptor;
29
+ item(value: Value, options?: MenuItemOptions): MixinDescriptor;
30
+ radioItem(value: Value, options: MenuCheckedItemOptions): MixinDescriptor;
31
+ setOpen(open: boolean): void;
32
+ trigger(): MixinDescriptor;
33
+ }
34
+ interface MenuProps<Value = unknown> extends MenuOptions<Value> {
35
+ children: (menu: MenuParts<Value>) => FigNode;
36
+ }
37
+ /**
38
+ * Coordinates one menu button.
39
+ *
40
+ * The popover underneath supplies the top layer, light dismiss, and Escape,
41
+ * and the composite supplies ordering and movement. What the menu itself owns
42
+ * is focus: `showPopover()` deliberately leaves focus where it was, so the
43
+ * menu moves it to an item on open and returns it to the trigger on close.
44
+ */
45
+ declare function useMenu<Value = unknown>(options?: MenuOptions<Value>): MenuParts<Value>;
46
+ /**
47
+ * {@link useMenu} as a component, for a menu that is not already a component
48
+ * of its own. Opening re-renders this root rather than the caller.
49
+ */
50
+ declare function Menu<Value = unknown>(props: MenuProps<Value>): FigNode;
51
+ //#endregion
52
+ export { Menu, MenuCheckedItemOptions, MenuItemOptions, MenuOpenChangeDetails, MenuOpenChangeHandler, MenuOptions, MenuParts, MenuProps, MenuSelectDetails, MenuSelectHandler, useMenu };
package/dist/menu.js ADDED
@@ -0,0 +1 @@
1
+ import{n as e,t}from"./menu-B4pKuF3z.js";export{t as Menu,e as useMenu};
@@ -0,0 +1,6 @@
1
+ import { t as ChangeDetails } from "./changes--uvVfvy8.js";
2
+ //#region src/internal/open-state.d.ts
3
+ type OpenChangeDetails = ChangeDetails;
4
+ type OpenChangeHandler = (open: boolean, details: OpenChangeDetails, signal: AbortSignal) => void;
5
+ //#endregion
6
+ export { OpenChangeHandler as n, OpenChangeDetails as t };
@@ -0,0 +1,2 @@
1
+ import{t as e}from"./changes-DglRuvQa.js";import{n as t}from"./controllable-value-CDbRzlId.js";import{useStableEvent as n}from"@bgub/fig";function r(r){let i=t({value:r.open,defaultValue:r.defaultOpen===!0,onChange:r.onOpenChange,reconcile:r.requestReconcile}),a=n((t,n,r)=>i.request(()=>t,e(n,r))),o=n(t=>i.request(()=>t,e(null)));return{open:i.value,requestOpen:a,setOpen:o}}export{r as t};
2
+ //# sourceMappingURL=open-state-G79p9WiE.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"open-state-G79p9WiE.js","names":[],"sources":["../src/internal/open-state.ts"],"sourcesContent":["import { useStableEvent } from \"@bgub/fig\";\nimport { type ChangeDetails, createChangeDetails } from \"./changes.ts\";\nimport { useControllableValue } from \"./controllable-value.ts\";\n\nexport type OpenChangeDetails = ChangeDetails;\nexport type OpenChangeHandler = (\n open: boolean,\n details: OpenChangeDetails,\n signal: AbortSignal,\n) => void;\nexport interface OpenStateOptions {\n defaultOpen?: boolean;\n onOpenChange?: OpenChangeHandler;\n open?: boolean;\n requestReconcile: () => void;\n}\n\n/** Widget open intent; browser observation belongs to the popup adapter. */\nexport function useOpenState(options: OpenStateOptions) {\n const state = useControllableValue({\n value: options.open,\n defaultValue: options.defaultOpen === true,\n onChange: options.onOpenChange,\n reconcile: options.requestReconcile,\n });\n const requestOpen = useStableEvent(\n (next: boolean, event: Event, trigger: Element | undefined) =>\n state.request(() => next, createChangeDetails(event, trigger)),\n );\n const setOpen = useStableEvent((next: boolean) =>\n state.request(() => next, createChangeDetails(null)),\n );\n return { open: state.value, requestOpen, setOpen };\n}\n"],"mappings":"0IAkBA,SAAgB,EAAa,EAA2B,CACtD,IAAM,EAAQ,EAAqB,CACjC,MAAO,EAAQ,KACf,aAAc,EAAQ,cAAgB,GACtC,SAAU,EAAQ,aAClB,UAAW,EAAQ,gBACrB,CAAC,EACK,EAAc,GACjB,EAAe,EAAc,IAC5B,EAAM,YAAc,EAAM,EAAoB,EAAO,CAAO,CAAC,CACjE,EACM,EAAU,EAAgB,GAC9B,EAAM,YAAc,EAAM,EAAoB,IAAI,CAAC,CACrD,EACA,MAAO,CAAE,KAAM,EAAM,MAAO,cAAa,SAAQ,CACnD"}
@@ -0,0 +1,2 @@
1
+ import{r as e}from"./parts-I0jdefyp.js";function t(t,n,r){let i=()=>n===`aria-labelledby`&&t.props[`aria-label`]!==void 0,a=t.props[n]==null&&!i(),o=t.props[n]??(a?r:void 0);return{props:{[n]:o},sync(r,o){return!a||!t.owns(n)?!1:i()?(e(r,n,void 0),!1):(e(r,n,o),!0)}}}export{t};
2
+ //# sourceMappingURL=part-reference-B7zINPUK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"part-reference-B7zINPUK.js","names":[],"sources":["../src/internal/part-reference.ts"],"sourcesContent":["import type { MixinContext } from \"@bgub/fig\";\nimport { setIdReference } from \"./parts.ts\";\n\ntype ReferenceAttribute = \"aria-labelledby\" | \"aria-describedby\" | \"for\";\n\n/**\n * A relationship owns only its generated default. Resolve the final mixin\n * props at commit so a later author override wins; a later aria-label also\n * retires our generated aria-labelledby instead of leaving it to take priority.\n */\nexport function createPartReference(\n context: MixinContext,\n attribute: ReferenceAttribute,\n generated?: string,\n) {\n const blocked = () =>\n attribute === \"aria-labelledby\" &&\n context.props[\"aria-label\"] !== undefined;\n const automatic = context.props[attribute] == null && !blocked();\n const value = context.props[attribute] ?? (automatic ? generated : undefined);\n return {\n props: { [attribute]: value },\n sync(node: HTMLElement, target: string | undefined): boolean {\n if (!automatic || !context.owns(attribute)) return false;\n if (blocked()) {\n setIdReference(node, attribute, undefined);\n return false;\n }\n setIdReference(node, attribute, target);\n return true;\n },\n };\n}\n\nexport type PartReference = ReturnType<typeof createPartReference>;\n"],"mappings":"wCAUA,SAAgB,EACd,EACA,EACA,EACA,CACA,IAAM,MACJ,IAAc,mBACd,EAAQ,MAAM,gBAAkB,IAAA,GAC5B,EAAY,EAAQ,MAAM,IAAc,MAAQ,CAAC,EAAQ,EACzD,EAAQ,EAAQ,MAAM,KAAe,EAAY,EAAY,IAAA,IACnE,MAAO,CACL,MAAO,EAAG,GAAY,CAAM,EAC5B,KAAK,EAAmB,EAAqC,CAO3D,MANI,CAAC,GAAa,CAAC,EAAQ,KAAK,CAAS,EAAU,GAC/C,EAAQ,GACV,EAAe,EAAM,EAAW,IAAA,EAAS,EAClC,KAET,EAAe,EAAM,EAAW,CAAM,EAC/B,GACT,CACF,CACF"}
@@ -0,0 +1,2 @@
1
+ import{useBeforeLayout as e,useBeforePaint as t,useMemo as n,useStableEvent as r,useState as i}from"@bgub/fig";import{hostBinding as a}from"@bgub/fig-dom";function o(e=()=>{}){let t=new Map;function n(n,r,i){let a=t.get(n);if(a?.signal===r)return a.value=i,e(),!1;let o={node:n,signal:r,value:i};return t.set(n,o),e(),r.addEventListener(`abort`,()=>{t.get(n)===o&&(t.delete(n),e())},{once:!0}),!0}return{bind:n,get:e=>t.get(e)?.value,items:()=>[...t.values()]}}function s(e){let t=null;function n(n,r){if(t?.node===n&&t.signal===r)return e(),!1;let i={node:n,signal:r};return t=i,e(),r.addEventListener(`abort`,()=>{t===i&&(t=null,e())},{once:!0}),!0}return{bind:n,node:()=>t?.node??null}}function c(){let a=n(()=>({reconciled:!1}),[]);e(()=>{a.reconciled=!1});let[,o]=i(0);return t(()=>{a.reconciled=!0}),r(e=>{e.aborted||!a.reconciled||(a.reconciled=!1,o(e=>e+1))})}function l(e,t,n){return[e.props.bind,a(e,t,(e,t)=>{e instanceof HTMLElement&&n(e,t)})]}function u(e,t){return n=>{if(n.defaultPrevented)return;let r=e.itemAt(n.target);if(r!==void 0){if(r.disabled){n.preventDefault();return}n.button===0&&t(r,n)}}}function d(e,t){return{"aria-disabled":t.disabled?`true`:void 0,"data-disabled":t.disabled?``:void 0,disabled:e.props.disabled,id:e.props.id??t.id,type:e.type===`button`?e.props.type??`button`:void 0}}function f(e,t,n){n===void 0||n===``?e.removeAttribute(t):e.setAttribute(t,n)}export{c as a,d as i,l as n,o,f as r,s,u as t};
2
+ //# sourceMappingURL=parts-I0jdefyp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parts-I0jdefyp.js","names":[],"sources":["../src/internal/registration.ts","../src/internal/reconcile.ts","../src/internal/parts.ts"],"sourcesContent":["export interface PartRegistration<Value> {\n readonly node: HTMLElement;\n readonly value: Value;\n}\n\n/** Registration data changes without ending the committed host lifetime. */\nexport function createPartCollection<Value>(changed: () => void = () => {}) {\n const entries = new Map<\n HTMLElement,\n { node: HTMLElement; value: Value; signal: AbortSignal }\n >();\n function bind(node: HTMLElement, signal: AbortSignal, value: Value): boolean {\n const previous = entries.get(node);\n if (previous?.signal === signal) {\n previous.value = value;\n // Host props such as id/form can change even when metadata is equal.\n changed();\n return false;\n }\n const entry = { node, signal, value };\n entries.set(node, entry);\n changed();\n signal.addEventListener(\n \"abort\",\n () => {\n if (entries.get(node) !== entry) return;\n entries.delete(node);\n changed();\n },\n { once: true },\n );\n return true;\n }\n return {\n bind,\n get: (node: HTMLElement): Value | undefined => entries.get(node)?.value,\n items: (): readonly PartRegistration<Value>[] => [...entries.values()],\n };\n}\n\n/** One role, with cleanup guarded by lifetime identity, not node identity. */\nexport function createPartSlot(changed: () => void) {\n let current: { node: HTMLElement; signal: AbortSignal } | null = null;\n function bind(node: HTMLElement, signal: AbortSignal): boolean {\n if (current?.node === node && current.signal === signal) {\n changed();\n return false;\n }\n const registration = { node, signal };\n current = registration;\n changed();\n signal.addEventListener(\n \"abort\",\n () => {\n if (current !== registration) return;\n current = null;\n changed();\n },\n { once: true },\n );\n return true;\n }\n return { bind, node: () => current?.node ?? null };\n}\n","import {\n useBeforePaint,\n useBeforeLayout,\n useMemo,\n useStableEvent,\n useState,\n} from \"@bgub/fig\";\n\n/**\n * Lets a widget root notice the DOM moving without it rendering.\n *\n * Two things cause that: a descendant component owning part of the markup, so\n * parts mount and unmount on their own, and a host element that changes itself\n * — a `<dialog>` closing on Escape. Either way the root has to render for its\n * before-paint pass to reconcile against the committed DOM. The guard matters:\n * part configurations update during a commit, and those registrations are\n * already covered by that commit’s before-paint pass.\n */\nexport function useRegistrationReconcile(): () => void {\n const tracker = useMemo(() => ({ reconciled: false }), []);\n // Binds run after this render, so they belong to it and need no extra work.\n useBeforeLayout(() => {\n tracker.reconciled = false;\n });\n const [, requestReconcile] = useState(0);\n\n useBeforePaint(() => {\n tracker.reconciled = true;\n });\n\n return useStableEvent((signal: AbortSignal) => {\n if (signal.aborted || !tracker.reconciled) return;\n tracker.reconciled = false;\n requestReconcile((epoch) => epoch + 1);\n });\n}\n","import type { MixinContext } from \"@bgub/fig\";\nimport { type Binding, hostBinding } from \"@bgub/fig-dom\";\nimport { type Composite, type CompositeItem } from \"./composite.ts\";\n\n/** Composes a widget's element binding after any the caller authored. */\nexport function bindPart(\n context: MixinContext,\n owner: object,\n bind: (node: HTMLElement, signal: AbortSignal) => void,\n): Binding {\n return [\n context.props.bind,\n hostBinding(context, owner, (node, signal) => {\n if (node instanceof HTMLElement) bind(node, signal);\n }),\n ];\n}\n\n/**\n * How every widget reacts to a click on one of its items: targets the\n * container does not own are ignored, a disabled item cancels the default\n * action instead of activating, and only the primary button activates.\n */\nexport function activateOnClick(\n registry: Composite,\n activate: (item: CompositeItem, event: MouseEvent) => void,\n): (event: MouseEvent) => void {\n return (event) => {\n if (event.defaultPrevented) return;\n const item = registry.itemAt(event.target);\n if (item === undefined) return;\n if (item.disabled) {\n event.preventDefault();\n return;\n }\n if (event.button === 0) activate(item, event);\n };\n}\n\n/**\n * The host props every activating item shares. Disabled items stay focusable\n * so their state stays discoverable: explicit widget disability uses ARIA\n * instead of native `disabled`. Otherwise preserve the caller's native prop.\n */\nexport function triggerProps(\n context: MixinContext,\n options: { readonly disabled: boolean; readonly id?: string },\n) {\n return {\n \"aria-disabled\": options.disabled ? \"true\" : undefined,\n \"data-disabled\": options.disabled ? \"\" : undefined,\n disabled: context.props.disabled,\n id: context.props.id ?? options.id,\n type:\n context.type === \"button\" ? (context.props.type ?? \"button\") : undefined,\n };\n}\n\n/** Points an ARIA relationship at an id, or removes it when there is none. */\nexport function setIdReference(\n node: HTMLElement,\n name: string,\n id: string | undefined,\n): void {\n if (id === undefined || id === \"\") node.removeAttribute(name);\n else node.setAttribute(name, id);\n}\n"],"mappings":"2JAMA,SAAgB,EAA4B,MAA4B,CAAC,EAAG,CAC1E,IAAM,EAAU,IAAI,IAIpB,SAAS,EAAK,EAAmB,EAAqB,EAAuB,CAC3E,IAAM,EAAW,EAAQ,IAAI,CAAI,EACjC,GAAI,GAAU,SAAW,EAIvB,MAHA,GAAS,MAAQ,EAEjB,EAAQ,EACD,GAET,IAAM,EAAQ,CAAE,OAAM,SAAQ,OAAM,EAYpC,OAXA,EAAQ,IAAI,EAAM,CAAK,EACvB,EAAQ,EACR,EAAO,iBACL,YACM,CACA,EAAQ,IAAI,CAAI,IAAM,IAC1B,EAAQ,OAAO,CAAI,EACnB,EAAQ,EACV,EACA,CAAE,KAAM,EAAK,CACf,EACO,EACT,CACA,MAAO,CACL,OACA,IAAM,GAAyC,EAAQ,IAAI,CAAI,CAAC,EAAE,MAClE,UAAiD,CAAC,GAAG,EAAQ,OAAO,CAAC,CACvE,CACF,CAGA,SAAgB,EAAe,EAAqB,CAClD,IAAI,EAA6D,KACjE,SAAS,EAAK,EAAmB,EAA8B,CAC7D,GAAI,GAAS,OAAS,GAAQ,EAAQ,SAAW,EAE/C,OADA,EAAQ,EACD,GAET,IAAM,EAAe,CAAE,OAAM,QAAO,EAYpC,MAXA,GAAU,EACV,EAAQ,EACR,EAAO,iBACL,YACM,CACA,IAAY,IAChB,EAAU,KACV,EAAQ,EACV,EACA,CAAE,KAAM,EAAK,CACf,EACO,EACT,CACA,MAAO,CAAE,OAAM,SAAY,GAAS,MAAQ,IAAK,CACnD,CC7CA,SAAgB,GAAuC,CACrD,IAAM,EAAU,OAAe,CAAE,WAAY,EAAM,GAAI,CAAC,CAAC,EAEzD,MAAsB,CACpB,EAAQ,WAAa,EACvB,CAAC,EACD,GAAM,EAAG,GAAoB,EAAS,CAAC,EAMvC,OAJA,MAAqB,CACnB,EAAQ,WAAa,EACvB,CAAC,EAEM,EAAgB,GAAwB,CACzC,EAAO,SAAW,CAAC,EAAQ,aAC/B,EAAQ,WAAa,GACrB,EAAkB,GAAU,EAAQ,CAAC,EACvC,CAAC,CACH,CC9BA,SAAgB,EACd,EACA,EACA,EACS,CACT,MAAO,CACL,EAAQ,MAAM,KACd,EAAY,EAAS,GAAQ,EAAM,IAAW,CACxC,aAAgB,aAAa,EAAK,EAAM,CAAM,CACpD,CAAC,CACH,CACF,CAOA,SAAgB,EACd,EACA,EAC6B,CAC7B,MAAQ,IAAU,CAChB,GAAI,EAAM,iBAAkB,OAC5B,IAAM,EAAO,EAAS,OAAO,EAAM,MAAM,EACrC,OAAS,IAAA,GACb,IAAI,EAAK,SAAU,CACjB,EAAM,eAAe,EACrB,MACF,CACI,EAAM,SAAW,GAAG,EAAS,EAAM,CAAK,CAD5C,CAEF,CACF,CAOA,SAAgB,EACd,EACA,EACA,CACA,MAAO,CACL,gBAAiB,EAAQ,SAAW,OAAS,IAAA,GAC7C,gBAAiB,EAAQ,SAAW,GAAK,IAAA,GACzC,SAAU,EAAQ,MAAM,SACxB,GAAI,EAAQ,MAAM,IAAM,EAAQ,GAChC,KACE,EAAQ,OAAS,SAAY,EAAQ,MAAM,MAAQ,SAAY,IAAA,EACnE,CACF,CAGA,SAAgB,EACd,EACA,EACA,EACM,CACF,IAAO,IAAA,IAAa,IAAO,GAAI,EAAK,gBAAgB,CAAI,EACvD,EAAK,aAAa,EAAM,CAAE,CACjC"}
@@ -0,0 +1,37 @@
1
+ import { n as OpenChangeHandler, t as OpenChangeDetails } from "./open-state-BmP__rp4.js";
2
+ import { FigNode, MixinDescriptor } from "@bgub/fig";
3
+ //#region src/popover/popover.d.ts
4
+ type PopoverOpenChangeDetails = OpenChangeDetails;
5
+ type PopoverOpenChangeHandler = OpenChangeHandler;
6
+ interface PopoverOptions {
7
+ defaultOpen?: boolean;
8
+ /** The popover id emitted on the server and referenced by its trigger. */
9
+ id?: string;
10
+ onOpenChange?: PopoverOpenChangeHandler;
11
+ open?: boolean;
12
+ }
13
+ interface PopoverParts {
14
+ readonly open: boolean;
15
+ /** Applies to the caller's own popover element. */
16
+ popover(): MixinDescriptor;
17
+ /** Opens or closes without an activation event. */
18
+ setOpen(open: boolean): void;
19
+ trigger(): MixinDescriptor;
20
+ }
21
+ interface PopoverProps extends PopoverOptions {
22
+ children: (popover: PopoverParts) => FigNode;
23
+ }
24
+ /**
25
+ * Coordinates one popover around the native popover API, which owns the top
26
+ * layer, light dismiss, and Escape, and around CSS anchor positioning, which
27
+ * owns placement. The widget owns open state and the wiring between the two
28
+ * elements, and measures nothing.
29
+ */
30
+ declare function usePopover(options?: PopoverOptions): PopoverParts;
31
+ /**
32
+ * {@link usePopover} as a component, for a popover that is not already a
33
+ * component of its own. Opening re-renders this root rather than the caller.
34
+ */
35
+ declare function Popover(props: PopoverProps): FigNode;
36
+ //#endregion
37
+ export { PopoverParts as a, PopoverOptions as i, PopoverOpenChangeDetails as n, PopoverProps as o, PopoverOpenChangeHandler as r, usePopover as s, Popover as t };
@@ -0,0 +1,2 @@
1
+ import{a as e,i as t,n}from"./parts-I0jdefyp.js";import{l as r}from"./diagnostics-CoURcwqb.js";import{n as i,t as a}from"./popup-state-CFSmDEwK.js";import{createMixin as o,useBeforePaint as s,useId as c,useMemo as l}from"@bgub/fig";import{on as u}from"@bgub/fig-dom";const d=o((e,r)=>({...t(e,{disabled:!1}),"aria-controls":r.popoverId,"aria-expanded":r.open?`true`:`false`,bind:n(e,r.registry,r.registry.bindAnchor),"data-open":r.open?``:void 0,popovertarget:r.popoverId,mix:u(`click`,e=>{if(e.defaultPrevented||r.registry.supported())return;let t=e.currentTarget;r.requestOpen(!r.open,e,t instanceof Element?t:void 0)})})),f=o((e,t)=>(r(e,t.popoverId),{bind:n(e,t.registry,t.registry.bindPopup),"data-open":t.open?``:void 0,id:t.popoverId,mix:[u(`beforetoggle`,t.nativeToggle),u(`toggle`,t.nativeToggle)],popover:e.props.popover??`auto`})),p=new WeakMap;function m(e,t){p.set(e,t)}function h(e,t,n){let r=p.get(e);if(r===void 0)throw Error(`Fig Headless popover source must belong to usePopover().`);r(t,n)}function g(t={}){let n=e(),r=l(()=>i(n,`popover`),[]),{getOpen:o,open:u,requestOpen:p,setOpen:h,nativeToggle:g}=a({...t,requestReconcile:n}),_=c(),v=`--fig-popover-${_.replaceAll(/[^\w-]/g,`-`)}`,y=t.id??`${_}-popover`;s(()=>{r.sync(o(),v)});let b={open:u,popoverId:y,registry:r,requestOpen:p,nativeToggle:g},x={open:u,popover:()=>f(b),setOpen:h,trigger:()=>d(b)};return m(x,r.bindSource),x}function _(e){return e.children(g(e))}export{g as n,h as r,_ as t};
2
+ //# sourceMappingURL=popover-UfJHN1h2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"popover-UfJHN1h2.js","names":[],"sources":["../src/popover/parts.ts","../src/popover/registry.ts","../src/popover/popover.tsx"],"sourcesContent":["import { createMixin, type MixinContext } from \"@bgub/fig\";\nimport { on } from \"@bgub/fig-dom\";\nimport { expectHost, expectPopupId } from \"../internal/diagnostics.ts\";\nimport { bindPart, triggerProps } from \"../internal/parts.ts\";\nimport type { PopoverRegistry } from \"./registry.ts\";\n\n/** Widget-level state every part reads. Built once per root render. */\nexport interface PopoverPartState {\n readonly nativeToggle: (event: Event) => void;\n readonly open: boolean;\n readonly popoverId: string;\n readonly registry: PopoverRegistry;\n readonly requestOpen: (\n open: boolean,\n event: Event,\n trigger: Element | undefined,\n ) => boolean;\n}\n\nexport const popoverTriggerMixin = /* @__PURE__ */ createMixin(\n (context: MixinContext, state: PopoverPartState) => {\n expectHost(context, \"popover trigger\", \"button\");\n return {\n ...triggerProps(context, { disabled: false }),\n \"aria-controls\": state.popoverId,\n \"aria-expanded\": state.open ? \"true\" : \"false\",\n bind: bindPart(context, state.registry, state.registry.bindAnchor),\n \"data-open\": state.open ? \"\" : undefined,\n popovertarget: state.popoverId,\n // With popover support the browser toggles through popovertarget, which\n // works before hydration; without it the widget does the toggling.\n mix: on(\"click\", (event) => {\n if (event.defaultPrevented || state.registry.supported()) return;\n const trigger = event.currentTarget;\n state.requestOpen(\n !state.open,\n event,\n trigger instanceof Element ? trigger : undefined,\n );\n }),\n };\n },\n);\n\nexport const popoverMixin = /* @__PURE__ */ createMixin(\n (context: MixinContext, state: PopoverPartState) => {\n expectPopupId(context, state.popoverId);\n return {\n bind: bindPart(context, state.registry, state.registry.bindPopup),\n \"data-open\": state.open ? \"\" : undefined,\n id: state.popoverId,\n mix: [\n on(\"beforetoggle\", state.nativeToggle),\n on(\"toggle\", state.nativeToggle),\n ],\n popover: context.props.popover ?? \"auto\",\n };\n },\n);\n","import type { createAnchoredPopup } from \"../internal/anchored-popup.ts\";\n\nexport type PopoverRegistry = ReturnType<typeof createAnchoredPopup>;\n\n// Widget composition can supply a native invoker without claiming CSS anchor\n// placement or changing the public popover parts contract.\nconst sourceBindings = new WeakMap<object, PopoverRegistry[\"bindSource\"]>();\n\nexport function registerPopoverSource(\n parts: object,\n bind: PopoverRegistry[\"bindSource\"],\n): void {\n sourceBindings.set(parts, bind);\n}\n\nexport function bindPopoverSource(\n parts: object,\n node: HTMLElement,\n signal: AbortSignal,\n): void {\n const bind = sourceBindings.get(parts);\n if (bind === undefined)\n throw new Error(\"Fig Headless popover source must belong to usePopover().\");\n bind(node, signal);\n}\n","import {\n type FigNode,\n type MixinDescriptor,\n useBeforePaint,\n useId,\n useMemo,\n} from \"@bgub/fig\";\nimport type {\n OpenChangeDetails,\n OpenChangeHandler,\n} from \"../internal/open-state.ts\";\nimport { usePopupState } from \"../internal/popup-state.ts\";\nimport { createAnchoredPopup } from \"../internal/anchored-popup.ts\";\nimport { useRegistrationReconcile } from \"../internal/reconcile.ts\";\nimport { popoverMixin, popoverTriggerMixin } from \"./parts.ts\";\nimport { registerPopoverSource } from \"./registry.ts\";\n\nexport type PopoverOpenChangeDetails = OpenChangeDetails;\n\nexport type PopoverOpenChangeHandler = OpenChangeHandler;\n\nexport interface PopoverOptions {\n defaultOpen?: boolean;\n /** The popover id emitted on the server and referenced by its trigger. */\n id?: string;\n onOpenChange?: PopoverOpenChangeHandler;\n open?: boolean;\n}\n\nexport interface PopoverParts {\n readonly open: boolean;\n /** Applies to the caller's own popover element. */\n popover(): MixinDescriptor;\n /** Opens or closes without an activation event. */\n setOpen(open: boolean): void;\n trigger(): MixinDescriptor;\n}\n\nexport interface PopoverProps extends PopoverOptions {\n children: (popover: PopoverParts) => FigNode;\n}\n\n/**\n * Coordinates one popover around the native popover API, which owns the top\n * layer, light dismiss, and Escape, and around CSS anchor positioning, which\n * owns placement. The widget owns open state and the wiring between the two\n * elements, and measures nothing.\n */\nexport function usePopover(options: PopoverOptions = {}): PopoverParts {\n const requestReconcile = useRegistrationReconcile();\n const registry = useMemo(\n () => createAnchoredPopup(requestReconcile, \"popover\"),\n [],\n );\n const { getOpen, open, requestOpen, setOpen, nativeToggle } = usePopupState({\n ...options,\n requestReconcile,\n });\n\n const id = useId();\n const anchorName = `--fig-popover-${id.replaceAll(/[^\\w-]/g, \"-\")}`;\n const popoverId = options.id ?? `${id}-popover`;\n\n useBeforePaint(() => {\n registry.sync(getOpen(), anchorName);\n });\n\n const state = { open, popoverId, registry, requestOpen, nativeToggle };\n\n const parts: PopoverParts = {\n open,\n popover: () => popoverMixin(state),\n setOpen,\n trigger: () => popoverTriggerMixin(state),\n };\n registerPopoverSource(parts, registry.bindSource);\n return parts;\n}\n\n/**\n * {@link usePopover} as a component, for a popover that is not already a\n * component of its own. Opening re-renders this root rather than the caller.\n */\nexport function Popover(props: PopoverProps): FigNode {\n return props.children(usePopover(props));\n}\n"],"mappings":"2QAmBA,MAAa,EAAsC,GAChD,EAAuB,KAEf,CACL,GAAG,EAAa,EAAS,CAAE,SAAU,EAAM,CAAC,EAC5C,gBAAiB,EAAM,UACvB,gBAAiB,EAAM,KAAO,OAAS,QACvC,KAAM,EAAS,EAAS,EAAM,SAAU,EAAM,SAAS,UAAU,EACjE,YAAa,EAAM,KAAO,GAAK,IAAA,GAC/B,cAAe,EAAM,UAGrB,IAAK,EAAG,QAAU,GAAU,CAC1B,GAAI,EAAM,kBAAoB,EAAM,SAAS,UAAU,EAAG,OAC1D,IAAM,EAAU,EAAM,cACtB,EAAM,YACJ,CAAC,EAAM,KACP,EACA,aAAmB,QAAU,EAAU,IAAA,EACzC,CACF,CAAC,CACH,EAEJ,EAEa,EAA+B,GACzC,EAAuB,KACtB,EAAc,EAAS,EAAM,SAAS,EAC/B,CACL,KAAM,EAAS,EAAS,EAAM,SAAU,EAAM,SAAS,SAAS,EAChE,YAAa,EAAM,KAAO,GAAK,IAAA,GAC/B,GAAI,EAAM,UACV,IAAK,CACH,EAAG,eAAgB,EAAM,YAAY,EACrC,EAAG,SAAU,EAAM,YAAY,CACjC,EACA,QAAS,EAAQ,MAAM,SAAW,MACpC,EAEJ,ECpDM,EAAiB,IAAI,QAE3B,SAAgB,EACd,EACA,EACM,CACN,EAAe,IAAI,EAAO,CAAI,CAChC,CAEA,SAAgB,EACd,EACA,EACA,EACM,CACN,IAAM,EAAO,EAAe,IAAI,CAAK,EACrC,GAAI,IAAS,IAAA,GACX,MAAU,MAAM,0DAA0D,EAC5E,EAAK,EAAM,CAAM,CACnB,CCwBA,SAAgB,EAAW,EAA0B,CAAC,EAAiB,CACrE,IAAM,EAAmB,EAAyB,EAC5C,EAAW,MACT,EAAoB,EAAkB,SAAS,EACrD,CAAC,CACH,EACM,CAAE,UAAS,OAAM,cAAa,UAAS,gBAAiB,EAAc,CAC1E,GAAG,EACH,kBACF,CAAC,EAEK,EAAK,EAAM,EACX,EAAa,iBAAiB,EAAG,WAAW,UAAW,GAAG,IAC1D,EAAY,EAAQ,IAAM,GAAG,EAAG,UAEtC,MAAqB,CACnB,EAAS,KAAK,EAAQ,EAAG,CAAU,CACrC,CAAC,EAED,IAAM,EAAQ,CAAE,OAAM,YAAW,WAAU,cAAa,cAAa,EAE/D,EAAsB,CAC1B,OACA,YAAe,EAAa,CAAK,EACjC,UACA,YAAe,EAAoB,CAAK,CAC1C,EAEA,OADA,EAAsB,EAAO,EAAS,UAAU,EACzC,CACT,CAMA,SAAgB,EAAQ,EAA8B,CACpD,OAAO,EAAM,SAAS,EAAW,CAAK,CAAC,CACzC"}
@@ -0,0 +1,2 @@
1
+ import { a as PopoverParts, i as PopoverOptions, n as PopoverOpenChangeDetails, o as PopoverProps, r as PopoverOpenChangeHandler, s as usePopover, t as Popover } from "./popover-CSPJNmHy.js";
2
+ export { Popover, PopoverOpenChangeDetails, PopoverOpenChangeHandler, PopoverOptions, PopoverParts, PopoverProps, usePopover };
@@ -0,0 +1 @@
1
+ import{n as e,t}from"./popover-UfJHN1h2.js";export{t as Popover,e as usePopover};
@@ -0,0 +1,26 @@
1
+ import { MixinDescriptor } from "@bgub/fig";
2
+ //#region src/popup/position.d.ts
3
+ interface PopupPositionOptions {
4
+ /** Match the popup widget's committed open state. */
5
+ open: boolean;
6
+ /** Preferred side in horizontal writing modes. Defaults to bottom. */
7
+ side?: "top" | "bottom" | "left" | "right" | "inline-start" | "inline-end";
8
+ /** Alignment along the side. Defaults to start. */
9
+ align?: "start" | "center" | "end";
10
+ /** Distance from the anchor in CSS pixels. Defaults to 4; may be negative. */
11
+ sideOffset?: number;
12
+ /** Cross-axis offset in CSS pixels; end alignment reverses its sign. Defaults to 0. */
13
+ alignOffset?: number;
14
+ /** Space to reserve at each viewport edge, in CSS pixels. Defaults to 4. */
15
+ collisionPadding?: number;
16
+ /** Check anchor bounds every frame for transform animations. Defaults to false. */
17
+ trackAnchorAnimation?: boolean;
18
+ }
19
+ interface PopupPositionParts {
20
+ anchor(): MixinDescriptor;
21
+ popup(): MixinDescriptor;
22
+ }
23
+ /** Optional viewport positioning for native top-layer popups. */
24
+ declare function usePopupPosition(options: PopupPositionOptions): PopupPositionParts;
25
+ //#endregion
26
+ export { PopupPositionOptions, PopupPositionParts, usePopupPosition };
@@ -0,0 +1,2 @@
1
+ import{a as e,n as t,s as n}from"../parts-I0jdefyp.js";import{createMixin as r,useBeforePaint as i,useMemo as a}from"@bgub/fig";function o(e,t,n,r,i){let a=e.ownerDocument.defaultView,o=e.getBoundingClientRect(),s,c=0,l=!1;function u(){if(r.aborted)return;let n=e.getBoundingClientRect();(n.x!==o.x||n.y!==o.y||n.width!==o.width||n.height!==o.height)&&(o=n,t(),f())}function d(){c=0,u(),!r.aborted&&(i||l)&&(c=a.requestAnimationFrame(d))}function f(t=1){if(s?.disconnect(),i||o.width===0||o.height===0)return;let n=e.ownerDocument.documentElement;s=new a.IntersectionObserver(e=>{if(r.aborted)return;let n=o;u();let i=e.at(-1)?.intersectionRatio??1;n===o&&t===1&&i>0&&i<1&&f(i)},{rootMargin:`${-o.top}px ${o.right-n.clientWidth}px ${o.bottom-n.clientHeight}px ${-o.left}px`,threshold:[t,1]}),s.observe(e)}let p=new a.IntersectionObserver(e=>{let t=e.at(-1);r.aborted||t===void 0||(n(t.intersectionRect.width<=0||t.intersectionRect.height<=0),l=t.intersectionRatio>0&&t.intersectionRatio<1,u(),l&&!c&&(c=a.requestAnimationFrame(d)))},{threshold:[0,1]});return p.observe(e),f(),a.addEventListener(`resize`,()=>{u(),f()},{signal:r}),i&&(c=a.requestAnimationFrame(d)),r.addEventListener(`abort`,()=>{p.disconnect(),s?.disconnect(),a.cancelAnimationFrame(c)},{once:!0}),u}const s=r((e,n)=>({bind:t(e,n,n.anchor.bind)})),c=r((e,n)=>({bind:t(e,n,n.bindPopup)}));function l(t){let n=e(),r=a(()=>d(n),[]);return i(e=>{let n=r.anchor.node(),i=r.popup.node(),a=r.styles;if(!t.open||n===null||i===null||a===void 0)return;let s=n.ownerDocument.defaultView;if(s===null)return;let c=0,l=!1,u=()=>{e.aborted||f(n,i,a,t,l)},d=()=>{e.aborted||c||(c=s.requestAnimationFrame(()=>{c=0,m(),u()}))};u();let p=new s.ResizeObserver(d);p.observe(i);for(let e=n;e!==null;e=e.parentElement)p.observe(e);let m=o(n,u,e=>{l=e,u()},e,t.trackAnchorAnimation??!1),h={signal:e,passive:!0};n.ownerDocument.addEventListener(`scroll`,e=>{e.target!==i&&d()},{...h,capture:!0}),s.addEventListener(`resize`,d,h),s.visualViewport?.addEventListener(`resize`,d,h),s.visualViewport?.addEventListener(`scroll`,d,h),e.addEventListener(`abort`,()=>{p.disconnect(),s.cancelAnimationFrame(c)},{once:!0})}),{anchor:()=>s(r),popup:()=>c(r)}}function u(e,t){let n=new Map;function r(t){let r=n.get(t);r!==void 0&&(e.style.getPropertyValue(t)===r.written&&e.style.getPropertyPriority(t)===``&&(r.value?e.style.setProperty(t,r.value,r.priority):e.style.removeProperty(t)),n.delete(t))}function i(t,i){if(n.get(t)?.written===i&&e.style.getPropertyValue(t)===i&&e.style.getPropertyPriority(t)===``)return;r(t);let a={value:e.style.getPropertyValue(t),priority:e.style.getPropertyPriority(t),written:i};e.style.setProperty(t,i),a.written=e.style.getPropertyValue(t),n.set(t,a)}return t.addEventListener(`abort`,()=>{for(let e of n.keys())r(e)},{once:!0}),{set:i,restore:r}}function d(e){let t=n(e),r=n(e),i={anchor:t,popup:r,bindPopup:a};function a(e,t){if(!r.bind(e,t))return;let n=u(e,t);i.styles=n;let a=[`data-side`,`data-align`,`data-anchor-hidden`].map(t=>[t,e.getAttribute(t)]);t.addEventListener(`abort`,()=>{for(let[t,n]of a)n===null?e.removeAttribute(t):e.setAttribute(t,n)},{once:!0});for(let[e,t]of Object.entries({position:`fixed`,"position-area":`none`,"position-try-fallbacks":`none`,top:`4px`,left:`4px`,right:`auto`,bottom:`auto`,"margin-top":`0px`,"margin-right":`0px`,"margin-bottom":`0px`,"margin-left":`0px`,"overflow-x":`auto`,"overflow-y":`auto`,"box-sizing":`border-box`}))n.set(e,t)}return i}function f(e,t,n,r,i){let a=e.ownerDocument.defaultView,o=a.visualViewport,s=o?.width??e.ownerDocument.documentElement.clientWidth,c=o?.height??e.ownerDocument.documentElement.clientHeight,l=Math.max(0,r.collisionPadding??4),u=Math.min(l,s/2),d=Math.min(l,c/2),f=(o?.offsetLeft??0)+u,m=(o?.offsetTop??0)+d,h=f+s-2*u,g=m+c-2*d,_=r.sideOffset??4,v=a.getComputedStyle(e).direction===`rtl`,y=e.getBoundingClientRect();t.toggleAttribute(`data-anchor-hidden`,i||y.width===0||y.height===0||y.right<=f-u||y.left>=h+u||y.bottom<=m-d||y.top>=g+d);let b=r.align??`start`,x=r.side??`bottom`;x===`inline-end`&&(x=v?`left`:`right`),x===`inline-start`&&(x=v?`right`:`left`);let S={top:p(y.top-m-_,0,g-m),bottom:p(g-y.bottom-_,0,g-m),left:p(y.left-f-_,0,h-f),right:p(h-y.right-_,0,h-f)},C={top:`bottom`,bottom:`top`,left:`right`,right:`left`};n.set(`--anchor-width`,`${y.width}px`),n.set(`--anchor-height`,`${y.height}px`),n.set(`--available-width`,`${h-f}px`),n.set(`--available-height`,`${g-m}px`);let w=t.scrollTop,T=t.scrollLeft;n.restore(`max-width`),n.restore(`max-height`);let E=a.getComputedStyle(t),D=E.maxWidth,O=E.maxHeight;function k(e,t){n.set(`--available-width`,`${Math.max(0,e)}px`),n.set(`--available-height`,`${Math.max(0,t)}px`),n.set(`max-width`,D===`none`||!D?`${Math.max(0,e)}px`:`min(${D}, ${Math.max(0,e)}px)`),n.set(`max-height`,O===`none`||!O?`${Math.max(0,t)}px`:`min(${O}, ${Math.max(0,t)}px)`)}k(h-f,g-m);let A=t.getBoundingClientRect(),j=x===`top`||x===`bottom`;(j?A.height:A.width)>S[x]&&S[C[x]]>S[x]&&(x=C[x]),!j&&A.width>S[x]&&(x=S.bottom>=A.height||S.bottom>=S.top?`bottom`:`top`,j=!0),k(j?h-f:S[x],j?S[x]:g-m);let M=(r.side===`inline-start`||r.side===`inline-end`)&&!j?x===`left`===v?`inline-end`:`inline-start`:x;t.setAttribute(`data-side`,M),t.setAttribute(`data-align`,b),A=t.getBoundingClientRect(),t.scrollTop=w,t.scrollLeft=T;let N=j?y.left:y.top,P=j?y.right:y.bottom,F=j?A.width:A.height,I=j&&v,L=b===`end`!==I,R=b===`center`?(N+P-F)/2:L?P-F:N,z=b===`end`?-1:1;R+=(r.alignOffset??0)*z*(I?-1:1);let B=j?R:x===`left`?y.left-_-A.width:y.right+_,V=j?x===`top`?y.top-_-A.height:y.bottom+_:R;B=p(B,f,h-A.width),V=p(V,m,g-A.height),n.set(`left`,`${B}px`),n.set(`top`,`${V}px`);let H=b!==`center`&&Math.abs((j?B:V)-R)<=1?L?F:0:p((N+P)/2-(j?B:V),0,F),U=j?H:(x===`left`?y.left:y.right)-B,W=j?(x===`top`?y.top:y.bottom)-V:H;n.set(`--transform-origin`,`${U}px ${W}px`)}function p(e,t,n){return Math.max(t,Math.min(e,n))}export{l as usePopupPosition};
2
+ //# sourceMappingURL=position.js.map