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
12 changes: 11 additions & 1 deletion pxtblocks/fields/field_dropdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ export class FieldDropdown extends Blockly.FieldDropdown {
override initView() {
super.initView();

if (this.fieldGroup_) {
this.fieldGroup_.classList.add("pxtFieldDropdown");
}

if (this.shouldAddBorderRect_()) {
return;
}
Expand Down Expand Up @@ -121,4 +125,10 @@ export class FieldDropdown extends Blockly.FieldDropdown {
protected showEditor_(e?: MouseEvent): void {
showEditorMixin.call(this, e);
}
}
}

Blockly.Css.register(`
.pxtFieldDropdown.blocklyActiveFocus > .blocklyFieldRect, .pxtFieldDropdown.blocklyPassiveFocus > .blocklyFieldRect {
stroke-opacity: 1;
}
`)
104 changes: 77 additions & 27 deletions pxtblocks/fields/field_gridpicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ export class FieldGridPicker extends FieldDropdownGrid implements FieldCustom {
private firstItem_: HTMLElement;

private hasSearchBar_: boolean;
private hideRect_: boolean;

private observer: IntersectionObserver;

Expand All @@ -42,8 +41,14 @@ export class FieldGridPicker extends FieldDropdownGrid implements FieldCustom {
private selectedBarText_: HTMLElement;
private selectedBarValue_: string;

protected scrollContainer: HTMLDivElement;

private static DEFAULT_IMG = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';

private firstFocusableElement: HTMLElement | SVGElement;
private lastFocusableElement: HTMLElement | SVGElement;
private tabKeyBind: Blockly.browserEvents.Data | null = null;

constructor(text: string, options: FieldGridPickerOptions, validator?: Function) {
super(options.data);

Expand All @@ -61,13 +66,28 @@ export class FieldGridPicker extends FieldDropdownGrid implements FieldCustom {

this.tooltipConfig_ = tooltipCfg;
this.hasSearchBar_ = !!options.hasSearchBar || false;
this.hideRect_ = !!options.hideRect || false;
}

protected setFocusedItem_(_gridItemContainer: HTMLElement) {
this.gridItems.forEach(button => button.classList.remove('gridpicker-option-focused', 'gridpicker-menuitem-highlight'));
const activeItem = this.gridItems[this.activeDescendantIndex];
activeItem.classList.add('gridpicker-option-focused');

Blockly.utils.style.scrollIntoContainerView(activeItem, this.scrollContainer);
const rect = activeItem.getBoundingClientRect();

if (this.gridTooltip_) {
const title = activeItem.title || (activeItem as any).alt;
this.gridTooltip_.textContent = title;

this.gridTooltip_.style.visibility = title ? 'visible' : 'hidden';
this.gridTooltip_.style.display = title ? '' : 'none';

this.gridTooltip_.style.top = `${rect.bottom + 5}px`;
this.gridTooltip_.style.left = `${rect.left}px`;
}

this.addKeyboardNavigableClass();
}

/**
Expand Down Expand Up @@ -99,6 +119,8 @@ export class FieldGridPicker extends FieldDropdownGrid implements FieldCustom {
* @param tableContainer
*/
private populateTableContainer(options: (Object | String[])[], tableContainer: HTMLElement, scrollContainer: HTMLElement) {
this.gridItems = [];
this.activeDescendantIndex = 0;

pxsim.U.removeChildren(tableContainer);
if (options.length == 0) {
Expand Down Expand Up @@ -288,15 +310,6 @@ export class FieldGridPicker extends FieldDropdownGrid implements FieldCustom {
this.buttonClick_(value);
};

/**
* Whether or not to show a box around the dropdown menu.
* @return {boolean} True if we should show a box (rect) around the dropdown menu. Otherwise false.
* @private
*/
shouldShowRect_() {
return !this.hideRect_ ? !this.sourceBlock_.isShadow() : false;
}

doClassValidation_(newValue: string) {
return newValue;
}
Expand All @@ -314,13 +327,7 @@ export class FieldGridPicker extends FieldDropdownGrid implements FieldCustom {

Blockly.WidgetDiv.hideIfOwner(this);
Blockly.Events.setGroup(false);
}

/**
* Getter method
*/
private getFirstItem() {
return this.firstItem_;
if (this.tabKeyBind) Blockly.browserEvents.unbind(this.tabKeyBind);
}

/**
Expand Down Expand Up @@ -368,6 +375,9 @@ export class FieldGridPicker extends FieldDropdownGrid implements FieldCustom {
const tableContainer = document.createElement("div");
this.positionMenu_(tableContainer);
tableContainer.focus();
if (!e) {
this.addKeyboardNavigableClass();
}
}

private positionMenu_(tableContainer: HTMLElement) {
Expand All @@ -376,6 +386,7 @@ export class FieldGridPicker extends FieldDropdownGrid implements FieldCustom {
const anchorBBox = this.getAnchorDimensions_();

const { paddingContainer, scrollContainer } = this.createWidget_(tableContainer);
this.scrollContainer = scrollContainer;

const containerSize = {
width: paddingContainer.offsetWidth,
Expand Down Expand Up @@ -473,20 +484,30 @@ export class FieldGridPicker extends FieldDropdownGrid implements FieldCustom {
widgetDiv.appendChild(paddingContainer);

// Search bar
let searchBar: HTMLDivElement | undefined;
if (this.hasSearchBar_) {
const searchBar = this.createSearchBar_(tableContainer, scrollContainer, options);
paddingContainer.insertBefore(searchBar, paddingContainer.childNodes[0]);
const { searchBarDiv, searchBar: input } = this.createSearchBar_(tableContainer, scrollContainer, options);
paddingContainer.insertBefore(searchBarDiv, paddingContainer.childNodes[0]);
searchBar = input;
}

// Selected bar
let cancelButton: HTMLButtonElement | undefined;
if (!this.shouldShowTooltips()) {
this.selectedBar_ = this.createSelectedBar_();
const { selectedBar, cancelButton: buttton } = this.createSelectedBar_();
this.selectedBar_ = selectedBar;
cancelButton = buttton;
paddingContainer.appendChild(this.selectedBar_);
}

// Render elements
this.populateTableContainer(options, tableContainer, scrollContainer);

if (this.hasSearchBar_ || this.selectedBar_) {
this.firstFocusableElement = searchBar || tableContainer;
this.lastFocusableElement = cancelButton || tableContainer;
this.tabKeyBind = Blockly.browserEvents.bind(widgetDiv, "keydown", this, this.handleTabKey.bind(this));
}

return { paddingContainer, scrollContainer };
}
Expand All @@ -501,21 +522,26 @@ export class FieldGridPicker extends FieldDropdownGrid implements FieldCustom {
searchBar.setAttribute("id", "search-bar");
searchBar.setAttribute("class", "blocklyGridPickerSearchBar");
searchBar.setAttribute("placeholder", pxt.Util.lf("Search"));
searchBar.setAttribute("tabindex", "0");
searchBar.addEventListener("click", () => {
searchBar.focus();
searchBar.setSelectionRange(0, searchBar.value.length);
});

// Search on key change
searchBar.addEventListener("keyup", pxt.Util.debounce(() => {
searchBar.addEventListener("keyup", pxt.Util.debounce((e: KeyboardEvent) => {
if (e.code === "Tab") {
return;
}

let text = searchBar.value;
let re = new RegExp(text, "i");
let filteredOptions = options.filter((block) => {
const alt = (block as any)[0].alt; // Human-readable text or image.
const value = (block as any)[1]; // Language-neutral value.
return alt ? re.test(alt) : re.test(value);
})
this.populateTableContainer.bind(this)(filteredOptions, tableContainer, scrollContainer);
this.populateTableContainer(filteredOptions, tableContainer, scrollContainer);
if (text) {
this.highlightFirstItem(tableContainer)
} else {
Expand Down Expand Up @@ -545,7 +571,7 @@ export class FieldGridPicker extends FieldDropdownGrid implements FieldCustom {
searchBarDiv.appendChild(searchBar);
searchBarDiv.appendChild(searchIcon);

return searchBarDiv;
return { searchBarDiv, searchBar };
}

private createSelectedBar_() {
Expand Down Expand Up @@ -601,7 +627,7 @@ export class FieldGridPicker extends FieldDropdownGrid implements FieldCustom {

selectedBar.appendChild(selectedWrapper);
selectedBar.appendChild(buttonsWrapper);
return selectedBar;
return { selectedBar, cancelButton };
}

private updateSelectedBar_(content: any, value: string) {
Expand Down Expand Up @@ -666,6 +692,26 @@ export class FieldGridPicker extends FieldDropdownGrid implements FieldCustom {
this.disposeTooltip();
this.disposeGrid();
}

// Used for focus trap
private handleTabKey(e: KeyboardEvent) {
if (e.code === "Tab") {
this.addKeyboardNavigableClass();
if (document.activeElement === this.lastFocusableElement && !e.shiftKey) {
this.firstFocusableElement.focus();
e.preventDefault();
} else if (document.activeElement === this.firstFocusableElement && e.shiftKey) {
this.lastFocusableElement.focus();
e.preventDefault();
}
}
}

private addKeyboardNavigableClass() {
if (this.scrollContainer) {
this.scrollContainer.classList.add("keyboardNavigable");
}
}
}

Blockly.Css.register(`
Expand Down Expand Up @@ -696,6 +742,10 @@ Blockly.Css.register(`
-webkit-overflow-scrolling: touch;
}

.blocklyGridPickerScroller.keyboardNavigable:has(:focus-visible) {
outline: 4px solid var(--pxt-focus-border);
}

.blocklyGridPickerPadder {
border-radius: 4px;
outline: none;
Expand Down Expand Up @@ -751,8 +801,8 @@ Blockly.Css.register(`
display: none;
}

.blocklyWidgetDiv .blocklyGridPickerMenu .gridpicker-option.gridpicker-option-focused {
box-shadow: 0px 0px 0px 4px rgb(255, 255, 255);
.blocklyWidgetDiv .blocklyGridPickerMenu:focus .blocklyGridPickerRow .gridpicker-menuitem.gridpicker-option-focused {
outline: 3px solid var(--pxt-focus-border);
}

.blocklyGridPickerTooltip {
Expand Down
2 changes: 1 addition & 1 deletion webapp/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3756,7 +3756,7 @@ export class ProjectView
}

setSimulatorFullScreen(enabled: boolean) {
if (this.state.collapseEditorTools) {
if (this.state.collapseEditorTools && !pxt.appTarget.simulator?.headless) {
this.expandSimulator();
}
if (enabled) {
Expand Down