@sit-onyx/headless 0.7.1-dev-20260316075404 → 0.7.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.
@@ -1,8 +1,8 @@
1
1
  import { MaybeRefOrGetter, Ref } from 'vue';
2
2
  type RequestLazyLoad = (colIndex: number, rowIndex: number) => Promise<void>;
3
3
  type CellIdentifier = {
4
- rowId: string;
5
- colKey: string;
4
+ rowId: PropertyKey;
5
+ colKey: PropertyKey;
6
6
  };
7
7
  export type LazyOptions<Lazy extends boolean> = Lazy extends true ? {
8
8
  lazy: MaybeRefOrGetter<{
@@ -29,7 +29,7 @@ export type CreateDataGridOptions<Lazy extends boolean> = {
29
29
  */
30
30
  multiselectable?: boolean;
31
31
  loading?: MaybeRefOrGetter<boolean>;
32
- selectedCell?: Ref<CellIdentifier>;
32
+ selectedCell?: Ref<CellIdentifier | undefined>;
33
33
  } & LazyOptions<Lazy>;
34
34
  export type TrOptions<Lazy extends boolean> = {
35
35
  rowId: PropertyKey;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './composables/calendar/createCalendar.js';
2
2
  export * from './composables/comboBox/createComboBox.js';
3
+ export * from './composables/dataGrid/createDataGrid.js';
3
4
  export * from './composables/helpers/useGlobalListener.js';
4
5
  export * from './composables/helpers/useOutsideClick.js';
5
6
  export * from './composables/listbox/createListbox.js';
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { shallowRef, computed, toValue, ref, watch, nextTick, reactive, onBeforeMount, watchEffect, onBeforeUnmount, unref, useId, toRef } from "vue";
1
+ import { shallowRef, computed, toValue, ref, watch, nextTick, reactive, onBeforeMount, watchEffect, onBeforeUnmount, unref, useId, toRef, onMounted } from "vue";
2
2
  const createBuilder = (builder) => builder;
3
3
  function createElRef() {
4
4
  const elementRef = shallowRef(null);
@@ -747,6 +747,247 @@ const createComboBox = createBuilder(
747
747
  };
748
748
  }
749
749
  );
750
+ const useAllSettled = (cb) => {
751
+ const active = ref(false);
752
+ const allPromises = [];
753
+ let latestPromise = Promise.resolve();
754
+ const add = (promise) => {
755
+ active.value = true;
756
+ allPromises.push(promise);
757
+ const newAllSettled = Promise.allSettled(allPromises).then(() => {
758
+ if (newAllSettled === latestPromise) {
759
+ active.value = false;
760
+ allPromises.splice(0, allPromises.length);
761
+ }
762
+ });
763
+ latestPromise = newAllSettled;
764
+ };
765
+ return { add, active };
766
+ };
767
+ const useLastSettled = (cb) => {
768
+ const active = ref(false);
769
+ let lastId = -1;
770
+ const add = (promise) => {
771
+ const promiseId = ++lastId;
772
+ active.value = true;
773
+ const onFinally = (success) => (resolved) => {
774
+ if (promiseId === lastId) {
775
+ active.value = false;
776
+ if (success) {
777
+ cb(success, resolved);
778
+ } else {
779
+ cb(success);
780
+ }
781
+ }
782
+ };
783
+ promise.then(onFinally(true)).catch(onFinally(false));
784
+ };
785
+ const cancel = () => {
786
+ active.value = false;
787
+ lastId++;
788
+ };
789
+ return { active, add, cancel };
790
+ };
791
+ const COL_KEY_DATA_ATTR = "data-onyx-col-key";
792
+ const COL_INDEX_ARIA_ATTR = "aria-colindex";
793
+ const ROW_ID_DATA_ATTR = "data-onyx-row-id";
794
+ const ROW_INDEX_ARIA_ATTR = "aria-rowindex";
795
+ const StaticResolver = {
796
+ mapCellToIndex: (cell) => Array.from(cell.closest("tr")?.cells ?? []).indexOf(cell),
797
+ mapRowToIndex: (row) => Array.from(row.closest("table")?.rows ?? []).indexOf(row),
798
+ resolveCell: async (cellIndex, rowIndex, table) => table.rows.item(rowIndex)?.cells.item(cellIndex),
799
+ getTotalRows: (table) => table.rows.length,
800
+ getTotalCols: (table) => table.rows.item(0)?.cells.length ?? 0
801
+ };
802
+ const LazyResolverFactory = ({
803
+ rows,
804
+ cols,
805
+ requestLazyLoad
806
+ }) => ({
807
+ mapCellToIndex: (cell) => Number(cell.getAttribute(COL_INDEX_ARIA_ATTR)) - 1,
808
+ mapRowToIndex: (row) => Number(row.getAttribute(ROW_INDEX_ARIA_ATTR)) - 1,
809
+ resolveCell: async (cellIndex, rowIndex, table) => {
810
+ const queryCell = () => table.querySelector(
811
+ `*[${ROW_INDEX_ARIA_ATTR}="${rowIndex + 1}"] *[${COL_INDEX_ARIA_ATTR}="${cellIndex + 1}"]`
812
+ );
813
+ let cell = queryCell();
814
+ if (cell) {
815
+ return cell;
816
+ }
817
+ await requestLazyLoad(cellIndex, rowIndex);
818
+ cell = queryCell();
819
+ if (cell) {
820
+ return cell;
821
+ }
822
+ throw new Error(
823
+ `Table cell with row index "${rowIndex}" and column index "${cellIndex}" was not found after requested lazy loading and is unable to be focused!`
824
+ );
825
+ },
826
+ getTotalRows: () => toValue(rows),
827
+ getTotalCols: () => toValue(cols)
828
+ });
829
+ const createDataGrid = createBuilder(
830
+ (options) => {
831
+ const tableElement = createElRef();
832
+ const lazy = options.lazy && toRef(options.lazy);
833
+ const busy = computed(() => toValue(options.loading) ?? busySet.active.value);
834
+ const resolver = lazy ? LazyResolverFactory({
835
+ cols: () => lazy.value.totalCols,
836
+ rows: () => lazy.value.totalRows,
837
+ requestLazyLoad: lazy.value.requestLazyLoad
838
+ }) : StaticResolver;
839
+ const labelId = useId();
840
+ const selectedCell = options.selectedCell || ref();
841
+ const selectedCellEl = createElRef();
842
+ const focusQueue = useLastSettled((success, cell) => {
843
+ if (success) {
844
+ cell?.focus();
845
+ }
846
+ });
847
+ const busySet = useAllSettled();
848
+ const findFirstCell = () => tableElement.value?.querySelector?.(
849
+ `[${ROW_ID_DATA_ATTR}] [${COL_KEY_DATA_ATTR}]`
850
+ );
851
+ const setSelected = (element) => {
852
+ const colKey = element.closest(`[${COL_KEY_DATA_ATTR}]`)?.getAttribute(COL_KEY_DATA_ATTR);
853
+ const rowId = element.closest(`[${ROW_ID_DATA_ATTR}]`)?.getAttribute(ROW_ID_DATA_ATTR);
854
+ if (colKey && rowId) {
855
+ selectedCell.value = {
856
+ rowId,
857
+ colKey
858
+ };
859
+ }
860
+ };
861
+ const ensureTabTarget = () => {
862
+ if (selectedCell.value && selectedCellEl.value?.isConnected) {
863
+ return;
864
+ }
865
+ const firstCell = findFirstCell();
866
+ if (firstCell) {
867
+ setSelected(firstCell);
868
+ }
869
+ };
870
+ let mutationObserver;
871
+ onMounted(() => {
872
+ ensureTabTarget();
873
+ mutationObserver = new MutationObserver(ensureTabTarget);
874
+ watch(
875
+ tableElement,
876
+ () => {
877
+ if (!tableElement.value) {
878
+ return;
879
+ }
880
+ mutationObserver?.disconnect();
881
+ mutationObserver?.observe(tableElement.value, {
882
+ childList: true,
883
+ attributes: true,
884
+ subtree: true,
885
+ attributeFilter: ["value"]
886
+ });
887
+ },
888
+ { immediate: true }
889
+ );
890
+ });
891
+ onBeforeUnmount(() => {
892
+ mutationObserver?.disconnect();
893
+ });
894
+ const onFocusin = (event) => {
895
+ setSelected(event.target);
896
+ focusQueue.cancel();
897
+ };
898
+ const onKeydown = (event) => {
899
+ const target = event.target;
900
+ const cellElement = target.closest("td, th");
901
+ const rowElement = target.closest("tr");
902
+ const tableElement2 = target.closest("table");
903
+ if (!cellElement || !rowElement || !tableElement2) {
904
+ return;
905
+ }
906
+ const { getTotalRows, getTotalCols, mapRowToIndex, mapCellToIndex, resolveCell } = resolver;
907
+ const colIndex = mapCellToIndex(cellElement);
908
+ const rowIndex = mapRowToIndex(rowElement);
909
+ const totalRows = getTotalRows(tableElement2);
910
+ const totalCols = getTotalCols(tableElement2);
911
+ let newColIndex = colIndex;
912
+ let newRowIndex = rowIndex;
913
+ if (wasKeyPressed(event, { ctrlKey: true, key: "Home" })) {
914
+ newColIndex = 0;
915
+ newRowIndex = 0;
916
+ } else if (wasKeyPressed(event, { ctrlKey: true, key: "End" })) {
917
+ newColIndex = totalCols === "unknown" ? Infinity : totalCols - 1;
918
+ newRowIndex = totalRows === "unknown" ? Infinity : totalRows - 1;
919
+ } else if (wasKeyPressed(event, "ArrowUp")) {
920
+ newRowIndex = rowIndex - 1;
921
+ } else if (wasKeyPressed(event, "ArrowDown")) {
922
+ newRowIndex = rowIndex + 1;
923
+ } else if (wasKeyPressed(event, "ArrowLeft")) {
924
+ newColIndex = colIndex - 1;
925
+ } else if (wasKeyPressed(event, "ArrowRight")) {
926
+ newColIndex = colIndex + 1;
927
+ } else if (wasKeyPressed(event, "Home")) {
928
+ newColIndex = 0;
929
+ } else if (wasKeyPressed(event, "End")) {
930
+ newColIndex = totalCols === "unknown" ? Infinity : totalCols - 1;
931
+ } else {
932
+ return;
933
+ }
934
+ event.preventDefault();
935
+ const maxRows = totalRows === "unknown" ? Infinity : totalRows - 1;
936
+ newRowIndex = Math.max(Math.min(newRowIndex, maxRows), 0);
937
+ const maxCols = totalCols === "unknown" ? Infinity : totalCols - 1;
938
+ newColIndex = Math.max(Math.min(newColIndex, maxCols), 0);
939
+ (async () => {
940
+ const promiseResolveCell = resolveCell(newColIndex, newRowIndex, tableElement2);
941
+ focusQueue.add(promiseResolveCell);
942
+ busySet.add(promiseResolveCell);
943
+ })();
944
+ };
945
+ return {
946
+ elements: {
947
+ label: {
948
+ id: labelId
949
+ },
950
+ table: computed(
951
+ () => ({
952
+ ref: tableElement,
953
+ onFocusin,
954
+ onKeydown,
955
+ role: "grid",
956
+ "aria-busy": busy.value,
957
+ "aria-labelledby": labelId,
958
+ "aria-rowcount": lazy?.value.totalRows === "unknown" ? -1 : lazy?.value.totalRows,
959
+ "aria-colcount": lazy?.value.totalCols === "unknown" ? -1 : lazy?.value.totalCols
960
+ })
961
+ ),
962
+ tr: ({ rowId, rowIndex }) => ({
963
+ [ROW_ID_DATA_ATTR]: rowId.toString(),
964
+ "aria-rowindex": rowIndex == void 0 ? void 0 : rowIndex + 1,
965
+ role: "row"
966
+ }),
967
+ td: computed(() => ({ rowId, colKey, colIndex }) => {
968
+ const isSelected = colKey.toString() === selectedCell.value?.colKey.toString() && rowId.toString() === selectedCell.value?.rowId.toString();
969
+ return {
970
+ tabindex: isSelected ? "0" : "-1",
971
+ ref: isSelected ? selectedCellEl : void 0,
972
+ [COL_KEY_DATA_ATTR]: colKey.toString(),
973
+ // TODO: handle symbols
974
+ "aria-colindex": colIndex == void 0 ? void 0 : colIndex + 1,
975
+ role: "cell"
976
+ };
977
+ })
978
+ },
979
+ state: {
980
+ /**
981
+ * Indicates that the data grid expects a content change soon, e.g. because more or other data is loaded.
982
+ * If `loading` is passed in via the options, this will mirror its value.
983
+ * Otherwise it will be dynamically set based on the running state of the `requestLazyLoad` promises.
984
+ */
985
+ busy
986
+ },
987
+ internals: {}
988
+ };
989
+ }
990
+ );
750
991
  const createMenuButton = createBuilder((options) => {
751
992
  const rootId = useId();
752
993
  const menuId = useId();
@@ -1426,6 +1667,7 @@ export {
1426
1667
  _unstableCreateCalendar,
1427
1668
  createBuilder,
1428
1669
  createComboBox,
1670
+ createDataGrid,
1429
1671
  createElRef,
1430
1672
  createListbox,
1431
1673
  createMenuButton,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sit-onyx/headless",
3
3
  "description": "Headless composables for Vue",
4
- "version": "0.7.1-dev-20260316075404",
4
+ "version": "0.7.1",
5
5
  "type": "module",
6
6
  "author": "Schwarz IT KG",
7
7
  "license": "Apache-2.0",