@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
@@ -14,6 +14,7 @@ import { useControllableState, useForkedRef, useFormField } from '../../hooks'
14
14
  import { validationClassName } from '../../utils/validationClassName'
15
15
  import { renderFormField } from '../form-field/renderFormField'
16
16
  import { OtpBox } from './OtpBox'
17
+ import { isRTL } from '../../utils/direction'
17
18
 
18
19
  export interface OtpInputProps extends Omit<
19
20
  HTMLAttributes<HTMLDivElement>,
@@ -101,9 +102,9 @@ export interface OtpInputProps extends Omit<
101
102
  */
102
103
  separator?: ReactNode
103
104
  /**
104
- * Size the boxes small or large.
105
+ * Size the boxes sm or lg.
105
106
  */
106
- size?: 'small' | 'large'
107
+ size?: 'sm' | 'lg'
107
108
  /**
108
109
  * Set component validation state to valid.
109
110
  */
@@ -218,17 +219,17 @@ export const OtpInput = forwardRef<HTMLDivElement, OtpInputProps>(
218
219
  commit(nextBoxes)
219
220
  break
220
221
  }
221
- case 'ArrowLeft': {
222
- if (index > 0) {
223
- event.preventDefault()
224
- focusBox(index - 1)
225
- }
226
- break
227
- }
222
+ // Arrow keys move by *visual* direction, so they mirror under RTL: the box to the left of
223
+ // the focused one is the next box, not the previous. `Carousel` and `MenuSubmenu` already
224
+ // mirror their own arrow handling this way; this used to hardcode the LTR mapping and sent
225
+ // an RTL user backwards through the field.
226
+ case 'ArrowLeft':
228
227
  case 'ArrowRight': {
229
- if (index < total - 1) {
228
+ const movesForward = (event.key === 'ArrowRight') !== isRTL(event.currentTarget)
229
+ const nextIndex = movesForward ? index + 1 : index - 1
230
+ if (nextIndex >= 0 && nextIndex < total) {
230
231
  event.preventDefault()
231
- focusBox(index + 1)
232
+ focusBox(nextIndex)
232
233
  }
233
234
  break
234
235
  }
@@ -67,9 +67,9 @@ export interface PaginationProps extends HTMLAttributes<HTMLElement> {
67
67
  */
68
68
  showPrevNext?: boolean
69
69
  /**
70
- * Size the component small or large.
70
+ * Size the component sm or lg.
71
71
  */
72
- size?: 'small' | 'large'
72
+ size?: 'sm' | 'lg'
73
73
  }
74
74
 
75
75
  function getPageRange(activePage: number, pages: number, maxVisible: number): (number | '...')[] {
@@ -13,7 +13,8 @@ import { useButtonSemantics } from '../../hooks'
13
13
  import {
14
14
  createPolymorphicComponent,
15
15
  PolymorphicComponentProps,
16
- PolymorphicRef
16
+ PolymorphicRef,
17
+ PolymorphicRefWithFallback
17
18
  } from '../../utils/polymorphic'
18
19
 
19
20
  type PaginationItemOwnProps<C extends ElementType> = {
@@ -41,7 +42,9 @@ export type PaginationItemProps<C extends ElementType = 'button'> = PolymorphicC
41
42
  >
42
43
 
43
44
  type PaginationItemComponent = (<C extends ElementType = 'button'>(
44
- props: PaginationItemProps<C> & { ref?: PolymorphicRef<C> }
45
+ props: PaginationItemProps<C> & {
46
+ ref?: PolymorphicRefWithFallback<C, HTMLButtonElement | HTMLAnchorElement | HTMLSpanElement>
47
+ }
45
48
  ) => ReactElement | null) & { displayName?: string }
46
49
 
47
50
  function PaginationItemRender<C extends ElementType = 'button'>(
@@ -66,7 +69,15 @@ function PaginationItemRender<C extends ElementType = 'button'>(
66
69
  className
67
70
  )
68
71
 
69
- const Component = (component ?? (active ? 'span' : href ? 'a' : 'button')) as ElementType
72
+ // The active page keeps whatever element it would render anyway (`<button>`, or `<a>` when
73
+ // given `href`) rather than collapsing to a `<span>`. Swapping the tag on activation made React
74
+ // unmount the focused control and mount a different element in its place, so a keyboard user who
75
+ // activated a page was dropped to `<body>` and had to tab in from the top of the document again
76
+ // (WCAG 2.4.3). The `<span>` wasn't even inert: `Pagination`'s smart mode passes an `onClick`,
77
+ // so `useButtonSemantics` gave it `role="button"` and `tabIndex={0}` — a synthetic button,
78
+ // strictly worse than the real one it replaced. Styling is unaffected either way: chassis-css
79
+ // matches `.pagination-link { &.active, .active > & }`, and `active` still lands on the `<li>`.
80
+ const Component = (component ?? (href ? 'a' : 'button')) as ElementType
70
81
 
71
82
  // `<a>` has no real `disabled` attribute, so a disabled anchor pagination item still fires
72
83
  // click (and still navigates) unless it's blocked here, same guard `Button` applies.
@@ -86,8 +97,13 @@ function PaginationItemRender<C extends ElementType = 'button'>(
86
97
  onClick: onClick as MouseEventHandler<HTMLElement> | undefined
87
98
  })
88
99
 
100
+ // `aria-current="page"` goes on the control itself, not the wrapping `<li>` — that's where the
101
+ // WAI-ARIA pagination pattern puts it, and it's what a screen reader conveys when focus lands on
102
+ // the control. On the `<li>` it was attached to a presentational list item nothing focuses.
103
+ const currentProps = active ? { 'aria-current': 'page' as const } : {}
104
+
89
105
  return (
90
- <li className={_className} {...(active && { 'aria-current': 'page' })}>
106
+ <li className={_className}>
91
107
  {Component === 'button' ? (
92
108
  <button
93
109
  {...(rest as Record<string, unknown>)}
@@ -95,6 +111,7 @@ function PaginationItemRender<C extends ElementType = 'button'>(
95
111
  type="button"
96
112
  disabled={disabled}
97
113
  onClick={handleClick}
114
+ {...currentProps}
98
115
  ref={ref as Ref<HTMLButtonElement>}
99
116
  >
100
117
  {children}
@@ -106,6 +123,7 @@ function PaginationItemRender<C extends ElementType = 'button'>(
106
123
  href={href}
107
124
  onClick={handleClick}
108
125
  {...(disabled && { 'aria-disabled': true, tabIndex: -1 })}
126
+ {...currentProps}
109
127
  ref={ref as Ref<HTMLAnchorElement>}
110
128
  >
111
129
  {children}
@@ -118,6 +136,7 @@ function PaginationItemRender<C extends ElementType = 'button'>(
118
136
  unknown
119
137
  >)}
120
138
  {...(!needsButtonSemantics && { onClick: handleClick })}
139
+ {...currentProps}
121
140
  ref={needsButtonSemantics ? forkedRef : ref}
122
141
  >
123
142
  {children}
@@ -4,6 +4,7 @@ import React, {
4
4
  ReactElement,
5
5
  ReactNode,
6
6
  RefObject,
7
+ useCallback,
7
8
  useEffect,
8
9
  useRef
9
10
  } from 'react'
@@ -13,13 +14,19 @@ import { mergeProps, useDialog, useOverlayPosition, useOverlayTrigger } from 're
13
14
  import { useOverlayTriggerState } from 'react-stately'
14
15
  import { Transition } from 'react-transition-group'
15
16
 
16
- import { getOverlayArrowStyle, getOverlayTransitionClass, useFloatingOverlay } from '../../hooks'
17
+ import {
18
+ getOverlayArrowStyle,
19
+ getOverlayTransitionClass,
20
+ useFloatingOverlay,
21
+ useForkedRef
22
+ } from '../../hooks'
17
23
  import { Placement, resolveDataPlacement, toAriaPlacement } from '../../utils/overlayPlacement'
24
+ import { asTriggerElement, getTriggerRef } from '../../utils/triggerElement'
18
25
 
19
26
  interface PopoverPanelProps extends Omit<HTMLAttributes<HTMLDivElement>, 'title' | 'content'> {
20
27
  arrowProps: HTMLAttributes<HTMLDivElement>
21
28
  content: ReactNode | string
22
- overlayRef: RefObject<HTMLDivElement>
29
+ overlayRef: RefObject<HTMLDivElement | null>
23
30
  overlayTriggerProps: HTMLAttributes<HTMLDivElement>
24
31
  title?: ReactNode | string
25
32
  }
@@ -103,6 +110,16 @@ export const Popover: FC<PopoverProps> = ({
103
110
  const triggerRef = useRef<HTMLElement | null>(null)
104
111
  const floatingRef = useRef<HTMLDivElement>(null)
105
112
 
113
+ // `cloneElement`'s config replaces the child's own `ref` outright rather than merging with it,
114
+ // so capturing the trigger node has to be forked with whatever ref the caller already put on
115
+ // that child — otherwise `<Popover><Button ref={mine} /></Popover>` silently never populates
116
+ // `mine`.
117
+ const setTriggerRef = useCallback((node: HTMLElement | null) => {
118
+ triggerRef.current = node
119
+ }, [])
120
+ const triggerElement = asTriggerElement(children)
121
+ const forkedTriggerRef = useForkedRef<HTMLElement>(setTriggerRef, getTriggerRef(triggerElement))
122
+
106
123
  const state = useOverlayTriggerState({ defaultOpen: !!visible })
107
124
  const {
108
125
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -189,15 +206,12 @@ export const Popover: FC<PopoverProps> = ({
189
206
 
190
207
  return (
191
208
  <>
192
- {React.cloneElement(children, {
193
- ref: (node: HTMLElement | null) => {
194
- triggerRef.current = node
195
- },
196
- ...triggerProps,
197
- onClick: (event: React.MouseEvent) => {
198
- children.props.onClick?.(event)
199
- state.toggle()
200
- }
209
+ {/* `mergeProps` chains the child's own `onClick` ahead of this one, so this no longer
210
+ calls `children.props.onClick` itself the way it did when it spread `triggerProps`
211
+ raw — doing both would fire the caller's handler twice per click. */}
212
+ {React.cloneElement(triggerElement, {
213
+ ...mergeProps(triggerElement.props, triggerProps, { onClick: () => state.toggle() }),
214
+ ref: forkedTriggerRef
201
215
  })}
202
216
  {typeof window !== 'undefined' &&
203
217
  createPortal(
@@ -207,9 +221,21 @@ export const Popover: FC<PopoverProps> = ({
207
221
  nodeRef={floatingRef}
208
222
  onExited={() => {
209
223
  const trigger = triggerRef.current
210
- if (trigger && document.contains(trigger)) {
211
- trigger.focus()
212
- }
224
+ if (!trigger || !document.contains(trigger)) return
225
+ // Only reclaim focus when the popover still owns it, or when nothing does.
226
+ // `useDialog` moves focus into the panel on open, so the panel still holds it here
227
+ // for an Escape/inside close (`onExited` runs before `unmountOnExit` detaches the
228
+ // node, so this reads the pre-removal state); an outside click on plain page
229
+ // content instead leaves focus loose on `<body>`. Both are worth restoring. Any
230
+ // other active element means the user deliberately put focus there — dismissing by
231
+ // clicking another control, or the app closing the popover via `visible` while
232
+ // they were typing somewhere else — and this used to steal it straight back off
233
+ // them. Mirrors react-aria's own `shouldRestoreFocus` condition.
234
+ const active = document.activeElement
235
+ const focusIsLoose = !active || active === document.body
236
+ const focusIsInPanel = !!active && !!floatingRef.current?.contains(active)
237
+ if (!focusIsLoose && !focusIsInPanel) return
238
+ trigger.focus()
213
239
  }}
214
240
  timeout={{
215
241
  enter: 0,
@@ -132,7 +132,7 @@ function ProgressRender<C extends ElementType = 'div'>(
132
132
  if (!hasCaption) return progressElement
133
133
 
134
134
  return (
135
- <div className="d-flex flex-column gap-xsmall">
135
+ <div className="d-flex flex-column gap-xs">
136
136
  <div
137
137
  className={classNames('d-flex', label ? 'justify-content-between' : 'justify-content-end')}
138
138
  >
@@ -77,7 +77,7 @@ function ProgressBarRender<C extends ElementType = 'div'>(
77
77
  style={{ width: `${clampedValue}%`, ...style }}
78
78
  ref={ref}
79
79
  >
80
- <span className="mx-2xsmall">{children}</span>
80
+ <span className="mx-2xs">{children}</span>
81
81
  </Component>
82
82
  )
83
83
  }
@@ -34,9 +34,9 @@ export interface RadioProps extends Omit<
34
34
  */
35
35
  label?: ReactNode
36
36
  /**
37
- * Size the component small or large.
37
+ * Size the component sm or lg.
38
38
  */
39
- size?: 'small' | 'large'
39
+ size?: 'sm' | 'lg'
40
40
  /**
41
41
  * The value of the radio button, used to identify it within its `<RadioGroup>`.
42
42
  */
@@ -127,7 +127,7 @@ export const RadioGroup = forwardRef<HTMLFieldSetElement, RadioGroupProps>(
127
127
  {label}
128
128
  </legend>
129
129
  )}
130
- {orientation === 'horizontal' ? <Flex gap="medium">{items}</Flex> : items}
130
+ {orientation === 'horizontal' ? <Flex gap="md">{items}</Flex> : items}
131
131
  {description && <FormHelp {...descriptionProps}>{description}</FormHelp>}
132
132
  {invalid && errorMessage && (
133
133
  <FormFeedback invalid {...errorMessageProps}>
@@ -11,6 +11,7 @@ import classNames from 'classnames'
11
11
  import { useForkedRef, useFormField } from '../../hooks'
12
12
  import { validationClassName } from '../../utils/validationClassName'
13
13
  import { renderFormField } from '../form-field/renderFormField'
14
+ import { devWarning } from '../../utils/devWarning'
14
15
 
15
16
  export interface SelectOptionDef {
16
17
  /**
@@ -98,9 +99,9 @@ export interface SelectProps extends Omit<InputHTMLAttributes<HTMLSelectElement>
98
99
  */
99
100
  placeholder?: string
100
101
  /**
101
- * Size the component small or large.
102
+ * Size the component sm or lg.
102
103
  */
103
- size?: 'small' | 'large'
104
+ size?: 'sm' | 'lg'
104
105
  /**
105
106
  * Set component validation state to valid.
106
107
  */
@@ -151,12 +152,11 @@ export const Select = forwardRef<HTMLSelectElement, SelectProps>(
151
152
  .map((option) => String(option.value ?? ''))
152
153
  : []
153
154
 
154
- if (!multiple && selectedValues.length > 1) {
155
- console.warn(
156
- 'Select: more than one option has `selected: true` but `multiple` is not set — only ' +
157
- 'the last one will be selected, matching native <select> behavior.'
158
- )
159
- }
155
+ devWarning(
156
+ !multiple && selectedValues.length > 1,
157
+ 'Select: more than one option has `selected: true` but `multiple` is not set — only ' +
158
+ 'the last one will be selected, matching native <select> behavior.'
159
+ )
160
160
 
161
161
  // React warns against setting `selected` directly on <option>, recommending `defaultValue`/
162
162
  // `value` on <select> instead — so `options[].selected` is resolved into a `defaultValue`
@@ -44,7 +44,7 @@ type SkeletonOwnProps<C extends ElementType> = {
44
44
  /**
45
45
  * Overrides `span` at a breakpoint and up.
46
46
  *
47
- * @type { Partial<Record<'small' | 'medium' | 'large' | 'xlarge' | '2xlarge', 'auto' | number | string | boolean>> }
47
+ * @type { Partial<Record<'sm' | 'md' | 'lg' | 'xl' | '2xl', 'auto' | number | string | boolean>> }
48
48
  */
49
49
  responsive?: Partial<Record<Breakpoint, Span>>
50
50
  }
@@ -22,9 +22,9 @@ type SpinnerOwnProps<C extends ElementType> = {
22
22
  */
23
23
  component?: C
24
24
  /**
25
- * Size the component small.
25
+ * Size the component sm.
26
26
  */
27
- size?: 'small'
27
+ size?: 'sm'
28
28
  /**
29
29
  * Set the button variant to an outlined button or a ghost button.
30
30
  */
@@ -65,11 +65,11 @@ type StepperOwnProps<C extends ElementType> = {
65
65
  */
66
66
  layout?:
67
67
  | 'horizontal'
68
- | 'small:horizontal'
69
- | 'medium:horizontal'
70
- | 'large:horizontal'
71
- | 'xlarge:horizontal'
72
- | '2xlarge:horizontal'
68
+ | 'sm:horizontal'
69
+ | 'md:horizontal'
70
+ | 'lg:horizontal'
71
+ | 'xl:horizontal'
72
+ | '2xl:horizontal'
73
73
  /**
74
74
  * Wraps the stepper in a horizontally scrollable container so steps keep their natural width
75
75
  * instead of shrinking to fit.
@@ -46,9 +46,9 @@ export interface SwitchProps extends Omit<
46
46
  */
47
47
  onChange?: (isSelected: boolean) => void
48
48
  /**
49
- * Size the component small or large.
49
+ * Size the component sm or lg.
50
50
  */
51
- size?: 'small' | 'large'
51
+ size?: 'sm' | 'lg'
52
52
  /**
53
53
  * Specifies the type of component.
54
54
  */
@@ -91,7 +91,7 @@ interface RenderSwitchInputOptions {
91
91
  inputProps: InputHTMLAttributes<HTMLInputElement>
92
92
  invalid?: boolean
93
93
  label?: ReactNode
94
- size?: 'small' | 'large'
94
+ size?: 'sm' | 'lg'
95
95
  valid?: boolean
96
96
  }
97
97
 
@@ -9,7 +9,7 @@
9
9
 
10
10
  .table-sort-icon {
11
11
  display: inline-block;
12
- margin-inline-start: var(--cx-space-2xsmall);
12
+ margin-inline-start: var(--cx-space-2xs);
13
13
  font-size: .75em;
14
14
  opacity: .6;
15
15
  }
@@ -34,6 +34,6 @@
34
34
  .table [role="gridcell"]:focus-visible,
35
35
  .table [role="columnheader"]:focus-visible,
36
36
  .table [role="rowheader"]:focus-visible {
37
- outline: var(--cx-border-width-xlarge) solid var(--cx-primary);
37
+ outline: var(--cx-border-width-xl) solid var(--cx-primary);
38
38
  outline-offset: -2px;
39
39
  }
@@ -103,7 +103,7 @@ export interface TableProps<T extends object> {
103
103
  /**
104
104
  * Make any table responsive across all viewports or pick a maximum breakpoint.
105
105
  */
106
- responsive?: boolean | 'small' | 'medium' | 'large' | 'xlarge' | '2xlarge'
106
+ responsive?: boolean | 'sm' | 'md' | 'lg' | 'xl' | '2xl'
107
107
  /**
108
108
  * The currently selected row keys (controlled).
109
109
  */
@@ -115,7 +115,7 @@ export interface TableProps<T extends object> {
115
115
  /**
116
116
  * Make table more compact by cutting all cell padding.
117
117
  */
118
- small?: boolean
118
+ sm?: boolean
119
119
  /**
120
120
  * The current sort column and direction.
121
121
  */
@@ -127,7 +127,7 @@ export interface TableProps<T extends object> {
127
127
  * separately, since stacking needs the same `.table-responsive` container-query ancestor.
128
128
  * Labels come from each `TableColumn`'s text (or its `textValue`, for non-text headers).
129
129
  */
130
- stacked?: boolean | 'small' | 'medium' | 'large' | 'xlarge' | '2xlarge'
130
+ stacked?: boolean | 'sm' | 'md' | 'lg' | 'xl' | '2xl'
131
131
  /**
132
132
  * Add zebra-striping to table rows.
133
133
  */
@@ -152,7 +152,7 @@ const TableInner = <T extends object>(
152
152
  responsive,
153
153
  selectedKeys,
154
154
  selectionMode = 'none',
155
- small,
155
+ sm,
156
156
  sortDescriptor,
157
157
  stacked,
158
158
  striped,
@@ -187,7 +187,7 @@ const TableInner = <T extends object>(
187
187
  bordered,
188
188
  borderless,
189
189
  hoverable: hover,
190
- small,
190
+ sm,
191
191
  stacked: stacked === true,
192
192
  striped
193
193
  },
@@ -296,6 +296,12 @@ const TableColumnHeader = <T extends object>({ column, state }: TableColumnHeade
296
296
  {...columnHeaderProps}
297
297
  className={column.props?.className || undefined}
298
298
  colSpan={column.colSpan ?? undefined}
299
+ // react-aria's `useTableColumnHeader` supplies `role="columnheader"` for the ARIA grid
300
+ // pattern but no `scope`, so the native table semantics were left ambiguous — a `<th>` with
301
+ // no `scope` fails WCAG H63, and assistive technology reading the table natively (rather
302
+ // than through the grid roles) has to guess whether the header applies to its column or its
303
+ // row. These are always column headers; the row-header case doesn't exist in this API.
304
+ scope="col"
299
305
  ref={ref}
300
306
  >
301
307
  {column.rendered}
@@ -319,7 +325,7 @@ const TableSelectAllCell = <T extends object>({ column, state }: TableSelectAllC
319
325
  const { checkboxProps } = useTableSelectAllCheckbox(state)
320
326
 
321
327
  return (
322
- <th {...columnHeaderProps} className="table-selection-cell" ref={ref}>
328
+ <th {...columnHeaderProps} className="table-selection-cell" scope="col" ref={ref}>
323
329
  <TableCheckbox checkboxProps={checkboxProps} />
324
330
  </th>
325
331
  )
@@ -63,9 +63,9 @@ export interface TextInputProps extends Omit<
63
63
  */
64
64
  readOnly?: boolean
65
65
  /**
66
- * Size the component small or large.
66
+ * Size the component sm or lg.
67
67
  */
68
- size?: 'small' | 'large'
68
+ size?: 'sm' | 'lg'
69
69
  /**
70
70
  * Specifies the type of component. For `color` or `file` inputs, use the dedicated `ColorInput` or `FileInput` components instead.
71
71
  */
@@ -55,9 +55,9 @@ export interface TextareaProps extends Omit<
55
55
  */
56
56
  rows?: number
57
57
  /**
58
- * Size the component small or large.
58
+ * Size the component sm or lg.
59
59
  */
60
- size?: 'small' | 'large'
60
+ size?: 'sm' | 'lg'
61
61
  /**
62
62
  * Set component validation state to valid.
63
63
  */
@@ -47,7 +47,7 @@ function ToastBodyRender<C extends ElementType = 'div'>(
47
47
  const Component = component ?? 'div'
48
48
  const _className = classNames(
49
49
  'toast-body',
50
- { 'd-flex align-items-start justify-content-between gap-small': closeButton },
50
+ { 'd-flex align-items-start justify-content-between gap-sm': closeButton },
51
51
  className
52
52
  )
53
53
  return (
@@ -81,11 +81,11 @@ function ToastHeaderRender<C extends ElementType = 'div'>(
81
81
  <Component className={_className} {...rest} ref={ref}>
82
82
  {icon != null &&
83
83
  (typeof icon === 'string' ? (
84
- <span aria-hidden="true" className="me-small">
84
+ <span aria-hidden="true" className="me-sm">
85
85
  <ToastIcon name={icon} />
86
86
  </span>
87
87
  ) : (
88
- <span className="me-small">{icon}</span>
88
+ <span className="me-sm">{icon}</span>
89
89
  ))}
90
90
  {children != null && (
91
91
  <strong id={titleId} className="me-auto">
@@ -43,7 +43,7 @@ export const Toaster = forwardRef<HTMLDivElement, ToasterProps>(
43
43
  const { forkedRef, regionProps, state } = useToastRegionQueue(toastQueue, ref)
44
44
 
45
45
  const _className = classNames(
46
- 'toaster toast-container p-medium',
46
+ 'toaster toast-container p-md',
47
47
  {
48
48
  'position-fixed': placement,
49
49
  'position-static': !placement,
@@ -1,12 +1,18 @@
1
- import React, { FC, ReactElement, ReactNode, useRef } from 'react'
1
+ import React, { FC, ReactElement, ReactNode, useCallback, useRef } from 'react'
2
2
  import { createPortal } from 'react-dom'
3
3
  import classNames from 'classnames'
4
4
  import { mergeProps, useOverlayPosition, useTooltip, useTooltipTrigger } from 'react-aria'
5
5
  import { useTooltipTriggerState } from 'react-stately'
6
6
  import { Transition } from 'react-transition-group'
7
7
 
8
- import { getOverlayArrowStyle, getOverlayTransitionClass, useFloatingOverlay } from '../../hooks'
8
+ import {
9
+ getOverlayArrowStyle,
10
+ getOverlayTransitionClass,
11
+ useFloatingOverlay,
12
+ useForkedRef
13
+ } from '../../hooks'
9
14
  import { Placement, resolveDataPlacement, toAriaPlacement } from '../../utils/overlayPlacement'
15
+ import { asTriggerElement, getTriggerRef } from '../../utils/triggerElement'
10
16
 
11
17
  export type { Placement }
12
18
 
@@ -59,6 +65,18 @@ export const Tooltip: FC<TooltipProps> = ({
59
65
  const triggerRef = useRef<HTMLElement | null>(null)
60
66
  const floatingRef = useRef<HTMLDivElement>(null)
61
67
 
68
+ // `cloneElement`'s config replaces the child's own `ref` outright rather than merging with it,
69
+ // so capturing the trigger node has to be forked with whatever ref the caller already put on
70
+ // that child — otherwise `<Tooltip><Button ref={mine} /></Tooltip>` silently never populates
71
+ // `mine`. Same reasoning for merging (rather than overwriting) the child's own props below:
72
+ // `triggerProps` carries the focus/hover handlers this component needs, and spreading them raw
73
+ // would drop a handler the caller put on their own trigger.
74
+ const setTriggerRef = useCallback((node: HTMLElement | null) => {
75
+ triggerRef.current = node
76
+ }, [])
77
+ const triggerElement = asTriggerElement(children)
78
+ const forkedTriggerRef = useForkedRef<HTMLElement>(setTriggerRef, getTriggerRef(triggerElement))
79
+
62
80
  // react-stately defaults to a 1500ms warmup delay (and 500ms cooldown) before a first tooltip
63
81
  // shows, spectrum-style — chassis-css's own JS plugin defaults to instant (`delay: 0`), so
64
82
  // match that here rather than leaving new adopters to wonder why the first hover lags.
@@ -107,11 +125,9 @@ export const Tooltip: FC<TooltipProps> = ({
107
125
 
108
126
  return (
109
127
  <>
110
- {React.cloneElement(children, {
111
- ref: (node: HTMLElement | null) => {
112
- triggerRef.current = node
113
- },
114
- ...triggerProps
128
+ {React.cloneElement(triggerElement, {
129
+ ...mergeProps(triggerElement.props, triggerProps),
130
+ ref: forkedTriggerRef
115
131
  })}
116
132
  {typeof window !== 'undefined' &&
117
133
  createPortal(
@@ -36,7 +36,10 @@ export function useAutoDismiss({
36
36
  visible,
37
37
  onHide
38
38
  }: UseAutoDismissOptions): UseAutoDismissResult {
39
- const timeout = useRef<number>()
39
+ // `useRef<number>()` (no argument) isn't valid under @types/react 19 — the overload requires
40
+ // an initial value, and the ref has to admit `undefined` for `clearAutoDismissTimeout` to reset
41
+ // it back to "no timer scheduled".
42
+ const timeout = useRef<number | undefined>(undefined)
40
43
  const hasMouseInteraction = useRef(false)
41
44
  const hasKeyboardInteraction = useRef(false)
42
45
 
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  ForwardedRef,
3
3
  MouseEvent as ReactMouseEvent,
4
+ MouseEventHandler,
4
5
  SyntheticEvent,
5
6
  useEffect,
6
7
  useRef,
@@ -38,6 +39,13 @@ export interface UseDialogElementOptions {
38
39
  * show effect, used to auto-close every other open drawer first.
39
40
  */
40
41
  onBeforeShow?: (dialog: HTMLDialogElement) => void
42
+ /**
43
+ * The caller's own `onClick`, chained ahead of the backdrop-click handling below rather than
44
+ * left in the props spread — a `<dialog>` needs `onClick` for its own backdrop detection, so
45
+ * spreading the caller's alongside it silently dropped one of the two. Fires for every click
46
+ * on the dialog, not just backdrop ones.
47
+ */
48
+ onClick?: MouseEventHandler<HTMLDialogElement>
41
49
  onClose?: () => void
42
50
  onClosePrevented?: () => void
43
51
  onHidden?: () => void
@@ -62,6 +70,7 @@ export const useDialogElement = ({
62
70
  isModal,
63
71
  keyboard = true,
64
72
  onBeforeShow,
73
+ onClick,
65
74
  onClose,
66
75
  onClosePrevented,
67
76
  onHidden,
@@ -204,6 +213,7 @@ export const useDialogElement = ({
204
213
  }
205
214
 
206
215
  const handleBackdropClick = (event: ReactMouseEvent<HTMLDialogElement>) => {
216
+ onClick?.(event)
207
217
  if (event.target !== dialogRef.current || !openedAsModalRef.current) return
208
218
  if (backdrop === 'static') {
209
219
  triggerStaticBounce()
@@ -60,11 +60,21 @@ export function useFloatingOverlay({
60
60
  else close()
61
61
  }, [visible])
62
62
 
63
+ // `onShow`/`onHide` report *transitions*, so neither fires for the initial commit — mounting
64
+ // closed is not a hide. Without this guard the `else` branch ran on mount and every
65
+ // `<Popover onHide>`/`<Tooltip onHide>` on the page reported a close that never happened,
66
+ // before `onShow` had ever fired once. The portal container still resolves on mount, since
67
+ // an overlay mounted already-open needs one immediately.
68
+ const mountedRef = useRef(false)
69
+
63
70
  useEffect(() => {
71
+ const isInitialCommit = !mountedRef.current
72
+ mountedRef.current = true
73
+
64
74
  if (isOpen) {
65
75
  setPortalContainer(resolvePortalContainer())
66
- onShow?.()
67
- } else {
76
+ if (!isInitialCommit) onShow?.()
77
+ } else if (!isInitialCommit) {
68
78
  onHide?.()
69
79
  }
70
80
  }, [isOpen])