@react-spectrum/s2 3.0.0-nightly-e4ec6b4e9-250422 → 3.0.0-nightly-f76c445b5-250423

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 (2) hide show
  1. package/package.json +22 -20
  2. package/src/CoachMark.tsx +530 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-spectrum/s2",
3
- "version": "3.0.0-nightly-e4ec6b4e9-250422",
3
+ "version": "3.0.0-nightly-f76c445b5-250423",
4
4
  "description": "Spectrum 2 UI components in React",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -57,26 +57,28 @@
57
57
  "src"
58
58
  ],
59
59
  "dependencies": {
60
- "@internationalized/number": "3.0.0-nightly-e4ec6b4e9-250422",
61
- "@react-aria/collections": "3.0.0-nightly-e4ec6b4e9-250422",
62
- "@react-aria/focus": "3.0.0-nightly-e4ec6b4e9-250422",
63
- "@react-aria/i18n": "3.0.0-nightly-e4ec6b4e9-250422",
64
- "@react-aria/interactions": "3.0.0-nightly-e4ec6b4e9-250422",
65
- "@react-aria/live-announcer": "3.0.0-nightly-e4ec6b4e9-250422",
66
- "@react-aria/utils": "3.0.0-nightly-e4ec6b4e9-250422",
67
- "@react-spectrum/utils": "3.0.0-nightly-e4ec6b4e9-250422",
68
- "@react-stately/layout": "3.0.0-nightly-e4ec6b4e9-250422",
69
- "@react-stately/utils": "3.0.0-nightly-e4ec6b4e9-250422",
70
- "@react-types/dialog": "3.0.0-nightly-e4ec6b4e9-250422",
71
- "@react-types/grid": "3.0.0-nightly-e4ec6b4e9-250422",
72
- "@react-types/provider": "3.0.0-nightly-e4ec6b4e9-250422",
73
- "@react-types/shared": "3.0.0-nightly-e4ec6b4e9-250422",
74
- "@react-types/table": "3.0.0-nightly-e4ec6b4e9-250422",
75
- "@react-types/textfield": "3.0.0-nightly-e4ec6b4e9-250422",
60
+ "@internationalized/number": "3.0.0-nightly-f76c445b5-250423",
61
+ "@react-aria/collections": "3.0.0-nightly-f76c445b5-250423",
62
+ "@react-aria/focus": "3.0.0-nightly-f76c445b5-250423",
63
+ "@react-aria/i18n": "3.0.0-nightly-f76c445b5-250423",
64
+ "@react-aria/interactions": "3.0.0-nightly-f76c445b5-250423",
65
+ "@react-aria/live-announcer": "3.0.0-nightly-f76c445b5-250423",
66
+ "@react-aria/overlays": "3.0.0-nightly-f76c445b5-250423",
67
+ "@react-aria/utils": "3.0.0-nightly-f76c445b5-250423",
68
+ "@react-spectrum/utils": "3.0.0-nightly-f76c445b5-250423",
69
+ "@react-stately/layout": "3.0.0-nightly-f76c445b5-250423",
70
+ "@react-stately/menu": "3.0.0-nightly-f76c445b5-250423",
71
+ "@react-stately/utils": "3.0.0-nightly-f76c445b5-250423",
72
+ "@react-types/dialog": "3.0.0-nightly-f76c445b5-250423",
73
+ "@react-types/grid": "3.0.0-nightly-f76c445b5-250423",
74
+ "@react-types/provider": "3.0.0-nightly-f76c445b5-250423",
75
+ "@react-types/shared": "3.0.0-nightly-f76c445b5-250423",
76
+ "@react-types/table": "3.0.0-nightly-f76c445b5-250423",
77
+ "@react-types/textfield": "3.0.0-nightly-f76c445b5-250423",
76
78
  "csstype": "^3.0.2",
77
- "react-aria": "3.0.0-nightly-e4ec6b4e9-250422",
78
- "react-aria-components": "3.0.0-nightly-e4ec6b4e9-250422",
79
- "react-stately": "3.0.0-nightly-e4ec6b4e9-250422"
79
+ "react-aria": "3.0.0-nightly-f76c445b5-250423",
80
+ "react-aria-components": "3.0.0-nightly-f76c445b5-250423",
81
+ "react-stately": "3.0.0-nightly-f76c445b5-250423"
80
82
  },
81
83
  "peerDependencies": {
82
84
  "react": "^18.0.0 || ^19.0.0-rc.1",
@@ -0,0 +1,530 @@
1
+
2
+ /*
3
+ * Copyright 2024 Adobe. All rights reserved.
4
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License. You may obtain a copy
6
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
7
+ *
8
+ * Unless required by applicable law or agreed to in writing, software distributed under
9
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
10
+ * OF ANY KIND, either express or implied. See the License for the specific language
11
+ * governing permissions and limitations under the License.
12
+ */
13
+ import {ActionMenuContext} from './ActionMenu';
14
+ import {
15
+ DialogTriggerProps as AriaDialogTriggerProps,
16
+ Popover as AriaPopover,
17
+ ContextValue,
18
+ DEFAULT_SLOT,
19
+ DialogContext,
20
+ OverlayTriggerStateContext,
21
+ PopoverContext,
22
+ PopoverProps,
23
+ Provider,
24
+ RootMenuTriggerStateContext,
25
+ useContextProps
26
+ } from 'react-aria-components';
27
+ import {ButtonContext} from './Button';
28
+ import {Card} from './Card';
29
+ import {CheckboxContext} from './Checkbox';
30
+ import {colorScheme, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'};
31
+ import {ColorSchemeContext} from './Provider';
32
+ import {ContentContext, FooterContext, KeyboardContext, TextContext} from './Content';
33
+ import {
34
+ createContext,
35
+ ForwardedRef,
36
+ forwardRef,
37
+ ReactNode,
38
+ useContext,
39
+ useRef
40
+ } from 'react';
41
+ import {DividerContext} from './Divider';
42
+ import {forwardRefType} from './types';
43
+ import {ImageContext} from './Image';
44
+ import {ImageCoordinator} from './ImageCoordinator';
45
+ import {keyframes, raw} from '../style/style-macro' with {type: 'macro'};
46
+ import {mergeStyles} from '../style/runtime';
47
+ import {PressResponder} from '@react-aria/interactions';
48
+ import {SliderContext} from './Slider';
49
+ import {space, style} from '../style' with {type: 'macro'};
50
+ import {useId, useObjectRef, useOverlayTrigger} from 'react-aria';
51
+ import {useLayoutEffect} from '@react-aria/utils';
52
+ import {useMenuTriggerState} from '@react-stately/menu';
53
+
54
+ export interface CoachMarkProps extends Omit<PopoverProps, 'children' | 'arrowBoundaryOffset' | 'isKeyboardDismissDisabled' | 'isNonModal'>, StyleProps {
55
+ /** The children of the coach mark. */
56
+ children: ReactNode,
57
+
58
+ size?: 'S' | 'M' | 'L' | 'XL'
59
+ }
60
+
61
+ const fadeKeyframes = keyframes(`
62
+ from {
63
+ opacity: 0;
64
+ }
65
+
66
+ to {
67
+ opacity: 1;
68
+ }
69
+ `);
70
+ const slideUpKeyframes = keyframes(`
71
+ from {
72
+ transform: translateY(-4px);
73
+ }
74
+
75
+ to {
76
+ transform: translateY(0);
77
+ }
78
+ `);
79
+ const slideDownKeyframes = keyframes(`
80
+ from {
81
+ transform: translateY(4px);
82
+ }
83
+
84
+ to {
85
+ transform: translateY(0);
86
+ }
87
+ `);
88
+ const slideRightKeyframes = keyframes(`
89
+ from {
90
+ transform: translateX(4px);
91
+ }
92
+
93
+ to {
94
+ transform: translateX(0);
95
+ }
96
+ `);
97
+ const slideLeftKeyframes = keyframes(`
98
+ from {
99
+ transform: translateX(-4px);
100
+ }
101
+
102
+ to {
103
+ transform: translateX(0);
104
+ }
105
+ `);
106
+
107
+ let popover = style({
108
+ ...colorScheme(),
109
+ '--s2-container-bg': {
110
+ type: 'backgroundColor',
111
+ value: 'layer-2'
112
+ },
113
+ backgroundColor: '--s2-container-bg',
114
+ borderRadius: 'lg',
115
+ filter: {
116
+ isArrowShown: 'elevated'
117
+ },
118
+ // Use box-shadow instead of filter when an arrow is not shown.
119
+ // This fixes the shadow stacking problem with submenus.
120
+ boxShadow: {
121
+ default: 'elevated',
122
+ isArrowShown: 'none'
123
+ },
124
+ borderStyle: 'solid',
125
+ borderWidth: 1,
126
+ borderColor: {
127
+ default: 'gray-200',
128
+ forcedColors: 'ButtonBorder'
129
+ },
130
+ width: {
131
+ size: {
132
+ // Copied from designs, not sure if correct.
133
+ S: 336,
134
+ M: 416,
135
+ L: 576
136
+ }
137
+ },
138
+ // Don't be larger than full screen minus 2 * containerPadding
139
+ maxWidth: '[calc(100vw - 24px)]',
140
+ boxSizing: 'border-box',
141
+ translateY: {
142
+ placement: {
143
+ bottom: {
144
+ isArrowShown: 8 // TODO: not defined yet should this change with font size? need boolean support for 'hideArrow' prop
145
+ },
146
+ top: {
147
+ isArrowShown: -8
148
+ }
149
+ }
150
+ },
151
+ translateX: {
152
+ placement: {
153
+ left: {
154
+ isArrowShown: -8
155
+ },
156
+ right: {
157
+ isArrowShown: 8
158
+ }
159
+ }
160
+ },
161
+ animation: {
162
+ placement: {
163
+ top: {
164
+ isEntering: `${slideDownKeyframes}, ${fadeKeyframes}`,
165
+ isExiting: `${slideDownKeyframes}, ${fadeKeyframes}`
166
+ },
167
+ bottom: {
168
+ isEntering: `${slideUpKeyframes}, ${fadeKeyframes}`,
169
+ isExiting: `${slideUpKeyframes}, ${fadeKeyframes}`
170
+ },
171
+ left: {
172
+ isEntering: `${slideRightKeyframes}, ${fadeKeyframes}`,
173
+ isExiting: `${slideRightKeyframes}, ${fadeKeyframes}`
174
+ },
175
+ right: {
176
+ isEntering: `${slideLeftKeyframes}, ${fadeKeyframes}`,
177
+ isExiting: `${slideLeftKeyframes}, ${fadeKeyframes}`
178
+ }
179
+ },
180
+ isSubmenu: {
181
+ isEntering: fadeKeyframes,
182
+ isExiting: fadeKeyframes
183
+ }
184
+ },
185
+ animationDuration: {
186
+ isEntering: 200,
187
+ isExiting: 200
188
+ },
189
+ animationDirection: {
190
+ isEntering: 'normal',
191
+ isExiting: 'reverse'
192
+ },
193
+ animationTimingFunction: {
194
+ isExiting: 'in'
195
+ },
196
+ transition: '[opacity, transform]',
197
+ willChange: '[opacity, transform]',
198
+ isolation: 'isolate',
199
+ pointerEvents: {
200
+ isExiting: 'none'
201
+ }
202
+ }, getAllowedOverrides());
203
+
204
+ const image = style({
205
+ width: 'full',
206
+ aspectRatio: '[3/2]',
207
+ objectFit: 'cover',
208
+ userSelect: 'none',
209
+ pointerEvents: 'none'
210
+ });
211
+
212
+ let title = style({
213
+ font: 'title',
214
+ fontSize: {
215
+ size: {
216
+ XS: 'title-xs',
217
+ S: 'title-xs',
218
+ M: 'title-sm',
219
+ L: 'title',
220
+ XL: 'title-lg'
221
+ }
222
+ },
223
+ lineClamp: 3,
224
+ gridArea: 'title'
225
+ });
226
+
227
+ let description = style({
228
+ font: 'body',
229
+ fontSize: {
230
+ size: {
231
+ XS: 'body-2xs',
232
+ S: 'body-2xs',
233
+ M: 'body-xs',
234
+ L: 'body-sm',
235
+ XL: 'body'
236
+ }
237
+ },
238
+ lineClamp: 3,
239
+ gridArea: 'description'
240
+ });
241
+
242
+ let keyboard = style({
243
+ gridArea: 'keyboard',
244
+ font: 'ui',
245
+ fontWeight: 'light',
246
+ color: 'gray-600',
247
+ background: 'gray-25',
248
+ unicodeBidi: 'plaintext'
249
+ });
250
+
251
+ let steps = style({
252
+ font: 'detail',
253
+ fontSize: 'detail-sm',
254
+ alignSelf: 'center'
255
+ });
256
+
257
+ let content = style({
258
+ display: 'grid',
259
+ // By default, all elements are displayed in a stack.
260
+ // If an action menu is present, place it next to the title.
261
+ gridTemplateColumns: {
262
+ default: ['1fr'],
263
+ ':has([data-slot=menu])': ['minmax(0, 1fr)', 'auto']
264
+ },
265
+ gridTemplateAreas: {
266
+ default: [
267
+ 'title keyboard',
268
+ 'description keyboard'
269
+ ],
270
+ ':has([data-slot=menu])': [
271
+ 'title menu',
272
+ 'keyboard keyboard',
273
+ 'description description'
274
+ ]
275
+ },
276
+ columnGap: 4,
277
+ flexGrow: 1,
278
+ alignItems: 'baseline',
279
+ alignContent: 'space-between',
280
+ rowGap: {
281
+ size: {
282
+ XS: 4,
283
+ S: 4,
284
+ M: space(6),
285
+ L: space(6),
286
+ XL: 8
287
+ }
288
+ },
289
+ paddingTop: {
290
+ default: '--card-spacing',
291
+ ':first-child': 0
292
+ },
293
+ paddingBottom: {
294
+ default: '[calc(var(--card-spacing) * 1.5 / 2)]',
295
+ ':last-child': 0
296
+ }
297
+ });
298
+
299
+ let actionMenu = style({
300
+ gridArea: 'menu',
301
+ // Don't cause the row to expand, preserve gap between title and description text.
302
+ // Would use -100% here but it doesn't work in Firefox.
303
+ marginY: '[calc(-1 * self(height))]'
304
+ });
305
+
306
+ let footer = style({
307
+ display: 'flex',
308
+ flexDirection: 'row',
309
+ alignItems: 'end',
310
+ justifyContent: 'space-between',
311
+ gap: 8,
312
+ paddingTop: '[calc(var(--card-spacing) * 1.5 / 2)]'
313
+ });
314
+
315
+ const actionButtonSize = {
316
+ XS: 'XS',
317
+ S: 'XS',
318
+ M: 'S',
319
+ L: 'M',
320
+ XL: 'L'
321
+ } as const;
322
+
323
+ export const CoachMarkContext = createContext<ContextValue<CoachMarkProps, HTMLElement>>({});
324
+
325
+ export const CoachMark = forwardRef((props: CoachMarkProps, ref: ForwardedRef<HTMLElement>) => {
326
+ let colorScheme = useContext(ColorSchemeContext);
327
+ [props, ref] = useContextProps(props, ref, CoachMarkContext);
328
+ let {UNSAFE_style} = props;
329
+ let {size = 'M'} = props;
330
+ let popoverRef = useObjectRef(ref);
331
+
332
+ let children = (
333
+ <Provider
334
+ values={[
335
+ [ImageContext, {alt: '', styles: image}],
336
+ [TextContext, {
337
+ slots: {
338
+ [DEFAULT_SLOT]: {},
339
+ title: {styles: title({size})},
340
+ description: {styles: description({size})},
341
+ steps: {styles: steps}
342
+ }
343
+ }],
344
+ [KeyboardContext, {styles: keyboard}],
345
+ [ContentContext, {styles: content({size})}],
346
+ [DividerContext, {size: 'S'}],
347
+ [FooterContext, {styles: footer}],
348
+ [ActionMenuContext, {
349
+ isQuiet: true,
350
+ size: actionButtonSize[size],
351
+ // @ts-ignore
352
+ 'data-slot': 'menu',
353
+ styles: actionMenu
354
+ }]
355
+ ]}>
356
+ <ImageCoordinator>
357
+ {props.children}
358
+ </ImageCoordinator>
359
+ </Provider>
360
+ );
361
+
362
+ return (
363
+ <AriaPopover
364
+ {...props}
365
+ ref={popoverRef}
366
+ style={{
367
+ ...UNSAFE_style,
368
+ // Override default z-index from useOverlayPosition. We use isolation: isolate instead.
369
+ zIndex: undefined
370
+ }}
371
+ className={(renderProps) => mergeStyles(popover({...renderProps, colorScheme}))}>
372
+ <Card>
373
+ {/* }// Reset OverlayTriggerStateContext so the buttons inside the dialog don't retain their hover state. */}
374
+ <OverlayTriggerStateContext.Provider value={null}>
375
+ {children}
376
+ </OverlayTriggerStateContext.Provider>
377
+ </Card>
378
+ </AriaPopover>
379
+ );
380
+ });
381
+
382
+
383
+ export interface CoachMarkTriggerProps extends AriaDialogTriggerProps {
384
+ }
385
+
386
+ /**
387
+ * DialogTrigger serves as a wrapper around a Dialog and its associated trigger, linking the Dialog's
388
+ * open state with the trigger's press state. Additionally, it allows you to customize the type and
389
+ * positioning of the Dialog.
390
+ */
391
+ export function CoachMarkTrigger(props: CoachMarkTriggerProps): ReactNode {
392
+ let triggerRef = useRef<HTMLDivElement>(null);
393
+ // Use useMenuTriggerState instead of useOverlayTriggerState in case a menu is embedded in the dialog.
394
+ // This is needed to handle submenus.
395
+ let state = useMenuTriggerState(props);
396
+
397
+ let {triggerProps, overlayProps} = useOverlayTrigger({type: 'dialog'}, state, triggerRef);
398
+
399
+ // Label dialog by the trigger as a fallback if there is no title slot.
400
+ // This is done in RAC instead of hooks because otherwise we cannot distinguish
401
+ // between context and props. Normally aria-labelledby overrides the title
402
+ // but when sent by context we want the title to win.
403
+ triggerProps.id = useId();
404
+ overlayProps['aria-labelledby'] = triggerProps.id;
405
+
406
+
407
+ return (
408
+ <Provider
409
+ values={[
410
+ [OverlayTriggerStateContext, state],
411
+ [RootMenuTriggerStateContext, state],
412
+ [DialogContext, overlayProps],
413
+ [PopoverContext, {trigger: 'DialogTrigger', triggerRef, isNonModal: true}] // valid to pass triggerRef?
414
+ ]}>
415
+ <PressResponder {...triggerProps} isPressed={state.isOpen}>
416
+ <CoachMarkIndicator ref={triggerRef} isActive={state.isOpen}>
417
+ {props.children}
418
+ </CoachMarkIndicator>
419
+ </PressResponder>
420
+ </Provider>
421
+ );
422
+ }
423
+
424
+
425
+ // TODO better way to calculate 4px transform? (not 4%?)
426
+ const pulseAnimation = keyframes(`
427
+ 0% {
428
+ box-shadow: 0 0 0 4px rgba(20, 115, 230, 0.40);
429
+ transform: scale(calc(100%));
430
+ }
431
+ 50% {
432
+ box-shadow: 0 0 0 10px rgba(20, 115, 230, 0.20);
433
+ transform: scale(104%);
434
+ }
435
+ 100% {
436
+ box-shadow: 0 0 0 4px rgba(20, 115, 230, 0.40);
437
+ transform: scale(calc(100%));
438
+ }
439
+ `);
440
+
441
+
442
+ const indicator = style({
443
+ animationDuration: 1000,
444
+ animationIterationCount: 'infinite',
445
+ animationFillMode: 'forwards',
446
+ animationTimingFunction: 'in-out',
447
+ position: 'relative',
448
+ '--activeElement': {
449
+ type: 'outlineColor',
450
+ value: {
451
+ default: 'focus-ring',
452
+ forcedColors: 'Highlight'
453
+ }
454
+ },
455
+ '--borderOffset': {
456
+ type: 'top',
457
+ value: {
458
+ default: '[-2px]',
459
+ ':has([data-trigger=checkbox])': '[-6px]',
460
+ ':has([data-trigger=slider])': '[-8px]',
461
+ offset: {
462
+ M: '[-6px]',
463
+ L: '[-8px]'
464
+ }
465
+ }
466
+ },
467
+ '--ringRadius': {
468
+ type: 'top', // is there a generic for pixel values?
469
+ value: {
470
+ default: '[10px]',
471
+ ':has([data-trigger=button])': '[18px]',
472
+ ':has([data-trigger=checkbox])': '[6px]'
473
+ }
474
+ }
475
+ });
476
+
477
+ const pulse = raw(`&:before { content: ""; display: inline-block; position: absolute; top: var(--borderOffset); bottom: var(--borderOffset); left: var(--borderOffset); right: var(--borderOffset); border-radius: var(--ringRadius); outline-style: solid; outline-color: var(--activeElement); outline-width: 4px; animation-duration: 2s; animation-iteration-count: infinite; animation-timing-function: ease-in-out; animation-fill-mode: forwards; animation-name: ${pulseAnimation}}`);
478
+
479
+ interface CoachMarkIndicatorProps {
480
+ children: ReactNode,
481
+ isActive?: boolean
482
+ }
483
+ export const CoachMarkIndicator = /*#__PURE__*/ (forwardRef as forwardRefType)(function CoachMarkIndicator(props: CoachMarkIndicatorProps, ref: ForwardedRef<HTMLDivElement>) {
484
+ const {children, isActive} = props;
485
+ let objRef = useObjectRef(ref);
486
+
487
+ // This is very silly... better ways? can't use display: contents because it breaks positioning
488
+ // this will break if there is a resize or different styles
489
+ useLayoutEffect(() => {
490
+ if (objRef.current) {
491
+ let styles = getComputedStyle(objRef.current.children[0]);
492
+ let childDisplay = styles.getPropertyValue('display');
493
+ let childMaxWidth = styles.getPropertyValue('max-width');
494
+ let childMaxHeight = styles.getPropertyValue('max-height');
495
+ let childWidth = styles.getPropertyValue('width');
496
+ let childHeight = styles.getPropertyValue('height');
497
+ let childMinWidth = styles.getPropertyValue('min-width');
498
+ let childMinHeight = styles.getPropertyValue('min-height');
499
+ objRef.current.style.display = childDisplay;
500
+ objRef.current.style.maxWidth = childMaxWidth;
501
+ objRef.current.style.maxHeight = childMaxHeight;
502
+ objRef.current.style.width = childWidth;
503
+ objRef.current.style.height = childHeight;
504
+ objRef.current.style.minWidth = childMinWidth;
505
+ objRef.current.style.minHeight = childMinHeight;
506
+ }
507
+ }, [children]);
508
+
509
+ return (
510
+ <div ref={objRef} className={indicator({isActive}) + ' ' + (isActive ? pulse : '')}>
511
+ <Provider
512
+ values={[
513
+ [ButtonContext, {
514
+ // @ts-ignore
515
+ 'data-trigger': 'button'
516
+ }],
517
+ [CheckboxContext, {
518
+ // @ts-ignore
519
+ 'data-trigger': 'checkbox'
520
+ }],
521
+ [SliderContext, {
522
+ // @ts-ignore
523
+ 'data-trigger': 'slider'
524
+ }]
525
+ ]}>
526
+ {children}
527
+ </Provider>
528
+ </div>
529
+ );
530
+ });