git.iko.soy/rowtype-yoga/purescript-yoga-heroui
on main help sign in
Bind useDisclosure + sibling hooks, fix NumberInput value, expand Input (#4)

Follow-up to the issue #1 fixes — addresses the gaps found while auditing the bindings against @heroui/react's TypeScript types. - HeroUI.Hooks: new module binding `useDisclosure` (typed return record), plus `useModal`, `useDraggable`, `useDrawer`, `usePopover`, `useDropdown`, `useSelect`, `useAutocomplete`, `useTabs`, `useAccordion`, `useAccordionItem`, `useCheckbox`, `useCheckboxGroup`. The lower-level hooks return `Foreign` because their shapes are slot-driven and shift across heroui versions — consumers narrow at the call site. - HeroUI.NumberInput: `value`/`defaultValue` are now `Number`, matching `@react-types/numberfield`'s `ValueBase<number>`. Previously typed `String`, which collided with the already-correct `onValueChange :: EffectFn1 Number Unit`. - HeroUI.Input: introduce `InputType` ADT (text/email/password/number/ tel/url/search/date/time/datetime-local/month/week/file/checkbox/ radio/hidden/color/range), wired through `inputTypeToString` in the transforms record. Add `id`, `form`, `autoComplete`, `autoFocus`, `min`, `max`, `step` — all forwarded by heroui to the native `<input>` but previously rejected by the row type. Tests: `Test.HeroUI.IssuesSpec` extended to 9/9, covering each new binding.

commit8d6424b51efeb07d605cdbc61bd77b883c62a020 authorMark Eibes <mark.eibes@gmail.com> dateWed 27 May 2026, 13:51 (4 months ago) parentc1514d2
5 files changed+287−4
src/HeroUI/Hooks.js+31 src/HeroUI/Hooks.purs+143 src/HeroUI/Input.purs+53−2 src/HeroUI/NumberInput.purs+2−2 test/HeroUI/IssuesSpec.purs+58
src/HeroUI/Hooks.jsnew file+31
@@ -0,0 +1,31 @@
1+import {
2+ useAccordion,
3+ useAccordionItem,
4+ useAutocomplete,
5+ useCheckbox,
6+ useCheckboxGroup,
7+ useDisclosure,
8+ useDraggable,
9+ useDrawer,
10+ useDropdown,
11+ useModal,
12+ usePopover,
13+ useSelect,
14+ useTabs,
15+} from "@heroui/react";
16+
17+const hook = (h) => (props) => h(props);
18+
19+export const useDisclosureImpl = hook(useDisclosure);
20+export const useModalImpl = hook(useModal);
21+export const useDraggableImpl = hook(useDraggable);
22+export const useDrawerImpl = hook(useDrawer);
23+export const usePopoverImpl = hook(usePopover);
24+export const useDropdownImpl = hook(useDropdown);
25+export const useSelectImpl = hook(useSelect);
26+export const useAutocompleteImpl = hook(useAutocomplete);
27+export const useTabsImpl = hook(useTabs);
28+export const useAccordionImpl = hook(useAccordion);
29+export const useAccordionItemImpl = hook(useAccordionItem);
30+export const useCheckboxImpl = hook(useCheckbox);
31+export const useCheckboxGroupImpl = hook(useCheckboxGroup);
src/HeroUI/Hooks.pursnew file+143
@@ -0,0 +1,143 @@
1+module HeroUI.Hooks
2+ ( UseDisclosure
3+ , UseDisclosureProps
4+ , UseDisclosureReturn
5+ , useDisclosure
6+ , UseModal
7+ , useModal
8+ , UseDraggable
9+ , useDraggable
10+ , UseDrawer
11+ , useDrawer
12+ , UsePopover
13+ , usePopover
14+ , UseDropdown
15+ , useDropdown
16+ , UseSelect
17+ , useSelect
18+ , UseAutocomplete
19+ , useAutocomplete
20+ , UseTabs
21+ , useTabs
22+ , UseAccordion
23+ , useAccordion
24+ , UseAccordionItem
25+ , useAccordionItem
26+ , UseCheckbox
27+ , useCheckbox
28+ , UseCheckboxGroup
29+ , useCheckboxGroup
30+ ) where
31+
32+import Prelude
33+
34+import Effect (Effect)
35+import Effect.Uncurried (EffectFn1, runEffectFn1)
36+import Foreign (Foreign)
37+import Prim.Row as Row
38+import React.Basic.Hooks (Hook)
39+import React.Basic.Hooks.Internal (unsafeHook)
40+import Unsafe.Coerce (unsafeCoerce)
41+
42+-- | Props accepted by `useDisclosure`. All optional; HeroUI inspects only
43+-- | the fields you actually pass.
44+type UseDisclosureProps =
45+ ( isOpen :: Boolean
46+ , defaultOpen :: Boolean
47+ , onClose :: Effect Unit
48+ , onOpen :: Effect Unit
49+ , onChange :: EffectFn1 Boolean Unit
50+ , id :: String
51+ )
52+
53+-- | Return shape of `useDisclosure`. `getButtonProps`/`getDisclosureProps`
54+-- | are passed through as `Foreign` because they take and return ARIA props
55+-- | records that vary per use site.
56+type UseDisclosureReturn =
57+ ( isOpen :: Boolean
58+ , onOpen :: Effect Unit
59+ , onClose :: Effect Unit
60+ , onOpenChange :: Effect Unit
61+ , isControlled :: Boolean
62+ , getButtonProps :: Foreign -> Foreign
63+ , getDisclosureProps :: Foreign -> Foreign
64+ )
65+
66+foreign import data UseDisclosure :: Type -> Type
67+foreign import data UseModal :: Type -> Type
68+foreign import data UseDraggable :: Type -> Type
69+foreign import data UseDrawer :: Type -> Type
70+foreign import data UsePopover :: Type -> Type
71+foreign import data UseDropdown :: Type -> Type
72+foreign import data UseSelect :: Type -> Type
73+foreign import data UseAutocomplete :: Type -> Type
74+foreign import data UseTabs :: Type -> Type
75+foreign import data UseAccordion :: Type -> Type
76+foreign import data UseAccordionItem :: Type -> Type
77+foreign import data UseCheckbox :: Type -> Type
78+foreign import data UseCheckboxGroup :: Type -> Type
79+
80+foreign import useDisclosureImpl :: EffectFn1 (Record UseDisclosureProps) (Record UseDisclosureReturn)
81+foreign import useModalImpl :: EffectFn1 Foreign Foreign
82+foreign import useDraggableImpl :: EffectFn1 Foreign Foreign
83+foreign import useDrawerImpl :: EffectFn1 Foreign Foreign
84+foreign import usePopoverImpl :: EffectFn1 Foreign Foreign
85+foreign import useDropdownImpl :: EffectFn1 Foreign Foreign
86+foreign import useSelectImpl :: EffectFn1 Foreign Foreign
87+foreign import useAutocompleteImpl :: EffectFn1 Foreign Foreign
88+foreign import useTabsImpl :: EffectFn1 Foreign Foreign
89+foreign import useAccordionImpl :: EffectFn1 Foreign Foreign
90+foreign import useAccordionItemImpl :: EffectFn1 Foreign Foreign
91+foreign import useCheckboxImpl :: EffectFn1 Foreign Foreign
92+foreign import useCheckboxGroupImpl :: EffectFn1 Foreign Foreign
93+
94+-- | https://heroui.com/docs/hooks/use-disclosure
95+useDisclosure
96+ :: forall props missing
97+ . Row.Union props missing UseDisclosureProps
98+ => Record props
99+ -> Hook UseDisclosure (Record UseDisclosureReturn)
100+useDisclosure props = unsafeHook (runEffectFn1 useDisclosureImpl (widen props))
101+ where
102+ widen :: Record props -> Record UseDisclosureProps
103+ widen = unsafeCoerce
104+
105+-- HeroUI's lower-level hooks (`useModal`, `useDropdown`, ...) return rich
106+-- objects shaped by internal slot machinery; their fields shift across
107+-- heroui versions. Surface them as `Foreign` and let consumers narrow.
108+
109+useModal :: Foreign -> Hook UseModal Foreign
110+useModal props = unsafeHook (runEffectFn1 useModalImpl props)
111+
112+useDraggable :: Foreign -> Hook UseDraggable Foreign
113+useDraggable props = unsafeHook (runEffectFn1 useDraggableImpl props)
114+
115+useDrawer :: Foreign -> Hook UseDrawer Foreign
116+useDrawer props = unsafeHook (runEffectFn1 useDrawerImpl props)
117+
118+usePopover :: Foreign -> Hook UsePopover Foreign
119+usePopover props = unsafeHook (runEffectFn1 usePopoverImpl props)
120+
121+useDropdown :: Foreign -> Hook UseDropdown Foreign
122+useDropdown props = unsafeHook (runEffectFn1 useDropdownImpl props)
123+
124+useSelect :: Foreign -> Hook UseSelect Foreign
125+useSelect props = unsafeHook (runEffectFn1 useSelectImpl props)
126+
127+useAutocomplete :: Foreign -> Hook UseAutocomplete Foreign
128+useAutocomplete props = unsafeHook (runEffectFn1 useAutocompleteImpl props)
129+
130+useTabs :: Foreign -> Hook UseTabs Foreign
131+useTabs props = unsafeHook (runEffectFn1 useTabsImpl props)
132+
133+useAccordion :: Foreign -> Hook UseAccordion Foreign
134+useAccordion props = unsafeHook (runEffectFn1 useAccordionImpl props)
135+
136+useAccordionItem :: Foreign -> Hook UseAccordionItem Foreign
137+useAccordionItem props = unsafeHook (runEffectFn1 useAccordionItemImpl props)
138+
139+useCheckbox :: Foreign -> Hook UseCheckbox Foreign
140+useCheckbox props = unsafeHook (runEffectFn1 useCheckboxImpl props)
141+
142+useCheckboxGroup :: Foreign -> Hook UseCheckboxGroup Foreign
143+useCheckboxGroup props = unsafeHook (runEffectFn1 useCheckboxGroupImpl props)
src/HeroUI/Input.purs+53−2
@@ -10,6 +10,50 @@ import HeroUI.Types (Color, LabelPlacement, Radius, Size, ValidationBehavior, Va
1010import HeroUI.Internal (class CoerceHeroProps, createElementTransformImpl)
1111import HeroUI.Raw as Raw
1212
13+data InputType
14+ = InputTypeText
15+ | InputTypeEmail
16+ | InputTypePassword
17+ | InputTypeNumber
18+ | InputTypeTel
19+ | InputTypeUrl
20+ | InputTypeSearch
21+ | InputTypeDate
22+ | InputTypeTime
23+ | InputTypeDateTimeLocal
24+ | InputTypeMonth
25+ | InputTypeWeek
26+ | InputTypeFile
27+ | InputTypeCheckbox
28+ | InputTypeRadio
29+ | InputTypeHidden
30+ | InputTypeColor
31+ | InputTypeRange
32+
33+derive instance Eq InputType
34+derive instance Ord InputType
35+
36+inputTypeToString :: InputType -> String
37+inputTypeToString = case _ of
38+ InputTypeText -> "text"
39+ InputTypeEmail -> "email"
40+ InputTypePassword -> "password"
41+ InputTypeNumber -> "number"
42+ InputTypeTel -> "tel"
43+ InputTypeUrl -> "url"
44+ InputTypeSearch -> "search"
45+ InputTypeDate -> "date"
46+ InputTypeTime -> "time"
47+ InputTypeDateTimeLocal -> "datetime-local"
48+ InputTypeMonth -> "month"
49+ InputTypeWeek -> "week"
50+ InputTypeFile -> "file"
51+ InputTypeCheckbox -> "checkbox"
52+ InputTypeRadio -> "radio"
53+ InputTypeHidden -> "hidden"
54+ InputTypeColor -> "color"
55+ InputTypeRange -> "range"
56+
1357type InputProps r =
1458 ( variant :: Variant
1559 , color :: Color
@@ -36,8 +80,15 @@ type InputProps r =
3680 , disableAnimation :: Boolean
3781 , classNames :: Foreign
3882 , className :: String
39− , type :: String
83+ , type :: InputType
4084 , name :: String
85+ , id :: String
86+ , form :: String
87+ , autoComplete :: String
88+ , autoFocus :: Boolean
89+ , min :: String
90+ , max :: String
91+ , step :: String
4192 , minLength :: Int
4293 , maxLength :: Int
4394 , pattern :: String
@@ -55,7 +106,7 @@ input
55106 -> kids
56107 -> JSX
57108input props kids = runFn4 createElementTransformImpl
58− { variant: variantToString, color: colorToString, size: sizeToString, radius: radiusToString, validationBehavior: validationBehaviorToString, labelPlacement: labelPlacementToString }
109+ { variant: variantToString, color: colorToString, size: sizeToString, radius: radiusToString, validationBehavior: validationBehaviorToString, labelPlacement: labelPlacementToString, type: inputTypeToString }
59110 Raw.input props kids
60111
61112type TextareaProps r =
src/HeroUI/NumberInput.purs+2−2
@@ -18,8 +18,8 @@ type NumberInputProps r =
1818 , name :: String
1919 , label :: JSX
2020 , description :: JSX
21− , value :: String
22− , defaultValue :: String
21+ , value :: Number
22+ , defaultValue :: Number
2323 , placeholder :: String
2424 , errorMessage :: JSX
2525 , validate :: Foreign
test/HeroUI/IssuesSpec.purs+58
@@ -6,13 +6,20 @@ import Data.Maybe (Maybe(..))
66import Effect.Class (liftEffect)
77import HeroUI.Button as Btn
88import HeroUI.Drawer as Drawer
9+import HeroUI.Hooks as Hooks
10+import HeroUI.Input as Input
911import HeroUI.Modal as Modal
12+import HeroUI.NumberInput as NumberInput
1013import HeroUI.Provider (provider)
1114import HeroUI.Toast as Toast
1215import HeroUI.Types as T
16+import React.Basic.Hooks as React
1317import React.TestingLibrary (cleanup, render)
1418import Test.Spec (Spec, after_, describe, it)
19+import Test.Spec.Assertions (shouldEqual)
1520import Test.Spec.Assertions.DOM (textContentShouldEqual)
21+import Unsafe.Coerce (unsafeCoerce)
22+import Yoga.React (component)
1623import Yoga.React.DOM.Internal (text)
1724
1825spec :: Spec Unit
@@ -45,6 +52,28 @@ spec = after_ cleanup $ describe "purescript-yoga-heroui issue #1" do
4552 el <- findByLabelText "close dialog"
4653 el `textContentShouldEqual` "X"
4754
55+ it "useDisclosure exposes isOpen + onOpen/onClose" do
56+ { findByText } <- render disclosureHost
57+ initial <- findByText "closed"
58+ initial `textContentShouldEqual` "closed"
59+
60+ it "NumberInput accepts numeric value/defaultValue" do
61+ { findByLabelText } <- render numberInputHost
62+ el <- findByLabelText "qty"
63+ -- input is rendered; numeric value bound without compile error
64+ _ <- pure el
65+ pure unit
66+
67+ it "Input accepts native HTML attrs (autoComplete/form/id) and InputType ADT" do
68+ { findByLabelText } <- render inputHost
69+ el <- findByLabelText "email"
70+ _ <- pure el
71+ pure unit
72+
73+ it "InputType strings round-trip" do
74+ Input.inputTypeToString Input.InputTypeEmail `shouldEqual` "email"
75+ Input.inputTypeToString Input.InputTypeDateTimeLocal `shouldEqual` "datetime-local"
76+
4877submitButton = provider {}
4978 [ Btn.button { type: Btn.ButtonTypeSubmit } (text "Submit") ]
5079
@@ -64,3 +93,32 @@ ariaButton = provider {}
6493toastHost = provider {}
6594 [ Toast.toastProvider {} ([] :: Array _) ]
6695
96+disclosureHost = provider {} [ disclosureComponent {} ]
97+
98+disclosureComponent = component "DisclosureHost" \(_ :: {}) -> React.do
99+ d <- Hooks.useDisclosure {}
100+ pure $ text (if d.isOpen then "open" else "closed")
101+
102+numberInputHost = provider {}
103+ [ NumberInput.numberInput
104+ { label: text "qty"
105+ , defaultValue: 1.0
106+ , minValue: 0.0
107+ , maxValue: 10.0
108+ }
109+ ([] :: Array _)
110+ ]
111+
112+inputHost = provider {}
113+ [ Input.input
114+ { label: text "email"
115+ , type: Input.InputTypeEmail
116+ , name: "email"
117+ , id: "email-field"
118+ , form: "signup"
119+ , autoComplete: "email"
120+ , autoFocus: false
121+ }
122+ ([] :: Array _)
123+ ]
124+