diff --git a/package.json b/package.json index fcdc7617a..50ebd71a2 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "types": "src/types/shepherd.d.ts", "module": "dist/js/shepherd.esm.js", "dependencies": { + "auto-bind": "^2.1.0", "body-scroll-lock": "^2.6.3", "core-js": "^3.1.4", "element-matches": "^0.1.2", diff --git a/src/js/step.js b/src/js/step.js index 9ba051b8e..e9a639831 100644 --- a/src/js/step.js +++ b/src/js/step.js @@ -1,6 +1,8 @@ -import { isElement, isFunction, isUndefined } from './utils/type-check'; +import autoBind from 'auto-bind'; + import { Evented } from './evented.js'; -import { bindAdvance, bindButtonEvents, bindCancelLink, bindMethods } from './utils/bind.js'; +import { isElement, isFunction, isUndefined } from './utils/type-check'; +import { bindAdvance, bindButtonEvents, bindCancelLink } from './utils/bind.js'; import { createFromHTML, setupTooltip, parseAttachTo } from './utils/general.js'; // Polyfills @@ -106,23 +108,10 @@ export class Step extends Evented { constructor(tour, options) { super(tour, options); this.tour = tour; - bindMethods.call(this, [ - '_scrollTo', - '_setupElements', - '_show', - 'cancel', - 'complete', - 'destroy', - 'hide', - 'isOpen', - 'show' - ]); + + autoBind(this); + this._setOptions(options); - this.bindAdvance = bindAdvance.bind(this); - this.bindButtonEvents = bindButtonEvents.bind(this); - this.bindCancelLink = bindCancelLink.bind(this); - this.setupTooltip = setupTooltip.bind(this); - this.parseAttachTo = parseAttachTo.bind(this); return this; } @@ -239,7 +228,7 @@ export class Step extends Evented { `` ); footer.appendChild(button); - this.bindButtonEvents(cfg, button); + bindButtonEvents(cfg, button, this); }); content.appendChild(footer); @@ -258,7 +247,7 @@ export class Step extends Evented { header.appendChild(link); element.classList.add('shepherd-has-cancel-link'); - this.bindCancelLink(link); + bindCancelLink(link, this); } } @@ -396,7 +385,7 @@ export class Step extends Evented { * @private */ _scrollTo(scrollToOptions) { - const { element } = this.parseAttachTo(); + const { element } = parseAttachTo(this); if (isFunction(this.options.scrollToHandler)) { this.options.scrollToHandler(element); @@ -438,10 +427,10 @@ export class Step extends Evented { this._addKeyDownHandler(this.el); if (this.options.advanceOn) { - this.bindAdvance(); + bindAdvance(this); } - this.setupTooltip(); + setupTooltip(this); } /** diff --git a/src/js/tour.js b/src/js/tour.js index d0fe437ce..79ed85259 100644 --- a/src/js/tour.js +++ b/src/js/tour.js @@ -1,12 +1,12 @@ -import { isFunction, isNumber, isString, isUndefined } from './utils/type-check'; +import autoBind from 'auto-bind'; +import { disableBodyScroll, clearAllBodyScrollLocks } from 'body-scroll-lock/lib/bodyScrollLock.es6.js'; +import tippy from 'tippy.js'; + import { Evented } from './evented.js'; import { Modal } from './modal.js'; import { Step } from './step.js'; -import { bindMethods } from './utils/bind.js'; -import tippy from 'tippy.js'; -import { disableBodyScroll, clearAllBodyScrollLocks } from 'body-scroll-lock/lib/bodyScrollLock.es6.js'; +import { isFunction, isNumber, isString, isUndefined } from './utils/type-check'; import { defaults as tooltipDefaults } from './utils/tooltip-defaults'; - import { cleanupSteps, cleanupStepEventListeners } from './utils/cleanup'; import { getElementForStep } from './utils/dom'; import { toggleShepherdModalClass } from './utils/modal'; @@ -47,13 +47,9 @@ export class Tour extends Evented { */ constructor(options = {}) { super(options); - bindMethods.call(this, [ - 'back', - 'cancel', - 'complete', - 'hide', - 'next' - ]); + + autoBind(this); + this.options = options; this.steps = this.options.steps || []; diff --git a/src/js/utils/bind.js b/src/js/utils/bind.js index 545563980..00a74c2a9 100644 --- a/src/js/utils/bind.js +++ b/src/js/utils/bind.js @@ -2,18 +2,19 @@ import { isString, isUndefined } from './type-check'; /** * Sets up the handler to determine if we should advance the tour - * @param selector + * @param {string} selector + * @param {Step} step The step instance * @return {Function} * @private */ -function _setupAdvanceOnHandler(selector) { +function _setupAdvanceOnHandler(selector, step) { return (event) => { - if (this.isOpen()) { - const targetIsEl = this.el && event.target === this.el; + if (step.isOpen()) { + const targetIsEl = step.el && event.target === step.el; const targetIsSelector = !isUndefined(selector) && event.target.matches(selector); if (targetIsSelector || targetIsEl) { - this.tour.next(); + step.tour.next(); } } }; @@ -21,12 +22,13 @@ function _setupAdvanceOnHandler(selector) { /** * Bind the event handler for advanceOn + * @param {Step} step The step instance */ -export function bindAdvance() { +export function bindAdvance(step) { // An empty selector matches the step element - const { event, selector } = this.options.advanceOn || {}; + const { event, selector } = step.options.advanceOn || {}; if (event) { - const handler = _setupAdvanceOnHandler.call(this, selector); + const handler = _setupAdvanceOnHandler(selector, step); // TODO: this should also bind/unbind on show/hide let el; @@ -39,12 +41,12 @@ export function bindAdvance() { return console.error(`No element was found for the selector supplied to advanceOn: ${selector}`); } else if (el) { el.addEventListener(event, handler); - this.on('destroy', () => { + step.on('destroy', () => { return el.removeEventListener(event, handler); }); } else { document.body.addEventListener(event, handler, true); - this.on('destroy', () => { + step.on('destroy', () => { return document.body.removeEventListener(event, handler, true); }); } @@ -57,8 +59,9 @@ export function bindAdvance() { * Bind events to the buttons for next, back, etc * @param {Object} cfg An object containing the config options for the button * @param {HTMLElement} el The element for the button + * @param {Step} step The step instance */ -export function bindButtonEvents(cfg, el) { +export function bindButtonEvents(cfg, el, step) { cfg.events = cfg.events || {}; if (!isUndefined(cfg.action)) { // Including both a click event and an action is not supported @@ -69,13 +72,13 @@ export function bindButtonEvents(cfg, el) { Object.entries(cfg.events).forEach(([event, handler]) => { if (isString(handler)) { const page = handler; - handler = () => this.tour.show(page); + handler = () => step.tour.show(page); } el.dataset.buttonEvent = true; el.addEventListener(event, handler); // Cleanup event listeners on destroy - this.on('destroy', () => { + step.on('destroy', () => { el.removeAttribute('data-button-event'); el.removeEventListener(event, handler); }); @@ -86,11 +89,12 @@ export function bindButtonEvents(cfg, el) { /** * Add a click listener to the cancel link that cancels the tour * @param {HTMLElement} link The cancel link element + * @param {Step} step The step instance */ -export function bindCancelLink(link) { +export function bindCancelLink(link, step) { link.addEventListener('click', (e) => { e.preventDefault(); - this.cancel(); + step.cancel(); }); } diff --git a/src/js/utils/general.js b/src/js/utils/general.js index 69ac31179..0ea6ba23c 100644 --- a/src/js/utils/general.js +++ b/src/js/utils/general.js @@ -73,34 +73,36 @@ export function debounce(func, wait, immediate) { /** * Determines options for the tooltip and initializes - * `this.tooltip` as a Tippy.js instance. + * `step.tooltip` as a Tippy.js instance. + * @param {Step} step The step instance */ -export function setupTooltip() { +export function setupTooltip(step) { if (isUndefined(tippy)) { throw new Error(missingTippy); } - if (this.tooltip) { - this.tooltip.destroy(); + if (step.tooltip) { + step.tooltip.destroy(); } - const attachToOpts = this.parseAttachTo(); + const attachToOpts = parseAttachTo(step); - this.tooltip = _makeTippyInstance.call(this, attachToOpts); + step.tooltip = _makeTippyInstance(attachToOpts, step); - this.target = attachToOpts.element || document.body; + step.target = attachToOpts.element || document.body; - this.el.classList.add('shepherd-element'); + step.el.classList.add('shepherd-element'); } /** * Checks if options.attachTo.element is a string, and if so, tries to find the element + * @param {Step} step The step instance * @returns {{element, on}} * `element` is a qualified HTML Element * `on` is a string position value */ -export function parseAttachTo() { - const options = this.options.attachTo || {}; +export function parseAttachTo(step) { + const options = step.options.attachTo || {}; const returnOpts = Object.assign({}, options); if (isString(options.element)) { @@ -137,16 +139,17 @@ function _createClassModifier(className) { /** * Generates a `Tippy` instance from a set of base `attachTo` options - * - * @return {tippy} The final tippy instance + * @param attachToOptions + * @param {Step} step The step instance + * @return {tippy|Instance | Instance[]} The final tippy instance * @private */ -function _makeTippyInstance(attachToOptions) { +function _makeTippyInstance(attachToOptions, step) { if (!attachToOptions.element) { - return _makeCenteredTippy.call(this); + return _makeCenteredTippy(step); } - const tippyOptions = _makeAttachedTippyOptions.call(this, attachToOptions); + const tippyOptions = _makeAttachedTippyOptions(attachToOptions, step); return tippy(attachToOptions.element, tippyOptions); } @@ -156,24 +159,25 @@ function _makeTippyInstance(attachToOptions) { * target an element in the DOM. * * @param {Object} attachToOptions The local `attachTo` options + * @param {Step} step The step instance * @return {Object} The final tippy options object * @private */ -function _makeAttachedTippyOptions(attachToOptions) { +function _makeAttachedTippyOptions(attachToOptions, step) { const resultingTippyOptions = { - content: this.el, + content: step.el, flipOnUpdate: true, placement: attachToOptions.on || 'right' }; - Object.assign(resultingTippyOptions, this.options.tippyOptions); + Object.assign(resultingTippyOptions, step.options.tippyOptions); - if (this.options.title) { + if (step.options.title) { Object.assign(defaultPopperOptions.modifiers, { addHasTitleClass }); } - if (this.options.tippyOptions && this.options.tippyOptions.popperOptions) { - Object.assign(defaultPopperOptions, this.options.tippyOptions.popperOptions); + if (step.options.tippyOptions && step.options.tippyOptions.popperOptions) { + Object.assign(defaultPopperOptions, step.options.tippyOptions.popperOptions); } resultingTippyOptions.popperOptions = defaultPopperOptions; @@ -186,20 +190,21 @@ function _makeAttachedTippyOptions(attachToOptions) { * target element in the DOM -- and thus is positioned in the center * of the view * + * @param {Step} step The step instance * @return {tippy} The final tippy instance * @private */ -function _makeCenteredTippy() { +function _makeCenteredTippy(step) { const tippyOptions = { - content: this.el, + content: step.el, placement: 'top', - ...this.options.tippyOptions + ...step.options.tippyOptions }; tippyOptions.arrow = false; tippyOptions.popperOptions = tippyOptions.popperOptions || {}; - if (this.options.title) { + if (step.options.title) { Object.assign(defaultPopperOptions.modifiers, { addHasTitleClass }); } diff --git a/test/unit/step.spec.js b/test/unit/step.spec.js index ce6473bd8..0e5539e38 100644 --- a/test/unit/step.spec.js +++ b/test/unit/step.spec.js @@ -132,164 +132,6 @@ describe('Tour | Step', () => { }); }); - describe('bindAdvance()', () => { - let event; - let link; - let hasAdvanced = false; - - const advanceOnSelector = 'test-selector'; - const advanceOnEventName = 'test-event'; - const tourProto = { - next() { hasAdvanced = true; } - }; - - beforeEach(() => { - event = new Event(advanceOnEventName); - - link = document.createElement('a'); - link.classList.add(advanceOnSelector); - link.textContent = 'Click Me 👋'; - - document.body.appendChild(link); - }); - - afterEach(() => { - link.remove(); - }); - - it('triggers the `advanceOn` option via object', () => { - const step = new Step(tourProto, { - advanceOn: { selector: `.${advanceOnSelector}`, event: advanceOnEventName } - }); - - step.isOpen = () => true; - - step.bindAdvance(); - link.dispatchEvent(event); - - expect(link.classList.contains(advanceOnSelector)).toBe(true); - expect(hasAdvanced, '`next()` triggered for advanceOn').toBe(true); - }); - - it('captures events attached to no element', () => { - const step = new Step(tourProto, { - advanceOn: { event: advanceOnEventName } - }); - - step.isOpen = () => true; - - step.bindAdvance(); - document.body.dispatchEvent(event); - - expect(hasAdvanced, '`next()` triggered for advanceOn').toBeTruthy(); - }); - - it('should support bubbling events for nodes that do not exist yet', () => { - const event = new Event('blur'); - - const step = new Step(tourProto, { - text: 'Lorem ipsum dolor: sit amet', - advanceOn: { - selector: 'a[href="https://example.com"]', - event: 'blur' - } - }); - - step.isOpen = () => true; - - step.bindAdvance(); - document.body.dispatchEvent(event); - - expect(hasAdvanced, '`next()` triggered for advanceOn').toBeTruthy(); - }); - - it('calls `removeEventListener` when destroyed', function(done) { - const bodySpy = spy(document.body, 'removeEventListener'); - const step = new Step(tourProto, { - advanceOn: { event: advanceOnEventName } - }); - - step.isOpen = () => true; - - step.bindAdvance(); - step.trigger('destroy'); - - expect(bodySpy.called).toBe(true); - bodySpy.restore(); - - done(); - }); - }); - - describe('bindButtonEvents()', () => { - const link = document.createElement('a'); - const step = new Step(new Tour(), {}); - it('adds button events', () => { - const event = new Event('test'); - const hover = new Event('mouseover'); - let eventTriggered = false; - - step.bindButtonEvents({ - events: { - 'mouseover': '1', - test: () => eventTriggered = true - }, - text: 'Next', - action: () => {} - }, link); - - link.dispatchEvent(event); - link.dispatchEvent(hover); - expect(eventTriggered, 'custom button event was bound/triggered').toBeTruthy(); - }); - - it('removes events once destroyed', () => { - step.destroy(); - - expect(link.hasAttribute('data-button-event'), 'attribute to confirm event is removed').toBeFalsy(); - }); - - }); - - describe('bindCancelLink()', () => { - it('adds an event handler for the cancel button', () => { - const event = new MouseEvent('click', { - view: window, - bubbles: true, - cancelable: true - }); - const link = document.createElement('a'); - const step = new Step(); - let cancelCalled = false; - - step.cancel = () => cancelCalled = true; - step.bindCancelLink(link); - - link.dispatchEvent(event); - expect(cancelCalled, 'cancel method was called from bound click event').toBeTruthy(); - }); - }); - - describe('bindMethods()', () => { - it('binds the expected methods', () => { - const step = new Step(); - const methods = [ - '_scrollTo', - '_setupElements', - '_show', - 'cancel', - 'complete', - 'destroy', - 'hide', - 'isOpen', - 'show' - ]; - methods.forEach((method) => { - expect(step[method], `${method} has been bound`).toBeTruthy(); - }); - }); - }); - describe('cancel()', () => { it('triggers the cancel event and tour method', () => { let cancelCalled = false; @@ -358,17 +200,6 @@ describe('Tour | Step', () => { }); }); - describe('parseAttachTo()', function() { - it('fails if element does not exist', function() { - const step = new Step({}, { - attachTo: { element: '.scroll-test', on: 'center' } - }); - - const { element } = step.parseAttachTo(); - expect(element).toBeFalsy(); - }); - }); - describe('_setupElements()', () => { it('calls destroy on the step if the content element is already set', () => { const step = new Step(); @@ -388,18 +219,6 @@ describe('Tour | Step', () => { step._setupElements(); expect(destroyCalled, '_setupElements method called destroy on the existing tooltip').toBe(true); }); - - it('calls bindAdvance() if advanceOn passed', () => { - const step = new Step({ - next: () => true - }, { - advanceOn: { selector: '.click-test', event: 'test' } - }); - const bindFunction = spy(step, 'bindAdvance'); - step._setupElements(); - - expect(bindFunction.called).toBeTruthy(); - }); }); describe('_scrollTo()', () => { @@ -581,13 +400,9 @@ describe('Tour | Step', () => { const element = document.createElement('div'); const step = new Step(null, { showCancelLink: true }); - const cancelLinkStub = stub(step, 'bindCancelLink'); - step._addCancelLink(element, header); - expect(cancelLinkStub.called).toBe(true); expect(element).toHaveClass('shepherd-has-cancel-link'); - cancelLinkStub.restore(); }); }); diff --git a/test/unit/utils/bind.spec.js b/test/unit/utils/bind.spec.js new file mode 100644 index 000000000..c72a8528e --- /dev/null +++ b/test/unit/utils/bind.spec.js @@ -0,0 +1,144 @@ +import { bindAdvance, bindButtonEvents, bindCancelLink } from '../../../src/js/utils/bind.js'; +import { Step } from '../../../src/js/step'; +import { spy } from 'sinon'; +import { Tour } from '../../../src/js/tour'; + +describe('Bind Utils', function() { + describe('bindAdvance()', () => { + let event; + let link; + let hasAdvanced = false; + + const advanceOnSelector = 'test-selector'; + const advanceOnEventName = 'test-event'; + const tourProto = { + next() { hasAdvanced = true; } + }; + + beforeEach(() => { + event = new Event(advanceOnEventName); + + link = document.createElement('a'); + link.classList.add(advanceOnSelector); + link.textContent = 'Click Me 👋'; + + document.body.appendChild(link); + }); + + afterEach(() => { + link.remove(); + }); + + it('triggers the `advanceOn` option via object', () => { + const step = new Step(tourProto, { + advanceOn: { selector: `.${advanceOnSelector}`, event: advanceOnEventName } + }); + + step.isOpen = () => true; + + bindAdvance(step); + link.dispatchEvent(event); + + expect(link.classList.contains(advanceOnSelector)).toBe(true); + expect(hasAdvanced, '`next()` triggered for advanceOn').toBe(true); + }); + + it('captures events attached to no element', () => { + const step = new Step(tourProto, { + advanceOn: { event: advanceOnEventName } + }); + + step.isOpen = () => true; + + bindAdvance(step); + document.body.dispatchEvent(event); + + expect(hasAdvanced, '`next()` triggered for advanceOn').toBeTruthy(); + }); + + it('should support bubbling events for nodes that do not exist yet', () => { + const event = new Event('blur'); + + const step = new Step(tourProto, { + text: 'Lorem ipsum dolor: sit amet', + advanceOn: { + selector: 'a[href="https://example.com"]', + event: 'blur' + } + }); + + step.isOpen = () => true; + + bindAdvance(step); + document.body.dispatchEvent(event); + + expect(hasAdvanced, '`next()` triggered for advanceOn').toBeTruthy(); + }); + + it('calls `removeEventListener` when destroyed', function(done) { + const bodySpy = spy(document.body, 'removeEventListener'); + const step = new Step(tourProto, { + advanceOn: { event: advanceOnEventName } + }); + + step.isOpen = () => true; + + bindAdvance(step); + step.trigger('destroy'); + + expect(bodySpy.called).toBe(true); + bodySpy.restore(); + + done(); + }); + }); + + describe('bindButtonEvents()', () => { + const link = document.createElement('a'); + const step = new Step(new Tour(), {}); + it('adds button events', () => { + const event = new Event('test'); + const hover = new Event('mouseover'); + let eventTriggered = false; + + bindButtonEvents({ + events: { + 'mouseover': '1', + test: () => eventTriggered = true + }, + text: 'Next', + action: () => {} + }, link, step); + + link.dispatchEvent(event); + link.dispatchEvent(hover); + expect(eventTriggered, 'custom button event was bound/triggered').toBeTruthy(); + }); + + it('removes events once destroyed', () => { + step.destroy(); + + expect(link.hasAttribute('data-button-event'), 'attribute to confirm event is removed').toBeFalsy(); + }); + + }); + + describe('bindCancelLink()', () => { + it('adds an event handler for the cancel button', () => { + const event = new MouseEvent('click', { + view: window, + bubbles: true, + cancelable: true + }); + const link = document.createElement('a'); + const step = new Step(); + let cancelCalled = false; + + step.cancel = () => cancelCalled = true; + bindCancelLink(link, step); + + link.dispatchEvent(event); + expect(cancelCalled, 'cancel method was called from bound click event').toBeTruthy(); + }); + }); +}); diff --git a/test/unit/utils/general.spec.js b/test/unit/utils/general.spec.js new file mode 100644 index 000000000..1faccbc73 --- /dev/null +++ b/test/unit/utils/general.spec.js @@ -0,0 +1,16 @@ +import { Step } from '../../../src/js/step'; +import { parseAttachTo } from '../../../src/js/utils/general'; + + +describe('General Utils', function() { + describe('parseAttachTo()', function() { + it('fails if element does not exist', function() { + const step = new Step({}, { + attachTo: { element: '.scroll-test', on: 'center' } + }); + + const { element } = parseAttachTo(step); + expect(element).toBeFalsy(); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index da6001b8c..d084182c0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1062,11 +1062,24 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.6.2.tgz#a5ccec6abb6060d5f20d256fb03ed743e9774999" integrity sha512-gojym4tX0FWeV2gsW4Xmzo5wxGjXGm550oVUII7f7G5o4BV6c7DBdiG1RRQd+y1bvqRyYtPfMK85UM95vsapqQ== +"@types/prop-types@*": + version "15.7.1" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.1.tgz#f1a11e7babb0c3cad68100be381d1e064c68f1f6" + integrity sha512-CFzn9idOEpHrgdw8JsoTkaDDyRWk1jrzIV8djzcgpq0y9tG4B4lFT+Nxh52DVpDXV+n4+NPNv7M1Dj5uMp6XFg== + "@types/q@^1.5.1": version "1.5.2" resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8" integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw== +"@types/react@^16.8.12": + version "16.8.23" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.8.23.tgz#ec6be3ceed6353a20948169b6cb4c97b65b97ad2" + integrity sha512-abkEOIeljniUN9qB5onp++g0EY38h7atnDHxwKUFz1r3VH1+yG1OKi2sNPTyObL40goBmfKFpdii2lEzwLX1cA== + dependencies: + "@types/prop-types" "*" + csstype "^2.2.0" + "@types/resolve@0.0.8": version "0.0.8" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-0.0.8.tgz#f26074d238e02659e323ce1a13d041eee280e194" @@ -1388,6 +1401,13 @@ atob@^2.1.1: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== +auto-bind@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/auto-bind/-/auto-bind-2.1.0.tgz#254e12d53063d7cab90446ce021accfb3faa1464" + integrity sha512-qZuFvkes1eh9lB2mg8/HG18C+5GIO51r+RrCSst/lh+i5B1CtVlkhTE488M805Nr3dKl0sM/pIFKSKUIlg3zUg== + dependencies: + "@types/react" "^16.8.12" + autoprefixer@^9.5.1, autoprefixer@^9.6.1: version "9.6.1" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.6.1.tgz#51967a02d2d2300bb01866c1611ec8348d355a47" @@ -2614,6 +2634,11 @@ cssstyle@^1.0.0: dependencies: cssom "~0.3.6" +csstype@^2.2.0: + version "2.6.6" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.6.tgz#c34f8226a94bbb10c32cc0d714afdf942291fc41" + integrity sha512-RpFbQGUE74iyPgvr46U9t1xoQBM8T4BL8SxrN66Le2xYAPSaDJJKeztV3awugusb3g3G9iL8StmkBBXhcbbXhg== + currently-unhandled@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"