@react-spectrum/s2 3.0.0-nightly-5ed06068e-241105 → 3.0.0-nightly-09ccc53e7-241107

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 (113) hide show
  1. package/dist/ActionButton.cjs +54 -11
  2. package/dist/ActionButton.cjs.map +1 -1
  3. package/dist/ActionButton.css +47 -3
  4. package/dist/ActionButton.css.map +1 -1
  5. package/dist/ActionButton.mjs +55 -12
  6. package/dist/ActionButton.mjs.map +1 -1
  7. package/dist/ActionButtonGroup.cjs +77 -0
  8. package/dist/ActionButtonGroup.cjs.map +1 -0
  9. package/dist/ActionButtonGroup.css +44 -0
  10. package/dist/ActionButtonGroup.css.map +1 -0
  11. package/dist/ActionButtonGroup.mjs +70 -0
  12. package/dist/ActionButtonGroup.mjs.map +1 -0
  13. package/dist/Badge.cjs +42 -42
  14. package/dist/Badge.css +35 -35
  15. package/dist/Badge.mjs +42 -42
  16. package/dist/Card.cjs +1 -1
  17. package/dist/Card.css +1 -1
  18. package/dist/Card.mjs +1 -1
  19. package/dist/Content.cjs +1 -0
  20. package/dist/Content.cjs.map +1 -1
  21. package/dist/Content.mjs +1 -0
  22. package/dist/Content.mjs.map +1 -1
  23. package/dist/ContextualHelp.cjs +1 -1
  24. package/dist/ContextualHelp.cjs.map +1 -1
  25. package/dist/ContextualHelp.css +32 -16
  26. package/dist/ContextualHelp.css.map +1 -1
  27. package/dist/ContextualHelp.mjs +1 -1
  28. package/dist/ContextualHelp.mjs.map +1 -1
  29. package/dist/IllustratedMessage.cjs +6 -2
  30. package/dist/IllustratedMessage.cjs.map +1 -1
  31. package/dist/IllustratedMessage.css +2 -2
  32. package/dist/IllustratedMessage.css.map +1 -1
  33. package/dist/IllustratedMessage.mjs +6 -2
  34. package/dist/IllustratedMessage.mjs.map +1 -1
  35. package/dist/InlineAlert.cjs +8 -8
  36. package/dist/InlineAlert.css +6 -6
  37. package/dist/InlineAlert.mjs +8 -8
  38. package/dist/Meter.cjs +4 -17
  39. package/dist/Meter.cjs.map +1 -1
  40. package/dist/Meter.css +7 -19
  41. package/dist/Meter.css.map +1 -1
  42. package/dist/Meter.mjs +4 -17
  43. package/dist/Meter.mjs.map +1 -1
  44. package/dist/Modal.cjs +1 -1
  45. package/dist/Modal.css +1 -1
  46. package/dist/Modal.mjs +1 -1
  47. package/dist/Picker.cjs +3 -0
  48. package/dist/Picker.cjs.map +1 -1
  49. package/dist/Picker.css +12 -0
  50. package/dist/Picker.css.map +1 -1
  51. package/dist/Picker.mjs +3 -0
  52. package/dist/Picker.mjs.map +1 -1
  53. package/dist/Popover.cjs +1 -1
  54. package/dist/Popover.css +1 -1
  55. package/dist/Popover.mjs +1 -1
  56. package/dist/ProgressBar.cjs +19 -3
  57. package/dist/ProgressBar.cjs.map +1 -1
  58. package/dist/ProgressBar.css +14 -0
  59. package/dist/ProgressBar.css.map +1 -1
  60. package/dist/ProgressBar.mjs +19 -3
  61. package/dist/ProgressBar.mjs.map +1 -1
  62. package/dist/Provider.cjs +1 -1
  63. package/dist/Provider.css +1 -1
  64. package/dist/Provider.mjs +1 -1
  65. package/dist/Radio.cjs +2 -2
  66. package/dist/Radio.cjs.map +1 -1
  67. package/dist/Radio.css +4 -8
  68. package/dist/Radio.css.map +1 -1
  69. package/dist/Radio.mjs +2 -2
  70. package/dist/Radio.mjs.map +1 -1
  71. package/dist/SegmentedControl.cjs +76 -62
  72. package/dist/SegmentedControl.cjs.map +1 -1
  73. package/dist/SegmentedControl.css +110 -69
  74. package/dist/SegmentedControl.css.map +1 -1
  75. package/dist/SegmentedControl.mjs +77 -63
  76. package/dist/SegmentedControl.mjs.map +1 -1
  77. package/dist/ToggleButton.cjs +13 -6
  78. package/dist/ToggleButton.cjs.map +1 -1
  79. package/dist/ToggleButton.css +16 -0
  80. package/dist/ToggleButton.css.map +1 -1
  81. package/dist/ToggleButton.mjs +14 -7
  82. package/dist/ToggleButton.mjs.map +1 -1
  83. package/dist/ToggleButtonGroup.cjs +54 -0
  84. package/dist/ToggleButtonGroup.cjs.map +1 -0
  85. package/dist/ToggleButtonGroup.mjs +48 -0
  86. package/dist/ToggleButtonGroup.mjs.map +1 -0
  87. package/dist/main.cjs +8 -0
  88. package/dist/main.cjs.map +1 -1
  89. package/dist/module.mjs +5 -1
  90. package/dist/module.mjs.map +1 -1
  91. package/dist/types.d.ts +57 -12
  92. package/dist/types.d.ts.map +1 -1
  93. package/package.json +17 -17
  94. package/src/ActionButton.tsx +88 -8
  95. package/src/ActionButtonGroup.tsx +106 -0
  96. package/src/Content.tsx +2 -1
  97. package/src/ContextualHelp.tsx +1 -1
  98. package/src/IllustratedMessage.tsx +1 -3
  99. package/src/Meter.tsx +4 -4
  100. package/src/Picker.tsx +10 -1
  101. package/src/ProgressBar.tsx +20 -3
  102. package/src/Radio.tsx +1 -3
  103. package/src/SegmentedControl.tsx +56 -45
  104. package/src/ToggleButton.tsx +23 -7
  105. package/src/ToggleButtonGroup.tsx +55 -0
  106. package/src/index.ts +4 -0
  107. package/style/dist/spectrum-theme.cjs +5 -0
  108. package/style/dist/spectrum-theme.cjs.map +1 -1
  109. package/style/dist/spectrum-theme.mjs +5 -0
  110. package/style/dist/spectrum-theme.mjs.map +1 -1
  111. package/style/dist/types.d.ts +2 -2
  112. package/style/dist/types.d.ts.map +1 -1
  113. package/style/spectrum-theme.ts +5 -0
@@ -10,8 +10,9 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
+ import {ActionButtonGroupContext} from './ActionButtonGroup';
13
14
  import {baseColor, focusRing, fontRelative, style} from '../style' with { type: 'macro' };
14
- import {ButtonProps, ButtonRenderProps, ContextValue, OverlayTriggerStateContext, Provider, Button as RACButton} from 'react-aria-components';
15
+ import {ButtonProps, ButtonRenderProps, ContextValue, OverlayTriggerStateContext, Provider, Button as RACButton, useSlottedContext} from 'react-aria-components';
15
16
  import {centerBaseline} from './CenterBaseline';
16
17
  import {createContext, forwardRef, ReactNode, useContext} from 'react';
17
18
  import {FocusableRef, FocusableRefValue} from '@react-types/shared';
@@ -44,18 +45,32 @@ interface ToggleButtonStyleProps {
44
45
  isEmphasized?: boolean
45
46
  }
46
47
 
48
+ interface ActionGroupItemStyleProps {
49
+ density?: 'regular' | 'compact',
50
+ orientation?: 'horizontal' | 'vertical',
51
+ isJustified?: boolean
52
+ }
53
+
47
54
  export interface ActionButtonProps extends Omit<ButtonProps, 'className' | 'style' | 'children' | 'onHover' | 'onHoverStart' | 'onHoverEnd' | 'onHoverChange' | 'isPending'>, StyleProps, ActionButtonStyleProps {
48
55
  /** The content to display in the ActionButton. */
49
56
  children?: ReactNode
50
57
  }
51
58
 
52
59
  // These styles handle both ActionButton and ToggleButton
53
- export const btnStyles = style<ButtonRenderProps & ActionButtonStyleProps & ToggleButtonStyleProps>({
60
+ const iconOnly = ':has([slot=icon]):not(:has([data-rsp-slot=text]))';
61
+ export const btnStyles = style<ButtonRenderProps & ActionButtonStyleProps & ToggleButtonStyleProps & ActionGroupItemStyleProps>({
54
62
  ...focusRing(),
55
63
  display: 'flex',
56
64
  alignItems: 'center',
57
65
  justifyContent: 'center',
58
66
  columnGap: 'text-to-visual',
67
+ flexShrink: 0,
68
+ flexGrow: {
69
+ isJustified: 1
70
+ },
71
+ flexBasis: {
72
+ isJustified: 0
73
+ },
59
74
  font: 'control',
60
75
  fontWeight: 'medium',
61
76
  userSelect: 'none',
@@ -159,17 +174,69 @@ export const btnStyles = style<ButtonRenderProps & ActionButtonStyleProps & Togg
159
174
  borderStyle: 'none',
160
175
  paddingX: {
161
176
  default: 'edge-to-text',
162
- ':has([slot=icon]:only-child)': 0
177
+ [iconOnly]: 0
163
178
  },
164
179
  paddingY: 0,
165
- borderRadius: 'control',
180
+ borderTopStartRadius: {
181
+ default: 'control',
182
+ density: {
183
+ compact: {
184
+ default: 'none',
185
+ ':first-child': 'control'
186
+ }
187
+ }
188
+ },
189
+ borderTopEndRadius: {
190
+ default: 'control',
191
+ density: {
192
+ compact: {
193
+ default: 'none',
194
+ orientation: {
195
+ horizontal: {
196
+ ':last-child': 'control'
197
+ },
198
+ vertical: {
199
+ ':first-child': 'control'
200
+ }
201
+ }
202
+ }
203
+ }
204
+ },
205
+ borderBottomStartRadius: {
206
+ default: 'control',
207
+ density: {
208
+ compact: {
209
+ default: 'none',
210
+ orientation: {
211
+ horizontal: {
212
+ ':first-child': 'control'
213
+ },
214
+ vertical: {
215
+ ':last-child': 'control'
216
+ }
217
+ }
218
+ }
219
+ }
220
+ },
221
+ borderBottomEndRadius: {
222
+ default: 'control',
223
+ density: {
224
+ compact: {
225
+ default: 'none',
226
+ ':last-child': 'control'
227
+ }
228
+ }
229
+ },
166
230
  '--iconMargin': {
167
231
  type: 'marginTop',
168
232
  value: {
169
233
  default: fontRelative(-2),
170
- ':has([slot=icon]:only-child)': 0
234
+ [iconOnly]: 0
171
235
  }
172
236
  },
237
+ zIndex: {
238
+ isFocusVisible: 2
239
+ },
173
240
  disableTapHighlight: true
174
241
  }, getAllowedOverrides());
175
242
 
@@ -180,19 +247,32 @@ function ActionButton(props: ActionButtonProps, ref: FocusableRef<HTMLButtonElem
180
247
  props = useFormProps(props as any);
181
248
  let domRef = useFocusableRef(ref);
182
249
  let overlayTriggerState = useContext(OverlayTriggerStateContext);
250
+ let {
251
+ density = 'regular',
252
+ isJustified,
253
+ orientation = 'horizontal',
254
+ staticColor = props.staticColor,
255
+ isQuiet = props.isQuiet,
256
+ size = props.size || 'M',
257
+ isDisabled = props.isDisabled
258
+ } = useSlottedContext(ActionButtonGroupContext) || {};
183
259
 
184
260
  return (
185
261
  <RACButton
186
262
  {...props}
263
+ isDisabled={isDisabled}
187
264
  ref={domRef}
188
265
  style={pressScale(domRef, props.UNSAFE_style)}
189
266
  className={renderProps => (props.UNSAFE_className || '') + btnStyles({
190
267
  ...renderProps,
191
268
  // Retain hover styles when an overlay is open.
192
269
  isHovered: renderProps.isHovered || overlayTriggerState?.isOpen || false,
193
- staticColor: props.staticColor,
194
- size: props.size || 'M',
195
- isQuiet: props.isQuiet
270
+ staticColor,
271
+ size,
272
+ isQuiet,
273
+ density,
274
+ isJustified,
275
+ orientation
196
276
  }, props.styles)}>
197
277
  <Provider
198
278
  values={[
@@ -0,0 +1,106 @@
1
+ /*
2
+ * Copyright 2024 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ import {ContextValue, SlotProps, Toolbar} from 'react-aria-components';
14
+ import {createContext, ForwardedRef, forwardRef, ReactNode} from 'react';
15
+ import {getAllowedOverrides, StylesPropWithHeight, UnsafeStyles} from './style-utils' with {type: 'macro'};
16
+ import {style} from '../style' with {type: 'macro'};
17
+ import {useSpectrumContextProps} from './useSpectrumContextProps';
18
+
19
+ export interface ActionButtonGroupProps extends UnsafeStyles, SlotProps {
20
+ /** Spectrum-defined styles, returned by the `style()` macro. */
21
+ styles?: StylesPropWithHeight,
22
+ /** The children of the group. */
23
+ children: ReactNode,
24
+ /**
25
+ * Size of the buttons.
26
+ * @default "M"
27
+ */
28
+ size?: 'XS' | 'S' | 'M' | 'L' | 'XL',
29
+ /**
30
+ * Spacing between the buttons.
31
+ * @default "regular"
32
+ */
33
+ density?: 'compact' | 'regular',
34
+ /** Whether the button should be displayed with a [quiet style](https://spectrum.adobe.com/page/action-button/#Quiet). */
35
+ isQuiet?: boolean,
36
+ /** Whether the buttons should divide the container width equally. */
37
+ isJustified?: boolean,
38
+ /** Whether the button should be displayed with an [emphasized style](https://spectrum.adobe.com/page/action-button/#Emphasis). */
39
+ staticColor?: 'white' | 'black',
40
+ /**
41
+ * The axis the group should align with.
42
+ * @default 'horizontal'
43
+ */
44
+ orientation?: 'horizontal' | 'vertical',
45
+ /** Whether the group is disabled. */
46
+ isDisabled?: boolean
47
+ }
48
+
49
+ export const actionGroupStyle = style({
50
+ display: 'flex',
51
+ flexDirection: {
52
+ orientation: {
53
+ horizontal: 'row',
54
+ vertical: 'column'
55
+ }
56
+ },
57
+ gap: {
58
+ density: {
59
+ compact: 2,
60
+ regular: {
61
+ size: {
62
+ XS: 4,
63
+ S: 4,
64
+ M: 8,
65
+ L: 8,
66
+ XL: 8
67
+ }
68
+ }
69
+ }
70
+ }
71
+ }, getAllowedOverrides({height: true}));
72
+
73
+
74
+ export const ActionButtonGroupContext = createContext<ContextValue<ActionButtonGroupProps, HTMLDivElement>>(null);
75
+
76
+ function ActionButtonGroup(props: ActionButtonGroupProps, ref: ForwardedRef<HTMLDivElement>) {
77
+ [props, ref] = useSpectrumContextProps(props, ref, ActionButtonGroupContext);
78
+ let {
79
+ density = 'regular',
80
+ size = 'M',
81
+ orientation = 'horizontal',
82
+ isJustified,
83
+ children,
84
+ UNSAFE_className = '',
85
+ UNSAFE_style,
86
+ styles
87
+ } = props;
88
+
89
+ return (
90
+ <Toolbar
91
+ {...props}
92
+ ref={ref}
93
+ style={UNSAFE_style}
94
+ className={UNSAFE_className + actionGroupStyle({size, density, orientation, isJustified}, styles)}>
95
+ <ActionButtonGroupContext.Provider value={props}>
96
+ {children}
97
+ </ActionButtonGroupContext.Provider>
98
+ </Toolbar>
99
+ );
100
+ }
101
+
102
+ /**
103
+ * An ActionButtonGroup is a grouping of related ActionButtons.
104
+ */
105
+ const _ActionButtonGroup = forwardRef(ActionButtonGroup);
106
+ export {_ActionButtonGroup as ActionButtonGroup};
package/src/Content.tsx CHANGED
@@ -120,7 +120,8 @@ function Text(props: ContentProps, ref: DOMRef) {
120
120
  inert={isSkeleton ? 'true' : undefined}
121
121
  className={UNSAFE_className + styles}
122
122
  style={UNSAFE_style}
123
- slot={slot || undefined}>
123
+ slot={slot || undefined}
124
+ data-rsp-slot="text">
124
125
  {children}
125
126
  </TextAria>
126
127
  );
@@ -101,7 +101,7 @@ function ContextualHelp(props: ContextualHelpProps, ref: FocusableRef<HTMLButton
101
101
  crossOffset={crossOffset}
102
102
  hideArrow
103
103
  UNSAFE_className={popover}>
104
- <RACDialog className={mergeStyles(dialogInner, style({borderRadius: 'none'}))}>
104
+ <RACDialog className={mergeStyles(dialogInner, style({borderRadius: 'none', margin: -24, padding: 24}))}>
105
105
  <Provider
106
106
  values={[
107
107
  [TextContext, {
@@ -114,9 +114,7 @@ const illustration = style<IllustratedMessageStyleProps & {isInDropZone?: boolea
114
114
  '--iconPrimary': {
115
115
  type: 'color',
116
116
  value: {
117
- // TODO: ask design about what the color should be. Says gray-800 in the designs file, neutral in token spec, but different neutral in dropzone spec
118
- default: 'gray-800',
119
- isInDropZone: 'gray-500', // neutral doesn't seem to match the color in designs, opted for gray-500 instead
117
+ default: 'neutral',
120
118
  isDropTarget: 'accent'
121
119
  }
122
120
  }
package/src/Meter.tsx CHANGED
@@ -82,11 +82,11 @@ const fillStyles = style<MeterStyleProps>({
82
82
  borderStyle: 'none',
83
83
  borderRadius: 'full',
84
84
  backgroundColor: {
85
- default: 'informative',
85
+ default: 'informative-visual',
86
86
  variant: {
87
- positive: 'positive',
88
- notice: 'notice',
89
- negative: 'negative'
87
+ positive: 'positive-visual',
88
+ notice: 'notice-visual',
89
+ negative: 'negative-visual'
90
90
  },
91
91
  staticColor: {
92
92
  white: {
package/src/Picker.tsx CHANGED
@@ -122,7 +122,16 @@ const inputButton = style<PickerButtonProps | AriaSelectRenderProps>({
122
122
  font: 'control',
123
123
  display: 'flex',
124
124
  textAlign: 'start',
125
- borderStyle: 'none',
125
+ borderStyle: {
126
+ default: 'none',
127
+ forcedColors: 'solid'
128
+ },
129
+ borderColor: {
130
+ forcedColors: {
131
+ default: 'ButtonText',
132
+ isDisabled: 'GrayText'
133
+ }
134
+ },
126
135
  borderRadius: 'control',
127
136
  alignItems: 'center',
128
137
  height: 'control',
@@ -24,6 +24,7 @@ import {keyframes} from '../style/style-macro' with {type: 'macro'};
24
24
  import {mergeStyles} from '../style/runtime';
25
25
  import {size, style} from '../style' with {type: 'macro'};
26
26
  import {useDOMRef} from '@react-spectrum/utils';
27
+ import {useLocale} from '@react-aria/i18n';
27
28
  import {useSpectrumContextProps} from './useSpectrumContextProps';
28
29
 
29
30
  interface ProgressBarStyleProps {
@@ -56,7 +57,7 @@ export interface ProgressBarProps extends Omit<AriaProgressBarProps, 'children'
56
57
 
57
58
  export const ProgressBarContext = createContext<ContextValue<ProgressBarProps, DOMRefValue<HTMLDivElement>>>(null);
58
59
 
59
- const indeterminate = keyframes(`
60
+ const indeterminateLTR = keyframes(`
60
61
  0% {
61
62
  transform: translateX(-70%) scaleX(0.7);
62
63
  }
@@ -65,6 +66,15 @@ const indeterminate = keyframes(`
65
66
  }
66
67
  `);
67
68
 
69
+ const indeterminateRTL = keyframes(`
70
+ 0% {
71
+ transform: translateX(100%) scaleX(0.7);
72
+ }
73
+ 100% {
74
+ transform: translateX(-70%) scaleX(0.7);
75
+ }
76
+ `);
77
+
68
78
  const wrapper = style({
69
79
  ...bar(),
70
80
  gridTemplateColumns: {
@@ -151,7 +161,12 @@ const fill = style<ProgressBarStyleProps>({
151
161
  });
152
162
 
153
163
  const indeterminateAnimation = style({
154
- animation: indeterminate,
164
+ animation: {
165
+ direction: {
166
+ ltr: indeterminateLTR,
167
+ rtl: indeterminateRTL
168
+ }
169
+ },
155
170
  animationDuration: 1000,
156
171
  animationIterationCount: 'infinite',
157
172
  animationTimingFunction: 'in-out',
@@ -170,6 +185,8 @@ function ProgressBar(props: ProgressBarProps, ref: DOMRef<HTMLDivElement>) {
170
185
  UNSAFE_className = ''
171
186
  } = props;
172
187
  let domRef = useDOMRef(ref);
188
+ let {direction} = useLocale();
189
+
173
190
  return (
174
191
  <AriaProgressBar
175
192
  {...props}
@@ -182,7 +199,7 @@ function ProgressBar(props: ProgressBarProps, ref: DOMRef<HTMLDivElement>) {
182
199
  {label && !isIndeterminate && <span className={valueStyles({size, labelAlign: 'end', staticColor})}>{valueText}</span>}
183
200
  <div className={trackStyles({...props})}>
184
201
  <div
185
- className={mergeStyles(fill({...props, staticColor}), (isIndeterminate ? indeterminateAnimation : null))}
202
+ className={mergeStyles(fill({...props, staticColor}), (isIndeterminate ? indeterminateAnimation({direction}) : null))}
186
203
  style={{width: isIndeterminate ? undefined : percentage + '%'}} />
187
204
  </div>
188
205
  </>
package/src/Radio.tsx CHANGED
@@ -74,9 +74,7 @@ const circle = style<RenderProps>({
74
74
  display: 'flex',
75
75
  alignItems: 'center',
76
76
  justifyContent: 'center',
77
- transition: '[border-width]',
78
- transitionDuration: 250,
79
- transitionTimingFunction: 'in-out',
77
+ transition: 'default',
80
78
  borderRadius: 'full',
81
79
  borderStyle: 'solid',
82
80
  boxSizing: 'border-box',
@@ -10,9 +10,9 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import {AriaLabelingProps, DOMRef, DOMRefValue, FocusableRef} from '@react-types/shared';
13
+ import {AriaLabelingProps, DOMRef, DOMRefValue, FocusableRef, Key} from '@react-types/shared';
14
14
  import {centerBaseline} from './CenterBaseline';
15
- import {ContextValue, DEFAULT_SLOT, Provider, TextContext as RACTextContext, Radio, RadioGroup, RadioGroupStateContext, SlotProps} from 'react-aria-components';
15
+ import {ContextValue, DEFAULT_SLOT, Provider, TextContext as RACTextContext, SlotProps, ToggleButton, ToggleButtonGroup, ToggleGroupStateContext} from 'react-aria-components';
16
16
  import {createContext, forwardRef, ReactNode, RefObject, useCallback, useContext, useRef} from 'react';
17
17
  import {focusRing, size, style} from '../style' with {type: 'macro'};
18
18
  import {getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'};
@@ -32,12 +32,14 @@ export interface SegmentedControlProps extends AriaLabelingProps, StyleProps, Sl
32
32
  * Whether the segmented control is disabled.
33
33
  */
34
34
  isDisabled?: boolean,
35
+ /** Whether the items should divide the container width equally. */
36
+ isJustified?: boolean,
35
37
  /** The id of the currently selected item (controlled). */
36
- selectedKey?: string | null,
38
+ selectedKey?: Key | null,
37
39
  /** The id of the initial selected item (uncontrolled). */
38
- defaultSelectedKey?: string,
40
+ defaultSelectedKey?: Key,
39
41
  /** Handler that is called when the selection changes. */
40
- onSelectionChange?: (id: string) => void
42
+ onSelectionChange?: (id: Key) => void
41
43
  }
42
44
  export interface SegmentedControlItemProps extends AriaLabelingProps, StyleProps {
43
45
  /**
@@ -45,25 +47,27 @@ export interface SegmentedControlItemProps extends AriaLabelingProps, StyleProps
45
47
  */
46
48
  children: ReactNode,
47
49
  /** The id of the item, matching the value used in SegmentedControl's `selectedKey` prop. */
48
- id: string,
50
+ id: Key,
49
51
  /** Whether the item is disabled or not. */
50
52
  isDisabled?: boolean
51
53
  }
52
54
 
53
55
  export const SegmentedControlContext = createContext<ContextValue<SegmentedControlProps, DOMRefValue<HTMLDivElement>>>(null);
54
56
 
55
- const segmentedControl = style<{size: string}>({
56
- font: 'control',
57
+ const segmentedControl = style({
57
58
  display: 'flex',
59
+ gap: 4,
58
60
  backgroundColor: 'gray-100',
59
- borderRadius: 'lg',
60
- width: 'full'
61
+ borderRadius: 'default',
62
+ width: 'fit'
61
63
  }, getAllowedOverrides());
62
64
 
63
65
  const controlItem = style({
66
+ ...focusRing(),
64
67
  position: 'relative',
65
68
  display: 'flex',
66
69
  forcedColorAdjust: 'none',
70
+ font: 'control',
67
71
  color: {
68
72
  default: 'gray-700',
69
73
  isHovered: 'neutral-subdued',
@@ -78,17 +82,25 @@ const controlItem = style({
78
82
  // TODO: update this padding for icon-only items when we introduce the non-track style back
79
83
  paddingX: {
80
84
  default: 'edge-to-text',
81
- ':has([slot=icon]:only-child)': size(6)
85
+ ':has([slot=icon]):not(:has([data-rsp-slot=text]))': size(6)
82
86
  },
83
87
  height: 32,
84
88
  alignItems: 'center',
85
- flexBasis: 0,
86
- flexGrow: 1,
89
+ flexGrow: {
90
+ isJustified: 1
91
+ },
92
+ flexBasis: {
93
+ isJustified: 0
94
+ },
87
95
  flexShrink: 0,
88
96
  minWidth: 0,
89
97
  justifyContent: 'center',
90
98
  whiteSpace: 'nowrap',
91
99
  disableTapHighlight: true,
100
+ userSelect: 'none',
101
+ backgroundColor: 'transparent',
102
+ borderStyle: 'none',
103
+ borderRadius: 'default',
92
104
  '--iconPrimary': {
93
105
  type: 'fill',
94
106
  value: 'currentColor'
@@ -96,7 +108,6 @@ const controlItem = style({
96
108
  }, getAllowedOverrides());
97
109
 
98
110
  const slider = style({
99
- ...focusRing(),
100
111
  backgroundColor: {
101
112
  default: 'gray-25',
102
113
  forcedColors: {
@@ -123,17 +134,19 @@ const slider = style({
123
134
  });
124
135
 
125
136
  interface InternalSegmentedControlContextProps {
126
- register?: (value: string, isDisabled?: boolean) => void,
137
+ register?: (value: Key, isDisabled?: boolean) => void,
127
138
  prevRef?: RefObject<DOMRect | null>,
128
- currentSelectedRef?: RefObject<HTMLDivElement | null>
139
+ currentSelectedRef?: RefObject<HTMLDivElement | null>,
140
+ isJustified?: boolean
129
141
  }
130
142
 
131
143
  interface DefaultSelectionTrackProps {
132
- defaultValue?: string | null,
133
- value?: string | null,
144
+ defaultValue?: Key | null,
145
+ value?: Key | null,
134
146
  children?: ReactNode,
135
147
  prevRef: RefObject<DOMRect | null>,
136
- currentSelectedRef: RefObject<HTMLDivElement | null>
148
+ currentSelectedRef: RefObject<HTMLDivElement | null>,
149
+ isJustified?: boolean
137
150
  }
138
151
 
139
152
  const InternalSegmentedControlContext = createContext<InternalSegmentedControlContextProps>({});
@@ -150,63 +163,63 @@ function SegmentedControl(props: SegmentedControlProps, ref: DOMRef<HTMLDivEleme
150
163
  let prevRef = useRef<DOMRect>(null);
151
164
  let currentSelectedRef = useRef<HTMLDivElement>(null);
152
165
 
153
- let onChange = (value: string) => {
166
+ let onChange = (values: Set<Key>) => {
154
167
  if (currentSelectedRef.current) {
155
168
  prevRef.current = currentSelectedRef?.current.getBoundingClientRect();
156
169
  }
157
170
 
158
171
  if (onSelectionChange) {
159
- onSelectionChange(value);
172
+ onSelectionChange(values.values().next().value);
160
173
  }
161
174
  };
162
175
 
163
176
  return (
164
- <RadioGroup
177
+ <ToggleButtonGroup
165
178
  {...props}
166
- value={selectedKey}
167
- defaultValue={defaultSelectedKey}
179
+ selectedKeys={selectedKey != null ? [selectedKey] : undefined}
180
+ defaultSelectedKeys={defaultSelectedKey != null ? [defaultSelectedKey] : undefined}
181
+ disallowEmptySelection
168
182
  ref={domRef}
169
183
  orientation="horizontal"
170
184
  style={props.UNSAFE_style}
171
- onChange={onChange}
172
- className={(props.UNSAFE_className || '') + segmentedControl({size: 'M'}, props.styles)}
185
+ onSelectionChange={onChange}
186
+ className={(props.UNSAFE_className || '') + segmentedControl(null, props.styles)}
173
187
  aria-label={props['aria-label']}>
174
- <DefaultSelectionTracker defaultValue={defaultSelectedKey} value={selectedKey} prevRef={prevRef} currentSelectedRef={currentSelectedRef}>
188
+ <DefaultSelectionTracker defaultValue={defaultSelectedKey} value={selectedKey} prevRef={prevRef} currentSelectedRef={currentSelectedRef} isJustified={props.isJustified}>
175
189
  {props.children}
176
190
  </DefaultSelectionTracker>
177
- </RadioGroup>
191
+ </ToggleButtonGroup>
178
192
  );
179
193
  }
180
194
 
181
195
  function DefaultSelectionTracker(props: DefaultSelectionTrackProps) {
182
- let state = useContext(RadioGroupStateContext);
196
+ let state = useContext(ToggleGroupStateContext);
183
197
  let isRegistered = useRef(!(props.defaultValue == null && props.value == null));
184
198
 
185
199
  // default select the first available item
186
- let register = useCallback((value: string) => {
200
+ let register = useCallback((value: Key) => {
187
201
  if (state && !isRegistered.current) {
188
202
  isRegistered.current = true;
189
- state.setSelectedValue(value);
203
+ state.toggleKey(value);
190
204
  }
191
205
  }, []);
192
206
 
193
207
  return (
194
208
  <Provider
195
209
  values={[
196
- [InternalSegmentedControlContext, {register: register, prevRef: props.prevRef, currentSelectedRef: props.currentSelectedRef}]
210
+ [InternalSegmentedControlContext, {register: register, prevRef: props.prevRef, currentSelectedRef: props.currentSelectedRef, isJustified: props.isJustified}]
197
211
  ]}>
198
212
  {props.children}
199
213
  </Provider>
200
214
  );
201
215
  }
202
216
 
203
- function SegmentedControlItem(props: SegmentedControlItemProps, ref: FocusableRef<HTMLLabelElement>) {
204
- let inputRef = useRef<HTMLInputElement>(null);
205
- let domRef = useFocusableRef(ref, inputRef);
217
+ function SegmentedControlItem(props: SegmentedControlItemProps, ref: FocusableRef<HTMLButtonElement>) {
218
+ let domRef = useFocusableRef(ref);
206
219
  let divRef = useRef<HTMLDivElement>(null);
207
- let {register, prevRef, currentSelectedRef} = useContext(InternalSegmentedControlContext);
208
- let state = useContext(RadioGroupStateContext);
209
- let isSelected = props.id === state?.selectedValue;
220
+ let {register, prevRef, currentSelectedRef, isJustified} = useContext(InternalSegmentedControlContext);
221
+ let state = useContext(ToggleGroupStateContext);
222
+ let isSelected = state?.selectedKeys.has(props.id);
210
223
  // do not apply animation if a user has the prefers-reduced-motion setting
211
224
  let isReduced = false;
212
225
  if (window?.matchMedia) {
@@ -239,16 +252,14 @@ function SegmentedControlItem(props: SegmentedControlItemProps, ref: FocusableRe
239
252
  }, [isSelected]);
240
253
 
241
254
  return (
242
- <Radio
255
+ <ToggleButton
243
256
  {...props}
244
- value={props.id}
245
257
  ref={domRef}
246
- inputRef={inputRef}
247
258
  style={props.UNSAFE_style}
248
- className={renderProps => (props.UNSAFE_className || '') + controlItem({...renderProps}, props.styles)} >
249
- {({isSelected, isFocusVisible, isPressed, isDisabled}) => (
259
+ className={renderProps => (props.UNSAFE_className || '') + controlItem({...renderProps, isJustified}, props.styles)} >
260
+ {({isSelected, isPressed, isDisabled}) => (
250
261
  <>
251
- {isSelected && <div className={slider({isFocusVisible, isDisabled})} ref={currentSelectedRef} />}
262
+ {isSelected && <div className={slider({isDisabled})} ref={currentSelectedRef} />}
252
263
  <Provider
253
264
  values={[
254
265
  [IconContext, {
@@ -264,7 +275,7 @@ function SegmentedControlItem(props: SegmentedControlItemProps, ref: FocusableRe
264
275
  </>
265
276
  )
266
277
  }
267
- </Radio>
278
+ </ToggleButton>
268
279
  );
269
280
  }
270
281