Simplify UI interaction and stabilize DataGrid tracks
This commit is contained in:
parent
5333d607ed
commit
e2f8fd812c
11
README.md
11
README.md
@ -43,9 +43,9 @@ the inherited Theme/default style available. Button has the deliberately small
|
||||
set, not a second widget taxonomy.
|
||||
|
||||
Enabled buttons and checkboxes expose the same interaction affordances: a hand
|
||||
pointer, `mouse-face` hover feedback, a readable `help-echo`, a numeric tab
|
||||
stop, and a semantic role. Button presses briefly enter a `pressed` visual
|
||||
state. Disabled controls retain their text and disabled appearance but expose
|
||||
pointer, native `mouse-face` hover feedback, a readable `help-echo`, a numeric
|
||||
tab stop, and a semantic role. Hover ends when the pointer leaves; activation
|
||||
does not create retained visual state. Disabled controls retain their text and disabled appearance but expose
|
||||
no callback, pointer activation, or tab stop. The Runtime owns composition of
|
||||
an explicit `:on-*` callback with a Behavior callback; the UI package only
|
||||
declares the control and its visual state.
|
||||
@ -69,9 +69,12 @@ have no callback or tab stop. `:row-key` remains a required non-nil stable
|
||||
scalar identity for retained rows. DataGrid retains one action closure per
|
||||
row key, so selection or data Range updates do not recreate handlers for
|
||||
unchanged rows.
|
||||
An integer column `:width` is readable character capacity, not a raw pixel
|
||||
value. DataGrid passes Ebox's native character unit through and reserves one
|
||||
native character between non-final tracks.
|
||||
|
||||
`etaf-pagination` is a controlled Data Component. It accepts a Data controller
|
||||
plus stable `:previous-ref` and `:next-ref` values, renders readable `‹`/`›`
|
||||
plus stable `:previous-ref` and `:next-ref` values, renders readable `←`/`→`
|
||||
controls and a `Page N / M` summary, and disables the boundary action while a
|
||||
page is loading or already at the first/last page. It owns no page state and
|
||||
uses the same Button interaction contract.
|
||||
|
||||
@ -38,9 +38,10 @@ Component 的 `:styles` 声明负责默认外观。调用者提供的非 nil pre
|
||||
只提供刻意收敛的 `primary`、`secondary`、`ghost` 三种视觉 variant;这是
|
||||
视觉状态集合,不是第二套 widget 分类体系。
|
||||
|
||||
启用的 button 和 checkbox 共享同一套交互反馈:小手指针、`mouse-face` 悬停
|
||||
反馈、可读的 `help-echo`、数字 tab stop 和语义 role。Button 按下时短暂进入
|
||||
`pressed` 外观。禁用控件保留文字和禁用样式,但没有回调、指针激活或 tab stop。
|
||||
启用的 button 和 checkbox 共享同一套交互反馈:小手指针、原生 `mouse-face`
|
||||
悬停反馈、可读的 `help-echo`、数字 tab stop 和语义 role。指针离开后 hover
|
||||
自动结束,点击不会产生 retained 视觉状态。禁用控件保留文字和禁用样式,但
|
||||
没有回调、指针激活或 tab stop。
|
||||
显式 `:on-*` 回调和 Behavior 回调的组合由 Runtime 负责;UI 包只声明控件和
|
||||
视觉状态。
|
||||
|
||||
@ -59,9 +60,11 @@ DataGrid 支持列描述、函数型 `:row-key`,以及可选的
|
||||
行没有回调或 tab stop。`:row-key` 仍然是 retained row 必须具备的非 nil
|
||||
稳定标量 identity。DataGrid 按 row key 保留唯一 action closure,因此 selection
|
||||
或 Data Range 更新不会为未变化的行重建 handler。
|
||||
整数列 `:width` 表示可读字符容量,不是裸像素;DataGrid 原样使用 Ebox 原生字符单位,
|
||||
并在非末列之间保留一个原生字符间距。
|
||||
|
||||
`etaf-pagination` 是受控 Data Component。它接收 Data controller 以及稳定的
|
||||
`:previous-ref`、`:next-ref`,显示易读的 `‹`/`›` 控件和 `Page N / M` 摘要;
|
||||
`:previous-ref`、`:next-ref`,显示易读的 `←`/`→` 控件和 `Page N / M` 摘要;
|
||||
加载中或已经位于首/末页时会禁用对应动作。它不拥有页码状态,并复用 Button
|
||||
的交互契约。
|
||||
|
||||
|
||||
191
etaf-ui.el
191
etaf-ui.el
@ -27,6 +27,7 @@
|
||||
(declare-function text "etaf-view" (&rest arguments))
|
||||
(declare-function row "etaf-view" (&rest arguments))
|
||||
(declare-function column "etaf-view" (&rest arguments))
|
||||
(declare-function flex "etaf-view" (&rest arguments))
|
||||
(declare-function expr "etaf-view" (&rest arguments))
|
||||
(declare-function slot "etaf-view" (&rest arguments))
|
||||
|
||||
@ -57,7 +58,7 @@ the optional text properties and keep the semantic role/event contract."
|
||||
|
||||
(defun etaf-ui--button-view
|
||||
(label on-press disabled ref class color bgcolor border padding face
|
||||
tab-index aria-label use pressed)
|
||||
tab-index aria-label use)
|
||||
"Return a Button Host showing LABEL.
|
||||
|
||||
ON-PRESS and USE provide callbacks and Behaviors. DISABLED controls whether
|
||||
@ -65,9 +66,7 @@ the Host is interactive. REF, CLASS, COLOR, BGCOLOR, BORDER, PADDING, FACE,
|
||||
TAB-INDEX, and ARIA-LABEL provide Host identity and presentation."
|
||||
(let ((class-value (etaf-ui--class-value
|
||||
"etaf-button"
|
||||
(cond (disabled "disabled")
|
||||
(pressed "pressed")
|
||||
(t "enabled"))
|
||||
(if disabled "disabled" "enabled")
|
||||
class))
|
||||
(tab-value (unless disabled (or tab-index 0)))
|
||||
(label-value (or aria-label label))
|
||||
@ -106,49 +105,64 @@ TAB-INDEX, and ARIA-LABEL provide Host identity and presentation."
|
||||
((listp row) (alist-get key row))
|
||||
(t nil)))
|
||||
|
||||
(defun etaf-ui--grid-fit-text (value column)
|
||||
"Return VALUE fitted to COLUMN's declared character capacity."
|
||||
(let* ((value (format "%s" (or value "")))
|
||||
(width (etaf-ui--column-value column :width)))
|
||||
(if (and (integerp width) (> width 0)
|
||||
(> (string-width value) width))
|
||||
(truncate-string-to-width value width 0 nil "…")
|
||||
value)))
|
||||
|
||||
(defun etaf-ui--grid-display-value (row column)
|
||||
"Return one single-line display value for ROW and COLUMN.
|
||||
|
||||
DataGrid columns are tabular tracks, not prose paragraphs. Keep each cell on
|
||||
one visual line and use an ellipsis when a fixed character-width descriptor is
|
||||
too small; the original ROW remains intact for selection and callbacks."
|
||||
(let* ((value (format "%s"
|
||||
(or (etaf-ui--grid-cell-value
|
||||
row (etaf-ui--column-value column :key)) "")))
|
||||
(width (etaf-ui--column-value column :width)))
|
||||
(if (and (integerp width) (> width 1)
|
||||
(> (string-width value) width))
|
||||
;; Leave one character of the declared track for inter-column air;
|
||||
;; Ebox's left justification fills that remainder without changing
|
||||
;; the stable column geometry.
|
||||
(truncate-string-to-width value (1- width) 0 nil "…")
|
||||
value)))
|
||||
(etaf-ui--grid-fit-text
|
||||
(etaf-ui--grid-cell-value row (etaf-ui--column-value column :key))
|
||||
column))
|
||||
|
||||
(defun etaf-ui--grid-header-cell (column)
|
||||
"Return one header View for COLUMN."
|
||||
(defun etaf-ui--grid-track-width (column gap-p)
|
||||
"Return COLUMN width with one native-character gap when GAP-P is non-nil."
|
||||
(let ((width (etaf-ui--column-value column :width)))
|
||||
(if (and gap-p (integerp width) (> width 0))
|
||||
(1+ width)
|
||||
width)))
|
||||
|
||||
(defun etaf-ui--grid-header-cell (column gap-p)
|
||||
"Return one header View for COLUMN, adding air when GAP-P is non-nil."
|
||||
(etaf-view
|
||||
(text :class "etaf-data-grid-header-cell"
|
||||
:width (etaf-ui--column-value column :width)
|
||||
(expr :value (format "%s"
|
||||
(or (etaf-ui--column-value column :label)
|
||||
(etaf-ui--column-value column :key)))))))
|
||||
:width (etaf-ui--grid-track-width column gap-p)
|
||||
(expr :value
|
||||
(etaf-ui--grid-fit-text
|
||||
(or (etaf-ui--column-value column :label)
|
||||
(etaf-ui--column-value column :key))
|
||||
column)))))
|
||||
|
||||
(defun etaf-ui--grid-header (columns)
|
||||
"Return a View header row for COLUMNS."
|
||||
(etaf-view
|
||||
(row
|
||||
:class "etaf-data-grid-header"
|
||||
(expr :value (mapcar #'etaf-ui--grid-header-cell columns)))))
|
||||
(row :class "etaf-data-grid-header"
|
||||
(expr :value
|
||||
(cl-loop for column in columns
|
||||
for tail on columns
|
||||
collect (etaf-ui--grid-header-cell
|
||||
column (cdr tail)))))))
|
||||
|
||||
(defun etaf-ui--grid-cell (row column)
|
||||
"Return one data cell View for ROW and COLUMN."
|
||||
(defun etaf-ui--grid-cell (row column gap-p)
|
||||
"Return one data cell View for ROW and COLUMN, using GAP-P for air."
|
||||
(etaf-view
|
||||
(text :width (etaf-ui--column-value column :width)
|
||||
(text :width (etaf-ui--grid-track-width column gap-p)
|
||||
(expr :value (etaf-ui--grid-display-value row column)))))
|
||||
|
||||
(defun etaf-ui--grid-cells (row columns)
|
||||
"Return data cell Views for ROW and COLUMNS."
|
||||
(mapcar (lambda (column) (etaf-ui--grid-cell row column)) columns))
|
||||
(cl-loop for column in columns
|
||||
for tail on columns
|
||||
collect (etaf-ui--grid-cell row column (cdr tail))))
|
||||
|
||||
(defun etaf-ui--grid-selected-p (row key selected-key row-selected-p)
|
||||
"Return whether ROW with KEY matches SELECTED-KEY or ROW-SELECTED-P."
|
||||
@ -192,10 +206,9 @@ owns stable keyed callbacks across Range reevaluation."
|
||||
(unless host-ref
|
||||
(error "ETAF DataGrid :row-ref must return a non-nil stable ref")))
|
||||
(etaf-view
|
||||
(row
|
||||
:key key
|
||||
:class (concat "etaf-data-grid-row"
|
||||
(if selected-p " selected" ""))
|
||||
(row :key key
|
||||
:class (concat "etaf-data-grid-row"
|
||||
(if selected-p " selected" ""))
|
||||
:ref host-ref
|
||||
:role (when interactive-p 'button)
|
||||
:tab-index (when interactive-p 0)
|
||||
@ -230,31 +243,14 @@ SELECTED-KEY, and ROW-SELECTED-P provide interaction state."
|
||||
|
||||
(defun etaf-ui--button-setup ()
|
||||
"Create the retained renderer for one Button instance."
|
||||
(let* ((pressed (etaf-ref nil))
|
||||
(timer nil)
|
||||
(scope (etaf-current-effect-scope))
|
||||
(current-callback nil)
|
||||
(let* ((current-callback nil)
|
||||
(current-press-p nil)
|
||||
(press nil))
|
||||
(setq press
|
||||
(lambda ()
|
||||
(when current-press-p
|
||||
(when (timerp timer)
|
||||
(cancel-timer timer))
|
||||
(setf (etaf-value pressed) t)
|
||||
(unwind-protect
|
||||
(when current-callback
|
||||
(funcall current-callback))
|
||||
(setq timer
|
||||
(run-at-time
|
||||
0.09 nil
|
||||
(lambda ()
|
||||
(when (etaf-effect-scope-active-p scope)
|
||||
(setf (etaf-value pressed) nil)))))))))
|
||||
(etaf-on-unmounted
|
||||
(lambda ()
|
||||
(when (timerp timer)
|
||||
(cancel-timer timer))))
|
||||
(when current-callback
|
||||
(funcall current-callback)))))
|
||||
(lambda ()
|
||||
(let* ((label (etaf-current-prop :label))
|
||||
(callback (etaf-current-prop :on-press))
|
||||
@ -263,41 +259,27 @@ SELECTED-KEY, and ROW-SELECTED-P provide interaction state."
|
||||
(press-p (and (not disabled) (or callback use))))
|
||||
(setq current-callback callback
|
||||
current-press-p press-p)
|
||||
(let* ((state-pressed (and (not disabled) (etaf-value pressed)))
|
||||
(state-class (cond (disabled "disabled")
|
||||
(state-pressed "pressed")
|
||||
(t "enabled")))
|
||||
(variant (and (not disabled) (etaf-current-prop :variant)))
|
||||
(let* ((variant (and (not disabled) (etaf-current-prop :variant)))
|
||||
(variant-values
|
||||
(cond
|
||||
(disabled
|
||||
'(:color "#687386" :bgcolor "#E5E7EB"
|
||||
:border ((1) solid "#9CA3AF") :face normal))
|
||||
((and (eq variant 'secondary) state-pressed)
|
||||
'(:color "#142235" :bgcolor "#B9DED7"
|
||||
:border ((1) solid "#24736C") :face bold))
|
||||
((eq variant 'secondary)
|
||||
'(:color "#142235" :bgcolor "#D9EEEA"
|
||||
:border ((1) solid "#2E8B83") :face bold))
|
||||
((and (eq variant 'ghost) state-pressed)
|
||||
'(:color "#142235" :bgcolor "#EEEAE2"
|
||||
:border ((1) solid "#A79F93") :face normal))
|
||||
((eq variant 'ghost)
|
||||
'(:color "#142235" :bgcolor "#FFFDF8"
|
||||
:border ((1) solid "#C8C1B6") :face normal))
|
||||
;; The enabled/pressed catalog defaults used to come from
|
||||
;; state selectors alone. Resolve them as props too so an
|
||||
;; The enabled catalog defaults used to come from state
|
||||
;; selectors alone. Resolve them as props too so an
|
||||
;; explicit theme can override the same state boundary.
|
||||
(state-pressed
|
||||
'(:color "#FFFFFF" :bgcolor "#1E5A56"
|
||||
:border ((1) solid "#174A47") :face bold))
|
||||
(t
|
||||
'(:color "#FFFFFF" :bgcolor "#2F6B43"
|
||||
:border ((1) solid "#2F6B43") :face bold)))))
|
||||
(etaf-ui--button-view
|
||||
label (and press-p press) disabled (etaf-current-prop :ref)
|
||||
(let ((custom-class (etaf-current-prop :class)))
|
||||
(delq nil (list custom-class state-class)))
|
||||
(etaf-current-prop :class)
|
||||
(or (etaf-current-prop :color)
|
||||
(plist-get variant-values :color))
|
||||
(or (etaf-current-prop :bgcolor)
|
||||
@ -309,7 +291,7 @@ SELECTED-KEY, and ROW-SELECTED-P provide interaction state."
|
||||
(plist-get variant-values :face))
|
||||
(etaf-current-prop :tab-index)
|
||||
(etaf-current-prop :aria-label)
|
||||
use state-pressed))))))
|
||||
use))))))
|
||||
|
||||
;;;###autoload
|
||||
(etaf-define-component etaf-button
|
||||
@ -327,8 +309,7 @@ contract; callers can still override presentation with the ordinary props."
|
||||
;; above remain authoritative, so a themed disabled Button cannot inherit
|
||||
;; the catalog's light default surface.
|
||||
("&.disabled" :padding (0 1) :face normal)
|
||||
("&.enabled" :padding (0 1) :face bold)
|
||||
("&.pressed" :padding (0 1) :face bold))
|
||||
("&.enabled" :padding (0 1) :face bold))
|
||||
:setup
|
||||
(etaf-ui--button-setup))
|
||||
|
||||
@ -505,7 +486,7 @@ a stable Host reference."
|
||||
The pager owns no data state: page, page-size, total, loading, and error stay
|
||||
with CONTROLLER. PREVIOUS-REF and NEXT-REF should be stable public refs when
|
||||
the pager participates in keyboard/mouse interaction. The visible glyphs
|
||||
(`‹' and `›') are paired with labels and help text so the compact control is
|
||||
(`←' and `→') are paired with labels and help text so the compact control is
|
||||
readable in both GUI and text review."
|
||||
:styles
|
||||
(styles
|
||||
@ -538,39 +519,47 @@ readable in both GUI and text review."
|
||||
(arrow-border '((0) solid "transparent")))
|
||||
(setq current-controller controller-value)
|
||||
(etaf-view
|
||||
(row :class (etaf-ui--class-value "etaf-pagination" nil
|
||||
(etaf-current-prop :class))
|
||||
:role 'navigation
|
||||
:aria-label (or (etaf-current-prop :aria-label) "Pagination")
|
||||
:color parent-color
|
||||
:bgcolor parent-bgcolor
|
||||
:border (etaf-current-prop :border)
|
||||
:padding (or (etaf-current-prop :padding) '(0 1))
|
||||
:gap '(0 (2))
|
||||
(button :label "‹" :ref (etaf-current-prop :previous-ref)
|
||||
:aria-label "Previous page"
|
||||
:disabled previous-disabled
|
||||
:padding '(0 0)
|
||||
:border arrow-border
|
||||
:color (if previous-disabled "#9CA3AF" parent-color)
|
||||
:bgcolor parent-bgcolor
|
||||
:face 'bold
|
||||
:on-press (unless previous-disabled previous))
|
||||
(flex :class (etaf-ui--class-value "etaf-pagination" nil
|
||||
(etaf-current-prop :class))
|
||||
:width 'stretch
|
||||
:flex-direction 'row
|
||||
:align-items 'center
|
||||
:role 'navigation
|
||||
:aria-label (or (etaf-current-prop :aria-label) "Pagination")
|
||||
:color parent-color
|
||||
:bgcolor parent-bgcolor
|
||||
:border (etaf-current-prop :border)
|
||||
:box-sizing 'border-box
|
||||
:padding (or (etaf-current-prop :padding) '(0 1))
|
||||
:gap '(0 (1))
|
||||
(column :width 'max-content
|
||||
:flex-grow 0 :flex-shrink 0 :flex-basis 'auto
|
||||
(button :label "←" :ref (etaf-current-prop :previous-ref)
|
||||
:aria-label "Previous page"
|
||||
:disabled previous-disabled
|
||||
:padding '(0 0)
|
||||
:border arrow-border
|
||||
:color (if previous-disabled "#9CA3AF" parent-color)
|
||||
:bgcolor parent-bgcolor
|
||||
:face 'bold
|
||||
:on-press (unless previous-disabled previous)))
|
||||
(column :flex-grow 1 :flex-shrink 1 :flex-basis '(0) :min-width 0
|
||||
(text :class "etaf-pagination-label" :text-align 'center
|
||||
(expr :value (format "Page %d / %d" page pages)))
|
||||
(text :class "etaf-pagination-summary" :text-align 'center
|
||||
(expr :value (format "%d–%d of %d"
|
||||
first-item last-item total))))
|
||||
(button :label "›" :ref (etaf-current-prop :next-ref)
|
||||
:aria-label "Next page"
|
||||
:disabled next-disabled
|
||||
:padding '(0 0)
|
||||
:border arrow-border
|
||||
:color (if next-disabled "#9CA3AF" parent-color)
|
||||
:bgcolor parent-bgcolor
|
||||
:face 'bold
|
||||
:on-press (unless next-disabled next))))))))
|
||||
(column :width 'max-content
|
||||
:flex-grow 0 :flex-shrink 0 :flex-basis 'auto
|
||||
(button :label "→" :ref (etaf-current-prop :next-ref)
|
||||
:aria-label "Next page"
|
||||
:disabled next-disabled
|
||||
:padding '(0 0)
|
||||
:border arrow-border
|
||||
:color (if next-disabled "#9CA3AF" parent-color)
|
||||
:bgcolor parent-bgcolor
|
||||
:face 'bold
|
||||
:on-press (unless next-disabled next)))))))))
|
||||
|
||||
(provide 'etaf-ui)
|
||||
|
||||
|
||||
@ -210,8 +210,8 @@
|
||||
(when-let ((buffer (get-buffer buffer-name)))
|
||||
(kill-buffer buffer))))))
|
||||
|
||||
(ert-deftest etaf-ui-button-owns-pointer-hover-and-pressed-feedback ()
|
||||
"Buttons expose native pointer/hover affordances and a pressed state."
|
||||
(ert-deftest etaf-ui-button-owns-native-hover-and-dispatch ()
|
||||
"Buttons expose native hover affordances without retained press state."
|
||||
(let ((buffer-name " *etaf-ui-button-surface-test*")
|
||||
(presses 0))
|
||||
(unwind-protect
|
||||
@ -237,9 +237,13 @@
|
||||
'health 'press)
|
||||
(should (= presses 1))
|
||||
(should (string-match-p
|
||||
"pressed"
|
||||
"enabled"
|
||||
(plist-get (etaf-ui-test--props buffer-name 'health)
|
||||
:class)))
|
||||
(should-not (string-match-p
|
||||
"pressed"
|
||||
(plist-get (etaf-ui-test--props buffer-name 'health)
|
||||
:class)))
|
||||
(should (eq before
|
||||
(cdr (assq 'press
|
||||
(etaf-runtime-handler-for runtime 'health)))))))
|
||||
@ -381,8 +385,10 @@
|
||||
(should (equal (plist-get first :tab-index) 0))
|
||||
(should (equal (plist-get second :role) 'button))
|
||||
(should (equal (plist-get second :tab-index) 0))
|
||||
(should (string-match-p "selected" (plist-get second :class)))
|
||||
(should-not (string-match-p "selected" (plist-get first :class))))
|
||||
(should (string-match-p
|
||||
"selected" (or (plist-get second :class) "")))
|
||||
(should-not (string-match-p
|
||||
"selected" (or (plist-get first :class) ""))))
|
||||
(let* ((runtime (etaf-runtime-for-buffer buffer-name))
|
||||
(handler (cdr (assq 'press
|
||||
(etaf-runtime-handler-for runtime
|
||||
@ -419,8 +425,8 @@
|
||||
:previous-ref 'page-previous
|
||||
:next-ref 'page-next)))
|
||||
(should (string-match-p "Page 1 / 3" (etaf-ui-test--text buffer-name)))
|
||||
(should (string-match-p "‹" (etaf-ui-test--text buffer-name)))
|
||||
(should (string-match-p "›" (etaf-ui-test--text buffer-name)))
|
||||
(should (string-match-p "←" (etaf-ui-test--text buffer-name)))
|
||||
(should (string-match-p "→" (etaf-ui-test--text buffer-name)))
|
||||
;; Live Ebox windows reserve two pixels for the exclusive display
|
||||
;; boundary; the pager must stay inside the corresponding 360px row.
|
||||
(ebox-surface-update-buffer-viewport (get-buffer buffer-name) 358 20)
|
||||
@ -451,6 +457,40 @@
|
||||
(when-let ((buffer (get-buffer buffer-name)))
|
||||
(kill-buffer buffer)))))
|
||||
|
||||
(ert-deftest etaf-ui-pagination-composes-inside-a-grid-track ()
|
||||
"Pagination must distribute free space inside its assigned Grid track."
|
||||
(let* ((source (etaf-data-memory-source
|
||||
'((:id 1) (:id 2) (:id 3) (:id 4) (:id 5))
|
||||
:id-key :id))
|
||||
(controller (etaf-data-controller source :page-size 2 :auto-load t))
|
||||
(buffer-name " *etaf-ui-pagination-grid-test*"))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(etaf-mount
|
||||
buffer-name
|
||||
(etaf-view
|
||||
(grid :width '(360)
|
||||
:grid-template-columns '((80) (200) (80))
|
||||
(text "Left")
|
||||
(column :width 'stretch :padding '(0 2) :border "#CBD5E1"
|
||||
(pagination :controller controller
|
||||
:previous-ref 'nested-page-previous
|
||||
:next-ref 'nested-page-next))
|
||||
(text "Right"))))
|
||||
(with-current-buffer buffer-name
|
||||
(goto-char (point-min))
|
||||
(while (< (point) (point-max))
|
||||
(should (<= (ebox-string-pixel-width
|
||||
(buffer-substring (line-beginning-position)
|
||||
(line-end-position)))
|
||||
360))
|
||||
(forward-line 1))))
|
||||
(when-let ((runtime (etaf-runtime-for-buffer buffer-name)))
|
||||
(etaf-unmount runtime))
|
||||
(etaf-data-stop controller)
|
||||
(when-let ((buffer (get-buffer buffer-name)))
|
||||
(kill-buffer buffer)))))
|
||||
|
||||
(ert-deftest etaf-ui-data-grid-keeps-long-cells-on-one-line ()
|
||||
"Long tabular values use an ellipsis instead of increasing row height."
|
||||
(let* ((source (etaf-data-memory-source
|
||||
@ -468,7 +508,7 @@
|
||||
:columns '((:key :title :label "Title" :width 22))
|
||||
:row-key (lambda (row) (plist-get row :id)))))
|
||||
(let ((text (etaf-ui-test--text buffer-name)))
|
||||
(should (string-match-p "The Cathedral and th…" text))
|
||||
(should (string-match-p "The Cathedral and the…" text))
|
||||
(should-not (string-match-p "The Cathedral and the Bazaar" text))))
|
||||
(when-let ((runtime (etaf-runtime-for-buffer buffer-name)))
|
||||
(etaf-unmount runtime))
|
||||
@ -476,6 +516,30 @@
|
||||
(when-let ((buffer (get-buffer buffer-name)))
|
||||
(kill-buffer buffer)))))
|
||||
|
||||
(ert-deftest etaf-ui-data-grid-converts-character-columns-and-keeps-air ()
|
||||
"Character column descriptors become pixel widths with a stable gap."
|
||||
(let* ((source (etaf-data-memory-source
|
||||
'((:id 1 :progress 64 :kind "Essay")) :id-key :id))
|
||||
(controller (etaf-data-controller source :auto-load t))
|
||||
(buffer-name " *etaf-ui-grid-column-unit-test*"))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(etaf-mount
|
||||
buffer-name
|
||||
(etaf-view
|
||||
(data-grid
|
||||
:controller controller
|
||||
:columns '((:key :progress :label "Progress" :width 8)
|
||||
(:key :kind :label "Kind" :width 7))
|
||||
:row-key (lambda (row) (plist-get row :id)))))
|
||||
(should (string-match-p "Progress[[:space:]]+Kind"
|
||||
(etaf-ui-test--text buffer-name))))
|
||||
(when-let ((runtime (etaf-runtime-for-buffer buffer-name)))
|
||||
(etaf-unmount runtime))
|
||||
(etaf-data-stop controller)
|
||||
(when-let ((buffer (get-buffer buffer-name)))
|
||||
(kill-buffer buffer)))))
|
||||
|
||||
(ert-deftest etaf-ui-data-grid-noninteractive-rows-have-no-focus-contract ()
|
||||
"Rows without ON-ROW-PRESS have no role, ref callback, or tab stop."
|
||||
(let* ((source (etaf-data-memory-source '((:id 1 :name "Ada"))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user