Repository: Thinkmill/keystatic
Pull request #587 (2023-08-24 06:46:25)

Data attributes DX
Move `toDataAttributes` to "style" package:

- add tests
- improve API
- improve implementation

changeset-bot[bot]: ###  ⚠️  No Changeset found

Latest commit: 9281b01d519a156ca01a588bce794b4ca0a10cf4

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/Thinkmill/keystatic/new/data-attributes-dx?filename=.changeset/nasty-wolves-dress.md&value=---%0A%22%40keystar%2Fui%22%3A%20patch%0A---%0A%0AData%20attributes%20DX%0A)


vercel[bot]: [vc]: #ZVhbBQlEZSjNh9wVwGoFnNr9k+B8PiuHDx3qega7rEk=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJrZXlzdGF0aWMiLCJyb290RGlyZWN0b3J5IjoiZGV2LXByb2plY3RzL25leHQtYXBwIiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3RoaW5rbWlsbC1sYWJzL2tleXN0YXRpYy8yUXBBZHc1NDFianZiejExUjV6aWNjRGlRMjNxIiwicHJldmlld1VybCI6ImtleXN0YXRpYy1naXQtZGF0YS1hdHRyaWJ1dGVzLWR4LXRoaW5rbWlsbC1sYWJzLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiUEVORElORyJ9LHsibmFtZSI6ImtleXN0YXItdWkiLCJyb290RGlyZWN0b3J5IjoiZGVzaWduLXN5c3RlbS9kb2NzIiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3RoaW5rbWlsbC1sYWJzL2tleXN0YXItdWkvSEJxcm1iR1VaU1JVdTRNcVRoN2FzUVhUbzZIQiIsInByZXZpZXdVcmwiOiJrZXlzdGFyLXVpLWdpdC1kYXRhLWF0dHJpYnV0ZXMtZHgtdGhpbmttaWxsLWxhYnMudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJQRU5ESU5HIn0seyJuYW1lIjoia2V5c3RhdGljLXNpdGUiLCJyb290RGlyZWN0b3J5IjoiZG9jcyIsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS90aGlua21pbGwtbGFicy9rZXlzdGF0aWMtc2l0ZS9CQVBQVFg3QjJrQ3BYbzRDVk16YXNSQ2trUVp3IiwicHJldmlld1VybCI6ImtleXN0YXRpYy1zaXRlLWdpdC1kYXRhLWF0dHJpYnV0ZXMtZHgtdGhpbmttaWxsLWxhYnMudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJQRU5ESU5HIn1dfQ==
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Updated (UTC) |
| :--- | :----- | :------ | :------ |
| **keystar-ui** | 🔄 Building ([Inspect](https://vercel.com/thinkmill-labs/keystar-ui/HBqrmbGUZSRUu4MqTh7asQXTo6HB)) | [Visit Preview](https://keystar-ui-git-data-attributes-dx-thinkmill-labs.vercel.app) | Aug 24, 2023 6:46am |
| **keystatic** | 🔄 Building ([Inspect](https://vercel.com/thinkmill-labs/keystatic/2QpAdw541bjvbz11R5ziccDiQ23q)) | [Visit Preview](https://keystatic-git-data-attributes-dx-thinkmill-labs.vercel.app) | Aug 24, 2023 6:46am |
| **keystatic-site** | 🔄 Building ([Inspect](https://vercel.com/thinkmill-labs/keystatic-site/BAPPTX7B2kCpXo4CVMzasRCkkQZw)) | [Visit Preview](https://keystatic-site-git-data-attributes-dx-thinkmill-labs.vercel.app) | Aug 24, 2023 6:46am |





Patch:
diff --git a/.changeset/perfect-kiwis-carry.md b/.changeset/perfect-kiwis-carry.md
new file mode 100644
index 00000000..65da8653
--- /dev/null
+++ b/.changeset/perfect-kiwis-carry.md
@@ -0,0 +1,10 @@
+---
+'@keystatic/core': patch
+'@keystar/ui': patch
+---
+
+Move `toDataAttributes` to "style" package:
+
+- add tests
+- improve API
+- improve implementation
diff --git a/design-system/pkg/src/action-group/ActionGroup.tsx b/design-system/pkg/src/action-group/ActionGroup.tsx
index b63efa27..68ec7bc3 100644
--- a/design-system/pkg/src/action-group/ActionGroup.tsx
+++ b/design-system/pkg/src/action-group/ActionGroup.tsx
@@ -38,10 +38,11 @@ import {
   BaseStyleProps,
   classNames,
   css,
+  toDataAttributes,
   tokenSchema,
   useStyleProps,
 } from '@keystar/ui/style';
-import { isReactText, toDataAttributes } from '@keystar/ui/utils';
+import { isReactText } from '@keystar/ui/utils';
 
 import { ActionGroupProps } from './types';
 
diff --git a/design-system/pkg/src/breadcrumbs/BreadcrumbItem.tsx b/design-system/pkg/src/breadcrumbs/BreadcrumbItem.tsx
index 2b0f7c8f..fb233a76 100644
--- a/design-system/pkg/src/breadcrumbs/BreadcrumbItem.tsx
+++ b/design-system/pkg/src/breadcrumbs/BreadcrumbItem.tsx
@@ -9,13 +9,12 @@ import { chevronRightIcon } from '@keystar/ui/icon/icons/chevronRightIcon';
 import { chevronLeftIcon } from '@keystar/ui/icon/icons/chevronLeftIcon';
 import {
   ClassList,
+  FocusRing,
   classNames,
   css,
-  FocusRing,
+  toDataAttributes,
   tokenSchema,
 } from '@keystar/ui/style';
-import {} from '@keystar/ui/typography';
-import { toDataAttributes } from '@keystar/ui/utils';
 
 import { BreadcrumbItemProps } from './types';
 
diff --git a/design-system/pkg/src/button/ButtonGroup.tsx b/design-system/pkg/src/button/ButtonGroup.tsx
index c14d45d3..d25c890c 100644
--- a/design-system/pkg/src/button/ButtonGroup.tsx
+++ b/design-system/pkg/src/button/ButtonGroup.tsx
@@ -20,8 +20,13 @@ import {
   useProviderProps,
 } from '@keystar/ui/core';
 import { useSlotProps } from '@keystar/ui/slots';
-import { classNames, css, tokenSchema, useStyleProps } from '@keystar/ui/style';
-import { toDataAttributes } from '@keystar/ui/utils';
+import {
+  classNames,
+  css,
+  toDataAttributes,
+  tokenSchema,
+  useStyleProps,
+} from '@keystar/ui/style';
 
 import { ButtonGroupProps } from './types';
 
diff --git a/design-system/pkg/src/button/ClearButton.tsx b/design-system/pkg/src/button/ClearButton.tsx
index 08a39bbf..4b972a1a 100644
--- a/design-system/pkg/src/button/ClearButton.tsx
+++ b/design-system/pkg/src/button/ClearButton.tsx
@@ -9,11 +9,11 @@ import {
   classNames,
   css,
   FocusRing,
+  toDataAttributes,
   tokenSchema,
   transition,
   useStyleProps,
 } from '@keystar/ui/style';
-import { toDataAttributes } from '@keystar/ui/utils';
 import { PropsWithElementType } from '@keystar/ui/utils/ts';
 
 import { ButtonProps } from './types';
diff --git a/design-system/pkg/src/button/useActionButtonStyles.tsx b/design-system/pkg/src/button/useActionButtonStyles.tsx
index 406e05c5..4787a017 100644
--- a/design-system/pkg/src/button/useActionButtonStyles.tsx
+++ b/design-system/pkg/src/button/useActionButtonStyles.tsx
@@ -2,11 +2,11 @@ import {
   ClassList,
   classNames,
   css,
+  toDataAttributes,
   tokenSchema,
   transition,
   useStyleProps,
 } from '@keystar/ui/style';
-import { toDataAttributes } from '@keystar/ui/utils';
 
 import { ActionButtonProps } from './types';
 
diff --git a/design-system/pkg/src/button/useButtonStyles.tsx b/design-system/pkg/src/button/useButtonStyles.tsx
index e18afb9a..28ff9119 100644
--- a/design-system/pkg/src/button/useButtonStyles.tsx
+++ b/design-system/pkg/src/button/useButtonStyles.tsx
@@ -2,11 +2,11 @@ import {
   ClassList,
   classNames,
   css,
+  toDataAttributes,
   tokenSchema,
   transition,
   useStyleProps,
 } from '@keystar/ui/style';
-import { toDataAttributes } from '@keystar/ui/utils';
 
 import { ButtonProps } from './types';
 
diff --git a/design-system/pkg/src/checkbox/CheckboxGroup.tsx b/design-system/pkg/src/checkbox/CheckboxGroup.tsx
index 5fa52b3f..eae90f1b 100644
--- a/design-system/pkg/src/checkbox/CheckboxGroup.tsx
+++ b/design-system/pkg/src/checkbox/CheckboxGroup.tsx
@@ -8,11 +8,15 @@ import React, {
 
 import { useProviderProps } from '@keystar/ui/core';
 import { FieldPrimitive, validateFieldProps } from '@keystar/ui/field';
-import { classNames, css, tokenSchema } from '@keystar/ui/style';
+import {
+  classNames,
+  css,
+  toDataAttributes,
+  tokenSchema,
+} from '@keystar/ui/style';
 
 import { CheckboxGroupContext } from './context';
 import { CheckboxGroupProps } from './types';
-import { toDataAttributes } from '@keystar/ui/utils';
 
 /**
  * A checkbox group allows users to select one or more items from a list of
diff --git a/design-system/pkg/src/combobox/MobileCombobox.tsx b/design-system/pkg/src/combobox/MobileCombobox.tsx
index dfb9346e..ecb63631 100644
--- a/design-system/pkg/src/combobox/MobileCombobox.tsx
+++ b/design-system/pkg/src/combobox/MobileCombobox.tsx
@@ -27,22 +27,26 @@ import {
   FieldButtonProps,
   useFieldButton,
 } from '@keystar/ui/button';
+import { useProviderProps } from '@keystar/ui/core';
 import { FieldPrimitive } from '@keystar/ui/field';
 import { Icon } from '@keystar/ui/icon';
 import { chevronDownIcon } from '@keystar/ui/icon/icons/chevronDownIcon';
+import { Flex } from '@keystar/ui/layout';
 import { ListBoxBase, useListBoxLayout } from '@keystar/ui/listbox';
 import { Tray } from '@keystar/ui/overlays';
 import { ProgressCircle } from '@keystar/ui/progress';
-import { FocusRing, css, tokenSchema, transition } from '@keystar/ui/style';
+import {
+  FocusRing,
+  css,
+  toDataAttributes,
+  tokenSchema,
+  transition,
+} from '@keystar/ui/style';
 import { TextFieldPrimitive } from '@keystar/ui/text-field';
-
-import { useProviderProps } from '@keystar/ui/core';
+import { Text } from '@keystar/ui/typography';
 
 import localizedMessages from './l10n.json';
 import { ComboboxProps } from './types';
-import { Flex } from '@keystar/ui/layout';
-import { Text } from '@keystar/ui/typography';
-import { toDataAttributes } from '@keystar/ui/utils';
 
 function MobileCombobox<T extends object>(
   props: ComboboxProps<T>,
diff --git a/design-system/pkg/src/date-time/Input.tsx b/design-system/pkg/src/date-time/Input.tsx
index 3045a8e1..5a0fba17 100644
--- a/design-system/pkg/src/date-time/Input.tsx
+++ b/design-system/pkg/src/date-time/Input.tsx
@@ -2,8 +2,12 @@ import { mergeProps, mergeRefs } from '@react-aria/utils';
 import { useFocusRing } from '@react-aria/focus';
 import { ReactNode, forwardRef, useRef } from 'react';
 
-import { classNames, css, tokenSchema } from '@keystar/ui/style';
-import { toDataAttributes } from '@keystar/ui/utils';
+import {
+  classNames,
+  css,
+  toDataAttributes,
+  tokenSchema,
+} from '@keystar/ui/style';
 
 type InputProps = {
   autoFocus?: boolean;
diff --git a/design-system/pkg/src/date-time/InputSegment.tsx b/design-system/pkg/src/date-time/InputSegment.tsx
index 5ff89a0c..67d8f84e 100644
--- a/design-system/pkg/src/date-time/InputSegment.tsx
+++ b/design-system/pkg/src/date-time/InputSegment.tsx
@@ -3,9 +3,8 @@ import { DateFieldState, DateSegment } from '@react-stately/datepicker';
 import { DatePickerBase, DateValue } from '@react-types/datepicker';
 import React, { useRef } from 'react';
 
-import { css, tokenSchema } from '@keystar/ui/style';
+import { css, toDataAttributes, tokenSchema } from '@keystar/ui/style';
 import { Text } from '@keystar/ui/typography';
-import { toDataAttributes } from '@keystar/ui/utils';
 
 interface InputSegmentProps extends DatePickerBase<DateValue> {
   segment: DateSegment;
diff --git a/design-system/pkg/src/dialog/Dialog.tsx b/design-system/pkg/src/dialog/Dialog.tsx
index b9087038..8b6a8c68 100644
--- a/design-system/pkg/src/dialog/Dialog.tsx
+++ b/design-system/pkg/src/dialog/Dialog.tsx
@@ -20,10 +20,11 @@ import {
   breakpointQueries,
   classNames,
   css,
+  toDataAttributes,
   tokenSchema,
   useStyleProps,
 } from '@keystar/ui/style';
-import { toDataAttributes, useHasChild } from '@keystar/ui/utils';
+import { useHasChild } from '@keystar/ui/utils';
 
 import localizedMessages from './l10n.json';
 import { DialogContext, DialogContextValue } from './context';
diff --git a/design-system/pkg/src/icon/Icon.tsx b/design-system/pkg/src/icon/Icon.tsx
index 2c994e08..6df18f88 100644
--- a/design-system/pkg/src/icon/Icon.tsx
+++ b/design-system/pkg/src/icon/Icon.tsx
@@ -6,11 +6,11 @@ import {
   classNames,
   css,
   maybeTokenByKey,
+  toDataAttributes,
   tokenSchema,
   useStyleProps,
 } from '@keystar/ui/style';
 import { IconProps } from '@keystar/ui/types';
-import { toDataAttributes } from '@keystar/ui/utils';
 
 export const Icon = (props: IconProps) => {
   props = useSlotProps(props, 'icon');
diff --git a/design-system/pkg/src/layout/Divider.tsx b/design-system/pkg/src/layout/Divider.tsx
index b8bbdd7e..b8e61bb0 100644
--- a/design-system/pkg/src/layout/Divider.tsx
+++ b/design-system/pkg/src/layout/Divider.tsx
@@ -6,11 +6,11 @@ import {
   ClassList,
   classNames,
   css,
-  useStyleProps,
+  toDataAttributes,
   tokenSchema,
+  useStyleProps,
 } from '@keystar/ui/style';
 import { DividerProps } from '@keystar/ui/types';
-import { toDataAttributes } from '@keystar/ui/utils';
 import { forwardRefWithAs } from '@keystar/ui/utils/ts';
 
 export const dividerClassList = new ClassList('Divider');
diff --git a/design-system/pkg/src/link/TextLink/useTextLink.ts b/design-system/pkg/src/link/TextLink/useTextLink.ts
index d61fcaae..b6246b36 100644
--- a/design-system/pkg/src/link/TextLink/useTextLink.ts
+++ b/design-system/pkg/src/link/TextLink/useTextLink.ts
@@ -3,13 +3,17 @@ import { useHover } from '@react-aria/interactions';
 import { mergeProps } from '@react-aria/utils';
 import { Fragment } from 'react';
 
-import { classNames, css, tokenSchema } from '@keystar/ui/style';
+import {
+  classNames,
+  css,
+  toDataAttributes,
+  tokenSchema,
+} from '@keystar/ui/style';
 import {
   Text,
   useHeadingContext,
   useTextContext,
 } from '@keystar/ui/typography';
-import { toDataAttributes } from '@keystar/ui/utils';
 
 import { TextLinkButtonProps } from './types';
 
diff --git a/design-system/pkg/src/list-view/DragPreview.tsx b/design-system/pkg/src/list-view/DragPreview.tsx
index f7e41959..21fda78d 100644
--- a/design-system/pkg/src/list-view/DragPreview.tsx
+++ b/design-system/pkg/src/list-view/DragPreview.tsx
@@ -2,9 +2,14 @@ import { GridNode } from '@react-types/grid';
 
 import { Flex, Grid } from '@keystar/ui/layout';
 import { SlotProvider } from '@keystar/ui/slots';
-import { classNames, css, tokenSchema } from '@keystar/ui/style';
+import {
+  classNames,
+  css,
+  toDataAttributes,
+  tokenSchema,
+} from '@keystar/ui/style';
 import { Text } from '@keystar/ui/typography';
-import { isReactText, toDataAttributes } from '@keystar/ui/utils';
+import { isReactText } from '@keystar/ui/utils';
 
 import type { ListViewProps } from './types';
 
diff --git a/design-system/pkg/src/list-view/InsertionIndicator.tsx b/design-system/pkg/src/list-view/InsertionIndicator.tsx
index b60cdf8e..e1bac6b0 100644
--- a/design-system/pkg/src/list-view/InsertionIndicator.tsx
+++ b/design-system/pkg/src/list-view/InsertionIndicator.tsx
@@ -3,8 +3,12 @@ import { ItemDropTarget } from '@react-types/shared';
 import { assert } from 'emery';
 import { useRef } from 'react';
 
-import { classNames, css, tokenSchema } from '@keystar/ui/style';
-import { toDataAttributes } from '@keystar/ui/utils';
+import {
+  classNames,
+  css,
+  toDataAttributes,
+  tokenSchema,
+} from '@keystar/ui/style';
 
 import { useListViewContext } from './context';
 
diff --git a/design-system/pkg/src/list-view/ListView.tsx b/design-system/pkg/src/list-view/ListView.tsx
index 921ce0b9..b9aae464 100644
--- a/design-system/pkg/src/list-view/ListView.tsx
+++ b/design-system/pkg/src/list-view/ListView.tsx
@@ -22,10 +22,10 @@ import {
   FocusRing,
   classNames,
   css,
+  toDataAttributes,
   tokenSchema,
   useStyleProps,
 } from '@keystar/ui/style';
-import { toDataAttributes } from '@keystar/ui/utils';
 
 import localizedMessages from './l10n.json';
 import { ListViewProvider, useListViewContext } from './context';
diff --git a/design-system/pkg/src/list-view/ListViewItem.tsx b/design-system/pkg/src/list-view/ListViewItem.tsx
index fede9c96..27b1667a 100644
--- a/design-system/pkg/src/list-view/ListViewItem.tsx
+++ b/design-system/pkg/src/list-view/ListViewItem.tsx
@@ -30,11 +30,12 @@ import {
   FocusRing,
   classNames,
   css,
+  toDataAttributes,
   tokenSchema,
   transition,
 } from '@keystar/ui/style';
 import { Text } from '@keystar/ui/typography';
-import { isReactText, toDataAttributes } from '@keystar/ui/utils';
+import { isReactText } from '@keystar/ui/utils';
 
 import { useListViewContext } from './context';
 
diff --git a/design-system/pkg/src/listbox/ListItem.tsx b/design-system/pkg/src/listbox/ListItem.tsx
index f04856a2..793cc788 100644
--- a/design-system/pkg/src/listbox/ListItem.tsx
+++ b/design-system/pkg/src/listbox/ListItem.tsx
@@ -3,8 +3,12 @@ import { HTMLAttributes, ReactNode } from 'react';
 import { Icon } from '@keystar/ui/icon';
 import { checkIcon } from '@keystar/ui/icon/icons/checkIcon';
 import { ClearSlots, SlotProvider } from '@keystar/ui/slots';
-import { classNames, css, tokenSchema } from '@keystar/ui/style';
-import { toDataAttributes } from '@keystar/ui/utils';
+import {
+  classNames,
+  css,
+  toDataAttributes,
+  tokenSchema,
+} from '@keystar/ui/style';
 import { forwardRefWithAs } from '@keystar/ui/utils/ts';
 
 type ListItemProps = {
diff --git a/design-system/pkg/src/nav-tree/NavTree.tsx b/design-system/pkg/src/nav-tree/NavTree.tsx
index 69391f5f..7e32f267 100644
--- a/design-system/pkg/src/nav-tree/NavTree.tsx
+++ b/design-system/pkg/src/nav-tree/NavTree.tsx
@@ -32,9 +32,15 @@ import { chevronLeftIcon } from '@keystar/ui/icon/icons/chevronLeftIcon';
 import { chevronRightIcon } from '@keystar/ui/icon/icons/chevronRightIcon';
 import { dotIcon } from '@keystar/ui/icon/icons/dotIcon';
 import { SlotProvider } from '@keystar/ui/slots';
-import { classNames, css, tokenSchema, useStyleProps } from '@keystar/ui/style';
+import {
+  classNames,
+  css,
+  toDataAttributes,
+  tokenSchema,
+  useStyleProps,
+} from '@keystar/ui/style';
 import { Text } from '@keystar/ui/typography';
-import { isReactText, toDataAttributes } from '@keystar/ui/utils';
+import { isReactText } from '@keystar/ui/utils';
 
 import { TreeKeyboardDelegate } from './TreeKeyboardDelegate';
 import { NavTreeProps } from './types';
diff --git a/design-system/pkg/src/number-field/NumberField.tsx b/design-system/pkg/src/number-field/NumberField.tsx
index b01772d5..62cf362a 100644
--- a/design-system/pkg/src/number-field/NumberField.tsx
+++ b/design-system/pkg/src/number-field/NumberField.tsx
@@ -10,9 +10,13 @@ import {
 } from 'react';
 
 import { useProvider, useProviderProps } from '@keystar/ui/core';
-import { css, onlyStyleProps, tokenSchema } from '@keystar/ui/style';
+import {
+  css,
+  onlyStyleProps,
+  toDataAttributes,
+  tokenSchema,
+} from '@keystar/ui/style';
 import { TextFieldPrimitive } from '@keystar/ui/text-field';
-import { toDataAttributes } from '@keystar/ui/utils';
 
 import { StepButton } from './StepButton';
 import { NumberFieldProps } from './types';
diff --git a/design-system/pkg/src/number-field/StepButton.tsx b/design-system/pkg/src/number-field/StepButton.tsx
index d0b6568b..323d5b6a 100644
--- a/design-system/pkg/src/number-field/StepButton.tsx
+++ b/design-system/pkg/src/number-field/StepButton.tsx
@@ -10,8 +10,13 @@ import { minusIcon } from '@keystar/ui/icon/icons/minusIcon';
 import { chevronDownIcon } from '@keystar/ui/icon/icons/chevronDownIcon';
 import { chevronUpIcon } from '@keystar/ui/icon/icons/chevronUpIcon';
 import { Icon } from '@keystar/ui/icon';
-import { classNames, css, tokenSchema, transition } from '@keystar/ui/style';
-import { toDataAttributes } from '@keystar/ui/utils';
+import {
+  classNames,
+  css,
+  toDataAttributes,
+  tokenSchema,
+  transition,
+} from '@keystar/ui/style';
 
 interface StepButtonProps extends AriaButtonProps {
   direction: 'up' | 'down';
diff --git a/design-system/pkg/src/overlays/Blanket.tsx b/design-system/pkg/src/overlays/Blanket.tsx
index 436cfcf3..1382f3c9 100644
--- a/design-system/pkg/src/overlays/Blanket.tsx
+++ b/design-system/pkg/src/overlays/Blanket.tsx
@@ -2,10 +2,10 @@ import {
   classNames,
   css,
   filterStyleProps,
+  toDataAttributes,
   transition,
   useStyleProps,
 } from '@keystar/ui/style';
-import { toDataAttributes } from '@keystar/ui/utils';
 
 import { BlanketProps } from './types';
 
diff --git a/design-system/pkg/src/overlays/DirectionIndicator.tsx b/design-system/pkg/src/overlays/DirectionIndicator.tsx
index 9b347087..977e758f 100644
--- a/design-system/pkg/src/overlays/DirectionIndicator.tsx
+++ b/design-system/pkg/src/overlays/DirectionIndicator.tsx
@@ -1,5 +1,9 @@
-import { classNames, css, tokenSchema } from '@keystar/ui/style';
-import { toDataAttributes } from '@keystar/ui/utils';
+import {
+  classNames,
+  css,
+  toDataAttributes,
+  tokenSchema,
+} from '@keystar/ui/style';
 import { HTMLAttributes } from 'react';
 
 type DirectionIndicatorProps = {
diff --git a/design-system/pkg/src/overlays/Modal.tsx b/design-system/pkg/src/overlays/Modal.tsx
index cac543d6..ff16897f 100644
--- a/design-system/pkg/src/overlays/Modal.tsx
+++ b/design-system/pkg/src/overlays/Modal.tsx
@@ -12,11 +12,11 @@ import {
 import {
   classNames,
   css,
+  toDataAttributes,
   tokenSchema,
   transition,
   useStyleProps,
 } from '@keystar/ui/style';
-import { toDataAttributes } from '@keystar/ui/utils';
 
 import { Blanket } from './Blanket';
 import { Overlay } from './Overlay';
diff --git a/design-system/pkg/src/overlays/Popover.tsx b/design-system/pkg/src/overlays/Popover.tsx
index edb64147..04fb9d05 100644
--- a/design-system/pkg/src/overlays/Popover.tsx
+++ b/design-system/pkg/src/overlays/Popover.tsx
@@ -12,11 +12,11 @@ import {
 import {
   classNames,
   css,
+  toDataAttributes,
   tokenSchema,
   transition,
   useStyleProps,
 } from '@keystar/ui/style';
-import { toDataAttributes } from '@keystar/ui/utils';
 
 import { Blanket } from './Blanket';
 import { Overlay } from './Overlay';
diff --git a/design-system/pkg/src/overlays/Tray.tsx b/design-system/pkg/src/overlays/Tray.tsx
index 51c7ef4a..555b2064 100644
--- a/design-system/pkg/src/overlays/Tray.tsx
+++ b/design-system/pkg/src/overlays/Tray.tsx
@@ -14,11 +14,11 @@ import {
   breakpoints,
   classNames,
   css,
+  toDataAttributes,
   tokenSchema,
   transition,
   useStyleProps,
 } from '@keystar/ui/style';
-import { toDataAttributes } from '@keystar/ui/utils';
 
 import { Blanket } from './Blanket';
 import { Overlay } from './Overlay';
diff --git a/design-system/pkg/src/progress/BarBase.tsx b/design-system/pkg/src/progress/BarBase.tsx
index 6228e1ab..3a3b6a8b 100644
--- a/design-system/pkg/src/progress/BarBase.tsx
+++ b/design-system/pkg/src/progress/BarBase.tsx
@@ -6,12 +6,12 @@ import {
   classNames,
   css,
   keyframes,
+  toDataAttributes,
   tokenSchema,
   transition,
   useStyleProps,
 } from '@keystar/ui/style';
 import { Text } from '@keystar/ui/typography';
-import { toDataAttributes } from '@keystar/ui/utils';
 
 import { ProgressBarProps } from './types';
 
diff --git a/design-system/pkg/src/progress/Meter.tsx b/design-system/pkg/src/progress/Meter.tsx
index bf88193b..efad7832 100644
--- a/design-system/pkg/src/progress/Meter.tsx
+++ b/design-system/pkg/src/progress/Meter.tsx
@@ -1,6 +1,5 @@
 import { useMeter } from '@react-aria/meter';
-import { css, tokenSchema } from '@keystar/ui/style';
-import { toDataAttributes } from '@keystar/ui/utils';
+import { css, toDataAttributes, tokenSchema } from '@keystar/ui/style';
 import { ForwardedRef, forwardRef } from 'react';
 
 import { BarBase } from './BarBase';
diff --git a/design-system/pkg/src/progress/ProgressCircle.tsx b/design-system/pkg/src/progress/ProgressCircle.tsx
index 95fb272a..f274baa9 100644
--- a/design-system/pkg/src/progress/ProgressCircle.tsx
+++ b/design-system/pkg/src/progress/ProgressCircle.tsx
@@ -7,11 +7,11 @@ import {
   classNames,
   css,
   keyframes,
+  toDataAttributes,
   tokenSchema,
   transition,
   useStyleProps,
 } from '@keystar/ui/style';
-import { toDataAttributes } from '@keystar/ui/utils';
 
 import { ProgressCircleProps } from './types';
 
diff --git a/design-system/pkg/src/radio/RadioGroup.tsx b/design-system/pkg/src/radio/RadioGroup.tsx
index 65943e6a..2672baf5 100644
--- a/design-system/pkg/src/radio/RadioGroup.tsx
+++ b/design-system/pkg/src/radio/RadioGroup.tsx
@@ -8,8 +8,12 @@ import React, {
 
 import { useProviderProps } from '@keystar/ui/core';
 import { FieldPrimitive, validateFieldProps } from '@keystar/ui/field';
-import { classNames, css, tokenSchema } from '@keystar/ui/style';
-import { toDataAttributes } from '@keystar/ui/utils';
+import {
+  classNames,
+  css,
+  toDataAttributes,
+  tokenSchema,
+} from '@keystar/ui/style';
 
 import { RadioContext } from './context';
 import { RadioGroupProps } from './types';
diff --git a/design-system/pkg/src/style/FocusRing.tsx b/design-system/pkg/src/style/FocusRing.tsx
index e949b20d..34876f07 100644
--- a/design-system/pkg/src/style/FocusRing.tsx
+++ b/design-system/pkg/src/style/FocusRing.tsx
@@ -2,7 +2,7 @@ import { FocusRingProps, useFocusRing } from '@react-aria/focus';
 import { mergeProps } from '@react-aria/utils';
 import { Children, cloneElement } from 'react';
 
-import { toDataAttributes } from '@keystar/ui/utils';
+import { toDataAttributes } from './toDataAttributes';
 
 /**
  * A utility component for styling interactions. Applies data-attributes to the
diff --git a/design-system/pkg/src/style/index.ts b/design-system/pkg/src/style/index.ts
index 0cb9f76d..9657e71b 100644
--- a/design-system/pkg/src/style/index.ts
+++ b/design-system/pkg/src/style/index.ts
@@ -19,6 +19,7 @@ export {
 } from './resolvers';
 export { breakpoints, breakpointQueries, containerQueries } from './responsive';
 export { tokenSchema } from './tokens';
+export { toDataAttributes } from './toDataAttributes';
 export { useIsMobileDevice } from './useIsMobileDevice';
 export { useMediaQuery } from './useMediaQuery';
 export { useStyleProps } from './useStyleProps';
diff --git a/design-system/pkg/src/style/toDataAttributes.ts b/design-system/pkg/src/style/toDataAttributes.ts
new file mode 100644
index 00000000..eeacb31b
--- /dev/null
+++ b/design-system/pkg/src/style/toDataAttributes.ts
@@ -0,0 +1,45 @@
+/** Converts an object to a set of data attributes. */
+export function toDataAttributes<
+  T extends {
+    [key: string]: string | number | boolean | null | undefined;
+  }
+>(
+  data: T,
+  options: {
+    /** Omit props with values of `false` or `""`. */
+    omitFalsyValues?: boolean;
+    /** Removes the `is*` prefix from keys.  */
+    trimBooleanKeys?: boolean;
+  } = {}
+) {
+  type Value = T[keyof T] | undefined;
+  let dataAttributes: Record<string, Value> = {};
+
+  for (let key in data) {
+    let prop: string = key;
+    let value: Value = data[key];
+
+    // always bail if the value is nullish; it'll never make it to the DOM node
+    // so there's no point doing any more work.
+    // a value of `0` isn't really falsy in this case so it's not included in
+    // the optional check.
+    if (
+      value == null ||
+      (options.omitFalsyValues && (value === false || value === ''))
+    ) {
+      continue;
+    }
+
+    // lowercase the first letter of the remaining key so it isn't affected by
+    // the kebab-case conversion later
+    if (options.trimBooleanKeys && key.startsWith('is')) {
+      prop = prop.charAt(2).toLowerCase() + prop.slice(3);
+    }
+
+    prop = prop.replace(/[A-Z]/g, char => `-${char.toLowerCase()}`);
+
+    dataAttributes[`data-${prop}`] = value;
+  }
+
+  return dataAttributes;
+}
diff --git a/design-system/pkg/src/switch/Switch.tsx b/design-system/pkg/src/switch/Switch.tsx
index 85339dac..d0f789d9 100644
--- a/design-system/pkg/src/switch/Switch.tsx
+++ b/design-system/pkg/src/switch/Switch.tsx
@@ -17,12 +17,13 @@ import {
   FocusRing,
   classNames,
   css,
+  toDataAttributes,
   tokenSchema,
   transition,
   useStyleProps,
 } from '@keystar/ui/style';
 import { Text } from '@keystar/ui/typography';
-import { isReactText, toDataAttributes } from '@keystar/ui/utils';
+import { isReactText } from '@keystar/ui/utils';
 
 export type SwitchProps = AriaSwitchProps &
   BaseStyleProps & {
diff --git a/design-system/pkg/src/table/styles.tsx b/design-system/pkg/src/table/styles.tsx
index 3d9ac817..aa9793ee 100644
--- a/design-system/pkg/src/table/styles.tsx
+++ b/design-system/pkg/src/table/styles.tsx
@@ -3,11 +3,11 @@ import { Icon } from '@keystar/ui/icon';
 import {
   classNames,
   css,
+  toDataAttributes,
   tokenSchema,
   transition,
   useStyleProps,
 } from '@keystar/ui/style';
-import { toDataAttributes } from '@keystar/ui/utils';
 
 import { TableProps } from './types';
 
@@ -75,13 +75,11 @@ export const SortIndicator = () => {
 // ----------------------------------------------------------------------------
 
 export function useTableStyleProps<T>(props: TableProps<T>) {
+  let { density, overflowMode, prominence } = props;
   let styleProps = useStyleProps(props);
 
   return {
-    ...toDataAttributes(
-      props,
-      new Set(['density', 'overflowMode', 'prominence'])
-    ),
+    ...toDataAttributes({ density, overflowMode, prominence }),
     className: classNames(
       styleProps.className,
       'ksv-table-view',
diff --git a/design-system/pkg/src/tabs/Tabs.tsx b/design-system/pkg/src/tabs/Tabs.tsx
index 4443dbfc..92ca20ce 100644
--- a/design-system/pkg/src/tabs/Tabs.tsx
+++ b/design-system/pkg/src/tabs/Tabs.tsx
@@ -32,12 +32,13 @@ import {
   FocusRing,
   classNames,
   css,
+  toDataAttributes,
   tokenSchema,
   transition,
   useStyleProps,
 } from '@keystar/ui/style';
 import { Text } from '@keystar/ui/typography';
-import { toDataAttributes, isReactText } from '@keystar/ui/utils';
+import { isReactText } from '@keystar/ui/utils';
 
 import {
   TabsProps,
diff --git a/design-system/pkg/src/text-field/TextFieldPrimitive.tsx b/design-system/pkg/src/text-field/TextFieldPrimitive.tsx
index 98fdbdef..56e97f3f 100644
--- a/design-system/pkg/src/text-field/TextFieldPrimitive.tsx
+++ b/design-system/pkg/src/text-field/TextFieldPrimitive.tsx
@@ -13,10 +13,10 @@ import {
   FocusRing,
   classNames,
   css,
+  toDataAttributes,
   tokenSchema,
   transition,
 } from '@keystar/ui/style';
-import { toDataAttributes } from '@keystar/ui/utils';
 
 import { TextFieldPrimitiveProps } from './types';
 
diff --git a/design-system/pkg/src/tooltip/Tooltip.tsx b/design-system/pkg/src/tooltip/Tooltip.tsx
index 14574faf..9d8dd99a 100644
--- a/design-system/pkg/src/tooltip/Tooltip.tsx
+++ b/design-system/pkg/src/tooltip/Tooltip.tsx
@@ -20,12 +20,13 @@ import { SlotProvider } from '@keystar/ui/slots';
 import {
   classNames,
   css,
+  toDataAttributes,
   tokenSchema,
   transition,
   useStyleProps,
 } from '@keystar/ui/style';
 import { Text } from '@keystar/ui/typography';
-import { isReactText, toDataAttributes } from '@keystar/ui/utils';
+import { isReactText } from '@keystar/ui/utils';
 
 import { TooltipContext } from './context';
 import { TooltipProps } from './types';
diff --git a/design-system/pkg/src/typography/Prose.tsx b/design-system/pkg/src/typography/Prose.tsx
index dd7e3c32..3de202a2 100644
--- a/design-system/pkg/src/typography/Prose.tsx
+++ b/design-system/pkg/src/typography/Prose.tsx
@@ -6,11 +6,11 @@ import {
   classNames,
   css,
   filterStyleProps,
+  toDataAttributes,
   tokenSchema,
   useStyleProps,
 } from '@keystar/ui/style';
 import { forwardRefWithAs } from '@keystar/ui/utils/ts';
-import { toDataAttributes } from '../utils';
 
 export type ProseProps = {
   /** The content to render. */
diff --git a/design-system/pkg/src/utils/index.ts b/design-system/pkg/src/utils/index.ts
index 69eaaae2..4aef50dc 100644
--- a/design-system/pkg/src/utils/index.ts
+++ b/design-system/pkg/src/utils/index.ts
@@ -1,7 +1,6 @@
 export { cloneValidElement, useHasChild } from './children';
 export { composeId } from './composeId';
 export { isReactText } from './guards';
-export { toDataAttributes } from './toDataAttributes';
 export { useId } from './useId';
 export { useIsMounted } from './useIsMounted';
 export { usePrevious } from './usePrevious';
diff --git a/design-system/pkg/src/utils/toDataAttributes.ts b/design-system/pkg/src/utils/toDataAttributes.ts
deleted file mode 100644
index d9e09e9c..00000000
--- a/design-system/pkg/src/utils/toDataAttributes.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-/**
- * Prefix an object's keys with "data-" for use as data attributes.
- */
-export function toDataAttributes<T>(data: T, pick?: Set<keyof T>) {
-  let dataAttributes: Record<string, T[keyof T]> = {};
-
-  for (let key in data) {
-    if (!pick || pick.has(key)) {
-      dataAttributes[
-        `data-${key.replace(/[A-Z]/g, char => `-${char.toLowerCase()}`)}`
-      ] = data[key];
-    }
-  }
-
-  return dataAttributes;
-}
diff --git a/packages/keystatic/src/form/fields/document/DocumentEditor/index.tsx b/packages/keystatic/src/form/fields/document/DocumentEditor/index.tsx
index 9ec93a1c..67c3b601 100644
--- a/packages/keystatic/src/form/fields/document/DocumentEditor/index.tsx
+++ b/packages/keystatic/src/form/fields/document/DocumentEditor/index.tsx
@@ -27,10 +27,10 @@ import {
   breakpointQueries,
   classNames,
   css,
+  toDataAttributes,
   tokenSchema,
 } from '@keystar/ui/style';
 import { Prose } from '@keystar/ui/typography';
-import { toDataAttributes } from '@keystar/ui/utils';
 
 import { useEntryLayoutSplitPaneContext } from '../../../../app/entry-form';
 import { useContentPanelSize } from '../../../../app/shell/context';
diff --git a/packages/keystatic/src/form/fields/document/DocumentEditor/table/table-ui.tsx b/packages/keystatic/src/form/fields/document/DocumentEditor/table/table-ui.tsx
index 50396893..2edf7191 100644
--- a/packages/keystatic/src/form/fields/document/DocumentEditor/table/table-ui.tsx
+++ b/packages/keystatic/src/form/fields/document/DocumentEditor/table/table-ui.tsx
@@ -15,10 +15,9 @@ import { tableIcon } from '@keystar/ui/icon/icons/tableIcon';
 import { trash2Icon } from '@keystar/ui/icon/icons/trash2Icon';
 import { Flex } from '@keystar/ui/layout';
 import { Item, Menu, MenuTrigger } from '@keystar/ui/menu';
-import { css, tokenSchema } from '@keystar/ui/style';
+import { css, toDataAttributes, tokenSchema } from '@keystar/ui/style';
 import { TooltipTrigger, Tooltip } from '@keystar/ui/tooltip';
 import { Text } from '@keystar/ui/typography';
-import { toDataAttributes } from '@keystar/ui/utils';
 
 import {
   BlockPopover,
@@ -443,12 +442,14 @@ function CellSelection(props: {
   const selectedCellsContext = useContext(SelectedCellsContext);
   const editor = useSlateStatic();
 
+  let { location, selected } = props;
+
   return (
     <div contentEditable={false}>
       <button
         tabIndex={-1}
         type="button"
-        {...toDataAttributes(props, new Set(['location', 'selected']))}
+        {...toDataAttributes({ location, selected })}
         className={css({
           background: tokenSchema.color.scale.slate3,
           border: `1px solid ${tokenSchema.color.alias.borderIdle}`,


Test patch:
diff --git a/design-system/pkg/src/style/test/toDataAttributes.test.tsx b/design-system/pkg/src/style/test/toDataAttributes.test.tsx
new file mode 100644
index 00000000..4e8c5588
--- /dev/null
+++ b/design-system/pkg/src/style/test/toDataAttributes.test.tsx
@@ -0,0 +1,70 @@
+import { toDataAttributes } from '@keystar/ui/style';
+
+describe('utils/toDataAttributes', () => {
+  it('should prepend the key with "data-"', () => {
+    expect(
+      toDataAttributes({
+        foo: 'bar',
+        bar: true,
+        baz: 123,
+        qux: undefined,
+        fred: false,
+        thud: null,
+      })
+    ).toStrictEqual({
+      'data-foo': 'bar',
+      'data-bar': true,
+      'data-baz': 123,
+      'data-fred': false,
+    });
+  });
+
+  describe('options', () => {
+    it('omitFalsyValues: should omit props with falsy values, except `0`', () => {
+      expect(
+        toDataAttributes(
+          {
+            foo: false,
+            bar: 123,
+            baz: '',
+            qux: 0,
+          },
+          { omitFalsyValues: true }
+        )
+      ).toStrictEqual({
+        'data-bar': 123,
+        'data-qux': 0,
+      });
+    });
+    it('trimBooleanKeys: should remove the `is*` prefix from keys', () => {
+      expect(
+        toDataAttributes(
+          { isFoo: true, isBar: false },
+          { trimBooleanKeys: true }
+        )
+      ).toStrictEqual({
+        'data-foo': true,
+        'data-bar': false,
+      });
+    });
+    it('should work with all options', () => {
+      expect(
+        toDataAttributes(
+          {
+            isFoo: true,
+            isBar: false,
+            baz: 123,
+            qux: '',
+          },
+          {
+            omitFalsyValues: true,
+            trimBooleanKeys: true,
+          }
+        )
+      ).toStrictEqual({
+        'data-foo': true,
+        'data-baz': 123,
+      });
+    });
+  });
+});