@crisp-ui-kit/crisp 0.31.0 → 0.31.2

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.
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.useRadioGroupKeyboard = useRadioGroupKeyboard;
37
37
  const React = __importStar(require("react"));
38
+ const core_1 = require("../../core");
38
39
  /**
39
40
  * The WAI-ARIA radio group keyboard model: ←/→/↑/↓ move focus to the next or
40
41
  * previous enabled radio, wrapping, and check it on arrival. Home/End jump to
@@ -113,7 +114,11 @@ function useRadioGroupKeyboard({ select, groupRef: externalRef, }) {
113
114
  else if (e.key === "End")
114
115
  next = radios[radios.length - 1];
115
116
  else {
116
- const fwd = e.key === "ArrowDown" || e.key === "ArrowRight";
117
+ // Down always means forward; the horizontal pair follows the direction
118
+ // the radios are drawn in, which for Segmented's pill row is the whole
119
+ // point (#255).
120
+ const rtl = (0, core_1.isRtlElement)(e.currentTarget);
121
+ const fwd = e.key === "ArrowDown" || e.key === (rtl ? "ArrowLeft" : "ArrowRight");
117
122
  // focus outside the group: enter at the near end rather than stepping
118
123
  // off an index of -1, which would land two from the back going backwards
119
124
  if (idx < 0)
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.useTablistKeyboard = useTablistKeyboard;
4
+ const core_1 = require("../../core");
4
5
  /**
5
6
  * The WAI-ARIA tablist keyboard model: ←/→ to move (wrapping), Home/End to
6
7
  * jump, disabled tabs skipped, focus following selection.
@@ -20,9 +21,15 @@ function useTablistKeyboard({ tabs, active, select, listRef, }) {
20
21
  return;
21
22
  const idx = enabled.findIndex((t) => t.value === active);
22
23
  let next = -1;
23
- if (e.key === "ArrowRight")
24
+ // The arrows follow the strip as it is drawn: under RTL the next tab is to
25
+ // the LEFT, so the keys swap. Everything else about the model is unchanged
26
+ // (#255).
27
+ const rtl = (0, core_1.isRtlElement)(e.currentTarget);
28
+ const forwardKey = rtl ? "ArrowLeft" : "ArrowRight";
29
+ const backKey = rtl ? "ArrowRight" : "ArrowLeft";
30
+ if (e.key === forwardKey)
24
31
  next = (idx + 1) % enabled.length;
25
- else if (e.key === "ArrowLeft")
32
+ else if (e.key === backKey)
26
33
  next = (idx - 1 + enabled.length) % enabled.length;
27
34
  else if (e.key === "Home")
28
35
  next = 0;
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.Board = Board;
37
37
  const jsx_runtime_1 = require("react/jsx-runtime");
38
38
  const React = __importStar(require("react"));
39
+ const core_1 = require("../../core");
39
40
  const board_recipe_1 = require("./board.recipe");
40
41
  /**
41
42
  * A kanban board with native drag-and-drop between columns. Cards are
@@ -108,11 +109,12 @@ function Board({ columns, cards, onCardsChange, renderCard, onNewCard, newCardLa
108
109
  }, onKeyDown: (e) => {
109
110
  if (!(e.ctrlKey || e.metaKey))
110
111
  return;
111
- const delta = e.key === "ArrowLeft"
112
- ? -1
113
- : e.key === "ArrowRight"
114
- ? 1
115
- : 0;
112
+ // the columns are a flex row, so under RTL the next one
113
+ // is to the LEFT and the keys swap (#255)
114
+ const rtl = (0, core_1.isRtlElement)(e.currentTarget);
115
+ const forwardKey = rtl ? "ArrowLeft" : "ArrowRight";
116
+ const backKey = rtl ? "ArrowRight" : "ArrowLeft";
117
+ const delta = e.key === backKey ? -1 : e.key === forwardKey ? 1 : 0;
116
118
  // only swallow the key when the move actually happens, so
117
119
  // Ctrl+Right at the last column still reaches the browser
118
120
  if (delta && moveByKey(card, delta))
@@ -414,10 +414,15 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
414
414
  if (!next)
415
415
  setFieldQuery("");
416
416
  }, children: showAllValues ? t.viewLess : t.viewAllValues }))] })) : ((0, jsx_runtime_1.jsxs)("div", { className: "recordpage-noresults", children: ["No fields match \u201C", fieldQuery, "\u201D."] })))] }), (0, jsx_runtime_1.jsxs)("div", { className: "recordpage-section", children: [(0, jsx_runtime_1.jsx)(SectionHead, { title: t.tabs.lists, action: (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "recordpage-addbtn", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 14, strokeWidth: 2 }), t.addToList] }) }), (0, jsx_runtime_1.jsx)("div", { className: "recordpage-emptystate", children: t.noLists })] })] }), (0, jsx_runtime_1.jsx)("div", { className: "recordpage-resizer", "data-active": resizing ? "" : undefined, role: "separator", "aria-orientation": "vertical", "aria-label": t.resizeDetails, "aria-valuenow": Math.round(detailsW), "aria-valuemin": 300, "aria-valuemax": 560, tabIndex: 0, onPointerDown: onResizeDown, onPointerMove: onResizeMove, onPointerUp: onResizeUp, onKeyDown: (e) => {
417
- if (e.key === "ArrowLeft")
418
- setDetailsW((w) => Math.max(300, w - 16));
419
- else if (e.key === "ArrowRight")
420
- setDetailsW((w) => Math.min(560, w + 16));
417
+ if (e.key !== "ArrowLeft" && e.key !== "ArrowRight")
418
+ return;
419
+ // The pointer drag learned its direction in #212; this did not.
420
+ // The details column is the flex row's first child, so it sits on
421
+ // the RIGHT under RTL and ArrowLeft has to grow it — the same
422
+ // half-conversion the drag had, one handler away (#255).
423
+ const rtl = (0, core_1.isRtlElement)(e.currentTarget);
424
+ const grow = e.key === (rtl ? "ArrowLeft" : "ArrowRight");
425
+ setDetailsW((w) => grow ? Math.min(560, w + 16) : Math.max(300, w - 16));
421
426
  } }), (0, jsx_runtime_1.jsx)("main", { className: "recordpage-main", children: (0, jsx_runtime_1.jsx)(tabs_1.Tabs, { tabs: tabs, defaultValue: "overview", "aria-label": `${name} record`, children: panel }) })] })] }));
422
427
  }
423
428
  RecordPage.displayName = "RecordPage";
@@ -640,10 +640,15 @@ function RecordsTable({ labels, columns: columnsProp = exports.defaultColumns, r
640
640
  nr = Math.max(0, r - 1);
641
641
  else if (e.key === "ArrowDown")
642
642
  nr = Math.min(visibleRows.length - 1, r + 1);
643
- else if (e.key === "ArrowLeft")
644
- nc = Math.max(0, c - 1);
645
- else if (e.key === "ArrowRight")
646
- nc = Math.min(cols.length - 1, c + 1);
643
+ // The arrows follow what the user SEES. Under RTL the first column renders
644
+ // rightmost, so ArrowLeft moves to the next column and ArrowRight to the
645
+ // previous — the grid's own half-conversion, of the same kind the resize
646
+ // grips had (#255).
647
+ else if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
648
+ const rtl = (0, core_1.isRtlElement)(e.currentTarget);
649
+ const forward = e.key === (rtl ? "ArrowLeft" : "ArrowRight");
650
+ nc = forward ? Math.min(cols.length - 1, c + 1) : Math.max(0, c - 1);
651
+ }
647
652
  else if (e.key === "Escape") {
648
653
  setSelCell(null);
649
654
  return;
@@ -716,6 +721,36 @@ function RecordsTable({ labels, columns: columnsProp = exports.defaultColumns, r
716
721
  const setSortField = (i, key) => setSorts(sorts.map((s, j) => (j === i ? { ...s, key } : s)));
717
722
  const setSortDir = (i, dir) => setSorts(sorts.map((s, j) => (j === i ? { ...s, dir } : s)));
718
723
  const removeSort = (i) => setSorts(sorts.filter((_, j) => j !== i));
724
+ /**
725
+ * Keep the selected cell on screen.
726
+ *
727
+ * Focus stays on the scroll region, never on a cell, so the browser has
728
+ * nothing to scroll to: the arrow keys moved the outline and the viewport
729
+ * stayed put. Measured in Chrome on a 384px-tall region over 40 rows —
730
+ * twenty ArrowDowns left `scrollTop` at 0 with the selected cell 761px below
731
+ * the top edge, i.e. the user was driving a selection they could not see
732
+ * (#257).
733
+ *
734
+ * `nearest` on both axes: a cell already in view is left exactly where it
735
+ * is, which is what makes this safe to run after a click too.
736
+ *
737
+ * No `behavior` of our own — the page's `scroll-behavior` decides, and Attio
738
+ * jumps rather than glides — except under reduced motion, where an explicit
739
+ * "auto" is the only thing that beats a host page that set
740
+ * `scroll-behavior: smooth` on the scroller (#208's rule: JS motion reads
741
+ * the preference itself, because CSS cannot reach an explicit argument).
742
+ */
743
+ const reduceMotion = (0, core_1.usePrefersReducedMotion)();
744
+ React.useEffect(() => {
745
+ if (!selCell)
746
+ return;
747
+ const cell = scrollRef.current?.querySelector(`[data-cell="${CSS.escape(`${selCell.id}:${selCell.key}`)}"]`);
748
+ cell?.scrollIntoView({
749
+ block: "nearest",
750
+ inline: "nearest",
751
+ ...(reduceMotion ? { behavior: "auto" } : null),
752
+ });
753
+ }, [selCell, reduceMotion]);
719
754
  const commitEdit = (id, key, raw) => {
720
755
  const col = cols.find((c) => c.key === key);
721
756
  const numeric = col?.type === "number" || col?.type === "currency";
@@ -1064,7 +1099,7 @@ function RecordsTable({ labels, columns: columnsProp = exports.defaultColumns, r
1064
1099
  // row instead of a bare "182,000", and the same header is
1065
1100
  // repeated in every cell's context (#231).
1066
1101
  const Cell = (i === 0 ? "th" : "td");
1067
- return ((0, jsx_runtime_1.jsxs)(Cell, { scope: i === 0 ? "row" : undefined, className: `recordstable-td${i === 0 ? " recordstable-col-entity" : ""}${canEdit ? " recordstable-td--editable" : ""}${isPicker ? " recordstable-td--picker" : ""}`, "data-enriched": col.enriched ? "" : undefined, "data-selected": selCell?.id === row.id && selCell?.key === col.key
1102
+ return ((0, jsx_runtime_1.jsxs)(Cell, { scope: i === 0 ? "row" : undefined, "data-cell": `${row.id}:${col.key}`, className: `recordstable-td${i === 0 ? " recordstable-col-entity" : ""}${canEdit ? " recordstable-td--editable" : ""}${isPicker ? " recordstable-td--picker" : ""}`, "data-enriched": col.enriched ? "" : undefined, "data-selected": selCell?.id === row.id && selCell?.key === col.key
1068
1103
  ? ""
1069
1104
  : undefined, onClick: () => {
1070
1105
  if (isPicker)
@@ -1,4 +1,5 @@
1
1
  import * as React from "react";
2
+ import { isRtlElement } from "../../core/index.js";
2
3
  /**
3
4
  * The WAI-ARIA radio group keyboard model: ←/→/↑/↓ move focus to the next or
4
5
  * previous enabled radio, wrapping, and check it on arrival. Home/End jump to
@@ -77,7 +78,11 @@ export function useRadioGroupKeyboard({ select, groupRef: externalRef, }) {
77
78
  else if (e.key === "End")
78
79
  next = radios[radios.length - 1];
79
80
  else {
80
- const fwd = e.key === "ArrowDown" || e.key === "ArrowRight";
81
+ // Down always means forward; the horizontal pair follows the direction
82
+ // the radios are drawn in, which for Segmented's pill row is the whole
83
+ // point (#255).
84
+ const rtl = isRtlElement(e.currentTarget);
85
+ const fwd = e.key === "ArrowDown" || e.key === (rtl ? "ArrowLeft" : "ArrowRight");
81
86
  // focus outside the group: enter at the near end rather than stepping
82
87
  // off an index of -1, which would land two from the back going backwards
83
88
  if (idx < 0)
@@ -1,3 +1,4 @@
1
+ import { isRtlElement } from "../../core/index.js";
1
2
  /**
2
3
  * The WAI-ARIA tablist keyboard model: ←/→ to move (wrapping), Home/End to
3
4
  * jump, disabled tabs skipped, focus following selection.
@@ -17,9 +18,15 @@ export function useTablistKeyboard({ tabs, active, select, listRef, }) {
17
18
  return;
18
19
  const idx = enabled.findIndex((t) => t.value === active);
19
20
  let next = -1;
20
- if (e.key === "ArrowRight")
21
+ // The arrows follow the strip as it is drawn: under RTL the next tab is to
22
+ // the LEFT, so the keys swap. Everything else about the model is unchanged
23
+ // (#255).
24
+ const rtl = isRtlElement(e.currentTarget);
25
+ const forwardKey = rtl ? "ArrowLeft" : "ArrowRight";
26
+ const backKey = rtl ? "ArrowRight" : "ArrowLeft";
27
+ if (e.key === forwardKey)
21
28
  next = (idx + 1) % enabled.length;
22
- else if (e.key === "ArrowLeft")
29
+ else if (e.key === backKey)
23
30
  next = (idx - 1 + enabled.length) % enabled.length;
24
31
  else if (e.key === "Home")
25
32
  next = 0;
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
+ import { isRtlElement } from "../../core/index.js";
3
4
  import { boardRecipe } from "./board.recipe.js";
4
5
  /**
5
6
  * A kanban board with native drag-and-drop between columns. Cards are
@@ -72,11 +73,12 @@ export function Board({ columns, cards, onCardsChange, renderCard, onNewCard, ne
72
73
  }, onKeyDown: (e) => {
73
74
  if (!(e.ctrlKey || e.metaKey))
74
75
  return;
75
- const delta = e.key === "ArrowLeft"
76
- ? -1
77
- : e.key === "ArrowRight"
78
- ? 1
79
- : 0;
76
+ // the columns are a flex row, so under RTL the next one
77
+ // is to the LEFT and the keys swap (#255)
78
+ const rtl = isRtlElement(e.currentTarget);
79
+ const forwardKey = rtl ? "ArrowLeft" : "ArrowRight";
80
+ const backKey = rtl ? "ArrowRight" : "ArrowLeft";
81
+ const delta = e.key === backKey ? -1 : e.key === forwardKey ? 1 : 0;
80
82
  // only swallow the key when the move actually happens, so
81
83
  // Ctrl+Right at the last column still reaches the browser
82
84
  if (delta && moveByKey(card, delta))
@@ -377,10 +377,15 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
377
377
  if (!next)
378
378
  setFieldQuery("");
379
379
  }, children: showAllValues ? t.viewLess : t.viewAllValues }))] })) : (_jsxs("div", { className: "recordpage-noresults", children: ["No fields match \u201C", fieldQuery, "\u201D."] })))] }), _jsxs("div", { className: "recordpage-section", children: [_jsx(SectionHead, { title: t.tabs.lists, action: _jsxs("button", { type: "button", className: "recordpage-addbtn", children: [_jsx(Plus, { size: 14, strokeWidth: 2 }), t.addToList] }) }), _jsx("div", { className: "recordpage-emptystate", children: t.noLists })] })] }), _jsx("div", { className: "recordpage-resizer", "data-active": resizing ? "" : undefined, role: "separator", "aria-orientation": "vertical", "aria-label": t.resizeDetails, "aria-valuenow": Math.round(detailsW), "aria-valuemin": 300, "aria-valuemax": 560, tabIndex: 0, onPointerDown: onResizeDown, onPointerMove: onResizeMove, onPointerUp: onResizeUp, onKeyDown: (e) => {
380
- if (e.key === "ArrowLeft")
381
- setDetailsW((w) => Math.max(300, w - 16));
382
- else if (e.key === "ArrowRight")
383
- setDetailsW((w) => Math.min(560, w + 16));
380
+ if (e.key !== "ArrowLeft" && e.key !== "ArrowRight")
381
+ return;
382
+ // The pointer drag learned its direction in #212; this did not.
383
+ // The details column is the flex row's first child, so it sits on
384
+ // the RIGHT under RTL and ArrowLeft has to grow it — the same
385
+ // half-conversion the drag had, one handler away (#255).
386
+ const rtl = isRtlElement(e.currentTarget);
387
+ const grow = e.key === (rtl ? "ArrowLeft" : "ArrowRight");
388
+ setDetailsW((w) => grow ? Math.min(560, w + 16) : Math.max(300, w - 16));
384
389
  } }), _jsx("main", { className: "recordpage-main", children: _jsx(Tabs, { tabs: tabs, defaultValue: "overview", "aria-label": `${name} record`, children: panel }) })] })] }));
385
390
  }
386
391
  RecordPage.displayName = "RecordPage";
@@ -14,7 +14,7 @@ import { Menu, MenuItem, useMenuKeyboard } from "../../components/menu/index.js"
14
14
  import { MultiSelect } from "../../components/multiselect/index.js";
15
15
  import { Select } from "../../components/select/index.js";
16
16
  import { Switch } from "../../components/switch/index.js";
17
- import { isRtlElement } from "../../core/index.js";
17
+ import { isRtlElement, usePrefersReducedMotion } from "../../core/index.js";
18
18
  import { companyLogos } from "./company-logos.js";
19
19
  import { recordstableRecipe } from "./recordstable.recipe.js";
20
20
  const EDITABLE = new Set(["text", "number", "url", "currency"]);
@@ -600,10 +600,15 @@ export function RecordsTable({ labels, columns: columnsProp = defaultColumns, ro
600
600
  nr = Math.max(0, r - 1);
601
601
  else if (e.key === "ArrowDown")
602
602
  nr = Math.min(visibleRows.length - 1, r + 1);
603
- else if (e.key === "ArrowLeft")
604
- nc = Math.max(0, c - 1);
605
- else if (e.key === "ArrowRight")
606
- nc = Math.min(cols.length - 1, c + 1);
603
+ // The arrows follow what the user SEES. Under RTL the first column renders
604
+ // rightmost, so ArrowLeft moves to the next column and ArrowRight to the
605
+ // previous — the grid's own half-conversion, of the same kind the resize
606
+ // grips had (#255).
607
+ else if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
608
+ const rtl = isRtlElement(e.currentTarget);
609
+ const forward = e.key === (rtl ? "ArrowLeft" : "ArrowRight");
610
+ nc = forward ? Math.min(cols.length - 1, c + 1) : Math.max(0, c - 1);
611
+ }
607
612
  else if (e.key === "Escape") {
608
613
  setSelCell(null);
609
614
  return;
@@ -676,6 +681,36 @@ export function RecordsTable({ labels, columns: columnsProp = defaultColumns, ro
676
681
  const setSortField = (i, key) => setSorts(sorts.map((s, j) => (j === i ? { ...s, key } : s)));
677
682
  const setSortDir = (i, dir) => setSorts(sorts.map((s, j) => (j === i ? { ...s, dir } : s)));
678
683
  const removeSort = (i) => setSorts(sorts.filter((_, j) => j !== i));
684
+ /**
685
+ * Keep the selected cell on screen.
686
+ *
687
+ * Focus stays on the scroll region, never on a cell, so the browser has
688
+ * nothing to scroll to: the arrow keys moved the outline and the viewport
689
+ * stayed put. Measured in Chrome on a 384px-tall region over 40 rows —
690
+ * twenty ArrowDowns left `scrollTop` at 0 with the selected cell 761px below
691
+ * the top edge, i.e. the user was driving a selection they could not see
692
+ * (#257).
693
+ *
694
+ * `nearest` on both axes: a cell already in view is left exactly where it
695
+ * is, which is what makes this safe to run after a click too.
696
+ *
697
+ * No `behavior` of our own — the page's `scroll-behavior` decides, and Attio
698
+ * jumps rather than glides — except under reduced motion, where an explicit
699
+ * "auto" is the only thing that beats a host page that set
700
+ * `scroll-behavior: smooth` on the scroller (#208's rule: JS motion reads
701
+ * the preference itself, because CSS cannot reach an explicit argument).
702
+ */
703
+ const reduceMotion = usePrefersReducedMotion();
704
+ React.useEffect(() => {
705
+ if (!selCell)
706
+ return;
707
+ const cell = scrollRef.current?.querySelector(`[data-cell="${CSS.escape(`${selCell.id}:${selCell.key}`)}"]`);
708
+ cell?.scrollIntoView({
709
+ block: "nearest",
710
+ inline: "nearest",
711
+ ...(reduceMotion ? { behavior: "auto" } : null),
712
+ });
713
+ }, [selCell, reduceMotion]);
679
714
  const commitEdit = (id, key, raw) => {
680
715
  const col = cols.find((c) => c.key === key);
681
716
  const numeric = col?.type === "number" || col?.type === "currency";
@@ -1024,7 +1059,7 @@ export function RecordsTable({ labels, columns: columnsProp = defaultColumns, ro
1024
1059
  // row instead of a bare "182,000", and the same header is
1025
1060
  // repeated in every cell's context (#231).
1026
1061
  const Cell = (i === 0 ? "th" : "td");
1027
- return (_jsxs(Cell, { scope: i === 0 ? "row" : undefined, className: `recordstable-td${i === 0 ? " recordstable-col-entity" : ""}${canEdit ? " recordstable-td--editable" : ""}${isPicker ? " recordstable-td--picker" : ""}`, "data-enriched": col.enriched ? "" : undefined, "data-selected": selCell?.id === row.id && selCell?.key === col.key
1062
+ return (_jsxs(Cell, { scope: i === 0 ? "row" : undefined, "data-cell": `${row.id}:${col.key}`, className: `recordstable-td${i === 0 ? " recordstable-col-entity" : ""}${canEdit ? " recordstable-td--editable" : ""}${isPicker ? " recordstable-td--picker" : ""}`, "data-enriched": col.enriched ? "" : undefined, "data-selected": selCell?.id === row.id && selCell?.key === col.key
1028
1063
  ? ""
1029
1064
  : undefined, onClick: () => {
1030
1065
  if (isPicker)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crisp-ui-kit/crisp",
3
- "version": "0.31.0",
3
+ "version": "0.31.2",
4
4
  "description": "A lean React design system, measured pixel-for-pixel from Attio.",
5
5
  "license": "MIT",
6
6
  "type": "module",