react-md
Version:
This is the full react-md library bundled together for convenience.
1,396 lines (1,349 loc) • 1.63 MB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('react'), require('react-dom'), require('crypto')) :
typeof define === 'function' && define.amd ? define(['exports', 'react', 'react-dom', 'crypto'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ReactMD = {}, global.React, global.ReactDOM, global.crypto));
}(this, (function (exports, React, ReactDOM, crypto) { 'use strict';
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var React__default = /*#__PURE__*/_interopDefaultLegacy(React);
var ReactDOM__default = /*#__PURE__*/_interopDefaultLegacy(ReactDOM);
var crypto__default = /*#__PURE__*/_interopDefaultLegacy(crypto);
var classnames = {exports: {}};
/*!
Copyright (c) 2018 Jed Watson.
Licensed under the MIT License (MIT), see
http://jedwatson.github.io/classnames
*/
(function (module) {
/* global define */
(function () {
var hasOwn = {}.hasOwnProperty;
function classNames() {
var classes = [];
for (var i = 0; i < arguments.length; i++) {
var arg = arguments[i];
if (!arg) continue;
var argType = typeof arg;
if (argType === 'string' || argType === 'number') {
classes.push(arg);
} else if (Array.isArray(arg)) {
if (arg.length) {
var inner = classNames.apply(null, arg);
if (inner) {
classes.push(inner);
}
}
} else if (argType === 'object') {
if (arg.toString === Object.prototype.toString) {
for (var key in arg) {
if (hasOwn.call(arg, key) && arg[key]) {
classes.push(key);
}
}
} else {
classes.push(arg.toString());
}
}
}
return classes.join(' ');
}
if (module.exports) {
classNames.default = classNames;
module.exports = classNames;
} else {
window.classNames = classNames;
}
}());
}(classnames));
var cn = classnames.exports;
/**
* A utility function to get the current container for the portal. For SSR, the
* container will always be `null` since portals don't work server side.
*
* @param into - The element to portal into
* @param intoId - An id for an element to portal into
* @returns the portal container element or null
*/
function getContainer(into, intoId) {
if (typeof document === "undefined") {
return null;
}
var container = null;
if (typeof into === "undefined" && typeof intoId === "undefined") {
container = document.body;
}
else if (typeof intoId === "string") {
container = document.getElementById(intoId);
}
else if (typeof into === "string") {
container = document.querySelector(into);
}
else if (typeof into === "function") {
container = into();
}
else if (into) {
container = into;
}
return container;
}
/**
* This component is a simple wrapper for the `createPortal` API from ReactDOM
* that will just ensure that `null` is always returned for server side
* rendering as well as a "nice" way to choose specific portal targets or just
* falling back to the `document.body`.
*/
function Portal(_a) {
var into = _a.into, intoId = _a.intoId, children = _a.children;
var _b = React.useState(null), container = _b[0], setContainer = _b[1];
// setting the container via useEffect instead of immediately in the render
// just so that it doesn't throw an error immediately if the dom hasn't fully
// painted after a SSR
React.useEffect(function () {
var nextContainer = getContainer(into, intoId);
if (container !== nextContainer) {
setContainer(nextContainer);
}
}, [into, intoId, container]);
if (!container) {
return null;
}
return ReactDOM.createPortal(children, container);
}
/**
* This is a very simple component that is used in other places within react-md
* to conditionally render the children within a portal or not based on general
* portal config props.
*/
function ConditionalPortal(_a) {
var portal = _a.portal, portalInto = _a.portalInto, portalIntoId = _a.portalIntoId, children = _a.children;
if (!portal && !portalInto && !portalIntoId) {
return children;
}
return (React__default['default'].createElement(Portal, { into: portalInto, intoId: portalIntoId }, children));
}
/**
* A small utility function that allows me to apply a passed in ref along with
* my own custom ref logic.
*
* @param instance - The DOM Node instance
* @param ref - The prop ref
*/
function applyRef(instance, ref) {
if (!ref) {
return;
}
if (typeof ref === "function") {
ref(instance);
}
else if (typeof ref === "object") {
ref.current = instance;
}
}
function modify(base, modifier) {
if (!modifier) {
return base;
}
var hasOwn = Object.prototype.hasOwnProperty;
return Object.keys(modifier).reduce(function (s, mod) {
if (hasOwn.call(modifier, mod) && modifier[mod]) {
s = s + " " + base + "--" + mod;
}
return s;
}, base);
}
/**
* Applies the BEM styled class name to an element.
*
* @see https://en.bem.info/methodology/css/
* @param base - The base class to use
* @returns a function to call that generates the full class name
*/
function bem(base) {
/**
* Creates the full class name from the base block name. This can be called
* without any arguments which will just return the base block name (kind of
* worthless), or you can provide a child element name and modifiers.
*
* @param elementOrModifier - This is either the child element name or an
* object of modifiers to apply. This **must** be a string if the second
* argument is provided.
* @param modifier - Any optional modifiers to apply to the block and optional
* element.
* @returns the full class name
*/
return function block(elementOrModifier, modifier) {
if (!elementOrModifier) {
return base;
}
if (typeof elementOrModifier !== "string") {
return modify(base, elementOrModifier);
}
return modify(base + "__" + elementOrModifier, modifier);
};
}
var SHORTHAND_REGEX = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
var VERBOSE_REGEX = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i;
/**
* Converts a hex string into an rgb value. This is useful for deteching color
* contrast ratios and other stuff.
*
* @param hex - The hex string to convert
* @returns an object containing the r, g, b values for the color.
*/
function hexToRGB(hex) {
hex = hex.replace(SHORTHAND_REGEX, function (_m, r, g, b) { return "" + r + r + g + g + b + b; });
var result = hex.match(VERBOSE_REGEX) || [];
var r = parseInt(result[1] || "", 16) || 0;
var g = parseInt(result[2] || "", 16) || 0;
var b = parseInt(result[3] || "", 16) || 0;
return [r, g, b];
}
var RED_MULTIPLIER = 0.2126;
var GREEN_MULTIPLIER = 0.7152;
var BLUE_MULTIPLIER = 0.0722;
/**
* I really couldn't figure out how to name these "magic" numbers since the
* formula doesn't really describe it much:
*
* @see https://www.w3.org/TR/WCAG20-TECHS/G17.html#G17-tests
* @internal
*/
function get8BitColor(color) {
color /= 255;
if (color <= 0.03928) {
return color / 12.92;
}
return Math.pow(((color + 0.055) / 1.055), 2.4);
}
/**
* A number closest to 0 should be closest to black while a number closest to 1
* should be closest to white.
*
* @see https://www.w3.org/TR/WCAG20-TECHS/G17.html#G17-tests
* @internal
*/
function getLuminance(color) {
var _a = hexToRGB(color), r = _a[0], g = _a[1], b = _a[2];
var red = get8BitColor(r) * RED_MULTIPLIER;
var green = get8BitColor(g) * GREEN_MULTIPLIER;
var blue = get8BitColor(b) * BLUE_MULTIPLIER;
return red + green + blue;
}
/**
* Gets the contrast ratio between a background color and a foreground color.
*
* @see https://www.w3.org/TR/WCAG20-TECHS/G17.html#G17-tests
*
* @param background - The background color
* @param foreground - The foreground color. This is normally the `color` css
* value.
* @returns the contrast ratio between the background and foreground colors.
*/
function getContrastRatio(background, foreground) {
var backgroundLuminance = getLuminance(background) + 0.05;
var foregroundLuminance = getLuminance(foreground) + 0.05;
return (Math.max(backgroundLuminance, foregroundLuminance) /
Math.min(backgroundLuminance, foregroundLuminance));
}
/**
* The contrast ratio that can be used for large text where large text is
* considered 18pt or 14pt bold.
*/
var LARGE_TEXT_CONTRAST_RATIO = 3;
/**
* The contrast ratio that can be used for normal text.
*/
var NORMAL_TEXT_CONTRAST_RATIO = 4.5;
/**
* The AAA contrast ratio for passing WGAC 2.0 color contrast ratios.
*/
var AAA_CONTRAST_RATIO = 7;
/**
* Checks if there is an acceptable contrast ratio between the background and
* foreground colors based on the provided compliance level.
*
* @param background - The background color to check against
* @param foreground - The foreground color to check against
* @param compliance - The compliance level to use or a custom number as a
* ratio.
* @returns true if there is enough contrast between the foreground and
* background colors for the provided compliance level.
*/
function isContrastCompliant(background, foreground, compliance) {
if (compliance === void 0) { compliance = "normal"; }
var ratio;
switch (compliance) {
case "large":
ratio = LARGE_TEXT_CONTRAST_RATIO;
break;
case "normal":
ratio = NORMAL_TEXT_CONTRAST_RATIO;
break;
case "AAA":
ratio = AAA_CONTRAST_RATIO;
break;
default:
ratio = compliance;
}
return getContrastRatio(background, foreground) >= ratio;
}
/**
* Typeguard that will check if the provided checkable thing is a
* MutableRefObject or just an HTMLElement.
*
* @internal
*/
var isMutableRefObject = function (thing) {
return !!thing &&
typeof thing.current !== "undefined";
};
/**
* Gets the HTMLElement or null from the checkable thing.
*
* @internal
*/
var getElement$1 = function (thing) {
if (isMutableRefObject(thing)) {
return thing.current;
}
return thing;
};
/**
* Checks if a container element contains another element as a child while
* allowing for nulls or a MutableRefObject of HTMLElement or null. Mostly just
* a convenience function that should be used internally.
*
* @param container - The element to use as a container element. This can be an
* HTMLElement, null, or a MutableRefObject of HTMLElement or null.
* @param child - The element that might be a child of the container
* element. This can be an HTMLElement, null, or a MutableRefObject of
* HTMLElement or null.
* @returns True if the container contains the child element and both the
* container and child are valid HTMLElements (not null).
* @internal
*/
function containsElement(container, child) {
container = getElement$1(container);
child = getElement$1(child);
return !!(container && child && container.contains(child));
}
var __assign$hj = (undefined && undefined.__assign) || function () {
__assign$hj = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign$hj.apply(this, arguments);
};
/**
* This is normally used for reusable shareable configs that have multiple
* shared options with default values that should be used. This basically works
* just like `defaultProps` in react.
*
* @internal
* @param optional - The original object that has the optional/omitted values
* @param required - The required default values that should be used to fill the
* optional object with
* @returns a new object with both the values of the optional and required
* objects but use the optional values if they were defined.
*/
function defaults(optional, required) {
var keys = Object.keys(required);
return keys.reduce(function (result, key) {
if (typeof result[key] === "undefined") {
result[key] = required[key];
}
return result;
}, __assign$hj({}, optional));
}
var __rest$2k = (undefined && undefined.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
var context$b = React.createContext({
root: true,
dir: "ltr",
toggleDir: function () {
throw new Error("Tried to toggle the current writing direction without initializing the `Dir` component.");
},
});
var Provider$5 = context$b.Provider;
/**
* Gets the writing direction context which provides access to the current `dir`
* and a `toggleDir` function.
*
* @remarks \@since 2.3.0
*/
function useDir() {
var _a = React.useContext(context$b); _a.root; var current = __rest$2k(_a, ["root"]);
return current;
}
/**
* @remarks \@since 2.3.0
*/
var DEFAULT_DIR = function () {
var dir = "ltr";
if (typeof document !== "undefined") {
var rootDir = document.documentElement.getAttribute("dir");
dir = rootDir === "rtl" ? "rtl" : "ltr";
}
return dir;
};
/**
* The `Dir` component is used to handle the current writing direction within
* your app as well as conditionally updating the writing direction for small
* sections in your app. When this component is used for the first time near the
* root of your React component tree, the current direction will be applied to
* the root `<html>` element. Otherwise the current dir will be cloned into the
* child element so it can be passed as a prop.
*
* ```tsx
* // html element will be updated to have `dir="ltr"`
* ReactDOM.render(<Dir><App /></Dir>, root)
* ```
*
* ```tsx
* // html element will be updated to have `dir="rtl"` while the `<span>` will
* // now be `<span dir="ltr">`
* ReactDOM.render(
* <Dir defaultDir="rtl">
* <Some>
* <Other>
* <Components>
* <Dir defaultDir="ltr">
* <span>Content</span>
* </Dir>
* </Components>
* </Other>
* </Some>
* </Dir>,
* root
* );
* ```
*
* Note: Since the `dir` is cloned into the child element, you need to make sure
* that the child is either a DOM element or the `dir` prop is passed from your
* custom component.
*
* @remarks \@since 2.3.0
*/
function Dir(_a) {
var children = _a.children, _b = _a.defaultDir, defaultDir = _b === void 0 ? DEFAULT_DIR : _b;
var root = React.useContext(context$b).root;
var _c = React.useState(defaultDir), dir = _c[0], setDir = _c[1];
React.useEffect(function () {
if (!root || typeof document === "undefined") {
return;
}
document.documentElement.setAttribute("dir", dir);
return function () {
document.documentElement.removeAttribute("dir");
};
}, [dir, root]);
var toggleDir = React.useCallback(function () {
setDir(function (prevDir) { return (prevDir === "ltr" ? "rtl" : "ltr"); });
}, []);
var value = React.useMemo(function () { return ({ root: false, dir: dir, toggleDir: toggleDir }); }, [dir, toggleDir]);
var child = React.Children.only(children);
if (!root) {
child = React.cloneElement(child, { dir: dir });
}
return React__default['default'].createElement(Provider$5, { value: value }, child);
}
/* eslint-disable import/no-mutable-exports, getter-return */
var noop$b = function () {
// do nothing
};
var isSupported = false;
/**
* Checks if the browser supports passive events. This shouldn't really be used
* outside of this file, but you can always check again if needed.
*/
function update() {
if (typeof window === "undefined") {
return false;
}
var isPassiveEventsSupported = false;
var opts = Object.defineProperty({}, "passive", {
get: function () {
isPassiveEventsSupported = true;
},
});
window.addEventListener("testSupportsPassive", noop$b, opts);
window.removeEventListener("testSupportsPassive", noop$b, opts);
isSupported = isPassiveEventsSupported;
return isPassiveEventsSupported;
}
// invoke immediately
update();
var passiveEvents = /*#__PURE__*/Object.freeze({
__proto__: null,
update: update,
get isSupported () { return isSupported; }
});
var delegatedEvents = [];
/* eslint-disable @typescript-eslint/explicit-function-return-type */
/**
* Creates the delegated event handler that will run all the callbacks once an
* event happens. The callbacks' invocation can also be throttled for event
* types that trigger rapidly for additional performance.
*
* The `<K extends keyof WindowEventMap` is a nice thing I found while looking
* through the `lib.d.ts` implementation of `addEventListener` that will allow
* you to get the "correct" event type when using the `add` and `remove`
* functions once you have created this event handler. Otherwise there'd be ts
* errors trying to do `MouseEvent` or `KeyboardEvent` in your listeners.
*/
function createEventHandler(throttle, callbacks) {
var running = false;
var runCallbacks = function (event) { return function () {
for (var i = 0; i < callbacks.length; i += 1) {
callbacks[i](event);
}
running = false;
}; };
return function eventHandler(event) {
if (!throttle) {
runCallbacks(event)();
return;
}
if (running) {
return;
}
running = true;
window.requestAnimationFrame(runCallbacks(event));
};
}
/* eslint-enable @typescript-eslint/explicit-function-return-type */
/**
* Creates a throttled event handler for the provided event type and event
* target.
*/
function createDelegatedEventHandler(eventType, eventTarget, throttle, options) {
if (eventTarget === void 0) { eventTarget = window; }
if (throttle === void 0) { throttle = false; }
var callbacks = [];
var handler = createEventHandler(throttle, callbacks);
return {
/**
* Attempts to add the provided callback to the list of callbacks for the
* throttled event. If this is the first callback to be added, the throttled
* event will also be started.
*/
add: function (callback) {
if (!callbacks.length) {
eventTarget.addEventListener(eventType, handler, options);
}
if (callbacks.indexOf(callback) === -1) {
callbacks.push(callback);
}
},
/**
* Attempts to remove the provided callback from the lsit of callbacks for
* the throttled event. If this is the last callback that was removed, the
* throttled event will also be stopped.
*/
remove: function (callback) {
var i = callbacks.indexOf(callback);
if (i >= 0) {
callbacks.splice(i, 1);
if (!callbacks.length) {
eventTarget.removeEventListener(eventType, handler, options);
}
}
},
};
}
/**
* Creates a delegated event listener using custom events. Most of this code
* comes from the MDN about resize listeners.
*
* This will return an object for adding or removing event handlers for the
* provided `eventType` since only one base throttled event listener will be
* created. Each callback that is added will be called with the event each time
* the event is triggered. This does mean that you will manually need to remove
* your callback like normal or else it can be called when no longer in use.
* This also means that it doesn't "hurt" to call this function without
* immediately calling the `add` function since the event won't start until
* there is at least 1 callback.
*
* @see https://developer.mozilla.org/en-US/docs/Web/Events/resize#Examples
* @param eventType - One of the event types that should be used to create a
* delegated event for. This should be things like resize, click, scroll, etc.
* @param eventTarget - The target that should have the delegated event handler
* attached to. This is normally the window, but can be any element as needed.
* @param throttle - Boolean if the event should be throttled or not. Normally
* only event types like resize or scroll should be throttled for performance
* boosts, but anything can be.
* @returns The delegated event handler that allows you to add or remove
* `EventListener`s to that event.
*/
function delegateEvent(eventType, eventTarget, throttle, options) {
if (eventTarget === void 0) { eventTarget = window; }
if (throttle === void 0) { throttle = eventType === "resize" || eventType === "scroll"; }
var index = delegatedEvents.findIndex(function (event) {
return event.type === eventType &&
event.target === eventTarget &&
event.options === options &&
event.throttle === throttle;
});
if (index === -1) {
delegatedEvents.push({
type: eventType,
target: eventTarget,
options: options,
throttle: throttle,
handler: createDelegatedEventHandler(eventType, eventTarget, throttle, options),
});
index = delegatedEvents.length - 1;
}
return delegatedEvents[index].handler;
}
var __assign$hi = (undefined && undefined.__assign) || function () {
__assign$hi = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign$hi.apply(this, arguments);
};
/**
* A helper function for manually setting touch events on elements when they
* cannot be directly added with a React event listener. This will attempt to
* create a passive event if the browser supports passive events so there is
* better scroll performance.
*/
function setTouchEvent(
/**
* Boolean if the event should be added or removed.
*/
add,
/**
* The element to add the touch event to.
*/
el,
/**
* One of the touch types to modify.
*/
eventType,
/**
* The touch event callback function to use.
*/
callback,
/**
* Boolean if the event should be captured if the browser does not support
* passive events.
*/
capture,
/**
* Any additional options to provide to the passive event.
*/
options) {
if (capture === void 0) { capture = false; }
el[(add ? "add" : "remove") + "EventListener"]("touch" + eventType,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
callback, isSupported ? __assign$hi({ passive: true, capture: capture }, options) : capture);
}
/**
* A simple wrapper for the `setTouchEvent` to just always add events.
*/
function addTouchEvent(
/**
* The element to add the touch event to.
*/
el,
/**
* One of the touch types to modify.
*/
eventType,
/**
* The touch event callback function to use.
*/
callback,
/**
* Boolean if the event should be captured if the browser does not support
* passive events.
*/
capture,
/**
* Any additional options to provide to the passive event.
*/
options) {
if (capture === void 0) { capture = false; }
setTouchEvent(true, el, eventType, callback, capture, options);
}
/**
* A simple wrapper for the `setTouchEvent` to just always remove events.
*
* @param el - The element to add the touch event to.
* @param eventType - One of the touch types to modify.
*/
function removeTouchEvent(el, eventType,
/**
* The touch event callback function to use.
*/
callback,
/**
* Boolean if the event should be captured if the browser does not support
* passive events.
*/
capture,
/**
* Any additional options to provide to the passive event.
*/
options) {
if (capture === void 0) { capture = false; }
setTouchEvent(false, el, eventType, callback, capture, options);
}
/**
* This hook allows you to provide anything that should be "cached" and puts it
* into a ref that'll be updated each render. This is pretty overkill for most
* places, but it's really nice when you want to create event handlers that
* shouldn't update if the developer used arrow functions to define callbacks.
* (A great example is for ref callbacks that *shouldn't* be triggered each
* render. But that might just be a programming error instead).
*
* @param cacheable - The cacheable thing that gets updated after each render.
* @returns a mutable ref object containing the current cache.
*/
function useRefCache(cacheable) {
var ref = React.useRef(cacheable);
React.useEffect(function () {
ref.current = cacheable;
});
return ref;
}
/**
* This hook will create a performant scroll listener by enabling passive events
* if it's supported by the browser and delegating the event as needed.
*/
function useScrollListener(_a) {
var _b = _a.enabled, enabled = _b === void 0 ? true : _b, onScroll = _a.onScroll, element = _a.element, _c = _a.options, options = _c === void 0 ? isSupported ? { passive: true } : false : _c;
var callback = useRefCache(onScroll);
React.useEffect(function () {
if (!enabled) {
return;
}
var eventHandler = delegateEvent("scroll", element || window, true, options);
var handler = function (event) { return callback.current(event); };
eventHandler.add(handler);
return function () {
eventHandler.remove(handler);
};
// disabled since useRefCache
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [enabled, element, options]);
}
/**
* This is a simple component wrapper for the `useScrollListener` hook.
*/
function ScrollListener(props) {
useScrollListener(props);
return null;
}
/**
* Gets the current percentage based on the min, max, and current value.
*
* @param min - the min value
* @param max - the max value
* @param value - the current value to compare against
* @returns the percentage that the `value` is between the `min` and `max`
* values.
*/
function getPercentage(min, max, value) {
if (min >= max) {
throw new RangeError("A range must have the min value less than the max value");
}
if (value > max || value < min) {
throw new RangeError("A value must be between the min and max values");
}
var range = max - min;
var start = value - min;
var percentage = start / range;
return Math.max(0, Math.min(Math.abs(percentage), 1));
}
/**
* The amount of time a user must hover an element before the temporary element
* becomes visible.
*
* @remarks \@since 2.8.0
*/
var DEFAULT_HOVER_MODE_VISIBLE_IN_TIME = 1000;
/**
* The amount of time the user must no longer hover any element attached to the
* {@link HoverModeProvider} to disable the hover mode.
*
* @remarks \@since 2.8.0
*/
var DEFAULT_HOVER_MODE_DEACTIVATION_TIME = 1000;
/**
* The amount of time the user must not hover any element attached to the same
* instance of the {@link useHoverMode} hook when the using the sticky mode.
*
* @remarks \@since 2.8.0
*/
var DEFAULT_HOVER_MODE_STICKY_EXIT_TIME = 300;
/**
* A simple hook that only triggers the callback when a component is unmounted.
* This will make sure that the callback function does not have a stale closure
* by the time the component unmounts as well.
*
* @example
* Simple Example
* ```ts
* useOnUnmount(() => {
* console.log('Component is unmounted.');
* });
*
* const [data, setData] = useState(initialData);
* useOnUnmount(() => {
* API.saveCurrentData(data);
* });
*
* // update data
* ```
*
* @remarks \@since 2.7.1
* @param callback - the function to call when the component unmounts.
*/
function useOnUnmount(callback) {
var ref = React.useRef(callback);
React.useEffect(function () {
ref.current = callback;
});
return React.useEffect(function () { return function () { return ref.current(); }; }, []);
}
/** @internal */
var noop$a = function () {
// do nothing
};
/** @internal */
var context$a = React.createContext({
visibleInTime: DEFAULT_HOVER_MODE_VISIBLE_IN_TIME,
enableHoverMode: noop$a,
disableHoverMode: noop$a,
startDisableTimer: noop$a,
});
/**
* @internal
* @remarks \@since 2.8.0
*/
var HoverModeContextProvider = context$a.Provider;
/**
* Gets the {@link HoverModeContext} which allows you implement hover mode
* functionality for any component. This is mostly an internal hook since
* everything you need will be available in the {@link useHoverMode} hook.
*
* @internal
* @remarks \@since 2.8.0
* @returns The {@link HoverModeContext}
*/
function useHoverModeContext() {
return React.useContext(context$a);
}
/**
* This component should normally be mounted near the root of your app to enable
* hover mode for child components. However, it can also be used at other levels
* if hover mode functionality should not carry over between two different parts
* of the screen.
*
* @example
* Separating Hover Mode
* ```tsx
* export default function Example(): ReactElement {
* return (
* <>
* <HoverModeProvider>
* <HeaderActions />
* </HoverModeProvider>
* <HoverModeProvider>
* <MainContent />
* </HoverModeProvider>
* </>
* );
* }
* ```
*
* @remarks \@since 2.8.0
*/
function HoverModeProvider(_a) {
var children = _a.children, _b = _a.disabled, disabled = _b === void 0 ? false : _b, _c = _a.defaultVisibleInTime, defaultVisibleInTime = _c === void 0 ? DEFAULT_HOVER_MODE_VISIBLE_IN_TIME : _c, _d = _a.deactivateTime, deactivateTime = _d === void 0 ? DEFAULT_HOVER_MODE_DEACTIVATION_TIME : _d;
var _e = React.useState(defaultVisibleInTime), visibleInTime = _e[0], setVisibleInTime = _e[1];
var timeoutRef = React.useRef();
var enableHoverMode = React.useCallback(function () {
if (disabled) {
return;
}
window.clearTimeout(timeoutRef.current);
setVisibleInTime(0);
}, [disabled]);
var disableHoverMode = React.useCallback(function () {
window.clearTimeout(timeoutRef.current);
setVisibleInTime(defaultVisibleInTime);
}, [defaultVisibleInTime]);
var startDisableTimer = React.useCallback(function () {
window.clearTimeout(timeoutRef.current);
timeoutRef.current = window.setTimeout(function () {
setVisibleInTime(defaultVisibleInTime);
}, deactivateTime);
}, [defaultVisibleInTime, deactivateTime]);
React.useEffect(function () {
if (disabled) {
window.clearTimeout(timeoutRef.current);
setVisibleInTime(defaultVisibleInTime);
}
}, [disabled, defaultVisibleInTime]);
useOnUnmount(function () {
window.clearTimeout(timeoutRef.current);
});
var context = React.useMemo(function () { return ({
visibleInTime: visibleInTime,
enableHoverMode: enableHoverMode,
disableHoverMode: disableHoverMode,
startDisableTimer: startDisableTimer,
}); }, [disableHoverMode, enableHoverMode, startDisableTimer, visibleInTime]);
return (React__default['default'].createElement(HoverModeContextProvider, { value: context }, children));
}
/**
* This is copy/pasted from react-redux which has some more information about
* this and how to fix "invalid" warnings while running tests.
*
* @see https://github.com/reduxjs/react-redux/blob/4c907c0870c6b9a136dd69be294c17d1dc63c8f5/src/utils/useIsomorphicLayoutEffect.js
*/
var useIsomorphicLayoutEffect = typeof window !== "undefined" &&
typeof window.document !== "undefined" &&
typeof window.document.createElement !== "undefined"
? React.useLayoutEffect
: React.useEffect;
/**
* @internal
*/
var TOUCH_TIMEOUT = 1200;
/**
* This hook helps determine the current interaction mode by attaching the
* required event listeners to the window. The `mode` will always be defaulted
* to `mouse` at first since it has the least possibilities of causing errors
* with styles since the mouse-only styles are normally just `:hover` effects.
*
* ## Switching between modes:
*
* ### While in `mouse` mode:
*
* - any `keydown` event will switch to `keyboard` mode
* - this does have the side effect of meta keys also causing the switch over,
* but it feels fine since it helps show the current focus in the document
* as well
* - any `touchstart` event will switch to `touch` mode
*
* ### While in `keyboard` mode:
*
* - any `mousedown` event will switch to `mouse` mode
* - it is perfectly okay to move the mouse while in keyboard mode, but still
* want to keep the keyboard styles until the user actually starts clicking
* - any `touchstart` event will switch to `touch` mode
*
* ### While in `touch` mode:
*
* - any `mousemove` event will switch to `mouse` mode, but **only** if there
* hasn't been a `contextmenu` event within the last `1.2s`
* - you can really only switch back to `mouse` mode if you are using the
* devtools to emulate devices OR using a touch-desktop. I don't know how
* common this really is though.
* - touching the screen will always fire a `mousemove` event (which is why
* the `:hover` styles are normally with `rmd-utils-mouse-only`) and even
* after the `contextmenu` event. Normally want to go back to `mouse` mode
* when the mouse re-enters the `window`
*
* Note: It's currently impossible to switch from `touch` to `keyboard`
* immediately. You'd first need to switch to `mouse` and then to `keyboard`. I
* don't really know of any use-cases other than the weird touch-desktop stuff
* and I have no experience using them.
*
* @internal
*/
function useInteractionMode() {
var _a = React.useState("mouse"), mode = _a[0], setMode = _a[1];
var lastTouchTime = React.useRef(0);
var isTouchContextMenu = React.useRef(false);
useIsomorphicLayoutEffect(function () {
var enableMouseMode = function () { return setMode("mouse"); };
var enableKeyboardMode = function () { return setMode("keyboard"); };
var handleTouchStart = function () {
lastTouchTime.current = Date.now();
isTouchContextMenu.current = false;
setMode("touch");
};
var handleMouseMove = function () {
if (isTouchContextMenu.current ||
Date.now() - lastTouchTime.current < TOUCH_TIMEOUT) {
isTouchContextMenu.current = false;
return;
}
enableMouseMode();
};
var handleContextMenu = function () {
isTouchContextMenu.current = true;
};
var className = "rmd-utils--" + mode;
document.body.classList.add(className);
window.addEventListener("touchstart", handleTouchStart, true);
if (mode === "mouse") {
window.addEventListener("keydown", enableKeyboardMode, true);
}
else if (mode === "keyboard") {
window.addEventListener("mousedown", enableMouseMode, true);
}
else {
window.addEventListener("mousemove", handleMouseMove, true);
window.addEventListener("contextmenu", handleContextMenu, true);
}
return function () {
document.body.classList.remove(className);
window.removeEventListener("touchstart", handleTouchStart, true);
if (mode === "mouse") {
window.removeEventListener("keydown", enableKeyboardMode, true);
}
else if (mode === "keyboard") {
window.removeEventListener("mousedown", enableMouseMode, true);
}
else {
window.removeEventListener("mousemove", handleMouseMove, true);
window.removeEventListener("contextmenu", handleContextMenu, true);
}
};
}, [mode]);
return mode;
}
/**
* @internal
*/
var modeContext = React.createContext("mouse");
/**
* @internal
*/
var parentContext = React.createContext(false);
/**
* @internal
*/
var UserInteractionModeProvider = modeContext.Provider;
/**
* @internal
*/
var ParentProvider = parentContext.Provider;
/**
* Returns the current user interaction mode.
*
* @returns {@link UserInteractionMode}
*/
function useUserInteractionMode() {
return React.useContext(modeContext);
}
/**
* Example:
*
* ```ts
* const isKeyboard = useIsUserInteractionMode("keyboard");
* // do stuff if keyboard only
* ```
*
* @param mode - The {@link UserInteractionMode} to check against.
* @returns `true` if the current user interaction mode matches the provided
* mode.
*/
function useIsUserInteractionMode(mode) {
return useInteractionMode() === mode;
}
/**
* This component is used to determine how the user is current interacting with
* your app as well as modifying the `document.body`'s `className` with the
* current mode. This is what allows the `rmd-utils-phone-only`,
* `rmd-utils-keyboard-only`, and `rmd-utils-mouse-only` mixins to work.
*
* @remarks \@since 2.6.0 Renamed from `InteractionModeListener`
* @throws When this component has been mounted multiple times in your app.
*/
function UserInteractionModeListener(_a) {
var children = _a.children;
var mode = useInteractionMode();
if (React.useContext(parentContext)) {
throw new Error("Mounted multiple `UserInteractionModeListener` components.");
}
return (React__default['default'].createElement(UserInteractionModeProvider, { value: mode },
React__default['default'].createElement(ParentProvider, { value: true }, children)));
}
var __assign$hh = (undefined && undefined.__assign) || function () {
__assign$hh = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign$hh.apply(this, arguments);
};
function useHoverMode(_a) {
var _b = _a === void 0 ? {} : _a, _c = _b.disabled, disabled = _c === void 0 ? false : _c, _d = _b.sticky, sticky = _d === void 0 ? false : _d, propOnClick = _b.onClick, propOnMouseEnter = _b.onMouseEnter, propOnMouseLeave = _b.onMouseLeave, _e = _b.defaultVisible, defaultVisible = _e === void 0 ? false : _e, _f = _b.exitVisibilityDelay, exitVisibilityDelay = _f === void 0 ? sticky ? DEFAULT_HOVER_MODE_STICKY_EXIT_TIME : 0 : _f;
var mode = useUserInteractionMode();
var isTouch = mode === "touch";
var _g = React.useState(defaultVisible), visible = _g[0], setVisible = _g[1];
var _h = React.useState(false), stuck = _h[0], setStuck = _h[1];
var timeoutRef = React.useRef();
var skipReset = React.useRef(defaultVisible);
var _j = useHoverModeContext(), visibleInTime = _j.visibleInTime, enableHoverMode = _j.enableHoverMode, disableHoverMode = _j.disableHoverMode, startDisableTimer = _j.startDisableTimer;
var active = visibleInTime === 0;
React.useEffect(function () {
if (sticky && !visible) {
setStuck(false);
}
}, [visible, sticky]);
useOnUnmount(function () {
window.clearTimeout(timeoutRef.current);
});
React.useEffect(function () {
if (disabled) {
return;
}
var reset = function () {
setVisible(false);
disableHoverMode();
window.clearTimeout(timeoutRef.current);
};
// this is just used so the `defaultOption` can be used
if (!skipReset.current) {
reset();
}
skipReset.current = false;
window.addEventListener("mousedown", reset);
return function () {
window.removeEventListener("mousedown", reset);
};
}, [disableHoverMode, mode, disabled]);
var onMouseEnter = React.useCallback(function (event) {
propOnMouseEnter === null || propOnMouseEnter === void 0 ? void 0 : propOnMouseEnter(event);
if (stuck || disabled || isTouch || event.isPropagationStopped()) {
return;
}
window.clearTimeout(timeoutRef.current);
if (visibleInTime === 0) {
enableHoverMode();
setVisible(true);
return;
}
timeoutRef.current = window.setTimeout(function () {
enableHoverMode();
setVisible(true);
}, visibleInTime);
}, [disabled, enableHoverMode, isTouch, propOnMouseEnter, stuck, visibleInTime]);
var onMouseLeave = React.useCallback(function (event) {
propOnMouseLeave === null || propOnMouseLeave === void 0 ? void 0 : propOnMouseLeave(event);
if (stuck || disabled || isTouch || event.isPropagationStopped()) {
return;
}
startDisableTimer();
window.clearTimeout(timeoutRef.current);
if (exitVisibilityDelay === 0) {
setVisible(false);
return;
}
timeoutRef.current = window.setTimeout(function () {
setVisible(false);
}, exitVisibilityDelay);
}, [
disabled,
exitVisibilityDelay,
isTouch,
propOnMouseLeave,
startDisableTimer,
stuck,
]);
var onClick = React.useCallback(function (event) {
propOnClick === null || propOnClick === void 0 ? void 0 : propOnClick(event);
if (event.isPropagationStopped() || disabled) {
return;
}
startDisableTimer();
window.clearTimeout(timeoutRef.current);
}, [disabled, propOnClick, startDisableTimer]);
var onStickyClick = React.useCallback(function (event) {
propOnClick === null || propOnClick === void 0 ? void 0 : propOnClick(event);
if (event.isPropagationStopped() || disabled) {
return;
}
if (!stuck) {
setStuck(true);
setVisible(true);
disableHoverMode();
}
else {
setStuck(false);
setVisible(function (prevVisible) { return !prevVisible; });
}
}, [disableHoverMode, disabled, propOnClick, stuck]);
var handlers = {
onClick: onClick,
onMouseEnter: onMouseEnter,
onMouseLeave: onMouseLeave,
};
var stickyHandlers;
if (sticky) {
stickyHandlers = __assign$hh(__assign$hh({}, handlers), { onClick: onStickyClick });
}
return {
active: active,
stuck: sticky ? stuck : undefined,
visible: visible,
setVisible: setVisible,
handlers: handlers,
stickyHandlers: stickyHandlers,
enableHoverMode: enableHoverMode,
disableHoverMode: disableHoverMode,
startDisableTimer: startDisableTimer,
};
}
var DEFAULT_DESKTOP_MIN_WIDTH = 1025 / 16 + "em";
var DEFAULT_TABLET_MIN_WIDTH = 768 / 16 + "em";
var DEFAULT_TABLET_MAX_WIDTH = 1024 / 16 + "em";
var DEFAULT_PHONE_MAX_WIDTH = 767 / 16 + "em";
var DEFAULT_DESKTOP_LARGE_MIN_WIDTH = 1280 / 16 + "em";
/**
* An extremely simple "pollyfill" for the `window.screen.orientation` just for
* the `type` value that is required for the `useOrientation` hook.
*/
var getOrientationType = function () {
var _a;
var screenOrientation = (_a = window.screen.orientation) === null || _a === void 0 ? void 0 : _a.type;
if (typeof screenOrientation === "string") {
return screenOrientation;
}
var _b = window.screen, availHeight = _b.availHeight, availWidth = _b.availWidth;
return availHeight > availWidth ? "portrait-primary" : "landscape-primary";
};
/**
* This media query is used to determine the current orientation of the app
* based on the `window.screen.orientation.type`. This will always be
* `"landscape-primary"` server side unless a default value is provided.
*
* @param defaultValue - an optional default value to use. When this is omitted,
* it will default to `"landscape-primary"` unless the `window` is defined. If
* the `window` is defined, it will immediately check the orientation type on
* mount.
* @returns the orientation type value.
*/
function useOrientation$1(defaultValue) {
var _a = React.useState(function () {
if (defaultValue) {
return defaultValue;
}
if (typeof window !== "undefined") {
return getOrientationType();
}
return "landscape-primary";
}), value = _a[0], setValue = _a[1];
React.useEffect(function () {
if (typeof window === "undefined") {
return;
}
var handler = function () {
setValue(getOrientationType());
};
window.addEventListener("orientationchange", handler);
return function () { return window.removeEventListener("orientationchange", handler); };
}, []);
return value;
}
/**
* A helper hook that is used to create a memoized media query tester for
* `window.matchMedia`.
*
* Note: This is a **client side only** hook as it requires the `window` to
* attach a resize event listener to.
*
* @param query - The media query to use
* @param defaultValue - The default value for if this media query matches. When
* this is `undefined`, it will default to `false` unless the `window` is
* defined and the `checkImmediately` param was not set to `false`. Otherwise,
* it will check the media query matches on mount and use that value.
* @param disabled - Boolean if the media query checking should be disabled.
* @param checkImmediately - Boolean if the media query should be checked
* immediately on mount. When omittied, it will default to checking when the
* window is defined.
* @returns true if the media query is a match.
*/
function useMediaQuery(query, defaultValue, disabled, checkImmediately) {
if (disabled === void 0) { disabled = false; }
if (checkImmediately === void 0) { checkImmediately = typeof window !== "undefined"; }
var _a = React.useState(function () {
if (typeof defaultValue !== "undefined") {
return defaultValue;
}
if (!disabled && checkImmediately && typeof window !== "undefined") {
return window.matchMedia(query).matches;
}
return false;
}), matches = _a[0], setMatches = _a[1];
React.useEffect(function () {
if (typeof window === "undefined" || disabled) {
return;
}
var mq = window.matchMedia(query);
var updater = function (_a) {
var matches = _a.matches;
return setMatches(matches);
};
mq.addEventListener("change", updater);
if (mq.matches !== matches) {
setMatches(mq.matches);
}
return function () {
mq.removeEventListener("change", updater);
};
}, [disabled, matches, query]);
return matches;
}
/**
* This is a small helper that will create a media query block based on the
* provided width value.
*/
var toWidthPart = function (v, prefix) {
var type = typeof v;
if (type === "undefined") {
return "";
}
var value = type === "number" ? v + "px" : v;
return "(" + prefix + "-width: " + value + ")";
};
/**
* This is a simple hoo that will create a memoized media query string with the
* provided min anx max values.
*
* @param min - An optional min value to use
* @param max - An optional max value to use
* @returns a boolean if the current media query is a match.
*/
function useWidthMediaQuery(_a) {
var min = _a.min, max = _a.max;
var query = React.useMemo(function () {
var parts = [toWidthPart(min, "min"), toWidthPart(max, "max")]
.filter(Boolean)
.join(" and ");
return "screen and " + parts;
}, [min, max]);
return useMediaQuery(query);
}
var DEFAULT_APP_SIZE = {
isPhone: false,
isTa