mirror of
https://github.com/pezkuwichain/pezkuwi-apps.git
synced 2026-07-02 22:37:27 +00:00
feat: initial Pezkuwi Apps rebrand from polkadot-apps
Rebranded terminology: - Polkadot → Pezkuwi - Kusama → Dicle - Westend → Zagros - Rococo → PezkuwiChain - Substrate → Bizinikiwi - parachain → teyrchain Custom logos with Kurdistan brand colors (#e6007a → #86e62a): - bizinikiwi-hexagon.svg - sora-bizinikiwi.svg - hezscanner.svg - heztreasury.svg - pezkuwiscan.svg - pezkuwistats.svg - pezkuwiassembly.svg - pezkuwiholic.svg
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
// Copyright 2017-2025 @pezkuwi/react-components authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
|
||||
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import React, { Suspense } from 'react';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
|
||||
import { lightTheme } from '@pezkuwi/react-components';
|
||||
import i18next from '@pezkuwi/react-components/i18n';
|
||||
|
||||
import Popup from './index.js';
|
||||
|
||||
function TestPopup () {
|
||||
return (
|
||||
<>
|
||||
<h1>Test outside text</h1>
|
||||
<Popup
|
||||
value={
|
||||
<div>
|
||||
Test popup content
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function renderPopup () {
|
||||
return render(
|
||||
<Suspense fallback='...'>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<TestPopup />
|
||||
</ThemeProvider>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
describe('Popup Component', () => {
|
||||
beforeAll(async () => {
|
||||
await i18next.changeLanguage('en');
|
||||
});
|
||||
|
||||
// eslint-disable-next-line jest/expect-expect
|
||||
it('opens and closes', async () => {
|
||||
renderPopup();
|
||||
|
||||
await waitFor(async () => {
|
||||
await expectPopupToBeClosed();
|
||||
await togglePopup();
|
||||
await expectPopupToBeOpen();
|
||||
await togglePopup();
|
||||
await expectPopupToBeClosed();
|
||||
});
|
||||
});
|
||||
|
||||
// eslint-disable-next-line jest/expect-expect
|
||||
it('closes popup with outside click', async () => {
|
||||
renderPopup();
|
||||
|
||||
await waitFor(async () => {
|
||||
await expectPopupToBeClosed();
|
||||
await togglePopup();
|
||||
await expectPopupToBeOpen();
|
||||
await clickOutside();
|
||||
await expectPopupToBeClosed();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function expectPopupToBeClosed () {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
await screen.findByRole('button');
|
||||
expect(screen.queryAllByText('Test popup content')).toHaveLength(0);
|
||||
}
|
||||
|
||||
async function expectPopupToBeOpen () {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
await screen.findByText('Test popup content');
|
||||
}
|
||||
|
||||
async function togglePopup () {
|
||||
fireEvent.click(await screen.findByTestId('popup-open'));
|
||||
}
|
||||
|
||||
async function clickOutside () {
|
||||
fireEvent.click(await screen.findByText('Test outside text'));
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright 2017-2025 @pezkuwi/react-components authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { PopupWindowProps as Props } from './types.js';
|
||||
|
||||
import React from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import { usePopupWindow } from '@pezkuwi/react-hooks/usePopupWindow';
|
||||
|
||||
import { styled } from '../styled.js';
|
||||
|
||||
function PopupWindow ({ children, className = '', position, triggerRef, windowRef }: Props): React.ReactElement<Props> {
|
||||
const { pointerStyle, renderCoords: { x, y } } = usePopupWindow(windowRef, triggerRef, position);
|
||||
|
||||
return createPortal(
|
||||
<StyledDiv
|
||||
className={`${className} ${pointerStyle}Pointer ${position}Position`}
|
||||
data-testid='popup-window'
|
||||
ref={windowRef}
|
||||
style={
|
||||
(x && y && {
|
||||
transform: `translate3d(${x}px, ${y}px, 0)`,
|
||||
zIndex: 1000
|
||||
}) || undefined
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</StyledDiv>,
|
||||
document.body
|
||||
);
|
||||
}
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
background-color: var(--bg-menu);
|
||||
border: 1px solid #d4d4d5;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px 0 rgb(34 36 38 / 12%), 0 2px 10px 0 rgb(34 36 38 / 15%);
|
||||
color: var(--color-text);
|
||||
left: 0;
|
||||
margin: 0.7rem 0;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: -1;
|
||||
|
||||
&.leftPosition {
|
||||
&::before {
|
||||
left: unset;
|
||||
right: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.rightPosition {
|
||||
&::before {
|
||||
left: 0.75rem;
|
||||
right: unset;
|
||||
}
|
||||
}
|
||||
|
||||
&::before {
|
||||
background-color: var(--bg-menu);
|
||||
bottom: -0.5rem;
|
||||
box-shadow: 1px 1px 0 0 #bababc;
|
||||
content: '';
|
||||
height: 1rem;
|
||||
position: absolute;
|
||||
right: 50%;
|
||||
top: unset;
|
||||
width: 1rem;
|
||||
transform: rotate(45deg);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&.bottomPointer::before {
|
||||
box-shadow: -1px -1px 0 0 #bababc;
|
||||
|
||||
top: -0.5rem;
|
||||
bottom: unset;
|
||||
}
|
||||
|
||||
.ui.text.menu .item {
|
||||
color: var(--color-text) !important;
|
||||
text-align: left;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
& > *:not(.ui--Menu) {
|
||||
margin-left: 1rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
& > *:first-child:not(.ui--Menu) {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
& > *:last-child:not(.ui--Menu) {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(PopupWindow);
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright 2017-2025 @pezkuwi/react-components authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { PopupProps } from './types.js';
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
|
||||
import { useOutsideClick, useTheme, useToggle } from '@pezkuwi/react-hooks';
|
||||
|
||||
import Button from '../Button/index.js';
|
||||
import { styled } from '../styled.js';
|
||||
import PopupWindow from './PopupWindow.js';
|
||||
|
||||
function Popup ({ children, className = '', closeOnScroll, isDisabled, onCloseAction, position = 'left', value }: PopupProps) {
|
||||
const { themeClassName } = useTheme();
|
||||
const [isOpen, toggleIsOpen, setIsOpen] = useToggle(false);
|
||||
const triggerRef = useRef<HTMLDivElement>(null);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const closeWindow = useCallback(
|
||||
() => setIsOpen(false),
|
||||
[setIsOpen]
|
||||
);
|
||||
|
||||
const refs = useMemo(
|
||||
() => [triggerRef, dropdownRef],
|
||||
[triggerRef, dropdownRef]
|
||||
);
|
||||
|
||||
useOutsideClick(refs, closeWindow);
|
||||
|
||||
useEffect(() => {
|
||||
if (closeOnScroll) {
|
||||
document.addEventListener('scroll', closeWindow, true);
|
||||
}
|
||||
|
||||
return () => document.removeEventListener('scroll', closeWindow, true);
|
||||
}, [closeOnScroll, closeWindow, setIsOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen && onCloseAction) {
|
||||
onCloseAction();
|
||||
}
|
||||
}, [isOpen, onCloseAction]);
|
||||
|
||||
return (
|
||||
<StyledDiv className={`${className} ui--Popup ${themeClassName}`}>
|
||||
{isOpen && (
|
||||
<PopupWindow
|
||||
position={position}
|
||||
triggerRef={triggerRef}
|
||||
windowRef={dropdownRef}
|
||||
>
|
||||
{value}
|
||||
</PopupWindow>
|
||||
)}
|
||||
<div
|
||||
data-testid='popup-open'
|
||||
onClick={toggleIsOpen}
|
||||
ref={triggerRef}
|
||||
>
|
||||
{children ?? (
|
||||
<Button
|
||||
className={isOpen ? 'isOpen' : ''}
|
||||
icon='ellipsis-v'
|
||||
isDisabled={isDisabled}
|
||||
isReadOnly={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</StyledDiv>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export default React.memo(Popup);
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright 2017-2025 @pezkuwi/react-components authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type React from 'react';
|
||||
|
||||
export interface ElementPosition {
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
}
|
||||
|
||||
export type HorizontalPosition = 'left' | 'middle' | 'right'
|
||||
|
||||
export type VerticalPosition = 'top' | 'bottom'
|
||||
|
||||
export interface PopupWindowProps {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
windowRef: React.RefObject<HTMLDivElement>;
|
||||
triggerRef: React.RefObject<HTMLDivElement>;
|
||||
position: HorizontalPosition;
|
||||
}
|
||||
|
||||
export interface PopupProps {
|
||||
isDisabled?: boolean;
|
||||
className?: string;
|
||||
closeOnScroll?: boolean;
|
||||
value?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
position?: HorizontalPosition;
|
||||
onCloseAction?: () => void;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright 2017-2025 @pezkuwi/react-components authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { WindowSize } from '@pezkuwi/react-hooks/ctx/types';
|
||||
import type { ElementPosition, HorizontalPosition, VerticalPosition } from './types.js';
|
||||
|
||||
interface Coords {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
// 0.8rem
|
||||
const POINTER_OFFSET = 14 * 0.8;
|
||||
// we don't want our popup window to cover apps menu
|
||||
const MENU_BAR_HEIGHT = 56;
|
||||
// adds 1rem margin between browsers window and popup
|
||||
const FIXED_VERTICAL_OFFSET_MARGIN = 14;
|
||||
|
||||
export function getPosition (triggerPosition: ElementPosition, positionX: HorizontalPosition, positionY: VerticalPosition, windowPosition: ElementPosition, scrollY: number, windowSize: WindowSize): Coords {
|
||||
const globalX = triggerPosition.x + (triggerPosition.width / 2);
|
||||
const globalY = triggerPosition.y + scrollY + (triggerPosition.height / 2);
|
||||
|
||||
return {
|
||||
x: globalX - getHorizontalOffset(windowPosition.width, positionX),
|
||||
y: fitsInView(positionY, triggerPosition, windowPosition.height, windowSize, scrollY)
|
||||
? globalY + getVerticalOffset(triggerPosition.height, positionY, windowPosition.height)
|
||||
: getFixedVerticalPosition(scrollY, positionY, windowSize, windowPosition.height)
|
||||
};
|
||||
}
|
||||
|
||||
function getHorizontalOffset (popupWindowWidth: number, position: HorizontalPosition): number {
|
||||
if (position === 'left') {
|
||||
return popupWindowWidth - POINTER_OFFSET;
|
||||
}
|
||||
|
||||
if (position === 'right') {
|
||||
return POINTER_OFFSET;
|
||||
}
|
||||
|
||||
return (popupWindowWidth / 2);
|
||||
}
|
||||
|
||||
function fitsInView (positionY: VerticalPosition, trigger: ElementPosition, popupWindowHeight: number, windowSize: WindowSize, scrollY: number): boolean {
|
||||
const { height: triggerHeight, y: triggerY } = trigger;
|
||||
|
||||
if (positionY === 'bottom') {
|
||||
return windowSize.height - triggerHeight - triggerY - FIXED_VERTICAL_OFFSET_MARGIN > popupWindowHeight;
|
||||
}
|
||||
|
||||
return scrollY < MENU_BAR_HEIGHT
|
||||
? triggerY - (MENU_BAR_HEIGHT - scrollY) > popupWindowHeight
|
||||
: triggerY > popupWindowHeight;
|
||||
}
|
||||
|
||||
function getVerticalOffset (triggerHeight: number, position: VerticalPosition, windowHeight: number): number {
|
||||
if (position === 'bottom') {
|
||||
return triggerHeight / 2;
|
||||
}
|
||||
|
||||
return (triggerHeight / 2 + windowHeight + POINTER_OFFSET) * -1;
|
||||
}
|
||||
|
||||
function getFixedVerticalPosition (scrollY: number, position: VerticalPosition, windowSize: WindowSize, popupWindowHeight: number): number {
|
||||
if (position === 'bottom') {
|
||||
return scrollY + windowSize.height - popupWindowHeight - FIXED_VERTICAL_OFFSET_MARGIN;
|
||||
}
|
||||
|
||||
return scrollY < MENU_BAR_HEIGHT
|
||||
? MENU_BAR_HEIGHT
|
||||
: scrollY;
|
||||
}
|
||||
Reference in New Issue
Block a user