@duro-app/ui 5.0.0 → 5.2.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 (139) hide show
  1. package/dist/Table-C70zdSkM.js +1419 -0
  2. package/dist/Table-C70zdSkM.js.map +1 -0
  3. package/dist/components/Button/Button.d.ts +11 -2
  4. package/dist/components/Button/Button.d.ts.map +1 -1
  5. package/dist/components/Button/Button.stories.d.ts +1 -0
  6. package/dist/components/Button/Button.stories.d.ts.map +1 -1
  7. package/dist/components/Card/Card.stories.d.ts.map +1 -1
  8. package/dist/components/Combobox/Combobox.d.ts +1 -1
  9. package/dist/components/Combobox/Combobox.d.ts.map +1 -1
  10. package/dist/components/Combobox/styles.css.d.ts +0 -64
  11. package/dist/components/Combobox/styles.css.d.ts.map +1 -1
  12. package/dist/components/Dialog/Dialog.d.ts +11 -3
  13. package/dist/components/Dialog/Dialog.d.ts.map +1 -1
  14. package/dist/components/Dialog/Dialog.stories.d.ts +4 -0
  15. package/dist/components/Dialog/Dialog.stories.d.ts.map +1 -1
  16. package/dist/components/Dialog/styles.css.d.ts +1 -0
  17. package/dist/components/Dialog/styles.css.d.ts.map +1 -1
  18. package/dist/components/DragDrop/styles.css.d.ts +10 -2
  19. package/dist/components/DragDrop/styles.css.d.ts.map +1 -1
  20. package/dist/components/Grid/Grid.d.ts +11 -2
  21. package/dist/components/Grid/Grid.d.ts.map +1 -1
  22. package/dist/components/Grid/Grid.stories.d.ts +2 -0
  23. package/dist/components/Grid/Grid.stories.d.ts.map +1 -1
  24. package/dist/components/Grid/styles.css.d.ts +2 -2
  25. package/dist/components/Grid/styles.css.d.ts.map +1 -1
  26. package/dist/components/Input/Input.d.ts +6 -1
  27. package/dist/components/Input/Input.d.ts.map +1 -1
  28. package/dist/components/Input/Input.meta.d.ts.map +1 -1
  29. package/dist/components/Input/Input.stories.d.ts +1 -0
  30. package/dist/components/Input/Input.stories.d.ts.map +1 -1
  31. package/dist/components/Input/styles.css.d.ts +18 -0
  32. package/dist/components/Input/styles.css.d.ts.map +1 -1
  33. package/dist/components/InputGroup/InputGroup.d.ts +6 -1
  34. package/dist/components/InputGroup/InputGroup.d.ts.map +1 -1
  35. package/dist/components/InputGroup/InputGroup.stories.d.ts +1 -0
  36. package/dist/components/InputGroup/InputGroup.stories.d.ts.map +1 -1
  37. package/dist/components/Listbox/Listbox.d.ts +75 -0
  38. package/dist/components/Listbox/Listbox.d.ts.map +1 -0
  39. package/dist/components/Listbox/Listbox.meta.d.ts +3 -0
  40. package/dist/components/Listbox/Listbox.meta.d.ts.map +1 -0
  41. package/dist/components/Listbox/Listbox.stories.d.ts +7 -0
  42. package/dist/components/Listbox/Listbox.stories.d.ts.map +1 -0
  43. package/dist/components/Listbox/styles.css.d.ts +75 -0
  44. package/dist/components/Listbox/styles.css.d.ts.map +1 -0
  45. package/dist/components/Menu/Menu.d.ts +18 -5
  46. package/dist/components/Menu/Menu.d.ts.map +1 -1
  47. package/dist/components/Menu/Menu.meta.d.ts.map +1 -1
  48. package/dist/components/Menu/Menu.stories.d.ts +7 -0
  49. package/dist/components/Menu/Menu.stories.d.ts.map +1 -1
  50. package/dist/components/Menu/MenuContext.d.ts +3 -0
  51. package/dist/components/Menu/MenuContext.d.ts.map +1 -1
  52. package/dist/components/Menu/styles.css.d.ts +24 -15
  53. package/dist/components/Menu/styles.css.d.ts.map +1 -1
  54. package/dist/components/Menu/useMenuRoot.d.ts +14 -1
  55. package/dist/components/Menu/useMenuRoot.d.ts.map +1 -1
  56. package/dist/components/StatusIcon/StatusIcon.stories.d.ts.map +1 -1
  57. package/dist/components/Table/Table.stories.d.ts +1 -0
  58. package/dist/components/Table/Table.stories.d.ts.map +1 -1
  59. package/dist/components/TextLink/TextLink.d.ts +24 -0
  60. package/dist/components/TextLink/TextLink.d.ts.map +1 -0
  61. package/dist/components/TextLink/TextLink.meta.d.ts +3 -0
  62. package/dist/components/TextLink/TextLink.meta.d.ts.map +1 -0
  63. package/dist/components/TextLink/TextLink.stories.d.ts +9 -0
  64. package/dist/components/TextLink/TextLink.stories.d.ts.map +1 -0
  65. package/dist/components/TextLink/styles.css.d.ts +28 -0
  66. package/dist/components/TextLink/styles.css.d.ts.map +1 -0
  67. package/dist/components/ThemeProvider/ThemeProvider.stories.d.ts +1 -0
  68. package/dist/components/ThemeProvider/ThemeProvider.stories.d.ts.map +1 -1
  69. package/dist/components/VisuallyHidden/VisuallyHidden.d.ts +14 -0
  70. package/dist/components/VisuallyHidden/VisuallyHidden.d.ts.map +1 -0
  71. package/dist/components/VisuallyHidden/VisuallyHidden.meta.d.ts +3 -0
  72. package/dist/components/VisuallyHidden/VisuallyHidden.meta.d.ts.map +1 -0
  73. package/dist/components/VisuallyHidden/VisuallyHidden.stories.d.ts +7 -0
  74. package/dist/components/VisuallyHidden/VisuallyHidden.stories.d.ts.map +1 -0
  75. package/dist/docs/FlexGrow.stories.d.ts +7 -0
  76. package/dist/docs/FlexGrow.stories.d.ts.map +1 -0
  77. package/dist/index.css +1 -1
  78. package/dist/index.d.ts +5 -0
  79. package/dist/index.d.ts.map +1 -1
  80. package/dist/index.js +2986 -2821
  81. package/dist/index.js.map +1 -1
  82. package/dist/index.native.d.ts +1 -0
  83. package/dist/index.native.d.ts.map +1 -1
  84. package/dist/shared/length.d.ts +14 -9
  85. package/dist/shared/length.d.ts.map +1 -1
  86. package/dist/shared/length.typecheck.d.ts +3 -0
  87. package/dist/shared/length.typecheck.d.ts.map +1 -1
  88. package/dist/styles/visually-hidden.css.d.ts +6 -0
  89. package/dist/styles/visually-hidden.css.d.ts.map +1 -1
  90. package/dist/table.js +1 -1
  91. package/package.json +2 -2
  92. package/src/components/Button/Button.stories.tsx +41 -0
  93. package/src/components/Button/Button.tsx +16 -1
  94. package/src/components/Card/Card.stories.tsx +2 -1
  95. package/src/components/Combobox/Combobox.tsx +25 -79
  96. package/src/components/Combobox/styles.css.ts +0 -74
  97. package/src/components/Dialog/Dialog.meta.ts +1 -1
  98. package/src/components/Dialog/Dialog.stories.tsx +109 -3
  99. package/src/components/Dialog/Dialog.tsx +63 -9
  100. package/src/components/Dialog/styles.css.ts +2 -0
  101. package/src/components/DragDrop/styles.css.ts +16 -3
  102. package/src/components/Grid/Grid.meta.ts +1 -1
  103. package/src/components/Grid/Grid.stories.tsx +53 -1
  104. package/src/components/Grid/Grid.tsx +23 -11
  105. package/src/components/Grid/styles.css.ts +3 -2
  106. package/src/components/Input/Input.meta.ts +10 -3
  107. package/src/components/Input/Input.stories.tsx +52 -2
  108. package/src/components/Input/Input.tsx +12 -3
  109. package/src/components/Input/styles.css.ts +29 -0
  110. package/src/components/InputGroup/InputGroup.stories.tsx +20 -0
  111. package/src/components/InputGroup/InputGroup.tsx +20 -2
  112. package/src/components/Listbox/Listbox.meta.ts +52 -0
  113. package/src/components/Listbox/Listbox.stories.tsx +170 -0
  114. package/src/components/Listbox/Listbox.tsx +278 -0
  115. package/src/components/Listbox/styles.css.ts +89 -0
  116. package/src/components/Menu/Menu.meta.ts +2 -1
  117. package/src/components/Menu/Menu.stories.tsx +289 -8
  118. package/src/components/Menu/Menu.tsx +100 -29
  119. package/src/components/Menu/MenuContext.ts +3 -0
  120. package/src/components/Menu/styles.css.ts +31 -15
  121. package/src/components/Menu/useMenuRoot.ts +105 -48
  122. package/src/components/StatusIcon/StatusIcon.stories.tsx +2 -2
  123. package/src/components/Table/Table.stories.tsx +39 -0
  124. package/src/components/TextLink/TextLink.meta.ts +33 -0
  125. package/src/components/TextLink/TextLink.stories.tsx +63 -0
  126. package/src/components/TextLink/TextLink.tsx +46 -0
  127. package/src/components/TextLink/styles.css.ts +58 -0
  128. package/src/components/ThemeProvider/ThemeProvider.stories.tsx +50 -0
  129. package/src/components/VisuallyHidden/VisuallyHidden.meta.ts +19 -0
  130. package/src/components/VisuallyHidden/VisuallyHidden.stories.tsx +30 -0
  131. package/src/components/VisuallyHidden/VisuallyHidden.tsx +22 -0
  132. package/src/docs/FlexGrow.stories.tsx +83 -0
  133. package/src/index.native.ts +1 -0
  134. package/src/index.ts +5 -0
  135. package/src/shared/length.ts +29 -10
  136. package/src/shared/length.typecheck.tsx +9 -0
  137. package/src/styles/visually-hidden.css.ts +8 -0
  138. package/dist/Table-BKN_d-mp.js +0 -1192
  139. package/dist/Table-BKN_d-mp.js.map +0 -1
@@ -28,5 +28,6 @@ export { Stack, type SpacingKey } from './components/Stack/Stack';
28
28
  export { Inline } from './components/Inline/Inline';
29
29
  export { Cluster } from './components/Cluster/Cluster';
30
30
  export { Grid } from './components/Grid/Grid';
31
+ export type { GridTrack } from './shared/length';
31
32
  export { ThemeProvider, usePortalMount, type ThemeName, } from './components/ThemeProvider/ThemeProvider';
32
33
  //# sourceMappingURL=index.native.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.native.d.ts","sourceRoot":"","sources":["../src/index.native.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,EAAC,KAAK,EAAE,KAAK,YAAY,EAAE,KAAK,SAAS,EAAC,MAAM,0BAA0B,CAAA;AACjF,OAAO,EAAC,MAAM,EAAE,KAAK,aAAa,EAAE,KAAK,UAAU,EAAC,MAAM,4BAA4B,CAAA;AACtF,OAAO,EAAC,IAAI,EAAE,KAAK,WAAW,EAAE,KAAK,QAAQ,EAAC,MAAM,wBAAwB,CAAA;AAC5E,OAAO,EAAC,QAAQ,EAAC,MAAM,gCAAgC,CAAA;AACvD,OAAO,EAAC,OAAO,EAAE,KAAK,cAAc,EAAC,MAAM,8BAA8B,CAAA;AACzE,OAAO,EAAC,KAAK,EAAE,KAAK,YAAY,EAAC,MAAM,0BAA0B,CAAA;AACjE,OAAO,EAAC,MAAM,EAAC,MAAM,4BAA4B,CAAA;AACjD,OAAO,EAAC,IAAI,EAAE,KAAK,WAAW,EAAE,KAAK,SAAS,EAAC,MAAM,wBAAwB,CAAA;AAC7E,OAAO,EAAC,QAAQ,EAAE,KAAK,eAAe,EAAC,MAAM,gCAAgC,CAAA;AAG7E,OAAO,EAAC,KAAK,EAAC,MAAM,0BAA0B,CAAA;AAC9C,OAAO,EAAC,UAAU,EAAC,MAAM,oCAAoC,CAAA;AAG7D,OAAO,EAAC,KAAK,EAAE,KAAK,UAAU,EAAC,MAAM,0BAA0B,CAAA;AAC/D,OAAO,EAAC,MAAM,EAAC,MAAM,4BAA4B,CAAA;AACjD,OAAO,EAAC,OAAO,EAAC,MAAM,8BAA8B,CAAA;AACpD,OAAO,EAAC,IAAI,EAAC,MAAM,wBAAwB,CAAA;AAI3C,OAAO,EACL,aAAa,EACb,cAAc,EACd,KAAK,SAAS,GACf,MAAM,0CAA0C,CAAA"}
1
+ {"version":3,"file":"index.native.d.ts","sourceRoot":"","sources":["../src/index.native.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,EAAC,KAAK,EAAE,KAAK,YAAY,EAAE,KAAK,SAAS,EAAC,MAAM,0BAA0B,CAAA;AACjF,OAAO,EAAC,MAAM,EAAE,KAAK,aAAa,EAAE,KAAK,UAAU,EAAC,MAAM,4BAA4B,CAAA;AACtF,OAAO,EAAC,IAAI,EAAE,KAAK,WAAW,EAAE,KAAK,QAAQ,EAAC,MAAM,wBAAwB,CAAA;AAC5E,OAAO,EAAC,QAAQ,EAAC,MAAM,gCAAgC,CAAA;AACvD,OAAO,EAAC,OAAO,EAAE,KAAK,cAAc,EAAC,MAAM,8BAA8B,CAAA;AACzE,OAAO,EAAC,KAAK,EAAE,KAAK,YAAY,EAAC,MAAM,0BAA0B,CAAA;AACjE,OAAO,EAAC,MAAM,EAAC,MAAM,4BAA4B,CAAA;AACjD,OAAO,EAAC,IAAI,EAAE,KAAK,WAAW,EAAE,KAAK,SAAS,EAAC,MAAM,wBAAwB,CAAA;AAC7E,OAAO,EAAC,QAAQ,EAAE,KAAK,eAAe,EAAC,MAAM,gCAAgC,CAAA;AAG7E,OAAO,EAAC,KAAK,EAAC,MAAM,0BAA0B,CAAA;AAC9C,OAAO,EAAC,UAAU,EAAC,MAAM,oCAAoC,CAAA;AAG7D,OAAO,EAAC,KAAK,EAAE,KAAK,UAAU,EAAC,MAAM,0BAA0B,CAAA;AAC/D,OAAO,EAAC,MAAM,EAAC,MAAM,4BAA4B,CAAA;AACjD,OAAO,EAAC,OAAO,EAAC,MAAM,8BAA8B,CAAA;AACpD,OAAO,EAAC,IAAI,EAAC,MAAM,wBAAwB,CAAA;AAC3C,YAAY,EAAC,SAAS,EAAC,MAAM,iBAAiB,CAAA;AAI9C,OAAO,EACL,aAAa,EACb,cAAc,EACd,KAAK,SAAS,GACf,MAAM,0CAA0C,CAAA"}
@@ -7,18 +7,23 @@ import { SizeToken } from '@duro-app/tokens/keys';
7
7
  export type RelativeLength = 0 | '0' | 'auto' | 'none' | 'min-content' | 'max-content' | 'fit-content' | `${number}%` | `${number}vh` | `${number}vw` | `${number}dvh` | `${number}svh` | `${number}lvh` | `${number}em` | `${number}rem` | `${number}ch`;
8
8
  /** A measure prop: a `sizes` token key (`"gridColMd"`) or a relative length. */
9
9
  export type Length = SizeToken | RelativeLength;
10
+ /** A Length written as a string, as it appears inside a track function. */
11
+ type LengthString = Exclude<Length, number>;
12
+ /** One argument of `minmax()`: a Length or a fraction. */
13
+ type TrackBreadth = LengthString | `${number}fr`;
10
14
  /**
11
- * A CSS grid track: a Length, a fraction, or a track function. The arguments
12
- * of `minmax()` / `fit-content()` are not checked by the type — pass tokens
13
- * through `resolveLength` or `sizes.*` when building one.
14
- *
15
- * holds-until: a structured track type (`{min: Length, max: GridTrack}`)
16
- * replaces the template-literal functions; a type cannot see inside
17
- * `minmax(${string})`, so a px there is the one measure lint and types miss.
15
+ * A CSS grid track: a Length, a fraction, or a track function whose
16
+ * arguments are themselves Lengths or fractions — `'minmax(gridColSm, 1fr)'`,
17
+ * `'minmax(0, 2fr)'`, `'fit-content(labelMinW)'`. A px inside is a type
18
+ * error, as it is anywhere else. Arguments are separated by `, `.
18
19
  */
19
- export type GridTrack = Length | `${number}fr` | `minmax(${string})` | `fit-content(${string})`;
20
+ export type GridTrack = Length | `${number}fr` | `minmax(${TrackBreadth}, ${TrackBreadth})` | `fit-content(${LengthString})`;
20
21
  /** The CSS value for a Length: a size token key becomes its variable. */
21
22
  export declare function resolveLength(value: Length): string | number;
22
- /** The CSS value for a GridTrack: a size token key becomes its variable. */
23
+ /**
24
+ * The CSS value for a GridTrack: a size token key becomes its variable, also
25
+ * as an argument of `minmax()` / `fit-content()`.
26
+ */
23
27
  export declare function resolveTrack(track: GridTrack): string | number;
28
+ export {};
24
29
  //# sourceMappingURL=length.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"length.d.ts","sourceRoot":"","sources":["../../src/shared/length.ts"],"names":[],"mappings":"AACA,OAAO,EAAY,KAAK,SAAS,EAAC,MAAM,uBAAuB,CAAA;AAE/D;;;;GAIG;AACH,MAAM,MAAM,cAAc,GACtB,CAAC,GACD,GAAG,GACH,MAAM,GACN,MAAM,GACN,aAAa,GACb,aAAa,GACb,aAAa,GACb,GAAG,MAAM,GAAG,GACZ,GAAG,MAAM,IAAI,GACb,GAAG,MAAM,IAAI,GACb,GAAG,MAAM,KAAK,GACd,GAAG,MAAM,KAAK,GACd,GAAG,MAAM,KAAK,GACd,GAAG,MAAM,IAAI,GACb,GAAG,MAAM,KAAK,GACd,GAAG,MAAM,IAAI,CAAA;AAEjB,gFAAgF;AAChF,MAAM,MAAM,MAAM,GAAG,SAAS,GAAG,cAAc,CAAA;AAE/C;;;;;;;;GAQG;AACH,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,GAAG,MAAM,IAAI,GAAG,UAAU,MAAM,GAAG,GAAG,eAAe,MAAM,GAAG,CAAA;AAI/F,yEAAyE;AACzE,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAE5D;AAED,4EAA4E;AAC5E,wBAAgB,YAAY,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,GAAG,MAAM,CAE9D"}
1
+ {"version":3,"file":"length.d.ts","sourceRoot":"","sources":["../../src/shared/length.ts"],"names":[],"mappings":"AACA,OAAO,EAAY,KAAK,SAAS,EAAC,MAAM,uBAAuB,CAAA;AAE/D;;;;GAIG;AACH,MAAM,MAAM,cAAc,GACtB,CAAC,GACD,GAAG,GACH,MAAM,GACN,MAAM,GACN,aAAa,GACb,aAAa,GACb,aAAa,GACb,GAAG,MAAM,GAAG,GACZ,GAAG,MAAM,IAAI,GACb,GAAG,MAAM,IAAI,GACb,GAAG,MAAM,KAAK,GACd,GAAG,MAAM,KAAK,GACd,GAAG,MAAM,KAAK,GACd,GAAG,MAAM,IAAI,GACb,GAAG,MAAM,KAAK,GACd,GAAG,MAAM,IAAI,CAAA;AAEjB,gFAAgF;AAChF,MAAM,MAAM,MAAM,GAAG,SAAS,GAAG,cAAc,CAAA;AAE/C,2EAA2E;AAC3E,KAAK,YAAY,GAAG,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;AAE3C,0DAA0D;AAC1D,KAAK,YAAY,GAAG,YAAY,GAAG,GAAG,MAAM,IAAI,CAAA;AAEhD;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GACjB,MAAM,GACN,GAAG,MAAM,IAAI,GACb,UAAU,YAAY,KAAK,YAAY,GAAG,GAC1C,eAAe,YAAY,GAAG,CAAA;AAIlC,yEAAyE;AACzE,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAE5D;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,GAAG,MAAM,CAW9D"}
@@ -15,6 +15,9 @@ export declare const trackForms: Assignable<'iconButton' | '2fr' | 'max-content'
15
15
  export declare const lengthPxString: Assignable<'280px', Length>;
16
16
  export declare const lengthPxNumber: Assignable<44, Length>;
17
17
  export declare const trackPx: Assignable<'40px', GridTrack>;
18
+ export declare const trackMinmaxPx: Assignable<'minmax(40px, 1fr)', GridTrack>;
19
+ export declare const trackFitPx: Assignable<'fit-content(120px)', GridTrack>;
20
+ export declare const trackMinmaxTokens: Assignable<'minmax(gridColSm, 1fr)' | 'minmax(0, 2fr)' | 'minmax(labelMinW, auto)' | 'fit-content(labelMinW)', GridTrack>;
18
21
  export declare const gridMin: Assignable<'280px', Prop<typeof Grid, 'minColumnWidth'>>;
19
22
  export declare const gridMinToken: Assignable<'gridColMd', Prop<typeof Grid, 'minColumnWidth'>>;
20
23
  export declare const addonMin: Assignable<80, Prop<typeof InputGroup.Addon, 'minWidth'>>;
@@ -1 +1 @@
1
- {"version":3,"file":"length.typecheck.d.ts","sourceRoot":"","sources":["../../src/shared/length.typecheck.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,yBAAyB,CAAA;AACjD,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,qCAAqC,CAAA;AACnE,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,qCAAqC,CAAA;AACnE,OAAO,KAAK,EAAC,KAAK,EAAC,MAAM,2BAA2B,CAAA;AACpD,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,yCAAyC,CAAA;AACzE,OAAO,KAAK,EAAC,SAAS,EAAE,MAAM,EAAC,MAAM,UAAU,CAAA;AAE/C,wDAAwD;AACxD,KAAK,UAAU,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,KAAK,CAAA;AAEtD,4CAA4C;AAC5C,KAAK,OAAO,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,MAAM,CAAC,KAAK,OAAO,GAAG,CAAC,GAAG,KAAK,CAAA;AACnE,KAAK,IAAI,CAAC,CAAC,EAAE,CAAC,SAAS,MAAM,OAAO,CAAC,CAAC,CAAC,IAAI,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AAGrE,eAAO,MAAM,YAAY,EAAE,UAAU,CAAC,WAAW,GAAG,aAAa,EAAE,MAAM,CAAQ,CAAA;AACjF,eAAO,MAAM,cAAc,EAAE,UAAU,CAAC,CAAC,GAAG,GAAG,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,MAAM,CAAQ,CAAA;AACnG,eAAO,MAAM,UAAU,EAAE,UAAU,CACjC,YAAY,GAAG,KAAK,GAAG,aAAa,GAAG,gBAAgB,EACvD,SAAS,CACH,CAAA;AAGR,eAAO,MAAM,cAAc,EAAE,UAAU,CAAC,OAAO,EAAE,MAAM,CAAS,CAAA;AAChE,eAAO,MAAM,cAAc,EAAE,UAAU,CAAC,EAAE,EAAE,MAAM,CAAS,CAAA;AAC3D,eAAO,MAAM,OAAO,EAAE,UAAU,CAAC,MAAM,EAAE,SAAS,CAAS,CAAA;AAG3D,eAAO,MAAM,OAAO,EAAE,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,EAAE,gBAAgB,CAAC,CAAS,CAAA;AACtF,eAAO,MAAM,YAAY,EAAE,UAAU,CAAC,WAAW,EAAE,IAAI,CAAC,OAAO,IAAI,EAAE,gBAAgB,CAAC,CAAQ,CAAA;AAC9F,eAAO,MAAM,QAAQ,EAAE,UAAU,CAAC,EAAE,EAAE,IAAI,CAAC,OAAO,UAAU,CAAC,KAAK,EAAE,UAAU,CAAC,CAAS,CAAA;AACxF,eAAO,MAAM,WAAW,EAAE,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,OAAO,UAAU,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAS,CAAA;AAChG,eAAO,MAAM,gBAAgB,EAAE,UAAU,CACvC,UAAU,EACV,IAAI,CAAC,OAAO,UAAU,CAAC,QAAQ,EAAE,WAAW,CAAC,CACvC,CAAA;AACR,eAAO,MAAM,QAAQ,EAAE,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,OAAO,KAAK,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAS,CAAA;AACzF,eAAO,MAAM,WAAW,EAAE,UAAU,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAS,CAAA;AAC5F,eAAO,MAAM,gBAAgB,EAAE,UAAU,CACvC,YAAY,EACZ,IAAI,CAAC,OAAO,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAChC,CAAA;AACR,eAAO,MAAM,UAAU,EAAE,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,OAAO,YAAY,EAAE,WAAW,CAAC,CAAS,CAAA"}
1
+ {"version":3,"file":"length.typecheck.d.ts","sourceRoot":"","sources":["../../src/shared/length.typecheck.tsx"],"names":[],"mappings":"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"}
@@ -6,5 +6,11 @@ export declare const visuallyHidden: Readonly<{
6
6
  readonly height: css.StyleXClassNameFor<"height", 1>;
7
7
  readonly overflow: css.StyleXClassNameFor<"overflow", "hidden">;
8
8
  }>;
9
+ readonly text: Readonly<{
10
+ readonly clip: css.StyleXClassNameFor<"clip", "rect(0, 0, 0, 0)">;
11
+ readonly whiteSpace: css.StyleXClassNameFor<"whiteSpace", "nowrap">;
12
+ readonly borderWidth: css.StyleXClassNameFor<"borderWidth", 0>;
13
+ readonly padding: css.StyleXClassNameFor<"padding", 0>;
14
+ }>;
9
15
  }>;
10
16
  //# sourceMappingURL=visually-hidden.css.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"visually-hidden.css.d.ts","sourceRoot":"","sources":["../../src/styles/visually-hidden.css.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,GAAG,EAAC,MAAM,kBAAkB,CAAA;AAQpC,eAAO,MAAM,cAAc;;;;;;;EAOzB,CAAA"}
1
+ {"version":3,"file":"visually-hidden.css.d.ts","sourceRoot":"","sources":["../../src/styles/visually-hidden.css.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,GAAG,EAAC,MAAM,kBAAkB,CAAA;AAQpC,eAAO,MAAM,cAAc;;;;;;;;;;;;;EAezB,CAAA"}
package/dist/table.js CHANGED
@@ -1,4 +1,4 @@
1
- import { M as F, a as xt, R as rt, H as kt, c as pt, B as bt, d as ft, C as mt, b as ht, e as yt, r as St, f as Tt } from "./Table-BKN_d-mp.js";
1
+ import { M as F, g as xt, R as rt, H as kt, h as pt, B as bt, j as ft, C as mt, e as ht, k as yt, r as St, l as Tt } from "./Table-C70zdSkM.js";
2
2
  import { jsx as u, jsxs as q } from "react/jsx-runtime";
3
3
  import { c as H, m as p, d as b } from "./compiler-runtime-B2IcI8_O.js";
4
4
  import { flexRender as E, useReactTable as it, getPaginationRowModel as Ct, getFilteredRowModel as vt, getSortedRowModel as at, getCoreRowModel as st } from "@tanstack/react-table";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duro-app/ui",
3
- "version": "5.0.0",
3
+ "version": "5.2.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -66,7 +66,7 @@
66
66
  },
67
67
  "dependencies": {
68
68
  "@tanstack/react-virtual": "^3.14.6",
69
- "@duro-app/tokens": "^5.0.0"
69
+ "@duro-app/tokens": "^5.2.0"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@babel/preset-typescript": "^7.28.0",
@@ -1,5 +1,6 @@
1
1
  import type {Meta, StoryObj} from '@storybook/react'
2
2
  import {expect, fn} from 'storybook/test'
3
+ import {useRef, useState} from 'react'
3
4
  import {css, html} from 'react-strict-dom'
4
5
  import {Button} from './Button'
5
6
  import {spacing} from '@duro-app/tokens/tokens/spacing.css'
@@ -129,3 +130,43 @@ export const AllVariants: Story = {
129
130
  await expect(disabledButtons.length).toBe(3)
130
131
  },
131
132
  }
133
+
134
+ // A disclosure: aria-expanded / aria-controls pass through, and the ref
135
+ // reaches the <button> (to return focus to it).
136
+ function DisclosureDemo() {
137
+ const [open, setOpen] = useState(false)
138
+ const ref = useRef<HTMLButtonElement>(null)
139
+ return (
140
+ <html.div>
141
+ <Button
142
+ ref={ref}
143
+ variant="secondary"
144
+ aria-expanded={open}
145
+ aria-controls="filters-panel"
146
+ onClick={() => setOpen(!open)}
147
+ >
148
+ Filters
149
+ </Button>
150
+ {open && (
151
+ <html.div id="filters-panel" role="region" aria-label="Filters panel">
152
+ <Button variant="link" onClick={() => ref.current?.focus()}>
153
+ Back to Filters
154
+ </Button>
155
+ </html.div>
156
+ )}
157
+ </html.div>
158
+ )
159
+ }
160
+
161
+ export const RefAndDisclosure: Story = {
162
+ render: () => <DisclosureDemo />,
163
+ play: async ({canvas, userEvent}) => {
164
+ const toggle = canvas.getByRole('button', {name: 'Filters'})
165
+ await expect(toggle).toHaveAttribute('aria-expanded', 'false')
166
+ await userEvent.click(toggle)
167
+ await expect(toggle).toHaveAttribute('aria-expanded', 'true')
168
+ await expect(toggle).toHaveAttribute('aria-controls', 'filters-panel')
169
+ await userEvent.click(canvas.getByRole('button', {name: 'Back to Filters'}))
170
+ await expect(toggle).toHaveFocus()
171
+ },
172
+ }
@@ -1,4 +1,4 @@
1
- import type {ReactNode} from 'react'
1
+ import type {ReactNode, Ref} from 'react'
2
2
  import {html} from 'react-strict-dom'
3
3
  import {isNative} from '../../platform'
4
4
  import {styles} from './styles.css'
@@ -15,6 +15,15 @@ interface ButtonProps {
15
15
  onClick?: () => void
16
16
  /** Accessible name override — e.g. for buttons whose visible text isn't unique. */
17
17
  'aria-label'?: string
18
+ /** For a button that shows or hides another element (a disclosure, a panel). */
19
+ 'aria-expanded'?: boolean
20
+ /** Id of the element this button controls. */
21
+ 'aria-controls'?: string
22
+ /**
23
+ * The underlying <button>, e.g. to anchor a Popover or return focus to it.
24
+ * Web only: on native the ref is not attached.
25
+ */
26
+ ref?: Ref<HTMLButtonElement>
18
27
  children: ReactNode
19
28
  }
20
29
 
@@ -31,14 +40,20 @@ export function Button({
31
40
  type = 'button',
32
41
  onClick,
33
42
  'aria-label': ariaLabel,
43
+ 'aria-expanded': ariaExpanded,
44
+ 'aria-controls': ariaControls,
45
+ ref,
34
46
  children,
35
47
  }: ButtonProps) {
36
48
  return (
37
49
  <html.button
50
+ ref={isNative ? undefined : ref}
38
51
  type={type}
39
52
  disabled={disabled}
40
53
  onClick={onClick}
41
54
  aria-label={ariaLabel}
55
+ aria-expanded={ariaExpanded}
56
+ aria-controls={ariaControls}
42
57
  style={[
43
58
  styles.base,
44
59
  isNative && styles.nativeFlex,
@@ -5,6 +5,7 @@ import {Card} from './Card'
5
5
  import {colors} from '@duro-app/tokens/tokens/colors.css'
6
6
  import {typography} from '@duro-app/tokens/tokens/typography.css'
7
7
  import {spacing} from '@duro-app/tokens/tokens/spacing.css'
8
+ import {sizes} from '@duro-app/tokens/tokens/sizes.css'
8
9
 
9
10
  const meta: Meta<typeof Card> = {
10
11
  title: 'Components/Card',
@@ -86,7 +87,7 @@ export const Full: Story = {
86
87
  const gridStyles = css.create({
87
88
  grid: {
88
89
  display: 'grid',
89
- gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
90
+ gridTemplateColumns: `repeat(auto-fill, minmax(${sizes.gridColSm}, 1fr))`,
90
91
  gap: spacing.md,
91
92
  },
92
93
  stack: {display: 'flex', flexDirection: 'column', gap: spacing.md},
@@ -1,11 +1,9 @@
1
- import {type ReactNode, useRef, useId, useEffect, useLayoutEffect, useState} from 'react'
2
- import {createPortal} from 'react-dom'
1
+ import {type ReactNode, useCallback, useRef, useId, useEffect, useState} from 'react'
3
2
  import {html} from 'react-strict-dom'
4
3
  import {styles} from './styles.css'
5
4
  import {ComboboxContext, useCombobox} from './ComboboxContext'
6
5
  import {useComboboxRoot} from './useComboboxRoot'
7
- import {usePortalMount} from '../ThemeProvider/ThemeProvider'
8
- import {usePopoverLayer} from '../Popover/PopoverLayerContext'
6
+ import {Listbox, getAnchorProps} from '../Listbox/Listbox'
9
7
 
10
8
  // --- Root ---
11
9
  interface RootProps {
@@ -54,6 +52,7 @@ function Input({placeholder, children}: {placeholder?: string; children?: ReactN
54
52
  useCombobox()
55
53
  const localRef = useRef<HTMLInputElement>(null)
56
54
  const [focused, setFocused] = useState(false)
55
+ const anchor = getAnchorProps({id: listboxId, open, highlightedId})
57
56
 
58
57
  useEffect(() => {
59
58
  inputRef.current = localRef.current
@@ -67,10 +66,10 @@ function Input({placeholder, children}: {placeholder?: string; children?: ReactN
67
66
  role={'combobox' as 'listbox'}
68
67
  value={inputValue}
69
68
  placeholder={placeholder}
70
- aria-expanded={open}
69
+ aria-expanded={anchor['aria-expanded']}
71
70
  aria-haspopup="listbox"
72
- aria-controls={open ? listboxId : undefined}
73
- aria-activedescendant={highlightedId ?? undefined}
71
+ aria-controls={anchor['aria-controls']}
72
+ aria-activedescendant={anchor['aria-activedescendant']}
74
73
  aria-autocomplete={'list' as 'none'}
75
74
  autoComplete="off"
76
75
  style={styles.input}
@@ -132,58 +131,27 @@ function Trigger({children}: {children?: ReactNode}) {
132
131
  }
133
132
 
134
133
  // --- Popup ---
135
- // Rendered into the ThemeProvider portal mount and positioned with viewport
136
- // coordinates so it escapes ancestor `overflow: hidden` / `transform`
137
- // containing blocks (notably Dialog and Drawer).
134
+ // A Listbox placed under the input, as wide as the field: rendered into the
135
+ // ThemeProvider portal mount so it escapes ancestor `overflow: hidden` /
136
+ // `transform` containing blocks (notably Dialog and Drawer).
138
137
  function Popup({children}: {children: ReactNode}) {
139
- const {open, listboxId, rootRef} = useCombobox()
140
- const mount = usePortalMount()
141
- const listboxRef = useRef<HTMLDivElement>(null)
142
- const [coords, setCoords] = useState<{top: number; left: number; width: number} | null>(null)
138
+ const {open, listboxId, rootRef, highlightedId, setHighlightedId, setValue} = useCombobox()
139
+ const anchor = useCallback(() => rootRef.current?.getBoundingClientRect(), [rootRef])
143
140
 
144
- // Measure the root each time the popup opens, and re-measure on scroll/resize
145
- // so the dropdown follows the input. Capture-phase scroll listener catches
146
- // scroll on any ancestor (e.g. Dialog body).
147
- useLayoutEffect(() => {
148
- if (!open) {
149
- setCoords(null)
150
- return
151
- }
152
- const update = () => {
153
- const el = rootRef.current
154
- if (!el) return
155
- const rect = el.getBoundingClientRect()
156
- // Position immediately below the input with a small gap. `xs` token = 4px.
157
- setCoords({top: rect.bottom + 4, left: rect.left, width: rect.width})
158
- }
159
- update()
160
- window.addEventListener('resize', update)
161
- window.addEventListener('scroll', update, true)
162
- return () => {
163
- window.removeEventListener('resize', update)
164
- window.removeEventListener('scroll', update, true)
165
- }
166
- }, [open, rootRef])
167
-
168
- usePopoverLayer(listboxRef, {active: open && coords !== null})
169
-
170
- if (!open || !coords) return null
171
-
172
- const node = (
173
- <html.div
174
- ref={listboxRef}
141
+ return (
142
+ <Listbox.Root
175
143
  id={listboxId}
176
- role="listbox"
177
- style={[styles.popup, styles.popupPosition(coords.top, coords.left, coords.width)]}
144
+ open={open}
145
+ anchor={anchor}
146
+ matchAnchorWidth
147
+ highlightedId={highlightedId}
148
+ onHighlight={setHighlightedId}
149
+ onSelect={setValue}
150
+ maxHeight="listMaxHSm"
178
151
  >
179
152
  {children}
180
- </html.div>
153
+ </Listbox.Root>
181
154
  )
182
-
183
- // SSR / pre-mount fallback: render inline (will be invisible under
184
- // overflow:hidden parents but won't crash). Once the mount is available,
185
- // portal into it.
186
- return mount ? createPortal(node, mount) : node
187
155
  }
188
156
 
189
157
  // --- Item ---
@@ -193,20 +161,10 @@ interface ItemProps {
193
161
  }
194
162
 
195
163
  function Item({value: itemValue, children}: ItemProps) {
196
- const {
197
- value: selectedValue,
198
- setValue,
199
- inputValue,
200
- labels,
201
- registerLabel,
202
- highlightedId,
203
- setHighlightedId,
204
- registerItem,
205
- } = useCombobox()
164
+ const {value: selectedValue, inputValue, labels, registerLabel, registerItem} = useCombobox()
206
165
  const id = useId()
207
166
  const ref = useRef<HTMLDivElement>(null)
208
167
  const isSelected = selectedValue === itemValue
209
- const isHighlighted = highlightedId === id
210
168
 
211
169
  // Filter: check if this item matches the input
212
170
  const label = labels[itemValue] ?? itemValue
@@ -229,21 +187,9 @@ function Item({value: itemValue, children}: ItemProps) {
229
187
  if (!isVisible) return null
230
188
 
231
189
  return (
232
- <html.div
233
- ref={ref}
234
- id={id}
235
- role="option"
236
- aria-selected={isSelected}
237
- onClick={() => setValue(itemValue)}
238
- onPointerEnter={() => setHighlightedId(id)}
239
- style={[
240
- styles.item,
241
- isSelected && styles.itemSelected,
242
- isHighlighted && styles.itemHighlighted,
243
- ]}
244
- >
190
+ <Listbox.Option ref={ref} id={id} value={itemValue} selected={isSelected}>
245
191
  {children}
246
- </html.div>
192
+ </Listbox.Option>
247
193
  )
248
194
  }
249
195
 
@@ -266,7 +212,7 @@ function Empty({children}: {children: ReactNode}) {
266
212
 
267
213
  if (hasVisibleItems) return null
268
214
 
269
- return <html.div style={styles.empty}>{children}</html.div>
215
+ return <Listbox.Empty>{children}</Listbox.Empty>
270
216
  }
271
217
 
272
218
  export const Combobox = {
@@ -2,9 +2,7 @@ import {css} from 'react-strict-dom'
2
2
  import {colors} from '@duro-app/tokens/tokens/colors.css'
3
3
  import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
4
4
  import {typography} from '@duro-app/tokens/tokens/typography.css'
5
- import {shadows} from '@duro-app/tokens/tokens/shadows.css'
6
5
  import {duration} from '@duro-app/tokens/tokens/motion.css'
7
- import {sizes} from '@duro-app/tokens/tokens/sizes.css'
8
6
  import {borders} from '@duro-app/tokens/tokens/borders.css'
9
7
 
10
8
  export const styles = css.create({
@@ -57,76 +55,4 @@ export const styles = css.create({
57
55
  color: colors.textMuted,
58
56
  cursor: 'pointer',
59
57
  },
60
- backdrop: {
61
- position: 'fixed',
62
- top: 0,
63
- left: 0,
64
- right: 0,
65
- bottom: 0,
66
- // Above Dialog/Drawer (1000/1001): they portal into the same mount, so a
67
- // popup opened from inside a dialog must out-rank the dialog itself.
68
- zIndex: 1049,
69
- },
70
- popup: {
71
- // Position is `fixed` and the top/left/width are applied inline by Popup
72
- // (computed from the root's bounding rect) so the dropdown can escape any
73
- // ancestor with `overflow: hidden` or `transform` (e.g. inside a Dialog).
74
- position: 'fixed',
75
- backgroundColor: colors.bgCard,
76
- borderWidth: borders.hairline,
77
- borderStyle: 'solid',
78
- borderColor: colors.border,
79
- borderRadius: radii.sm,
80
- boxShadow: shadows.md,
81
- paddingTop: spacing.xs,
82
- paddingBottom: spacing.xs,
83
- maxHeight: sizes.listMaxHSm,
84
- overflowY: 'auto',
85
- zIndex: 1050,
86
- // Re-enable pointer events: the portal mount is pointer-events: none so
87
- // clicks fall through except on the popup itself.
88
- pointerEvents: 'auto',
89
- },
90
- item: {
91
- display: 'flex',
92
- alignItems: 'center',
93
- paddingTop: spacing.sm,
94
- paddingBottom: spacing.sm,
95
- paddingLeft: spacing.md,
96
- paddingRight: spacing.md,
97
- fontSize: typography.fontSizeSm,
98
- fontFamily: typography.fontFamily,
99
- color: colors.text,
100
- cursor: 'pointer',
101
- backgroundColor: 'transparent',
102
- transitionProperty: 'background-color',
103
- transitionDuration: duration.fast,
104
- },
105
- itemSelected: {
106
- color: colors.accent,
107
- fontWeight: typography.fontWeightMedium,
108
- },
109
- itemHighlighted: {
110
- backgroundColor: colors.bgCardHover,
111
- },
112
- itemHidden: {
113
- display: 'none',
114
- },
115
- empty: {
116
- paddingTop: spacing.sm,
117
- paddingBottom: spacing.sm,
118
- paddingLeft: spacing.md,
119
- paddingRight: spacing.md,
120
- fontSize: typography.fontSizeSm,
121
- color: colors.textMuted,
122
- },
123
- hidden: {
124
- display: 'none',
125
- },
126
- // Dynamic position — applied at runtime from the input's bounding rect.
127
- popupPosition: (top: number, left: number, width: number) => ({
128
- top,
129
- left,
130
- width,
131
- }),
132
58
  })
@@ -2,7 +2,7 @@ import type {ComponentMeta} from '../component-meta'
2
2
 
3
3
  export const meta: ComponentMeta = {
4
4
  description:
5
- 'Modal dialog with backdrop overlay. Centered on screen with scale+fade animation. Traps focus, dismisses on backdrop click or Escape. Use for confirmations, forms, or content that requires user attention before continuing.',
5
+ 'Modal dialog with backdrop overlay. Centered on screen with scale+fade animation. Moves focus in on open (initialFocus, else the first focusable element, else the dialog) and back on close; dismisses on backdrop click (dismissable) or Escape (closeOnEscape, independent of dismissable). Use for confirmations, forms, or content that requires user attention before continuing.',
6
6
  whenToUse: [
7
7
  'Confirmation prompts (delete, revoke, destructive actions)',
8
8
  'Short forms or data entry that blocks the main flow',
@@ -1,7 +1,7 @@
1
1
  import type {Meta, StoryObj} from '@storybook/react'
2
- import {expect, within, userEvent} from 'storybook/test'
2
+ import {expect, screen, waitFor, within, userEvent} from 'storybook/test'
3
3
  import {css, html} from 'react-strict-dom'
4
- import {useState} from 'react'
4
+ import {useRef, useState} from 'react'
5
5
  import {Dialog} from './Dialog'
6
6
  import {Button} from '../Button/Button'
7
7
  import {Text} from '../Text/Text'
@@ -260,7 +260,7 @@ export const DangerConfirmation: Story = {
260
260
 
261
261
  export const NonDismissable: Story = {
262
262
  render: () => (
263
- <Dialog.Root dismissable={false}>
263
+ <Dialog.Root dismissable={false} closeOnEscape={false}>
264
264
  <Dialog.Trigger>
265
265
  <Button>Open non-dismissable</Button>
266
266
  </Dialog.Trigger>
@@ -454,3 +454,109 @@ export const BackdropClickDismisses: Story = {
454
454
  await expect(await canvas.findByText('Last action: Dismissed')).toBeInTheDocument()
455
455
  },
456
456
  }
457
+
458
+ // --- Focus and Escape (5.2) ---
459
+
460
+ // dismissable only governs the backdrop: Escape still closes.
461
+ export const EscapeClosesNonDismissable: Story = {
462
+ render: () => (
463
+ <Dialog.Root dismissable={false}>
464
+ <Dialog.Trigger>
465
+ <Button>Open terms</Button>
466
+ </Dialog.Trigger>
467
+ <Dialog.Portal size="sm">
468
+ <Dialog.Header>
469
+ <Dialog.Title>Terms</Dialog.Title>
470
+ </Dialog.Header>
471
+ <Dialog.Body>
472
+ <Text>Read before continuing.</Text>
473
+ </Dialog.Body>
474
+ </Dialog.Portal>
475
+ </Dialog.Root>
476
+ ),
477
+ play: async ({canvas, userEvent}) => {
478
+ const opener = canvas.getByRole('button', {name: 'Open terms'})
479
+ await userEvent.click(opener)
480
+ const dialog = await screen.findByRole('dialog', {name: 'Terms'})
481
+ // Nothing focusable inside: the dialog itself takes focus.
482
+ await waitFor(() => expect(dialog).toHaveFocus())
483
+ await userEvent.keyboard('{Escape}')
484
+ await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument())
485
+ await waitFor(() => expect(opener).toHaveFocus())
486
+ },
487
+ }
488
+
489
+ function InitialFocusDemo() {
490
+ const nameRef = useRef<HTMLInputElement>(null)
491
+ return (
492
+ <Dialog.Root initialFocus={nameRef}>
493
+ <Dialog.Trigger>
494
+ <Button>Rename</Button>
495
+ </Dialog.Trigger>
496
+ <Dialog.Portal size="sm">
497
+ <Dialog.Header>
498
+ <Dialog.Title>Rename file</Dialog.Title>
499
+ <Dialog.Close />
500
+ </Dialog.Header>
501
+ <Dialog.Body>
502
+ <Field.Root>
503
+ <Field.Label>Name</Field.Label>
504
+ <Input ref={nameRef} defaultValue="notes.md" />
505
+ </Field.Root>
506
+ </Dialog.Body>
507
+ </Dialog.Portal>
508
+ </Dialog.Root>
509
+ )
510
+ }
511
+
512
+ export const InitialFocus: Story = {
513
+ render: () => <InitialFocusDemo />,
514
+ play: async ({canvas, userEvent}) => {
515
+ await userEvent.click(canvas.getByRole('button', {name: 'Rename'}))
516
+ const input = await screen.findByRole('textbox', {name: 'Name'})
517
+ await waitFor(() => expect(input).toHaveFocus())
518
+ },
519
+ }
520
+
521
+ export const FirstFocusableByDefault: Story = {
522
+ render: () => (
523
+ <Dialog.Root>
524
+ <Dialog.Trigger>
525
+ <Button>Open</Button>
526
+ </Dialog.Trigger>
527
+ <Dialog.Portal size="sm">
528
+ <Dialog.Header>
529
+ <Dialog.Title>Share</Dialog.Title>
530
+ <Dialog.Close aria-label="Close share" />
531
+ </Dialog.Header>
532
+ </Dialog.Portal>
533
+ </Dialog.Root>
534
+ ),
535
+ play: async ({canvas, userEvent}) => {
536
+ await userEvent.click(canvas.getByRole('button', {name: 'Open'}))
537
+ const close = await screen.findByRole('button', {name: 'Close share'})
538
+ await waitFor(() => expect(close).toHaveFocus())
539
+ },
540
+ }
541
+
542
+ export const CloseOnEscapeOff: Story = {
543
+ render: () => (
544
+ <Dialog.Root defaultOpen closeOnEscape={false}>
545
+ <Dialog.Portal size="sm">
546
+ <Dialog.Header>
547
+ <Dialog.Title>Required step</Dialog.Title>
548
+ </Dialog.Header>
549
+ <Dialog.Footer>
550
+ <Dialog.Close>
551
+ <Button>Done</Button>
552
+ </Dialog.Close>
553
+ </Dialog.Footer>
554
+ </Dialog.Portal>
555
+ </Dialog.Root>
556
+ ),
557
+ play: async ({userEvent}) => {
558
+ await screen.findByRole('dialog', {name: 'Required step'})
559
+ await userEvent.keyboard('{Escape}')
560
+ await expect(screen.getByRole('dialog', {name: 'Required step'})).toBeInTheDocument()
561
+ },
562
+ }