@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.
- package/AGENTS.md +93 -0
- package/README.md +22 -625
- package/browser/AGENTS.md +213 -0
- package/browser/CONTRIBUTING.md +375 -0
- package/browser/README.md +11 -0
- package/browser/TESTING.md +59 -0
- package/browser/browser.rip +56 -0
- package/{components → browser/components}/accordion.rip +1 -1
- package/{components → browser/components}/alert-dialog.rip +6 -3
- package/{components → browser/components}/autocomplete.rip +27 -21
- package/{components → browser/components}/avatar.rip +3 -3
- package/{components → browser/components}/badge.rip +1 -1
- package/{components → browser/components}/breadcrumb.rip +2 -2
- package/{components → browser/components}/button-group.rip +3 -3
- package/{components → browser/components}/button.rip +2 -2
- package/{components → browser/components}/card.rip +1 -1
- package/{components → browser/components}/carousel.rip +5 -5
- package/{components → browser/components}/checkbox-group.rip +40 -11
- package/{components → browser/components}/checkbox.rip +4 -4
- package/{components → browser/components}/collapsible.rip +2 -2
- package/{components → browser/components}/combobox.rip +36 -23
- package/{components → browser/components}/context-menu.rip +1 -1
- package/{components → browser/components}/date-picker.rip +5 -5
- package/{components → browser/components}/dialog.rip +8 -4
- package/{components → browser/components}/drawer.rip +8 -4
- package/{components → browser/components}/editable-value.rip +7 -1
- package/{components → browser/components}/field.rip +5 -5
- package/{components → browser/components}/fieldset.rip +2 -2
- package/{components → browser/components}/form.rip +1 -1
- package/{components → browser/components}/grid.rip +8 -8
- package/{components → browser/components}/input-group.rip +1 -1
- package/{components → browser/components}/input.rip +6 -6
- package/{components → browser/components}/label.rip +2 -2
- package/{components → browser/components}/menu.rip +17 -10
- package/{components → browser/components}/menubar.rip +1 -1
- package/{components → browser/components}/meter.rip +7 -7
- package/{components → browser/components}/multi-select.rip +76 -33
- package/{components → browser/components}/native-select.rip +3 -3
- package/{components → browser/components}/nav-menu.rip +3 -3
- package/{components → browser/components}/number-field.rip +11 -11
- package/{components → browser/components}/otp-field.rip +4 -4
- package/{components → browser/components}/pagination.rip +4 -4
- package/{components → browser/components}/popover.rip +11 -24
- package/{components → browser/components}/preview-card.rip +7 -11
- package/{components → browser/components}/progress.rip +3 -3
- package/{components → browser/components}/radio-group.rip +4 -4
- package/{components → browser/components}/resizable.rip +3 -3
- package/{components → browser/components}/scroll-area.rip +1 -1
- package/{components → browser/components}/select.rip +55 -27
- package/{components → browser/components}/separator.rip +2 -2
- package/{components → browser/components}/skeleton.rip +4 -4
- package/{components → browser/components}/slider.rip +15 -10
- package/{components → browser/components}/spinner.rip +2 -2
- package/{components → browser/components}/table.rip +2 -2
- package/{components → browser/components}/tabs.rip +12 -7
- package/{components → browser/components}/textarea.rip +8 -8
- package/{components → browser/components}/toast.rip +3 -3
- package/{components → browser/components}/toggle-group.rip +42 -11
- package/{components → browser/components}/toggle.rip +2 -2
- package/{components → browser/components}/toolbar.rip +2 -2
- package/{components → browser/components}/tooltip.rip +19 -23
- package/browser/hljs-rip.js +209 -0
- package/browser/playwright.config.mjs +31 -0
- package/browser/tests/overlays.js +349 -0
- package/email/AGENTS.md +16 -0
- package/email/README.md +55 -0
- package/email/benchmarks/benchmark.rip +94 -0
- package/email/benchmarks/samples.rip +104 -0
- package/email/compat.rip +129 -0
- package/email/components.rip +371 -0
- package/email/dom.rip +330 -0
- package/email/email.rip +10 -0
- package/email/render.rip +82 -0
- package/package.json +29 -39
- package/shared/README.md +3 -0
- package/shared/styles.rip +17 -0
- package/tailwind/AGENTS.md +3 -0
- package/tailwind/README.md +27 -0
- package/tailwind/engine.js +107 -0
- package/tailwind/inline.js +215 -0
- package/tailwind/serve.js +6 -0
- package/tailwind/tailwind.rip +13 -0
- 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
|
|
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 :=
|
|
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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
@
|
|
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"
|
|
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 :=
|
|
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 :=
|
|
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 :=
|
|
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
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
# p "Content for tab two"
|
|
20
20
|
|
|
21
21
|
export Tabs = component
|
|
22
|
-
@active
|
|
23
|
-
@orientation :=
|
|
24
|
-
@activation
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 :=
|
|
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 :=
|
|
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 = @
|
|
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: =>
|
|
51
|
-
prev: =>
|
|
52
|
-
first: =>
|
|
53
|
-
last: =>
|
|
54
|
-
},
|
|
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:
|
|
96
|
+
button tabindex: "-1"
|
|
66
97
|
aria-pressed: !!@_isPressed(item)
|
|
67
98
|
$pressed: @_isPressed(item)?!
|
|
68
99
|
$disabled: @disabled?!
|
|
@@ -12,38 +12,24 @@ lastCloseTime = 0
|
|
|
12
12
|
GROUP_TIMEOUT = 400
|
|
13
13
|
|
|
14
14
|
export Tooltip = component
|
|
15
|
-
@text :=
|
|
16
|
-
@placement :=
|
|
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
|
|
32
|
-
|
|
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"
|
|
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?!
|