diff --git a/src/components/data/datagrid/datagrid.scss b/src/components/data/datagrid/datagrid.scss
index af790f2..410060a 100644
--- a/src/components/data/datagrid/datagrid.scss
+++ b/src/components/data/datagrid/datagrid.scss
@@ -3,8 +3,38 @@
.mykn-datagrid {
background-color: var(--typography-color-background);
border-radius: var(--border-radius-l);
+ display: flex;
+ flex-direction: column;
width: 100%;
+ &__toolbar {
+ position: sticky;
+ top: 0;
+ z-index: 10;
+ }
+
+ &__scrollpane {
+ height: 100%;
+ }
+
+ &__scrollpane--overflow-x {
+ overflow-x: auto;
+ overflow-y: hidden;
+ }
+
+ &__scrollpane--overflow-y {
+ overflow-x: hidden;
+ overflow-y: auto;
+ }
+ &__scrollpane--overflow-x#{&}__scrollpane--overflow-y {
+ overflow-x: auto;
+ overflow-y: auto;
+ }
+
+ &__scrollpane ~ .mykn-toolbar {
+ border-block-start: 1px solid var(--typography-color-border);
+ }
+
&__header {
padding-block: var(--spacing-v);
padding-inline: var(--spacing-h);
@@ -15,13 +45,17 @@
width: 100%;
}
- &__table &__thead {
+ &__thead {
background-color: var(--typography-color-background);
position: sticky;
top: 0;
z-index: 1;
}
+ &__toolbar ~ &__scrollpane:not(&__scrollpane--overflow-y) &__thead {
+ top: var(--mykn-datagrid-thead-top-base, 50px);
+ }
+
&__thead &__row--filter {
background-color: var(--typography-color-background-alt);
@@ -49,6 +83,7 @@
box-sizing: border-box;
padding: var(--spacing-v) var(--spacing-h);
position: relative;
+ white-space: nowrap;
.mykn-a:not(:last-child) {
margin-inline-end: var(--spacing-h);
@@ -115,133 +150,4 @@
&__cell--link .mykn-button {
width: calc(100% - 1em - 3 * var(--spacing-h));
}
-
- &__footer {
- position: sticky;
- bottom: 0;
- }
-
- &__footer &__cell {
- border-block-start: 1px solid var(--typography-color-border);
- border-block-end: none;
- padding: 0;
- }
-
- @media screen and (max-width: constants.$breakpoint-desktop - 1px) {
- background-color: transparent;
- overflow: visible;
-
- &__table {
- display: block;
- }
-
- &__table--layout--fixed {
- table-layout: fixed;
- }
-
- &__toolbar {
- background-color: var(--typography-color-background);
- border-radius: var(--border-radius-l);
- display: block;
-
- > * {
- float: none;
- }
- }
-
- &__thead {
- position: static;
- }
-
- &__thead &__row--header {
- display: none;
- }
-
- &__tbody {
- display: block;
- }
-
- &__row {
- background-color: var(--typography-color-background);
- display: flex;
- flex-wrap: wrap;
-
- &:nth-child(even) {
- background-color: var(--typography-color-background-alt);
- }
- }
-
- &__row:nth-child(even) &__cell {
- border-block-end: 1px solid var(--typography-color-background);
- }
-
- &__cell {
- display: flex;
- flex-direction: column;
- gap: var(--spacing-h);
- width: 100%;
- padding: var(--spacing-v) var(--spacing-h);
- position: relative;
-
- .mykn-p {
- font-weight: var(--typography-font-weight-bold);
- width: 100%;
- }
-
- &:before {
- color: var(--typography-color-body);
- content: attr(aria-description);
- font-family: var(--typography-font-family-body);
- font-size: var(--typography-font-size-body-xs);
- font-weight: var(--typography-font-weight-normal);
- line-height: var(--typography-line-height-body-xs);
- display: block;
- text-align: start;
- width: 40%;
- }
-
- &:first-child .mykn-a:has(.mykn-icon) {
- float: right;
- }
- }
-
- &__cell--type-boolean,
- &__cell--type-number {
- flex-direction: row;
- justify-content: space-between;
- padding: var(--spacing-v) var(--spacing-h);
- width: 100%;
- }
-
- &__footer {
- display: flex;
- }
-
- &__footer &__row {
- width: 100%;
- }
-
- &__footer &__cell {
- &:before {
- display: none;
- }
- }
-
- .mykn-toolbar {
- border-radius: var(--border-radius-l);
- }
-
- .mykn-paginator .mykn-icon--spin:first-child {
- display: none;
- }
- }
-
- // FIXME: Improve this...
- .mykn-toolbar--sticky-top + & &__thead {
- top: calc(var(--typography-line-height-h1) + 2 * var(--spacing-v));
- }
-
- .mykn-toolbar--sticky-top:has(.mykn-form) + & &__thead {
- top: calc(42px + 2 * var(--spacing-v));
- }
}
diff --git a/src/components/data/datagrid/datagrid.tsx b/src/components/data/datagrid/datagrid.tsx
index f623276..30ea563 100644
--- a/src/components/data/datagrid/datagrid.tsx
+++ b/src/components/data/datagrid/datagrid.tsx
@@ -1,5 +1,6 @@
import clsx from "clsx";
import React, {
+ CSSProperties,
useCallback,
useContext,
useEffect,
@@ -46,6 +47,14 @@ export type DataGridProps = {
/** The object list (after pagination), only primitive types supported for now. */
objectList: AttributeData[];
+ /**
+ * Whether to allow horizontal contents to exceed the bounding box width
+ * resulting in a horizontal scrollbar. When this is set to `true` (default) a
+ * "stickyfix" is applied in certain cases to preserve the sticky behaviour of
+ * nested components.
+ */
+ allowOverflowX?: boolean;
+
/** Whether to use a "decorative" component instead of `
` if applicable. */
decorate?: boolean;
@@ -73,6 +82,16 @@ export type DataGridProps = {
*/
filterTransform?: (value: AttributeData) => AttributeData;
+ /**
+ * This value is copied one-to-one to the style attribute of the rendered
+ * datagrid.
+ *
+ * NOTE: When using `allowOverflowX=true` (default). Setting this disables
+ * the sticky fix in favor of the native implementation. Even if style is
+ * overridden completely.
+ */
+ height?: string;
+
/** Whether to allow sorting/the field to sort on. */
sort?: boolean | string;
@@ -202,10 +221,18 @@ export type DataGridProps = {
onSort?: (sort: string) => Promise | void;
} & PaginatorPropsAliases;
+const dataGridRef = React.createRef();
+const toolbarRef = React.createRef();
+const scrollPaneRef = React.createRef();
+
export type DataGridContextType = Omit<
DataGridProps,
"equalityChecker" | "fields" | "onSelect" | "onSort"
> & {
+ dataGridRef: React.RefObject;
+ toolbarRef: React.RefObject;
+ scrollPaneRef: React.RefObject;
+
amountSelected: number;
count: number;
dataGridId: string;
@@ -255,6 +282,7 @@ type PaginatorPropsAliases = {
export const DataGrid: React.FC = (props) => {
// Specify the default props.
const defaults: Partial = {
+ allowOverflowX: true,
showPaginator: Boolean(props.paginatorProps),
selectable: false,
allowSelectAll: true,
@@ -276,6 +304,7 @@ export const DataGrid: React.FC = (props) => {
// Strip all `props` from `attrs`, allowing `attrs` to be passed to the DOM.
/* eslint-disable @typescript-eslint/no-unused-vars */
const {
+ allowOverflowX,
aProps,
badgeProps,
boolProps,
@@ -286,6 +315,7 @@ export const DataGrid: React.FC = (props) => {
fields,
filterable,
filterTransform,
+ height,
paginatorProps,
showPaginator,
pProps,
@@ -332,18 +362,23 @@ export const DataGrid: React.FC = (props) => {
const id = useId();
const onFilterTimeoutRef = useRef();
+
const [editingState, setEditingState] = useState<
[AttributeData | null, number | null]
>([null, null]);
+
const [filterState, setFilterState] = useState();
const [selectedState, setSelectedState] = useState(
null,
);
+
const [allPagesSelectedState, setAllPagesSelectedState] =
useState(allPagesSelected);
+
const [sortState, setSortState] = useState<
[string, "ASC" | "DESC"] | undefined
>();
+
const [fieldsState, setFieldsState] = useState>([]);
// Update selectedState when selected prop changes.
@@ -537,6 +572,10 @@ export const DataGrid: React.FC = (props) => {
return (
= (props) => {
onSelectAllPages: handleSelectAllPages,
}}
>
-
+
{title &&
}
{(selectable || fieldsSelectable) &&
}
-
+
+
+
+
+
+
{showPaginator &&
}
{filterable &&
}
@@ -615,6 +650,7 @@ export const DataGridHeader: React.FC = () => {
* DataGrid toolbar, shows selection actions and/or allows the user to select fields (columns).
*/
export const DataGridToolbar: React.FC = () => {
+ const { toolbarRef } = useContext(DataGridContext);
const intl = useIntl();
const [selectFieldsModalState, setSelectFieldsModalState] = useState(false);
const [selectFieldsActiveState, setSelectFieldsActiveState] = useState<
@@ -704,8 +740,8 @@ export const DataGridToolbar: React.FC = () => {
];
return (
-
-
+
+
{
);
};
+/**
+ * Datagrid scroll pane, contains the scrollable content.
+ * @param children
+ * @constructor
+ */
+export const DataGridScrollPane: React.FC = ({
+ children,
+}) => {
+ const { allowOverflowX, scrollPaneRef } = useContext(DataGridContext);
+
+ // Overflow detection
+ useEffect(() => {
+ detectOverflowX();
+ window.addEventListener("resize", detectOverflowX);
+ window.addEventListener("scroll", detectOverflowX);
+ () => window.removeEventListener("resize", detectOverflowX);
+ });
+
+ /**
+ * Toggles "mykn-datagrid__scrollpane--overflow-x" to class list based on
+ * whether `allowOverflowX=true` and the contents are overflowing.
+ */
+ const detectOverflowX = () => {
+ if (!scrollPaneRef?.current) {
+ return;
+ }
+ const node = scrollPaneRef.current;
+
+ const hasOverflowX = node.scrollWidth > node.clientWidth;
+ const expX = allowOverflowX && hasOverflowX;
+ node.classList.toggle("mykn-datagrid__scrollpane--overflow-x", expX);
+
+ const hasOverflowY = node.scrollHeight > node.clientHeight;
+ const expY = hasOverflowY;
+ node.classList.toggle("mykn-datagrid__scrollpane--overflow-y", expY);
+ };
+
+ return (
+
+ {children}
+
+ );
+ // return null;
+};
+
+/**
+ * DataGrid table, represents tabular: information presented in a two-dimensional table comprised of rows and columns
+ * (fields) of cells containing data.
+ */
+export const DataGridTable: React.FC = ({
+ children,
+}) => {
+ const { tableLayout, titleId } = useContext(DataGridContext);
+
+ return (
+
+ );
+};
+
/**
* DataGrid table head, encapsulates a set of table rows, indicating that they
* comprise the head of a table with information about the table's columns.
*/
export const DataGridTHead: React.FC = () => {
+ const { toolbarRef, height } = useContext(DataGridContext);
const intl = useIntl();
const onFilterTimeoutRef = useRef();
+ const ref = useRef(null);
const [filterState, setFilterState] = useState();
const {
@@ -772,6 +877,49 @@ export const DataGridTHead: React.FC = () => {
selectable,
} = useContext(DataGridContext);
+ // Sticky fix
+ useEffect(() => {
+ stickyFix();
+ window.addEventListener("resize", stickyFix);
+ window.addEventListener("scroll", stickyFix);
+ () => {
+ window.removeEventListener("resize", stickyFix);
+ window.addEventListener("scroll", stickyFix);
+ };
+ });
+
+ /**
+ * Fixes sticky behaviour due to `overflow-x: auto;` not being compatible
+ * with native sticky in all cases.
+ */
+ const stickyFix = () => {
+ if (!ref.current || !scrollPaneRef.current) {
+ return;
+ }
+
+ const node = ref.current;
+ const scrollPaneNode = scrollPaneRef.current;
+ const indicator = "mykn-datagrid__scrollpane--overflow-x";
+
+ // No need for fallback implementation, native behaviour should work if height is set of no overflow is applied..
+ if (height || !scrollPaneNode?.classList?.contains(indicator)) {
+ node.style.top = "";
+ return;
+ }
+
+ requestAnimationFrame(() => {
+ node.style.top = "";
+ const computedStyle = getComputedStyle(node);
+ const cssTop = parseInt(computedStyle.top);
+
+ const boundingClientRect = node.getBoundingClientRect();
+ const boundingTop = boundingClientRect.top;
+ const compensation = boundingTop * -1 + cssTop * 2;
+
+ node.style.top = compensation + "px";
+ });
+ };
+
// Debounce filter
useEffect(() => {
const handler = () => {
@@ -786,7 +934,18 @@ export const DataGridTHead: React.FC = () => {
}, [filterState]);
return (
-
+
{/* Captions */}
{selectable && (
@@ -1281,19 +1440,17 @@ export const DataGridFooter: React.FC = () => {
} = useContext(DataGridContext);
return (
-
+
+
+
);
};
diff --git a/src/components/navbar/navbar.scss b/src/components/navbar/navbar.scss
index 93cb974..612a219 100644
--- a/src/components/navbar/navbar.scss
+++ b/src/components/navbar/navbar.scss
@@ -2,4 +2,9 @@
.mykn-logo {
width: 32px;
}
+
+ // This is a hack, it prevents unintended overflowing in specific scenario's with DataGrid.
+ .mykn-column & + :last-child {
+ width: calc(100% - 40px);
+ }
}
diff --git a/src/components/toolbar/toolbar.scss b/src/components/toolbar/toolbar.scss
index 6eeec58..9d5a7d1 100644
--- a/src/components/toolbar/toolbar.scss
+++ b/src/components/toolbar/toolbar.scss
@@ -26,6 +26,14 @@
height: 100cqh;
}
+ &--direction-responsive#{&}--direction-horizontal {
+ @media screen and (max-width: constants.$breakpoint-desktop - 1px) {
+ #{$self}__spacer {
+ display: none;
+ }
+ }
+ }
+
&:not(#{&}--direction-responsive)#{&}--direction-horizontal {
max-height: 100%;
flex-direction: row;
@@ -56,6 +64,11 @@
top: 0;
}
+ &--sticky-bottom {
+ position: sticky;
+ bottom: 0;
+ }
+
&__spacer {
height: 100%;
flex-shrink: 9999;
diff --git a/src/components/toolbar/toolbar.tsx b/src/components/toolbar/toolbar.tsx
index 4af3703..0dd262e 100644
--- a/src/components/toolbar/toolbar.tsx
+++ b/src/components/toolbar/toolbar.tsx
@@ -50,7 +50,7 @@ export type ToolbarProps = React.PropsWithChildren<
size?: "fit-content";
/** When set tot true, toolbar will be positioned using display: sticky. */
- sticky?: false | "top";
+ sticky?: false | "top" | "bottom";
/** The variant (style) of the toolbar. */
variant?: "normal" | "primary" | "accent" | "transparent";
diff --git a/src/style/tokens/base.scss b/src/style/tokens/base.scss
index c77ce9a..e7ba2cf 100644
--- a/src/style/tokens/base.scss
+++ b/src/style/tokens/base.scss
@@ -1,4 +1,10 @@
@mixin tokens {
+ @at-root {
+ body {
+ overflow-x: hidden;
+ }
+ }
+
/* BRANDING */
$purple-rain: #341a90;
$blue-suede-shoes: #00bfcb;