Commit b6bd3c92 authored by Alex Danilo's avatar Alex Danilo Committed by Commit Bot

[footer] Create files-ng select style control

Implements behavior of a <select> element using a <div> so that it can
be styled for files-ng.

Creates an absolutely positioned <div> to hold the <option> elements
that would normally get inserted as children of <select>.

Adds the styles needed for the files-ng case to apply to the <div> and
its descendants.

Points the initialization code that populates the <select> to use the
<div> if files-ng is enabled.

Adds event handling logic to mimic the behavior of <select> in regard to
activation, keyboard events, etc.

Fires a 'change' event on the <div> whenever an <option> is selected,
that is needed so that the browser can match CSS :checked selectors
that are targeting the emulated select (div) element.

Sets the width of the <div> acting as host to the width of the
absolutely positioned <div> hosting the <option> elements during
selection to stop the host <div> changing width when different
sized options are selected.

Bug: 1002410
Change-Id: I5d7b227b561d96aaf61e88d9e206ac7de80d468e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2201336
Commit-Queue: Alex Danilo <adanilo@chromium.org>
Reviewed-by: default avatarLuciano Pacheco <lucmult@chromium.org>
Reviewed-by: default avatarNoel Gordon <noel@chromium.org>
Cr-Commit-Position: refs/heads/master@{#772577}
parent 322085c1
......@@ -1398,11 +1398,7 @@ body.files-ng .dialog-footer .secondary:hover {
background: var(--cros-default-button-background-color-secondary-hover);
}
.dialog-footer select {
min-height: 21px;
}
.dialog-footer select {
body:not(.files-ng) .dialog-footer .select {
-webkit-appearance: none;
background: -webkit-image-set(
url(../images/common/disclosure_arrow_dk_grey_down.png) 1x,
......@@ -1414,10 +1410,73 @@ body.files-ng .dialog-footer .secondary:hover {
color: rgb(51, 51, 51);
cursor: pointer;
margin-inline-start: 16px;
min-height: 21px;
outline: none;
padding: 0 12px 0 0;
}
body.files-ng .dialog-footer .select {
background: -webkit-image-set(
url(../images/common/disclosure_arrow_dk_grey_down.png) 1x,
url(../images/common/2x/disclosure_arrow_dk_grey_down.png) 2x) no-repeat
right transparent;
background-color: var(--google-grey-100);
border: 0;
border-radius: 4px;
cursor: pointer;
margin-inline-start: 16px;
max-width: 152px;
min-height: 32px;
min-width: 104px;
outline: none;
padding: 0 12px 0 16px;
position: relative;
}
body.files-ng .dialog-footer div.select:not(:active):focus {
box-shadow: 0 0 0 2px rgba(var(--cros-menu-button-outline-color-focused-rgb),
50%);
}
body.files-ng .dialog-footer div.select > span {
display: inline-block;
line-height: 32px;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
white-space: nowrap;
}
body.files-ng .dialog-footer div.select > div.options {
background-color: white;
border-radius: 4px;
bottom: calc(100% + 2px);
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 50%);
left: 0;
min-width: max-content;
padding: 8px 0;
position: absolute;
right: 0;
z-index: 550;
}
body.files-ng .dialog-footer div.select > div.options:not([expanded]) {
display: none;
}
body.files-ng .dialog-footer div.select > div.options option {
line-height: 32px;
min-height: 32px;
padding: 0 12px 0 16px;
vertical-align: middle;
}
body.files-ng .dialog-footer div.select > div.options option:hover,
body.files-ng .dialog-footer div.select > div.options:not(:hover) option.selected {
background-color: rgba(0, 0, 0, 6%);
}
.dialog-footer select:hover {
border-image: none;
}
......
......@@ -54,10 +54,33 @@ class DialogFooter {
/**
* File type selector in the footer.
* @public @const {!HTMLSelectElement}
* TODO(adanilo) Replace annotation HTMLSelectElement when we can style
* them.
*/
this.fileTypeSelector = /** @type {!HTMLSelectElement} */
(container.querySelector('.file-type'));
if (util.isFilesNg()) {
this.fileTypeSelector = container.querySelector('div.file-type');
// TODO(adanilo) Work out why this is needed to satisfy Closure.
const selectorReference = /** @type {!Object} */ (this.fileTypeSelector);
Object.defineProperty(selectorReference, 'value', {
get() {
return this.getSelectValue();
},
enumerable: true,
configurable: true
});
this.fileTypeSelector.getSelectValue = this.getSelectValue_.bind(this);
this.fileTypeSelector.addEventListener(
'activate', this.onActivate_.bind(this));
this.fileTypeSelector.addEventListener(
'click', this.onActivate_.bind(this));
this.fileTypeSelector.addEventListener('blur', this.onBlur_.bind(this));
this.fileTypeSelector.addEventListener(
'keydown', this.onKeyDown_.bind(this));
this.fileTypeSelectorText = this.fileTypeSelector.querySelector('span');
} else {
this.fileTypeSelector = container.querySelector('select.file-type');
}
/** @public @const {!CrInputElement} */
this.filenameInput = /** @type {!CrInputElement} */ (filenameInput);
......@@ -85,6 +108,134 @@ class DialogFooter {
return ~~this.fileTypeSelector.value;
}
/**
* Get the 'value' property from the file type selector.
* @return {!string} containing the value attribute of the selected type.
*/
getSelectValue_() {
const selected = this.element.querySelector('.selected');
if (selected) {
return selected.getAttribute('value');
} else {
return '0';
}
}
/**
* Open (expand) the fake select drop down.
*/
selectShowDropDown(options) {
options.setAttribute('expanded', 'expanded');
// TODO(files-ng): Unify to use only aria-expanded.
this.fileTypeSelector.setAttribute('aria-expanded', 'true');
const selectedOption = options.querySelector('.selected');
if (selectedOption) {
this.fileTypeSelector.setAttribute(
'aria-activedescendant', selectedOption.id);
}
}
/**
* Hide (collapse) the fake select drop down.
*/
selectHideDropDown(options) {
// TODO: Unify to use only aria-expanded.
options.removeAttribute('expanded');
this.fileTypeSelector.setAttribute('aria-expanded', 'false');
this.fileTypeSelector.removeAttribute('aria-activedescendant');
}
/**
* Event handler for an activation or click.
* @param {Event} evt
* @private
*/
onActivate_(evt) {
const options = this.element.querySelector('.options');
if (evt.target instanceof HTMLOptionElement) {
this.setOptionSelected(evt.target);
this.selectHideDropDown(options);
const changeEvent = new Event('change');
this.fileTypeSelector.dispatchEvent(changeEvent);
} else {
const ancestor = evt.target.closest('div');
if (ancestor && ancestor.classList.contains('select')) {
if (options.getAttribute('expanded') === 'expanded') {
this.selectHideDropDown(options);
} else {
this.selectShowDropDown(options);
}
}
}
}
/**
* Event handler for a blur.
* @param {Event} evt
* @private
*/
onBlur_(evt) {
const options = this.fileTypeSelector.querySelector('.options');
if (options.getAttribute('expanded') === 'expanded') {
this.selectHideDropDown(options);
}
}
/**
* Event handler for a key down.
* @param {Event} evt
* @private
*/
onKeyDown_(evt) {
const options = this.fileTypeSelector.querySelector('.options');
switch (evt.key) {
case 'Escape':
// If options are open, stop the window from closing.
if (options.getAttribute('expanded') === 'expanded') {
evt.stopPropagation();
evt.preventDefault();
}
// Drop through.
case 'Tab':
this.selectHideDropDown(options);
break;
case 'Enter':
case ' ':
if (options.getAttribute('expanded') === 'expanded') {
const changeEvent = new Event('change');
this.fileTypeSelector.dispatchEvent(changeEvent);
this.selectHideDropDown(options);
} else {
this.selectShowDropDown(options);
}
break;
case 'ArrowDown':
case 'ArrowUp':
if (options.getAttribute('expanded') === 'expanded') {
const selectedItem = options.querySelector('.selected');
if (selectedItem) {
if (evt.key === 'ArrowDown') {
if (selectedItem.nextSibling) {
this.setOptionSelected(
/** @type {HTMLOptionElement} */ (
selectedItem.nextSibling));
}
} else { // ArrowUp.
if (selectedItem.previousSibling) {
this.setOptionSelected(
/** @type {HTMLOptionElement} */ (
selectedItem.previousSibling));
}
}
}
}
break;
}
}
/**
* Finds the dialog footer element for the dialog type.
* @param {DialogType} dialogType Dialog type.
......@@ -122,6 +273,34 @@ class DialogFooter {
}
}
/**
* Helper to set the option as the selected one.
* @param {HTMLOptionElement} option Element being set as selected.
*/
setOptionSelected(option) {
option.selected = true;
// Update our fake 'select' HTMLDivElement.
if (util.isFilesNg()) {
const existingSelected =
this.fileTypeSelector.querySelector('.options .selected');
if (existingSelected) {
existingSelected.removeAttribute('class');
}
option.setAttribute('class', 'selected');
this.fileTypeSelectorText.innerText = option.innerText;
this.fileTypeSelectorText.parentElement.setAttribute(
'aria-activedescendant', option.id);
// Force the width of the file-type selector div to be the width
// of the options area to stop it jittering on selection change.
if (option.parentNode) {
let optionsWidth = option.parentNode.getBoundingClientRect().width;
optionsWidth -= 16 + 12; // Padding of 16 + 12 px.
this.fileTypeSelector.setAttribute(
'style', 'width: ' + optionsWidth + 'px');
}
}
}
/**
* Fills the file type list or hides it.
* @param {!Array<{extensions: Array<string>, description: string}>} fileTypes
......@@ -130,9 +309,14 @@ class DialogFooter {
* files' item or not.
*/
initFileTypeFilter(fileTypes, includeAllFiles) {
let optionHost = this.fileTypeSelector;
if (util.isFilesNg()) {
optionHost = optionHost.querySelector('.options');
}
for (let i = 0; i < fileTypes.length; i++) {
const fileType = fileTypes[i];
const option = document.createElement('option');
const option =
/** @type {HTMLOptionElement } */ (document.createElement('option'));
let description = fileType.description;
if (!description) {
// See if all the extensions in the group have the same description.
......@@ -160,28 +344,41 @@ class DialogFooter {
}
option.innerText = description;
option.value = i + 1;
option.id = 'file-type-option-' + (i + 1);
if (fileType.selected) {
option.selected = true;
this.setOptionSelected(option);
}
this.fileTypeSelector.appendChild(option);
optionHost.appendChild(option);
}
if (includeAllFiles) {
const option = document.createElement('option');
const option =
/** @type {HTMLOptionElement } */ (document.createElement('option'));
option.innerText = str('ALL_FILES_FILTER');
option.value = 0;
if (this.dialogType_ === DialogType.SELECT_SAVEAS_FILE) {
option.selected = true;
this.setOptionSelected(option);
}
this.fileTypeSelector.appendChild(option);
optionHost.appendChild(option);
}
const options = this.fileTypeSelector.querySelectorAll('option');
if (options.length >= 2) {
// There is in fact no choice, show the selector.
this.fileTypeSelector.hidden = false;
if (util.isFilesNg()) {
// Make sure one of the options is selected to match real <select>.
let selectedOption =
this.fileTypeSelector.querySelector('.options .selected');
if (!selectedOption) {
selectedOption =
this.fileTypeSelector.querySelector('.options option');
this.setOptionSelected(
/** @type {HTMLOptionElement } */ (selectedOption));
}
}
}
}
......
......@@ -556,7 +556,13 @@
<paper-ripple fit></paper-ripple>
<span>$i18n{NEW_FOLDER_BUTTON_LABEL}</span>
</button>
<select class="file-type" hidden tabindex="0"></select>
<select class="file-type select" hidden tabindex="0"></select>
<div class="file-type select body2-primary" role="combobox"
aria-haspopup="listbox"
hidden tabindex="0">
<span></span>
<div class="options" id="file-type-options" role="listbox"></div>
</div>
<div id="filename-input-box" visibleif="saveas-file">
<cr-input id="filename-input-textbox" tabindex="0" class="entry-name"
type="text" spellcheck="false"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment