@chassis-ui/react 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +11 -0
  2. package/dist/index.d.ts +682 -237
  3. package/dist/index.js +2 -2
  4. package/dist/style.css +1 -1
  5. package/package.json +32 -25
  6. package/src/components/accordion/Accordion.tsx +2 -2
  7. package/src/components/accordion/AccordionItem.tsx +6 -6
  8. package/src/components/autocomplete/Autocomplete.tsx +4 -4
  9. package/src/components/avatar/Avatar.tsx +5 -2
  10. package/src/components/breadcrumb/Breadcrumb.tsx +11 -2
  11. package/src/components/button/Button.tsx +7 -4
  12. package/src/components/button-group/ButtonGroup.tsx +2 -2
  13. package/src/components/calendar/Calendar.tsx +56 -42
  14. package/src/components/calendar/CalendarMonthGrid.tsx +3 -1
  15. package/src/components/calendar/CalendarMonthYearPicker.tsx +4 -2
  16. package/src/components/calendar/CalendarYearGrid.tsx +5 -3
  17. package/src/components/calendar/RangeCalendar.scss +8 -8
  18. package/src/components/calendar/RangeCalendar.tsx +56 -44
  19. package/src/components/calendar/index.ts +1 -0
  20. package/src/components/calendar/labels.tsx +71 -0
  21. package/src/components/card/Card.tsx +4 -4
  22. package/src/components/card/CardGroup.tsx +1 -1
  23. package/src/components/card/CardImage.tsx +1 -1
  24. package/src/components/carousel/Carousel.tsx +20 -9
  25. package/src/components/carousel/CarouselControlButton.tsx +1 -1
  26. package/src/components/carousel/CarouselPlayPause.tsx +1 -1
  27. package/src/components/carousel/carouselEngine.ts +3 -3
  28. package/src/components/checkbox/Checkbox.tsx +12 -11
  29. package/src/components/checkbox/CheckboxGroup.tsx +1 -1
  30. package/src/components/chip/Chip.tsx +5 -4
  31. package/src/components/chip-input/ChipInput.tsx +12 -4
  32. package/src/components/chip-input/ChipList.tsx +2 -2
  33. package/src/components/close-button/CloseButton.tsx +2 -2
  34. package/src/components/color-input/ColorInput.tsx +2 -2
  35. package/src/components/combobox/Combobox.tsx +2 -2
  36. package/src/components/datagrid/DataGrid.scss +203 -0
  37. package/src/components/datagrid/DataGrid.tsx +259 -0
  38. package/src/components/datagrid/DataGridBody.tsx +162 -0
  39. package/src/components/datagrid/DataGridCell.tsx +47 -0
  40. package/src/components/datagrid/DataGridColumn.tsx +109 -0
  41. package/src/components/datagrid/DataGridHeader.tsx +47 -0
  42. package/src/components/datagrid/DataGridPinBehavior.tsx +225 -0
  43. package/src/components/datagrid/DataGridRow.tsx +48 -0
  44. package/src/components/datagrid/DataGridSelectionCell.tsx +74 -0
  45. package/src/components/datagrid/DataGridTableLayout.ts +30 -0
  46. package/src/components/datagrid/index.ts +14 -0
  47. package/src/components/datagrid/useColumnGeometry.ts +161 -0
  48. package/src/components/datepicker/ClearButton.tsx +4 -1
  49. package/src/components/datepicker/DatePicker.scss +2 -2
  50. package/src/components/datepicker/DatePicker.tsx +179 -147
  51. package/src/components/datepicker/DateRangePicker.tsx +107 -89
  52. package/src/components/datepicker/renderDatePickerShell.tsx +1 -1
  53. package/src/components/drawer/Drawer.tsx +3 -1
  54. package/src/components/file-input/FileInput.tsx +2 -2
  55. package/src/components/floating-input/FloatingInput.tsx +8 -8
  56. package/src/components/form/renderFormCheck.tsx +3 -3
  57. package/src/components/form-field/FormField.tsx +8 -8
  58. package/src/components/form-field/renderFormField.tsx +14 -3
  59. package/src/components/grid/Col.tsx +1 -1
  60. package/src/components/grid/GridItem.tsx +1 -1
  61. package/src/components/grid/Row.tsx +1 -1
  62. package/src/components/input-group/InputGroup.tsx +2 -2
  63. package/src/components/list/List.tsx +5 -5
  64. package/src/components/menu/Menu.tsx +10 -0
  65. package/src/components/menu/MenuSubmenu.tsx +3 -3
  66. package/src/components/menu/MenuSubmenuBack.tsx +1 -1
  67. package/src/components/menu/MenuToggle.tsx +2 -2
  68. package/src/components/modal/Modal.tsx +5 -3
  69. package/src/components/navbar/Navbar.tsx +1 -1
  70. package/src/components/navbar/NavbarBrand.tsx +5 -2
  71. package/src/components/notification/NotificationStack.tsx +1 -1
  72. package/src/components/otp-input/OtpBox.tsx +1 -1
  73. package/src/components/otp-input/OtpInput.tsx +12 -11
  74. package/src/components/pagination/Pagination.tsx +2 -2
  75. package/src/components/pagination/PaginationItem.tsx +23 -4
  76. package/src/components/popover/Popover.tsx +40 -14
  77. package/src/components/progress/Progress.tsx +1 -1
  78. package/src/components/progress/ProgressBar.tsx +1 -1
  79. package/src/components/radio/Radio.tsx +2 -2
  80. package/src/components/radio/RadioGroup.tsx +1 -1
  81. package/src/components/select/Select.tsx +8 -8
  82. package/src/components/skeleton/Skeleton.tsx +1 -1
  83. package/src/components/spinner/Spinner.tsx +2 -2
  84. package/src/components/stepper/Stepper.tsx +5 -5
  85. package/src/components/switch/Switch.tsx +3 -3
  86. package/src/components/table/Table.css +2 -2
  87. package/src/components/table/Table.tsx +12 -6
  88. package/src/components/text-input/TextInput.tsx +2 -2
  89. package/src/components/textarea/Textarea.tsx +2 -2
  90. package/src/components/toast/ToastBody.tsx +1 -1
  91. package/src/components/toast/ToastHeader.tsx +2 -2
  92. package/src/components/toast/Toaster.tsx +1 -1
  93. package/src/components/tooltip/Tooltip.tsx +23 -7
  94. package/src/hooks/useAutoDismiss.ts +4 -1
  95. package/src/hooks/useDialogElement.ts +10 -0
  96. package/src/hooks/useFloatingOverlay.ts +12 -2
  97. package/src/hooks/usePagination.ts +6 -2
  98. package/src/index.ts +226 -0
  99. package/src/types.ts +26 -26
  100. package/src/utils/breakpoints.ts +1 -1
  101. package/src/utils/devWarning.ts +58 -0
  102. package/src/utils/direction.ts +14 -0
  103. package/src/utils/focusRedirect.ts +10 -16
  104. package/src/utils/polymorphic.ts +18 -1
  105. package/src/utils/suppressFocusRingGlobally.ts +22 -7
  106. package/src/utils/triggerElement.ts +25 -0
@@ -22,8 +22,12 @@ export interface UsePaginationOptions {
22
22
  }
23
23
 
24
24
  export interface UsePaginationResult<T extends HTMLElement = HTMLButtonElement> {
25
- prevRef: RefObject<T>
26
- nextRef: RefObject<T>
25
+ // `RefObject<T | null>`, not `RefObject<T>`: these come from `useRef<T>(null)`, so `.current`
26
+ // really is null before mount (and whenever the control isn't rendered — a hand-composed
27
+ // pagination may omit Prev/Next entirely). Declaring the non-null form let a consumer write
28
+ // `prevRef.current.focus()` with no type error and crash at runtime.
29
+ prevRef: RefObject<T | null>
30
+ nextRef: RefObject<T | null>
27
31
  handlePrevClick: (event: MouseEvent<T>) => void
28
32
  handleNextClick: (event: MouseEvent<T>) => void
29
33
  }
package/src/index.ts CHANGED
@@ -24,6 +24,16 @@ import { Breadcrumb, BreadcrumbItem } from './components/breadcrumb'
24
24
  import { Button } from './components/button'
25
25
  import { ButtonGroup, ButtonToolbar } from './components/button-group'
26
26
  import { Calendar, RangeCalendar } from './components/calendar'
27
+ import {
28
+ DataGrid,
29
+ DataGridBody,
30
+ DataGridCell,
31
+ DataGridColumn,
32
+ DataGridHeader,
33
+ DataGridRow,
34
+ DataGridSelectAllCell,
35
+ DataGridSelectionCell
36
+ } from './components/datagrid'
27
37
  import {
28
38
  Card,
29
39
  CardBody,
@@ -145,6 +155,14 @@ export {
145
155
  ButtonToolbar,
146
156
  Calendar,
147
157
  RangeCalendar,
158
+ DataGrid,
159
+ DataGridBody,
160
+ DataGridCell,
161
+ DataGridColumn,
162
+ DataGridHeader,
163
+ DataGridRow,
164
+ DataGridSelectAllCell,
165
+ DataGridSelectionCell,
148
166
  Card,
149
167
  CardBody,
150
168
  CardFooter,
@@ -274,3 +292,211 @@ export {
274
292
  useToast,
275
293
  Tooltip
276
294
  }
295
+
296
+ // Type-only public surface. Kept as `export type ... from` re-exports (the same shape
297
+ // `src/hooks/index.ts` uses) rather than the import-then-name-in-the-export-block pattern the
298
+ // value exports above follow — 164 type names would otherwise need 164 more import lines for no
299
+ // added clarity. Every component folder's barrel already exports its own prop types; this block
300
+ // is what actually makes them reachable as `@chassis-ui/react` imports, so a consumer can write
301
+ // `interface MyButtonProps extends ButtonProps` instead of re-deriving it with
302
+ // `ComponentProps<typeof Button>`.
303
+ export type {
304
+ AccordionBodyProps,
305
+ AccordionHeaderProps,
306
+ AccordionItemDef,
307
+ AccordionItemProps,
308
+ AccordionProps
309
+ } from './components/accordion'
310
+ export type {
311
+ AutocompleteGroupProps,
312
+ AutocompleteItemProps,
313
+ AutocompleteProps
314
+ } from './components/autocomplete'
315
+ export type {
316
+ AvatarImageProps,
317
+ AvatarProps,
318
+ AvatarStackItemDef,
319
+ AvatarStackProps
320
+ } from './components/avatar'
321
+ export type { BadgeProps } from './components/badge'
322
+ export type {
323
+ BreadcrumbItemDef,
324
+ BreadcrumbItemProps,
325
+ BreadcrumbProps
326
+ } from './components/breadcrumb'
327
+ export type { ButtonProps } from './components/button'
328
+ export type { ButtonGroupProps, ButtonToolbarProps } from './components/button-group'
329
+ export type {
330
+ CalendarLabels,
331
+ CalendarMultipleProps,
332
+ CalendarProps,
333
+ CalendarSingleProps,
334
+ DateRangePreset,
335
+ RangeCalendarProps
336
+ } from './components/calendar'
337
+ export type {
338
+ CardBodyProps,
339
+ CardFooterProps,
340
+ CardGroupProps,
341
+ CardHeaderProps,
342
+ CardImageOverlayProps,
343
+ CardImageProps,
344
+ CardLinkProps,
345
+ CardProps,
346
+ CardSubtitleProps,
347
+ CardTextProps,
348
+ CardTitleProps
349
+ } from './components/card'
350
+ export type {
351
+ CarouselControlNextProps,
352
+ CarouselControlPrevProps,
353
+ CarouselEnds,
354
+ CarouselIndicatorsProps,
355
+ CarouselInnerProps,
356
+ CarouselItemProps,
357
+ CarouselOverlayProps,
358
+ CarouselPlayPauseProps,
359
+ CarouselProps,
360
+ CarouselSlideDetail,
361
+ CarouselTransition
362
+ } from './components/carousel'
363
+ export type { ButtonObject, CheckboxGroupProps, CheckboxProps } from './components/checkbox'
364
+ export type { ChipProps } from './components/chip'
365
+ export type { ChipInputProps } from './components/chip-input'
366
+ export type { CloseButtonProps } from './components/close-button'
367
+ export type { CollapseProps } from './components/collapse'
368
+ export type { ColorInputProps } from './components/color-input'
369
+ export type { ComboboxGroupProps, ComboboxItemProps, ComboboxProps } from './components/combobox'
370
+ export type {
371
+ DatePickerMultipleProps,
372
+ DatePickerProps,
373
+ DatePickerSingleProps,
374
+ DateRangePickerProps
375
+ } from './components/datepicker'
376
+ export type {
377
+ DrawerBodyProps,
378
+ DrawerFooterProps,
379
+ DrawerHeaderProps,
380
+ DrawerProps,
381
+ DrawerTitleProps
382
+ } from './components/drawer'
383
+ export type { FileInputProps } from './components/file-input'
384
+ export type { FlexProps } from './components/flex'
385
+ export type { FloatingInputProps } from './components/floating-input'
386
+ export type { FormFeedbackProps, FormHelpProps, FormLabelProps, FormProps } from './components/form'
387
+ export type { FormFieldProps } from './components/form-field'
388
+ export type {
389
+ ColProps,
390
+ ContainerProps,
391
+ GridItemLayout,
392
+ GridItemProps,
393
+ GridProps,
394
+ RowProps
395
+ } from './components/grid'
396
+ export type { IconProps } from './components/icon'
397
+ export type { InputAdornProps } from './components/input-adorn'
398
+ export type { InputGroupAddonProps, InputGroupProps } from './components/input-group'
399
+ export type { LinkProps } from './components/link'
400
+ export type { ListItemDef, ListItemProps, ListProps } from './components/list'
401
+ export type {
402
+ MenuAutoClose,
403
+ MenuDividerDef,
404
+ MenuDividerProps,
405
+ MenuFocusStrategy,
406
+ MenuHeaderDef,
407
+ MenuHeaderProps,
408
+ MenuItemDef,
409
+ MenuItemProps,
410
+ MenuItemsDef,
411
+ MenuListProps,
412
+ MenuProps,
413
+ MenuSubmenuBackProps,
414
+ MenuSubmenuProps,
415
+ MenuTextProps,
416
+ MenuToggleProps
417
+ } from './components/menu'
418
+ export type {
419
+ ModalBodyProps,
420
+ ModalFooterProps,
421
+ ModalHeaderProps,
422
+ ModalProps,
423
+ ModalTitleProps
424
+ } from './components/modal'
425
+ export type { NavItemDef, NavLinkProps, NavProps, NavTitleProps } from './components/nav'
426
+ export type {
427
+ NavbarBrandProps,
428
+ NavbarNavProps,
429
+ NavbarProps,
430
+ NavbarTextProps,
431
+ NavbarTogglerProps
432
+ } from './components/navbar'
433
+ export type {
434
+ NotificationContent,
435
+ NotificationIconProps,
436
+ NotificationProps,
437
+ NotificationStackProps,
438
+ NotificationTextProps,
439
+ NotificationTitleProps
440
+ } from './components/notification'
441
+ export type { OtpInputProps } from './components/otp-input'
442
+ export type { PaginationItemProps, PaginationProps } from './components/pagination'
443
+ export type { PasswordStrengthProps } from './components/password-strength'
444
+ export type { PlaceholderProps } from './components/placeholder'
445
+ export type { PopoverProps } from './components/popover'
446
+ export type { ProgressBarProps, ProgressProps } from './components/progress'
447
+ export type { RadioGroupProps, RadioProps } from './components/radio'
448
+ export type { RangeInputProps } from './components/range-input'
449
+ export type { SelectOptionDef, SelectProps } from './components/select'
450
+ export type { SkeletonLoaderProps, SkeletonProps } from './components/skeleton'
451
+ export type { SpinnerProps } from './components/spinner'
452
+ export type { StackProps } from './components/stack'
453
+ export type { StepperItemDef, StepperItemProps, StepperProps } from './components/stepper'
454
+ export type { SwitchProps } from './components/switch'
455
+ export type {
456
+ TableBodyProps,
457
+ TableCellProps,
458
+ TableColumnProps,
459
+ TableHeaderProps,
460
+ TableProps,
461
+ TableRowProps
462
+ } from './components/table'
463
+ export type { TabListProps, TabPanelProps, TabProps, TabsProps } from './components/tabs'
464
+ export type { TextInputProps } from './components/text-input'
465
+ export type { TextareaProps } from './components/textarea'
466
+ export type {
467
+ ToastBodyProps,
468
+ ToastContent,
469
+ ToastFooterProps,
470
+ ToastHeaderProps,
471
+ ToastIconProps,
472
+ ToastProps,
473
+ ToasterProps
474
+ } from './components/toast'
475
+ export type { TooltipProps } from './components/tooltip'
476
+
477
+ // Cross-cutting types that aren't owned by any one component folder: the scales and vocabularies
478
+ // the prop types above are built from. `types.ts`'s `FontFamily`/`FontWeight`/`FontSize`/
479
+ // `Placements`/`TextColor`/`Triggers` are deliberately not re-exported — nothing in this package
480
+ // references them, so publishing them would commit this package's public API to types it doesn't
481
+ // actually use.
482
+ export type {
483
+ Breakpoint,
484
+ ContextColor,
485
+ ContextStyle,
486
+ ExtendedSizing,
487
+ Shapes,
488
+ Sizing,
489
+ Spacing
490
+ } from './types'
491
+ // Backs the `placement` prop on `Menu`/`MenuSubmenu`/`Popover`/`Tooltip`. Lives in `utils/`
492
+ // rather than a component barrel because all four share it.
493
+ export type { Placement } from './utils/overlayPlacement'
494
+ // Result types for the five hooks exported as values above. Mirrors what `hooks/index.ts` itself
495
+ // exposes for them — the other hooks in that folder stay internal, so their types do too.
496
+ export type {
497
+ UseDrawerResult,
498
+ UseModalResult,
499
+ UseNotificationResult,
500
+ UsePaginationResult,
501
+ UseToastResult
502
+ } from './hooks'
package/src/types.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Breakpoints
3
3
  */
4
- export type Breakpoint = 'small' | 'medium' | 'large' | 'xlarge' | '2xlarge'
4
+ export type Breakpoint = 'sm' | 'md' | 'lg' | 'xl' | '2xl'
5
5
 
6
6
  /**
7
7
  * Context colors
@@ -27,12 +27,12 @@ export type ContextStyle = 'basic' | 'solid' | 'outline' | 'smooth'
27
27
  /**
28
28
  * Component sizes
29
29
  */
30
- export type Sizing = 'small' | 'medium' | 'large'
30
+ export type Sizing = 'sm' | 'md' | 'lg'
31
31
 
32
32
  /**
33
33
  * Extended sizes
34
34
  */
35
- export type ExtendedSizing = '2xsmall' | 'xsmall' | Sizing | 'xlarge' | '2xlarge'
35
+ export type ExtendedSizing = '2xs' | 'xs' | Sizing | 'xl' | '2xl'
36
36
 
37
37
  /**
38
38
  * Spacing values. `SPACING` is the runtime source of truth — `Spacing` is derived from it so the
@@ -41,19 +41,19 @@ export type ExtendedSizing = '2xsmall' | 'xsmall' | Sizing | 'xlarge' | '2xlarge
41
41
  */
42
42
  export const SPACING = [
43
43
  'zero',
44
- '4xsmall',
45
- '3xsmall',
46
- '2xsmall',
47
- 'xsmall',
48
- 'small',
49
- 'medium',
50
- 'large',
51
- 'xlarge',
52
- '2xlarge',
53
- '3xlarge',
54
- '4xlarge',
55
- '5xlarge',
56
- '6xlarge'
44
+ '4xs',
45
+ '3xs',
46
+ '2xs',
47
+ 'xs',
48
+ 'sm',
49
+ 'md',
50
+ 'lg',
51
+ 'xl',
52
+ '2xl',
53
+ '3xl',
54
+ '4xl',
55
+ '5xl',
56
+ '6xl'
57
57
  ] as const
58
58
 
59
59
  export type Spacing = (typeof SPACING)[number]
@@ -72,16 +72,16 @@ export type FontWeight = 'elegant' | 'normal' | 'strong' | 'mass'
72
72
  * Font sizes
73
73
  */
74
74
  export type FontSize =
75
- | '2xsmall'
76
- | 'xsmall'
77
- | 'small'
78
- | 'medium'
79
- | 'large'
80
- | 'xlarge'
81
- | '2xlarge'
82
- | '3xlarge'
83
- | '4xlarge'
84
- | '5xlarge'
75
+ | '2xs'
76
+ | 'xs'
77
+ | 'sm'
78
+ | 'md'
79
+ | 'lg'
80
+ | 'xl'
81
+ | '2xl'
82
+ | '3xl'
83
+ | '4xl'
84
+ | '5xl'
85
85
 
86
86
  /**
87
87
  * Component placements
@@ -2,7 +2,7 @@ import { Breakpoint } from '../types'
2
2
 
3
3
  // Mobile-first ascending order. Every responsive layout prop (`Flex`/`Stack`/`Row`/`Col`/
4
4
  // `Skeleton`'s `responsive`) keys its overrides by these literal chassis-css breakpoint names.
5
- export const BREAKPOINTS: Breakpoint[] = ['small', 'medium', 'large', 'xlarge', '2xlarge']
5
+ export const BREAKPOINTS: Breakpoint[] = ['sm', 'md', 'lg', 'xl', '2xl']
6
6
 
7
7
  // A column/width span: a track count, `'auto'` for a natural-width track, or `true` for the bare
8
8
  // (no explicit width) case. Shared by `Col` and `Skeleton`, both of which map it to the same
@@ -0,0 +1,58 @@
1
+ // Development-only misuse warnings, in one place so every component reports misuse the same way
2
+ // and none of it reaches a consumer's production bundle.
3
+ //
4
+ // Two problems this fixes over calling `console.warn` directly, which is what every call site used
5
+ // to do:
6
+ //
7
+ // 1. **They fired in production.** Nothing in this package guarded on `NODE_ENV`, so every
8
+ // `console.warn` ran for end users of a consuming app. `process.env.NODE_ENV` is the idiom
9
+ // bundlers (Vite, webpack, Turbopack, Rollup) replace with a literal, so in a production build
10
+ // `IS_DEV` folds to `false` and no warning is ever emitted. Relying on that substitution adds
11
+ // no new constraint: `react-dom` — a peer dependency, so always present — already requires
12
+ // exactly the same replacement.
13
+ // 2. **They repeated.** Most call sites warn during render, so a warning re-fired on every
14
+ // re-render, and twice per render under StrictMode's double-invoke. De-duplicating on the
15
+ // message means a misuse is reported once and stays readable in the console.
16
+ //
17
+ // What this does *not* do is strip the message strings from the bundle. Measured with esbuild
18
+ // against the built `dist/index.js`: a `NODE_ENV=production` consumer bundle still contains them.
19
+ // `tsdown` minifies this package's own output first (`minify: true`), collapsing each function to
20
+ // a `IS_DEV && cond && ...` expression, and a consumer's minifier won't then inline that call to
21
+ // discover its arguments are droppable. Building unminified would let them drop, at the cost of
22
+ // shipping pretty-printed dist — not worth it for a few hundred gzipped bytes. The behavior that
23
+ // mattered (nothing logged in production, nothing repeated in development) is the part that's
24
+ // fixed; treat the strings as part of the bundle's fixed cost.
25
+ const IS_DEV = process.env.NODE_ENV !== 'production'
26
+
27
+ const warned = new Set<string>()
28
+
29
+ /**
30
+ * Resets the de-duplication set. Test-only — the suite asserts on these warnings, and without a
31
+ * reset between tests the *second* test in a file to trigger the same message would see nothing.
32
+ * Called from a global `beforeEach` in `test/setup.ts`.
33
+ */
34
+ export function resetDevWarnings(): void {
35
+ warned.clear()
36
+ }
37
+
38
+ /**
39
+ * Logs `message` once via `console.warn` when `condition` is true, in development builds only.
40
+ * Use for a recoverable misuse: the component still renders something sensible.
41
+ */
42
+ export function devWarning(condition: boolean, message: string): void {
43
+ if (!IS_DEV) return
44
+ if (!condition || warned.has(message)) return
45
+ warned.add(message)
46
+ console.warn(message)
47
+ }
48
+
49
+ /**
50
+ * Logs `message` once via `console.error` when `condition` is true, in development builds only.
51
+ * Use for a misuse that leaves the component genuinely broken (e.g. a required prop missing).
52
+ */
53
+ export function devError(condition: boolean, message: string): void {
54
+ if (!IS_DEV) return
55
+ if (!condition || warned.has(message)) return
56
+ warned.add(message)
57
+ console.error(message)
58
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Whether `element` renders right-to-left.
3
+ *
4
+ * Reads the *computed* direction rather than react-aria's `useLocale()`. The two are deliberately
5
+ * separate signals in this library (see the Internationalization docs page): `useLocale()` follows
6
+ * `I18nProvider`, and drives locale-dependent behavior like which calendar system to use;
7
+ * everything about visual layout — including which physical arrow key moves "forward" through a
8
+ * horizontally-laid-out control — follows the `dir` attribute, which is what `getComputedStyle`
9
+ * resolves here. A page can set one without the other, and for arrow-key navigation it's the
10
+ * rendered order the user sees that has to match.
11
+ */
12
+ export function isRTL(element: Element): boolean {
13
+ return getComputedStyle(element).direction === 'rtl'
14
+ }
@@ -1,29 +1,23 @@
1
+ import { suppressFocusRing } from './suppressFocusRingGlobally'
2
+
1
3
  // Safari shows a `:focus-visible` ring on an element focused via script even when the
2
4
  // interaction that triggered the script was a real pointer click — unlike Chromium/Firefox,
3
5
  // which correctly suppress it in that case. Force the ring off for a pointer-triggered redirect,
4
6
  // then let normal focus-visible behavior resume the next time this element is genuinely
5
7
  // (re)focused, e.g. via Tab.
8
+ //
9
+ // The suppress/restore cycle itself is `suppressFocusRing`'s, not reimplemented here: it owns the
10
+ // re-entrancy guard that keeps a second call before the element blurs from capturing the
11
+ // already-suppressed value as the one to restore (which left the ring off for good — reachable by
12
+ // clicking twice at the same disabled end of a `ends="stop"` carousel). `boxShadow` is suppressed
13
+ // alongside `outline` here, but not on the global pointerdown path, because a redirect can land
14
+ // on an element whose ring is painted that way.
6
15
  export function focusRedirect(
7
16
  el: HTMLElement | null,
8
17
  viaPointer: boolean,
9
18
  focusOptions?: FocusOptions
10
19
  ): void {
11
20
  if (!el) return
12
- if (!viaPointer) {
13
- el.focus(focusOptions)
14
- return
15
- }
16
- const prevOutline = el.style.outline
17
- const prevBoxShadow = el.style.boxShadow
18
- el.style.outline = 'none'
19
- el.style.boxShadow = 'none'
21
+ if (viaPointer) suppressFocusRing(el, ['outline', 'boxShadow'])
20
22
  el.focus(focusOptions)
21
- el.addEventListener(
22
- 'blur',
23
- () => {
24
- el.style.outline = prevOutline
25
- el.style.boxShadow = prevBoxShadow
26
- },
27
- { once: true }
28
- )
29
23
  }
@@ -3,12 +3,29 @@ import {
3
3
  ComponentPropsWithRef,
4
4
  ElementType,
5
5
  ForwardRefRenderFunction,
6
- forwardRef
6
+ forwardRef,
7
+ Ref
7
8
  } from 'react'
8
9
 
9
10
  // Ref type inferred from a polymorphic component's currently-selected `component` element type.
10
11
  export type PolymorphicRef<C extends ElementType> = ComponentPropsWithRef<C>['ref']
11
12
 
13
+ /**
14
+ * Ref type for a polymorphic component that can also pick its element from a prop *other* than
15
+ * `component` — `Button`, `Chip`, `Avatar`, `NavbarBrand` and `PaginationItem` all render an
16
+ * `<a>` when given `href`, and `PaginationItem` a `<span>` when `active`, none of which moves `C`
17
+ * off its default. A plain `PolymorphicRef<C>` describes only the `component` branch, so a caller
18
+ * holding a ref for the element that actually gets rendered (`<Button href="/x" ref={anchorRef}>`)
19
+ * got a spurious type error while the runtime happily populated it.
20
+ *
21
+ * `Ref` over the *union* rather than a union of `Ref`s is what makes a
22
+ * `createRef<HTMLButtonElement | HTMLAnchorElement>()` assignable here: `RefObject` is invariant
23
+ * in its type argument, so `RefObject<A | B>` satisfies `Ref<A | B>` but neither `Ref<A>` nor
24
+ * `Ref<B>` on its own.
25
+ */
26
+ export type PolymorphicRefWithFallback<C extends ElementType, Fallback extends HTMLElement> =
27
+ PolymorphicRef<C> | Ref<Fallback>
28
+
12
29
  /**
13
30
  * Props for a polymorphic component: `OwnProps` (which must declare `component?: C`) plus
14
31
  * whatever props `C` itself accepts, minus any name already claimed by `OwnProps` so the two
@@ -20,10 +20,22 @@ const FOCUSABLE_SELECTOR = 'a[href], button, input, select, textarea, [tabindex]
20
20
 
21
21
  const suppressed = new WeakSet<HTMLElement>()
22
22
 
23
+ // Which ring-painting style properties a given call suppresses. The global `pointerdown` path
24
+ // only ever needs `outline` (that's the whole of chassis-css's `:focus-visible` ring); a
25
+ // script-driven focus redirect also clears `boxShadow`, since an element it lands on may carry a
26
+ // ring painted that way — see `focusRedirect`.
27
+ export type SuppressibleStyle = 'outline' | 'boxShadow'
28
+
29
+ const DEFAULT_STYLES: readonly SuppressibleStyle[] = ['outline']
30
+
23
31
  // Shared by the global `pointerdown` listener below and by call sites that move focus themselves
24
32
  // via script (e.g. `RangeCalendar`'s hover-to-preview-a-range, which calls `element.focus()` from
25
- // a `pointerenter` handler — outside the `pointerdown` this module otherwise listens for).
26
- export function suppressFocusRing(el: HTMLElement): void {
33
+ // a `pointerenter` handler — outside the `pointerdown` this module otherwise listens for, and
34
+ // `focusRedirect`, which delegates here rather than reimplementing the save/restore cycle).
35
+ export function suppressFocusRing(
36
+ el: HTMLElement,
37
+ styles: readonly SuppressibleStyle[] = DEFAULT_STYLES
38
+ ): void {
27
39
  // chassis-css's `.form-input` placeholder deliberately grants its focus ring on `:focus-within`
28
40
  // as well as `:focus-visible` (see `_form.scss`), unlike the `:focus-visible`-only policy this
29
41
  // module otherwise mirrors for buttons/links/checks/etc. — a form field is meant to show its
@@ -33,17 +45,20 @@ export function suppressFocusRing(el: HTMLElement): void {
33
45
  // an outline chassis-css wants visible the whole time the field is focused.
34
46
  if (el.classList.contains('form-input')) return
35
47
  // `suppressed` guards re-entrancy: a second call for the same element before it blurs (e.g. a
36
- // rapid re-press, or the pointer re-entering a cell) would otherwise stack a second blur
37
- // listener that clobbers the first restoration and leaves the ring permanently suppressed.
48
+ // rapid re-press, the pointer re-entering a cell, or a repeated `focusRedirect` onto the same
49
+ // element) would otherwise stack a second blur listener whose "previous" value it captured
50
+ // *after* the first call already set the property to `none` — restoring `none` and leaving the
51
+ // ring permanently suppressed. One shared set across every caller, so a global-listener
52
+ // suppression and a `focusRedirect` one can't stack on each other either.
38
53
  if (suppressed.has(el)) return
39
54
 
40
55
  suppressed.add(el)
41
- const prevOutline = el.style.outline
42
- el.style.outline = 'none'
56
+ const restore = styles.map((style) => [style, el.style[style]] as const)
57
+ for (const [style] of restore) el.style[style] = 'none'
43
58
  el.addEventListener(
44
59
  'blur',
45
60
  () => {
46
- el.style.outline = prevOutline
61
+ for (const [style, value] of restore) el.style[style] = value
47
62
  suppressed.delete(el)
48
63
  },
49
64
  { once: true }
@@ -0,0 +1,25 @@
1
+ import { HTMLAttributes, ReactElement, Ref, version as reactVersion } from 'react'
2
+
3
+ /**
4
+ * `Popover`/`Tooltip` both take their trigger as a single `ReactElement` child and re-render it
5
+ * with `cloneElement`. A bare `ReactElement` types its props as `unknown`, which makes
6
+ * `cloneElement`'s config `Partial<unknown> & Attributes` — no `ref`, no event handlers, nothing
7
+ * to merge against. This is the narrowed view both components read the child through.
8
+ *
9
+ * Kept internal: each component's own public `children: ReactElement` stays as-is, so a caller
10
+ * can still pass any element without having to satisfy this shape.
11
+ */
12
+ export type TriggerElementProps = HTMLAttributes<HTMLElement> & { ref?: Ref<HTMLElement> }
13
+
14
+ export type TriggerElement = ReactElement<TriggerElementProps>
15
+
16
+ export const asTriggerElement = (element: ReactElement): TriggerElement => element as TriggerElement
17
+
18
+ // React 19 turned `ref` into an ordinary prop; before that it lived on the element itself and was
19
+ // stripped out of `props`. React 19's development build installs a warning getter on
20
+ // `element.ref`, so this reads only the location that's correct for the React actually running
21
+ // rather than probing both and tripping that warning on every trigger that has no ref at all.
22
+ const REF_LIVES_IN_PROPS = parseInt(reactVersion, 10) >= 19
23
+
24
+ export const getTriggerRef = (element: TriggerElement): Ref<HTMLElement> | undefined =>
25
+ REF_LIVES_IN_PROPS ? element.props.ref : (element as { ref?: Ref<HTMLElement> }).ref