@restart/ui
Advanced tools
Comparing version 0.0.4 to 0.0.5
@@ -13,3 +13,3 @@ import * as React from 'react'; | ||
tabIndex?: number; | ||
tagName?: string; | ||
tagName?: keyof JSX.IntrinsicElements; | ||
} | ||
@@ -29,5 +29,6 @@ export declare function isTrivialHref(href?: string): boolean; | ||
} | ||
export declare function useButtonProps({ tagName, disabled, href, target, rel, onClick, tabIndex, type, }: UseButtonPropsOptions): [AriaButtonProps, { | ||
tagName: string; | ||
}]; | ||
export interface UseButtonPropsMetadata { | ||
tagName: React.ElementType; | ||
} | ||
export declare function useButtonProps({ tagName, disabled, href, target, rel, onClick, tabIndex, type, }: UseButtonPropsOptions): [AriaButtonProps, UseButtonPropsMetadata]; | ||
export interface BaseButtonProps { | ||
@@ -38,3 +39,3 @@ /** | ||
*/ | ||
as?: string | undefined; | ||
as?: keyof JSX.IntrinsicElements | undefined; | ||
/** The disabled state of the button */ | ||
@@ -41,0 +42,0 @@ disabled?: boolean | undefined; |
@@ -94,3 +94,3 @@ "use strict"; | ||
const [buttonProps, { | ||
tagName | ||
tagName: Component | ||
}] = useButtonProps(Object.assign({ | ||
@@ -100,3 +100,2 @@ tagName: asProp, | ||
}, props)); | ||
const Component = tagName; | ||
return /*#__PURE__*/(0, _jsxRuntime.jsx)(Component, Object.assign({}, props, buttonProps, { | ||
@@ -103,0 +102,0 @@ ref: ref |
@@ -12,5 +12,6 @@ import * as React from 'react'; | ||
} | ||
export declare type ToggleEvent = React.SyntheticEvent | KeyboardEvent | MouseEvent; | ||
export interface ToggleMetadata { | ||
source: string | undefined; | ||
originalEvent: React.SyntheticEvent | Event | undefined; | ||
originalEvent: ToggleEvent | undefined; | ||
} | ||
@@ -17,0 +18,0 @@ export interface DropdownProps { |
@@ -27,19 +27,99 @@ import * as React from 'react'; | ||
className?: string; | ||
/** | ||
* Set the visibility of the Modal | ||
*/ | ||
show?: boolean; | ||
/** | ||
* A DOM element, a `ref` to an element, or function that returns either. The Modal is appended to it's `container` element. | ||
* | ||
*/ | ||
container?: DOMContainer; | ||
/** | ||
* A callback fired when the Modal is opening. | ||
*/ | ||
onShow?: () => void; | ||
/** | ||
* A callback fired when either the backdrop is clicked, or the escape key is pressed. | ||
* | ||
* The `onHide` callback only signals intent from the Modal, | ||
* you must actually set the `show` prop to `false` for the Modal to close. | ||
*/ | ||
onHide?: () => void; | ||
/** | ||
* A ModalManager instance used to track and manage the state of open | ||
* Modals. Useful when customizing how modals interact within a container | ||
*/ | ||
manager?: ModalManager; | ||
/** | ||
* Include a backdrop component. | ||
*/ | ||
backdrop?: true | false | 'static'; | ||
/** | ||
* A function that returns the dialog component. Useful for custom | ||
* rendering. **Note:** the component should make sure to apply the provided ref. | ||
* | ||
* ```js static | ||
* renderDialog={props => <MyDialog {...props} />} | ||
* ``` | ||
*/ | ||
renderDialog?: (props: RenderModalDialogProps) => React.ReactNode; | ||
/** | ||
* A function that returns a backdrop component. Useful for custom | ||
* backdrop rendering. | ||
* | ||
* ```js | ||
* renderBackdrop={props => <MyBackdrop {...props} />} | ||
* ``` | ||
*/ | ||
renderBackdrop?: (props: RenderModalBackdropProps) => React.ReactNode; | ||
/** | ||
* A callback fired when the escape key, if specified in `keyboard`, is pressed. | ||
* | ||
* If preventDefault() is called on the keyboard event, closing the modal will be cancelled. | ||
*/ | ||
onEscapeKeyDown?: (e: KeyboardEvent) => void; | ||
/** | ||
* A callback fired when the backdrop, if specified, is clicked. | ||
*/ | ||
onBackdropClick?: (e: React.SyntheticEvent) => void; | ||
containerClassName?: string; | ||
/** | ||
* Close the modal when escape key is pressed | ||
*/ | ||
keyboard?: boolean; | ||
/** | ||
* A `react-transition-group` `<Transition/>` component used | ||
* to control animations for the dialog component. | ||
*/ | ||
transition?: ModalTransitionComponent; | ||
/** | ||
* A `react-transition-group` `<Transition/>` component used | ||
* to control animations for the backdrop components. | ||
*/ | ||
backdropTransition?: ModalTransitionComponent; | ||
/** | ||
* When `true` The modal will automatically shift focus to itself when it opens, and | ||
* replace it to the last focused element when it closes. This also | ||
* works correctly with any Modal children that have the `autoFocus` prop. | ||
* | ||
* Generally this should never be set to `false` as it makes the Modal less | ||
* accessible to assistive technologies, like screen readers. | ||
*/ | ||
autoFocus?: boolean; | ||
/** | ||
* When `true` The modal will prevent focus from leaving the Modal while open. | ||
* | ||
* Generally this should never be set to `false` as it makes the Modal less | ||
* accessible to assistive technologies, like screen readers. | ||
*/ | ||
enforceFocus?: boolean; | ||
/** | ||
* When `true` The modal will restore focus to previously focused element once | ||
* modal is hidden | ||
*/ | ||
restoreFocus?: boolean; | ||
/** | ||
* Options passed to focus function when `restoreFocus` is set to `true` | ||
* | ||
* @link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#Parameters | ||
*/ | ||
restoreFocusOptions?: { | ||
@@ -46,0 +126,0 @@ preventScroll: boolean; |
166
cjs/Modal.js
@@ -14,4 +14,2 @@ "use strict"; | ||
var _propTypes = _interopRequireDefault(require("prop-types")); | ||
var React = _interopRequireWildcard(require("react")); | ||
@@ -57,3 +55,3 @@ | ||
return Object.assign(modal.current, { | ||
add: (container, className) => modalManager.add(modal.current, container, className), | ||
add: () => modalManager.add(modal.current), | ||
remove: () => modalManager.remove(modal.current), | ||
@@ -91,3 +89,2 @@ isTopModal: () => modalManager.isTopModal(modal.current), | ||
container: containerRef, | ||
containerClassName, | ||
onShow, | ||
@@ -102,3 +99,3 @@ onHide = () => {}, | ||
} = _ref, | ||
rest = _objectWithoutPropertiesLoose(_ref, ["show", "role", "className", "style", "children", "backdrop", "keyboard", "onBackdropClick", "onEscapeKeyDown", "transition", "backdropTransition", "autoFocus", "enforceFocus", "restoreFocus", "restoreFocusOptions", "renderDialog", "renderBackdrop", "manager", "container", "containerClassName", "onShow", "onHide", "onExit", "onExited", "onExiting", "onEnter", "onEntering", "onEntered"]); | ||
rest = _objectWithoutPropertiesLoose(_ref, ["show", "role", "className", "style", "children", "backdrop", "keyboard", "onBackdropClick", "onEscapeKeyDown", "transition", "backdropTransition", "autoFocus", "enforceFocus", "restoreFocus", "restoreFocusOptions", "renderDialog", "renderBackdrop", "manager", "container", "onShow", "onHide", "onExit", "onExited", "onExiting", "onEnter", "onEntering", "onEntered"]); | ||
@@ -124,3 +121,3 @@ const container = (0, _useWaitForDOMRef.default)(containerRef); | ||
const handleShow = (0, _useEventCallback.default)(() => { | ||
modal.add(container, containerClassName); | ||
modal.add(); | ||
removeKeydownListenerRef.current = (0, _listen.default)(document, 'keydown', handleDocumentKeyDown); | ||
@@ -279,160 +276,3 @@ removeFocusListenerRef.current = (0, _listen.default)(document, 'focus', // the timeout is necessary b/c this will run before the new modal is mounted | ||
}); | ||
const propTypes = { | ||
/** | ||
* Set the visibility of the Modal | ||
*/ | ||
show: _propTypes.default.bool, | ||
/** | ||
* A DOM element, a `ref` to an element, or function that returns either. The Modal is appended to it's `container` element. | ||
* | ||
* For the sake of assistive technologies, the container should usually be the document body, so that the rest of the | ||
* page content can be placed behind a virtual backdrop as well as a visual one. | ||
*/ | ||
container: _propTypes.default.any, | ||
/** | ||
* A callback fired when the Modal is opening. | ||
*/ | ||
onShow: _propTypes.default.func, | ||
/** | ||
* A callback fired when either the backdrop is clicked, or the escape key is pressed. | ||
* | ||
* The `onHide` callback only signals intent from the Modal, | ||
* you must actually set the `show` prop to `false` for the Modal to close. | ||
*/ | ||
onHide: _propTypes.default.func, | ||
/** | ||
* Include a backdrop component. | ||
*/ | ||
backdrop: _propTypes.default.oneOfType([_propTypes.default.bool, _propTypes.default.oneOf(['static'])]), | ||
/** | ||
* A function that returns the dialog component. Useful for custom | ||
* rendering. **Note:** the component should make sure to apply the provided ref. | ||
* | ||
* ```js static | ||
* renderDialog={props => <MyDialog {...props} />} | ||
* ``` | ||
*/ | ||
renderDialog: _propTypes.default.func, | ||
/** | ||
* A function that returns a backdrop component. Useful for custom | ||
* backdrop rendering. | ||
* | ||
* ```js | ||
* renderBackdrop={props => <MyBackdrop {...props} />} | ||
* ``` | ||
*/ | ||
renderBackdrop: _propTypes.default.func, | ||
/** | ||
* A callback fired when the escape key, if specified in `keyboard`, is pressed. | ||
* | ||
* If preventDefault() is called on the keyboard event, closing the modal will be cancelled. | ||
*/ | ||
onEscapeKeyDown: _propTypes.default.func, | ||
/** | ||
* A callback fired when the backdrop, if specified, is clicked. | ||
*/ | ||
onBackdropClick: _propTypes.default.func, | ||
/** | ||
* A css class or set of classes applied to the modal container when the modal is open, | ||
* and removed when it is closed. | ||
*/ | ||
containerClassName: _propTypes.default.string, | ||
/** | ||
* Close the modal when escape key is pressed | ||
*/ | ||
keyboard: _propTypes.default.bool, | ||
/** | ||
* A `react-transition-group@2.0.0` `<Transition/>` component used | ||
* to control animations for the dialog component. | ||
*/ | ||
transition: _propTypes.default.elementType, | ||
/** | ||
* A `react-transition-group@2.0.0` `<Transition/>` component used | ||
* to control animations for the backdrop components. | ||
*/ | ||
backdropTransition: _propTypes.default.elementType, | ||
/** | ||
* When `true` The modal will automatically shift focus to itself when it opens, and | ||
* replace it to the last focused element when it closes. This also | ||
* works correctly with any Modal children that have the `autoFocus` prop. | ||
* | ||
* Generally this should never be set to `false` as it makes the Modal less | ||
* accessible to assistive technologies, like screen readers. | ||
*/ | ||
autoFocus: _propTypes.default.bool, | ||
/** | ||
* When `true` The modal will prevent focus from leaving the Modal while open. | ||
* | ||
* Generally this should never be set to `false` as it makes the Modal less | ||
* accessible to assistive technologies, like screen readers. | ||
*/ | ||
enforceFocus: _propTypes.default.bool, | ||
/** | ||
* When `true` The modal will restore focus to previously focused element once | ||
* modal is hidden | ||
*/ | ||
restoreFocus: _propTypes.default.bool, | ||
/** | ||
* Options passed to focus function when `restoreFocus` is set to `true` | ||
* | ||
* @link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#Parameters | ||
*/ | ||
restoreFocusOptions: _propTypes.default.shape({ | ||
preventScroll: _propTypes.default.bool | ||
}), | ||
/** | ||
* Callback fired before the Modal transitions in | ||
*/ | ||
onEnter: _propTypes.default.func, | ||
/** | ||
* Callback fired as the Modal begins to transition in | ||
*/ | ||
onEntering: _propTypes.default.func, | ||
/** | ||
* Callback fired after the Modal finishes transitioning in | ||
*/ | ||
onEntered: _propTypes.default.func, | ||
/** | ||
* Callback fired right before the Modal transitions out | ||
*/ | ||
onExit: _propTypes.default.func, | ||
/** | ||
* Callback fired as the Modal begins to transition out | ||
*/ | ||
onExiting: _propTypes.default.func, | ||
/** | ||
* Callback fired after the Modal finishes transitioning out | ||
*/ | ||
onExited: _propTypes.default.func, | ||
/** | ||
* A ModalManager instance used to track and manage the state of open | ||
* Modals. Useful when customizing how modals interact within a container | ||
*/ | ||
manager: _propTypes.default.instanceOf(_ModalManager.default) | ||
}; | ||
Modal.displayName = 'Modal'; | ||
Modal.propTypes = propTypes; | ||
@@ -439,0 +279,0 @@ var _default = Object.assign(Modal, { |
@@ -5,30 +5,29 @@ export interface ModalInstance { | ||
} | ||
export declare type ContainerState = Record<string, any> & { | ||
isOverflowing?: boolean; | ||
style?: Partial<CSSStyleDeclaration>; | ||
modals: ModalInstance[]; | ||
export declare type ContainerState = { | ||
scrollBarWidth: number; | ||
style: Record<string, any>; | ||
[key: string]: any; | ||
}; | ||
export declare const OPEN_DATA_ATTRIBUTE: "data-rr-ui-modal-open"; | ||
/** | ||
* Proper state management for containers and the modals in those containers. | ||
* | ||
* @internal Used by the Modal to ensure proper styling of containers. | ||
* Manages a stack of Modals as well as ensuring | ||
* body scrolling is is disabled and padding accounted for | ||
*/ | ||
declare class ModalManager { | ||
readonly hideSiblingNodes: boolean; | ||
readonly handleContainerOverflow: boolean; | ||
readonly isRTL: boolean; | ||
readonly modals: ModalInstance[]; | ||
readonly containers: HTMLElement[]; | ||
readonly data: ContainerState[]; | ||
readonly scrollbarSize: number; | ||
constructor({ hideSiblingNodes, handleContainerOverflow, isRTL, }?: { | ||
hideSiblingNodes?: boolean | undefined; | ||
private state; | ||
constructor({ handleContainerOverflow, isRTL }?: { | ||
handleContainerOverflow?: boolean | undefined; | ||
isRTL?: boolean | undefined; | ||
}); | ||
isContainerOverflowing(modal: ModalInstance): any; | ||
containerIndexFromModal(modal: ModalInstance): number; | ||
setContainerStyle(containerState: ContainerState, container: HTMLElement): void; | ||
removeContainerStyle(containerState: ContainerState, container: HTMLElement): void; | ||
add(modal: ModalInstance, container: HTMLElement, className?: string): number; | ||
getScrollbarWidth(): number; | ||
getElement(): HTMLElement; | ||
setModalAttributes(_modal: ModalInstance): void; | ||
removeModalAttributes(_modal: ModalInstance): void; | ||
setContainerStyle(containerState: ContainerState): void; | ||
reset(): void; | ||
removeContainerStyle(containerState: ContainerState): void; | ||
add(modal: ModalInstance): number; | ||
remove(modal: ModalInstance): void; | ||
@@ -35,0 +34,0 @@ isTopModal(modal: ModalInstance): boolean; |
"use strict"; | ||
exports.__esModule = true; | ||
exports.default = void 0; | ||
exports.default = exports.OPEN_DATA_ATTRIBUTE = void 0; | ||
var _addClass = _interopRequireDefault(require("dom-helpers/addClass")); | ||
var _removeClass = _interopRequireDefault(require("dom-helpers/removeClass")); | ||
var _css = _interopRequireDefault(require("dom-helpers/css")); | ||
var _scrollbarSize = _interopRequireDefault(require("dom-helpers/scrollbarSize")); | ||
var _DataKey = require("./DataKey"); | ||
var _isOverflowing = _interopRequireDefault(require("./isOverflowing")); | ||
var _getScrollbarWidth = _interopRequireDefault(require("./getScrollbarWidth")); | ||
var _manageAriaHidden = require("./manageAriaHidden"); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
function findIndexOf(arr, cb) { | ||
let idx = -1; | ||
arr.some((d, i) => { | ||
if (cb(d, i)) { | ||
idx = i; | ||
return true; | ||
} | ||
const OPEN_DATA_ATTRIBUTE = (0, _DataKey.dataAttr)('modal-open'); | ||
/** | ||
* Manages a stack of Modals as well as ensuring | ||
* body scrolling is is disabled and padding accounted for | ||
*/ | ||
return false; | ||
}); | ||
return idx; | ||
} | ||
exports.OPEN_DATA_ATTRIBUTE = OPEN_DATA_ATTRIBUTE; | ||
/** | ||
* Proper state management for containers and the modals in those containers. | ||
* | ||
* @internal Used by the Modal to ensure proper styling of containers. | ||
*/ | ||
class ModalManager { | ||
constructor({ | ||
hideSiblingNodes = true, | ||
handleContainerOverflow = true, | ||
isRTL = false | ||
} = {}) { | ||
this.hideSiblingNodes = hideSiblingNodes; | ||
this.handleContainerOverflow = handleContainerOverflow; | ||
this.isRTL = isRTL; | ||
this.modals = []; | ||
this.containers = []; | ||
this.data = []; | ||
this.scrollbarSize = (0, _scrollbarSize.default)(); | ||
} | ||
isContainerOverflowing(modal) { | ||
const data = this.data[this.containerIndexFromModal(modal)]; | ||
return data ? data.overflowing : false; | ||
getScrollbarWidth() { | ||
return (0, _getScrollbarWidth.default)(); | ||
} | ||
containerIndexFromModal(modal) { | ||
return findIndexOf(this.data, d => d.modals.indexOf(modal) !== -1); | ||
getElement() { | ||
return document.body; | ||
} | ||
setContainerStyle(containerState, container) { | ||
setModalAttributes(_modal) {// For overriding | ||
} | ||
removeModalAttributes(_modal) {// For overriding | ||
} | ||
setContainerStyle(containerState) { | ||
const style = { | ||
@@ -69,2 +53,3 @@ overflow: 'hidden' | ||
const paddingProp = this.isRTL ? 'paddingLeft' : 'paddingRight'; | ||
const container = this.getElement(); | ||
containerState.style = { | ||
@@ -75,18 +60,24 @@ overflow: container.style.overflow, | ||
if (containerState.overflowing) { | ||
if (containerState.scrollBarWidth) { | ||
// use computed style, here to get the real padding | ||
// to add our scrollbar width | ||
style[paddingProp] = `${parseInt((0, _css.default)(container, paddingProp) || '0', 10) + this.scrollbarSize}px`; | ||
style[paddingProp] = `${parseInt((0, _css.default)(container, paddingProp) || '0', 10) + containerState.scrollBarWidth}px`; | ||
} | ||
container.setAttribute(OPEN_DATA_ATTRIBUTE, ''); | ||
(0, _css.default)(container, style); | ||
} | ||
removeContainerStyle(containerState, container) { | ||
reset() { | ||
[...this.modals].forEach(m => this.remove(m)); | ||
} | ||
removeContainerStyle(containerState) { | ||
const container = this.getElement(); | ||
container.removeAttribute(OPEN_DATA_ATTRIBUTE); | ||
Object.assign(container.style, containerState.style); | ||
} | ||
add(modal, container, className) { | ||
add(modal) { | ||
let modalIdx = this.modals.indexOf(modal); | ||
const containerIdx = this.containers.indexOf(container); | ||
@@ -99,26 +90,17 @@ if (modalIdx !== -1) { | ||
this.modals.push(modal); | ||
this.setModalAttributes(modal); | ||
if (this.hideSiblingNodes) { | ||
(0, _manageAriaHidden.hideSiblings)(container, modal); | ||
} | ||
if (containerIdx !== -1) { | ||
this.data[containerIdx].modals.push(modal); | ||
if (modalIdx !== 0) { | ||
return modalIdx; | ||
} | ||
const data = { | ||
modals: [modal], | ||
// right now only the first modal of a container will have its classes applied | ||
classes: className ? className.split(/\s+/) : [], | ||
overflowing: (0, _isOverflowing.default)(container) | ||
this.state = { | ||
scrollBarWidth: this.getScrollbarWidth(), | ||
style: {} | ||
}; | ||
if (this.handleContainerOverflow) { | ||
this.setContainerStyle(data, container); | ||
this.setContainerStyle(this.state); | ||
} | ||
data.classes.forEach(_addClass.default.bind(null, container)); | ||
this.containers.push(container); | ||
this.data.push(data); | ||
return modalIdx; | ||
@@ -134,31 +116,10 @@ } | ||
const containerIdx = this.containerIndexFromModal(modal); | ||
const data = this.data[containerIdx]; | ||
const container = this.containers[containerIdx]; | ||
data.modals.splice(data.modals.indexOf(modal), 1); | ||
this.modals.splice(modalIdx, 1); // if that was the last modal in a container, | ||
// clean up the container | ||
if (data.modals.length === 0) { | ||
data.classes.forEach(_removeClass.default.bind(null, container)); | ||
if (!this.modals.length && this.handleContainerOverflow) { | ||
this.removeContainerStyle(this.state); | ||
} | ||
if (this.handleContainerOverflow) { | ||
this.removeContainerStyle(data, container); | ||
} | ||
if (this.hideSiblingNodes) { | ||
(0, _manageAriaHidden.showSiblings)(container, modal); | ||
} | ||
this.containers.splice(containerIdx, 1); | ||
this.data.splice(containerIdx, 1); | ||
} else if (this.hideSiblingNodes) { | ||
// otherwise make sure the next top modal is visible to a SR | ||
const { | ||
backdrop, | ||
dialog | ||
} = data.modals[data.modals.length - 1]; | ||
(0, _manageAriaHidden.ariaHidden)(false, dialog); | ||
(0, _manageAriaHidden.ariaHidden)(false, backdrop); | ||
} | ||
this.removeModalAttributes(modal); | ||
} | ||
@@ -165,0 +126,0 @@ |
@@ -23,8 +23,26 @@ import * as React from 'react'; | ||
export interface TransitionCallbacks { | ||
/** | ||
* Callback fired before the component transitions in | ||
*/ | ||
onEnter?(node: HTMLElement, isAppearing: boolean): any; | ||
/** | ||
* Callback fired as the component begins to transition in | ||
*/ | ||
onEntering?(node: HTMLElement, isAppearing: boolean): any; | ||
/** | ||
* Callback fired after the component finishes transitioning in | ||
*/ | ||
onEntered?(node: HTMLElement, isAppearing: boolean): any; | ||
onEntering?(node: HTMLElement, isAppearing: boolean): any; | ||
/** | ||
* Callback fired right before the component transitions out | ||
*/ | ||
onExit?(node: HTMLElement): any; | ||
/** | ||
* Callback fired as the component begins to transition out | ||
*/ | ||
onExiting?(node: HTMLElement): any; | ||
/** | ||
* Callback fired after the component finishes transitioning out | ||
*/ | ||
onExited?(node: HTMLElement): any; | ||
onExiting?(node: HTMLElement): any; | ||
} | ||
@@ -31,0 +49,0 @@ export interface TransitionProps extends TransitionCallbacks { |
@@ -13,3 +13,3 @@ import * as React from 'react'; | ||
tabIndex?: number; | ||
tagName?: string; | ||
tagName?: keyof JSX.IntrinsicElements; | ||
} | ||
@@ -29,5 +29,6 @@ export declare function isTrivialHref(href?: string): boolean; | ||
} | ||
export declare function useButtonProps({ tagName, disabled, href, target, rel, onClick, tabIndex, type, }: UseButtonPropsOptions): [AriaButtonProps, { | ||
tagName: string; | ||
}]; | ||
export interface UseButtonPropsMetadata { | ||
tagName: React.ElementType; | ||
} | ||
export declare function useButtonProps({ tagName, disabled, href, target, rel, onClick, tabIndex, type, }: UseButtonPropsOptions): [AriaButtonProps, UseButtonPropsMetadata]; | ||
export interface BaseButtonProps { | ||
@@ -38,3 +39,3 @@ /** | ||
*/ | ||
as?: string | undefined; | ||
as?: keyof JSX.IntrinsicElements | undefined; | ||
/** The disabled state of the button */ | ||
@@ -41,0 +42,0 @@ disabled?: boolean | undefined; |
@@ -79,3 +79,3 @@ function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; } | ||
const [buttonProps, { | ||
tagName | ||
tagName: Component | ||
}] = useButtonProps(Object.assign({ | ||
@@ -85,3 +85,2 @@ tagName: asProp, | ||
}, props)); | ||
const Component = tagName; | ||
return /*#__PURE__*/_jsx(Component, Object.assign({}, props, buttonProps, { | ||
@@ -88,0 +87,0 @@ ref: ref |
@@ -12,5 +12,6 @@ import * as React from 'react'; | ||
} | ||
export declare type ToggleEvent = React.SyntheticEvent | KeyboardEvent | MouseEvent; | ||
export interface ToggleMetadata { | ||
source: string | undefined; | ||
originalEvent: React.SyntheticEvent | Event | undefined; | ||
originalEvent: ToggleEvent | undefined; | ||
} | ||
@@ -17,0 +18,0 @@ export interface DropdownProps { |
@@ -27,19 +27,99 @@ import * as React from 'react'; | ||
className?: string; | ||
/** | ||
* Set the visibility of the Modal | ||
*/ | ||
show?: boolean; | ||
/** | ||
* A DOM element, a `ref` to an element, or function that returns either. The Modal is appended to it's `container` element. | ||
* | ||
*/ | ||
container?: DOMContainer; | ||
/** | ||
* A callback fired when the Modal is opening. | ||
*/ | ||
onShow?: () => void; | ||
/** | ||
* A callback fired when either the backdrop is clicked, or the escape key is pressed. | ||
* | ||
* The `onHide` callback only signals intent from the Modal, | ||
* you must actually set the `show` prop to `false` for the Modal to close. | ||
*/ | ||
onHide?: () => void; | ||
/** | ||
* A ModalManager instance used to track and manage the state of open | ||
* Modals. Useful when customizing how modals interact within a container | ||
*/ | ||
manager?: ModalManager; | ||
/** | ||
* Include a backdrop component. | ||
*/ | ||
backdrop?: true | false | 'static'; | ||
/** | ||
* A function that returns the dialog component. Useful for custom | ||
* rendering. **Note:** the component should make sure to apply the provided ref. | ||
* | ||
* ```js static | ||
* renderDialog={props => <MyDialog {...props} />} | ||
* ``` | ||
*/ | ||
renderDialog?: (props: RenderModalDialogProps) => React.ReactNode; | ||
/** | ||
* A function that returns a backdrop component. Useful for custom | ||
* backdrop rendering. | ||
* | ||
* ```js | ||
* renderBackdrop={props => <MyBackdrop {...props} />} | ||
* ``` | ||
*/ | ||
renderBackdrop?: (props: RenderModalBackdropProps) => React.ReactNode; | ||
/** | ||
* A callback fired when the escape key, if specified in `keyboard`, is pressed. | ||
* | ||
* If preventDefault() is called on the keyboard event, closing the modal will be cancelled. | ||
*/ | ||
onEscapeKeyDown?: (e: KeyboardEvent) => void; | ||
/** | ||
* A callback fired when the backdrop, if specified, is clicked. | ||
*/ | ||
onBackdropClick?: (e: React.SyntheticEvent) => void; | ||
containerClassName?: string; | ||
/** | ||
* Close the modal when escape key is pressed | ||
*/ | ||
keyboard?: boolean; | ||
/** | ||
* A `react-transition-group` `<Transition/>` component used | ||
* to control animations for the dialog component. | ||
*/ | ||
transition?: ModalTransitionComponent; | ||
/** | ||
* A `react-transition-group` `<Transition/>` component used | ||
* to control animations for the backdrop components. | ||
*/ | ||
backdropTransition?: ModalTransitionComponent; | ||
/** | ||
* When `true` The modal will automatically shift focus to itself when it opens, and | ||
* replace it to the last focused element when it closes. This also | ||
* works correctly with any Modal children that have the `autoFocus` prop. | ||
* | ||
* Generally this should never be set to `false` as it makes the Modal less | ||
* accessible to assistive technologies, like screen readers. | ||
*/ | ||
autoFocus?: boolean; | ||
/** | ||
* When `true` The modal will prevent focus from leaving the Modal while open. | ||
* | ||
* Generally this should never be set to `false` as it makes the Modal less | ||
* accessible to assistive technologies, like screen readers. | ||
*/ | ||
enforceFocus?: boolean; | ||
/** | ||
* When `true` The modal will restore focus to previously focused element once | ||
* modal is hidden | ||
*/ | ||
restoreFocus?: boolean; | ||
/** | ||
* Options passed to focus function when `restoreFocus` is set to `true` | ||
* | ||
* @link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#Parameters | ||
*/ | ||
restoreFocusOptions?: { | ||
@@ -46,0 +126,0 @@ preventScroll: boolean; |
165
esm/Modal.js
@@ -8,3 +8,2 @@ function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; } | ||
import listen from 'dom-helpers/listen'; | ||
import PropTypes from 'prop-types'; | ||
import { useState, useRef, useCallback, useImperativeHandle, forwardRef, useEffect } from 'react'; | ||
@@ -36,3 +35,3 @@ import * as React from 'react'; | ||
return Object.assign(modal.current, { | ||
add: (container, className) => modalManager.add(modal.current, container, className), | ||
add: () => modalManager.add(modal.current), | ||
remove: () => modalManager.remove(modal.current), | ||
@@ -70,3 +69,2 @@ isTopModal: () => modalManager.isTopModal(modal.current), | ||
container: containerRef, | ||
containerClassName, | ||
onShow, | ||
@@ -81,3 +79,3 @@ onHide = () => {}, | ||
} = _ref, | ||
rest = _objectWithoutPropertiesLoose(_ref, ["show", "role", "className", "style", "children", "backdrop", "keyboard", "onBackdropClick", "onEscapeKeyDown", "transition", "backdropTransition", "autoFocus", "enforceFocus", "restoreFocus", "restoreFocusOptions", "renderDialog", "renderBackdrop", "manager", "container", "containerClassName", "onShow", "onHide", "onExit", "onExited", "onExiting", "onEnter", "onEntering", "onEntered"]); | ||
rest = _objectWithoutPropertiesLoose(_ref, ["show", "role", "className", "style", "children", "backdrop", "keyboard", "onBackdropClick", "onEscapeKeyDown", "transition", "backdropTransition", "autoFocus", "enforceFocus", "restoreFocus", "restoreFocusOptions", "renderDialog", "renderBackdrop", "manager", "container", "onShow", "onHide", "onExit", "onExited", "onExiting", "onEnter", "onEntering", "onEntered"]); | ||
@@ -103,3 +101,3 @@ const container = useWaitForDOMRef(containerRef); | ||
const handleShow = useEventCallback(() => { | ||
modal.add(container, containerClassName); | ||
modal.add(); | ||
removeKeydownListenerRef.current = listen(document, 'keydown', handleDocumentKeyDown); | ||
@@ -258,162 +256,5 @@ removeFocusListenerRef.current = listen(document, 'focus', // the timeout is necessary b/c this will run before the new modal is mounted | ||
}); | ||
const propTypes = { | ||
/** | ||
* Set the visibility of the Modal | ||
*/ | ||
show: PropTypes.bool, | ||
/** | ||
* A DOM element, a `ref` to an element, or function that returns either. The Modal is appended to it's `container` element. | ||
* | ||
* For the sake of assistive technologies, the container should usually be the document body, so that the rest of the | ||
* page content can be placed behind a virtual backdrop as well as a visual one. | ||
*/ | ||
container: PropTypes.any, | ||
/** | ||
* A callback fired when the Modal is opening. | ||
*/ | ||
onShow: PropTypes.func, | ||
/** | ||
* A callback fired when either the backdrop is clicked, or the escape key is pressed. | ||
* | ||
* The `onHide` callback only signals intent from the Modal, | ||
* you must actually set the `show` prop to `false` for the Modal to close. | ||
*/ | ||
onHide: PropTypes.func, | ||
/** | ||
* Include a backdrop component. | ||
*/ | ||
backdrop: PropTypes.oneOfType([PropTypes.bool, PropTypes.oneOf(['static'])]), | ||
/** | ||
* A function that returns the dialog component. Useful for custom | ||
* rendering. **Note:** the component should make sure to apply the provided ref. | ||
* | ||
* ```js static | ||
* renderDialog={props => <MyDialog {...props} />} | ||
* ``` | ||
*/ | ||
renderDialog: PropTypes.func, | ||
/** | ||
* A function that returns a backdrop component. Useful for custom | ||
* backdrop rendering. | ||
* | ||
* ```js | ||
* renderBackdrop={props => <MyBackdrop {...props} />} | ||
* ``` | ||
*/ | ||
renderBackdrop: PropTypes.func, | ||
/** | ||
* A callback fired when the escape key, if specified in `keyboard`, is pressed. | ||
* | ||
* If preventDefault() is called on the keyboard event, closing the modal will be cancelled. | ||
*/ | ||
onEscapeKeyDown: PropTypes.func, | ||
/** | ||
* A callback fired when the backdrop, if specified, is clicked. | ||
*/ | ||
onBackdropClick: PropTypes.func, | ||
/** | ||
* A css class or set of classes applied to the modal container when the modal is open, | ||
* and removed when it is closed. | ||
*/ | ||
containerClassName: PropTypes.string, | ||
/** | ||
* Close the modal when escape key is pressed | ||
*/ | ||
keyboard: PropTypes.bool, | ||
/** | ||
* A `react-transition-group@2.0.0` `<Transition/>` component used | ||
* to control animations for the dialog component. | ||
*/ | ||
transition: PropTypes.elementType, | ||
/** | ||
* A `react-transition-group@2.0.0` `<Transition/>` component used | ||
* to control animations for the backdrop components. | ||
*/ | ||
backdropTransition: PropTypes.elementType, | ||
/** | ||
* When `true` The modal will automatically shift focus to itself when it opens, and | ||
* replace it to the last focused element when it closes. This also | ||
* works correctly with any Modal children that have the `autoFocus` prop. | ||
* | ||
* Generally this should never be set to `false` as it makes the Modal less | ||
* accessible to assistive technologies, like screen readers. | ||
*/ | ||
autoFocus: PropTypes.bool, | ||
/** | ||
* When `true` The modal will prevent focus from leaving the Modal while open. | ||
* | ||
* Generally this should never be set to `false` as it makes the Modal less | ||
* accessible to assistive technologies, like screen readers. | ||
*/ | ||
enforceFocus: PropTypes.bool, | ||
/** | ||
* When `true` The modal will restore focus to previously focused element once | ||
* modal is hidden | ||
*/ | ||
restoreFocus: PropTypes.bool, | ||
/** | ||
* Options passed to focus function when `restoreFocus` is set to `true` | ||
* | ||
* @link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#Parameters | ||
*/ | ||
restoreFocusOptions: PropTypes.shape({ | ||
preventScroll: PropTypes.bool | ||
}), | ||
/** | ||
* Callback fired before the Modal transitions in | ||
*/ | ||
onEnter: PropTypes.func, | ||
/** | ||
* Callback fired as the Modal begins to transition in | ||
*/ | ||
onEntering: PropTypes.func, | ||
/** | ||
* Callback fired after the Modal finishes transitioning in | ||
*/ | ||
onEntered: PropTypes.func, | ||
/** | ||
* Callback fired right before the Modal transitions out | ||
*/ | ||
onExit: PropTypes.func, | ||
/** | ||
* Callback fired as the Modal begins to transition out | ||
*/ | ||
onExiting: PropTypes.func, | ||
/** | ||
* Callback fired after the Modal finishes transitioning out | ||
*/ | ||
onExited: PropTypes.func, | ||
/** | ||
* A ModalManager instance used to track and manage the state of open | ||
* Modals. Useful when customizing how modals interact within a container | ||
*/ | ||
manager: PropTypes.instanceOf(ModalManager) | ||
}; | ||
Modal.displayName = 'Modal'; | ||
Modal.propTypes = propTypes; | ||
export default Object.assign(Modal, { | ||
Manager: ModalManager | ||
}); |
@@ -5,30 +5,29 @@ export interface ModalInstance { | ||
} | ||
export declare type ContainerState = Record<string, any> & { | ||
isOverflowing?: boolean; | ||
style?: Partial<CSSStyleDeclaration>; | ||
modals: ModalInstance[]; | ||
export declare type ContainerState = { | ||
scrollBarWidth: number; | ||
style: Record<string, any>; | ||
[key: string]: any; | ||
}; | ||
export declare const OPEN_DATA_ATTRIBUTE: "data-rr-ui-modal-open"; | ||
/** | ||
* Proper state management for containers and the modals in those containers. | ||
* | ||
* @internal Used by the Modal to ensure proper styling of containers. | ||
* Manages a stack of Modals as well as ensuring | ||
* body scrolling is is disabled and padding accounted for | ||
*/ | ||
declare class ModalManager { | ||
readonly hideSiblingNodes: boolean; | ||
readonly handleContainerOverflow: boolean; | ||
readonly isRTL: boolean; | ||
readonly modals: ModalInstance[]; | ||
readonly containers: HTMLElement[]; | ||
readonly data: ContainerState[]; | ||
readonly scrollbarSize: number; | ||
constructor({ hideSiblingNodes, handleContainerOverflow, isRTL, }?: { | ||
hideSiblingNodes?: boolean | undefined; | ||
private state; | ||
constructor({ handleContainerOverflow, isRTL }?: { | ||
handleContainerOverflow?: boolean | undefined; | ||
isRTL?: boolean | undefined; | ||
}); | ||
isContainerOverflowing(modal: ModalInstance): any; | ||
containerIndexFromModal(modal: ModalInstance): number; | ||
setContainerStyle(containerState: ContainerState, container: HTMLElement): void; | ||
removeContainerStyle(containerState: ContainerState, container: HTMLElement): void; | ||
add(modal: ModalInstance, container: HTMLElement, className?: string): number; | ||
getScrollbarWidth(): number; | ||
getElement(): HTMLElement; | ||
setModalAttributes(_modal: ModalInstance): void; | ||
removeModalAttributes(_modal: ModalInstance): void; | ||
setContainerStyle(containerState: ContainerState): void; | ||
reset(): void; | ||
removeContainerStyle(containerState: ContainerState): void; | ||
add(modal: ModalInstance): number; | ||
remove(modal: ModalInstance): void; | ||
@@ -35,0 +34,0 @@ isTopModal(modal: ModalInstance): boolean; |
@@ -1,51 +0,35 @@ | ||
import addClass from 'dom-helpers/addClass'; | ||
import removeClass from 'dom-helpers/removeClass'; | ||
import css from 'dom-helpers/css'; | ||
import getScrollbarSize from 'dom-helpers/scrollbarSize'; | ||
import isOverflowing from './isOverflowing'; | ||
import { ariaHidden, hideSiblings, showSiblings } from './manageAriaHidden'; | ||
function findIndexOf(arr, cb) { | ||
let idx = -1; | ||
arr.some((d, i) => { | ||
if (cb(d, i)) { | ||
idx = i; | ||
return true; | ||
} | ||
return false; | ||
}); | ||
return idx; | ||
} | ||
import { dataAttr } from './DataKey'; | ||
import getBodyScrollbarWidth from './getScrollbarWidth'; | ||
export const OPEN_DATA_ATTRIBUTE = dataAttr('modal-open'); | ||
/** | ||
* Proper state management for containers and the modals in those containers. | ||
* | ||
* @internal Used by the Modal to ensure proper styling of containers. | ||
* Manages a stack of Modals as well as ensuring | ||
* body scrolling is is disabled and padding accounted for | ||
*/ | ||
class ModalManager { | ||
constructor({ | ||
hideSiblingNodes = true, | ||
handleContainerOverflow = true, | ||
isRTL = false | ||
} = {}) { | ||
this.hideSiblingNodes = hideSiblingNodes; | ||
this.handleContainerOverflow = handleContainerOverflow; | ||
this.isRTL = isRTL; | ||
this.modals = []; | ||
this.containers = []; | ||
this.data = []; | ||
this.scrollbarSize = getScrollbarSize(); | ||
} | ||
isContainerOverflowing(modal) { | ||
const data = this.data[this.containerIndexFromModal(modal)]; | ||
return data ? data.overflowing : false; | ||
getScrollbarWidth() { | ||
return getBodyScrollbarWidth(); | ||
} | ||
containerIndexFromModal(modal) { | ||
return findIndexOf(this.data, d => d.modals.indexOf(modal) !== -1); | ||
getElement() { | ||
return document.body; | ||
} | ||
setContainerStyle(containerState, container) { | ||
setModalAttributes(_modal) {// For overriding | ||
} | ||
removeModalAttributes(_modal) {// For overriding | ||
} | ||
setContainerStyle(containerState) { | ||
const style = { | ||
@@ -57,2 +41,3 @@ overflow: 'hidden' | ||
const paddingProp = this.isRTL ? 'paddingLeft' : 'paddingRight'; | ||
const container = this.getElement(); | ||
containerState.style = { | ||
@@ -63,18 +48,24 @@ overflow: container.style.overflow, | ||
if (containerState.overflowing) { | ||
if (containerState.scrollBarWidth) { | ||
// use computed style, here to get the real padding | ||
// to add our scrollbar width | ||
style[paddingProp] = `${parseInt(css(container, paddingProp) || '0', 10) + this.scrollbarSize}px`; | ||
style[paddingProp] = `${parseInt(css(container, paddingProp) || '0', 10) + containerState.scrollBarWidth}px`; | ||
} | ||
container.setAttribute(OPEN_DATA_ATTRIBUTE, ''); | ||
css(container, style); | ||
} | ||
removeContainerStyle(containerState, container) { | ||
reset() { | ||
[...this.modals].forEach(m => this.remove(m)); | ||
} | ||
removeContainerStyle(containerState) { | ||
const container = this.getElement(); | ||
container.removeAttribute(OPEN_DATA_ATTRIBUTE); | ||
Object.assign(container.style, containerState.style); | ||
} | ||
add(modal, container, className) { | ||
add(modal) { | ||
let modalIdx = this.modals.indexOf(modal); | ||
const containerIdx = this.containers.indexOf(container); | ||
@@ -87,26 +78,17 @@ if (modalIdx !== -1) { | ||
this.modals.push(modal); | ||
this.setModalAttributes(modal); | ||
if (this.hideSiblingNodes) { | ||
hideSiblings(container, modal); | ||
} | ||
if (containerIdx !== -1) { | ||
this.data[containerIdx].modals.push(modal); | ||
if (modalIdx !== 0) { | ||
return modalIdx; | ||
} | ||
const data = { | ||
modals: [modal], | ||
// right now only the first modal of a container will have its classes applied | ||
classes: className ? className.split(/\s+/) : [], | ||
overflowing: isOverflowing(container) | ||
this.state = { | ||
scrollBarWidth: this.getScrollbarWidth(), | ||
style: {} | ||
}; | ||
if (this.handleContainerOverflow) { | ||
this.setContainerStyle(data, container); | ||
this.setContainerStyle(this.state); | ||
} | ||
data.classes.forEach(addClass.bind(null, container)); | ||
this.containers.push(container); | ||
this.data.push(data); | ||
return modalIdx; | ||
@@ -122,31 +104,10 @@ } | ||
const containerIdx = this.containerIndexFromModal(modal); | ||
const data = this.data[containerIdx]; | ||
const container = this.containers[containerIdx]; | ||
data.modals.splice(data.modals.indexOf(modal), 1); | ||
this.modals.splice(modalIdx, 1); // if that was the last modal in a container, | ||
// clean up the container | ||
if (data.modals.length === 0) { | ||
data.classes.forEach(removeClass.bind(null, container)); | ||
if (!this.modals.length && this.handleContainerOverflow) { | ||
this.removeContainerStyle(this.state); | ||
} | ||
if (this.handleContainerOverflow) { | ||
this.removeContainerStyle(data, container); | ||
} | ||
if (this.hideSiblingNodes) { | ||
showSiblings(container, modal); | ||
} | ||
this.containers.splice(containerIdx, 1); | ||
this.data.splice(containerIdx, 1); | ||
} else if (this.hideSiblingNodes) { | ||
// otherwise make sure the next top modal is visible to a SR | ||
const { | ||
backdrop, | ||
dialog | ||
} = data.modals[data.modals.length - 1]; | ||
ariaHidden(false, dialog); | ||
ariaHidden(false, backdrop); | ||
} | ||
this.removeModalAttributes(modal); | ||
} | ||
@@ -153,0 +114,0 @@ |
@@ -23,8 +23,26 @@ import * as React from 'react'; | ||
export interface TransitionCallbacks { | ||
/** | ||
* Callback fired before the component transitions in | ||
*/ | ||
onEnter?(node: HTMLElement, isAppearing: boolean): any; | ||
/** | ||
* Callback fired as the component begins to transition in | ||
*/ | ||
onEntering?(node: HTMLElement, isAppearing: boolean): any; | ||
/** | ||
* Callback fired after the component finishes transitioning in | ||
*/ | ||
onEntered?(node: HTMLElement, isAppearing: boolean): any; | ||
onEntering?(node: HTMLElement, isAppearing: boolean): any; | ||
/** | ||
* Callback fired right before the component transitions out | ||
*/ | ||
onExit?(node: HTMLElement): any; | ||
/** | ||
* Callback fired as the component begins to transition out | ||
*/ | ||
onExiting?(node: HTMLElement): any; | ||
/** | ||
* Callback fired after the component finishes transitioning out | ||
*/ | ||
onExited?(node: HTMLElement): any; | ||
onExiting?(node: HTMLElement): any; | ||
} | ||
@@ -31,0 +49,0 @@ export interface TransitionProps extends TransitionCallbacks { |
{ | ||
"name": "@restart/ui", | ||
"version": "0.0.4", | ||
"version": "0.0.5", | ||
"description": "Utilities for creating robust overlay components", | ||
@@ -5,0 +5,0 @@ "author": { |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
334329
157
8101