@rip-lang/ui 0.3.20 → 0.3.21
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/README.md +442 -572
- package/accordion.rip +113 -0
- package/alert-dialog.rip +96 -0
- package/autocomplete.rip +141 -0
- package/avatar.rip +37 -0
- package/badge.rip +15 -0
- package/breadcrumb.rip +46 -0
- package/button-group.rip +26 -0
- package/button.rip +23 -0
- package/card.rip +25 -0
- package/carousel.rip +110 -0
- package/checkbox-group.rip +65 -0
- package/checkbox.rip +33 -0
- package/collapsible.rip +50 -0
- package/combobox.rip +155 -0
- package/context-menu.rip +105 -0
- package/date-picker.rip +214 -0
- package/dialog.rip +107 -0
- package/drawer.rip +79 -0
- package/editable-value.rip +80 -0
- package/field.rip +53 -0
- package/fieldset.rip +22 -0
- package/form.rip +39 -0
- package/grid.rip +901 -0
- package/index.rip +16 -0
- package/input-group.rip +28 -0
- package/input.rip +36 -0
- package/label.rip +16 -0
- package/menu.rip +162 -0
- package/menubar.rip +155 -0
- package/meter.rip +36 -0
- package/multi-select.rip +158 -0
- package/native-select.rip +32 -0
- package/nav-menu.rip +129 -0
- package/number-field.rip +162 -0
- package/otp-field.rip +89 -0
- package/package.json +18 -27
- package/pagination.rip +123 -0
- package/popover.rip +143 -0
- package/preview-card.rip +73 -0
- package/progress.rip +25 -0
- package/radio-group.rip +67 -0
- package/resizable.rip +123 -0
- package/scroll-area.rip +145 -0
- package/select.rip +184 -0
- package/separator.rip +17 -0
- package/skeleton.rip +22 -0
- package/slider.rip +165 -0
- package/spinner.rip +17 -0
- package/table.rip +27 -0
- package/tabs.rip +124 -0
- package/textarea.rip +48 -0
- package/toast.rip +87 -0
- package/toggle-group.rip +78 -0
- package/toggle.rip +24 -0
- package/toolbar.rip +46 -0
- package/tooltip.rip +115 -0
- package/dist/rip-ui.min.js +0 -522
- package/dist/rip-ui.min.js.br +0 -0
- package/serve.rip +0 -92
- package/ui.rip +0 -964
package/dialog.rip
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Dialog — accessible headless modal dialog
|
|
2
|
+
#
|
|
3
|
+
# Traps focus, locks scroll, dismisses on Escape or click outside.
|
|
4
|
+
# Restores focus to the previously focused element on close.
|
|
5
|
+
# Auto-wires aria-labelledby (first h1-h6) and aria-describedby (first p).
|
|
6
|
+
#
|
|
7
|
+
# Exposes $open on the backdrop. Ships zero CSS.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# Dialog open <=> showDialog, @close: handleClose
|
|
11
|
+
# h2 "Title"
|
|
12
|
+
# p "Content"
|
|
13
|
+
# button @click: (=> showDialog = false), "Close"
|
|
14
|
+
|
|
15
|
+
dialogStack = []
|
|
16
|
+
|
|
17
|
+
export Dialog = component
|
|
18
|
+
@open := false
|
|
19
|
+
@dismissable := true
|
|
20
|
+
@initialFocus := null
|
|
21
|
+
|
|
22
|
+
_prevFocus = null
|
|
23
|
+
_cleanupTrap = null
|
|
24
|
+
_scrollY = 0
|
|
25
|
+
_id =! "dlg-#{Math.random().toString(36).slice(2, 8)}"
|
|
26
|
+
|
|
27
|
+
_wireAria: ->
|
|
28
|
+
panel = @_panel
|
|
29
|
+
return unless panel
|
|
30
|
+
heading = panel.querySelector('h1,h2,h3,h4,h5,h6')
|
|
31
|
+
if heading
|
|
32
|
+
heading.id ?= "#{_id}-title"
|
|
33
|
+
panel.setAttribute 'aria-labelledby', heading.id
|
|
34
|
+
desc = panel.querySelector('p')
|
|
35
|
+
if desc
|
|
36
|
+
desc.id ?= "#{_id}-desc"
|
|
37
|
+
panel.setAttribute 'aria-describedby', desc.id
|
|
38
|
+
|
|
39
|
+
~>
|
|
40
|
+
if @open
|
|
41
|
+
_prevFocus = document.activeElement
|
|
42
|
+
_scrollY = window.scrollY
|
|
43
|
+
dialogStack.push this
|
|
44
|
+
document.body.style.position = 'fixed'
|
|
45
|
+
document.body.style.top = "-#{_scrollY}px"
|
|
46
|
+
document.body.style.width = '100%'
|
|
47
|
+
|
|
48
|
+
setTimeout =>
|
|
49
|
+
panel = @_panel
|
|
50
|
+
if panel
|
|
51
|
+
@_wireAria()
|
|
52
|
+
if @initialFocus
|
|
53
|
+
target = if typeof @initialFocus is 'string' then panel.querySelector(@initialFocus) else @initialFocus
|
|
54
|
+
target?.focus()
|
|
55
|
+
else
|
|
56
|
+
focusable = panel.querySelectorAll 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])'
|
|
57
|
+
focusable[0]?.focus()
|
|
58
|
+
_cleanupTrap = (e) ->
|
|
59
|
+
return unless e.key is 'Tab'
|
|
60
|
+
list = Array.from(panel.querySelectorAll('a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])')).filter (f) -> f.offsetParent isnt null
|
|
61
|
+
return unless list.length
|
|
62
|
+
first = list[0]
|
|
63
|
+
last = list[list.length - 1]
|
|
64
|
+
if e.shiftKey
|
|
65
|
+
if document.activeElement is first then (e.preventDefault(); last.focus())
|
|
66
|
+
else
|
|
67
|
+
if document.activeElement is last then (e.preventDefault(); first.focus())
|
|
68
|
+
panel.addEventListener 'keydown', _cleanupTrap
|
|
69
|
+
, 0
|
|
70
|
+
|
|
71
|
+
return ->
|
|
72
|
+
idx = dialogStack.indexOf this
|
|
73
|
+
dialogStack.splice(idx, 1) if idx >= 0
|
|
74
|
+
document.body.style.position = '' unless dialogStack.length
|
|
75
|
+
document.body.style.top = '' unless dialogStack.length
|
|
76
|
+
document.body.style.width = '' unless dialogStack.length
|
|
77
|
+
window.scrollTo 0, _scrollY unless dialogStack.length
|
|
78
|
+
_prevFocus?.focus()
|
|
79
|
+
else
|
|
80
|
+
idx = dialogStack.indexOf this
|
|
81
|
+
dialogStack.splice(idx, 1) if idx >= 0
|
|
82
|
+
unless dialogStack.length
|
|
83
|
+
document.body.style.position = ''
|
|
84
|
+
document.body.style.top = ''
|
|
85
|
+
document.body.style.width = ''
|
|
86
|
+
window.scrollTo 0, _scrollY
|
|
87
|
+
_prevFocus?.focus()
|
|
88
|
+
|
|
89
|
+
close: ->
|
|
90
|
+
@open = false
|
|
91
|
+
@emit 'close'
|
|
92
|
+
|
|
93
|
+
onKeydown: (e) ->
|
|
94
|
+
if e.key is 'Escape' and dialogStack[dialogStack.length - 1] is this
|
|
95
|
+
e.preventDefault()
|
|
96
|
+
@close() if @dismissable
|
|
97
|
+
|
|
98
|
+
onBackdropClick: (e) ->
|
|
99
|
+
@close() if e.target is e.currentTarget and @dismissable
|
|
100
|
+
|
|
101
|
+
render
|
|
102
|
+
if @open
|
|
103
|
+
div ref: "_backdrop", $open: true,
|
|
104
|
+
@click: @onBackdropClick,
|
|
105
|
+
@keydown: @onKeydown
|
|
106
|
+
div ref: "_panel", role: "dialog", aria-modal: "true", tabindex: "-1"
|
|
107
|
+
slot
|
package/drawer.rip
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Drawer — accessible headless slide-out panel
|
|
2
|
+
#
|
|
3
|
+
# A Dialog variant that slides from an edge of the screen.
|
|
4
|
+
# Supports dismiss on escape, click-outside, and optional swipe-to-close.
|
|
5
|
+
# Ships zero CSS.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# Drawer open <=> showDrawer, side: "right"
|
|
9
|
+
# h2 "Settings"
|
|
10
|
+
# p "Panel content here"
|
|
11
|
+
|
|
12
|
+
export Drawer = component
|
|
13
|
+
@open := false
|
|
14
|
+
@side := 'right'
|
|
15
|
+
@dismissable := true
|
|
16
|
+
|
|
17
|
+
_prevFocus = null
|
|
18
|
+
_scrollY = 0
|
|
19
|
+
_id =! "drw-#{Math.random().toString(36).slice(2, 8)}"
|
|
20
|
+
|
|
21
|
+
_wireAria: ->
|
|
22
|
+
panel = @_panel
|
|
23
|
+
return unless panel
|
|
24
|
+
heading = panel.querySelector('h1,h2,h3,h4,h5,h6')
|
|
25
|
+
if heading
|
|
26
|
+
heading.id ?= "#{_id}-title"
|
|
27
|
+
panel.setAttribute 'aria-labelledby', heading.id
|
|
28
|
+
desc = panel.querySelector('p')
|
|
29
|
+
if desc
|
|
30
|
+
desc.id ?= "#{_id}-desc"
|
|
31
|
+
panel.setAttribute 'aria-describedby', desc.id
|
|
32
|
+
|
|
33
|
+
~>
|
|
34
|
+
if @open
|
|
35
|
+
_prevFocus = document.activeElement
|
|
36
|
+
document.body.style.overflow = 'hidden'
|
|
37
|
+
|
|
38
|
+
setTimeout =>
|
|
39
|
+
panel = @_panel
|
|
40
|
+
if panel
|
|
41
|
+
@_wireAria()
|
|
42
|
+
focusable = panel.querySelectorAll 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])'
|
|
43
|
+
focusable[0]?.focus()
|
|
44
|
+
@_trapHandler = (e) ->
|
|
45
|
+
return unless e.key is 'Tab'
|
|
46
|
+
list = Array.from(panel.querySelectorAll('a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])')).filter (f) -> f.offsetParent isnt null
|
|
47
|
+
return unless list.length
|
|
48
|
+
first = list[0]
|
|
49
|
+
last = list[list.length - 1]
|
|
50
|
+
if e.shiftKey
|
|
51
|
+
if document.activeElement is first then (e.preventDefault(); last.focus())
|
|
52
|
+
else
|
|
53
|
+
if document.activeElement is last then (e.preventDefault(); first.focus())
|
|
54
|
+
panel.addEventListener 'keydown', @_trapHandler
|
|
55
|
+
, 0
|
|
56
|
+
else
|
|
57
|
+
document.body.style.overflow = ''
|
|
58
|
+
_prevFocus?.focus()
|
|
59
|
+
|
|
60
|
+
close: ->
|
|
61
|
+
@open = false
|
|
62
|
+
@emit 'close'
|
|
63
|
+
|
|
64
|
+
onKeydown: (e) ->
|
|
65
|
+
if e.key is 'Escape' and @dismissable
|
|
66
|
+
e.preventDefault()
|
|
67
|
+
@close()
|
|
68
|
+
|
|
69
|
+
onBackdropClick: (e) ->
|
|
70
|
+
@close() if e.target is e.currentTarget and @dismissable
|
|
71
|
+
|
|
72
|
+
render
|
|
73
|
+
if @open
|
|
74
|
+
div ref: "_backdrop", $open: true, $side: @side
|
|
75
|
+
@click: @onBackdropClick
|
|
76
|
+
@keydown: @onKeydown
|
|
77
|
+
div ref: "_panel", role: "dialog", aria-modal: "true", tabindex: "-1"
|
|
78
|
+
$side: @side
|
|
79
|
+
slot
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# EditableValue — accessible headless inline editable value
|
|
2
|
+
#
|
|
3
|
+
# Displays a value with an edit trigger. Clicking opens a popover form.
|
|
4
|
+
# Emits 'save' on submit. Ships zero CSS.
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# EditableValue @save: handleSave
|
|
8
|
+
# span $display: true
|
|
9
|
+
# "John Doe"
|
|
10
|
+
# div $editor: true
|
|
11
|
+
# input type: "text", value: name, @input: (e) => name = e.target.value
|
|
12
|
+
|
|
13
|
+
export EditableValue = component
|
|
14
|
+
@disabled := false
|
|
15
|
+
|
|
16
|
+
editing := false
|
|
17
|
+
saving := false
|
|
18
|
+
|
|
19
|
+
_onEdit: ->
|
|
20
|
+
return if @disabled
|
|
21
|
+
editing = true
|
|
22
|
+
requestAnimationFrame => @_position()
|
|
23
|
+
|
|
24
|
+
_onSave: ->
|
|
25
|
+
return if saving
|
|
26
|
+
saving = true
|
|
27
|
+
@emit 'save'
|
|
28
|
+
|
|
29
|
+
_onCancel: ->
|
|
30
|
+
editing = false
|
|
31
|
+
saving = false
|
|
32
|
+
|
|
33
|
+
close: ->
|
|
34
|
+
editing = false
|
|
35
|
+
saving = false
|
|
36
|
+
|
|
37
|
+
setSaving: (val) -> saving = val
|
|
38
|
+
|
|
39
|
+
_position: ->
|
|
40
|
+
display = @_root?.querySelector('[data-display]')
|
|
41
|
+
editor = @_root?.querySelector('[data-editor]')
|
|
42
|
+
return unless display and editor
|
|
43
|
+
@_root.style.position = 'relative'
|
|
44
|
+
dr = display.getBoundingClientRect()
|
|
45
|
+
cr = @_root.getBoundingClientRect()
|
|
46
|
+
editor.style.position = 'absolute'
|
|
47
|
+
editor.style.left = "0px"
|
|
48
|
+
editor.style.top = "#{dr.bottom - cr.top + 4}px"
|
|
49
|
+
editor.style.zIndex = '50'
|
|
50
|
+
editor.querySelector('input, textarea, select')?.focus()
|
|
51
|
+
|
|
52
|
+
~>
|
|
53
|
+
display = @_root?.querySelector('[data-display]')
|
|
54
|
+
editor = @_root?.querySelector('[data-editor]')
|
|
55
|
+
return unless display and editor
|
|
56
|
+
editor.hidden = not editing
|
|
57
|
+
if editing
|
|
58
|
+
editor.setAttribute 'data-open', ''
|
|
59
|
+
onDown = (e) =>
|
|
60
|
+
unless @_root?.contains(e.target)
|
|
61
|
+
@_onCancel()
|
|
62
|
+
document.addEventListener 'mousedown', onDown
|
|
63
|
+
return -> document.removeEventListener 'mousedown', onDown
|
|
64
|
+
else
|
|
65
|
+
editor.removeAttribute 'data-open'
|
|
66
|
+
|
|
67
|
+
onKeydown: (e) ->
|
|
68
|
+
if e.key is 'Escape' and editing
|
|
69
|
+
e.preventDefault()
|
|
70
|
+
@_onCancel()
|
|
71
|
+
if e.key is 'Enter' and editing
|
|
72
|
+
e.preventDefault()
|
|
73
|
+
@_onSave()
|
|
74
|
+
|
|
75
|
+
render
|
|
76
|
+
div ref: "_root", $editing: editing?!, $disabled: @disabled?!, $saving: saving?!
|
|
77
|
+
slot
|
|
78
|
+
unless editing
|
|
79
|
+
button $edit-trigger: true, aria-label: "Edit", @click: @_onEdit
|
|
80
|
+
"✎"
|
package/field.rip
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Field — accessible headless form field wrapper
|
|
2
|
+
#
|
|
3
|
+
# Associates a label, description, and error message with a form control.
|
|
4
|
+
# Generates linked IDs for aria-labelledby/describedby/errormessage.
|
|
5
|
+
# Ships zero CSS.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# Field label: "Email", error: errors.email
|
|
9
|
+
# Input value <=> email, type: "email"
|
|
10
|
+
|
|
11
|
+
export Field = component
|
|
12
|
+
@label := ''
|
|
13
|
+
@description := ''
|
|
14
|
+
@error := ''
|
|
15
|
+
@disabled := false
|
|
16
|
+
@required := false
|
|
17
|
+
|
|
18
|
+
_id =! "fld-#{Math.random().toString(36).slice(2, 8)}"
|
|
19
|
+
|
|
20
|
+
mounted: ->
|
|
21
|
+
ctrl = @_root?.querySelector('input, select, textarea, button, [role]')
|
|
22
|
+
if ctrl
|
|
23
|
+
ctrl.setAttribute 'aria-labelledby', "#{_id}-label" if @label
|
|
24
|
+
ctrl.setAttribute 'aria-describedby', "#{_id}-desc" if @description
|
|
25
|
+
ctrl.setAttribute 'aria-errormessage', "#{_id}-err" if @error
|
|
26
|
+
ctrl.setAttribute 'aria-invalid', true if @error
|
|
27
|
+
ctrl.setAttribute 'aria-required', true if @required
|
|
28
|
+
|
|
29
|
+
~>
|
|
30
|
+
ctrl = @_root?.querySelector('input, select, textarea, button, [role]')
|
|
31
|
+
return unless ctrl
|
|
32
|
+
if @error
|
|
33
|
+
ctrl.setAttribute 'aria-invalid', true
|
|
34
|
+
ctrl.setAttribute 'aria-errormessage', "#{_id}-err"
|
|
35
|
+
else
|
|
36
|
+
ctrl.removeAttribute 'aria-invalid'
|
|
37
|
+
ctrl.removeAttribute 'aria-errormessage'
|
|
38
|
+
|
|
39
|
+
render
|
|
40
|
+
div $disabled: @disabled?!, $invalid: @error?!
|
|
41
|
+
if @label
|
|
42
|
+
label id: "#{_id}-label", $label: true
|
|
43
|
+
@label
|
|
44
|
+
if @required
|
|
45
|
+
span $required: true, aria-hidden: "true"
|
|
46
|
+
" *"
|
|
47
|
+
slot
|
|
48
|
+
if @description and not @error
|
|
49
|
+
div id: "#{_id}-desc", $description: true
|
|
50
|
+
@description
|
|
51
|
+
if @error
|
|
52
|
+
div id: "#{_id}-err", role: "alert", $error: true
|
|
53
|
+
@error
|
package/fieldset.rip
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Fieldset — accessible headless fieldset with legend
|
|
2
|
+
#
|
|
3
|
+
# Groups related fields with an optional legend. Disables all children
|
|
4
|
+
# when @disabled is set. Ships zero CSS.
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# Fieldset legend: "Shipping Address"
|
|
8
|
+
# Field label: "Street"
|
|
9
|
+
# Input value <=> street
|
|
10
|
+
# Field label: "City"
|
|
11
|
+
# Input value <=> city
|
|
12
|
+
|
|
13
|
+
export Fieldset = component
|
|
14
|
+
@legend := ''
|
|
15
|
+
@disabled := false
|
|
16
|
+
|
|
17
|
+
render
|
|
18
|
+
fieldset disabled: @disabled, $disabled: @disabled?!
|
|
19
|
+
if @legend
|
|
20
|
+
legend $legend: true
|
|
21
|
+
@legend
|
|
22
|
+
slot
|
package/form.rip
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Form — accessible headless form with validation and submission
|
|
2
|
+
#
|
|
3
|
+
# Wraps native <form> with submit handling, validation state, and
|
|
4
|
+
# loading indicator support. Prevents default submission and emits
|
|
5
|
+
# a 'submit' event. Ships zero CSS.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# Form @submit: handleSubmit
|
|
9
|
+
# Field label: "Name"
|
|
10
|
+
# Input value <=> name
|
|
11
|
+
# Button
|
|
12
|
+
# "Submit"
|
|
13
|
+
|
|
14
|
+
export Form = component
|
|
15
|
+
@disabled := false
|
|
16
|
+
|
|
17
|
+
submitting := false
|
|
18
|
+
submitted := false
|
|
19
|
+
errors := {}
|
|
20
|
+
|
|
21
|
+
_onSubmit: (e) ->
|
|
22
|
+
e.preventDefault()
|
|
23
|
+
return if @disabled or submitting
|
|
24
|
+
submitting = true
|
|
25
|
+
submitted = true
|
|
26
|
+
@emit 'submit', { form: e.target }
|
|
27
|
+
|
|
28
|
+
setErrors: (errs) ->
|
|
29
|
+
errors = errs or {}
|
|
30
|
+
|
|
31
|
+
setSubmitting: (val) ->
|
|
32
|
+
submitting = val
|
|
33
|
+
|
|
34
|
+
render
|
|
35
|
+
form @submit: @_onSubmit, novalidate: true
|
|
36
|
+
$disabled: @disabled?!
|
|
37
|
+
$submitting: submitting?!
|
|
38
|
+
$submitted: submitted?!
|
|
39
|
+
slot
|