@drivenets/design-system 0.18.1 → 0.20.0

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.
Files changed (216) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/index.cjs +17038 -10114
  3. package/dist/index.d.cts +1355 -444
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.ts +1354 -443
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +17073 -10172
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.min.css +1 -1
  10. package/package.json +41 -41
  11. package/src/components/ds-bot-button/ds-bot-button.figma.ts +38 -0
  12. package/src/components/ds-bot-button/ds-bot-button.module.scss +41 -0
  13. package/src/components/ds-bot-button/ds-bot-button.tsx +48 -0
  14. package/src/components/ds-bot-button/ds-bot-button.types.ts +33 -0
  15. package/src/components/ds-bot-button/index.ts +2 -0
  16. package/src/components/ds-bot-button/net-gen-logo.tsx +46 -0
  17. package/src/components/ds-bulk-actions/components/action-button.tsx +42 -0
  18. package/src/components/ds-bulk-actions/components/menu-entries.tsx +76 -0
  19. package/src/components/ds-bulk-actions/components/overflow.tsx +75 -0
  20. package/src/components/ds-bulk-actions/ds-bulk-actions.figma.ts +28 -0
  21. package/src/components/ds-bulk-actions/ds-bulk-actions.module.scss +199 -0
  22. package/src/components/ds-bulk-actions/ds-bulk-actions.tsx +220 -0
  23. package/src/components/ds-bulk-actions/ds-bulk-actions.types.ts +170 -0
  24. package/src/components/ds-bulk-actions/ds-bulk-actions.unit.test.ts +195 -0
  25. package/src/components/ds-bulk-actions/ds-bulk-actions.utils.ts +91 -0
  26. package/src/components/ds-bulk-actions/ds-bulk-actions.utils.unit.test.ts +134 -0
  27. package/src/components/ds-bulk-actions/hooks/compute-bulk-actions-overflow.ts +85 -0
  28. package/src/components/ds-bulk-actions/hooks/use-bulk-actions-overflow.ts +153 -0
  29. package/src/components/ds-bulk-actions/index.ts +18 -0
  30. package/src/components/ds-button-v3/ds-button-v3.module.scss +2 -2
  31. package/src/components/ds-catalog-layout/ds-catalog-layout.figma.ts +1 -1
  32. package/src/components/ds-checkbox/ds-checkbox.figma.ts +3 -0
  33. package/src/components/ds-checkbox/ds-checkbox.module.scss +102 -20
  34. package/src/components/ds-checkbox/ds-checkbox.tsx +30 -2
  35. package/src/components/ds-checkbox/ds-checkbox.types.ts +10 -0
  36. package/src/components/ds-code-input/collapsed-code-field/collapsed-code-field.module.scss +169 -0
  37. package/src/components/ds-code-input/collapsed-code-field/collapsed-code-field.tsx +92 -0
  38. package/src/components/ds-code-input/collapsed-code-field/index.ts +1 -0
  39. package/src/components/ds-code-input/ds-code-input-panel/ds-code-input-panel.module.scss +110 -0
  40. package/src/components/ds-code-input/ds-code-input-panel/ds-code-input-panel.tsx +61 -0
  41. package/src/components/ds-code-input/ds-code-input-panel/index.ts +1 -0
  42. package/src/components/ds-code-input/ds-code-input.module.scss +25 -0
  43. package/src/components/ds-code-input/ds-code-input.tsx +401 -0
  44. package/src/components/ds-code-input/ds-code-input.types.ts +166 -0
  45. package/src/components/ds-code-input/ds-code-input.utils.ts +94 -0
  46. package/src/components/ds-code-input/ds-code-input.utils.unit.test.ts +166 -0
  47. package/src/components/ds-code-input/index.ts +2 -0
  48. package/src/components/ds-code-input/use-collapsed-viewport.ts +289 -0
  49. package/src/components/ds-code-input/use-editor-host.ts +87 -0
  50. package/src/components/ds-dropdown-menu/ds-dropdown-menu.tsx +2 -0
  51. package/src/components/ds-empty-state/ds-empty-state.module.scss +16 -0
  52. package/src/components/ds-empty-state/ds-empty-state.tsx +45 -0
  53. package/src/components/ds-empty-state/ds-empty-state.types.ts +38 -0
  54. package/src/components/ds-empty-state/index.ts +3 -0
  55. package/src/components/ds-file-upload/ds-file-upload.tsx +4 -1
  56. package/src/components/ds-filters-bar/components/ds-filters-bar-builder/ds-filters-bar-builder.ts +11 -0
  57. package/src/components/ds-filters-bar/components/ds-filters-bar-builder/ds-filters-bar-builder.types.ts +41 -0
  58. package/src/components/ds-filters-bar/components/ds-filters-bar-builder/index.ts +1 -0
  59. package/src/components/ds-filters-bar/components/ds-filters-bar-clear-all.ts +11 -0
  60. package/src/components/ds-filters-bar/components/ds-filters-bar-conditions/ds-filters-bar-conditions.ts +11 -0
  61. package/src/components/ds-filters-bar/components/ds-filters-bar-conditions/ds-filters-bar-conditions.types.ts +30 -0
  62. package/src/components/ds-filters-bar/components/ds-filters-bar-conditions/index.ts +1 -0
  63. package/src/components/ds-filters-bar/components/ds-filters-bar-pinned.ts +31 -0
  64. package/src/components/ds-filters-bar/components/ds-filters-bar-query/ds-filters-bar-query.ts +11 -0
  65. package/src/components/ds-filters-bar/components/ds-filters-bar-query/ds-filters-bar-query.types.ts +22 -0
  66. package/src/components/ds-filters-bar/components/ds-filters-bar-query/index.ts +1 -0
  67. package/src/components/ds-filters-bar/components/ds-filters-bar-saved-filters.ts +19 -0
  68. package/src/components/ds-filters-bar/components/ds-filters-bar-search.ts +11 -0
  69. package/src/components/ds-filters-bar/components/ds-filters-bar-summary.ts +11 -0
  70. package/src/components/ds-filters-bar/components/ds-filters-bar-toolbar.ts +11 -0
  71. package/src/components/ds-filters-bar/components/ds-filters-bar-view-switch.ts +11 -0
  72. package/src/components/ds-filters-bar/components/ds-filters-bar-view.ts +11 -0
  73. package/src/components/ds-filters-bar/components/index.ts +11 -0
  74. package/src/components/ds-filters-bar/ds-filters-bar.context.ts +69 -0
  75. package/src/components/ds-filters-bar/ds-filters-bar.tsx +120 -0
  76. package/src/components/ds-filters-bar/ds-filters-bar.types.ts +440 -0
  77. package/src/components/ds-filters-bar/ds-filters-bar.unit.test.ts +216 -0
  78. package/src/components/ds-filters-bar/ds-filters-bar.utils.ts +141 -0
  79. package/src/components/ds-filters-bar/index.ts +1 -0
  80. package/src/components/ds-form-control/ds-form-control.module.scss +18 -0
  81. package/src/components/ds-form-control/ds-form-control.tsx +61 -19
  82. package/src/components/ds-form-control/ds-form-control.types.ts +2 -1
  83. package/src/components/ds-icon/custom-icons/index.ts +2 -0
  84. package/src/components/ds-icon/custom-icons/special-adv-filters.tsx +28 -0
  85. package/src/components/ds-illustration/ds-illustration.figma.ts +34 -0
  86. package/src/components/ds-illustration/ds-illustration.module.scss +6 -0
  87. package/src/components/ds-illustration/ds-illustration.tsx +29 -0
  88. package/src/components/ds-illustration/ds-illustration.types.ts +45 -0
  89. package/src/components/ds-illustration/illustrations/configuration.tsx +51 -0
  90. package/src/components/ds-illustration/illustrations/device.tsx +133 -0
  91. package/src/components/ds-illustration/illustrations/done.tsx +107 -0
  92. package/src/components/{ds-catalog-layout/catalog-layout-empty-illustration.tsx → ds-illustration/illustrations/empty-inbox.tsx} +10 -14
  93. package/src/components/ds-illustration/illustrations/error.tsx +86 -0
  94. package/src/components/ds-illustration/illustrations/go-filter.tsx +51 -0
  95. package/src/components/ds-illustration/illustrations/illustration-clouds.tsx +10 -0
  96. package/src/components/ds-illustration/illustrations/index.ts +39 -0
  97. package/src/components/ds-illustration/illustrations/no-connection.tsx +54 -0
  98. package/src/components/ds-illustration/illustrations/no-credit-card.tsx +48 -0
  99. package/src/components/ds-illustration/illustrations/no-device.tsx +61 -0
  100. package/src/components/ds-illustration/illustrations/no-documents.tsx +52 -0
  101. package/src/components/ds-illustration/illustrations/no-gps.tsx +57 -0
  102. package/src/components/ds-illustration/illustrations/no-images.tsx +82 -0
  103. package/src/components/ds-illustration/illustrations/no-items-cart.tsx +86 -0
  104. package/src/components/ds-illustration/illustrations/no-messages.tsx +51 -0
  105. package/src/components/ds-illustration/illustrations/no-search-result.tsx +76 -0
  106. package/src/components/ds-illustration/illustrations/no-tasks.tsx +61 -0
  107. package/src/components/ds-illustration/illustrations/site.tsx +80 -0
  108. package/src/components/ds-illustration/index.ts +3 -0
  109. package/src/components/ds-main-menu/components/expanded-item/expanded-item.tsx +108 -0
  110. package/src/components/ds-main-menu/components/expanded-item/expanded-item.types.ts +8 -0
  111. package/src/components/ds-main-menu/components/expanded-item/index.ts +1 -0
  112. package/src/components/ds-main-menu/components/menu-graphic/index.ts +1 -0
  113. package/src/components/ds-main-menu/components/menu-graphic/menu-graphic.tsx +16 -0
  114. package/src/components/ds-main-menu/components/menu-graphic/menu-graphic.types.ts +7 -0
  115. package/src/components/ds-main-menu/components/tile-link/index.ts +1 -0
  116. package/src/components/ds-main-menu/components/tile-link/tile-link.tsx +85 -0
  117. package/src/components/ds-main-menu/components/tile-link/tile-link.types.ts +8 -0
  118. package/src/components/ds-main-menu/components/utility-link/index.ts +1 -0
  119. package/src/components/ds-main-menu/components/utility-link/utility-link.tsx +37 -0
  120. package/src/components/ds-main-menu/components/utility-link/utility-link.types.ts +7 -0
  121. package/src/components/ds-main-menu/ds-main-menu.figma.ts +17 -0
  122. package/src/components/ds-main-menu/ds-main-menu.module.scss +349 -0
  123. package/src/components/ds-main-menu/ds-main-menu.tsx +102 -0
  124. package/src/components/ds-main-menu/ds-main-menu.types.ts +86 -0
  125. package/src/components/ds-main-menu/ds-main-menu.utils.ts +40 -0
  126. package/src/components/ds-main-menu/index.ts +9 -0
  127. package/src/components/ds-modal/figma/ds-modal-footer.figma.ts +1 -1
  128. package/src/components/ds-pin-toggle/ds-pin-toggle.figma.ts +27 -0
  129. package/src/components/ds-pin-toggle/ds-pin-toggle.module.scss +41 -0
  130. package/src/components/ds-pin-toggle/ds-pin-toggle.tsx +43 -0
  131. package/src/components/ds-pin-toggle/ds-pin-toggle.types.ts +29 -0
  132. package/src/components/ds-pin-toggle/index.ts +2 -0
  133. package/src/components/ds-popover/components/popover-trigger/index.ts +1 -0
  134. package/src/components/ds-popover/components/popover-trigger/popover-trigger.tsx +15 -0
  135. package/src/components/ds-popover/ds-popover.context.ts +18 -0
  136. package/src/components/ds-popover/ds-popover.hover-intent.ts +65 -0
  137. package/src/components/ds-popover/ds-popover.tsx +164 -41
  138. package/src/components/ds-popover/ds-popover.types.ts +65 -0
  139. package/src/components/ds-popover/ds-popover.utils.ts +60 -0
  140. package/src/components/ds-popover/ds-popover.utils.unit.test.ts +25 -0
  141. package/src/components/ds-saved-filters/components/ds-saved-filters-delete-confirm.tsx +53 -0
  142. package/src/components/ds-saved-filters/components/ds-saved-filters-name-entry.tsx +64 -0
  143. package/src/components/ds-saved-filters/components/ds-saved-filters-name-modal.tsx +50 -0
  144. package/src/components/ds-saved-filters/components/ds-saved-filters-row/ds-saved-filters-row.module.scss +68 -0
  145. package/src/components/ds-saved-filters/components/ds-saved-filters-row/ds-saved-filters-row.tsx +83 -0
  146. package/src/components/ds-saved-filters/components/ds-saved-filters-row/index.ts +1 -0
  147. package/src/components/ds-saved-filters/components/ds-saved-filters-save.tsx +164 -0
  148. package/src/components/ds-saved-filters/components/ds-saved-filters-trigger/ds-saved-filters-trigger.module.scss +21 -0
  149. package/src/components/ds-saved-filters/components/ds-saved-filters-trigger/ds-saved-filters-trigger.tsx +237 -0
  150. package/src/components/ds-saved-filters/components/ds-saved-filters-trigger/index.ts +1 -0
  151. package/src/components/ds-saved-filters/components/index.ts +2 -0
  152. package/src/components/ds-saved-filters/ds-saved-filters.ts +6 -0
  153. package/src/components/ds-saved-filters/ds-saved-filters.types.ts +106 -0
  154. package/src/components/ds-saved-filters/ds-saved-filters.utils.ts +35 -0
  155. package/src/components/ds-saved-filters/hooks/index.ts +1 -0
  156. package/src/components/ds-saved-filters/hooks/use-picker-outside-dismiss.ts +42 -0
  157. package/src/components/ds-saved-filters/index.ts +2 -0
  158. package/src/components/ds-segment-group/ds-segment-group.module.scss +65 -37
  159. package/src/components/ds-segment-group/ds-segment-group.tsx +17 -5
  160. package/src/components/ds-segment-group/ds-segment-group.types.ts +20 -5
  161. package/src/components/ds-segment-group/figma/ds-segment-group-v2-item.figma.ts +109 -0
  162. package/src/components/ds-segment-group/figma/ds-segment-group-v2.figma.ts +31 -0
  163. package/src/components/ds-site-node/ds-site-node.tsx +2 -2
  164. package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.module.scss +12 -2
  165. package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.tsx +2 -3
  166. package/src/components/ds-table/components/ds-table-group-header-cell/ds-table-group-header-cell.module.scss +1 -6
  167. package/src/components/ds-table/components/ds-table-group-header-cell/ds-table-group-header-cell.tsx +6 -3
  168. package/src/components/ds-table/components/ds-table-header/ds-table-header.module.scss +18 -7
  169. package/src/components/ds-table/components/ds-table-header/ds-table-header.tsx +27 -21
  170. package/src/components/ds-table/components/ds-table-header-label.module.scss +12 -0
  171. package/src/components/ds-table/components/ds-table-header-label.tsx +28 -0
  172. package/src/components/ds-table/components/ds-table-row/ds-table-row.module.scss +0 -1
  173. package/src/components/ds-table/context/ds-table-context.tsx +8 -0
  174. package/src/components/ds-table/ds-table.module.scss +26 -16
  175. package/src/components/ds-table/ds-table.tsx +46 -14
  176. package/src/components/ds-table/ds-table.types.ts +33 -3
  177. package/src/components/ds-table/hooks/use-column-resize.ts +114 -11
  178. package/src/components/ds-table/hooks/use-scrollbar-spacer.ts +108 -0
  179. package/src/components/ds-table/hooks/use-scrollbar-spacer.unit.test.ts +26 -0
  180. package/src/components/ds-table/styles/_variables.scss +0 -2
  181. package/src/components/ds-table/utils/column-size.ts +274 -1
  182. package/src/components/ds-table/utils/column-size.unit.test.ts +157 -0
  183. package/src/components/ds-table/utils/constants.ts +13 -5
  184. package/src/components/ds-table/utils/table-columns.tsx +27 -13
  185. package/src/components/ds-table/utils/table-columns.unit.test.ts +44 -0
  186. package/src/components/ds-tag/ds-tag.figma.ts +23 -3
  187. package/src/components/ds-tag/ds-tag.module.scss +71 -7
  188. package/src/components/ds-tag/ds-tag.tsx +115 -14
  189. package/src/components/ds-tag/ds-tag.types.ts +70 -3
  190. package/src/components/ds-tag/ds-tag.unit.test.ts +23 -4
  191. package/src/components/ds-tag/index.ts +2 -2
  192. package/src/components/ds-text-input/ds-input-field-v02.figma.ts +2 -0
  193. package/src/components/ds-toggle/ds-toggle.module.scss +18 -0
  194. package/src/components/ds-toggle/ds-toggle.tsx +15 -9
  195. package/src/components/ds-toggle/ds-toggle.types.ts +6 -0
  196. package/src/components/ds-toggle-filter-data/ds-toggle-filter-data.module.scss +69 -0
  197. package/src/components/ds-toggle-filter-data/ds-toggle-filter-data.tsx +55 -0
  198. package/src/components/ds-toggle-filter-data/ds-toggle-filter-data.types.ts +43 -0
  199. package/src/components/ds-toggle-filter-data/index.ts +1 -0
  200. package/src/components/ds-tooltip/ds-tooltip.module.scss +8 -0
  201. package/src/components/ds-tooltip/ds-tooltip.tsx +7 -3
  202. package/src/components/ds-tooltip/ds-tooltip.types.ts +17 -0
  203. package/src/components/ds-top-bar-navigation/ds-top-bar-navigation.figma.ts +35 -0
  204. package/src/components/ds-top-bar-navigation/ds-top-bar-navigation.module.scss +150 -0
  205. package/src/components/ds-top-bar-navigation/ds-top-bar-navigation.tsx +161 -0
  206. package/src/components/ds-top-bar-navigation/ds-top-bar-navigation.types.ts +117 -0
  207. package/src/components/ds-top-bar-navigation/index.ts +2 -0
  208. package/src/components/ds-tree/ds-tree.tsx +10 -10
  209. package/src/components/ds-tree/ds-tree.types.ts +40 -7
  210. package/src/index.ts +8 -0
  211. package/src/utils/use-is-horizontally-overflowing.ts +35 -0
  212. package/src/components/ds-site-node/hooks/use-is-overflowing.ts +0 -31
  213. package/src/components/ds-table/components/ds-table-bulk-actions/ds-table-bulk-actions.module.scss +0 -112
  214. package/src/components/ds-table/components/ds-table-bulk-actions/ds-table-bulk-actions.tsx +0 -67
  215. package/src/components/ds-table/components/ds-table-bulk-actions/ds-table-bulk-actions.types.ts +0 -41
  216. package/src/components/ds-table/components/ds-table-bulk-actions/index.ts +0 -2
@@ -0,0 +1,40 @@
1
+ import type { KeyboardEvent, MouseEvent } from 'react';
2
+ import type { DsMainMenuItem, DsMainMenuProps } from './ds-main-menu.types';
3
+
4
+ export const isNonActivatable = (state: DsMainMenuItem['state']) =>
5
+ state === 'disabled' || state === 'comingSoon';
6
+
7
+ export const handleActivatableClick = (
8
+ event: MouseEvent<HTMLAnchorElement | HTMLButtonElement>,
9
+ id: string,
10
+ href: string | undefined,
11
+ onItemSelect: DsMainMenuProps['onItemSelect'],
12
+ onClose: () => void,
13
+ ) => {
14
+ if (!href) {
15
+ event.preventDefault();
16
+ }
17
+
18
+ onItemSelect?.(id);
19
+ onClose();
20
+ };
21
+
22
+ export const handleActivatableKeyDown = (
23
+ event: KeyboardEvent<HTMLAnchorElement | HTMLButtonElement>,
24
+ id: string,
25
+ href: string | undefined,
26
+ onItemSelect: DsMainMenuProps['onItemSelect'],
27
+ onClose: () => void,
28
+ ) => {
29
+ if (event.key !== 'Enter' && event.key !== ' ') {
30
+ return;
31
+ }
32
+
33
+ event.preventDefault();
34
+ onItemSelect?.(id);
35
+ onClose();
36
+
37
+ if (href) {
38
+ (event.currentTarget as HTMLAnchorElement).click();
39
+ }
40
+ };
@@ -0,0 +1,9 @@
1
+ export { default as DsMainMenu } from './ds-main-menu';
2
+ export type {
3
+ DsMainMenuItem,
4
+ DsMainMenuItemState,
5
+ DsMainMenuProps,
6
+ DsMainMenuUtilityLink,
7
+ DsMainMenuVariant,
8
+ } from './ds-main-menu.types';
9
+ export { COMING_SOON_TOOLTIP, mainMenuItemStates, mainMenuVariants } from './ds-main-menu.types';
@@ -1,4 +1,4 @@
1
- // url=https://www.figma.com/design/nha3m67y7S57cHCSuQO2gp/DAP-Design-System-1.2?node-id=32014-16402
1
+ // url=https://www.figma.com/design/nha3m67y7S57cHCSuQO2gp/DAP-Design-System-1.2?node-id=43101-8753
2
2
  // source=https://github.com/drivenets/design-system/tree/main/packages/design-system/src/components/ds-modal
3
3
  // component=DsModal.Footer
4
4
  //
@@ -0,0 +1,27 @@
1
+ // url=https://www.figma.com/design/nha3m67y7S57cHCSuQO2gp/DAP-Design-System-1.2?node-id=42863-16495
2
+ // source=https://github.com/drivenets/design-system/tree/main/packages/design-system/src/components/ds-pin-toggle
3
+ // component=DsPinToggle
4
+ import figma from 'figma';
5
+
6
+ const instance = figma.selectedInstance;
7
+
8
+ // `Part_PinnedIcon` is a `Part_*` node, which we normally do not mirror. It is
9
+ // mapped here because, unlike `Part_CheckboxBtnV3`, it has a real exported
10
+ // counterpart in code — the rule exists to stop us inventing components for
11
+ // markup, not to hide ones that exist.
12
+ const active = instance.getEnum('active', {
13
+ true: 'true',
14
+ false: 'false',
15
+ });
16
+
17
+ // `itemLabel` has no Figma source — the pin carries no text, and the name it
18
+ // needs comes from the row it sits in. The snippet emits a placeholder so the
19
+ // required prop is visible rather than silently missing.
20
+ const attrs = ['itemLabel="Label"', active === 'true' ? 'pinned' : ''].filter(Boolean).join(' ');
21
+
22
+ export default {
23
+ example: figma.code`<DsPinToggle ${attrs} />`,
24
+ imports: ["import { DsPinToggle } from '@drivenets/design-system';"],
25
+ id: 'ds-pin-toggle',
26
+ metadata: { nestable: true },
27
+ } satisfies figma.Template;
@@ -0,0 +1,41 @@
1
+ $pin-border-radius: 2px;
2
+ $pin-focus-ring-width: 2px;
3
+ $transition-duration: 0.2s;
4
+
5
+ .root {
6
+ display: inline-flex;
7
+ align-items: center;
8
+ justify-content: center;
9
+ flex-shrink: 0;
10
+ margin: 0;
11
+ padding: var(--4xs);
12
+ border: none;
13
+ border-radius: $pin-border-radius;
14
+ background-color: transparent;
15
+ line-height: 0;
16
+ color: var(--icon-tertiary);
17
+ cursor: pointer;
18
+ transition: color $transition-duration ease-in-out;
19
+
20
+ &:not(:disabled):hover {
21
+ color: var(--icon-secondary);
22
+ }
23
+
24
+ &[aria-pressed='true'] {
25
+ color: var(--icon-action);
26
+
27
+ &:not(:disabled):hover {
28
+ color: var(--icon-action-hover);
29
+ }
30
+ }
31
+
32
+ &:focus-visible {
33
+ outline: $pin-focus-ring-width solid var(--border-action-primary);
34
+ outline-offset: 0;
35
+ }
36
+
37
+ &:disabled {
38
+ color: var(--icon-disabled);
39
+ cursor: not-allowed;
40
+ }
41
+ }
@@ -0,0 +1,43 @@
1
+ import classNames from 'classnames';
2
+ import styles from './ds-pin-toggle.module.scss';
3
+ import type { DsPinToggleProps } from './ds-pin-toggle.types';
4
+ import { DsIcon } from '../ds-icon';
5
+
6
+ const DEFAULT_LOCALE = Object.freeze({ pin: 'Pin' });
7
+
8
+ /**
9
+ * Bare pushpin for "keep this at the top" affordances.
10
+ */
11
+ const DsPinToggle = ({
12
+ ref,
13
+ className,
14
+ style,
15
+ itemLabel,
16
+ pinned = false,
17
+ disabled = false,
18
+ locale,
19
+ onPinnedChange,
20
+ onClick,
21
+ ...rest
22
+ }: DsPinToggleProps) => (
23
+ <button
24
+ {...rest}
25
+ ref={ref}
26
+ type="button"
27
+ disabled={disabled}
28
+ aria-pressed={pinned}
29
+ aria-label={`${locale?.pin ?? DEFAULT_LOCALE.pin} ${itemLabel}`}
30
+ className={classNames(styles.root, className)}
31
+ style={style}
32
+ onClick={(event) => {
33
+ onClick?.(event);
34
+ onPinnedChange?.(!pinned);
35
+ }}
36
+ >
37
+ <DsIcon icon="keep" size="tiny" variant="rounded" filled aria-hidden />
38
+ </button>
39
+ );
40
+
41
+ DsPinToggle.displayName = 'DsPinToggle';
42
+
43
+ export default DsPinToggle;
@@ -0,0 +1,29 @@
1
+ import type { ButtonHTMLAttributes, Ref } from 'react';
2
+
3
+ export interface DsPinToggleProps extends Omit<
4
+ ButtonHTMLAttributes<HTMLButtonElement>,
5
+ 'aria-label' | 'aria-labelledby' | 'aria-pressed' | 'children' | 'type'
6
+ > {
7
+ ref?: Ref<HTMLButtonElement>;
8
+
9
+ itemLabel: string;
10
+
11
+ /**
12
+ * @default false
13
+ */
14
+ pinned?: boolean;
15
+
16
+ /**
17
+ * @default false
18
+ */
19
+ disabled?: boolean;
20
+
21
+ locale?: {
22
+ /**
23
+ * @default 'Pin'
24
+ */
25
+ pin?: string;
26
+ };
27
+
28
+ onPinnedChange?: (pinned: boolean) => void;
29
+ }
@@ -0,0 +1,2 @@
1
+ export { default as DsPinToggle } from './ds-pin-toggle';
2
+ export * from './ds-pin-toggle.types';
@@ -0,0 +1 @@
1
+ export { PopoverTrigger } from './popover-trigger';
@@ -0,0 +1,15 @@
1
+ import { Popover } from '@ark-ui/react/popover';
2
+ import { useHoverTriggerProps } from '../../ds-popover.hover-intent';
3
+ import type { DsPopoverTriggerProps } from '../../ds-popover.types';
4
+
5
+ export const PopoverTrigger = ({ children, className }: DsPopoverTriggerProps) => {
6
+ const hoverProps = useHoverTriggerProps();
7
+
8
+ return (
9
+ <Popover.Trigger asChild className={className} {...hoverProps}>
10
+ {children}
11
+ </Popover.Trigger>
12
+ );
13
+ };
14
+
15
+ PopoverTrigger.displayName = 'DsPopover.Trigger';
@@ -0,0 +1,18 @@
1
+ import { createContext, useContext } from 'react';
2
+ import type { HoverIntent } from './ds-popover.hover-intent';
3
+
4
+ export interface DsPopoverContextValue {
5
+ matchAnchorWidth: boolean;
6
+ registerAnchor: (el: HTMLElement | null) => void;
7
+ registerContentId: (id: string | undefined) => void;
8
+ hoverIntent: HoverIntent | null;
9
+ }
10
+
11
+ export const DsPopoverContext = createContext<DsPopoverContextValue>({
12
+ matchAnchorWidth: false,
13
+ registerAnchor: () => undefined,
14
+ registerContentId: () => undefined,
15
+ hoverIntent: null,
16
+ });
17
+
18
+ export const useDsPopoverContext = () => useContext(DsPopoverContext);
@@ -0,0 +1,65 @@
1
+ import type { FocusEvent, PointerEvent } from 'react';
2
+ import { usePopoverContext } from '@ark-ui/react/popover';
3
+ import { useDsPopoverContext } from './ds-popover.context';
4
+ import { isHoverPointer } from './ds-popover.utils';
5
+
6
+ export const DEFAULT_OPEN_DELAY_MS = 200;
7
+ export const DEFAULT_CLOSE_DELAY_MS = 150;
8
+
9
+ export interface HoverIntent {
10
+ openDelay: number;
11
+ closeDelay: number;
12
+ setFocusInPanel: (inPanel: boolean) => void;
13
+ /** True once if the trigger is about to be re-focused by Ark's close-time restore. */
14
+ consumeFocusRestore: () => boolean;
15
+ /** Owns one shared timer */
16
+ schedule: (action: () => void, delay: number) => void;
17
+ }
18
+
19
+ const useHoverIntent = () => useDsPopoverContext().hoverIntent;
20
+
21
+ export const useHoverIntentProps = () => {
22
+ const intent = useHoverIntent();
23
+ const { setOpen } = usePopoverContext();
24
+
25
+ if (!intent) {
26
+ return undefined;
27
+ }
28
+
29
+ const onPointerIntent = (event: PointerEvent, open: boolean, delay: number) => {
30
+ if (!isHoverPointer(event.pointerType)) {
31
+ return;
32
+ }
33
+
34
+ intent.schedule(() => setOpen(open), delay);
35
+ };
36
+
37
+ return {
38
+ onPointerEnter: (event: PointerEvent) => onPointerIntent(event, true, intent.openDelay),
39
+ onPointerLeave: (event: PointerEvent) => onPointerIntent(event, false, intent.closeDelay),
40
+ };
41
+ };
42
+
43
+ export const useHoverTriggerProps = () => {
44
+ const pointerProps = useHoverIntentProps();
45
+ const intent = useHoverIntent();
46
+ const { setOpen } = usePopoverContext();
47
+
48
+ if (!intent) {
49
+ return undefined;
50
+ }
51
+
52
+ return {
53
+ ...pointerProps,
54
+ onFocus: (event: FocusEvent<HTMLElement>) => {
55
+ // Closing with focus inside the panel makes Ark re-focus the trigger
56
+ if (intent.consumeFocusRestore()) {
57
+ return;
58
+ }
59
+
60
+ if (event.target.matches(':focus-visible')) {
61
+ setOpen(true);
62
+ }
63
+ },
64
+ };
65
+ };
@@ -1,11 +1,20 @@
1
- import { Popover } from '@ark-ui/react/popover';
1
+ import { useEffect, useLayoutEffect, useRef, useState, type FocusEvent } from 'react';
2
+ import { Popover, type PopoverRootProps } from '@ark-ui/react/popover';
2
3
  import { Portal } from '@ark-ui/react/portal';
3
4
  import classNames from 'classnames';
4
5
  import { DsStack } from '../ds-stack';
5
6
  import { DsTypography } from '../ds-typography';
7
+ import { PopoverTrigger } from './components/popover-trigger';
8
+ import { DsPopoverContext, useDsPopoverContext } from './ds-popover.context';
9
+ import {
10
+ DEFAULT_CLOSE_DELAY_MS,
11
+ DEFAULT_OPEN_DELAY_MS,
12
+ useHoverIntentProps,
13
+ } from './ds-popover.hover-intent';
14
+ import { invokeCloseAutoFocus, toFocusEl, toPlacement } from './ds-popover.utils';
6
15
  import styles from './ds-popover.module.scss';
7
16
  import type {
8
- DsPopoverAlign,
17
+ DsPopoverAnchorProps,
9
18
  DsPopoverContentItemProps,
10
19
  DsPopoverContentProps,
11
20
  DsPopoverFooterProps,
@@ -13,14 +22,10 @@ import type {
13
22
  DsPopoverPanelProps,
14
23
  DsPopoverProps,
15
24
  DsPopoverRootProps,
16
- DsPopoverSide,
17
- DsPopoverTriggerProps,
18
25
  } from './ds-popover.types';
19
26
 
20
27
  const DEFAULT_PANEL_WIDTH = 400;
21
-
22
- export const toPlacement = (side: DsPopoverSide, align: DsPopoverAlign) =>
23
- align === 'center' ? side : (`${side}-${align}` as const);
28
+ const MATCHED_PANEL_WIDTH = 'var(--reference-width)';
24
29
 
25
30
  const DsPopoverRoot = ({
26
31
  open,
@@ -29,47 +34,164 @@ const DsPopoverRoot = ({
29
34
  align = 'center',
30
35
  gutter = 8,
31
36
  modal = false,
37
+ matchAnchorWidth = false,
38
+ restoreFocus,
39
+ openOn = 'click',
40
+ openDelay = DEFAULT_OPEN_DELAY_MS,
41
+ closeDelay = DEFAULT_CLOSE_DELAY_MS,
42
+ getAnchorElement,
32
43
  children,
44
+ onOpenAutoFocus,
45
+ onCloseAutoFocus,
46
+ onInteractOutside,
33
47
  onOpenChange,
34
- }: DsPopoverRootProps) => (
35
- <Popover.Root
36
- open={open}
37
- defaultOpen={defaultOpen}
38
- modal={modal}
39
- positioning={{ placement: toPlacement(side, align), gutter }}
40
- onOpenChange={(details) => onOpenChange?.(details.open)}
41
- >
42
- {children}
43
- </Popover.Root>
44
- );
48
+ }: DsPopoverRootProps) => {
49
+ const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
50
+ const anchorRef = useRef<HTMLElement | null>(null);
51
+ const restoringFocus = useRef(false);
52
+ const [contentId, setContentId] = useState<string>();
53
+ const [focusInPanel, setFocusInPanel] = useState(false);
45
54
 
46
- const DsPopoverTrigger = ({ children, className }: DsPopoverTriggerProps) => (
47
- <Popover.Trigger asChild className={className}>
48
- {children}
49
- </Popover.Trigger>
50
- );
55
+ useEffect(() => () => clearTimeout(timer.current), []);
56
+
57
+ const schedule = (action: () => void, delay: number) => {
58
+ clearTimeout(timer.current);
59
+ timer.current = setTimeout(action, delay);
60
+ };
61
+
62
+ const isHover = openOn === 'hover';
63
+
64
+ const consumeFocusRestore = () => {
65
+ const restoring = restoringFocus.current;
66
+ restoringFocus.current = false;
67
+
68
+ return restoring;
69
+ };
70
+
71
+ const registerAnchor = (el: HTMLElement | null) => {
72
+ anchorRef.current = el;
73
+ };
74
+
75
+ const ownsCloseFocus = restoreFocus === false && Boolean(onCloseAutoFocus);
76
+ const initialFocusEl = toFocusEl(onOpenAutoFocus, 'openAutoFocus') as PopoverRootProps['initialFocusEl'];
77
+ const finalFocusEl = ownsCloseFocus
78
+ ? undefined
79
+ : (toFocusEl(onCloseAutoFocus, 'closeAutoFocus') as PopoverRootProps['finalFocusEl']);
80
+ const arkRestoreFocus = ownsCloseFocus ? false : (restoreFocus ?? (!isHover || focusInPanel));
81
+
82
+ return (
83
+ <DsPopoverContext.Provider
84
+ value={{
85
+ matchAnchorWidth,
86
+ registerAnchor,
87
+ registerContentId: setContentId,
88
+ hoverIntent: isHover
89
+ ? { openDelay, closeDelay, schedule, setFocusInPanel, consumeFocusRestore }
90
+ : null,
91
+ }}
92
+ >
93
+ <Popover.Root
94
+ open={open}
95
+ defaultOpen={defaultOpen}
96
+ modal={modal}
97
+ ids={contentId ? { content: contentId } : undefined}
98
+ // Trigger focus is lost when open on hover
99
+ // eslint-disable-next-line jsx-a11y/no-autofocus
100
+ autoFocus={!isHover}
101
+ restoreFocus={arkRestoreFocus}
102
+ positioning={{
103
+ placement: toPlacement(side, align),
104
+ gutter,
105
+ sameWidth: matchAnchorWidth,
106
+ ...(getAnchorElement ? { getAnchorElement } : {}),
107
+ }}
108
+ initialFocusEl={initialFocusEl}
109
+ finalFocusEl={finalFocusEl}
110
+ persistentElements={[() => anchorRef.current]}
111
+ onInteractOutside={
112
+ onInteractOutside
113
+ ? (event) => {
114
+ const dsEvent = new Event('interactOutside', { cancelable: true });
115
+ onInteractOutside(dsEvent);
116
+
117
+ if (dsEvent.defaultPrevented) {
118
+ event.preventDefault();
119
+ }
120
+ }
121
+ : undefined
122
+ }
123
+ onOpenChange={(details) => {
124
+ if (!details.open && focusInPanel) {
125
+ restoringFocus.current = true;
126
+ }
127
+
128
+ if (!details.open && ownsCloseFocus && onCloseAutoFocus) {
129
+ invokeCloseAutoFocus(onCloseAutoFocus);
130
+ }
131
+
132
+ onOpenChange?.(details.open);
133
+ }}
134
+ >
135
+ {children}
136
+ </Popover.Root>
137
+ </DsPopoverContext.Provider>
138
+ );
139
+ };
140
+
141
+ const DsPopoverAnchor = ({ children, className }: DsPopoverAnchorProps) => {
142
+ const { registerAnchor } = useDsPopoverContext();
143
+
144
+ return (
145
+ <Popover.Anchor asChild className={className} ref={registerAnchor}>
146
+ {children}
147
+ </Popover.Anchor>
148
+ );
149
+ };
51
150
 
52
151
  const DsPopoverPanel = ({
53
- width = DEFAULT_PANEL_WIDTH,
152
+ width,
54
153
  className,
55
154
  style,
56
155
  children,
57
156
  ref,
157
+ id,
58
158
  'aria-label': ariaLabel,
59
- }: DsPopoverPanelProps) => (
60
- <Portal>
61
- <Popover.Positioner>
62
- <Popover.Content
63
- ref={ref}
64
- aria-label={ariaLabel}
65
- className={classNames(styles.panel, className)}
66
- style={{ ...style, width }}
67
- >
68
- {children}
69
- </Popover.Content>
70
- </Popover.Positioner>
71
- </Portal>
72
- );
159
+ }: DsPopoverPanelProps) => {
160
+ const { matchAnchorWidth, registerContentId, hoverIntent } = useDsPopoverContext();
161
+ const hoverProps = useHoverIntentProps();
162
+ const resolvedWidth = width ?? (matchAnchorWidth ? MATCHED_PANEL_WIDTH : DEFAULT_PANEL_WIDTH);
163
+
164
+ useLayoutEffect(() => {
165
+ registerContentId(id);
166
+
167
+ return () => registerContentId(undefined);
168
+ }, [id, registerContentId]);
169
+
170
+ const onFocus = () => hoverIntent?.setFocusInPanel(true);
171
+ const onBlur = (event: FocusEvent<HTMLDivElement>) => {
172
+ if (!event.currentTarget.contains(event.relatedTarget)) {
173
+ hoverIntent?.setFocusInPanel(false);
174
+ }
175
+ };
176
+
177
+ return (
178
+ <Portal>
179
+ <Popover.Positioner>
180
+ <Popover.Content
181
+ {...hoverProps}
182
+ onFocus={onFocus}
183
+ onBlur={onBlur}
184
+ ref={ref}
185
+ aria-label={ariaLabel}
186
+ className={classNames(styles.panel, className)}
187
+ style={{ ...style, width: resolvedWidth }}
188
+ >
189
+ {children}
190
+ </Popover.Content>
191
+ </Popover.Positioner>
192
+ </Portal>
193
+ );
194
+ };
73
195
 
74
196
  const DsPopoverHeader = ({ icon, className, style, children }: DsPopoverHeaderProps) => (
75
197
  <div className={classNames(styles.header, className)} style={style}>
@@ -141,14 +263,14 @@ const DsPopoverLegacy = ({
141
263
  align = 'center',
142
264
  }: DsPopoverProps) => (
143
265
  <DsPopoverRoot side={side} align={align}>
144
- <DsPopoverTrigger>{trigger}</DsPopoverTrigger>
266
+ <PopoverTrigger>{trigger}</PopoverTrigger>
145
267
  <DsPopoverPanel className={className}>{children}</DsPopoverPanel>
146
268
  </DsPopoverRoot>
147
269
  );
148
270
 
149
271
  DsPopoverLegacy.displayName = 'DsPopover';
150
272
  DsPopoverRoot.displayName = 'DsPopover.Root';
151
- DsPopoverTrigger.displayName = 'DsPopover.Trigger';
273
+ DsPopoverAnchor.displayName = 'DsPopover.Anchor';
152
274
  DsPopoverPanel.displayName = 'DsPopover.Panel';
153
275
  DsPopoverHeader.displayName = 'DsPopover.Header';
154
276
  DsPopoverContent.displayName = 'DsPopover.Content';
@@ -157,7 +279,8 @@ DsPopoverFooter.displayName = 'DsPopover.Footer';
157
279
 
158
280
  export const DsPopover = Object.assign(DsPopoverLegacy, {
159
281
  Root: DsPopoverRoot,
160
- Trigger: DsPopoverTrigger,
282
+ Trigger: PopoverTrigger,
283
+ Anchor: DsPopoverAnchor,
161
284
  Panel: DsPopoverPanel,
162
285
  Header: DsPopoverHeader,
163
286
  Content: DsPopoverContent,
@@ -6,6 +6,9 @@ export type DsPopoverSide = (typeof popoverSides)[number];
6
6
  export const popoverAligns = ['start', 'center', 'end'] as const;
7
7
  export type DsPopoverAlign = (typeof popoverAligns)[number];
8
8
 
9
+ export const popoverOpenTriggers = ['click', 'hover'] as const;
10
+ export type DsPopoverOpenTrigger = (typeof popoverOpenTriggers)[number];
11
+
9
12
  export interface DsPopoverRootProps {
10
13
  /** Controlled open state. Pair with `onOpenChange`. */
11
14
  open?: boolean;
@@ -31,7 +34,62 @@ export interface DsPopoverRootProps {
31
34
  * @default false
32
35
  */
33
36
  modal?: boolean;
37
+ /**
38
+ * Size the panel to the positioning reference (Anchor, or the trigger).
39
+ * An explicit `DsPopover.Panel` `width` wins over this.
40
+ * @default false
41
+ */
42
+ matchAnchorWidth?: boolean;
43
+ /**
44
+ * Whether to restore focus to the previously focused element on close.
45
+ * @default true
46
+ */
47
+ restoreFocus?: boolean;
48
+ /**
49
+ * Trigger open method - click / hover
50
+ * @default 'click'
51
+ */
52
+ openOn?: DsPopoverOpenTrigger;
53
+ /**
54
+ * Milliseconds the pointer must rest on the trigger before the panel opens.
55
+ * Ignored unless `openOn` is `'hover'`.
56
+ * @default 200
57
+ */
58
+ openDelay?: number;
59
+ /**
60
+ * Milliseconds after the pointer leaves the trigger or the panel before it
61
+ * closes. Ignored unless `openOn` is `'hover'`.
62
+ * @default 150
63
+ */
64
+ closeDelay?: number;
65
+ /**
66
+ * Returns the element the panel should position against.
67
+ * When provided, the panel anchors to this element instead of the trigger —
68
+ * useful when the trigger lives in one place (e.g. a sidebar) and the panel
69
+ * should appear relative to a different reference point. Prefer `DsPopover.Anchor`
70
+ * when the reference is in the tree.
71
+ */
72
+ getAnchorElement?: () => HTMLElement | null;
34
73
  children: ReactNode;
74
+ /**
75
+ * Called when focus is about to move into the panel as it opens.
76
+ * Call `event.preventDefault()` to stop the default move to the first focusable
77
+ * control; focus another node in the same handler if needed.
78
+ */
79
+ onOpenAutoFocus?: (event: Event) => void;
80
+ /**
81
+ * Called when focus is about to leave the panel as it closes.
82
+ * Call `event.preventDefault()` to stop restoring focus to the trigger;
83
+ * focus another node in the same handler if needed.
84
+ * Still runs when `restoreFocus` is false.
85
+ */
86
+ onCloseAutoFocus?: (event: Event) => void;
87
+ /**
88
+ * Called when the user interacts outside the panel.
89
+ * Call `event.preventDefault()` to keep the panel open. Clicks on
90
+ * `DsPopover.Anchor` already do not dismiss.
91
+ */
92
+ onInteractOutside?: (event: Event) => void;
35
93
  /** Fires when the popover opens or closes. */
36
94
  onOpenChange?: (open: boolean) => void;
37
95
  }
@@ -42,6 +100,12 @@ export interface DsPopoverTriggerProps {
42
100
  className?: string;
43
101
  }
44
102
 
103
+ export interface DsPopoverAnchorProps {
104
+ /** Single element the panel positions against. Wraps the trigger when they share a field. */
105
+ children: ReactNode;
106
+ className?: string;
107
+ }
108
+
45
109
  export interface DsPopoverPanelProps {
46
110
  /**
47
111
  * Panel width in pixels.
@@ -52,6 +116,7 @@ export interface DsPopoverPanelProps {
52
116
  style?: CSSProperties;
53
117
  children: ReactNode;
54
118
  ref?: Ref<HTMLDivElement>;
119
+ id?: string;
55
120
  'aria-label'?: string;
56
121
  }
57
122