Visitar URL original
USWDS - File input: Remove drag instruction on mobile by manichandra · Pull Request #6660 · uswds/uswds · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 91 additions & 18 deletions packages/usa-file-input/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,28 @@ const SR_ONLY_CLASS = `${PREFIX}-sr-only`;
const SPACER_GIF =
"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
const DEFAULT_ERROR_LABEL_TEXT = "Error: This is not a valid file type.";
const FINE_POINTER_MEDIA_QUERY = "(hover: hover) and (pointer: fine)";

let TYPE_IS_VALID = Boolean(true); // logic gate for change listener
let DEFAULT_ARIA_LABEL_TEXT = "";
let DEFAULT_FILE_STATUS_TEXT = "";

const instructionMediaQueryCleanups = new WeakMap();

const getFinePointerMediaQueryList = () => {
if (typeof window.matchMedia !== "function") {
return null;
}

return window.matchMedia(FINE_POINTER_MEDIA_QUERY);
};

const shouldShowDragText = () => {
const finePointerMediaQuery = getFinePointerMediaQueryList();

return Boolean(finePointerMediaQuery && finePointerMediaQuery.matches);
};

/**
* The properties and elements within the file input.
* @typedef {Object} FileInputContext
Expand Down Expand Up @@ -168,39 +185,78 @@ const createTargetArea = (fileInputEl) => {
/**
* Build the visible element with default interaction instructions.
*
* @param {HTMLInputElement} fileInputEl - The input element.
* @returns {HTMLDivElement} The container for visible interaction instructions.
* @param {HTMLInputElement} fileInputEl - The input element
* @param {HTMLDivElement} instructions - The container for visible interaction instructions
*/
const createVisibleInstructions = (fileInputEl) => {
const fileInputParent = fileInputEl.closest(DROPZONE);
const updateVisibleInstructions = (fileInputEl, instructions) => {
const instructionsEl = instructions;
const itemsLabel = getItemsLabel(fileInputEl);
const instructions = document.createElement("div");
const dragText = `Drag ${itemsLabel} here or`;
const chooseText = "choose from folder";
const dragTextEl = document.createElement("span");
const chooseTextEl = document.createElement("span");
const dragTextIsVisible = shouldShowDragText();
const chooseText = dragTextIsVisible
? "choose from folder"
: "Choose from folder";

// Create instructions text for aria-label
DEFAULT_ARIA_LABEL_TEXT = `${dragText} ${chooseText}`;
DEFAULT_ARIA_LABEL_TEXT = dragTextIsVisible
? `${dragText} ${chooseText}`
: chooseText;

instructionsEl.textContent = "";
chooseTextEl.classList.add(CHOOSE_CLASS);
chooseTextEl.textContent = chooseText;

if (dragTextIsVisible) {
dragTextEl.classList.add(DRAG_TEXT_CLASS);
dragTextEl.textContent = dragText;
instructionsEl.appendChild(dragTextEl);
instructionsEl.appendChild(document.createTextNode(" "));
}

instructionsEl.appendChild(chooseTextEl);

if (!instructionsEl.hasAttribute("hidden")) {
fileInputEl.setAttribute("aria-label", DEFAULT_ARIA_LABEL_TEXT);
}
};

/**
* Build the visible element with default interaction instructions.
*
* @param {HTMLInputElement} fileInputEl - The input element.
* @returns {HTMLDivElement} The container for visible interaction instructions.
*/
const createVisibleInstructions = (fileInputEl) => {
const instructions = document.createElement("div");

// Adds class names and other attributes
instructions.classList.add(INSTRUCTIONS_CLASS);
instructions.setAttribute("aria-hidden", "true");

// Add initial instructions for input usage
fileInputEl.setAttribute("aria-label", DEFAULT_ARIA_LABEL_TEXT);
instructions.innerHTML = Sanitizer.escapeHTML`<span class="${DRAG_TEXT_CLASS}">${dragText}</span> <span class="${CHOOSE_CLASS}">${chooseText}</span>`;
updateVisibleInstructions(fileInputEl, instructions);

// Add the instructions element to the DOM
fileInputEl.parentNode.insertBefore(instructions, fileInputEl);

// IE11 and Edge do not support drop files on file inputs, so we've removed text that indicates that
if (
/rv:11.0/i.test(navigator.userAgent) ||
/Edge\/\d./i.test(navigator.userAgent)
) {
fileInputParent.querySelector(`.${DRAG_TEXT_CLASS}`).outerHTML = "";
return instructions;
};

const addInstructionMediaQueryListener = (fileInputEl, instructions) => {
const finePointerMediaQuery = getFinePointerMediaQueryList();
const handleMediaQueryChange = () => {
updateVisibleInstructions(fileInputEl, instructions);
};

if (!finePointerMediaQuery) {
return null;
}

return instructions;
finePointerMediaQuery.addEventListener("change", handleMediaQueryChange);

return () => {
finePointerMediaQuery.removeEventListener("change", handleMediaQueryChange);
};
};

/**
Expand Down Expand Up @@ -559,6 +615,15 @@ const fileInput = behavior(
if (fileInputEl.dataset.enhanced) return;

const { instructions, dropTarget } = enhanceFileInput(fileInputEl);
const removeInstructionMediaQueryListener =
addInstructionMediaQueryListener(fileInputEl, instructions);

if (removeInstructionMediaQueryListener) {
instructionMediaQueryCleanups.set(
fileInputEl,
removeInstructionMediaQueryListener,
);
}

dropTarget.addEventListener(
"dragover",
Expand Down Expand Up @@ -593,7 +658,15 @@ const fileInput = behavior(
},
teardown(root) {
selectOrMatches(INPUT, root).forEach((fileInputEl) => {
const removeInstructionMediaQueryListener =
instructionMediaQueryCleanups.get(fileInputEl);
const fileInputTopElement = fileInputEl.parentElement.parentElement;

if (removeInstructionMediaQueryListener) {
removeInstructionMediaQueryListener();
instructionMediaQueryCleanups.delete(fileInputEl);
}

fileInputTopElement.parentElement.replaceChild(
fileInputEl,
fileInputTopElement,
Expand Down
10 changes: 10 additions & 0 deletions packages/usa-file-input/src/test/file-input-accepts.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ const jsdomFileList = require("jsdom/lib/jsdom/living/generated/FileList");
const fileInput = require("../index");

const TEMPLATE = fs.readFileSync(`${__dirname}/template.html`);
const FINE_POINTER_MEDIA_QUERY = "(hover: hover) and (pointer: fine)";
const matchFinePointer = () => ({
matches: true,
media: FINE_POINTER_MEDIA_QUERY,
addEventListener() {},
removeEventListener() {},
});

// allows us to create mock files
function MockFile() {}
Expand Down Expand Up @@ -60,6 +67,7 @@ tests.forEach(({ name, selector: containerSelector }) => {
describe(`File input initialized at ${name}`, () => {
describe("file input component should respond to file type on change", () => {
const { body } = document;
const originalMatchMedia = window.matchMedia;
const INVALID_FILE_CLASS = "has-invalid-file";
const size = 1024 * 1024 * 2;
const mock = new MockFile();
Expand All @@ -83,6 +91,7 @@ tests.forEach(({ name, selector: containerSelector }) => {
};

beforeEach(() => {
window.matchMedia = matchFinePointer;
body.innerHTML = TEMPLATE;
fileInput.on(containerSelector());
dropZone = body.querySelector(".usa-file-input__target");
Expand All @@ -95,6 +104,7 @@ tests.forEach(({ name, selector: containerSelector }) => {
afterEach(() => {
fileInput.off(containerSelector());
body.innerHTML = "";
window.matchMedia = originalMatchMedia;
});

it("instructions are created", () => {
Expand Down
10 changes: 10 additions & 0 deletions packages/usa-file-input/src/test/file-input.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ const fileInput = require("../index");
const TEMPLATE = fs.readFileSync(
`${__dirname}/file-input-multiple.template.html`,
);
const FINE_POINTER_MEDIA_QUERY = "(hover: hover) and (pointer: fine)";
const matchFinePointer = () => ({
matches: true,
media: FINE_POINTER_MEDIA_QUERY,
addEventListener() {},
removeEventListener() {},
});

const tests = [
{ name: "document.body", selector: () => document.body },
Expand All @@ -18,6 +25,7 @@ tests.forEach(({ name, selector: containerSelector }) => {
describe(`File input initialized at ${name}`, () => {
describe("file input component builds successfully", () => {
const { body } = document;
const originalMatchMedia = window.matchMedia;

let dropZone;
let instructions;
Expand All @@ -27,6 +35,7 @@ tests.forEach(({ name, selector: containerSelector }) => {
let statusMessage;

beforeEach(() => {
window.matchMedia = matchFinePointer;
body.innerHTML = TEMPLATE;
fileInput.on(containerSelector());
dropZone = body.querySelector(".usa-file-input__target");
Expand All @@ -40,6 +49,7 @@ tests.forEach(({ name, selector: containerSelector }) => {
afterEach(() => {
fileInput.off(containerSelector());
body.innerHTML = "";
window.matchMedia = originalMatchMedia;
});

it("instructions are created", () => {
Expand Down
Loading