@react-spectrum/utils 3.5.2 → 3.6.3

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.
@@ -0,0 +1,91 @@
1
+ import React, {ReactNode, useContext, useEffect, useState} from 'react';
2
+ import {useIsSSR} from '@react-aria/ssr';
3
+
4
+ interface Breakpoints {
5
+ S?: number,
6
+ M?: number,
7
+ L?: number,
8
+ [custom: string]: number | undefined
9
+ }
10
+
11
+ interface BreakpointContext {
12
+ matchedBreakpoints: string[]
13
+ }
14
+
15
+ const Context = React.createContext<BreakpointContext>(null);
16
+ Context.displayName = 'BreakpointContext';
17
+
18
+ interface BreakpointProviderProps {
19
+ children?: ReactNode,
20
+ matchedBreakpoints: string[]
21
+ }
22
+
23
+ export function BreakpointProvider(props: BreakpointProviderProps) {
24
+ let {
25
+ children,
26
+ matchedBreakpoints
27
+ } = props;
28
+ return (
29
+ <Context.Provider
30
+ value={{matchedBreakpoints}} >
31
+ {children}
32
+ </Context.Provider>
33
+ );
34
+ }
35
+
36
+ export function useMatchedBreakpoints(breakpoints: Breakpoints): string[] {
37
+ let entries = Object.entries(breakpoints).sort(([, valueA], [, valueB]) => valueB - valueA);
38
+ let breakpointQueries = entries.map(([, value]) => `(min-width: ${value}px)`);
39
+
40
+ let supportsMatchMedia = typeof window !== 'undefined' && typeof window.matchMedia === 'function';
41
+ let getBreakpointHandler = () => {
42
+ let matched = [];
43
+ for (let i in breakpointQueries) {
44
+ let query = breakpointQueries[i];
45
+ if (window.matchMedia(query).matches) {
46
+ matched.push(entries[i][0]);
47
+ }
48
+ }
49
+ matched.push('base');
50
+ return matched;
51
+ };
52
+
53
+ let [breakpoint, setBreakpoint] = useState(() =>
54
+ supportsMatchMedia
55
+ ? getBreakpointHandler()
56
+ : ['base']
57
+ );
58
+
59
+ useEffect(() => {
60
+ if (!supportsMatchMedia) {
61
+ return;
62
+ }
63
+
64
+ let onResize = () => {
65
+ const breakpointHandler = getBreakpointHandler();
66
+
67
+ setBreakpoint(previousBreakpointHandler => {
68
+ if (previousBreakpointHandler.length !== breakpointHandler.length ||
69
+ previousBreakpointHandler.some((breakpoint, idx) => breakpoint !== breakpointHandler[idx])) {
70
+ return [...breakpointHandler]; // Return a new array to force state change
71
+ }
72
+
73
+ return previousBreakpointHandler;
74
+ });
75
+ };
76
+
77
+ window.addEventListener('resize', onResize);
78
+ return () => {
79
+ window.removeEventListener('resize', onResize);
80
+ };
81
+ }, [supportsMatchMedia]);
82
+
83
+ // If in SSR, the media query should never match. Once the page hydrates,
84
+ // this will update and the real value will be returned.
85
+ let isSSR = useIsSSR();
86
+ return isSSR ? ['base'] : breakpoint;
87
+ }
88
+
89
+ export function useBreakpoint(): BreakpointContext {
90
+ return useContext(Context);
91
+ }
package/src/Slots.tsx CHANGED
@@ -19,10 +19,11 @@ interface SlotProps {
19
19
 
20
20
  let SlotContext = React.createContext(null);
21
21
 
22
- export function useSlotProps<T>(props: T, defaultSlot?: string): T {
22
+ export function useSlotProps<T>(props: T & {id?: string}, defaultSlot?: string): T {
23
23
  let slot = (props as SlotProps).slot || defaultSlot;
24
24
  let {[slot]: slotProps = {}} = useContext(SlotContext) || {};
25
- return mergeProps(slotProps, props);
25
+
26
+ return mergeProps(props, mergeProps(slotProps, {id: props.id}));
26
27
  }
27
28
 
28
29
  export function cssModuleToSlots(cssModule) {
@@ -37,7 +38,7 @@ export function SlotProvider(props) {
37
38
  let {slots = {}, children} = props;
38
39
 
39
40
  // Merge props for each slot from parent context and props
40
- let value = useMemo(() =>
41
+ let value = useMemo(() =>
41
42
  Object.keys(parentSlots)
42
43
  .concat(Object.keys(slots))
43
44
  .reduce((o, p) => ({
package/src/index.ts CHANGED
@@ -23,3 +23,4 @@ export * from './useHasChild';
23
23
  export * from './useResizeObserver';
24
24
  export * from './useIsMobileDevice';
25
25
  export * from './useValueEffect';
26
+ export * from './BreakpointProvider';
package/src/styleProps.ts CHANGED
@@ -10,10 +10,12 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import {BackgroundColorValue, BorderColorValue, BorderRadiusValue, BorderSizeValue, ColorValue, DimensionValue, Direction, StyleProps, ViewStyleProps} from '@react-types/shared';
13
+ import {BackgroundColorValue, BorderColorValue, BorderRadiusValue, BorderSizeValue, ColorValue, DimensionValue, Direction, Responsive, ResponsiveProp, StyleProps, ViewStyleProps} from '@react-types/shared';
14
14
  import {CSSProperties, HTMLAttributes} from 'react';
15
+ import {useBreakpoint} from './BreakpointProvider';
15
16
  import {useLocale} from '@react-aria/i18n';
16
17
 
18
+ type Breakpoint = 'base' | 'S' | 'M' | 'L' | string;
17
19
  type StyleName = string | string[] | ((dir: Direction) => string);
18
20
  type StyleHandler = (value: any) => string;
19
21
  export interface StyleHandlers {
@@ -118,6 +120,9 @@ function rtl(ltr: string, rtl: string) {
118
120
  }
119
121
 
120
122
  const UNIT_RE = /(%|px|em|rem|vw|vh|auto|cm|mm|in|pt|pc|ex|ch|rem|vmin|vmax|fr)$/;
123
+ const FUNC_RE = /^\s*\w+\(/;
124
+ const SPECTRUM_VARIABLE_RE = /(static-)?size-\d+|single-line-(height|width)/g;
125
+
121
126
  export function dimensionValue(value: DimensionValue) {
122
127
  if (typeof value === 'number') {
123
128
  return value + 'px';
@@ -127,9 +132,18 @@ export function dimensionValue(value: DimensionValue) {
127
132
  return value;
128
133
  }
129
134
 
135
+ if (FUNC_RE.test(value)) {
136
+ return value.replace(SPECTRUM_VARIABLE_RE, 'var(--spectrum-global-dimension-$&, var(--spectrum-alias-$&))');
137
+ }
138
+
130
139
  return `var(--spectrum-global-dimension-${value}, var(--spectrum-alias-${value}))`;
131
140
  }
132
141
 
142
+ export function responsiveDimensionValue(value: Responsive<DimensionValue>, matchedBreakpoints: Breakpoint[]) {
143
+ value = getResponsiveProp(value, matchedBreakpoints);
144
+ return dimensionValue(value);
145
+ }
146
+
133
147
  type ColorType = 'default' | 'background' | 'border' | 'icon' | 'status';
134
148
  function colorValue(value: ColorValue, type: ColorType = 'default') {
135
149
  return `var(--spectrum-global-color-${value}, var(--spectrum-semantic-${value}-color-${type}))`;
@@ -171,7 +185,7 @@ function flexValue(value: boolean | number | string) {
171
185
  return '' + value;
172
186
  }
173
187
 
174
- export function convertStyleProps(props: ViewStyleProps, handlers: StyleHandlers, direction: Direction) {
188
+ export function convertStyleProps(props: ViewStyleProps, handlers: StyleHandlers, direction: Direction, matchedBreakpoints: Breakpoint[]) {
175
189
  let style: CSSProperties = {};
176
190
  for (let key in props) {
177
191
  let styleProp = handlers[key];
@@ -184,7 +198,8 @@ export function convertStyleProps(props: ViewStyleProps, handlers: StyleHandlers
184
198
  name = name(direction);
185
199
  }
186
200
 
187
- let value = convert(props[key]);
201
+ let prop = getResponsiveProp(props[key], matchedBreakpoints);
202
+ let value = convert(prop);
188
203
  if (Array.isArray(name)) {
189
204
  for (let k of name) {
190
205
  style[k] = value;
@@ -204,21 +219,33 @@ export function convertStyleProps(props: ViewStyleProps, handlers: StyleHandlers
204
219
  return style;
205
220
  }
206
221
 
207
- export function useStyleProps<T extends StyleProps>(props: T, handlers: StyleHandlers = baseStyleProps) {
222
+ type StylePropsOptions = {
223
+ matchedBreakpoints?: Breakpoint[]
224
+ };
225
+
226
+ export function useStyleProps<T extends StyleProps>(
227
+ props: T,
228
+ handlers: StyleHandlers = baseStyleProps,
229
+ options: StylePropsOptions = {}
230
+ ) {
208
231
  let {
209
232
  UNSAFE_className,
210
233
  UNSAFE_style,
211
234
  ...otherProps
212
235
  } = props;
236
+ let breakpointProvider = useBreakpoint();
213
237
  let {direction} = useLocale();
214
- let styles = convertStyleProps(props, handlers, direction);
238
+ let {
239
+ matchedBreakpoints = breakpointProvider?.matchedBreakpoints || ['base']
240
+ } = options;
241
+ let styles = convertStyleProps(props, handlers, direction, matchedBreakpoints);
215
242
  let style = {...UNSAFE_style, ...styles};
216
243
 
217
244
  // @ts-ignore
218
245
  if (otherProps.className) {
219
246
  console.warn(
220
247
  'The className prop is unsafe and is unsupported in React Spectrum v3. ' +
221
- 'Please use style props with Spectrum variables, or UNSAFE_className if you absolutely must to something custom. ' +
248
+ 'Please use style props with Spectrum variables, or UNSAFE_className if you absolutely must do something custom. ' +
222
249
  'Note that this may break in future versions due to DOM structure changes.'
223
250
  );
224
251
  }
@@ -227,7 +254,7 @@ export function useStyleProps<T extends StyleProps>(props: T, handlers: StyleHan
227
254
  if (otherProps.style) {
228
255
  console.warn(
229
256
  'The style prop is unsafe and is unsupported in React Spectrum v3. ' +
230
- 'Please use style props with Spectrum variables, or UNSAFE_style if you absolutely must to something custom. ' +
257
+ 'Please use style props with Spectrum variables, or UNSAFE_style if you absolutely must do something custom. ' +
231
258
  'Note that this may break in future versions due to DOM structure changes.'
232
259
  );
233
260
  }
@@ -237,7 +264,7 @@ export function useStyleProps<T extends StyleProps>(props: T, handlers: StyleHan
237
264
  className: UNSAFE_className
238
265
  };
239
266
 
240
- if (props.isHidden) {
267
+ if (getResponsiveProp(props.isHidden, matchedBreakpoints)) {
241
268
  styleProps.hidden = true;
242
269
  }
243
270
 
@@ -249,3 +276,16 @@ export function useStyleProps<T extends StyleProps>(props: T, handlers: StyleHan
249
276
  export function passthroughStyle(value) {
250
277
  return value;
251
278
  }
279
+
280
+ export function getResponsiveProp<T>(prop: Responsive<T>, matchedBreakpoints: Breakpoint[]): T {
281
+ if (prop && typeof prop === 'object' && !Array.isArray(prop)) {
282
+ for (let i = 0; i < matchedBreakpoints.length; i++) {
283
+ let breakpoint = matchedBreakpoints[i];
284
+ if (prop[breakpoint] != null) {
285
+ return prop[breakpoint];
286
+ }
287
+ }
288
+ return (prop as ResponsiveProp<T>).base;
289
+ }
290
+ return prop as T;
291
+ }
@@ -10,53 +10,6 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import {Dispatch, useCallback, useRef, useState} from 'react';
14
- import {useLayoutEffect} from '@react-aria/utils';
13
+ import {useValueEffect} from '@react-aria/utils';
15
14
 
16
- type SetValueAction = () => Generator<any, void, unknown>;
17
-
18
- // This hook works like `useState`, but when setting the value, you pass a generator function
19
- // that can yield multiple values. Each yielded value updates the state and waits for the next
20
- // layout effect, then continues the generator. This allows sequential updates to state to be
21
- // written linearly.
22
- export function useValueEffect<S>(defaultValue: S | (() => S)): [S, Dispatch<SetValueAction>] {
23
- let [value, setValue] = useState(defaultValue);
24
- let effect = useRef(null);
25
-
26
- // Store the function in a ref so we can always access the current version
27
- // which has the proper `value` in scope.
28
- let nextRef = useRef(null);
29
- nextRef.current = () => {
30
- // Run the generator to the next yield.
31
- let newValue = effect.current.next();
32
-
33
- // If the generator is done, reset the effect.
34
- if (newValue.done) {
35
- effect.current = null;
36
- return;
37
- }
38
-
39
- // If the value is the same as the current value,
40
- // then continue to the next yield. Otherwise,
41
- // set the value in state and wait for the next layout effect.
42
- if (value === newValue.value) {
43
- nextRef.current();
44
- } else {
45
- setValue(newValue.value);
46
- }
47
- };
48
-
49
- useLayoutEffect(() => {
50
- // If there is an effect currently running, continue to the next yield.
51
- if (effect.current) {
52
- nextRef.current();
53
- }
54
- });
55
-
56
- let queue = useCallback(fn => {
57
- effect.current = fn();
58
- nextRef.current();
59
- }, [effect, nextRef]);
60
-
61
- return [value, queue];
62
- }
15
+ export {useValueEffect};