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 1 commit
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
Prev Previous commit
Next Next commit
Refine file input pointer instructions
  • Loading branch information
manichandra committed Jun 23, 2026
commit 951cf74cde3ccfc284782bd6e9871ffcb9a71bc7
97 changes: 75 additions & 22 deletions packages/usa-file-input/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,22 +29,27 @@ 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 DRAG_AND_DROP_MEDIA_QUERY = "(hover: none), (pointer: coarse)";
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 isLegacyDragUnsupported = () =>
/rv:11.0/i.test(navigator.userAgent) ||
/Edge\/\d./i.test(navigator.userAgent);
const instructionMediaQueryCleanups = new WeakMap();

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

return window.matchMedia(FINE_POINTER_MEDIA_QUERY);
};

const shouldShowDragText = () =>
!isLegacyDragUnsupported() && !isCoarsePointerDevice();
const shouldShowDragText = () => {
const finePointerMediaQuery = getFinePointerMediaQueryList();

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

/**
* The properties and elements within the file input.
Expand Down Expand Up @@ -180,12 +185,12 @@ 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 updateVisibleInstructions = (fileInputEl, instructions) => {
const instructionsEl = instructions;
const itemsLabel = getItemsLabel(fileInputEl);
const instructions = document.createElement("div");
const dragText = `Drag ${itemsLabel} here or`;
const dragTextEl = document.createElement("span");
const chooseTextEl = document.createElement("span");
Expand All @@ -199,30 +204,61 @@ const createVisibleInstructions = (fileInputEl) => {
? `${dragText} ${chooseText}`
: chooseText;

// 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);
instructionsEl.textContent = "";
chooseTextEl.classList.add(CHOOSE_CLASS);
chooseTextEl.textContent = chooseText;

if (dragTextIsVisible) {
dragTextEl.classList.add(DRAG_TEXT_CLASS);
dragTextEl.textContent = dragText;
instructions.appendChild(dragTextEl);
instructions.appendChild(document.createTextNode(" "));
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");

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

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

return instructions;
};

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

if (!finePointerMediaQuery) {
return null;
}

finePointerMediaQuery.addEventListener("change", handleMediaQueryChange);

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

/**
* Build a screen reader-only message element that contains file status updates and
* Create and set the default file status message
Expand Down Expand Up @@ -579,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 @@ -613,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
Loading