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
JavaScript
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