@rip-lang/ui 0.3.66 → 0.4.1

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 (83) hide show
  1. package/AGENTS.md +93 -0
  2. package/README.md +22 -625
  3. package/browser/AGENTS.md +213 -0
  4. package/browser/CONTRIBUTING.md +375 -0
  5. package/browser/README.md +11 -0
  6. package/browser/TESTING.md +59 -0
  7. package/browser/browser.rip +56 -0
  8. package/{components → browser/components}/accordion.rip +1 -1
  9. package/{components → browser/components}/alert-dialog.rip +6 -3
  10. package/{components → browser/components}/autocomplete.rip +27 -21
  11. package/{components → browser/components}/avatar.rip +3 -3
  12. package/{components → browser/components}/badge.rip +1 -1
  13. package/{components → browser/components}/breadcrumb.rip +2 -2
  14. package/{components → browser/components}/button-group.rip +3 -3
  15. package/{components → browser/components}/button.rip +2 -2
  16. package/{components → browser/components}/card.rip +1 -1
  17. package/{components → browser/components}/carousel.rip +5 -5
  18. package/{components → browser/components}/checkbox-group.rip +40 -11
  19. package/{components → browser/components}/checkbox.rip +4 -4
  20. package/{components → browser/components}/collapsible.rip +2 -2
  21. package/{components → browser/components}/combobox.rip +36 -23
  22. package/{components → browser/components}/context-menu.rip +1 -1
  23. package/{components → browser/components}/date-picker.rip +5 -5
  24. package/{components → browser/components}/dialog.rip +8 -4
  25. package/{components → browser/components}/drawer.rip +8 -4
  26. package/{components → browser/components}/editable-value.rip +7 -1
  27. package/{components → browser/components}/field.rip +5 -5
  28. package/{components → browser/components}/fieldset.rip +2 -2
  29. package/{components → browser/components}/form.rip +1 -1
  30. package/{components → browser/components}/grid.rip +8 -8
  31. package/{components → browser/components}/input-group.rip +1 -1
  32. package/{components → browser/components}/input.rip +6 -6
  33. package/{components → browser/components}/label.rip +2 -2
  34. package/{components → browser/components}/menu.rip +17 -10
  35. package/{components → browser/components}/menubar.rip +1 -1
  36. package/{components → browser/components}/meter.rip +7 -7
  37. package/{components → browser/components}/multi-select.rip +76 -33
  38. package/{components → browser/components}/native-select.rip +3 -3
  39. package/{components → browser/components}/nav-menu.rip +3 -3
  40. package/{components → browser/components}/number-field.rip +11 -11
  41. package/{components → browser/components}/otp-field.rip +4 -4
  42. package/{components → browser/components}/pagination.rip +4 -4
  43. package/{components → browser/components}/popover.rip +11 -24
  44. package/{components → browser/components}/preview-card.rip +7 -11
  45. package/{components → browser/components}/progress.rip +3 -3
  46. package/{components → browser/components}/radio-group.rip +4 -4
  47. package/{components → browser/components}/resizable.rip +3 -3
  48. package/{components → browser/components}/scroll-area.rip +1 -1
  49. package/{components → browser/components}/select.rip +55 -27
  50. package/{components → browser/components}/separator.rip +2 -2
  51. package/{components → browser/components}/skeleton.rip +4 -4
  52. package/{components → browser/components}/slider.rip +15 -10
  53. package/{components → browser/components}/spinner.rip +2 -2
  54. package/{components → browser/components}/table.rip +2 -2
  55. package/{components → browser/components}/tabs.rip +12 -7
  56. package/{components → browser/components}/textarea.rip +8 -8
  57. package/{components → browser/components}/toast.rip +3 -3
  58. package/{components → browser/components}/toggle-group.rip +42 -11
  59. package/{components → browser/components}/toggle.rip +2 -2
  60. package/{components → browser/components}/toolbar.rip +2 -2
  61. package/{components → browser/components}/tooltip.rip +19 -23
  62. package/browser/hljs-rip.js +209 -0
  63. package/browser/playwright.config.mjs +31 -0
  64. package/browser/tests/overlays.js +349 -0
  65. package/email/AGENTS.md +16 -0
  66. package/email/README.md +55 -0
  67. package/email/benchmarks/benchmark.rip +94 -0
  68. package/email/benchmarks/samples.rip +104 -0
  69. package/email/compat.rip +129 -0
  70. package/email/components.rip +371 -0
  71. package/email/dom.rip +330 -0
  72. package/email/email.rip +10 -0
  73. package/email/render.rip +82 -0
  74. package/package.json +29 -39
  75. package/shared/README.md +3 -0
  76. package/shared/styles.rip +17 -0
  77. package/tailwind/AGENTS.md +3 -0
  78. package/tailwind/README.md +27 -0
  79. package/tailwind/engine.js +107 -0
  80. package/tailwind/inline.js +215 -0
  81. package/tailwind/serve.js +6 -0
  82. package/tailwind/tailwind.rip +13 -0
  83. package/ui.rip +3 -0
@@ -2,9 +2,12 @@
2
2
  #
3
3
  # Keyboard: ArrowDown/Up to navigate, Enter/Space to select, Escape to close,
4
4
  # Home/End for first/last, typeahead to jump by character.
5
+ # Pointer: mouse/pen opens on pointerdown so release can land on an option in
6
+ # one gesture; touch opens on click so scrolling can still be canceled.
5
7
  #
6
8
  # Exposes $open and $placeholder on button, $highlighted and $selected on options.
7
- # Uses native `popover="auto"` + anchor positioning.
9
+ # Uses native Popover API in manual mode so pointerdown open does not get
10
+ # auto-dismissed by the same click lifecycle.
8
11
  # Ships zero CSS — style entirely via attribute selectors in your stylesheet.
9
12
  #
10
13
  # Usage:
@@ -13,21 +16,24 @@
13
16
  # option value: "b", "Option B"
14
17
 
15
18
  export Select = component
16
- @value := null
17
- @placeholder := 'Select...'
18
- @disabled := false
19
+ @value:: any := null
20
+ @placeholder:: string := "Select..."
21
+ @disabled:: boolean := false
19
22
 
23
+ options := []
20
24
  open := false
21
25
  highlightedIndex := -1
22
26
  typeaheadBuffer := ''
23
27
  typeaheadTimer := null
28
+ suppressTriggerClick := false
29
+ _ready := false
30
+ _popupGuard =! ARIA.popupGuard()
24
31
  _listId =! "sel-#{Math.random().toString(36).slice(2, 8)}"
25
32
 
26
33
  getOpt: (o) -> o.dataset.value ?? o.value
27
34
 
28
- options ~=
29
- return [] unless @_slot
30
- Array.from(@_slot.querySelectorAll('[data-value], option[value]') or [])
35
+ _readOptions: ->
36
+ options = Array.from(@_slot?.querySelectorAll('[data-value], option[value]') or [])
31
37
 
32
38
  selectedLabel ~=
33
39
  if @value?
@@ -36,19 +42,18 @@ export Select = component
36
42
  else
37
43
  @placeholder
38
44
 
39
- toggle: ->
40
- return if @disabled
41
- if open then @close() else @openMenu()
42
-
43
45
  openMenu: ->
46
+ return unless _popupGuard.canOpen()
47
+ @_readOptions()
44
48
  open = true
45
49
  highlightedIndex = Math.max(0, options.findIndex (o) -> @getOpt(o) is String(@value))
46
50
  requestAnimationFrame => @_focusHighlighted()
47
51
 
48
- close: ->
52
+ close: (restoreFocus = true, blockOpen = false) ->
49
53
  open = false
50
54
  highlightedIndex = -1
51
- @_trigger?.focus()
55
+ _popupGuard.block() if blockOpen
56
+ @_trigger?.focus() if restoreFocus
52
57
 
53
58
  isDisabled: (opt) -> opt?.hasAttribute?('data-disabled') or opt?.disabled
54
59
 
@@ -58,7 +63,7 @@ export Select = component
58
63
  return if @isDisabled(opt)
59
64
  @value = @getOpt(opt)
60
65
  @emit 'change', @value
61
- @close()
66
+ @close(true, true)
62
67
 
63
68
  onTriggerKeydown: (e) ->
64
69
  return if @disabled
@@ -70,6 +75,25 @@ export Select = component
70
75
  e.preventDefault()
71
76
  @close() if open
72
77
 
78
+ onTriggerPointerdown: (e) ->
79
+ return if @disabled
80
+ return unless e.button is 0
81
+ return if e.ctrlKey
82
+ return unless e.pointerType in ['mouse', 'pen']
83
+ return if open
84
+ e.preventDefault()
85
+ suppressTriggerClick = true
86
+ @openMenu()
87
+
88
+ onTriggerClick: (e) ->
89
+ return if @disabled
90
+ if suppressTriggerClick
91
+ suppressTriggerClick = false
92
+ e.preventDefault()
93
+ return
94
+ e.preventDefault()
95
+ if open then @close(false, true) else @openMenu()
96
+
73
97
  _nextEnabled: (from, dir) ->
74
98
  len = options.length
75
99
  i = from
@@ -87,7 +111,7 @@ export Select = component
87
111
  last: => highlightedIndex = options.length - 1; @_focusHighlighted()
88
112
  select: => @selectIndex(highlightedIndex)
89
113
  dismiss: => @close()
90
- tab: => @close()
114
+ tab: => @close(false, true)
91
115
  char: => @_typeahead(e.key)
92
116
 
93
117
  _typeahead: (char) ->
@@ -105,21 +129,18 @@ export Select = component
105
129
  el?.scrollIntoView { block: 'nearest' }
106
130
 
107
131
  _applyPlacement: ->
108
- return unless @_list
109
- @_list.style.position = 'fixed'
110
- @_list.style.inset = 'auto'
111
- @_list.style.margin = '0'
112
- @_list.style.positionArea = 'bottom start'
113
- @_list.style.positionTry = 'flip-block, flip-inline, flip-block flip-inline'
114
- @_list.style.positionVisibility = 'anchors-visible'
115
- @_list.style.marginTop = '4px'
116
- @_list.style.minWidth = 'anchor-size(width)'
132
+ ARIA.position @_trigger, @_list, placement: 'bottom start', offset: 4, matchWidth: true
133
+
134
+ mounted: ->
135
+ _ready = true
136
+ requestAnimationFrame => @_readOptions()
117
137
 
118
138
  ~>
139
+ return unless _ready
119
140
  if @_list
120
- @_list.setAttribute 'popover', 'auto'
121
141
  @_applyPlacement()
122
142
  ARIA.bindPopover open, (=> @_list), ((isOpen) => open = isOpen), (=> @_trigger)
143
+ ARIA.popupDismiss open, (=> @_list), (=> @close(false, true)), [=> @_trigger]
123
144
 
124
145
  render
125
146
  .
@@ -133,7 +154,8 @@ export Select = component
133
154
  $placeholder: (!@value)?!
134
155
  $disabled: @disabled?!
135
156
  disabled: @disabled
136
- @click: @toggle
157
+ @pointerdown: @onTriggerPointerdown
158
+ @click: @onTriggerClick
137
159
  @keydown: @onTriggerKeydown
138
160
  span selectedLabel
139
161
 
@@ -142,7 +164,13 @@ export Select = component
142
164
  slot
143
165
 
144
166
  # Dropdown listbox
145
- div ref: "_list", id: _listId, role: "listbox", style: "position:fixed;margin:0;inset:auto"
167
+ div ref: "_list"
168
+ id: _listId
169
+ role: "listbox"
170
+ popover: "manual"
171
+ hidden: not open
172
+ aria-hidden: (open ? undefined : "true")
173
+ style: "position:fixed;margin:0;inset:auto"
146
174
  $open: open?!
147
175
  @keydown: @onListKeydown
148
176
  for opt, idx in options
@@ -8,8 +8,8 @@
8
8
  # Separator orientation: "vertical"
9
9
 
10
10
  export Separator = component
11
- @orientation := 'horizontal'
12
- @decorative := true
11
+ @orientation:: "horizontal" | "vertical" := "horizontal"
12
+ @decorative:: boolean := true
13
13
 
14
14
  render
15
15
  div role: (if @decorative then 'none' else 'separator')
@@ -10,10 +10,10 @@
10
10
  # Skeleton circle: true, width: "48px"
11
11
 
12
12
  export Skeleton = component
13
- @width := null
14
- @height := null
15
- @circle := false
16
- @label := 'Loading'
13
+ @width:: any := null
14
+ @height:: any := null
15
+ @circle:: boolean := false
16
+ @label:: string := "Loading"
17
17
 
18
18
  render
19
19
  div role: "status", aria-busy: "true", aria-label: @label
@@ -10,15 +10,15 @@
10
10
  # Slider value <=> range, min: 0, max: 100 (pass array for range mode)
11
11
 
12
12
  export Slider = component
13
- @value := 0
14
- @min := 0
15
- @max := 100
16
- @step := 1
17
- @largeStep := 10
18
- @orientation := 'horizontal'
19
- @disabled := false
20
- @name := null
21
- @valueText := null
13
+ @value:: number := 0
14
+ @min:: number := 0
15
+ @max:: number := 100
16
+ @step:: number := 1
17
+ @largeStep:: number := 10
18
+ @orientation:: "horizontal" | "vertical" := "horizontal"
19
+ @disabled:: boolean := false
20
+ @name:: any := null
21
+ @valueText:: any := null
22
22
 
23
23
  dragging := false
24
24
  activeThumb := -1
@@ -75,6 +75,10 @@ export Slider = component
75
75
  _commitValue: ->
76
76
  @emit 'change', @value
77
77
 
78
+ _focusThumb: (idx) ->
79
+ requestAnimationFrame =>
80
+ @_track?.querySelector("#" + "#{_id}-thumb-#{idx}")?.focus()
81
+
78
82
  _onPointerDown: (e) ->
79
83
  return if @disabled or e.button isnt 0
80
84
  e.preventDefault()
@@ -125,6 +129,7 @@ export Slider = component
125
129
  e.preventDefault()
126
130
  @_setValue idx, newVal
127
131
  @_commitValue()
132
+ @_focusThumb idx
128
133
 
129
134
  render
130
135
  div role: "group", $orientation: @orientation, $disabled: @disabled?!, $dragging: dragging?!
@@ -149,7 +154,6 @@ export Slider = component
149
154
  for val, idx in values
150
155
  div $thumb: true, $active: (idx is activeThumb)?!
151
156
  style: "position:absolute; #{if horiz then 'left' else 'bottom'}: #{@_percentOf(val)}%; z-index: #{if idx is activeThumb then 2 else 1}"
152
- @keydown: (e) => @_onKeydown(e, idx)
153
157
  input type: "range", style: "position:absolute;opacity:0;width:0;height:0;pointer-events:none"
154
158
  id: "#{_id}-thumb-#{idx}"
155
159
  name: @name?!
@@ -161,5 +165,6 @@ export Slider = component
161
165
  aria-valuetext: if @valueText then @valueText(val, idx) else undefined
162
166
  aria-orientation: @orientation
163
167
  aria-disabled: @disabled?!
168
+ @keydown: (e) => @_onKeydown(e, idx)
164
169
 
165
170
  slot
@@ -9,8 +9,8 @@
9
9
  # Spinner label: "Saving...", size: "24px"
10
10
 
11
11
  export Spinner = component
12
- @label := 'Loading'
13
- @size := null
12
+ @label:: string := "Loading"
13
+ @size:: any := null
14
14
 
15
15
  render
16
16
  div role: "status", aria-label: @label
@@ -16,8 +16,8 @@
16
16
  # td "Engineer"
17
17
 
18
18
  export Table = component
19
- @caption := ''
20
- @striped := false
19
+ @caption:: string := ""
20
+ @striped:: boolean := false
21
21
 
22
22
  render
23
23
  div $striped: @striped?!
@@ -19,9 +19,9 @@
19
19
  # p "Content for tab two"
20
20
 
21
21
  export Tabs = component
22
- @active := null
23
- @orientation := 'horizontal'
24
- @activation := 'automatic'
22
+ @active:: any := null
23
+ @orientation:: "horizontal" | "vertical" := "horizontal"
24
+ @activation:: "automatic" | "manual" := "automatic"
25
25
  _ready := false
26
26
  _id =! "tabs-#{Math.random().toString(36).slice(2, 8)}"
27
27
  activationDirection := 'none'
@@ -53,6 +53,9 @@ export Tabs = component
53
53
 
54
54
  _isDisabled: (el) -> el?.hasAttribute('data-disabled')
55
55
 
56
+ _tabButtons: ->
57
+ Array.from(@_tablist?.querySelectorAll('[role="tab"]') or [])
58
+
56
59
  select: (id) ->
57
60
  prev = @active
58
61
  horiz = @orientation is 'horizontal'
@@ -74,17 +77,18 @@ export Tabs = component
74
77
  i = from
75
78
  loop len
76
79
  i = (i + dir) %% len
77
- tab = tabs.find (t) -> t.dataset.tab is ids[i]
80
+ tab = @_tabButtons().find (t) -> t.dataset.tab is ids[i]
78
81
  return ids[i] unless @_isDisabled(tab)
79
82
  ids[from]
80
83
 
81
84
  onKeydown: (e) ->
82
- ids = tabs.map (t) -> t.dataset.tab
85
+ buttons = @_tabButtons()
86
+ ids = buttons.map (t) -> t.dataset.tab
83
87
  idx = ids.indexOf @active
84
88
  return if idx is -1
85
89
  move = (nextId) =>
86
90
  return unless nextId
87
- tabs.find((t) -> t.dataset.tab is nextId)?.focus()
91
+ buttons.find((t) -> t.dataset.tab is nextId)?.focus()
88
92
  @select(nextId) if @activation is 'automatic'
89
93
  ARIA.rovingNav e, {
90
94
  next: => move(@_nextEnabled(ids, idx, 1))
@@ -96,9 +100,10 @@ export Tabs = component
96
100
 
97
101
  render
98
102
  .
99
- div role: "tablist", aria-orientation: @orientation, data-activation-direction: activationDirection, @keydown: @onKeydown
103
+ div ref: "_tablist", role: "tablist", aria-orientation: @orientation, data-activation-direction: activationDirection, @keydown: @onKeydown
100
104
  for tab in tabs
101
105
  button role: "tab"
106
+ data-tab: tab.dataset.tab
102
107
  id: "#{_id}-tab-#{tab.dataset.tab}"
103
108
  aria-selected: tab.dataset.tab is @active
104
109
  aria-controls: "#{_id}-panel-#{tab.dataset.tab}"
@@ -7,13 +7,13 @@
7
7
  # Textarea value <=> bio, placeholder: "Tell us about yourself"
8
8
  # Textarea value <=> notes, autoResize: true, rows: 3
9
9
 
10
- export Textarea = component
11
- @value := ''
12
- @placeholder := ''
13
- @disabled := false
14
- @required := false
15
- @rows := 3
16
- @autoResize := false
10
+ export Textarea = component extends textarea
11
+ @value:: string := ""
12
+ @placeholder:: string := ""
13
+ @disabled:: boolean := false
14
+ @required:: boolean := false
15
+ @rows:: number := 3
16
+ @autoResize:: boolean := false
17
17
 
18
18
  focused := false
19
19
  touched := false
@@ -35,7 +35,7 @@ export Textarea = component
35
35
  @_resize(@_root) if @autoResize and @value
36
36
 
37
37
  render
38
- textarea ref: "_root", value: @value, placeholder: @placeholder, rows: @rows
38
+ textarea value: @value, placeholder: @placeholder, rows: @rows
39
39
  disabled: @disabled
40
40
  required: @required
41
41
  aria-disabled: @disabled?!
@@ -19,8 +19,8 @@
19
19
  # ToastViewport toasts <=> toasts
20
20
 
21
21
  export ToastViewport = component
22
- @toasts := []
23
- @placement := 'bottom-right'
22
+ @toasts:: any[] := []
23
+ @placement:: "top-left" | "top-right" | "bottom-left" | "bottom-right" := "bottom-right"
24
24
 
25
25
  _onDismiss: (toast) ->
26
26
  @toasts = @toasts.filter (t) -> t isnt toast
@@ -31,7 +31,7 @@ export ToastViewport = component
31
31
  Toast toast: toast, @dismiss: (e) => @_onDismiss(e.detail)
32
32
 
33
33
  export Toast = component
34
- @toast := {}
34
+ @toast:: Record<string, any> := {}
35
35
 
36
36
  leaving := false
37
37
  _timer := null
@@ -11,10 +11,10 @@
11
11
  # div $value: "right", "Right"
12
12
 
13
13
  export ToggleGroup = component
14
- @value := null
15
- @disabled := false
16
- @multiple := false
17
- @orientation := 'horizontal'
14
+ @value:: any := null
15
+ @disabled:: boolean := false
16
+ @multiple:: boolean := false
17
+ @orientation:: "horizontal" | "vertical" := "horizontal"
18
18
 
19
19
  _items ~=
20
20
  return [] unless @_slot
@@ -40,18 +40,49 @@ export ToggleGroup = component
40
40
  @value = if val is @value then null else val
41
41
  @emit 'change', @value
42
42
 
43
+ _buttons: ->
44
+ Array.from(@_root?.querySelectorAll('button[aria-pressed]') or [])
45
+
46
+ _syncTabStops: (focusIdx = null) ->
47
+ buttons = @_buttons()
48
+ return unless buttons.length
49
+ idx = focusIdx
50
+ if idx is null
51
+ idx = buttons.indexOf(document.activeElement)
52
+ if idx < 0
53
+ idx = buttons.findIndex (btn) -> btn.getAttribute('aria-pressed') is 'true'
54
+ idx = 0 if idx < 0
55
+ buttons.forEach (btn, i) -> btn.tabIndex = if i is idx then 0 else -1
56
+
57
+ _focusIndex: (idx) ->
58
+ buttons = @_buttons()
59
+ return unless buttons.length
60
+ idx = Math.max(0, Math.min(idx, buttons.length - 1))
61
+ @_syncTabStops(idx)
62
+ buttons[idx]?.focus()
63
+
43
64
  onKeydown: (e) ->
44
- opts = @_root?.querySelectorAll('[data-value]')
65
+ opts = @_buttons()
45
66
  return unless opts?.length
46
67
  focused = Array.from(opts).indexOf(document.activeElement)
47
68
  return if focused < 0
48
69
  len = opts.length
49
70
  ARIA.rovingNav e, {
50
- next: => opts[(focused + 1) %% len]?.focus()
51
- prev: => opts[(focused - 1) %% len]?.focus()
52
- first: => opts[0]?.focus()
53
- last: => opts[len - 1]?.focus()
54
- }, 'both'
71
+ next: => @_focusIndex((focused + 1) %% len)
72
+ prev: => @_focusIndex((focused - 1) %% len)
73
+ first: => @_focusIndex(0)
74
+ last: => @_focusIndex(len - 1)
75
+ }, @orientation
76
+
77
+ onFocusin: (e) ->
78
+ buttons = @_buttons()
79
+ idx = buttons.indexOf(e.target)
80
+ @_syncTabStops(idx) if idx >= 0
81
+
82
+ mounted: ->
83
+ requestAnimationFrame => @_syncTabStops()
84
+
85
+ ~> @_syncTabStops()
55
86
 
56
87
  render
57
88
  div ref: "_root", role: "group", aria-orientation: @orientation
@@ -62,7 +93,7 @@ export ToggleGroup = component
62
93
  slot
63
94
 
64
95
  for item, idx in _items
65
- button tabindex: (if idx is 0 then "0" else "-1")
96
+ button tabindex: "-1"
66
97
  aria-pressed: !!@_isPressed(item)
67
98
  $pressed: @_isPressed(item)?!
68
99
  $disabled: @disabled?!
@@ -8,8 +8,8 @@
8
8
  # "Bold"
9
9
 
10
10
  export Toggle = component
11
- @pressed := false
12
- @disabled := false
11
+ @pressed:: boolean := false
12
+ @disabled:: boolean := false
13
13
 
14
14
  onClick: ->
15
15
  return if @disabled
@@ -11,8 +11,8 @@
11
11
  # Toggle pressed <=> isBold, "Bold"
12
12
 
13
13
  export Toolbar = component
14
- @orientation := 'horizontal'
15
- @label := ''
14
+ @orientation:: "horizontal" | "vertical" := "horizontal"
15
+ @label:: string := ""
16
16
 
17
17
  _getFocusable: ->
18
18
  return [] unless @_root
@@ -12,38 +12,24 @@ lastCloseTime = 0
12
12
  GROUP_TIMEOUT = 400
13
13
 
14
14
  export Tooltip = component
15
- @text := ''
16
- @placement := 'top'
17
- @delay := 300
18
- @offset := 6
19
- @hoverable := false
15
+ @text:: string := ""
16
+ @placement:: "top" | "top-start" | "top-end" | "bottom" | "bottom-start" | "bottom-end" | "left" | "right" := "top"
17
+ @delay:: number := 300
18
+ @offset:: number := 6
19
+ @hoverable:: boolean := false
20
20
 
21
21
  open := false
22
22
  entering := false
23
23
  exiting := false
24
24
  _showTimer := null
25
25
  _hideTimer := null
26
+ _ready := false
26
27
  _id =! "tip-#{Math.random().toString(36).slice(2, 8)}"
27
28
 
28
29
  _applyPlacement: ->
29
- return unless @_tip
30
30
  [side, align] = @placement.split('-')
31
- align ?= 'center'
32
- @_tip.style.position = 'fixed'
33
- @_tip.style.inset = 'auto'
34
- @_tip.style.margin = '0'
35
- @_tip.style.positionArea = "#{side} #{align}"
36
- @_tip.style.positionTry = 'flip-block, flip-inline, flip-block flip-inline'
37
- @_tip.style.positionVisibility = 'anchors-visible'
38
- @_tip.style.marginTop = ''
39
- @_tip.style.marginRight = ''
40
- @_tip.style.marginBottom = ''
41
- @_tip.style.marginLeft = ''
42
- switch side
43
- when 'bottom' then @_tip.style.marginTop = "#{@offset}px"
44
- when 'top' then @_tip.style.marginBottom = "#{@offset}px"
45
- when 'left' then @_tip.style.marginRight = "#{@offset}px"
46
- when 'right' then @_tip.style.marginLeft = "#{@offset}px"
31
+ align ??= 'center'
32
+ ARIA.position @_trigger, @_tip, placement: "#{side} #{align}", offset: @offset
47
33
 
48
34
  show: ->
49
35
  clearTimeout _hideTimer if _hideTimer
@@ -73,7 +59,11 @@ export Tooltip = component
73
59
  clearTimeout _showTimer if _showTimer
74
60
  clearTimeout _hideTimer if _hideTimer
75
61
 
62
+ mounted: ->
63
+ _ready = true
64
+
76
65
  ~>
66
+ return unless _ready
77
67
  if @_tip
78
68
  @_tip.setAttribute 'popover', 'hint'
79
69
  @_applyPlacement()
@@ -90,7 +80,13 @@ export Tooltip = component
90
80
  @focusout: @hide
91
81
  slot
92
82
 
93
- div ref: "_tip", id: _id, role: "tooltip", style: "position:fixed;margin:0;inset:auto"
83
+ div ref: "_tip"
84
+ id: _id
85
+ role: "tooltip"
86
+ popover: "hint"
87
+ hidden: not open
88
+ aria-hidden: (open ? undefined : "true")
89
+ style: "position:fixed;margin:0;inset:auto"
94
90
  $open: open?!
95
91
  $entering: entering?!
96
92
  $exiting: exiting?!