Source file popover.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
open! Core
open Virtual_dom
open Bonsai_web_toplayer_private_floating
module Portal = Bonsai_web_portal_private

module Popover_attr = struct
  module Impl = struct
    module Input = struct
      module For_one = struct
        type t =
          { content : Vdom_with_phys_equal.Node.t
          ; popover_attrs : Vdom_with_phys_equal.Attr.t list
          ; arrow : Vdom_with_phys_equal.Node.t option
          ; restore_focus_on_close : Popover_dom.Restore_focus_on_close.t
          ; overflow_auto_wrapper : bool
          ; position : Position.t
          ; alignment : Alignment.t
          ; offset : Offset.t
          ; match_anchor_side_length : Match_anchor_side.t option
          }
        [@@deriving equal]

        let equal a b = phys_equal a b || equal a b
      end

      type t = For_one.t list [@@deriving equal]

      let combine a b = a @ b
      let sexp_of_t _ = Sexp.Atom "<omitted>"
    end

    module State = struct
      module For_one = struct
        type t =
          { portal : Portal.t
          ; input : Input.For_one.t
          }
      end

      type t = For_one.t list ref
    end

    let wrap_content
      { Input.For_one.position
      ; alignment
      ; offset
      ; match_anchor_side_length
      ; content
      ; restore_focus_on_close
      ; overflow_auto_wrapper
      ; popover_attrs
      ; arrow
      }
      ~anchor
      =
      let position_attr =
        Bonsai_web_toplayer_private_floating.position_me
          ~prepare:Popover_dom.show_popover
          ~arrow_selector:Popover_dom.arrow_selector
          ~position
          ~alignment
          ~offset
          ?match_anchor_side_length
          (Bonsai_web_toplayer_private_floating.Anchor.of_element anchor)
      in
      Popover_dom.node
        ?arrow
        ~restore_focus_on_close
        ~overflow_auto_wrapper
        ~extra_attrs:(popover_attrs @ [ Inertness_management.for_popover; position_attr ])
        ~kind:`Manual
        content
    ;;

    let create_one (input : Input.For_one.t) ~anchor =
      let parent = Popover_dom.find_popover_portal_root anchor in
      let portal = Portal.create ~parent (wrap_content input ~anchor) in
      { State.For_one.portal; input }
    ;;

    let update_one input (state : State.For_one.t) ~anchor =
      match Input.For_one.equal input state.input with
      | true -> state
      | false ->
        let new_portal = Portal.apply_patch state.portal (wrap_content input ~anchor) in
        { input; portal = new_portal }
    ;;

    let destroy_one { State.For_one.portal; input = _ } = Portal.destroy portal
    let init _ _ = ref []

    let on_mount all_inputs state_ref anchor =
      let state = List.map all_inputs ~f:(create_one ~anchor) in
      state_ref := state
    ;;

    let on_mount = `Schedule_immediately_after_this_dom_patch_completes on_mount

    let update ~old_input ~(new_input : Input.t) (state_ref : State.t) anchor =
      match phys_equal old_input new_input with
      | true -> ()
      | false ->
        let zipped, remainder = List.zip_with_remainder new_input !state_ref in
        let updated_state =
          List.map zipped ~f:(fun (input, state) -> update_one input state ~anchor)
        in
        let state_from_remainder =
          match remainder with
          | None -> []
          | Some (Second old_states) ->
            List.iter old_states ~f:destroy_one;
            []
          | Some (First new_inputs) -> List.map new_inputs ~f:(create_one ~anchor)
        in
        state_ref := updated_state @ state_from_remainder
    ;;

    let destroy _ (state : State.t) _ = List.iter !state ~f:destroy_one
  end

  include Impl
  include Vdom.Attr.Hooks.Make (Impl)
end

let hook_name = "vdom_toplayer_popover"

let attr
  ?(popover_attrs = [])
  ?(position = Position.Auto)
  ?(alignment = Alignment.Center)
  ?(offset = Offset.zero)
  ?match_anchor_side_length
  ?(restore_focus_on_close =
    Popover_dom.Restore_focus_on_close.Yes { prevent_scroll = false })
  ?(overflow_auto_wrapper = false)
  ?arrow
  content
  =
  Popover_attr.create
    [ { position
      ; alignment
      ; offset
      ; restore_focus_on_close
      ; match_anchor_side_length
      ; content
      ; popover_attrs
      ; overflow_auto_wrapper
      ; arrow
      }
    ]
  |> Vdom.Attr.create_hook hook_name
;;

let custom
  ?(popover_attrs = [])
  ?(restore_focus_on_close =
    Popover_dom.Restore_focus_on_close.Yes { prevent_scroll = false })
  ?(overflow_auto_wrapper = false)
  ?arrow
  ~popover_content
  ()
  =
  Popover_dom.node
    ?arrow
    ~kind:`Manual
    ~restore_focus_on_close
    ~overflow_auto_wrapper
    ~extra_attrs:(popover_attrs @ [ Inertness_management.for_popover ])
    popover_content
;;

module For_testing_popover_hook = struct
  type for_one = Popover_attr.Input.For_one.t =
    { content : Vdom_with_phys_equal.Node.t
    ; popover_attrs : Vdom_with_phys_equal.Attr.t list
    ; arrow : Vdom_with_phys_equal.Node.t option
    ; restore_focus_on_close : Popover_dom.Restore_focus_on_close.t
    ; overflow_auto_wrapper : bool
    ; position : Position.t
    ; alignment : Alignment.t
    ; offset : Offset.t
    ; match_anchor_side_length : Match_anchor_side.t option
    }
  [@@deriving sexp_of]

  type t = for_one list [@@deriving sexp_of]

  let type_id = Popover_attr.For_testing.type_id
  let hook_name = hook_name
end

module For_testing_bonsai_web_toplayer = struct
  let wrap_anchored_popover
    ~position
    ~alignment
    ~offset
    ~match_anchor_side_length
    ~(restore_focus_on_close : Popover_dom.Restore_focus_on_close.t)
    ~overflow_auto_wrapper
    ~content
    ~popover_attrs
    ~arrow
    ~anchor
    =
    Popover_attr.Impl.wrap_content
      { position
      ; alignment
      ; offset
      ; match_anchor_side_length
      ; restore_focus_on_close
      ; overflow_auto_wrapper
      ; content
      ; popover_attrs
      ; arrow
      }
      ~anchor
  ;;
end