UNPKG

storybook-mobile

Version:

This addon offers suggestions on how you can improve the HTML, CSS and UX of your components to be more mobile-friendly.

887 lines (877 loc) 54.5 kB
var React = require('react'); var addons = require('@storybook/addons'); var coreEvents = require('@storybook/core-events'); var api = require('@storybook/api'); var components = require('@storybook/components'); var theming = require('@storybook/theming'); var lrt = require('lrt'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var React__default = /*#__PURE__*/_interopDefaultLegacy(React); function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } function _taggedTemplateLiteralLoose(strings, raw) { if (!raw) { raw = strings.slice(0); } strings.raw = raw; return strings; } function getDomPath(el) { var stack = []; while (el.parentNode) { var sibCount = 0; var sibIndex = 0; for (var i = 0; i < el.parentNode.childNodes.length; i++) { var sib = el.parentNode.childNodes[i]; if (sib.nodeName === el.nodeName) { if (sib === el) { sibIndex = sibCount; } sibCount++; } } if (el.hasAttribute('id') && el.id !== '') { stack.unshift(el.nodeName.toLowerCase() + '#' + el.id); } else if (el.classList.toString() !== '' && el.tagName !== 'BODY') { stack.unshift(el.nodeName.toLowerCase() + '.' + el.classList.toString()); } else if (sibCount > 1) { stack.unshift(el.nodeName.toLowerCase() + ':eq(' + sibIndex + ')'); } else { stack.unshift(el.nodeName.toLowerCase()); } el = el.parentNode; } var toFilter = ['html', 'body', 'div#root']; return stack.filter(function (el) { return !toFilter.includes(el); }).join(' > '); } var _marked = /*#__PURE__*/regeneratorRuntime.mark(getTouchTargetSizeWarning), _marked2 = /*#__PURE__*/regeneratorRuntime.mark(getTapHighlightWarnings), _marked3 = /*#__PURE__*/regeneratorRuntime.mark(getSrcsetWarnings), _marked4 = /*#__PURE__*/regeneratorRuntime.mark(getBackgroundImageWarnings), _marked5 = /*#__PURE__*/regeneratorRuntime.mark(getActiveWarnings), _marked6 = /*#__PURE__*/regeneratorRuntime.mark(get100vhWarnings); var getElements = function getElements(container, tag) { return Array.from(container.querySelectorAll(tag)); }; var getStylesheetRules = function getStylesheetRules(sheets, k) { var rules = []; try { rules = Array.from(sheets[k].rules || sheets[k].cssRules); } catch (e) { // } return rules; }; var getNodeName = function getNodeName(el) { return el.nodeName === 'A' ? 'a' : el.nodeName === 'BUTTON' ? 'button' : el.nodeName.toLowerCase() + "[role=\"button\"]"; }; var attachLabels = function attachLabels(inputs, container) { return inputs.map(function (input) { var labelText = ''; if (input.labels && input.labels[0]) { labelText = input.labels[0].innerText; } else if (input.parentElement.nodeName === 'LABEL') { labelText = input.parentElement.innerText; } else if (input.id) { var label = container.querySelector("label[for=\"" + input.id + "\"]"); if (label) labelText = label.innerText; } return { labelText: labelText, path: getDomPath(input), type: input.type }; }); }; var textInputs = { text: true, search: true, tel: true, url: true, email: true, number: true, password: true }; var getAutocompleteWarnings = function getAutocompleteWarnings(container) { var inputs = getElements(container, 'input'); var warnings = inputs.filter(function (input) { var currentType = input.getAttribute('type'); var autocomplete = input.getAttribute('autocomplete'); return textInputs[currentType] && !autocomplete; }); return attachLabels(warnings, container); }; var getInputTypeNumberWarnings = function getInputTypeNumberWarnings(container) { var inputs = getElements(container, 'input[type="number"]'); return attachLabels(inputs); }; var getInputTypeWarnings = function getInputTypeWarnings(container) { var inputs = getElements(container, 'input[type="text"]').concat(getElements(container, 'input:not([type])')).filter(function (input) { return !input.getAttribute('inputmode'); }); return attachLabels(inputs, container); }; var getInstantWarnings = function getInstantWarnings(container) { return { autocomplete: getAutocompleteWarnings(container), inputType: getInputTypeWarnings(container), inputTypeNumber: getInputTypeNumberWarnings(container) }; }; // SCHEDULED ANALYSES // We schedule these so the UI does not lock up while they're running var isInside = function isInside(dangerZone, bounding) { return bounding.top <= dangerZone.bottom && bounding.bottom >= dangerZone.top && bounding.left <= dangerZone.right && bounding.right >= dangerZone.left; }; var toPresent = function toPresent(_ref) { var el = _ref.el, _ref$bounding = _ref.bounding, width = _ref$bounding.width, height = _ref$bounding.height, close = _ref.close; return { type: el.nodeName === 'A' ? 'a' : el.nodeName === 'BUTTON' ? 'button' : el.nodeName.toLowerCase() + "[role=\"button\"]", path: getDomPath(el), text: el.innerText, html: el.innerHTML, width: Math.floor(width), height: Math.floor(height), close: close }; }; var MIN_SIZE = 32; var RECOMMENDED_DISTANCE = 8; //const RECOMMENDED_SIZE = 48 var checkMinSize = function checkMinSize(_ref2) { var height = _ref2.height, width = _ref2.width; return height < MIN_SIZE || width < MIN_SIZE; }; function getTouchTargetSizeWarning(container) { var elements, suspectElements, len, underMinSize, tooClose, _loop, i; return regeneratorRuntime.wrap(function getTouchTargetSizeWarning$(_context2) { while (1) { switch (_context2.prev = _context2.next) { case 0: elements = getElements(container, 'button').concat(getElements(container, '[role="button"]')).concat(getElements(container, 'a')); suspectElements = Array.from(new Set(elements)).map(function (el) { return [el, el.getBoundingClientRect()]; }); len = elements.length; underMinSize = []; tooClose = []; _loop = /*#__PURE__*/regeneratorRuntime.mark(function _callee(i) { var el, bounding, dangerZone, close, isUnderMinSize, present; return regeneratorRuntime.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: el = elements[i]; bounding = el.getBoundingClientRect(); dangerZone = { top: bounding.top - RECOMMENDED_DISTANCE, left: bounding.left - RECOMMENDED_DISTANCE, right: bounding.right + RECOMMENDED_DISTANCE, bottom: bounding.bottom + RECOMMENDED_DISTANCE }; close = suspectElements.filter(function (_ref3) { var susEl = _ref3[0], susBounding = _ref3[1]; return susEl !== el && isInside(dangerZone, susBounding); }); isUnderMinSize = checkMinSize(bounding); if (isUnderMinSize || close.length > 0) { present = toPresent({ el: el, bounding: bounding, close: close }); if (isUnderMinSize) { underMinSize.push(present); } if (close.length > 0) { tooClose.push(present); } } _context.next = 8; return i; case 8: case "end": return _context.stop(); } } }, _callee); }); i = 0; case 7: if (!(i < len)) { _context2.next = 12; break; } return _context2.delegateYield(_loop(i), "t0", 9); case 9: i++; _context2.next = 7; break; case 12: return _context2.abrupt("return", { tooClose: tooClose, underMinSize: underMinSize }); case 13: case "end": return _context2.stop(); } } }, _marked); } function getTapHighlightWarnings(container) { var buttons, links, elements, len, result, i, el; return regeneratorRuntime.wrap(function getTapHighlightWarnings$(_context3) { while (1) { switch (_context3.prev = _context3.next) { case 0: buttons = getElements(container, 'button').concat(getElements(container, '[role="button"]')); links = getElements(container, 'a'); elements = buttons.concat(links); len = elements.length; result = []; i = 0; case 6: if (!(i < len)) { _context3.next = 14; break; } el = elements[i]; if (getComputedStyle(el)['-webkit-tap-highlight-color'] === 'rgba(0, 0, 0, 0)') { result.push({ type: getNodeName(el), text: el.innerText, html: el.innerHTML, path: getDomPath(el) }); } _context3.next = 11; return i; case 11: i++; _context3.next = 6; break; case 14: return _context3.abrupt("return", result); case 15: case "end": return _context3.stop(); } } }, _marked2); } var MAX_WIDTH = 600; function getSrcsetWarnings(container) { var images, len, result, i, img, srcSet, src, isSVG, isLarge; return regeneratorRuntime.wrap(function getSrcsetWarnings$(_context4) { while (1) { switch (_context4.prev = _context4.next) { case 0: images = getElements(container, 'img'); len = images.length; result = []; i = 0; case 4: if (!(i < len)) { _context4.next = 14; break; } img = images[i]; srcSet = img.getAttribute('srcset'); src = img.getAttribute('src'); if (!srcSet && src) { isSVG = Boolean(src.match(/svg$/)); if (!isSVG) { isLarge = parseInt(getComputedStyle(img).width, 10) > MAX_WIDTH || img.naturalWidth > MAX_WIDTH; if (isLarge) { result.push({ src: img.src, path: getDomPath(img), alt: img.alt }); } } } _context4.next = 11; return i; case 11: i++; _context4.next = 4; break; case 14: return _context4.abrupt("return", result); case 15: case "end": return _context4.stop(); } } }, _marked3); } function getBackgroundImageWarnings(container) { var backgroundImageRegex, elsWithBackgroundImage, styleDict, responsiveBackgroundImgRegex, result, elements, len, i, _elements$i, el, styles, requiresResponsiveWarning, bg, src; return regeneratorRuntime.wrap(function getBackgroundImageWarnings$(_context5) { while (1) { switch (_context5.prev = _context5.next) { case 0: backgroundImageRegex = /url\(".*?(.png|.jpg|.jpeg)"\)/; elsWithBackgroundImage = getElements(container, '#root *').filter(function (el) { var style = getComputedStyle(el); return style['background-image'] && backgroundImageRegex.test(style['background-image']) && // HACK // ideally, we would make a new image element and check its "naturalWidth" // to get a better idea of the size of the background image, this is a hack el.clientWidth > 200; }); if (elsWithBackgroundImage.length) { _context5.next = 4; break; } return _context5.abrupt("return", []); case 4: styleDict = new Map(); Object.keys(container.styleSheets).forEach(function (k) { getStylesheetRules(container.styleSheets, k).forEach(function (rule) { if (rule) { try { elsWithBackgroundImage.forEach(function (el) { if (el.matches(rule.selectorText)) { styleDict.set(el, (styleDict.get(el) || []).concat(rule)); } }); } catch (e) { // catch errors in safari } } }); }); responsiveBackgroundImgRegex = /-webkit-min-device-pixel-ratio|min-resolution|image-set/; result = []; elements = Array.from(styleDict.entries()); len = elements.length; i = 0; case 11: if (!(i < len)) { _context5.next = 19; break; } _elements$i = elements[i], el = _elements$i[0], styles = _elements$i[1]; if (styles) { requiresResponsiveWarning = styles.some(function (style) { return !responsiveBackgroundImgRegex.test(style); }); if (requiresResponsiveWarning) { bg = getComputedStyle(el).backgroundImage; src = bg.match(/url\("(.*)"\)/) ? bg.match(/url\("(.*)"\)/)[1] : undefined; result.push({ path: getDomPath(el), src: src }); } } _context5.next = 16; return i; case 16: i++; _context5.next = 11; break; case 19: return _context5.abrupt("return", result); case 20: case "end": return _context5.stop(); } } }, _marked4); } var getActiveStyles = function getActiveStyles(container, el) { var sheets = container.styleSheets; var result = []; var activeRegex = /:active$/; Object.keys(sheets).forEach(function (k) { getStylesheetRules(sheets, k).forEach(function (rule) { if (rule && rule.selectorText && rule.selectorText.match(activeRegex)) { var ruleNoPseudoClass = rule.selectorText.replace(activeRegex, ''); try { if (el.matches(ruleNoPseudoClass)) { result.push(rule); } } catch (e) { // safari } } }); }); return result; }; function getActiveWarnings(container) { var buttons, links, elements, len, result, i, el, hasActive; return regeneratorRuntime.wrap(function getActiveWarnings$(_context6) { while (1) { switch (_context6.prev = _context6.next) { case 0: buttons = getElements(container, 'button').concat(getElements(container, '[role="button"]')); links = getElements(container, 'a'); elements = buttons.concat(links); len = elements.length; result = []; i = 0; case 6: if (!(i < len)) { _context6.next = 15; break; } el = elements[i]; hasActive = getActiveStyles(container, el); if (hasActive.length) { result.push({ type: getNodeName(el), text: el.innerText, html: el.innerHTML, path: getDomPath(el) }); } _context6.next = 12; return i; case 12: i++; _context6.next = 6; break; case 15: return _context6.abrupt("return", result); case 16: case "end": return _context6.stop(); } } }, _marked5); } var getOriginalStyles = function getOriginalStyles(container, el) { var sheets = container.styleSheets; var result = []; Object.keys(sheets).forEach(function (k) { var rules = getStylesheetRules(sheets, k); rules.forEach(function (rule) { if (rule) { try { if (el.matches(rule.selectorText)) { result.push(rule.cssText); } } catch (e) { // catch errors in safari } } }); }); return result; }; function get100vhWarnings(container) { var elements, len, result, i, el, styles, vhWarning; return regeneratorRuntime.wrap(function get100vhWarnings$(_context7) { while (1) { switch (_context7.prev = _context7.next) { case 0: elements = getElements(container, '#root *'); len = elements.length; result = []; i = 0; case 4: if (!(i < len)) { _context7.next = 14; break; } el = elements[i]; styles = getOriginalStyles(container, el); vhWarning = styles.find(function (style) { return /100vh/.test(style); }); if (vhWarning) { result.push({ el: el, css: vhWarning, path: getDomPath(el) }); } _context7.next = 11; return i; case 11: i++; _context7.next = 4; break; case 14: return _context7.abrupt("return", result); case 15: case "end": return _context7.stop(); } } }, _marked6); } var schedule = function schedule(iterator) { // 100ms is the threshold where users start to notice UI lag // higher values increase lag but do not noticeably improve processing time so 100ms is the sweet spot var scheduler = lrt.createScheduler({ chunkBudget: 100 }); var task = scheduler.runTask(iterator); return { task: task, abort: function abort() { return scheduler.abortTask(task); } }; }; var getScheduledWarnings = function getScheduledWarnings(container, setState, setComplete) { var analyses = { tapHighlight: schedule(getTapHighlightWarnings(container)), srcset: schedule(getSrcsetWarnings(container)), backgroundImg: schedule(getBackgroundImageWarnings(container)), touchTarget: schedule(getTouchTargetSizeWarning(container)), active: schedule(getActiveWarnings(container)), height: schedule(get100vhWarnings(container)) }; var analysesArray = Object.keys(analyses); var remaining = analysesArray.length; analysesArray.forEach(function (key) { //const start = performance.now() analyses[key].task.then(function (result) { //console.log(key, performance.now() - start) setState(function (prev) { var _extends2; return _extends({}, prev, (_extends2 = {}, _extends2[key] = result, _extends2)); }); if (--remaining === 0) { setComplete(true); } }); }); return function () { return analysesArray.forEach(function (key) { return analyses[key].abort(); }); }; }; var _templateObject, _templateObject2, _templateObject3, _templateObject4, _templateObject5, _templateObject6, _templateObject7, _templateObject8, _templateObject9, _templateObject10; var accessibleBlue = '#0965df'; var warning = '#bd4700'; var tagStyles = "\n padding: .25rem .5rem;\n font-weight: bold;\n display:inline-block;\n border-radius: 10px;\n margin-bottom: 1rem;\n svg {\n margin-right: .25rem;\n display: inline-block;\n height: .7rem;\n line-height: 1;\n position: relative;\n top: .03rem;\n letter-spacing: .01rem;\n }\n"; var StyledWarningTag = theming.styled.div(_templateObject || (_templateObject = _taggedTemplateLiteralLoose(["\n color: ", ";\n background-color: hsl(41, 100%, 92%);\n ", "\n"])), warning, tagStyles); var Warning = function Warning() { return /*#__PURE__*/React__default["default"].createElement(StyledWarningTag, null, /*#__PURE__*/React__default["default"].createElement("svg", { "aria-hidden": "true", focusable: "false", role: "img", xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 576 512" }, /*#__PURE__*/React__default["default"].createElement("path", { fill: "currentColor", d: "M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z" })), "warning"); }; var StyledInfoTag = theming.styled.div(_templateObject2 || (_templateObject2 = _taggedTemplateLiteralLoose(["\n ", "\n color: ", ";\n background-color: hsla(214, 92%, 45%, 0.1);\n"])), tagStyles, accessibleBlue); var Hint = function Hint() { return /*#__PURE__*/React__default["default"].createElement(StyledInfoTag, null, /*#__PURE__*/React__default["default"].createElement("svg", { "aria-hidden": "true", focusable: "false", "data-prefix": "fas", "data-icon": "magic", role: "img", xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 512 512", className: "svg-inline--fa fa-magic fa-w-16 fa-5x" }, /*#__PURE__*/React__default["default"].createElement("path", { fill: "currentColor", d: "M224 96l16-32 32-16-32-16-16-32-16 32-32 16 32 16 16 32zM80 160l26.66-53.33L160 80l-53.34-26.67L80 0 53.34 53.33 0 80l53.34 26.67L80 160zm352 128l-26.66 53.33L352 368l53.34 26.67L432 448l26.66-53.33L512 368l-53.34-26.67L432 288zm70.62-193.77L417.77 9.38C411.53 3.12 403.34 0 395.15 0c-8.19 0-16.38 3.12-22.63 9.38L9.38 372.52c-12.5 12.5-12.5 32.76 0 45.25l84.85 84.85c6.25 6.25 14.44 9.37 22.62 9.37 8.19 0 16.38-3.12 22.63-9.37l363.14-363.15c12.5-12.48 12.5-32.75 0-45.24zM359.45 203.46l-50.91-50.91 86.6-86.6 50.91 50.91-86.6 86.6z", className: "" })), "hint"); }; var Spacer = theming.styled.div(_templateObject3 || (_templateObject3 = _taggedTemplateLiteralLoose(["\n padding: 1rem;\n"]))); var StyledTappableContents = theming.styled.div(_templateObject4 || (_templateObject4 = _taggedTemplateLiteralLoose(["\n display: inline-block;\n padding-top: 0.25rem;\n height: 2rem;\n min-width: 1rem;\n width: auto;\n background-color: hsla(0, 0%, 50%, 0.1);\n border-radius: 3px;\n li {\n list-style-type: none;\n }\n img,\n svg {\n max-height: 2rem !important;\n min-height: 1rem !important;\n width: auto !important;\n }\n"]))); var DemoImg = theming.styled.img(_templateObject5 || (_templateObject5 = _taggedTemplateLiteralLoose(["\n height: 4rem;\n width: auto;\n max-width: 100%;\n background-color: hsla(0, 0%, 0%, 0.2);\n"]))); var ListEntry = theming.styled.li(_templateObject6 || (_templateObject6 = _taggedTemplateLiteralLoose(["\n margin-bottom: 0.5rem;\n ", ";\n"])), function (props) { return props.nostyle ? 'list-style-type: none;' : ''; }); var Container = theming.styled.div(_templateObject7 || (_templateObject7 = _taggedTemplateLiteralLoose(["\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(25rem, 1fr));\n\n font-size: ", "px;\n\n p {\n line-height: 1.4;\n }\n\n h3 {\n font-size: ", "px;\n font-weight: bold;\n margin-bottom: 0.5rem;\n margin-top: 0;\n }\n\n code {\n background: hsla(0, 0%, 50%, 0.1);\n border-radius: 3px;\n }\n\n summary {\n cursor: pointer;\n display: block;\n margin-right: 1rem;\n padding: 0.2rem 0.3rem;\n border-radius: 5px;\n color: ", ";\n &:focus {\n outline: none;\n box-shadow: 0 0 0 3px ", ";\n }\n }\n\n ul {\n padding-left: 1.25rem;\n max-height: 12rem;\n overflow: auto;\n padding-bottom: 0.5rem;\n li {\n margin-bottom: 0.3rem;\n }\n }\n a {\n text-decoration: none;\n color: ", ";\n &:hover {\n border-bottom: 1px solid ", ";\n }\n }\n > div {\n border-bottom: 1px solid ", ";\n border-right: 1px solid ", ";\n }\n"])), function (props) { return props.theme.typography.size.s2; }, function (props) { return props.theme.typography.size.s2; }, accessibleBlue, function (props) { return props.theme.color.mediumlight; }, accessibleBlue, accessibleBlue, function (props) { return props.theme.color.medium; }, function (props) { return props.theme.color.medium; }); var StyledBanner = theming.styled.div(_templateObject8 || (_templateObject8 = _taggedTemplateLiteralLoose(["\n display: flex;\n align-items: center;\n padding: 0 0.75rem;\n grid-column: 1 / -1;\n height: 2.875rem;\n"]))); var StyledRescanButton = theming.styled.button(_templateObject9 || (_templateObject9 = _taggedTemplateLiteralLoose(["\n margin-left: 0.5rem;\n border-width: 1px;\n border-radius: 3px;\n padding: 0.2rem 0.5rem;\n cursor: pointer;\n font-family: inherit;\n color: inherit;\n border: none;\n font-size: 100%;\n background-color: transparent;\n appearance: none;\n box-shadow: none;\n border: 1px solid;\n &:hover {\n background-color: hsla(0, 0%, 0%, 0.15);\n }\n"]))); var Spinner = theming.styled.div(_templateObject10 || (_templateObject10 = _taggedTemplateLiteralLoose(["\n cursor: progress;\n display: inline-block;\n overflow: hidden;\n position: relative;\n margin-right: 0.7rem;\n height: 1.25rem;\n width: 1.25rem;\n border-width: 2px;\n border-style: solid;\n border-radius: 50%;\n border-color: rgba(97, 97, 97, 0.29);\n border-top-color: rgb(100, 100, 100);\n animation: spinner 0.7s linear infinite;\n mix-blend-mode: difference;\n\n @keyframes spinner {\n from {\n transform: rotate(0deg);\n }\n to {\n transform: rotate(360deg);\n }\n }\n"]))); var fixText = 'Learn more'; var ActiveWarnings = function ActiveWarnings(_ref) { var warnings = _ref.warnings; if (!warnings || !warnings.length) return null; return /*#__PURE__*/React__default["default"].createElement(Spacer, null, /*#__PURE__*/React__default["default"].createElement(Hint, null), /*#__PURE__*/React__default["default"].createElement("h3", null, /*#__PURE__*/React__default["default"].createElement("code", null, ":active"), " styles on iOS"), /*#__PURE__*/React__default["default"].createElement("p", null, /*#__PURE__*/React__default["default"].createElement("code", null, ":active"), " styles will only appear in iOS", ' ', /*#__PURE__*/React__default["default"].createElement("a", { href: "https://stackoverflow.com/questions/3885018/active-pseudo-class-doesnt-work-in-mobile-safari", target: "_blank", rel: "noopener noreferrer" }, "if a touch listener is added to the element or one of its ancestors"), ". Once activated in this manner, ", /*#__PURE__*/React__default["default"].createElement("code", null, ":active"), " styles (along with", ' ', /*#__PURE__*/React__default["default"].createElement("code", null, ":hover"), " styles) will be applied immediately in iOS when a user taps, possibly creating a confusing UX. (On Android,", ' ', /*#__PURE__*/React__default["default"].createElement("code", null, ":active"), " styles are applied with a slight delay to allow the user to use gestures like scroll without necessarily activating", ' ', /*#__PURE__*/React__default["default"].createElement("code", null, ":active"), " styles.)"), /*#__PURE__*/React__default["default"].createElement("ul", null, warnings.map(function (w, i) { return /*#__PURE__*/React__default["default"].createElement(ListEntry, { key: i }, /*#__PURE__*/React__default["default"].createElement("code", null, w.type), " with content\xA0\xA0", w.text ? /*#__PURE__*/React__default["default"].createElement("b", null, w.text) : w.html ? /*#__PURE__*/React__default["default"].createElement(StyledTappableContents, { dangerouslySetInnerHTML: { __html: w.html } }) : '[no text found]'); })), /*#__PURE__*/React__default["default"].createElement("details", null, /*#__PURE__*/React__default["default"].createElement("summary", null, fixText), /*#__PURE__*/React__default["default"].createElement("p", { style: { marginTop: '1rem' } }, /*#__PURE__*/React__default["default"].createElement("a", { href: "https://stackoverflow.com/questions/3885018/active-pseudo-class-doesnt-work-in-mobile-safari/33681490#33681490", target: "_blank", rel: "noopener noreferrer" }, "Relevant Stack Overflow thread")))); }; var TapWarnings = function TapWarnings(_ref2) { var warnings = _ref2.warnings; if (!warnings || !warnings.length) return null; return /*#__PURE__*/React__default["default"].createElement(Spacer, null, /*#__PURE__*/React__default["default"].createElement(Hint, null), /*#__PURE__*/React__default["default"].createElement("h3", null, "Tap style removed from tappable element"), /*#__PURE__*/React__default["default"].createElement("p", null, "These elements have an invisible", ' ', /*#__PURE__*/React__default["default"].createElement("code", null, "-webkit-tap-highlight-color"), ". While this might be intentional, please verify that they have appropriate tap indication styles added through other means."), /*#__PURE__*/React__default["default"].createElement("ul", null, warnings.map(function (w, i) { return /*#__PURE__*/React__default["default"].createElement(ListEntry, { key: i }, /*#__PURE__*/React__default["default"].createElement("code", null, w.type), " with content\xA0\xA0", w.text ? /*#__PURE__*/React__default["default"].createElement("b", null, w.text) : w.html ? /*#__PURE__*/React__default["default"].createElement(StyledTappableContents, { dangerouslySetInnerHTML: { __html: w.html } }) : '[no text found]'); })), /*#__PURE__*/React__default["default"].createElement("details", null, /*#__PURE__*/React__default["default"].createElement("summary", null, fixText), /*#__PURE__*/React__default["default"].createElement("p", null, "Some stylesheets remove the tap indication highlight shown on iOS and Android browsers by adding the code", ' ', /*#__PURE__*/React__default["default"].createElement("code", null, "-webkit-tap-highlight-color: transparent"), ". In order to maintain a good mobile experience, tap styles should be added via appropriate ", /*#__PURE__*/React__default["default"].createElement("code", null, ":active"), " CSS styles (though, note that", ' ', /*#__PURE__*/React__default["default"].createElement("a", { href: "https://stackoverflow.com/questions/3885018/active-pseudo-class-doesnt-work-in-mobile-safari", target: "_blank", rel: "noopener noreferrer" }, /*#__PURE__*/React__default["default"].createElement("code", null, ":active"), " styles work inconsistently in iOS"), ") , or via JavaScript on the ", /*#__PURE__*/React__default["default"].createElement("code", null, "touchstart"), " event."))); }; var AutocompleteWarnings = function AutocompleteWarnings(_ref3) { var warnings = _ref3.warnings; if (!warnings || !warnings.length) return null; return /*#__PURE__*/React__default["default"].createElement(Spacer, null, /*#__PURE__*/React__default["default"].createElement(Warning, null), /*#__PURE__*/React__default["default"].createElement("h3", null, "Input with no ", /*#__PURE__*/React__default["default"].createElement("code", null, "autocomplete"), " attribute"), /*#__PURE__*/React__default["default"].createElement("p", null, "Most textual inputs should have an explicit ", /*#__PURE__*/React__default["default"].createElement("code", null, "autocomplete"), ' ', "attribute."), /*#__PURE__*/React__default["default"].createElement("p", null, "If you truly want to disable autocomplete, try using a", ' ', /*#__PURE__*/React__default["default"].createElement("a", { href: "https://bugs.chromium.org/p/chromium/issues/detail?id=468153#c164", target: "_blank", rel: "noopener noreferrer" }, "semantically valid but unique value rather than", ' ', /*#__PURE__*/React__default["default"].createElement("code", null, "autocomplete=\"off\"")), ", which doesn't work in Chrome."), /*#__PURE__*/React__default["default"].createElement("p", null, "Note: ", /*#__PURE__*/React__default["default"].createElement("code", null, "autocomplete"), " is styled as ", /*#__PURE__*/React__default["default"].createElement("code", null, "autoComplete"), ' ', "in JSX."), /*#__PURE__*/React__default["default"].createElement("ul", null, warnings.map(function (w, i) { return /*#__PURE__*/React__default["default"].createElement(ListEntry, { key: i }, /*#__PURE__*/React__default["default"].createElement("code", null, "input type=\"", w.type, "\""), " and label", ' ', /*#__PURE__*/React__default["default"].createElement("b", null, w.labelText || '[no label found]')); })), /*#__PURE__*/React__default["default"].createElement("details", null, /*#__PURE__*/React__default["default"].createElement("summary", null, fixText), /*#__PURE__*/React__default["default"].createElement("ul", null, /*#__PURE__*/React__default["default"].createElement("li", null, /*#__PURE__*/React__default["default"].createElement("a", { href: "https://developers.google.com/web/updates/2015/06/checkout-faster-with-autofill", target: "_blank", rel: "noopener noreferrer" }, "Autocomplete documentation by Google")), /*#__PURE__*/React__default["default"].createElement("li", null, /*#__PURE__*/React__default["default"].createElement("a", { href: "https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete", target: "_blank", rel: "noopener noreferrer" }, "Autocomplete documentation by Mozilla"))))); }; var InputTypeWarnings = function InputTypeWarnings(_ref4) { var warnings = _ref4.warnings; if (!warnings || !warnings.length) return null; return /*#__PURE__*/React__default["default"].createElement(Spacer, null, /*#__PURE__*/React__default["default"].createElement(Hint, null), /*#__PURE__*/React__default["default"].createElement("h3", null, "Plain input type ", /*#__PURE__*/React__default["default"].createElement("code", null, "text"), " detected"), /*#__PURE__*/React__default["default"].createElement("p", null, "This will render the default text keyboard on mobile (which could very well be what you want!) If you haven't already, take a moment to make sure this is correct. You can use", ' ', /*#__PURE__*/React__default["default"].createElement("a", { href: "https://better-mobile-inputs.netlify.com/", target: "_blank", rel: "noopener noreferrer" }, "this tool"), ' ', "to explore keyboard options."), /*#__PURE__*/React__default["default"].createElement("ul", null, warnings.map(function (w, i) { return /*#__PURE__*/React__default["default"].createElement(ListEntry, { key: i }, /*#__PURE__*/React__default["default"].createElement("code", null, "input type=\"", w.type, "\""), " and label", ' ', /*#__PURE__*/React__default["default"].createElement("b", null, w.labelText || '[no label found]')); })), /*#__PURE__*/React__default["default"].createElement("details", null, /*#__PURE__*/React__default["default"].createElement("summary", null, fixText), /*#__PURE__*/React__default["default"].createElement("p", null, /*#__PURE__*/React__default["default"].createElement("a", { href: "https://css-tricks.com/better-form-inputs-for-better-mobile-user-experiences/", target: "_blank", rel: "noopener noreferrer" }, "Article reviewing the importance of using correct input types on the mobile web from CSS Tricks.")))); }; var InputTypeNumberWarnings = function InputTypeNumberWarnings(_ref5) { var warnings = _ref5.warnings; if (!warnings || !warnings.length) return null; return /*#__PURE__*/React__default["default"].createElement(Spacer, null, /*#__PURE__*/React__default["default"].createElement(Hint, null), /*#__PURE__*/React__default["default"].createElement("h3", null, "Input type ", /*#__PURE__*/React__default["default"].createElement("code", null, "number"), " detected"), /*#__PURE__*/React__default["default"].createElement("p", null, /*#__PURE__*/React__default["default"].createElement("code", null, "<input type=\"text\" inputmode=\"decimal\"/>"), ' ', "could give you improved usability over", ' ', /*#__PURE__*/React__default["default"].createElement("code", null, "<input type=\"number\" />"), "."), /*#__PURE__*/React__default["default"].createElement("p", null, "Note: ", /*#__PURE__*/React__default["default"].createElement("code", null, "inputmode"), " is styled as ", /*#__PURE__*/React__default["default"].createElement("code", null, "inputMode"), " in JSX.", ' '), /*#__PURE__*/React__default["default"].createElement("ul", null, warnings.map(function (w, i) { return /*#__PURE__*/React__default["default"].createElement(ListEntry, { key: i }, /*#__PURE__*/React__default["default"].createElement("code", null, "input type=\"", w.type, "\""), " and label", ' ', /*#__PURE__*/React__default["default"].createElement("b", null, w.labelText || '[no label found]')); })), /*#__PURE__*/React__default["default"].createElement("details", null, /*#__PURE__*/React__default["default"].createElement("summary", null, fixText), /*#__PURE__*/React__default["default"].createElement("p", null, /*#__PURE__*/React__default["default"].createElement("a", { href: "https://technology.blog.gov.uk/2020/02/24/why-the-gov-uk-design-system-team-changed-the-input-type-for-numbers/", target: "_blank", rel: "noopener noreferrer" }, "Overview of the issues with", ' ', /*#__PURE__*/React__default["default"].createElement("code", null, "input type=\"number\""), " from gov.uk.")))); }; var HeightWarnings = function HeightWarnings(_ref6) { var warnings = _ref6.warnings; if (!warnings || !warnings.length) return null; return /*#__PURE__*/React__default["default"].createElement(Spacer, null, /*#__PURE__*/React__default["default"].createElement(Hint, null), /*#__PURE__*/React__default["default"].createElement("h3", null, "Usage of ", /*#__PURE__*/React__default["default"].createElement("code", null, "100vh"), " CSS"), /*#__PURE__*/React__default["default"].createElement("p", null, /*#__PURE__*/React__default["default"].createElement("a", { href: "https://chanind.github.io/javascript/2019/09/28/avoid-100vh-on-mobile-web.html", target: "_blank", rel: "noopener noreferrer" }, "Viewport units are tricky on mobile."), ' ', "On some mobile browers, depending on scroll position, ", /*#__PURE__*/React__default["default"].createElement("code", null, "100vh"), ' ', "might take up more than 100% of screen height due to browser chrome like the address bar."), /*#__PURE__*/React__default["default"].createElement("ul", null, warnings.map(function (_ref7, i) { var path = _ref7.path; return /*#__PURE__*/React__default["default"].createElement(ListEntry, { key: i }, /*#__PURE__*/React__default["default"].createElement("code", null, path)); }))); }; var BackgroundImageWarnings = function BackgroundImageWarnings(_ref8) { var warnings = _ref8.warnings; if (!warnings || !warnings.length) return null; return /*#__PURE__*/React__default["default"].createElement(Spacer, null, /*#__PURE__*/React__default["default"].createElement(Warning, null), /*#__PURE__*/React__default["default"].createElement("h3", null, "Non-dynamic background image"), /*#__PURE__*/React__default["default"].createElement("p", null, "Downloading larger-than-necessary images hurts performance for users on mobile. You can use", ' ', /*#__PURE__*/React__default["default"].createElement("a", { href: "https://developer.mozilla.org/en-US/docs/Web/CSS/image-set", target: "_blank", rel: "noopener noreferrer" }, /*#__PURE__*/React__default["default"].createElement("code", null, "image-set")), ' ', "to serve an appropriate background image based on the user's device resolution."), /*#__PURE__*/React__default["default"].createElement("ul", null, warnings.map(function (_ref9, i) { var src = _ref9.src, alt = _ref9.alt; return /*#__PURE__*/React__default["default"].createElement(ListEntry, { key: i, nostyle: true }, /*#__PURE__*/React__default["default"].createElement("div", null, /*#__PURE__*/React__default["default"].createElement(DemoImg, { src: src, alt: alt }))); })), /*#__PURE__*/React__default["default"].createElement("details", null, /*#__PURE__*/React__default["default"].createElement("summary", null, fixText), /*#__PURE__*/React__default["default"].createElement("ul", null, /*#__PURE__*/React__default["default"].createElement("li", null, /*#__PURE__*/React__default["default"].createElement("a", { href: "https://css-tricks.com/responsive-images-css/", target: "_blank", rel: "noopener noreferrer" }, "Article discussing responsive background images in greater detail, including the interaction of ", /*#__PURE__*/React__default["default"].createElement("code", null, "image-set"), " with media queries, from CSS Tricks."))))); }; var SrcsetWarnings = function SrcsetWarnings(_ref10) { var warnings = _ref10.warnings; if (!warnings || !warnings.length) return null; return /*#__PURE__*/React__default["default"].createElement(Spacer, null, /*#__PURE__*/React__default["default"].createElement(Warning, null), /*#__PURE__*/React__default["default"].createElement("h3", null, "Large image without ", /*#__PURE__*/React__default["default"].createElement("code", null, "srcset")), /*#__PURE__*/React__default["default"].createElement("p", null, "Downloading larger-than-necessary images hurts performance for users on mobile. You can use ", /*#__PURE__*/React__default["default"].createElement("code", null, "srcset"), " to customize image sizes for different device resolutions and sizes."), /*#__PURE__*/React__default["default"].createElement("ul", null, warnings.map(function (_ref11, i) { var src = _ref11.src, alt = _ref11.alt; return /*#__PURE__*/React__default["default"].createElement(ListEntry, { key: i, nostyle: true }, /*#__PURE__*/React__default["default"].createElement("div", null, /*#__PURE__*/React__default["default"].createElement(DemoImg, { src: src, alt: alt }))); })), /*#__PURE__*/React__default["default"].createElement("details", null, /*#__PURE__*/React__default["default"].createElement("summary", null, fixText), /*#__PURE__*/React__default["default"].createElement("ul", null, /*#__PURE__*/React__default["default"].createElement("li", null, /*#__PURE__*/React__default["default"].createElement("a", { href: "https://cloudfour.com/thinks/responsive-images-the-simple-way", target: "_blank", rel: "noopener noreferrer" }, "Summary of the why and how of responsive images")), /*#__PURE__*/React__default["default"].createElement("li", null, /*#__PURE__*/React__default["default"].createElement("a", { href: "https://www.responsivebreakpoints.com/", target: "_blank", rel: "noopener noreferrer" }, "A tool to generate responsive images"))))); }; var TouchTargetWarnings = function TouchTargetWarnings(_ref12) { var warnings = _ref12.warnings; if (!warnings) return null; var underMinSize = warnings.underMinSize, tooClose = warnings.tooClose; if (!underMinSize.length && !tooClose.length) return null; return /*#__PURE__*/React__default["default"].createElement(Spacer, null, /*#__PURE__*/React__default["default"].createElement(Warning, null), Boolean(underMinSize.length) && /*#__PURE__*/React__default["default"].createElement("div", null, /*#__PURE__*/React__default["default"].createElement("h3", null, "Small touch target"), /*#__PURE__*/React__default["default"].createElement("p", null, "With heights and/or widths of less than ", MIN_SIZE, "px, these tappable elements could be difficult for users to press:"), /*#__PURE__*/React__default["default"].createElement("ul", null, underMinSize.map(function (w, i) { return /*#__PURE__*/React__default["default"].createElement(ListEntry, { key: i }, /*#__PURE__*/React__default["default"].createElement("code", null, w.type), " with content\xA0\xA0", w.text ? /*#__PURE__*/React__default["default"].createElement("b", null, w.text) : w.html ? /*#__PURE__*/React__default["default"].createElement(StyledTappableContents, { dangerouslySetInnerHTML: { __html: w.html } }) : '[no text found]'); }))), Boolean(tooClose.length) && /*#__PURE__*/React__default["default"].createElement("div", null, /*#__PURE__*/React__default["default"].createElement("h3", { style: { marginTop: underMinSize.length ? '.5rem' : '0' } }, "Touch targets close together", ' '), /*#__PURE__*/React__default["default"].createElement("p", null, "These tappable elements are less than ", RECOMMENDED_DISTANCE, "px from at least one other tappable element:"), /*#__PURE__*/React__default["default"].createElement("ul", null, tooClose.map(function (w, i) { return /*#__PURE__*/React__default["default"].createElement(ListEntry, { key: i }, /*#__PURE__*/React__default["default"].createElement("code", null, w.type), " with content\xA0\xA0", w.text ? /*#__PURE__*/React__default["default"].createElement("b", null, w.text) : w.html ? /*#__PURE__*/React__default["default"].createElement(StyledTappableContents, { dangerouslySetInnerHTML: { __html: w.html } }) : '[no text found]'); }))), /*#__PURE__*/React__default["default"].createElement("details", null, /*#__PURE__*/React__default["default"].createElement("summary", null, fixText), /*#__PURE__*/React__default["default"].createElement("ul", null, /*#__PURE__*/React__default["default"].createElement("li", null, /*#__PURE__*/React__default["default"].createElement("a", { href: "https://www.nngroup.com/articles/touch-target-size/", target: "_blank", rel: "noopener noreferrer" }, "Touch target size article from the Nielsen Norman Group")), /*#__PURE__*/React__default["default"].createElement("li", null, /*#__PURE__*/React__default["default"].createElement("a", { href: "https://web.dev/accessible-tap-targets/", target: "_blank", rel: "noopener noreferrer" }, "Tap target size recommendations from Google"))))); }; var convertToBool = function convertToBool(num) { return num > 0 ? 1 : 0; }; var getIssuesFound = function getIssuesFound(warningCount) { return warningCount + " issue" + (warningCount !== 1 ? 's' : '') + " found"; }; var Loading = function Loading() { return /*#__PURE__*/React__default["default"].createElement(StyledBanner, null, /*#__PURE__*/React__default["default"].createElement(Spinner, null), /*#__PURE__*/React__default["default"].createElement("span", null, "Running scan...")); }; var Hints = function Hints(_ref13) { var container = _ref13.container; var _React$useState = React__default["default"].useState(undefined), warnings = _React$useState[0], setWarnings = _React$useState[1]; var _React$useState2 = React__default["default"].useState(false), scanComplete = _React$useState2[0], setScanComplete = _React$useState2[1]; var _React$useState3 = React__default["default"].useState(0), rescan = _React$useState3[0], setRescan = _React$useState3[1]; React__default["default"].useEffect(function () { setScanComplete(false); setWarnings(getInstantWarnings(container)); return getScheduledWarnings(container, setWarnings, setScanComplete); }, [container, rescan]); var warningCount = React__default["default"].useMemo(function () { return warnings ? Object.keys(warnings).reduce(function (acc, key) { var curr = warnings[key]; var count = Array.isArray(curr) ? convertToBool(curr.length) : //touchTarget returns an object not an array Object.keys(curr).map(function (key) { return curr[key]; }).reduce(function (acc, curr) { return acc + convertToBool(curr.length); }, 0); return acc + count; }, 0) : 0; }, [warnings]); // Before counting, show the Loading state if (!warnings) { return /*#__PURE__*/React__default["default"].createElement(Loading, null); } var onRescanClick = function onRescanClick() { return setRescan(function (prev) { return prev + 1; }); }; if (warningCount === 0 && scanComplete) { return /*#__PURE__*/React__default["default"].createElement(StyledBanner, null, /*#__PURE__*/React__default["default"].createElement("span", null, "Scan complete! No issues found."), /*#__PURE__*/React__default["default"].createElement(StyledRescanButton, { onClick: onRescanClick, type: "button" }, "Rescan")); } var issuesFound = getIssuesFound(warningCount); return /*#__PURE__*/React__default["default"].createElement(Container, null, /*#__PURE__*/React__default["default"].createElement(StyledBanner, null, scanComplete ? /*#__PURE__*/React__default["default"].createElement(React.Fragment, null, /*#__PURE__*/React__default["default"].createElement("span", null, "Scan complete! ", issuesFound, "."), /*#__PURE__*/React__default["default"].createElement(StyledRescanButton, { onClick: onRescanClick, type: "button" }, "Rescan")) : /*#__PURE__*/React__default["default"].createElement(React.Fragment, null, /*#__PURE__*/React__default["default"].createElement(Spinner, null), /*#__PURE__*/React__default["default"].createElement("span", null, warningCount > 0 ? "Running scan - " + issuesFound + " so far" : 'Running scan', "..."))), /*#__PURE__*/React__default["default"].createElement(TouchTargetWarnings, { warnings: warnings.touchTarget }), /*#__PURE__*/React__default["default"].createElement(AutocompleteWarnings, { warnings: warnings.autocomplet