feat: DropdownButton + printRoot() экспорт из PrintButton (0.12.0)
Кнопка с выпадающим меню (клик по кнопке — открыть список, выбор варианта сразу вызывает onSelect). Логика печати PrintButton вынесена в printRoot(), чтобы её можно было вызвать программно после выбора варианта в DropdownButton.
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@tnsu/ui-kit",
|
||||
"version": "0.11.0",
|
||||
"version": "0.12.0",
|
||||
"description": "TNSU shared UI components for SPFx web parts",
|
||||
"author": "Timur Gorodetskiy",
|
||||
"license": "ISC",
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
@import "../tokens/tokens";
|
||||
|
||||
.root {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.chevron,
|
||||
.chevronOpen {
|
||||
transition: transform 0.18s;
|
||||
}
|
||||
|
||||
.chevronOpen {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 6px);
|
||||
z-index: 20;
|
||||
min-width: 220px;
|
||||
max-width: min(360px, calc(100vw - 24px));
|
||||
background: $white;
|
||||
border: 1px solid $gray-200;
|
||||
border-radius: $r-md;
|
||||
box-shadow: $sh-3;
|
||||
padding: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.right {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.left {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 9px 12px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
border-radius: $r-sm;
|
||||
font-family: $font-sans;
|
||||
font-size: 13px;
|
||||
color: $navy-800;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: background 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: $navy-50;
|
||||
}
|
||||
}
|
||||
|
||||
.itemLabel {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.check {
|
||||
flex: none;
|
||||
color: $gold-600;
|
||||
}
|
||||
|
||||
@include below($bp-xs) {
|
||||
.menu {
|
||||
min-width: 180px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.item {
|
||||
padding: 8px 10px;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { useState } from 'react';
|
||||
import { DropdownButton } from './DropdownButton';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
|
||||
const OPTIONS = [
|
||||
{ value: '0', label: 'ТОО Монтажник-Т — Директор' },
|
||||
{ value: '1', label: 'ТОО Стройсервис — Главный инженер' },
|
||||
];
|
||||
|
||||
const meta: Meta<typeof DropdownButton> = {
|
||||
title: 'Кнопки/DropdownButton',
|
||||
component: DropdownButton,
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export const Default: StoryObj<typeof DropdownButton> = {
|
||||
render: () => {
|
||||
const [v, setV] = useState('0');
|
||||
return (
|
||||
<DropdownButton options={OPTIONS} value={v} onSelect={setV}>
|
||||
<Icon name="document" size="sm" />
|
||||
Печать
|
||||
</DropdownButton>
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,107 @@
|
||||
import * as React from 'react';
|
||||
import styles from './DropdownButton.module.scss';
|
||||
import { Button } from '../Button/Button';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
|
||||
export interface IDropdownButtonOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
type DropdownButtonVariant = 'primary' | 'accent' | 'secondary' | 'ghost' | 'danger';
|
||||
type DropdownButtonSize = 'md' | 'sm';
|
||||
|
||||
export interface IDropdownButtonProps {
|
||||
options: IDropdownButtonOption[];
|
||||
onSelect: (value: string) => void;
|
||||
/** Текущее значение — помечается галочкой в списке. */
|
||||
value?: string;
|
||||
variant?: DropdownButtonVariant;
|
||||
size?: DropdownButtonSize;
|
||||
disabled?: boolean;
|
||||
/** К какому краю кнопки прижимать меню (по умолчанию — правый). */
|
||||
align?: 'left' | 'right';
|
||||
/** Содержимое кнопки-триггера (текст/иконка) — стрелка добавляется сама. */
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Кнопка с выпадающим списком вариантов: клик по кнопке открывает меню,
|
||||
* выбор варианта сразу вызывает onSelect и закрывает его. В отличие от
|
||||
* классической split-button здесь нет отдельной зоны действия по умолчанию —
|
||||
* вся кнопка целиком является триггером меню, так как без выбора варианта
|
||||
* действие не определено (см. PrintSplitButton в request-portal).
|
||||
*/
|
||||
export const DropdownButton: React.FC<IDropdownButtonProps> = ({
|
||||
options,
|
||||
onSelect,
|
||||
value,
|
||||
variant = 'secondary',
|
||||
size = 'md',
|
||||
disabled,
|
||||
align = 'right',
|
||||
children
|
||||
}) => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const rootRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!open) {
|
||||
return undefined;
|
||||
}
|
||||
const onDocMouseDown = (e: MouseEvent): void => {
|
||||
if (rootRef.current && !rootRef.current.contains(e.target as Node)) {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
const onKey = (e: KeyboardEvent): void => {
|
||||
if (e.key === 'Escape') {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', onDocMouseDown);
|
||||
document.addEventListener('keydown', onKey);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', onDocMouseDown);
|
||||
document.removeEventListener('keydown', onKey);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
const pick = (v: string): void => {
|
||||
setOpen(false);
|
||||
onSelect(v);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.root} ref={rootRef}>
|
||||
<Button
|
||||
type="button"
|
||||
variant={variant}
|
||||
size={size}
|
||||
disabled={disabled}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={open}
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
>
|
||||
{children}
|
||||
<Icon name="chevron-down" size="sm" className={open ? styles.chevronOpen : styles.chevron} />
|
||||
</Button>
|
||||
{open && (
|
||||
<div className={`${styles.menu} ${align === 'left' ? styles.left : styles.right}`} role="menu">
|
||||
{options.map((opt) => (
|
||||
<button
|
||||
key={opt.value}
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className={styles.item}
|
||||
onClick={() => pick(opt.value)}
|
||||
>
|
||||
<span className={styles.itemLabel}>{opt.label}</span>
|
||||
{value === opt.value && <Icon name="check" size="sm" className={styles.check} />}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -59,13 +59,15 @@ const PRINT_CSS =
|
||||
'.rp-print-value{white-space:pre-wrap;color:#000;padding:1px 0 2px}';
|
||||
|
||||
/**
|
||||
* Кнопка «Печать»: переносит документ (`.rp-print-root`) в отдельное чистое окно,
|
||||
* Переносит документ (по умолчанию `.rp-print-root`) в отдельное чистое окно,
|
||||
* заменяя поля ввода на текст значений, и печатает его. Не зависит от скролл-
|
||||
* контейнеров/высоты SharePoint, корректно разбивается на страницы. Общая для форм.
|
||||
* контейнеров/высоты SharePoint, корректно разбивается на страницы. Вынесена
|
||||
* из PrintButton отдельной функцией, чтобы печать можно было вызвать
|
||||
* программно — например, сразу после выбора варианта в DropdownButton, а не
|
||||
* только по клику на саму кнопку (см. PrintSplitButton в request-portal).
|
||||
*/
|
||||
export const PrintButton: React.FC<{ label?: string }> = ({ label = 'Печать' }) => {
|
||||
const onPrint = (): void => {
|
||||
const source = document.querySelector('.rp-print-root');
|
||||
export function printRoot(selector = '.rp-print-root'): void {
|
||||
const source = document.querySelector(selector);
|
||||
if (!source) {
|
||||
window.print();
|
||||
return;
|
||||
@@ -106,10 +108,14 @@ export const PrintButton: React.FC<{ label?: string }> = ({ label = 'Печат
|
||||
window.setTimeout(done, 350);
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Кнопка «Печать» — тонкая обёртка над printRoot(). Общая для форм.
|
||||
*/
|
||||
export const PrintButton: React.FC<{ label?: string }> = ({ label = 'Печать' }) => {
|
||||
return (
|
||||
<Button variant="secondary" onClick={onPrint}>
|
||||
<Button variant="secondary" onClick={() => printRoot()}>
|
||||
<Icon name="document" size="sm" />
|
||||
{label}
|
||||
</Button>
|
||||
|
||||
+3
-1
@@ -5,9 +5,11 @@
|
||||
|
||||
// --- Кнопки ---
|
||||
export { Button } from './Button/Button';
|
||||
export { PrintButton } from './PrintButton/PrintButton';
|
||||
export { PrintButton, printRoot } from './PrintButton/PrintButton';
|
||||
export { IconButton } from './IconButton/IconButton';
|
||||
export type { IIconButtonProps } from './IconButton/IconButton';
|
||||
export { DropdownButton } from './DropdownButton/DropdownButton';
|
||||
export type { IDropdownButtonProps, IDropdownButtonOption } from './DropdownButton/DropdownButton';
|
||||
|
||||
// --- Сообщения / плашки ---
|
||||
export { Alert } from './Alert/Alert';
|
||||
|
||||
Reference in New Issue
Block a user