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
14 changes: 10 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,17 @@ jobs:
restore-keys: |
${{ runner.os }}-node-

- name: npm install and test
run: npm run setup && npm run js:test
- name: setup JS
run: npm run setup

- name: typecheck
run: npm run build && npm run typecheck:tests

- name: check lint and format
run: npm run js:lint && npm run js:format.check

- name: eslint
run: npx eslint
- name: test
run: npm run js:test

- uses: actions/upload-artifact@v4
if: always()
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ node_modules
/test/e2e/test-results/
/playwright-report/
/coverage/
/assets/js/types/
1 change: 1 addition & 0 deletions assets/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
js/types/
Empty file added assets/.prettierrc
Empty file.
79 changes: 48 additions & 31 deletions assets/js/phoenix_live_view/aria.js
Original file line number Diff line number Diff line change
@@ -1,49 +1,66 @@
let ARIA = {
anyOf(instance, classes){ return classes.find(name => instance instanceof name) },
const ARIA = {
anyOf(instance, classes) {
return classes.find((name) => instance instanceof name);
},

isFocusable(el, interactiveOnly){
isFocusable(el, interactiveOnly) {
return (
(el instanceof HTMLAnchorElement && el.rel !== "ignore") ||
(el instanceof HTMLAreaElement && el.href !== undefined) ||
(!el.disabled && (this.anyOf(el, [HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement, HTMLButtonElement]))) ||
(el instanceof HTMLIFrameElement) ||
(el.tabIndex >= 0 || (!interactiveOnly && el.getAttribute("tabindex") !== null && el.getAttribute("aria-hidden") !== "true"))
)
(!el.disabled &&
this.anyOf(el, [
HTMLInputElement,
HTMLSelectElement,
HTMLTextAreaElement,
HTMLButtonElement,
])) ||
el instanceof HTMLIFrameElement ||
el.tabIndex >= 0 ||
(!interactiveOnly &&
el.getAttribute("tabindex") !== null &&
el.getAttribute("aria-hidden") !== "true")
);
},

attemptFocus(el, interactiveOnly){
if(this.isFocusable(el, interactiveOnly)){ try { el.focus() } catch {} }
return !!document.activeElement && document.activeElement.isSameNode(el)
attemptFocus(el, interactiveOnly) {
if (this.isFocusable(el, interactiveOnly)) {
try {
el.focus();
} catch {
// that's fine
}
}
return !!document.activeElement && document.activeElement.isSameNode(el);
},

focusFirstInteractive(el){
let child = el.firstElementChild
while(child){
if(this.attemptFocus(child, true) || this.focusFirstInteractive(child, true)){
return true
focusFirstInteractive(el) {
let child = el.firstElementChild;
while (child) {
if (this.attemptFocus(child, true) || this.focusFirstInteractive(child)) {
return true;
}
child = child.nextElementSibling
child = child.nextElementSibling;
}
},

focusFirst(el){
let child = el.firstElementChild
while(child){
if(this.attemptFocus(child) || this.focusFirst(child)){
return true
focusFirst(el) {
let child = el.firstElementChild;
while (child) {
if (this.attemptFocus(child) || this.focusFirst(child)) {
return true;
}
child = child.nextElementSibling
child = child.nextElementSibling;
}
},

focusLast(el){
let child = el.lastElementChild
while(child){
if(this.attemptFocus(child) || this.focusLast(child)){
return true
focusLast(el) {
let child = el.lastElementChild;
while (child) {
if (this.attemptFocus(child) || this.focusLast(child)) {
return true;
}
child = child.previousElementSibling
child = child.previousElementSibling;
}
}
}
export default ARIA
},
};
export default ARIA;
121 changes: 71 additions & 50 deletions assets/js/phoenix_live_view/browser.js
Original file line number Diff line number Diff line change
@@ -1,84 +1,105 @@
let Browser = {
canPushState(){ return (typeof (history.pushState) !== "undefined") },
const Browser = {
canPushState() {
return typeof history.pushState !== "undefined";
},

dropLocal(localStorage, namespace, subkey){
return localStorage.removeItem(this.localKey(namespace, subkey))
dropLocal(localStorage, namespace, subkey) {
return localStorage.removeItem(this.localKey(namespace, subkey));
},

updateLocal(localStorage, namespace, subkey, initial, func){
let current = this.getLocal(localStorage, namespace, subkey)
let key = this.localKey(namespace, subkey)
let newVal = current === null ? initial : func(current)
localStorage.setItem(key, JSON.stringify(newVal))
return newVal
updateLocal(localStorage, namespace, subkey, initial, func) {
const current = this.getLocal(localStorage, namespace, subkey);
const key = this.localKey(namespace, subkey);
const newVal = current === null ? initial : func(current);
localStorage.setItem(key, JSON.stringify(newVal));
return newVal;
},

getLocal(localStorage, namespace, subkey){
return JSON.parse(localStorage.getItem(this.localKey(namespace, subkey)))
getLocal(localStorage, namespace, subkey) {
return JSON.parse(localStorage.getItem(this.localKey(namespace, subkey)));
},

updateCurrentState(callback){
if(!this.canPushState()){ return }
history.replaceState(callback(history.state || {}), "", window.location.href)
updateCurrentState(callback) {
if (!this.canPushState()) {
return;
}
history.replaceState(
callback(history.state || {}),
"",
window.location.href,
);
},

pushState(kind, meta, to){
if(this.canPushState()){
if(to !== window.location.href){
if(meta.type == "redirect" && meta.scroll){
pushState(kind, meta, to) {
if (this.canPushState()) {
if (to !== window.location.href) {
if (meta.type == "redirect" && meta.scroll) {
// If we're redirecting store the current scrollY for the current history state.
let currentState = history.state || {}
currentState.scroll = meta.scroll
history.replaceState(currentState, "", window.location.href)
const currentState = history.state || {};
currentState.scroll = meta.scroll;
history.replaceState(currentState, "", window.location.href);
}

delete meta.scroll // Only store the scroll in the redirect case.
history[kind + "State"](meta, "", to || null) // IE will coerce undefined to string
delete meta.scroll; // Only store the scroll in the redirect case.
history[kind + "State"](meta, "", to || null); // IE will coerce undefined to string

// when using navigate, we'd call pushState immediately before patching the DOM,
// jumping back to the top of the page, effectively ignoring the scrollIntoView;
// therefore we wait for the next frame (after the DOM patch) and only then try
// to scroll to the hashEl
window.requestAnimationFrame(() => {
let hashEl = this.getHashTargetEl(window.location.hash)
if(hashEl){
hashEl.scrollIntoView()
} else if(meta.type === "redirect"){
window.scroll(0, 0)
const hashEl = this.getHashTargetEl(window.location.hash);

if (hashEl) {
hashEl.scrollIntoView();
} else if (meta.type === "redirect") {
window.scroll(0, 0);
}
})
});
}
} else {
this.redirect(to)
this.redirect(to);
}
},

setCookie(name, value, maxAgeSeconds){
let expires = typeof(maxAgeSeconds) === "number" ? ` max-age=${maxAgeSeconds};` : ""
document.cookie = `${name}=${value};${expires} path=/`
setCookie(name, value, maxAgeSeconds) {
const expires =
typeof maxAgeSeconds === "number" ? ` max-age=${maxAgeSeconds};` : "";
document.cookie = `${name}=${value};${expires} path=/`;
},

getCookie(name){
return document.cookie.replace(new RegExp(`(?:(?:^|.*;\s*)${name}\s*\=\s*([^;]*).*$)|^.*$`), "$1")
getCookie(name) {
return document.cookie.replace(
new RegExp(`(?:(?:^|.*;\s*)${name}\s*\=\s*([^;]*).*$)|^.*$`),
"$1",
);
},

deleteCookie(name){
document.cookie = `${name}=; max-age=-1; path=/`
deleteCookie(name) {
document.cookie = `${name}=; max-age=-1; path=/`;
},

redirect(toURL, flash){
if(flash){ this.setCookie("__phoenix_flash__", flash, 60) }
window.location = toURL
redirect(toURL, flash) {
if (flash) {
this.setCookie("__phoenix_flash__", flash, 60);
}
window.location.href = toURL;
},

localKey(namespace, subkey){ return `${namespace}-${subkey}` },
localKey(namespace, subkey) {
return `${namespace}-${subkey}`;
},

getHashTargetEl(maybeHash){
let hash = maybeHash.toString().substring(1)
if(hash === ""){ return }
return document.getElementById(hash) || document.querySelector(`a[name="${hash}"]`)
}
}
getHashTargetEl(maybeHash) {
const hash = maybeHash.toString().substring(1);
if (hash === "") {
return;
}
return (
document.getElementById(hash) ||
document.querySelector(`a[name="${hash}"]`)
);
},
};

export default Browser
export default Browser;
Loading
Loading