@rip-lang/ui 0.3.19 → 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.
Files changed (61) hide show
  1. package/README.md +443 -576
  2. package/accordion.rip +113 -0
  3. package/alert-dialog.rip +96 -0
  4. package/autocomplete.rip +141 -0
  5. package/avatar.rip +37 -0
  6. package/badge.rip +15 -0
  7. package/breadcrumb.rip +46 -0
  8. package/button-group.rip +26 -0
  9. package/button.rip +23 -0
  10. package/card.rip +25 -0
  11. package/carousel.rip +110 -0
  12. package/checkbox-group.rip +65 -0
  13. package/checkbox.rip +33 -0
  14. package/collapsible.rip +50 -0
  15. package/combobox.rip +155 -0
  16. package/context-menu.rip +105 -0
  17. package/date-picker.rip +214 -0
  18. package/dialog.rip +107 -0
  19. package/drawer.rip +79 -0
  20. package/editable-value.rip +80 -0
  21. package/field.rip +53 -0
  22. package/fieldset.rip +22 -0
  23. package/form.rip +39 -0
  24. package/grid.rip +901 -0
  25. package/index.rip +16 -0
  26. package/input-group.rip +28 -0
  27. package/input.rip +36 -0
  28. package/label.rip +16 -0
  29. package/menu.rip +162 -0
  30. package/menubar.rip +155 -0
  31. package/meter.rip +36 -0
  32. package/multi-select.rip +158 -0
  33. package/native-select.rip +32 -0
  34. package/nav-menu.rip +129 -0
  35. package/number-field.rip +162 -0
  36. package/otp-field.rip +89 -0
  37. package/package.json +18 -27
  38. package/pagination.rip +123 -0
  39. package/popover.rip +143 -0
  40. package/preview-card.rip +73 -0
  41. package/progress.rip +25 -0
  42. package/radio-group.rip +67 -0
  43. package/resizable.rip +123 -0
  44. package/scroll-area.rip +145 -0
  45. package/select.rip +184 -0
  46. package/separator.rip +17 -0
  47. package/skeleton.rip +22 -0
  48. package/slider.rip +165 -0
  49. package/spinner.rip +17 -0
  50. package/table.rip +27 -0
  51. package/tabs.rip +124 -0
  52. package/textarea.rip +48 -0
  53. package/toast.rip +87 -0
  54. package/toggle-group.rip +78 -0
  55. package/toggle.rip +24 -0
  56. package/toolbar.rip +46 -0
  57. package/tooltip.rip +115 -0
  58. package/dist/rip-ui.min.js +0 -524
  59. package/dist/rip-ui.min.js.br +0 -0
  60. package/serve.rip +0 -92
  61. package/ui.rip +0 -964
@@ -0,0 +1,65 @@
1
+ # CheckboxGroup — accessible headless checkbox group
2
+ #
3
+ # Multiple options can be checked independently. Wraps individual checkboxes
4
+ # with group semantics. Value is an array of checked option values.
5
+ # Ships zero CSS.
6
+ #
7
+ # Usage:
8
+ # CheckboxGroup value <=> selectedToppings
9
+ # div $value: "cheese", "Cheese"
10
+ # div $value: "bacon", "Bacon"
11
+ # div $value: "lettuce", "Lettuce"
12
+
13
+ export CheckboxGroup = component
14
+ @value := []
15
+ @disabled := false
16
+ @orientation := 'vertical'
17
+ @label := ''
18
+
19
+ _options ~=
20
+ return [] unless @_slot
21
+ Array.from(@_slot.querySelectorAll('[data-value]') or [])
22
+
23
+ _isChecked: (val) ->
24
+ Array.isArray(@value) and val in @value
25
+
26
+ _toggle: (val) ->
27
+ return if @disabled
28
+ arr = if Array.isArray(@value) then [...@value] else []
29
+ if val in arr
30
+ arr = arr.filter (v) -> v isnt val
31
+ else
32
+ arr.push val
33
+ @value = arr
34
+ @emit 'change', @value
35
+
36
+ onKeydown: (e) ->
37
+ boxes = @_root?.querySelectorAll('[role="checkbox"]')
38
+ return unless boxes?.length
39
+ focused = Array.from(boxes).indexOf(document.activeElement)
40
+ return if focused < 0
41
+ len = boxes.length
42
+ switch e.key
43
+ when 'ArrowDown', 'ArrowRight'
44
+ e.preventDefault()
45
+ boxes[(focused + 1) %% len]?.focus()
46
+ when 'ArrowUp', 'ArrowLeft'
47
+ e.preventDefault()
48
+ boxes[(focused - 1) %% len]?.focus()
49
+
50
+ render
51
+ div ref: "_root", role: "group", aria-label: @label or undefined, aria-orientation: @orientation
52
+ $orientation: @orientation
53
+ $disabled: @disabled?!
54
+
55
+ . ref: "_slot", style: "display:none"
56
+ slot
57
+
58
+ for opt, idx in _options
59
+ button role: "checkbox", tabindex: (if idx is 0 then "0" else "-1")
60
+ aria-checked: !!@_isChecked(opt.dataset.value)
61
+ $checked: @_isChecked(opt.dataset.value)?!
62
+ $disabled: @disabled?!
63
+ $value: opt.dataset.value
64
+ @click: (=> @_toggle(opt.dataset.value))
65
+ = opt.textContent
package/checkbox.rip ADDED
@@ -0,0 +1,33 @@
1
+ # Checkbox — accessible headless checkbox/switch widget
2
+ #
3
+ # Toggles on click, Enter, or Space. Supports indeterminate state.
4
+ # Exposes $checked, $indeterminate, $disabled. Ships zero CSS.
5
+ # Set @switch to true for switch semantics (role="switch").
6
+ #
7
+ # Usage:
8
+ # Checkbox checked <=> isActive, @change: handleChange
9
+ # span "Enable notifications"
10
+ #
11
+ # Checkbox checked <=> isDark, switch: true
12
+ # span "Dark mode"
13
+
14
+ export Checkbox = component
15
+ @checked := false
16
+ @disabled := false
17
+ @indeterminate := false
18
+ @switch := false
19
+
20
+ onClick: ->
21
+ return if @disabled
22
+ @indeterminate = false
23
+ @checked = not @checked
24
+ @emit 'change', @checked
25
+
26
+ render
27
+ button role: @switch ? 'switch' : 'checkbox'
28
+ aria-checked: @indeterminate ? 'mixed' : !!@checked
29
+ aria-disabled: @disabled?!
30
+ $checked: @checked?!
31
+ $indeterminate: @indeterminate?!
32
+ $disabled: @disabled?!
33
+ slot
@@ -0,0 +1,50 @@
1
+ # Collapsible — accessible headless expand/collapse section
2
+ #
3
+ # Single open/close section. Simpler than Accordion (no item IDs,
4
+ # no single/multiple mode). Exposes content dimensions as CSS
5
+ # custom properties for animated expand/collapse. Ships zero CSS.
6
+ #
7
+ # Usage:
8
+ # Collapsible open <=> isOpen
9
+ # button $trigger: true, "Show details"
10
+ # div $content: true
11
+ # p "Hidden content here"
12
+
13
+ export Collapsible = component
14
+ @open := false
15
+ @disabled := false
16
+
17
+ _ready := false
18
+
19
+ mounted: ->
20
+ _ready = true
21
+ trigger = @_root?.querySelector('[data-trigger]')
22
+ return unless trigger
23
+ trigger.addEventListener 'click', => @toggle() unless @disabled
24
+ trigger.addEventListener 'keydown', (e) =>
25
+ if e.key in ['Enter', ' '] and not @disabled
26
+ e.preventDefault()
27
+ @toggle()
28
+
29
+ toggle: ->
30
+ @open = not @open
31
+ @emit 'change', @open
32
+
33
+ ~>
34
+ return unless _ready
35
+ trigger = @_root?.querySelector('[data-trigger]')
36
+ content = @_root?.querySelector('[data-content]')
37
+ if trigger
38
+ trigger.setAttribute 'aria-expanded', !!@open
39
+ trigger.setAttribute 'aria-disabled', true if @disabled
40
+ trigger.tabIndex = if @disabled then -1 else 0
41
+ if content
42
+ content.hidden = not @open
43
+ if @open
44
+ rect = content.getBoundingClientRect()
45
+ content.style.setProperty '--collapsible-height', "#{rect.height}px"
46
+ content.style.setProperty '--collapsible-width', "#{rect.width}px"
47
+
48
+ render
49
+ div ref: "_root", $open: @open?!, $disabled: @disabled?!
50
+ slot
package/combobox.rip ADDED
@@ -0,0 +1,155 @@
1
+ # Combobox — accessible headless combobox (input + filterable listbox)
2
+ #
3
+ # Keyboard: ArrowDown/Up to navigate, Enter to select, Escape to close,
4
+ # typing filters the list via the @filter callback.
5
+ #
6
+ # Exposes $open on the wrapper, $highlighted on options.
7
+ # Ships zero CSS — style entirely via attribute selectors in your stylesheet.
8
+ #
9
+ # Usage:
10
+ # Combobox query <=> searchText, items: fruits, @select: handleSelect, @filter: filterFn
11
+
12
+ export Combobox = component
13
+ @query := ''
14
+ @items := []
15
+ @placeholder := 'Search...'
16
+ @disabled := false
17
+ @autoHighlight := true
18
+
19
+ open := false
20
+ highlightedIndex := -1
21
+ _listId =! "cb-#{Math.random().toString(36).slice(2, 8)}"
22
+
23
+ getItems: ->
24
+ return [] unless @_list
25
+ Array.from(@_list.querySelectorAll('[role="option"]'))
26
+
27
+ _scrollToItem: ->
28
+ @getItems()[highlightedIndex]?.scrollIntoView({ block: 'nearest' })
29
+
30
+ clear: ->
31
+ @query = ''
32
+ highlightedIndex = -1
33
+ @emit 'filter', ''
34
+
35
+ onInput: (e) ->
36
+ @query = e.target.value
37
+ open = true
38
+ highlightedIndex = if @autoHighlight and @items.length > 0 then 0 else -1
39
+ @emit 'filter', @query
40
+
41
+ onFocusin: -> @openMenu()
42
+
43
+ onFocusout: (e) ->
44
+ unless @_content?.contains(e.relatedTarget)
45
+ @close()
46
+
47
+ openMenu: ->
48
+ open = true
49
+ highlightedIndex = -1
50
+ setTimeout => @_position(), 0
51
+
52
+ close: ->
53
+ open = false
54
+ highlightedIndex = -1
55
+
56
+ _position: ->
57
+ return unless @_input and @_list
58
+ tr = @_input.getBoundingClientRect()
59
+ @_list.style.left = "#{tr.left}px"
60
+ @_list.style.top = "#{tr.bottom + 2}px"
61
+ @_list.style.minWidth = "#{tr.width}px"
62
+ fl = @_list.getBoundingClientRect()
63
+ if fl.bottom > window.innerHeight
64
+ @_list.style.top = "#{tr.top - fl.height - 2}px"
65
+
66
+ isDisabled: (item) -> item?.hasAttribute?('data-disabled')
67
+
68
+ selectIndex: (idx) ->
69
+ item = @getItems()[idx]
70
+ return unless item
71
+ return if @isDisabled(item)
72
+ val = item.dataset.value ?? item.textContent.trim()
73
+ @query = val
74
+ @emit 'select', val
75
+ @close()
76
+ @_input?.blur()
77
+
78
+ _nextEnabled: (from, dir) ->
79
+ opts = @getItems()
80
+ len = opts.length
81
+ i = from
82
+ loop len
83
+ i = (i + dir) %% len
84
+ return i unless @isDisabled(opts[i])
85
+ from
86
+
87
+ _onKeydown: (e) ->
88
+ len = @getItems().length
89
+ switch e.key
90
+ when 'ArrowDown'
91
+ e.preventDefault()
92
+ @openMenu() unless open
93
+ highlightedIndex = @_nextEnabled(highlightedIndex, 1)
94
+ @_scrollToItem()
95
+ when 'ArrowUp'
96
+ e.preventDefault()
97
+ highlightedIndex = @_nextEnabled(highlightedIndex, -1)
98
+ @_scrollToItem()
99
+ when 'Enter'
100
+ e.preventDefault()
101
+ if highlightedIndex >= 0
102
+ @selectIndex(highlightedIndex)
103
+ else if len is 1
104
+ @selectIndex(0)
105
+ when 'Escape'
106
+ e.preventDefault()
107
+ if open then @close() else @query = ''
108
+ when 'Tab'
109
+ @close()
110
+
111
+ ~>
112
+ if open
113
+ onDown = (e) =>
114
+ root = @_content
115
+ unless root?.contains(e.target)
116
+ @close()
117
+ document.addEventListener 'mousedown', onDown
118
+ return -> document.removeEventListener 'mousedown', onDown
119
+
120
+ render
121
+ . ref: "_content", $open: open?!
122
+
123
+ # Text input
124
+ . style: "position:relative;display:inline-flex;align-items:center"
125
+ input ref: "_input", role: "combobox"
126
+ type: "text"
127
+ autocomplete: "off"
128
+ aria-expanded: !!open
129
+ aria-haspopup: "listbox"
130
+ aria-autocomplete: "list"
131
+ aria-controls: if open then _listId else undefined
132
+ aria-activedescendant: if highlightedIndex >= 0 then "#{_listId}-#{highlightedIndex}" else undefined
133
+ $disabled: @disabled?!
134
+ disabled: @disabled
135
+ placeholder: @placeholder
136
+ value: @query
137
+ @keydown: @_onKeydown
138
+ if @query
139
+ button aria-label: "Clear", $clear: true, @click: @clear
140
+ "✕"
141
+
142
+ # Listbox — conditionally rendered (like Select)
143
+ if open and @items.length > 0
144
+ div ref: "_list", id: _listId, role: "listbox", $open: true
145
+ style: "position:fixed"
146
+ for item, idx in @items
147
+ div role: "option", tabindex: "-1", id: "#{_listId}-#{idx}"
148
+ $value: item
149
+ $highlighted: (idx is highlightedIndex)?!
150
+ @click: (=> @selectIndex(idx))
151
+ @mouseenter: (=> highlightedIndex = idx)
152
+ "#{item}"
153
+ if open and @items.length is 0 and @query
154
+ div role: "status", aria-live: "polite", $empty: true
155
+ "No results"
@@ -0,0 +1,105 @@
1
+ # ContextMenu — accessible headless right-click menu
2
+ #
3
+ # Opens on contextmenu event (right-click) over the trigger area.
4
+ # Keyboard navigation matches Menu. Ships zero CSS.
5
+ #
6
+ # Usage:
7
+ # ContextMenu @select: handleAction
8
+ # div $trigger: true
9
+ # p "Right-click this area"
10
+ # div $item: "cut", "Cut"
11
+ # div $item: "copy", "Copy"
12
+ # div $item: "paste", "Paste"
13
+
14
+ export ContextMenu = component
15
+ @disabled := false
16
+
17
+ open := false
18
+ highlightedIndex := -1
19
+ posX := 0
20
+ posY := 0
21
+
22
+ _menuItems ~=
23
+ return [] unless @_slot
24
+ Array.from(@_slot.querySelectorAll('[data-item]') or [])
25
+
26
+ _triggerEl ~=
27
+ return null unless @_slot
28
+ @_slot.querySelector('[data-trigger]')
29
+
30
+ _onContextMenu: (e) ->
31
+ return if @disabled
32
+ e.preventDefault()
33
+ posX = e.clientX
34
+ posY = e.clientY
35
+ open = true
36
+ highlightedIndex = 0
37
+ requestAnimationFrame =>
38
+ @_list?.querySelectorAll('[role="menuitem"]')[0]?.focus()
39
+
40
+ close: ->
41
+ open = false
42
+ highlightedIndex = -1
43
+
44
+ selectIndex: (idx) ->
45
+ item = _menuItems[idx]
46
+ return unless item
47
+ return if item.dataset.disabled?
48
+ @emit 'select', item.dataset.item
49
+ @close()
50
+
51
+ _onKeydown: (e) ->
52
+ len = _menuItems.length
53
+ return unless len
54
+ switch e.key
55
+ when 'ArrowDown'
56
+ e.preventDefault()
57
+ highlightedIndex = (highlightedIndex + 1) %% len
58
+ @_list?.querySelectorAll('[role="menuitem"]')[highlightedIndex]?.focus()
59
+ when 'ArrowUp'
60
+ e.preventDefault()
61
+ highlightedIndex = (highlightedIndex - 1) %% len
62
+ @_list?.querySelectorAll('[role="menuitem"]')[highlightedIndex]?.focus()
63
+ when 'Home'
64
+ e.preventDefault()
65
+ highlightedIndex = 0
66
+ @_list?.querySelectorAll('[role="menuitem"]')[0]?.focus()
67
+ when 'End'
68
+ e.preventDefault()
69
+ highlightedIndex = len - 1
70
+ @_list?.querySelectorAll('[role="menuitem"]')[len - 1]?.focus()
71
+ when 'Enter', ' '
72
+ e.preventDefault()
73
+ @selectIndex(highlightedIndex)
74
+ when 'Escape', 'Tab'
75
+ e.preventDefault() if e.key is 'Escape'
76
+ @close()
77
+
78
+ ~>
79
+ if open
80
+ onDown = (e) =>
81
+ unless @_list?.contains(e.target)
82
+ @close()
83
+ document.addEventListener 'mousedown', onDown
84
+ return -> document.removeEventListener 'mousedown', onDown
85
+
86
+ render
87
+ . @contextmenu: @_onContextMenu
88
+
89
+ . ref: "_slot", style: "display:none"
90
+ slot
91
+
92
+ if _triggerEl
93
+ . innerHTML: _triggerEl.innerHTML
94
+
95
+ if open
96
+ div ref: "_list", role: "menu", $open: true, @keydown: @_onKeydown
97
+ style: "position:fixed;left:#{posX}px;top:#{posY}px;z-index:50"
98
+ for item, idx in _menuItems
99
+ div role: "menuitem", tabindex: "-1"
100
+ $highlighted: (idx is highlightedIndex)?!
101
+ $disabled: item.dataset.disabled?!
102
+ $value: item.dataset.item
103
+ @click: (=> @selectIndex(idx))
104
+ @mouseenter: (=> highlightedIndex = idx)
105
+ = item.textContent
@@ -0,0 +1,214 @@
1
+ # DatePicker — accessible headless date picker with calendar
2
+ #
3
+ # A popover calendar for selecting a single date or a date range.
4
+ # Set @range to true for range selection (value becomes [from, to]).
5
+ # Keyboard: Arrow keys navigate days, Enter selects, Escape closes.
6
+ # Ships zero CSS — style entirely via attribute selectors in your stylesheet.
7
+ #
8
+ # Usage:
9
+ # DatePicker value <=> selectedDate, placeholder: "Pick a date"
10
+ # DatePicker value <=> dateRange, range: true
11
+
12
+ dpFmt = (d) ->
13
+ return '' unless d
14
+ m = String(d.getMonth() + 1).padStart(2, '0')
15
+ day = String(d.getDate()).padStart(2, '0')
16
+ "#{m}/#{day}/#{d.getFullYear()}"
17
+
18
+ dpParse = (str) ->
19
+ return null unless str?.length is 10
20
+ parts = str.split('/')
21
+ return null unless parts.length is 3
22
+ [m, d, y] = parts.map Number
23
+ return null if isNaN(m) or isNaN(d) or isNaN(y)
24
+ dt = new Date(y, m - 1, d)
25
+ return null if dt.getMonth() isnt m - 1
26
+ dt
27
+
28
+ dpSameDay = (a, b) ->
29
+ return false unless a and b
30
+ a.getFullYear() is b.getFullYear() and a.getMonth() is b.getMonth() and a.getDate() is b.getDate()
31
+
32
+ dpInRange = (day, from, to) ->
33
+ return false unless day and from and to
34
+ t = day.getTime()
35
+ lo = Math.min(from.getTime(), to.getTime())
36
+ hi = Math.max(from.getTime(), to.getTime())
37
+ t >= lo and t <= hi
38
+
39
+ export DatePicker = component
40
+ @value := null
41
+ @placeholder := 'mm/dd/yyyy'
42
+ @disabled := false
43
+ @range := false
44
+ @firstDayOfWeek := 0
45
+
46
+ open := false
47
+ viewMonth := new Date()
48
+ _rangeStart := null
49
+ _hoveredDay := null
50
+ _inputText := ''
51
+ _id =! "dp-#{Math.random().toString(36).slice(2, 8)}"
52
+
53
+ _dayNames =! ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']
54
+
55
+ _daysInView ~=
56
+ yr = viewMonth.getFullYear()
57
+ mo = viewMonth.getMonth()
58
+ firstOfMonth = new Date(yr, mo, 1)
59
+ startDay = firstOfMonth.getDay()
60
+ offset = (startDay - @firstDayOfWeek + 7) %% 7
61
+ daysInMonth = new Date(yr, mo + 1, 0).getDate()
62
+ prevMonthDays = new Date(yr, mo, 0).getDate()
63
+ dayList = []
64
+ for n in [0...offset]
65
+ dayList.push { date: new Date(yr, mo - 1, prevMonthDays - offset + n + 1), outside: true }
66
+ for n in [1..daysInMonth]
67
+ dayList.push { date: new Date(yr, mo, n), outside: false }
68
+ trailing = (7 - dayList.length %% 7) %% 7
69
+ for n in [1..trailing]
70
+ dayList.push { date: new Date(yr, mo + 1, n), outside: true }
71
+ dayList
72
+
73
+ _displayText ~=
74
+ if @range
75
+ if Array.isArray(@value) and @value[0]
76
+ from = dpFmt(@value[0])
77
+ to = if @value[1] then dpFmt(@value[1]) else '...'
78
+ "#{from} – #{to}"
79
+ else
80
+ @placeholder
81
+ else
82
+ if @value then dpFmt(@value) else @placeholder
83
+
84
+ _monthLabel ~=
85
+ viewMonth.toLocaleDateString(undefined, { month: 'long', year: 'numeric' })
86
+
87
+ _today =! new Date()
88
+
89
+ _prevMonth: ->
90
+ viewMonth = new Date(viewMonth.getFullYear(), viewMonth.getMonth() - 1, 1)
91
+
92
+ _nextMonth: ->
93
+ viewMonth = new Date(viewMonth.getFullYear(), viewMonth.getMonth() + 1, 1)
94
+
95
+ _selectDay: (day) ->
96
+ return if @disabled
97
+ if @range
98
+ if _rangeStart and not dpSameDay(_rangeStart, day)
99
+ from = if _rangeStart < day then _rangeStart else day
100
+ to = if _rangeStart < day then day else _rangeStart
101
+ @value = [from, to]
102
+ _rangeStart = null
103
+ @emit 'change', @value
104
+ open = false
105
+ else
106
+ _rangeStart = day
107
+ @value = [day, null]
108
+ else
109
+ @value = day
110
+ @emit 'change', @value
111
+ open = false
112
+ _inputText = _displayText
113
+
114
+ _onInputChange: (e) ->
115
+ raw = e.target.value.replace(/[^\d\/]/g, '')
116
+ if raw.length <= 10
117
+ _inputText = raw
118
+ if raw.length is 10
119
+ dt = dpParse(raw)
120
+ if dt
121
+ @value = dt
122
+ viewMonth = new Date(dt.getFullYear(), dt.getMonth(), 1)
123
+ @emit 'change', @value
124
+
125
+ toggle: ->
126
+ return if @disabled
127
+ if open then @close() else @openPicker()
128
+
129
+ openPicker: ->
130
+ open = true
131
+ if @value and not @range
132
+ viewMonth = new Date(@value.getFullYear(), @value.getMonth(), 1)
133
+ else if @range and Array.isArray(@value) and @value[0]
134
+ viewMonth = new Date(@value[0].getFullYear(), @value[0].getMonth(), 1)
135
+ _inputText = _displayText
136
+ setTimeout => @_position(), 0
137
+
138
+ close: ->
139
+ open = false
140
+ _rangeStart = null
141
+ _hoveredDay = null
142
+
143
+ _position: ->
144
+ return unless @_trigger and @_cal
145
+ tr = @_trigger.getBoundingClientRect()
146
+ @_cal.style.position = 'fixed'
147
+ @_cal.style.left = "#{tr.left}px"
148
+ @_cal.style.top = "#{tr.bottom + 4}px"
149
+ fl = @_cal.getBoundingClientRect()
150
+ if fl.bottom > window.innerHeight
151
+ @_cal.style.top = "#{tr.top - fl.height - 4}px"
152
+
153
+ _onKeydown: (e) ->
154
+ switch e.key
155
+ when 'Escape'
156
+ e.preventDefault()
157
+ @close()
158
+ @_trigger?.focus()
159
+ when 'Enter', ' '
160
+ e.preventDefault()
161
+ @toggle() unless open
162
+
163
+ ~>
164
+ if open
165
+ onDown = (e) =>
166
+ unless @_trigger?.contains(e.target) or @_cal?.contains(e.target)
167
+ @close()
168
+ document.addEventListener 'mousedown', onDown
169
+ return -> document.removeEventListener 'mousedown', onDown
170
+
171
+ render
172
+ . $open: open?!, $disabled: @disabled?!, $range: @range?!
173
+
174
+ # Trigger
175
+ button ref: "_trigger", $trigger: true
176
+ aria-haspopup: "dialog"
177
+ aria-expanded: !!open
178
+ disabled: @disabled
179
+ @click: @toggle
180
+ @keydown: @_onKeydown
181
+ _displayText
182
+
183
+ # Calendar dropdown
184
+ if open
185
+ div ref: "_cal", role: "dialog", aria-label: "Date picker", $calendar: true
186
+ style: "position:fixed;z-index:50"
187
+
188
+ # Month navigation
189
+ . $header: true
190
+ button $prev: true, aria-label: "Previous month", @click: @_prevMonth
191
+ "‹"
192
+ span $month-label: true
193
+ _monthLabel
194
+ button $next: true, aria-label: "Next month", @click: @_nextMonth
195
+ "›"
196
+
197
+ # Day-of-week headers
198
+ . $weekdays: true
199
+ for dayName in _dayNames
200
+ span $weekday: true
201
+ dayName
202
+
203
+ # Day grid
204
+ . $days: true, role: "grid"
205
+ for entry, dIdx in _daysInView
206
+ button role: "gridcell", tabindex: "-1"
207
+ $outside: entry.outside?!
208
+ $today: dpSameDay(entry.date, _today)?!
209
+ $selected: (if @range then (Array.isArray(@value) and (dpSameDay(entry.date, @value[0]) or dpSameDay(entry.date, @value[1]))) else dpSameDay(entry.date, @value))?!
210
+ $in-range: (if @range then (if _rangeStart then dpInRange(entry.date, _rangeStart, _hoveredDay or _rangeStart) else if Array.isArray(@value) and @value[0] and @value[1] then dpInRange(entry.date, @value[0], @value[1]) else false) else false)?!
211
+ $range-start: (if @range and _rangeStart then dpSameDay(entry.date, _rangeStart) else false)?!
212
+ @click: (=> @_selectDay(entry.date))
213
+ @mouseenter: (=> _hoveredDay = entry.date)
214
+ entry.date.getDate()