feat: improve website accessibility interactions

This commit is contained in:
sladro 2026-04-13 12:10:29 +08:00
parent 03497f97a7
commit f033f29c08
22 changed files with 351 additions and 151 deletions

View File

@ -70,6 +70,13 @@ export function AddDiv (parentElement, className, innerHTML)
return AddDomElement (parentElement, 'div', className, innerHTML);
}
export function AddButtonElement (parentElement, className, innerHTML)
{
let button = AddDomElement (parentElement, 'button', className, innerHTML);
button.setAttribute ('type', 'button');
return button;
}
export function ClearDomElement (element)
{
while (element.firstChild) {
@ -139,3 +146,10 @@ export function CreateDiv (className, innerHTML)
{
return CreateDomElement ('div', className, innerHTML);
}
export function CreateButtonElement (className, innerHTML)
{
let button = CreateDomElement ('button', className, innerHTML);
button.setAttribute ('type', 'button');
return button;
}

View File

@ -51,18 +51,23 @@ div.ov_thin_scrollbar::-webkit-scrollbar-thumb
background: #cccccc;
}
div.ov_button
.ov_button
{
color: var(--ov_button_text_color);
background: var(--ov_button_color);
text-align: center;
padding: 3px;
padding: 8px 16px;
min-height: 44px;
border: 1px solid var(--ov_button_color);
border-radius: 5px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
div.ov_button.outline
.ov_button.outline
{
color: var(--ov_outline_button_text_color);
background: transparent;
@ -173,7 +178,6 @@ input.ov_slider
{
height: 1px;
background: var(--ov_border_color);
outline: none;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
@ -207,7 +211,7 @@ span.ov_slider_label
bottom: -4px;
}
div.ov_toggle
button.ov_toggle
{
width: 24px;
height: 8px;
@ -218,7 +222,7 @@ div.ov_toggle
cursor: pointer;
}
div.ov_toggle_slider
button.ov_toggle div.ov_toggle_slider
{
width: 6px;
height: 6px;
@ -227,12 +231,12 @@ div.ov_toggle_slider
border: 1px solid var(--ov_foreground_color);
}
div.ov_toggle.on
button.ov_toggle.on
{
background: var(--ov_foreground_color);
}
div.ov_toggle.on div.ov_toggle_slider
button.ov_toggle.on div.ov_toggle_slider
{
background: var(--ov_background_color);
transform: translateX(16px);
@ -247,13 +251,13 @@ div.ov_svg_icon.selected:hover
color: var(--ov_hover_text_color);
}
div.ov_button:hover
.ov_button:hover
{
background: var(--ov_button_hover_color);
border: 1px solid var(--ov_button_hover_color);
}
div.ov_button.outline:hover
.ov_button.outline:hover
{
background: var(--ov_outline_button_hover_color);
border: 1px solid var(--ov_outline_button_color);

View File

@ -28,6 +28,21 @@ a
text-decoration: none;
}
button
{
font-family: Quicksand, Helvetica, sans-serif;
font-size: 16px;
}
button[class]
{
color: inherit;
background: none;
border: 0px;
padding: 0px;
margin: 0px;
}
img
{
display: block;
@ -54,7 +69,18 @@ input, select, textarea
{
font-family: Quicksand, Helvetica, sans-serif;
font-size: 16px;
outline: none;
}
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
canvas:focus-visible,
[role=button]:focus-visible
{
outline: 2px solid var(--ov_focus_ring_color);
outline-offset: 2px;
}
@media (hover)

View File

@ -57,7 +57,7 @@ div.ov_dialog div.ov_dialog_buttons_inner
overflow: auto;
}
div.ov_dialog div.ov_dialog_buttons div.ov_dialog_button
div.ov_dialog div.ov_dialog_buttons .ov_dialog_button
{
margin-left: 10px;
width: 80px;
@ -104,7 +104,7 @@ div.ov_dialog div.ov_dialog_import_file_list
overflow: auto;
}
div.ov_dialog div.ov_dialog_file_link
div.ov_dialog .ov_dialog_file_link
{
color: var(--ov_button_color);
padding: 5px;
@ -114,7 +114,7 @@ div.ov_dialog div.ov_dialog_file_link
cursor: pointer;
}
div.ov_dialog div.ov_dialog_file_link div.ov_file_link_img
div.ov_dialog .ov_dialog_file_link div.ov_file_link_img
{
color: var(--ov_button_color);
margin-top: 2px;
@ -122,7 +122,7 @@ div.ov_dialog div.ov_dialog_file_link div.ov_file_link_img
float: left;
}
div.ov_dialog div.ov_dialog_file_link div.ov_dialog_file_link_text
div.ov_dialog .ov_dialog_file_link div.ov_dialog_file_link_text
{
float: left;
}
@ -145,7 +145,7 @@ div.ov_dialog div.ov_dialog_copyable_input input
box-sizing: border-box;
}
div.ov_dialog div.ov_dialog_copyable_input div.ov_dialog_copyable_input_button
div.ov_dialog div.ov_dialog_copyable_input .ov_dialog_copyable_input_button
{
width: 28%;
margin-left: 0px;
@ -196,12 +196,15 @@ div.ov_popup div.ov_popup_list
overflow: auto;
}
div.ov_popup div.ov_popup_list_item
div.ov_popup .ov_popup_list_item
{
padding: 10px;
border-radius: 5px;
cursor: pointer;
overflow: auto;
display: block;
width: 100%;
text-align: left;
}
div.ov_popup div.ov_popup_list_item_icon
@ -299,18 +302,18 @@ div.ov_snapshot_dialog_separator
@media (hover)
{
div.ov_dialog div.ov_dialog_file_link:hover
div.ov_dialog .ov_dialog_file_link:hover
{
color: var(--ov_hover_text_color);
background: var(--ov_hover_color);
}
div.ov_dialog div.ov_dialog_file_link:hover div.ov_file_link_img
div.ov_dialog .ov_dialog_file_link:hover div.ov_file_link_img
{
color: var(--ov_hover_text_color);
}
div.ov_popup div.ov_popup_list_item:hover
div.ov_popup .ov_popup_list_item:hover
{
background: var(--ov_hover_color);
}

View File

@ -6,14 +6,15 @@ div.ov_navigator_buttons
overflow: auto;
}
div.ov_navigator_button
.ov_navigator_button
{
float: left;
cursor: pointer;
padding: 5px;
padding: 13px;
display: block;
}
div.ov_navigator_button.right
.ov_navigator_button.right
{
float: right;
}
@ -76,7 +77,7 @@ div.ov_navigator_info_panel div.ov_navigator_info_panel_title:hover
background: var(--ov_hover_color);
}
div.ov_navigator_button:hover
.ov_navigator_button:hover
{
background: var(--ov_hover_color);
}

View File

@ -8,10 +8,21 @@ div.ov_panel_set_right_container div.ov_panel_set_menu
float: right;
}
div.ov_panel_set_menu div.ov_panel_set_menu_button
.ov_panel_set_menu .ov_panel_set_menu_button
{
padding: 10px;
padding: 13px;
cursor: pointer;
display: block;
}
.ov_panel_set_menu .ov_panel_set_menu_button.selected
{
background: var(--ov_hover_color);
}
.ov_panel_set_menu .ov_panel_set_menu_button.selected .ov_svg_icon
{
color: var(--ov_selected_icon_color);
}
div.ov_panel_set_container div.ov_panel_set_content
@ -28,13 +39,16 @@ div.ov_panel_set_right_container div.ov_panel_set_content
overflow: auto;
}
div.ov_panel_button
.ov_panel_button
{
cursor: pointer;
margin-top: 10px;
border: 1px solid var(--ov_border_color);
border-radius: 5px;
overflow: auto;
display: block;
width: 100%;
text-align: left;
}
div.ov_panel_button_text
@ -60,13 +74,13 @@ div.ov_panel_button_left_icon
@media (hover)
{
div.ov_panel_button:hover
.ov_panel_button:hover
{
background: var(--ov_hover_color);
}
div.ov_panel_set_menu div.ov_panel_set_menu_button:hover
.ov_panel_set_menu .ov_panel_set_menu_button:hover
{
background: var(--ov_hover_color);
}

View File

@ -40,18 +40,19 @@ div.ov_sidebar_parameter div.ov_sidebar_parameter_text
float: left;
}
div.ov_sidebar_image_picker
.ov_sidebar_image_picker
{
background-size: cover;
background-position: center center;
width: 28px;
height: 13px;
width: 44px;
height: 26px;
margin-top: 3px;
margin-right: 10px;
border: 1px solid var(--ov_border_color);
border-radius: 3px;
float: left;
cursor: pointer;
display: block;
}
div.ov_sidebar_content
@ -105,7 +106,6 @@ div.ov_sidebar_content button.pcr-button
margin: 3px 10px 3px 0px;
border: 1px solid var(--ov_border_color);
box-shadow: none;
outline: none;
float: left;
}

View File

@ -3,18 +3,18 @@
--ov_foreground_color: #000000;
--ov_background_color: #ffffff;
--ov_disabled_foreground_color: #cccccc;
--ov_button_color: #3393bd;
--ov_button_hover_color: #146a8f;
--ov_button_color: #1f6f94;
--ov_button_hover_color: #165876;
--ov_button_text_color: #ffffff;
--ov_outline_button_color: #3393bd;
--ov_outline_button_hover_color: #c9e5f8;
--ov_outline_button_text_color: #3393bd;
--ov_outline_button_color: #1f6f94;
--ov_outline_button_hover_color: #d9e9f2;
--ov_outline_button_text_color: #1f6f94;
--ov_icon_color: #263238;
--ov_light_icon_color: #838383;
--ov_selected_icon_color: #3393bd;
--ov_selected_icon_color: #1f6f94;
--ov_disabled_icon_color: #cccccc;
--ov_hover_color: #c9e5f8;
--ov_hover_text_color: #3393bd;
--ov_hover_color: #d9e9f2;
--ov_hover_text_color: #1f6f94;
--ov_logo_text_color: #15334a;
--ov_logo_border_color: #000000;
--ov_toolbar_background_color: #f5f5f5;
@ -26,19 +26,20 @@
--ov_dialog_control_border_color: #e1e1e1;
--ov_border_color: #dddddd;
--ov_shadow: 0px 0px 10px #cccccc;
--ov_focus_ring_color: #0d5c80;
--ov_foreground_color_dark: #fafafa;
--ov_background_color_dark: #2a2b2e;
--ov_disabled_foreground_color_dark: #888888;
--ov_button_color_dark: #3393bd;
--ov_button_hover_color_dark: #146a8f;
--ov_button_color_dark: #3e8fb5;
--ov_button_hover_color_dark: #5aa5c8;
--ov_button_text_color_dark: #ffffff;
--ov_outline_button_color_dark: #c9e5f8;
--ov_outline_button_hover_color_dark: #2f6984;
--ov_outline_button_text_color_dark: #c9e5f8;
--ov_outline_button_color_dark: #9fd1ea;
--ov_outline_button_hover_color_dark: #315d70;
--ov_outline_button_text_color_dark: #d8f0fb;
--ov_icon_color_dark: #fafafa;
--ov_light_icon_color_dark: #bababa;
--ov_selected_icon_color_dark: #3393bd;
--ov_selected_icon_color_dark: #9fd1ea;
--ov_disabled_icon_color_dark: #888888;
--ov_hover_color_dark: #667c86;
--ov_hover_text_color_dark: #fafafa;
@ -53,4 +54,5 @@
--ov_dialog_control_border_color_dark: #e1e1e1;
--ov_border_color_dark: #444444;
--ov_shadow_dark: 0px 0px 10px #222222;
--ov_focus_ring_color_dark: #8fc8e5;
}

View File

@ -27,20 +27,28 @@ div.ov_tree_view div.ov_tree_item_button_container
float: right;
}
div.ov_tree_view div.ov_tree_item_button
.ov_tree_view .ov_tree_item_button
{
color: var(--ov_light_icon_color);
padding: 5px;
padding: 13px;
float: left;
cursor: pointer;
display: block;
}
div.ov_tree_view div.ov_tree_item_icon
.ov_tree_view div.ov_tree_item_icon,
.ov_tree_view button.ov_tree_item_icon
{
padding: 5px;
float: left;
}
.ov_tree_view button.ov_tree_item_toggle
{
display: block;
cursor: pointer;
}
div.ov_tree_view div.ov_tree_item_name
{
padding: 4px 5px;

View File

@ -13,7 +13,7 @@ div.ov_color_circle
div.header
{
overflow: auto;
display: none;
display: block;
}
div.title
@ -69,7 +69,7 @@ div.intro
text-align: center;
border: 2px dashed var(--ov_border_color);
overflow: auto;
display: none;
display: block;
}
div.intro_content
@ -116,12 +116,16 @@ div.intro div.intro_file_formats a
color: var(--ov_outline_button_text_color);
text-decoration: none;
font-size: 17px;
width: 50px;
min-width: 56px;
min-height: 44px;
border-radius: 5px;
padding: 4px 8px;
margin: 6px 4px;
border: 1px solid var(--ov_outline_button_color);
display: inline-block;
display: inline-flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
cursor: pointer;
}
@ -212,19 +216,20 @@ div.ov_toolbar
user-select: none;
}
div.ov_toolbar div.ov_toolbar_button
.ov_toolbar .ov_toolbar_button
{
float: left;
cursor: pointer;
padding: 10px;
padding: 13px;
display: block;
}
div.ov_toolbar div.ov_toolbar_button.align_right
.ov_toolbar .ov_toolbar_button.align_right
{
float: right;
}
div.ov_toolbar div.ov_toolbar_button.selected
.ov_toolbar .ov_toolbar_button.selected
{
background: var(--ov_toolbar_selected_color);
}
@ -314,23 +319,27 @@ div.ov_bottom_floating_panel
background: var(--ov_background_color);
border-top: 1px solid var(--ov_border_color);
width: 100%;
padding: 30px;
padding: 20px 24px;
box-sizing: border-box;
position: absolute;
bottom: 0px;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
}
div.ov_bottom_floating_panel div.ov_floating_panel_text
{
padding: 3px;
margin-bottom: 10px;
float: left;
padding: 3px 0px;
flex: 1 1 260px;
}
div.ov_bottom_floating_panel div.ov_floating_panel_button
.ov_bottom_floating_panel .ov_floating_panel_button
{
width: 120px;
float: right;
flex: 0 0 auto;
margin-left: auto;
}
div.ov_measure_panel
@ -362,7 +371,7 @@ div.title_right div.header_button:hover
color: var(--ov_button_color);
}
div.ov_toolbar div.ov_toolbar_button:hover
.ov_toolbar .ov_toolbar_button:hover
{
background: var(--ov_hover_color);
}
@ -392,6 +401,62 @@ div.intro_content
width: auto;
}
div.main_left_container.only_full_width,
div.main_right_container.only_full_width,
div.main_file_name.only_full_width
{
display: block;
}
div.main
{
position: relative;
}
div.main_file_name
{
margin: 8px 56px;
font-size: 14px;
}
div.main_left_container,
div.main_right_container
{
position: absolute;
top: 0px;
bottom: 0px;
z-index: 2;
overflow: visible;
}
div.main_left_container
{
left: 0px;
}
div.main_right_container
{
right: 0px;
}
div.main_navigator,
div.main_sidebar
{
margin: 0px;
background: var(--ov_background_color);
box-shadow: var(--ov_shadow);
}
div.main_splitter
{
display: none;
}
div.main_viewer
{
float: none;
}
div.main_viewer canvas
{
border: 0px;
@ -410,7 +475,7 @@ div.ov_progress
div.ov_bottom_floating_panel
{
padding: 10px;
padding: 12px;
}
}

View File

@ -1,5 +1,5 @@
import { AddDiv, CreateDiv } from '../engine/viewer/domutils.js';
import { AddSvgIconElement, CreateInlineColorCircle, IsHoverEnabled } from './utils.js';
import { AddDiv, CreateDiv, AddButtonElement } from '../engine/viewer/domutils.js';
import { AddSvgIconElement, CreateInlineColorCircle, IsHoverEnabled, SetElementAccessibleName } from './utils.js';
let currentDialog = null;
@ -164,10 +164,11 @@ export class ButtonDialog extends Dialog
{
function AddButton (button, buttonsDiv)
{
let buttonDiv = AddDiv (buttonsDiv, 'ov_button ov_dialog_button', button.name);
let buttonDiv = AddButtonElement (buttonsDiv, 'ov_button ov_dialog_button', button.name);
if (button.subClass) {
buttonDiv.classList.add (button.subClass);
}
SetElementAccessibleName (buttonDiv, button.name);
buttonDiv.addEventListener ('click', () => {
button.onClick ();
});
@ -221,7 +222,8 @@ export class ListPopup extends PopupDialog
AddListItem (item, callbacks)
{
let listItemDiv = AddDiv (this.listDiv, 'ov_popup_list_item');
let listItemDiv = AddButtonElement (this.listDiv, 'ov_popup_list_item');
SetElementAccessibleName (listItemDiv, item.name);
if (item.icon) {
AddSvgIconElement (listItemDiv, item.icon, 'left_inline');
}

View File

@ -47,46 +47,42 @@ export function RegisterToolbarPlugin (plugin)
export function StartWebsite ()
{
window.addEventListener ('load', () => {
if (window.self !== window.top) {
let noEmbeddingDiv = AddDiv (document.body, 'noembed');
AddDiv (noEmbeddingDiv, null, Loc ('Embedding Online 3D Viewer in an iframe is not supported.'));
let link = AddDomElement (noEmbeddingDiv, 'a', null, Loc ('Open Online 3D Viewer'));
link.target = '_blank';
link.href = window.self.location;
return;
}
if (window.self !== window.top) {
let noEmbeddingDiv = AddDiv (document.body, 'noembed');
AddDiv (noEmbeddingDiv, null, Loc ('Embedding Online 3D Viewer in an iframe is not supported.'));
let link = AddDomElement (noEmbeddingDiv, 'a', null, Loc ('Open Online 3D Viewer'));
link.target = '_blank';
link.href = window.self.location;
return;
}
document.getElementById ('intro_dragdrop_text').innerHTML = Loc ('Drag and drop 3D models here.');
document.getElementById ('intro_formats_title').innerHTML = Loc ('Check an example file:');
document.getElementById ('intro_dragdrop_text').textContent = Loc ('Drag and drop 3D models here.');
document.getElementById ('intro_formats_title').textContent = Loc ('Check an example file:');
let website = new Website ({
headerDiv : document.getElementById ('header'),
headerButtonsDiv : document.getElementById ('header_buttons'),
toolbarDiv : document.getElementById ('toolbar'),
mainDiv : document.getElementById ('main'),
introDiv : document.getElementById ('intro'),
fileNameDiv : document.getElementById ('main_file_name'),
leftContainerDiv : document.getElementById ('main_left_container'),
navigatorDiv : document.getElementById ('main_navigator'),
navigatorSplitterDiv : document.getElementById ('main_navigator_splitter'),
rightContainerDiv : document.getElementById ('main_right_container'),
sidebarDiv : document.getElementById ('main_sidebar'),
sidebarSplitterDiv : document.getElementById ('main_sidebar_splitter'),
viewerDiv : document.getElementById ('main_viewer'),
fileInput : document.getElementById ('open_file')
});
website.Load ();
let website = new Website ({
headerDiv : document.getElementById ('header'),
headerButtonsDiv : document.getElementById ('header_buttons'),
toolbarDiv : document.getElementById ('toolbar'),
mainDiv : document.getElementById ('main'),
introDiv : document.getElementById ('intro'),
fileNameDiv : document.getElementById ('main_file_name'),
leftContainerDiv : document.getElementById ('main_left_container'),
navigatorDiv : document.getElementById ('main_navigator'),
navigatorSplitterDiv : document.getElementById ('main_navigator_splitter'),
rightContainerDiv : document.getElementById ('main_right_container'),
sidebarDiv : document.getElementById ('main_sidebar'),
sidebarSplitterDiv : document.getElementById ('main_sidebar_splitter'),
viewerDiv : document.getElementById ('main_viewer'),
fileInput : document.getElementById ('open_file')
});
website.Load ();
}
export function StartEmbed ()
{
window.addEventListener ('load', () => {
let embed = new Embed ({
viewerDiv : document.getElementById ('embed_viewer'),
websiteLinkDiv : document.getElementById ('website_link')
});
embed.Load ();
let embed = new Embed ({
viewerDiv : document.getElementById ('embed_viewer'),
websiteLinkDiv : document.getElementById ('website_link')
});
embed.Load ();
}

View File

@ -1,4 +1,5 @@
import { IsDefined } from '../engine/core/core.js';
import { Loc } from '../engine/core/localization.js';
import { TreeViewButton, TreeViewButtonItem, TreeViewGroupButtonItem, TreeViewSingleItem } from './treeview.js';
export const NavigatorItemRecurse =
@ -29,13 +30,13 @@ export class MeshItem extends TreeViewButtonItem
this.meshInstanceId = meshInstanceId;
this.visible = true;
this.fitToWindowButton = new TreeViewButton ('fit');
this.fitToWindowButton = new TreeViewButton ('fit', Loc ('Fit mesh to window'));
this.fitToWindowButton.OnClick (() => {
callbacks.onFitToWindow (this.meshInstanceId);
});
this.AppendButton (this.fitToWindowButton);
this.showHideButton = new TreeViewButton ('visible');
this.showHideButton = new TreeViewButton ('visible', Loc ('Show or hide mesh'));
this.showHideButton.OnClick (() => {
callbacks.onShowHide (this.meshInstanceId);
});
@ -85,13 +86,13 @@ export class NodeItem extends TreeViewGroupButtonItem
this.callbacks = callbacks;
this.visible = true;
this.fitToWindowButton = new TreeViewButton ('fit');
this.fitToWindowButton = new TreeViewButton ('fit', Loc ('Fit node to window'));
this.fitToWindowButton.OnClick (() => {
this.callbacks.onFitToWindow (nodeId);
});
this.AppendButton (this.fitToWindowButton);
this.showHideButton = new TreeViewButton ('visible');
this.showHideButton = new TreeViewButton ('visible', Loc ('Show or hide node'));
this.showHideButton.OnClick (() => {
this.callbacks.onShowHide (nodeId);
});

View File

@ -1,4 +1,4 @@
import { AddDiv } from '../engine/viewer/domutils.js';
import { AddDiv, AddButtonElement } from '../engine/viewer/domutils.js';
import { Panel } from './panelset.js';
import { TreeView } from './treeview.js';
import { AddSvgIconElement } from './utils.js';
@ -11,7 +11,7 @@ export class NavigatorPopupButton
this.callbacks = null;
this.popup = null;
this.button = AddDiv (this.parentDiv, 'ov_panel_button');
this.button = AddButtonElement (this.parentDiv, 'ov_panel_button');
this.buttonText = AddDiv (this.button, 'ov_panel_button_text');
AddSvgIconElement (this.button, 'arrow_right', 'ov_panel_button_icon');
this.button.addEventListener ('click', () => {

View File

@ -1,5 +1,5 @@
import { AddDiv, ShowDomElement, IsDomElementVisible, SetDomElementWidth, SetDomElementHeight } from '../engine/viewer/domutils.js';
import { AddSvgIconElement, SetSvgIconImageElement } from './utils.js';
import { AddSvgIconButtonElement, SetSvgIconImageElement } from './utils.js';
export class Panel
{
@ -74,9 +74,7 @@ export class PanelSet
AddPanel (panel)
{
this.panels.push (panel);
let button = AddSvgIconElement (this.menuDiv, panel.GetIcon (), 'ov_panel_set_menu_button');
button.setAttribute ('alt', panel.GetName ());
button.setAttribute ('title', panel.GetName ());
let button = AddSvgIconButtonElement (this.menuDiv, panel.GetIcon (), 'ov_panel_set_menu_button', panel.GetName ());
this.panelButtons.push (button);
button.addEventListener ('click', () => {
if (panel === this.GetVisiblePanel ()) {
@ -110,6 +108,7 @@ export class PanelSet
} else {
for (let panelButton of this.panelButtons) {
panelButton.classList.remove ('selected');
panelButton.setAttribute ('aria-pressed', 'false');
}
for (let panel of this.panels) {
panel.Show (false);
@ -133,9 +132,11 @@ export class PanelSet
for (let otherPanelButton of this.panelButtons) {
if (otherPanelButton !== panelButton) {
otherPanelButton.classList.remove ('selected');
otherPanelButton.setAttribute ('aria-pressed', 'false');
}
}
panelButton.classList.add ('selected');
panelButton.setAttribute ('aria-pressed', 'true');
for (let otherPanel of this.panels) {
if (otherPanel !== panel) {

View File

@ -1,10 +1,11 @@
import { FileSource } from '../engine/io/fileutils.js';
import { AddDiv, AddDomElement } from '../engine/viewer/domutils.js';
import { AddDiv, AddDomElement, AddButtonElement } from '../engine/viewer/domutils.js';
import { AddCheckbox } from '../website/utils.js';
import { CreateUrlBuilder } from '../engine/parameters/parameterlist.js';
import { ShowMessageDialog } from './dialogs.js';
import { ButtonDialog } from './dialog.js';
import { CopyToClipboard } from './utils.js';
import { SetElementAccessibleName } from './utils.js';
import { HandleEvent } from './eventhandler.js';
import { Loc } from '../engine/core/localization.js';
@ -26,12 +27,15 @@ export function ShowSharingDialog (fileList, settings, viewer)
let input = AddDomElement (container, 'input', null);
input.setAttribute ('type', 'text');
input.readOnly = true;
let button = AddDiv (container, 'ov_button outline ov_dialog_copyable_input_button', copyText);
let button = AddButtonElement (container, 'ov_button outline ov_dialog_copyable_input_button', copyText);
SetElementAccessibleName (button, copyText);
button.addEventListener ('click', () => {
CopyToClipboard (getText ());
button.innerHTML = copiedText;
SetElementAccessibleName (button, copiedText);
setTimeout (() => {
button.innerHTML = copyText;
SetElementAccessibleName (button, copyText);
}, 2000);
});
return input;

View File

@ -3,9 +3,9 @@ import { SubCoord3D } from '../engine/geometry/coord3d.js';
import { GetBoundingBox, IsTwoManifold } from '../engine/model/modelutils.js';
import { CalculateVolume, CalculateSurfaceArea } from '../engine/model/quantities.js';
import { Property, PropertyToString, PropertyType } from '../engine/model/property.js';
import { AddDiv, AddDomElement, ClearDomElement } from '../engine/viewer/domutils.js';
import { AddDiv, AddDomElement, ClearDomElement, AddButtonElement } from '../engine/viewer/domutils.js';
import { SidebarPanel } from './sidebarpanel.js';
import { CreateInlineColorCircle } from './utils.js';
import { CreateInlineColorCircle, SetElementAccessibleName } from './utils.js';
import { GetFileName, IsUrl } from '../engine/io/fileutils.js';
import { MaterialSource, MaterialType } from '../engine/model/material.js';
import { RGBColorToHexString } from '../engine/model/color.js';
@ -170,7 +170,8 @@ export class SidebarDetailsPanel extends SidebarPanel
let valueColumn = AddDiv (row, 'ov_property_table_cell ov_property_table_value');
nameColumn.setAttribute ('title', name);
let calculateButton = AddDiv (valueColumn, 'ov_property_table_button', Loc ('Calculate...'));
let calculateButton = AddButtonElement (valueColumn, 'ov_property_table_button', Loc ('Calculate...'));
SetElementAccessibleName (calculateButton, name);
calculateButton.addEventListener ('click', () => {
ClearDomElement (valueColumn);
valueColumn.innerHTML = Loc ('Please wait...');

View File

@ -1,6 +1,6 @@
import { RGBColor, RGBColorToHexString, RGBAColor, RGBAColorToHexString, ColorComponentFromFloat } from '../engine/model/color.js';
import { AddDiv, AddDomElement, ShowDomElement, SetDomElementOuterHeight } from '../engine/viewer/domutils.js';
import { AddRangeSlider, AddToggle, AddCheckbox } from '../website/utils.js';
import { AddDiv, AddDomElement, ShowDomElement, SetDomElementOuterHeight, AddButtonElement } from '../engine/viewer/domutils.js';
import { AddRangeSlider, AddToggle, AddCheckbox, SetElementAccessibleName, MakeElementButtonLike } from '../website/utils.js';
import { CalculatePopupPositionToElementTopLeft } from './dialogs.js';
import { PopupDialog } from './dialog.js';
import { Settings } from './settings.js';
@ -107,7 +107,7 @@ class EnvironmentMapPopup extends PopupDialog
if (isSelected) {
envMapImage.element.classList.add ('selected');
}
envMapImage.element.addEventListener ('click', () => {
let onSelect = () => {
for (let otherImage of envMapImages) {
otherImage.element.classList.remove ('selected');
}
@ -120,7 +120,9 @@ class EnvironmentMapPopup extends PopupDialog
settings.environmentMapName = envMapImage.name;
}
callbacks.onEnvironmentMapChanged ();
});
};
MakeElementButtonLike (envMapImage.element, envMapImage.name, onSelect);
envMapImage.element.addEventListener ('click', onSelect);
}
} else if (shadingType === ShadingType.Physical) {
let isPerspective = (callbacks.getProjectionMode () === ProjectionMode.Perspective);
@ -138,14 +140,16 @@ class EnvironmentMapPopup extends PopupDialog
if (envMapImage.name === settings.environmentMapName) {
envMapImage.element.classList.add ('selected');
}
envMapImage.element.addEventListener ('click', () => {
let onSelect = () => {
for (let otherImage of envMapImages) {
otherImage.element.classList.remove ('selected');
}
envMapImage.element.classList.add ('selected');
settings.environmentMapName = envMapImage.name;
callbacks.onEnvironmentMapChanged ();
});
};
MakeElementButtonLike (envMapImage.element, envMapImage.name, onSelect);
envMapImage.element.addEventListener ('click', onSelect);
}
}
@ -229,7 +233,8 @@ class SettingsModelDisplaySection extends SettingsSection
});
this.environmentMapPhongDiv = AddDiv (this.contentDiv, 'ov_sidebar_parameter');
this.environmentMapPhongInput = AddDiv (this.environmentMapPhongDiv, 'ov_sidebar_image_picker');
this.environmentMapPhongInput = AddButtonElement (this.environmentMapPhongDiv, 'ov_sidebar_image_picker');
SetElementAccessibleName (this.environmentMapPhongInput, Loc ('Background Image'));
AddDiv (this.environmentMapPhongDiv, null, Loc ('Background Image'));
this.environmentMapPhongInput.addEventListener ('click', () => {
this.environmentMapPopup = new EnvironmentMapPopup ();
@ -245,7 +250,8 @@ class SettingsModelDisplaySection extends SettingsSection
});
this.environmentMapPbrDiv = AddDiv (this.contentDiv, 'ov_sidebar_parameter');
this.environmentMapPbrInput = AddDiv (this.environmentMapPbrDiv, 'ov_sidebar_image_picker');
this.environmentMapPbrInput = AddButtonElement (this.environmentMapPbrDiv, 'ov_sidebar_image_picker');
SetElementAccessibleName (this.environmentMapPbrInput, Loc ('Environment'));
AddDiv (this.environmentMapPbrDiv, null, Loc ('Environment'));
this.environmentMapPbrInput.addEventListener ('click', () => {
this.environmentMapPopup = new EnvironmentMapPopup ();
@ -263,7 +269,7 @@ class SettingsModelDisplaySection extends SettingsSection
this.UpdateEnvironmentMap ();
let edgeParameterDiv = AddDiv (this.contentDiv, 'ov_sidebar_parameter');
this.edgeDisplayToggle = AddToggle (edgeParameterDiv, 'ov_sidebar_parameter_toggle');
this.edgeDisplayToggle = AddToggle (edgeParameterDiv, 'ov_sidebar_parameter_toggle', Loc ('Show Edges'));
AddDiv (edgeParameterDiv, 'ov_sidebar_parameter_text', Loc ('Show Edges'));
this.edgeSettingsDiv = AddDiv (this.contentDiv, 'ov_sidebar_settings_padded');
@ -458,7 +464,8 @@ export class SidebarSettingsPanel extends SidebarPanel
this.modelDisplaySection = new SettingsModelDisplaySection (this.sectionsDiv, this.settings);
this.importParametersSection = new SettingsImportParametersSection (this.sectionsDiv, this.settings);
this.resetToDefaultsButton = AddDiv (this.contentDiv, 'ov_button ov_panel_button outline', 'Reset to Default');
this.resetToDefaultsButton = AddButtonElement (this.contentDiv, 'ov_button ov_panel_button outline', 'Reset to Default');
SetElementAccessibleName (this.resetToDefaultsButton, Loc ('Reset to Default'));
this.resetToDefaultsButton.addEventListener ('click', () => {
this.ResetToDefaults ();
});

View File

@ -1,8 +1,8 @@
import { AddDiv } from '../engine/viewer/domutils.js';
import { AddDiv, AddButtonElement } from '../engine/viewer/domutils.js';
import { ThreeModelLoader } from '../engine/threejs/threemodelloader.js';
import { ShowMessageDialog } from './dialogs.js';
import { ButtonDialog, ProgressDialog } from './dialog.js';
import { AddSvgIconElement } from './utils.js';
import { AddSvgIconElement, SetElementAccessibleName } from './utils.js';
import { ImportErrorCode } from '../engine/import/importer.js';
import { Loc } from '../engine/core/localization.js';
@ -124,7 +124,8 @@ export class ThreeModelLoaderUI
for (let i = 0; i < fileNames.length; i++) {
let fileName = fileNames[i];
let fileLink = AddDiv (fileList, 'ov_dialog_file_link');
let fileLink = AddButtonElement (fileList, 'ov_dialog_file_link');
SetElementAccessibleName (fileLink, fileName);
AddSvgIconElement (fileLink, 'meshes', 'ov_file_link_img');
AddDiv (fileLink, 'ov_dialog_file_link_text', fileName);
fileLink.addEventListener ('click', () => {

View File

@ -1,5 +1,5 @@
import { AddDiv, CreateDiv } from '../engine/viewer/domutils.js';
import { AddSvgIconElement, InstallTooltip } from './utils.js';
import { AddDiv, CreateButtonElement } from '../engine/viewer/domutils.js';
import { AddSvgIconElement, InstallTooltip, SetElementAccessibleName } from './utils.js';
export class ToolbarButton
{
@ -9,13 +9,13 @@ export class ToolbarButton
this.imageTitle = imageTitle;
this.selected = false;
this.buttonDiv = CreateDiv ('ov_toolbar_button');
this.buttonDiv = CreateButtonElement ('ov_toolbar_button');
this.buttonImg = AddSvgIconElement (this.buttonDiv, this.image);
if (onClick !== null) {
this.buttonDiv.addEventListener ('click', onClick);
}
this.buttonDiv.setAttribute ('alt', this.imageTitle);
SetElementAccessibleName (this.buttonDiv, this.imageTitle);
InstallTooltip (this.buttonDiv, this.imageTitle);
}
@ -57,6 +57,7 @@ export class ToolbarButton
} else {
this.buttonDiv.classList.remove ('selected');
}
this.buttonDiv.setAttribute ('aria-pressed', this.selected ? 'true' : 'false');
}
}

View File

@ -1,6 +1,6 @@
import { RGBColor, RGBColorToHexString } from '../engine/model/color.js';
import { CreateObjectUrl } from '../engine/io/bufferutils.js';
import { AddDiv, CreateDiv, AddDomElement } from '../engine/viewer/domutils.js';
import { AddDiv, CreateDiv, AddDomElement, AddButtonElement, CreateButtonElement } from '../engine/viewer/domutils.js';
import { Loc } from '../engine/core/localization.js';
import { Theme } from './settings.js';
@ -128,6 +128,7 @@ export function DownloadArrayBufferAsFile (arrayBuffer, fileName)
export function CreateSvgIconElement (iconName, className)
{
let iconDiv = CreateDiv ('ov_svg_icon');
iconDiv.setAttribute ('aria-hidden', 'true');
if (className) {
iconDiv.classList.add (className);
}
@ -135,6 +136,29 @@ export function CreateSvgIconElement (iconName, className)
return iconDiv;
}
export function SetElementAccessibleName (element, text)
{
element.setAttribute ('aria-label', text);
element.setAttribute ('title', text);
}
export function CreateSvgIconButtonElement (iconName, className, accessibleName)
{
let button = CreateButtonElement (className);
if (accessibleName) {
SetElementAccessibleName (button, accessibleName);
}
AddSvgIconElement (button, iconName);
return button;
}
export function AddSvgIconButtonElement (parentElement, iconName, className, accessibleName)
{
let button = CreateSvgIconButtonElement (iconName, className, accessibleName);
parentElement.appendChild (button);
return button;
}
export function AddSvgIconElement (parentElement, iconName, className)
{
let iconDiv = CreateSvgIconElement (iconName, className);
@ -148,6 +172,21 @@ export function SetSvgIconImageElement (iconElement, iconName)
iconDiv.className = 'icon icon-' + iconName;
}
export function MakeElementButtonLike (element, accessibleName, onActivate)
{
element.setAttribute ('role', 'button');
element.setAttribute ('tabindex', '0');
if (accessibleName) {
SetElementAccessibleName (element, accessibleName);
}
element.addEventListener ('keydown', (ev) => {
if (ev.key === 'Enter' || ev.key === ' ') {
ev.preventDefault ();
onActivate (ev);
}
});
}
export function CreateInlineColorCircle (color)
{
let hexString = '#' + RGBColorToHexString (color);
@ -158,6 +197,7 @@ export function CreateInlineColorCircle (color)
);
let darkerColorHexString = '#' + RGBColorToHexString (darkerColor);
let circleDiv = CreateDiv ('ov_color_circle');
circleDiv.setAttribute ('aria-hidden', 'true');
circleDiv.style.background = hexString;
circleDiv.style.border = '1px solid ' + darkerColorHexString;
return circleDiv;
@ -320,7 +360,7 @@ export function AddSelect (parentElement, options, selectedIndex, onChange)
return select;
}
export function AddToggle (parentElement, className)
export function AddToggle (parentElement, className, accessibleName)
{
function UpdateStatus (toggle, status)
{
@ -329,6 +369,7 @@ export function AddToggle (parentElement, className)
} else {
toggle.classList.remove ('on');
}
toggle.setAttribute ('aria-pressed', status ? 'true' : 'false');
}
let status = false;
@ -338,7 +379,10 @@ export function AddToggle (parentElement, className)
if (className) {
toggleClassName += ' ' + className;
}
let toggle = AddDiv (parentElement, toggleClassName);
let toggle = AddButtonElement (parentElement, toggleClassName);
if (accessibleName) {
SetElementAccessibleName (toggle, accessibleName);
}
AddDiv (toggle, 'ov_toggle_slider');
toggle.addEventListener ('click', () => {

View File

@ -1,9 +1,10 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="Open, inspect, and share 3D models directly in your browser with Online 3D Viewer.">
<link rel="icon" type="image/png" href="assets/images/3dviewer_net_favicon.ico">
<link rel="canonical" href="https://3dviewer.net">
@ -14,25 +15,29 @@
<!-- website start -->
<link rel="stylesheet" type="text/css" href="../build/website_dev/o3dv.website.min.css">
<script type="text/javascript" src="../build/website_dev/o3dv.website.min.js"></script>
<script type="text/javascript" src="../build/website_dev/o3dv.website.min.js" defer></script>
<!-- website end -->
<!-- plugins start -->
<!-- plugins end -->
<!-- website analytics start -->
<script type="text/javascript">
OV.SetWebsiteEventHandler ((eventName, eventLabel, eventParams) => {
console.log ({
eventName : eventName,
eventLabel : eventLabel,
eventParams : eventParams
document.addEventListener ('DOMContentLoaded', () => {
OV.SetWebsiteEventHandler ((eventName, eventLabel, eventParams) => {
console.log ({
eventName : eventName,
eventLabel : eventLabel,
eventParams : eventParams
});
});
});
</script>
<!-- website analytics end -->
<script type="text/javascript">
OV.StartWebsite ();
document.addEventListener ('DOMContentLoaded', () => {
OV.StartWebsite ();
});
</script>
</head>
@ -42,7 +47,7 @@
<div class="header" id="header">
<div class="title">
<div class="title_left">
<a href="index.html">
<a href="index.html" aria-label="Online 3D Viewer home">
<svg class="logo_image"><use href="assets/images/3dviewer_net_logo_text.svg#logo"></use></svg>
</a>
</div>
@ -66,10 +71,10 @@
<div class="intro_content" id="intro_content">
<div class="intro_logo">
<svg class="intro_logo"><use href="assets/images/3dviewer_net_logo_text.svg#logo"></use></svg>
<div class="intro_dragdrop_text" id="intro_dragdrop_text"></div>
<div class="intro_dragdrop_text" id="intro_dragdrop_text">Drag and drop 3D models here.</div>
</div>
<div class="intro_formats">
<div class="intro_formats_title" id="intro_formats_title"></div>
<div class="intro_formats_title" id="intro_formats_title">Check an example file:</div>
<div class="intro_file_formats">
<a href="#model=assets/models/RhinoLogo.3dm">3dm</a>
<a href="#model=assets/models/cubes.3ds,assets/models/texture.png">3ds</a>