@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.
- package/dist/main.js +135 -56
- package/dist/main.js.map +1 -1
- package/dist/module.js +119 -53
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts +31 -7
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/BreakpointProvider.tsx +91 -0
- package/src/Slots.tsx +4 -3
- package/src/index.ts +1 -0
- package/src/styleProps.ts +48 -8
- package/src/useValueEffect.ts +2 -49
|
@@ -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
|
-
|
|
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
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
+
}
|
package/src/useValueEffect.ts
CHANGED
|
@@ -10,53 +10,6 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
14
|
-
import {useLayoutEffect} from '@react-aria/utils';
|
|
13
|
+
import {useValueEffect} from '@react-aria/utils';
|
|
15
14
|
|
|
16
|
-
|
|
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};
|