Skip to content

Commit

Permalink
[WC-2704]: language selector not show properly (#1332)
Browse files Browse the repository at this point in the history
  • Loading branch information
samuelreichert authored Nov 27, 2024
2 parents a8da237 + 0146329 commit 5f45cea
Show file tree
Hide file tree
Showing 11 changed files with 287 additions and 427 deletions.
4 changes: 4 additions & 0 deletions packages/pluggableWidgets/language-selector-web/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## [Unreleased]

### Fixed

- We fixed an issue where the language selector dropdown would not show properly when inside an accordion.

## [1.1.2] - 2024-09-20

### Changed
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
...require("@mendix/pluggable-widgets-tools/test-config/jest.enzyme-free.config.js")
};
5 changes: 3 additions & 2 deletions packages/pluggableWidgets/language-selector-web/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@mendix/language-selector-web",
"widgetName": "LanguageSelector",
"version": "1.1.2",
"version": "1.1.3",
"description": "Display a list of available languages",
"copyright": "© Mendix Technology BV 2023. All rights reserved.",
"license": "Apache-2.0",
Expand Down Expand Up @@ -31,7 +31,7 @@
"build": "pluggable-widgets-tools build:web",
"format": "prettier --write .",
"lint": "eslint --ext .jsx,.js,.ts,.tsx src/",
"test": "pluggable-widgets-tools test:unit:web",
"test": "jest --projects jest.config.js",
"release": "pluggable-widgets-tools release:web",
"create-gh-release": "rui-create-gh-release",
"create-translation": "rui-create-translation",
Expand All @@ -50,6 +50,7 @@
"@mendix/widget-plugin-platform": "workspace:*"
},
"dependencies": {
"@floating-ui/react": "^0.26.27",
"classnames": "^2.3.2"
}
}
Original file line number Diff line number Diff line change
@@ -1,159 +1,120 @@
import { FloatingFocusManager } from "@floating-ui/react";
import classNames from "classnames";
import { createElement, ReactElement, useEffect, useRef, CSSProperties } from "react";
import {
isBehindElement,
isBehindRandomElement,
isElementPartiallyOffScreen,
isElementVisibleByUser,
moveAbsoluteElementOnScreen,
unBlockAbsoluteElementBottom,
unBlockAbsoluteElementLeft,
unBlockAbsoluteElementRight,
unBlockAbsoluteElementTop
} from "../utils/document";
import { createElement, CSSProperties, ReactElement, useState } from "react";
import { PositionEnum, TriggerEnum } from "../../typings/LanguageSelectorProps";
import { useFloatingUI } from "../hooks/useFloatingUI";
import { LanguageItem } from "../LanguageSelector";
import { useSelect } from "downshift";

export interface LanguageSwitcherProps {
className: string;
currentLanguage: LanguageItem | undefined;
languageList: LanguageItem[];
position: PositionEnum;
onSelect?: (lang: LanguageItem) => void;
trigger: TriggerEnum;
className: string;
position: PositionEnum;
screenReaderLabelCaption?: string;
style?: CSSProperties;
tabIndex: number;
screenReaderLabelCaption?: string;
trigger: TriggerEnum;
}
export const LanguageSwitcher = (props: LanguageSwitcherProps): ReactElement => {
const { languageList } = props;
const ref = useRef<HTMLDivElement>(null);

function itemToString(item: LanguageItem): string {
return item ? item.value : "";
}
const { isOpen, selectItem, highlightedIndex, getMenuProps, getItemProps, getToggleButtonProps } = useSelect({
items: languageList,
itemToString,
onSelectedItemChange(changes) {
if (!props.onSelect || !changes.selectedItem || changes.selectedItem === props.currentLanguage) {
return;
}
props.onSelect(changes.selectedItem);
}
});

useEffect(() => {
if (props.currentLanguage === undefined) {
return;
}
selectItem(props.currentLanguage);
}, [props.currentLanguage, selectItem]);
export const LanguageSwitcher = ({
className,
currentLanguage,
languageList,
onSelect,
position,
screenReaderLabelCaption,
style,
tabIndex,
trigger
}: LanguageSwitcherProps): ReactElement => {
const [isOpen, setOpen] = useState(false);

useEffect(() => {
const element = ref.current?.querySelector(".popupmenu-menu") as HTMLDivElement | null;
if (element) {
element.style.display = isOpen ? "flex" : "none";
if (isOpen) {
correctPosition(element, props.position);
}
}
}, [props.position, isOpen]);
const {
activeIndex,
context,
floatingStyles,
getFloatingProps,
getItemProps,
getReferenceProps,
handleSelect,
isTypingRef,
listRef,
refs
} = useFloatingUI({
currentLanguage,
isOpen,
languageList,
onSelect,
position,
setOpen,
triggerOn: trigger
});

return (
<div
ref={ref}
className={classNames(props.className, "widget-language-selector", "popupmenu")}
style={props.style}
>
<div className={classNames(className, "widget-language-selector", "popupmenu")} style={style}>
<div
ref={refs?.setReference}
className={"popupmenu-trigger popupmenu-trigger-alignment"}
{...getToggleButtonProps(
{
tabIndex: props.tabIndex,
"aria-label": props.screenReaderLabelCaption || "Language selector"
},
{ suppressRefError: true }
)}
aria-label={screenReaderLabelCaption || "Language selector"}
aria-autocomplete="none"
aria-activedescendant={isOpen && activeIndex !== null ? "" : undefined}
tabIndex={tabIndex}
{...getReferenceProps?.()}
>
<span className="current-language-text">{props.currentLanguage?.value || ""}</span>
<span className="current-language-text">{currentLanguage?.value || ""}</span>
<span className="language-arrow" aria-hidden="true">
<div className={`arrow-image ${isOpen ? "arrow-up" : "arrow-down"}`} />
</span>
</div>
<div
className={classNames("popupmenu-menu", `popupmenu-position-${props.position}`)}
{...getMenuProps(
{
"aria-labelledby": undefined
},
{ suppressRefError: true }
)}
>
{languageList.map((item, index) => (
{isOpen && (
<FloatingFocusManager context={context!} modal={false}>
<div
key={item._guid}
className={`popupmenu-basic-item ${props.currentLanguage === item ? "active" : ""} ${
highlightedIndex === index ? "highlighted" : ""
}`}
{...getItemProps({ item, index })}
aria-activedescendant={isOpen && activeIndex !== null ? "" : undefined}
className="popupmenu-menu"
ref={refs?.setFloating}
style={{
...floatingStyles,
outline: 0,
overflowY: "auto"
}}
{...getFloatingProps?.()}
>
{item.value}
{languageList.map((item, index) => (
<div
key={item._guid}
ref={node => {
listRef.current[index] = node;
}}
role="option"
tabIndex={index === activeIndex ? 0 : -1}
className={classNames("popupmenu-basic-item", {
active: currentLanguage === item,
highlighted: activeIndex === index
})}
{...getItemProps?.({
onKeyDown(event) {
if (event.key === "Enter") {
event.preventDefault();
handleSelect(index);
}

if (event.key === " " && !isTypingRef.current) {
event.preventDefault();
handleSelect(index);
}
},
onClick() {
handleSelect(index);
}
})}
>
{item.value}
</div>
))}
</div>
))}
</div>
</FloatingFocusManager>
)}
</div>
);
};

function correctPosition(element: HTMLElement, position: PositionEnum): void {
const dynamicDocument: Document = element.ownerDocument;
const dynamicWindow = dynamicDocument.defaultView as Window;
let boundingRect: DOMRect = element.getBoundingClientRect();
const isOffScreen = isElementPartiallyOffScreen(dynamicWindow, boundingRect);
if (isOffScreen) {
moveAbsoluteElementOnScreen(dynamicWindow, element, boundingRect);
}

boundingRect = element.getBoundingClientRect();
const blockingElement = isBehindRandomElement(dynamicDocument, element, boundingRect, 3, "popupmenu");
if (blockingElement && isElementVisibleByUser(dynamicDocument, dynamicWindow, blockingElement)) {
unBlockAbsoluteElement(element, boundingRect, blockingElement.getBoundingClientRect(), position);
} else if (blockingElement) {
let node = blockingElement;
do {
if (isBehindElement(element, node, 3) && isElementVisibleByUser(dynamicDocument, dynamicWindow, node)) {
return unBlockAbsoluteElement(element, boundingRect, node.getBoundingClientRect(), position);
} else if (node.parentElement) {
node = node.parentElement as HTMLElement;
} else {
break;
}
} while (node.parentElement);
}
}

function unBlockAbsoluteElement(
element: HTMLElement,
boundingRect: DOMRect,
blockingElementRect: DOMRect,
position: PositionEnum
): void {
switch (position) {
case "left":
unBlockAbsoluteElementLeft(element, boundingRect, blockingElementRect);
unBlockAbsoluteElementBottom(element, boundingRect, blockingElementRect);
break;
case "right":
unBlockAbsoluteElementRight(element, boundingRect, blockingElementRect);
unBlockAbsoluteElementBottom(element, boundingRect, blockingElementRect);
break;
case "top":
unBlockAbsoluteElementTop(element, boundingRect, blockingElementRect);
break;
case "bottom":
unBlockAbsoluteElementBottom(element, boundingRect, blockingElementRect);
break;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { render } from "@testing-library/react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { createElement } from "react";
import { PositionEnum, TriggerEnum } from "typings/LanguageSelectorProps";
import { LanguageSwitcher, LanguageSwitcherProps } from "../LanguageSwitcher";
import "@testing-library/jest-dom";

jest.useFakeTimers();

let props: LanguageSwitcherProps = {
currentLanguage: undefined,
Expand All @@ -15,14 +19,22 @@ let props: LanguageSwitcherProps = {
const language = { _guid: "111", value: "En us" };

describe("Language switcher", () => {
it("renders the structure with empty language list", () => {
it("renders the structure with empty language list", async () => {
const { asFragment } = render(<LanguageSwitcher {...props} />);
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
const triggerElement = screen.getByRole("combobox");

await user.click(triggerElement);
expect(asFragment()).toMatchSnapshot();
});

it("renders the structure with language list and selected default language", () => {
it("renders the structure with language list and selected default language", async () => {
props = { ...props, languageList: [language], currentLanguage: language };
const { asFragment } = render(<LanguageSwitcher {...props} />);
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
const triggerElement = screen.getByRole("combobox");

await user.click(triggerElement);
expect(asFragment()).toMatchSnapshot();
});
});
Loading

0 comments on commit 5f45cea

Please sign in to comment.