diff --git a/lib/app/communications/contact.js b/lib/app/communications/contact.js index 1a57c2a..734fd2b 100644 --- a/lib/app/communications/contact.js +++ b/lib/app/communications/contact.js @@ -10,14 +10,17 @@ class Contact { } async show(parent = document.body) { + const tabIndex = Math.max.apply(null, [...document.querySelectorAll('[tabindex]')].map(e => e.tabIndex ?? 0)) + 3; + const c = this.#option.contact; const contactName = createElement('input', input => { input.type = 'text'; - input.tabIndex = 1; + input.className = 'ui-input'; + input.tabIndex = tabIndex + 1; input.maxLength = 200; input.autocomplete = 'off'; }); - const preferences = new Dropdown({ tabindex: 2 }); + const preferences = new Dropdown({ tabindex: tabIndex + 2 }); preferences.source = [ { value: '0', text: r('text', 'Text') }, { value: '1', text: r('email', 'Email') }, @@ -25,23 +28,22 @@ class Contact { ]; const contactEmail = createElement('input', input => { input.type = 'email'; - input.tabIndex = 3; + input.className = 'ui-input'; + input.tabIndex = tabIndex + 3; input.maxLength = 100; input.autocomplete = 'off'; }); const contactMobile = createElement('input', input => { input.type = 'tel'; - input.tabIndex = 4; + input.className = 'ui-input'; + input.tabIndex = tabIndex + 4; input.maxLength = 50; input.autocomplete = 'off'; }); - const checkOpt = createCheckbox({ - customerAttributes: { - tabindex: 5 - } - }); + const checkOpt = createCheckbox({ tabindex: tabIndex + 5 }); const contactNotes = createElement('textarea', txt => { - txt.tabIndex = 6; + txt.className = 'ui-text'; + txt.tabIndex = tabIndex + 6; txt.maxLength = 2000; txt.style.height = '100px'; }); @@ -49,6 +51,7 @@ class Contact { if (this.#option.company) { buttons.push({ text: c == null ? r('addContactRecord', 'Add Contact Record') : r('editContactRecord', 'Edit Contact Record'), + // tabindex: tabIndex + 7, trigger: () => { const item = this.prepare(); if (item == null) { @@ -64,6 +67,7 @@ class Contact { buttons.push( { text: r('workOrderOnly', 'Work Order Only'), + // tabindex: tabIndex + 8, trigger: () => { const item = this.prepare(); if (item == null) { @@ -76,7 +80,10 @@ class Contact { } } }, - { text: r('cancel', 'Cancel') } + { + text: r('cancel', 'Cancel'), + // tabindex: tabIndex + 9 + } ); const popup = createPopup( c == null ? r('addContact', 'Add Contact') : r('editContact', 'Edit Contact'), diff --git a/lib/app/communications/customer.js b/lib/app/communications/customer.js index 2640c93..feb1258 100644 --- a/lib/app/communications/customer.js +++ b/lib/app/communications/customer.js @@ -378,7 +378,10 @@ class CustomerCommunication { } }, createElement('span', span => span.innerText = r('nameColon', 'Name:')), - createElement('input', 'ui-input') + createElement('input', input => { + input.type = 'text'; + input.className = 'ui-input'; + }) ), createElement('div', 'prompt-count'), createElement('button', button => { diff --git a/lib/app/communications/follower.js b/lib/app/communications/follower.js index fbcba0d..f939928 100644 --- a/lib/app/communications/follower.js +++ b/lib/app/communications/follower.js @@ -10,12 +10,16 @@ class Follower { } async show(parent = document.body) { + const tabIndex = Math.max.apply(null, [...document.querySelectorAll('[tabindex]')].map(e => e.tabIndex ?? 0)) + 3; + const gridContainer = createElement('div', 'follower-grid'); const popup = createPopup( r('addFollowers', 'Add Followers'), createElement('div', 'follower-wrapper', createElement('div', div => div.innerText = r('whoWantReceiveCustomerNotification', 'Who do you want to receive customer notifications?')), createElement('input', search => { + search.type = 'text'; + search.tabIndex = tabIndex + 3; search.className = 'ui-input follower-search'; search.addEventListener('input', () => { const key = search.value; diff --git a/lib/app/communications/style.scss b/lib/app/communications/style.scss index 07d700e..e6d5aa8 100644 --- a/lib/app/communications/style.scss +++ b/lib/app/communications/style.scss @@ -1,3 +1,5 @@ +@import "../../ui/css/functions/func.scss"; + .popup-mask .wrapper-edit-method { width: 100%; @@ -32,10 +34,11 @@ fill: var(--dark-fore-color); border-radius: 15px; border: none; - outline: none; transition: background-color .2s; user-select: none; + @include outline(); + &:hover { background-color: var(--dark-fore-opacity-color); @@ -218,10 +221,7 @@ font-size: var(--font-smaller-size); font-family: var(--font-family); - &:focus, - &:focus-visible { - outline: none; - } + @include outline(); } >div { diff --git a/lib/ui/checkbox.js b/lib/ui/checkbox.js index 1ddb19d..8faf596 100644 --- a/lib/ui/checkbox.js +++ b/lib/ui/checkbox.js @@ -2,9 +2,23 @@ import './css/checkbox.scss'; import { createElement } from "../functions"; import { createIcon } from "./icon"; -function fillCheckbox(container, type, label, charactor = 'check') { +function fillCheckbox(container, type = 'fa-regular', label, tabindex = -1, charactor = 'check') { container.appendChild( - createElement('layer', 'check-box-inner', createIcon(type, charactor)) + createElement('layer', layer => { + layer.className = 'check-box-inner'; + layer.addEventListener('keypress', e => { + if (e.key === ' ' || e.key === 'Enter') { + const input = container.querySelector('input'); + if (input != null) { + input.checked = !input.checked; + input.dispatchEvent(new Event('change')); + } + } + }); + if (tabindex >= 0) { + layer.tabIndex = tabindex; + } + }, createIcon(type, charactor)) ); if (label instanceof Element) { container.appendChild(label); @@ -38,7 +52,7 @@ function createRadiobox(opts = {}) { if (opts.className) { container.classList.add(opts.className); } - fillCheckbox(container, opts.type || 'fa-regular', opts.label, 'circle'); + fillCheckbox(container, opts.type, opts.label, opts.tabindex, 'circle'); return container; } @@ -78,7 +92,7 @@ function createCheckbox(opts = {}) { opts.uncheckedNode.classList.add('unchecked'); container.appendChild(opts.uncheckedNode); } else { - fillCheckbox(container, opts.type || 'fa-regular', opts.label); + fillCheckbox(container, opts.type, opts.label, opts.tabindex); } return container; } @@ -130,7 +144,7 @@ function resolveCheckbox(container = document.body, legacy) { label.className = 'checkbox-wrapper'; } label.replaceChildren(); - fillCheckbox(label, 'fa-regular', text); + fillCheckbox(label, 'fa-regular', text, chk.tabIndex); label.insertBefore(chk, label.firstChild); } } @@ -144,9 +158,10 @@ function resolveCheckbox(container = document.body, legacy) { box.classList.add('checkbox-image'); } } else { - const type = box.dataset.type || 'fa-regular'; - const label = box.dataset.label; - fillCheckbox(box, type, label) + fillCheckbox(box, + box.dataset.type, + box.dataset.label, + box.dataset.tabindex) box.removeAttribute('data-type'); box.removeAttribute('data-label'); } diff --git a/lib/ui/css/checkbox.scss b/lib/ui/css/checkbox.scss index 5c802d3..0081a0b 100644 --- a/lib/ui/css/checkbox.scss +++ b/lib/ui/css/checkbox.scss @@ -1,4 +1,4 @@ -@import './functions/checkbox.scss'; +@import "./functions/checkbox.scss"; .checkbox-image { >input[type="checkbox"] { diff --git a/lib/ui/css/common.scss b/lib/ui/css/common.scss index 223f3ef..4fda95e 100644 --- a/lib/ui/css/common.scss +++ b/lib/ui/css/common.scss @@ -1,26 +1,11 @@ +@import "../css/functions/func.scss"; + .ui-text, -.ui-input { +.ui-input[type] { font-size: var(--font-size); font-family: var(--font-family); - border: 1px solid var(--box-color); - border-radius: var(--border-radius); - transition: border-color .2s; - &:focus, - &:focus-visible { - outline: none; - } - - &:focus, - &:hover { - border-color: var(--focus-color); - } - - &:disabled { - border-color: var(--disabled-box-color); - color: var(--disabled-color); - background-color: var(--disabled-bg-color); - } + @include outborder(); } .ui-input { diff --git a/lib/ui/css/dropdown.scss b/lib/ui/css/dropdown.scss index f30cb1f..93a7e4c 100644 --- a/lib/ui/css/dropdown.scss +++ b/lib/ui/css/dropdown.scss @@ -1,4 +1,4 @@ -@import './functions/func.scss'; +@import "./functions/func.scss"; $headerHeight: 26px; $caretWidth: 26px; @@ -16,90 +16,15 @@ $listMaxHeight: 210px; border-radius: unset; user-select: none; position: relative; + font-size: var(--font-size); + font-family: var(--font-family); >.drop-header { - border: 1px solid var(--border-color); - border-radius: var(--border-radius); background-color: var(--bg-color); display: flex; height: $headerHeight; - transition: border-color .2s; - &:focus, - &:hover { - border-color: var(--focus-color); - // box-shadow: 0 0 3px 1px rgba(0, 0, 0, .2); - } - - &:focus, - &:focus-visible { - outline: none; - } - - /*>.drop-select-container { - flex: 1 1 auto; - overflow-x: auto; - white-space: nowrap; - display: flex; - flex-direction: row; - @include scrollbar(); - - &::-webkit-scrollbar { - height: $scrollBarSize; - } - - &::-webkit-scrollbar-thumb { - border-radius: var(--border-radius); - } - - >span { - display: inline-block; - margin: 2px; - padding: 0 2px; - border: 1px solid lightgray; - height: 20px; - line-height: 20px; - background-color: white; - font-size: $tinySize; - border-radius: var(--border-radius); - cursor: pointer; - position: relative; - - >svg { - display: none; - width: 8px; - height: 20px; - fill: white; - vertical-align: top; - } - - &:hover { - border-color: #1890ff; - border-top-right-radius: 0; - border-bottom-right-radius: 0; - background-color: #1890ff; - color: white; - - >svg { - display: inline-block; - margin-left: 3px; - padding: 0 2px; - } - } - } - - >label { - flex: 1 1 auto; - min-width: 40px; - cursor: pointer; - outline: none; - line-height: $headerHeight; - padding: 0 4px; - font-weight: 400; - font-size: var(--font-smaller-size); - color: var(--color); - } - }*/ + @include outborder(); >.drop-text { flex: 1 1 auto; @@ -110,8 +35,9 @@ $listMaxHeight: 210px; overflow: hidden; text-overflow: ellipsis; border: none; - outline: none; white-space: nowrap; + + @include outline(); } >input.drop-text { @@ -143,11 +69,12 @@ $listMaxHeight: 210px; } &.disabled { - border-color: var(--disabled-bg-color); + border-color: var(--disabled-border-color); + background-color: var(--disabled-bg-color); color: var(--disabled-color); &:focus { - border-color: var(--disabled-bg-color); + border-color: var(--disabled-border-color); // box-shadow: none; } @@ -170,8 +97,6 @@ $listMaxHeight: 210px; transition: transform 120ms ease, opacity 120ms ease, visibility 120ms ease; width: calc(100% + 2px); box-sizing: border-box; - /*border: 1px solid var(--border-color); - border-top-width: 0;*/ box-shadow: 0 3px 6px -4px rgba(0, 0, 0, .12), 0 6px 16px 0 rgba(0, 0, 0, .08), 0 9px 28px 8px rgba(0, 0, 0, .05); left: -1px; @@ -202,17 +127,10 @@ $listMaxHeight: 210px; box-sizing: border-box; width: 100%; height: $searchInputHeight; - outline: none; - border: 1px solid var(--border-color); - border-radius: var(--border-radius); padding: 0 6px 0 22px; color: var(--color); - transition: border-color .2s; - &:hover, - &:focus { - border-color: var(--focus-color); - } + @include outborder(); // &:focus { // box-shadow: 0 0 3px 1px rgba(0, 0, 0, .2); diff --git a/lib/ui/css/functions/checkbox.scss b/lib/ui/css/functions/checkbox.scss index c8ddd1e..216ff54 100644 --- a/lib/ui/css/functions/checkbox.scss +++ b/lib/ui/css/functions/checkbox.scss @@ -1,3 +1,5 @@ +@import "./func.scss"; + @mixin check-box() { .check-box-inner { position: relative; @@ -6,12 +8,11 @@ width: 14px; height: 14px; background-color: #fff; - border: 1px solid var(--box-color); user-select: none; - border-radius: 2px; - transition: all .2s; cursor: pointer; + @include outborder(); + >svg { position: absolute; top: 0; @@ -50,17 +51,18 @@ &:disabled { &+.check-box-inner { - border-color: var(--disabled-box-color); + border-color: var(--disabled-border-color); + background-color: var(--disabled-bg-color); cursor: default; } &:checked+.check-box-inner { - border-color: var(--disabled-box-color); - background-color: var(--disabled-box-color); + border-color: var(--disabled-border-color); + background-color: var(--disabled-border-color); } &~span { - color: var(--disabled-box-color); + color: var(--disabled-border-color); cursor: default; } } diff --git a/lib/ui/css/functions/func.scss b/lib/ui/css/functions/func.scss index 02b909a..4d8b6ae 100644 --- a/lib/ui/css/functions/func.scss +++ b/lib/ui/css/functions/func.scss @@ -15,4 +15,31 @@ background-color: rgba(168, 168, 168, 0.9); border-radius: 4px; } +} + +@mixin outline() { + + &:focus, + &:focus-visible { + outline: none; + } +} + +@mixin outborder() { + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + transition: border-color .12s ease; + + @include outline(); + + &:focus, + &:hover { + border-color: var(--focus-border-color); + } + + &:disabled { + border-color: var(--disabled-border-color); + color: var(--disabled-color); + background-color: var(--disabled-bg-color); + } } \ No newline at end of file diff --git a/lib/ui/css/grid.scss b/lib/ui/css/grid.scss index 931e36c..c1fdd88 100644 --- a/lib/ui/css/grid.scss +++ b/lib/ui/css/grid.scss @@ -1,4 +1,4 @@ -@import './functions/func.scss'; +@import "./functions/func.scss"; .grid { position: relative; @@ -43,16 +43,11 @@ --spacing-cell: 6px 4px 6px 8px; } - &:focus, - &:focus-visible { - outline: none; - } + @include outline(); &, input[type="text"], - textarea, - .drop-wrapper>.drop-header>.drop-text, - .drop-wrapper>.drop-box>.drop-list { + textarea { font-size: var(--font-size); font-family: var(--font-family); } @@ -238,10 +233,7 @@ width: 100%; padding: 0; - &:focus, - &:focus-visible { - outline: none; - } + @include outline(); &:disabled { color: var(--text-disabled-color); diff --git a/lib/ui/css/popup.scss b/lib/ui/css/popup.scss index 9ec5ec8..0f8db69 100644 --- a/lib/ui/css/popup.scss +++ b/lib/ui/css/popup.scss @@ -1,4 +1,4 @@ -@import './functions/func.scss'; +@import "./functions/func.scss"; $headerLineHeight: 24px; $buttonHeight: 28px; @@ -75,6 +75,14 @@ $buttonHeight: 28px; &:hover { opacity: .8; } + + &:focus, + &:focus-visible { + outline: none; + opacity: .8; + background-color: rgb(0 0 0/10%); + border-radius: var(--corner-radius); + } } } @@ -118,7 +126,7 @@ $buttonHeight: 28px; width: 40px; height: 40px; - &+span { + +span { padding-left: 16px; } } @@ -148,7 +156,7 @@ $buttonHeight: 28px; font-weight: bold; } - &+* { + +* { flex: 1 1 auto; margin-right: 10px; box-sizing: border-box; @@ -156,30 +164,11 @@ $buttonHeight: 28px; line-height: var(--line-height); } - &+input[type="text"], - &+input[type="email"], - &+input[type="tel"], - &+textarea { - border: 1px solid var(--border-color); - border-radius: var(--border-radius); + +textarea { text-indent: var(--text-indent); - transition: border-color .12s ease; - - &:hover { - border-color: var(--focus-color); - } - - &:focus, - &:focus-visible { - outline: none; - } } - &+.drop-wrapper>.drop-header>.drop-text { - font-size: 12px; - } - - &+.checkbox-wrapper { + +.checkbox-wrapper { padding: 0; } } @@ -210,14 +199,12 @@ $buttonHeight: 28px; background-color: var(--title-bg-color); transition: opacity .12s ease; + &:focus, &:hover { opacity: .8; } - &:focus, - &:focus-visible { - outline: none; - } + @include outline(); } } diff --git a/lib/ui/css/tooltip.scss b/lib/ui/css/tooltip.scss index 860e97e..b19b41d 100644 --- a/lib/ui/css/tooltip.scss +++ b/lib/ui/css/tooltip.scss @@ -1,8 +1,11 @@ +@import "./functions/func.scss"; + .tooltip-color { background-color: #fff; color: #323130; border-color: rgba(204, 204, 204, .8); - outline: none; + + @include outline(); } .tooltip-wrapper { diff --git a/lib/ui/css/variables/definition.scss b/lib/ui/css/variables/definition.scss index 1e44cdd..cf0fcb4 100644 --- a/lib/ui/css/variables/definition.scss +++ b/lib/ui/css/variables/definition.scss @@ -11,16 +11,16 @@ :root { --color: #201f1e; - --red-color: red; --bg-color: #fff; + --border-color: #b9b9b9; + --focus-border-color: #666; + --disabled-color: #aaa; + --disabled-bg-color: #e9e9e9; + --disabled-border-color: #d9d9d9; + + --red-color: red; --title-color: #fff; --title-bg-color: rgb(68, 114, 196); - --border-color: #d9d9d9; - --focus-color: #666; - --disabled-bg-color: #e9e9e9; - --disabled-color: #aaa; - --box-color: #999898; - --disabled-box-color: #d9d9d9; --hover-bg-color: #eee; --link-color: #1890ff; --primary-color: rgb(123, 28, 33); diff --git a/lib/ui/dropdown.js b/lib/ui/dropdown.js index 2795029..9a3b1ad 100644 --- a/lib/ui/dropdown.js +++ b/lib/ui/dropdown.js @@ -120,12 +120,54 @@ class Dropdown { // header const header = createElement('div', 'drop-header'); + header.addEventListener('keypress', e => { + if (e.key === ' ' || e.key === 'Enter') { + header.dispatchEvent(new MouseEvent('click')); + } + }); + header.addEventListener('keydown', e => { + const up = e.key === 'ArrowUp'; + const down = e.key === 'ArrowDown'; + if (up || down) { + const source = this.source; + const count = source.length; + const valuekey = this.#options.valuekey; + let index = source?.indexOf(this.#selected); + if (isNaN(index) || index < -1) { + index = -1; + } else if (index >= count) { + index = count - 1; + } + if (up) { + if (index > 0) { + index--; + } else { + index = 0; + } + } else if (down) { + if (index < 0) { + index = 0; + } else if (index < count) { + index++; + } else { + index = count - 1; + } + } + const target = source[index]?.[valuekey]; + if (target != null) { + this.select(target); + } + } else if (e.key === 'Tab') { + this.#dropdown(false); + } + }); header.addEventListener('click', () => { if (this.disabled) { return; } const active = this.#expanded; - if (active && this.#label.hasFocus()) { + const label = this.#label; + if (active && label.ownerDocument.activeElement === label) { return; } this.#dropdown(!active); @@ -230,6 +272,10 @@ class Dropdown { } this.#label.value = selected; } else { + const expanded = this.#expanded; + if (expanded) { + this.#container.querySelectorAll('li[data-value].selected').forEach(li => li.classList.remove('selected')); + } if (item == null) { this.#selected = null; this.#label.innerText = ' '; @@ -245,6 +291,13 @@ class Dropdown { } this.#label.innerText = text; } + if (expanded) { + const val = selected.replace(/"/g, '\\"'); + const li = this.#container.querySelector(`li[data-value="${val}"]`); + if (li != null) { + li.classList.add('selected'); + } + } } this.#selected = item; if (!silence && typeof this.onselected === 'function') { @@ -276,7 +329,7 @@ class Dropdown { } } - get #expanded() { return this.#container?.style?.visibility === 'visible' } + get #expanded() { return this.#container?.classList?.contains('active') } #dropdown(flag = true) { const options = this.#options; diff --git a/lib/ui/popup.js b/lib/ui/popup.js index 0354e2e..c202481 100644 --- a/lib/ui/popup.js +++ b/lib/ui/popup.js @@ -110,6 +110,8 @@ class Popup { mask.classList.add('popup-transparent'); } const container = createElement('div', 'popup-container'); + let tabIndex = Math.max.apply(null, [...document.querySelectorAll('[tabindex]')].map(e => e.tabIndex ?? 0)); + container.tabIndex = tabIndex + 1; const close = () => { mask.classList.add('popup-active'); mask.style.opacity = 0; @@ -155,7 +157,13 @@ class Popup { } if (this.#option.collapsable === true) { const collapse = createIcon('fa-regular', 'compress-alt'); + collapse.tabIndex = tabIndex + 2; collapse.classList.add('icon-expand'); + collapse.addEventListener('keypress', e => { + if (e.key === ' ' || e.key === 'Enter') { + collapse.dispatchEvent(new MouseEvent('click')); + } + }); collapse.addEventListener('click', () => { if (container.classList.contains('popup-collapse')) { const bounds = this.#bounds; @@ -176,6 +184,12 @@ class Popup { header.appendChild(collapse); } const cancel = createIcon('fa-regular', 'times'); + cancel.tabIndex = tabIndex + 3; + cancel.addEventListener('keypress', e => { + if (e.key === ' ' || e.key === 'Enter') { + close(); + } + }); cancel.addEventListener('click', () => close()); header.appendChild(cancel); }), @@ -184,9 +198,15 @@ class Popup { )) ); if (Array.isArray(this.#option.buttons)) { + tabIndex = Math.max.apply(null, [...container.querySelectorAll('[tabindex]')].map(e => e.tabIndex ?? 0)); container.appendChild( - createElement('div', 'popup-footer', ...this.#option.buttons.map(b => { - const button = createElement('div', 'popup-button'); + createElement('div', 'popup-footer', ...this.#option.buttons.map((b, i) => { + const button = createElement('button', 'popup-button'); + if (b.tabindex > 0) { + button.tabIndex = b.tabindex; + } else { + button.tabIndex = tabIndex + i + 1; + } button.innerText = b.text; button.addEventListener('click', () => { if (typeof b.trigger === 'function') { @@ -207,6 +227,19 @@ class Popup { return button; })) ); + const tabs = [...container.querySelectorAll('[tabindex]')].map(e => e.tabIndex ?? 0); + const tabMin = Math.min.apply(null, tabs); + const tabMax = Math.max.apply(null, tabs); + const last = container.querySelector(`[tabindex="${tabMax}"]`); + if (last != null) { + last.addEventListener('keydown', e => { + if (e.key === 'Tab') { + const first = container.querySelector(`[tabindex="${tabMin}"]`); + first?.focus(); + e.preventDefault(); + } + }); + } } // resizable if (this.#option.resizable === true) { @@ -264,7 +297,8 @@ class Popup { } return new Promise(resolve => { setTimeout(() => { - mask.style.opacity = 1 + mask.style.opacity = 1; + this.container.focus(); resolve(mask); }, 0); }); @@ -396,7 +430,10 @@ export function showAlert(title, message, iconType = 'info', parent = document.b { text: r('ok', 'OK'), trigger: resolve } ] }); - popup.show(parent); + popup.show(parent).then(mask => { + const button = mask.querySelector('.popup-container .popup-footer .popup-button:last-child'); + button?.focus(); + }); }); } @@ -442,6 +479,9 @@ export function showConfirm(title, content, buttons, iconType = 'question', pare { text: r('no', 'No'), trigger: p => resolve({ key: 'no', popup: p }) } ] }); - popup.show(parent); + popup.show(parent).then(mask => { + const button = mask.querySelector('.popup-container .popup-footer .popup-button:last-child'); + button?.focus(); + }); }); } \ No newline at end of file