-
-
Notifications
You must be signed in to change notification settings - Fork 32.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[utils] Add all @material-ui/core/utils to @material-ui/utils #23264
Changes from 2 commits
5901bda
c9489d7
e866f5e
080dd7e
c625415
bd907da
324b3a8
11bbc63
a9a4247
3957119
328ca77
2747327
9066f7a
e0f137c
2d1ac56
c9e7ba0
7a3b4c2
fca1e8d
ebfdc06
623a89e
76810d7
831d74b
e0bc921
b5fe610
39dac58
b65508e
6fd966f
837e33f
9eefcca
5706036
21c706d
18be046
8438658
e2104a5
1312f19
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
// TODO: error TS7016: Could not find a declaration file for module '../macros/MuiError.macro'. '/tmp/material-ui/packages/material-ui-utils/macros/MuiError.macro.js' implicitly has an 'any' type. | ||
// import MuiError from '../macros/MuiError.macro'; | ||
eps1lon marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// It should to be noted that this function isn't equivalent to `text-transform: capitalize`. | ||
// | ||
// A strict capitalization should uppercase the first letter of each word a the sentence. | ||
// We only handle the first word. | ||
export default function capitalize(string: string): string { | ||
if (typeof string !== 'string') { | ||
throw new Error('Material-UI: capitalize(string) expects a string argument.'); | ||
} | ||
|
||
return string.charAt(0).toUpperCase() + string.slice(1); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
export type ChainedFunction = ((...args: any[]) => void) | undefined | null; | ||
|
||
/** | ||
* Safe chained function | ||
* | ||
* Will only create a new function if needed, | ||
* otherwise will pass back existing functions or null. | ||
* @param {function} functions to chain | ||
* @returns {function|null} | ||
*/ | ||
export default function createChainedFunction( | ||
...funcs: ChainedFunction[] | ||
): (...args: any[]) => never { | ||
return funcs.reduce( | ||
(acc, func) => { | ||
if (func == null) { | ||
return acc; | ||
} | ||
|
||
if (process.env.NODE_ENV !== 'production') { | ||
if (typeof func !== 'function') { | ||
console.error( | ||
'Material-UI: Invalid Argument Type, must only provide functions, undefined, or null.', | ||
); | ||
} | ||
} | ||
|
||
return function chainedFunction(...args) { | ||
// @ts-ignore | ||
acc.apply(this, args); | ||
// @ts-ignore | ||
func.apply(this, args); | ||
}; | ||
}, | ||
() => {}, | ||
) as (...args: any[]) => never; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
export interface Cancelable { | ||
clear(): void; | ||
} | ||
|
||
// Corresponds to 10 frames at 60 Hz. | ||
// A few bytes payload overhead when lodash/debounce is ~3 kB and debounce ~300 B. | ||
export default function debounce<T extends (...args: any[]) => any>( | ||
func: T, | ||
wait: number = 166, | ||
): T & Cancelable { | ||
let timeout: any; | ||
function debounced(...args: any[]) { | ||
const later = () => { | ||
// @ts-ignore | ||
func.apply(this, args); | ||
}; | ||
clearTimeout(timeout); | ||
timeout = setTimeout(later, wait); | ||
} | ||
|
||
debounced.clear = () => { | ||
clearTimeout(timeout); | ||
}; | ||
|
||
return debounced as T & Cancelable; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
export default function deprecatedPropType<T>(validator: T, reason: string): T | Function { | ||
if (process.env.NODE_ENV === 'production') { | ||
return () => null; | ||
} | ||
|
||
// @ts-ignore | ||
return (props, propName, componentName, location, propFullName) => { | ||
const componentNameSafe = componentName || '<<anonymous>>'; | ||
const propFullNameSafe = propFullName || propName; | ||
|
||
if (typeof props[propName] !== 'undefined') { | ||
return new Error( | ||
`The ${location} \`${propFullNameSafe}\` of ` + | ||
`\`${componentNameSafe}\` is deprecated. ${reason}`, | ||
); | ||
} | ||
|
||
return null; | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
// A change of the browser zoom change the scrollbar size. | ||
// Credit https://github.com/twbs/bootstrap/blob/3ffe3a5d82f6f561b82ff78d82b32a7d14aed558/js/src/modal.js#L512-L519 | ||
export default function getScrollbarSize(doc: Document): number { | ||
const scrollDiv = doc.createElement('div'); | ||
scrollDiv.style.width = '99px'; | ||
scrollDiv.style.height = '99px'; | ||
scrollDiv.style.position = 'absolute'; | ||
scrollDiv.style.top = '-9999px'; | ||
scrollDiv.style.overflow = 'scroll'; | ||
|
||
doc.body.appendChild(scrollDiv); | ||
const scrollbarSize = scrollDiv.offsetWidth - scrollDiv.clientWidth; | ||
doc.body.removeChild(scrollDiv); | ||
|
||
return scrollbarSize; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,3 +8,21 @@ export { default as getDisplayName } from './getDisplayName'; | |
export { default as HTMLElementType } from './HTMLElementType'; | ||
export { default as ponyfillGlobal } from './ponyfillGlobal'; | ||
export { default as refType } from './refType'; | ||
export { default as capitalize } from './capitalize'; | ||
export { default as createChainedFunction } from './createChainedFunction'; | ||
export { default as debounce } from './debounce'; | ||
export { default as deprecatedPropType } from './deprecatedPropType'; | ||
export { default as isMuiElement } from './isMuiElement'; | ||
export { default as ownerDocument } from './ownerDocument'; | ||
export { default as ownerWindow } from './ownerWindow'; | ||
export { default as requirePropFactory } from './requirePropFactory'; | ||
export { default as setRef } from './setRef'; | ||
export { default as unstable_useEnhancedEffect } from './useEnhancedEffect'; | ||
export { default as unstable_useId } from './useId'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need the unstable prefix if we treat all the modules here private? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If they're here, they're public. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I forgot where I said but I would be way more comfortable by renaming Or we release it as an alpha. Either way, I agree that the current state of /utils being on the same release line as /core (5.x) does not signal that the package is private. It's simply not enough that we treat it as private since we have some responsibility to not clutter npm with more underdocumented packages. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we then proceed with #23270 move all utils in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I don't understand what that solves. If we change the API of
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Considering that some of the utils are required by the system, it would mean the system needs to depend on unstyled. I assume that it's not something we want because the two are solving independent problems.
I think that this option can work 👍. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alright, let me update the exports then There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done, everything in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We're importing What did we try to accomplish by adding (and not moving) /core/utils to /utils? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't want to make changes to the existing |
||
export { default as unsupportedProp } from './unsupportedProp'; | ||
export { default as useControlled } from './useControlled'; | ||
export { default as useEventCallback } from './useEventCallback'; | ||
export { default as useForkRef } from './useForkRef'; | ||
export { default as useIsFocusVisible } from './useIsFocusVisible'; | ||
export { default as getScrollbarSize } from './getScrollbarSize'; | ||
export { detectScrollType, getNormalizedScrollLeft } from './scrollLeft'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import * as React from 'react'; | ||
|
||
export type ClassNameMap<ClassKey extends string = string> = Record<ClassKey, string>; | ||
|
||
export interface StyledComponentProps<ClassKey extends string = string> { | ||
/** | ||
* Override or extend the styles applied to the component. | ||
*/ | ||
classes?: Partial<ClassNameMap<ClassKey>>; | ||
innerRef?: React.Ref<any>; | ||
} | ||
|
||
/** | ||
* @private ONLY USE FROM WITHIN mui-org/material-ui | ||
* | ||
* Internal helper type for conform (describeConformance) components that are decorated with `withStyles | ||
* However, we don't declare classes on this type. | ||
* It is recommended to declare them manually with an interface so that each class can have a separate JSDOC. | ||
*/ | ||
export type StandardProps<C, Removals extends keyof C = never> = Omit<C, 'classes' | Removals> & | ||
// each component declares it's classes in a separate interface for proper JSDOC | ||
StyledComponentProps<never> & { | ||
ref?: C extends { ref?: infer RefType } ? RefType : React.Ref<unknown>; | ||
// TODO: Remove implicit props. Up to each component. | ||
className?: string; | ||
style?: React.CSSProperties; | ||
}; | ||
|
||
export type NamedMuiComponent = React.ComponentType & { muiName: string }; | ||
|
||
export interface NamedMuiElement { | ||
type: NamedMuiComponent; | ||
props: StandardProps<{}>; | ||
key: string | number | null; | ||
} | ||
|
||
export default function isMuiElement(element: any, muiNames: string[]): element is NamedMuiElement { | ||
// @ts-ignore | ||
return React.isValidElement(element) && muiNames.indexOf(element.type.muiName) !== -1; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export default function ownerDocument(node: Node | undefined): Document { | ||
return (node && node.ownerDocument) || document; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import ownerDocument from './ownerDocument'; | ||
|
||
export default function ownerWindow(node: Node | undefined): Window { | ||
const doc = ownerDocument(node); | ||
return doc.defaultView || window; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
export default function requirePropFactory(componentNameInError: string): any { | ||
if (process.env.NODE_ENV === 'production') { | ||
return () => null; | ||
} | ||
|
||
const requireProp = (requiredProp: string): any => ( | ||
props: { [key: string]: any }, | ||
propName: string, | ||
componentName: string, | ||
location: string, | ||
propFullName: string, | ||
) => { | ||
const propFullNameSafe = propFullName || propName; | ||
|
||
if (typeof props[propName] !== 'undefined' && !props[requiredProp]) { | ||
return new Error( | ||
`The prop \`${propFullNameSafe}\` of ` + | ||
`\`${componentNameInError}\` must be used on \`${requiredProp}\`.`, | ||
); | ||
} | ||
|
||
return null; | ||
}; | ||
return requireProp; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
// Source from https://github.com/alitaheri/normalize-scroll-left | ||
let cachedType: string; | ||
|
||
/** | ||
* Based on the jquery plugin https://github.com/othree/jquery.rtl-scroll-type | ||
* | ||
* Types of scrollLeft, assuming scrollWidth=100 and direction is rtl. | ||
* | ||
* Type | <- Most Left | Most Right -> | Initial | ||
* ---------------- | ------------ | ------------- | ------- | ||
* default | 0 | 100 | 100 | ||
* negative (spec*) | -100 | 0 | 0 | ||
* reverse | 100 | 0 | 0 | ||
* | ||
* Edge 85: default | ||
* Safari 14: negative | ||
* Chrome 85: negative | ||
* Firefox 81: negative | ||
* IE11: reverse | ||
* | ||
* spec* https://drafts.csswg.org/cssom-view/#dom-window-scroll | ||
*/ | ||
export function detectScrollType(): string { | ||
if (cachedType) { | ||
return cachedType; | ||
} | ||
|
||
const dummy = document.createElement('div'); | ||
const container = document.createElement('div'); | ||
container.style.width = '10px'; | ||
container.style.height = '1px'; | ||
dummy.appendChild(container); | ||
dummy.dir = 'rtl'; | ||
dummy.style.fontSize = '14px'; | ||
dummy.style.width = '4px'; | ||
dummy.style.height = '1px'; | ||
dummy.style.position = 'absolute'; | ||
dummy.style.top = '-1000px'; | ||
dummy.style.overflow = 'scroll'; | ||
|
||
document.body.appendChild(dummy); | ||
|
||
cachedType = 'reverse'; | ||
|
||
if (dummy.scrollLeft > 0) { | ||
cachedType = 'default'; | ||
} else { | ||
dummy.scrollLeft = 1; | ||
if (dummy.scrollLeft === 0) { | ||
cachedType = 'negative'; | ||
} | ||
} | ||
|
||
document.body.removeChild(dummy); | ||
return cachedType; | ||
} | ||
|
||
// Based on https://stackoverflow.com/a/24394376 | ||
export function getNormalizedScrollLeft(element: Element, direction: string): number { | ||
const scrollLeft = element.scrollLeft; | ||
|
||
// Perform the calculations only when direction is rtl to avoid messing up the ltr behavior | ||
if (direction !== 'rtl') { | ||
return scrollLeft; | ||
} | ||
|
||
const type = detectScrollType(); | ||
|
||
switch (type) { | ||
case 'negative': | ||
return element.scrollWidth - element.clientWidth + scrollLeft; | ||
case 'reverse': | ||
return element.scrollWidth - element.clientWidth - scrollLeft; | ||
default: | ||
return scrollLeft; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import * as React from 'react'; | ||
|
||
/** | ||
* TODO v5: consider to make it private | ||
mnajdova marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* | ||
* passes {value} to {ref} | ||
* | ||
* WARNING: Be sure to only call this inside a callback that is passed as a ref. | ||
* Otherwise make sure to cleanup previous {ref} if it changes. See | ||
mnajdova marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* https://github.com/mui-org/material-ui/issues/13539 | ||
* | ||
* useful if you want to expose the ref of an inner component to the public api | ||
mnajdova marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* while still using it inside the component | ||
mnajdova marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* @param ref a ref callback or ref object if anything falsy this is a no-op | ||
mnajdova marked this conversation as resolved.
Show resolved
Hide resolved
|
||
*/ | ||
export default function setRef<T>( | ||
ref: React.MutableRefObject<T | null> | ((instance: T | null) => void) | null | undefined, | ||
value: T | null, | ||
): void { | ||
if (typeof ref === 'function') { | ||
ref(value); | ||
} else if (ref) { | ||
ref.current = value; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export default function unsupportedProp( | ||
props: { [key: string]: any }, | ||
propName: string, | ||
componentName: string, | ||
location: string, | ||
propFullName: string | ||
): Error | null; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
export default function unsupportedProp( | ||
props: { [key: string]: any }, | ||
propName: string, | ||
componentName: string, | ||
location: string, | ||
propFullName: string, | ||
): Error | null { | ||
if (process.env.NODE_ENV === 'production') { | ||
return null; | ||
} | ||
|
||
const propFullNameSafe = propFullName || propName; | ||
|
||
if (typeof props[propName] !== 'undefined') { | ||
return new Error(`The prop \`${propFullNameSafe}\` is not supported. Please remove it.`); | ||
} | ||
|
||
return null; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@eps1lon FYI we have this issue with the macros
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you
@ts-expect-error
this instead for now so that we don't sacrifice the runtime behavior for typings?