@ahrowe/ui 0.24.3 → 0.25.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/common/configProvider/configProvider.mjs +1 -1
- package/dist/esm/common/configProvider/configProvider.mjs.map +1 -1
- package/dist/esm/common/configProvider/usePresentationDefault.mjs +2 -0
- package/dist/esm/common/configProvider/usePresentationDefault.mjs.map +1 -0
- package/dist/esm/common/dropdown/dropdown.mjs +1 -1
- package/dist/esm/common/dropdown/dropdown.mjs.map +1 -1
- package/dist/esm/common/floatingMenu/floatingMenu.mjs +1 -1
- package/dist/esm/common/floatingMenu/floatingMenu.mjs.map +1 -1
- package/dist/esm/common/floatingMenu/floatingMenu.module.mjs +1 -1
- package/dist/esm/common/floatingMenu/floatingMenu.module.mjs.map +1 -1
- package/dist/esm/common/floatingMenu/floatingMenu.types.mjs +1 -1
- package/dist/esm/common/floatingMenu/floatingMenu.types.mjs.map +1 -1
- package/dist/esm/common/floatingMenu/useFloatingPosition.mjs +1 -1
- package/dist/esm/common/floatingMenu/useFloatingPosition.mjs.map +1 -1
- package/dist/esm/common/hooks/useCoarsePointer.mjs +1 -1
- package/dist/esm/common/hooks/useCoarsePointer.mjs.map +1 -1
- package/dist/esm/common/hooks/useMediaQuery.mjs +2 -0
- package/dist/esm/common/hooks/useMediaQuery.mjs.map +1 -0
- package/dist/esm/common/hooks/useOverlay.mjs +2 -0
- package/dist/esm/common/hooks/useOverlay.mjs.map +1 -0
- package/dist/esm/common/hooks/useSheet.mjs +2 -0
- package/dist/esm/common/hooks/useSheet.mjs.map +1 -0
- package/dist/esm/common/modal/modal.mjs +1 -1
- package/dist/esm/common/modal/modal.mjs.map +1 -1
- package/dist/esm/common/modal/modal.module.mjs +1 -1
- package/dist/esm/common/modal/modal.module.mjs.map +1 -1
- package/dist/esm/common/numberInput/numberInput.mjs +1 -1
- package/dist/esm/common/numberInput/numberInput.mjs.map +1 -1
- package/dist/esm/common/popover/popover.mjs +1 -1
- package/dist/esm/common/popover/popover.mjs.map +1 -1
- package/dist/esm/common/popover/popover.types.mjs.map +1 -1
- package/dist/esm/common/popover/usePopoverPosition.mjs +1 -1
- package/dist/esm/common/popover/usePopoverPosition.mjs.map +1 -1
- package/dist/esm/common/sheetBackdrop/sheetBackdrop.mjs +2 -0
- package/dist/esm/common/sheetBackdrop/sheetBackdrop.mjs.map +1 -0
- package/dist/esm/common/styles/sheet.module.mjs +2 -0
- package/dist/esm/common/styles/sheet.module.mjs.map +1 -0
- package/dist/esm/index.mjs +1 -1
- package/dist/index.cjs +6 -6
- package/dist/index.cjs.map +1 -1
- package/dist/style.css +1 -1
- package/dist/types/package/common/configProvider/configProvider.d.ts +9 -1
- package/dist/types/package/common/configProvider/configProvider.types.d.ts +13 -0
- package/dist/types/package/common/configProvider/index.d.ts +1 -0
- package/dist/types/package/common/configProvider/usePresentationDefault.d.ts +8 -0
- package/dist/types/package/common/dropdown/dropdown.types.d.ts +4 -0
- package/dist/types/package/common/floatingMenu/floatingMenu.d.ts +1 -1
- package/dist/types/package/common/floatingMenu/floatingMenu.types.d.ts +13 -1
- package/dist/types/package/common/hooks/useCoarsePointer.d.ts +1 -8
- package/dist/types/package/common/hooks/useMediaQuery.d.ts +9 -0
- package/dist/types/package/common/hooks/useOverlay.d.ts +26 -0
- package/dist/types/package/common/hooks/useSheet.d.ts +16 -0
- package/dist/types/package/common/modal/modal.d.ts +1 -1
- package/dist/types/package/common/modal/modal.types.d.ts +9 -3
- package/dist/types/package/common/popover/popover.d.ts +1 -1
- package/dist/types/package/common/popover/popover.types.d.ts +6 -1
- package/dist/types/package/common/sheetBackdrop/index.d.ts +2 -0
- package/dist/types/package/common/sheetBackdrop/sheetBackdrop.d.ts +16 -0
- package/dist/types/package/common/sheetBackdrop/sheetBackdrop.types.d.ts +7 -0
- package/docs/ConfigProvider.md +32 -1
- package/docs/Dropdown.md +7 -1
- package/docs/FloatingMenu.md +32 -3
- package/docs/Modal.md +27 -3
- package/docs/Popover.md +12 -3
- package/package.json +1 -1
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Presentation } from '../floatingMenu/floatingMenu.types';
|
|
2
|
+
/**
|
|
3
|
+
* The nearest `ConfigProvider`'s app-wide `presentation`, or `undefined` when none is set.
|
|
4
|
+
*
|
|
5
|
+
* Sits between a component-keyed global default and the component's own built-in default, so the
|
|
6
|
+
* full order is: explicit prop → `defaultProps.<Component>.presentation` → this → built-in.
|
|
7
|
+
*/
|
|
8
|
+
export declare function usePresentationDefault(): Presentation | undefined;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ReactNode, Ref } from 'react';
|
|
2
2
|
import { FormValidator } from '../../services/formValidation';
|
|
3
3
|
import { HtmlProps } from '../types/slots.types';
|
|
4
|
+
import { Presentation } from '../floatingMenu/floatingMenu.types';
|
|
4
5
|
export interface DropdownItem {
|
|
5
6
|
key: string | number;
|
|
6
7
|
label: ReactNode;
|
|
@@ -29,6 +30,9 @@ export interface DropdownProps extends Omit<HtmlProps, 'onFocus' | 'onBlur' | 'o
|
|
|
29
30
|
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
|
30
31
|
dropdownLabel?: string;
|
|
31
32
|
isDropdownListWider?: boolean;
|
|
33
|
+
/** Where the list opens: anchored to the input (`Default`), pinned to the bottom of the
|
|
34
|
+
* screen, or the latter only on a small touch screen. */
|
|
35
|
+
presentation?: Presentation;
|
|
32
36
|
inputClassName?: string;
|
|
33
37
|
dropDownIconClassName?: string;
|
|
34
38
|
dropDownClassName?: string;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { FloatingMenuProps } from './floatingMenu.types';
|
|
2
|
-
declare function FloatingMenu(
|
|
2
|
+
declare function FloatingMenu(props: FloatingMenuProps): import("react/jsx-runtime").JSX.Element;
|
|
3
3
|
export default FloatingMenu;
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import { CSSProperties, ReactNode } from 'react';
|
|
2
2
|
import { SlotClassNames, SlotStyles, HtmlProps } from '../types/slots.types';
|
|
3
|
+
/** How a surface is presented on screen. */
|
|
4
|
+
export declare enum Presentation {
|
|
5
|
+
/** Wherever the component normally puts it: anchored to its trigger, or centred for a Modal. */
|
|
6
|
+
Default = "default",
|
|
7
|
+
/** Pinned to the bottom of the screen, full width up to a maximum. */
|
|
8
|
+
Sheet = "sheet",
|
|
9
|
+
/** A sheet on touch devices with a small screen, anchored everywhere else. */
|
|
10
|
+
Auto = "auto"
|
|
11
|
+
}
|
|
3
12
|
export declare enum Align {
|
|
4
13
|
Left = "left",
|
|
5
14
|
Right = "right",
|
|
6
15
|
Center = "center"
|
|
7
16
|
}
|
|
8
|
-
export type FloatingMenuSlots = 'root' | 'trigger' | 'menu' | 'menuContainer';
|
|
17
|
+
export type FloatingMenuSlots = 'root' | 'trigger' | 'menu' | 'menuContainer' | 'backdrop';
|
|
9
18
|
export interface FloatingMenuProps extends Omit<HtmlProps, 'content'> {
|
|
10
19
|
align?: Align;
|
|
11
20
|
children?: ReactNode;
|
|
@@ -13,6 +22,9 @@ export interface FloatingMenuProps extends Omit<HtmlProps, 'content'> {
|
|
|
13
22
|
isOpen?: boolean;
|
|
14
23
|
onOpenChange?: (isOpen: boolean) => void;
|
|
15
24
|
dontCloseOnChildClick?: boolean;
|
|
25
|
+
/** Where the menu opens: anchored to the trigger (`Default`), pinned to the bottom of the
|
|
26
|
+
* screen, or the latter only on a small touch screen. */
|
|
27
|
+
presentation?: Presentation;
|
|
16
28
|
className?: string;
|
|
17
29
|
style?: CSSProperties;
|
|
18
30
|
classNames?: SlotClassNames<FloatingMenuSlots>;
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `true` when the primary pointing device is coarse (touch), kept current so a device that
|
|
3
|
-
* switches input modes re-renders.
|
|
4
|
-
*
|
|
5
|
-
* `useSyncExternalStore` rather than `useState` + `useEffect`: callers pick *markup* with this,
|
|
6
|
-
* so hydration has to start from the server's answer. Reading `matchMedia` in a `useState`
|
|
7
|
-
* initialiser makes the client's first render disagree with the server's HTML.
|
|
8
|
-
*/
|
|
1
|
+
/** `true` when the primary pointing device is coarse (touch). */
|
|
9
2
|
export declare function useCoarsePointer(): boolean;
|
|
10
3
|
export default useCoarsePointer;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `true` while `query` matches, kept current so a resize or an input-mode change re-renders.
|
|
3
|
+
*
|
|
4
|
+
* `useSyncExternalStore` rather than `useState` + `useEffect`: callers pick *markup* with this,
|
|
5
|
+
* so hydration has to start from the server's answer. Reading `matchMedia` in a `useState`
|
|
6
|
+
* initialiser makes the client's first render disagree with the server's HTML.
|
|
7
|
+
*/
|
|
8
|
+
export declare function useMediaQuery(query: string): boolean;
|
|
9
|
+
export default useMediaQuery;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
export interface UseOverlayArgs {
|
|
3
|
+
/** Whether the overlay is currently visible. Everything below is set up while this is true. */
|
|
4
|
+
isActive: boolean;
|
|
5
|
+
/** The overlay panel. Focus is trapped inside it and Escape is scoped to it. */
|
|
6
|
+
containerRef: RefObject<HTMLElement | null>;
|
|
7
|
+
/** Called when Escape is pressed while this is the topmost overlay. Omit to not handle Escape. */
|
|
8
|
+
onEscape?: () => void;
|
|
9
|
+
/** Keep Tab inside the panel and restore focus to the previously focused element on close. */
|
|
10
|
+
trapFocus?: boolean;
|
|
11
|
+
/** Prevent the page behind the overlay from scrolling. */
|
|
12
|
+
lockScroll?: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The three behaviours every blocking overlay needs: Escape to dismiss, a focus trap, and a
|
|
16
|
+
* scroll lock on the page behind it.
|
|
17
|
+
*
|
|
18
|
+
* They share one module-level stack because they share one question: overlays nest (a
|
|
19
|
+
* ConfirmModal opened from inside a Modal), every instance listens on `document`, and only the
|
|
20
|
+
* innermost one may react. Without the stack, Escape would close every open layer at once and
|
|
21
|
+
* the outer trap would yank focus out of the inner panel on the next Tab.
|
|
22
|
+
*
|
|
23
|
+
* The scroll lock is reference-counted for the same reason: the page stays locked until the last
|
|
24
|
+
* overlay closes, and the body's original inline styles are restored rather than cleared.
|
|
25
|
+
*/
|
|
26
|
+
export declare function useOverlay({ isActive, containerRef, onEscape, trapFocus, lockScroll, }: UseOverlayArgs): void;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { CSSProperties } from 'react';
|
|
2
|
+
import { Presentation } from '../floatingMenu/floatingMenu.types';
|
|
3
|
+
export interface Sheet {
|
|
4
|
+
/** Whether the popup should render as a bottom sheet instead of anchored to its trigger. */
|
|
5
|
+
isSheet: boolean;
|
|
6
|
+
/** Inline style for the sheet root, keeping it above an open on-screen keyboard. */
|
|
7
|
+
sheetStyle: CSSProperties | undefined;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Resolves whether a popup presents as a bottom sheet.
|
|
11
|
+
*
|
|
12
|
+
* `Auto` asks for a coarse pointer *and* a small screen: pointer alone turns a touchscreen laptop
|
|
13
|
+
* into a phone, and width alone turns a narrow desktop window into one.
|
|
14
|
+
*/
|
|
15
|
+
export declare function useSheet(presentation?: Presentation): Sheet;
|
|
16
|
+
export default useSheet;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { ModalProps } from './modal.types';
|
|
2
|
-
declare function Modal(
|
|
2
|
+
declare function Modal(props: ModalProps): import("react/jsx-runtime").JSX.Element | null;
|
|
3
3
|
export default Modal;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { CSSProperties } from 'react';
|
|
2
|
-
import { SlotClassNames, SlotStyles } from '../types/slots.types';
|
|
3
|
-
|
|
4
|
-
export
|
|
2
|
+
import { SlotClassNames, SlotStyles, HtmlProps } from '../types/slots.types';
|
|
3
|
+
import { Presentation } from '../floatingMenu/floatingMenu.types';
|
|
4
|
+
export type ModalSlots = 'container' | 'backdrop' | 'modal' | 'header' | 'title' | 'closeButton' | 'body';
|
|
5
|
+
export interface ModalProps extends HtmlProps<HTMLDivElement> {
|
|
5
6
|
children?: React.ReactNode;
|
|
6
7
|
title?: string;
|
|
7
8
|
isOpen?: boolean;
|
|
@@ -10,6 +11,11 @@ export interface ModalProps {
|
|
|
10
11
|
className?: string;
|
|
11
12
|
style?: CSSProperties;
|
|
12
13
|
hideHeader?: boolean;
|
|
14
|
+
/** Close on Escape while this is the topmost overlay. Default `true`. */
|
|
15
|
+
closeOnEscape?: boolean;
|
|
16
|
+
/** Present as a bottom sheet, always or only on a small touch screen. `Presentation.Default`
|
|
17
|
+
* (or omitting this) is the ordinary centred dialog. */
|
|
18
|
+
presentation?: Presentation;
|
|
13
19
|
classNames?: SlotClassNames<ModalSlots>;
|
|
14
20
|
styles?: SlotStyles<ModalSlots>;
|
|
15
21
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { PopoverProps } from './popover.types';
|
|
2
|
-
declare function Popover(
|
|
2
|
+
declare function Popover(props: PopoverProps): import("react/jsx-runtime").JSX.Element;
|
|
3
3
|
export default Popover;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CSSProperties, ReactNode } from 'react';
|
|
2
2
|
import { SlotClassNames, SlotStyles, HtmlProps } from '../types/slots.types';
|
|
3
|
+
import { Presentation } from '../floatingMenu/floatingMenu.types';
|
|
3
4
|
/** Preferred side of the trigger the panel opens on. Flips to the opposite side when there's no room. */
|
|
4
5
|
export declare enum PopoverPlacement {
|
|
5
6
|
Top = "top",
|
|
@@ -19,7 +20,7 @@ export declare enum PopoverTrigger {
|
|
|
19
20
|
Hover = "hover",
|
|
20
21
|
Focus = "focus"
|
|
21
22
|
}
|
|
22
|
-
export type PopoverSlots = 'root' | 'trigger' | 'floating' | 'panel' | 'arrow';
|
|
23
|
+
export type PopoverSlots = 'root' | 'trigger' | 'floating' | 'panel' | 'arrow' | 'backdrop';
|
|
23
24
|
export interface PopoverProps extends Omit<HtmlProps, 'content'> {
|
|
24
25
|
/** The trigger element. */
|
|
25
26
|
children?: ReactNode;
|
|
@@ -51,6 +52,10 @@ export interface PopoverProps extends Omit<HtmlProps, 'content'> {
|
|
|
51
52
|
closeDelay?: number;
|
|
52
53
|
/** Prevent the popover from opening. */
|
|
53
54
|
disabled?: boolean;
|
|
55
|
+
/** Where the panel opens: anchored to the trigger (`Default`), pinned to the bottom of the
|
|
56
|
+
* screen, or the latter only on a small touch screen. Only a `Click` trigger presents as a
|
|
57
|
+
* sheet. */
|
|
58
|
+
presentation?: Presentation;
|
|
54
59
|
className?: string;
|
|
55
60
|
style?: CSSProperties;
|
|
56
61
|
classNames?: SlotClassNames<PopoverSlots>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { SheetBackdropProps } from './sheetBackdrop.types';
|
|
2
|
+
/**
|
|
3
|
+
* The dimmed layer behind a bottom sheet, and the thing that closes it when tapped.
|
|
4
|
+
*
|
|
5
|
+
* It closes on its own `click` rather than letting the sheet's usual outside-click handling see
|
|
6
|
+
* the press, because that handling fires on mousedown/mouseup — and the sheet, backdrop included,
|
|
7
|
+
* unmounts the moment it closes. The browser then dispatches the `click` that completes the tap
|
|
8
|
+
* at whatever is under the cursor by then, which is the page behind the sheet: tapping to dismiss
|
|
9
|
+
* would also press a button underneath it.
|
|
10
|
+
*
|
|
11
|
+
* The press is stopped with native listeners rather than React's `stopPropagation`, which would
|
|
12
|
+
* not be enough: React listens at its own root container, so a native handler on `document` or
|
|
13
|
+
* `window` still runs afterwards.
|
|
14
|
+
*/
|
|
15
|
+
declare function SheetBackdrop({ onClose, className, style }: SheetBackdropProps): import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
export default SheetBackdrop;
|
package/docs/ConfigProvider.md
CHANGED
|
@@ -54,15 +54,46 @@ So a global default only fills in props a given instance left out — any instan
|
|
|
54
54
|
| Prop | Type | Description |
|
|
55
55
|
|------|------|-------------|
|
|
56
56
|
| `defaultProps` | `ComponentDefaults` | Default props keyed by component name, e.g. `{ Input: { alwaysFloatLabel: true } }` |
|
|
57
|
+
| `presentation` | `Presentation` | How every sheet-capable component presents by default — one switch for all of them (see below) |
|
|
57
58
|
| `children` | `ReactNode` | Your app content |
|
|
58
59
|
|
|
60
|
+
## App-wide `presentation`
|
|
61
|
+
|
|
62
|
+
`FloatingMenu`, `Dropdown`, `Popover` and `Modal` can each present as a bottom sheet instead of anchored to their trigger (centred, for `Modal`). They're unrelated components, so setting that through `defaultProps` would mean four entries, and missing one would leave sheets in half an app. `presentation` is the single switch:
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
// Bottom sheets on small touch screens, everywhere, in one line
|
|
66
|
+
<ConfigProvider presentation={Presentation.Auto}>
|
|
67
|
+
<App />
|
|
68
|
+
</ConfigProvider>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
It resolves *under* the component-keyed defaults, so the full order for a `presentation` prop is:
|
|
72
|
+
|
|
73
|
+
1. The prop passed to the instance
|
|
74
|
+
2. `defaultProps.<Component>.presentation`
|
|
75
|
+
3. This app-wide `presentation`
|
|
76
|
+
4. The component's built-in default, `Presentation.Default` — anchored to the trigger, or centred for `Modal`
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
// Sheets app-wide, except Dropdown, except this one Dropdown
|
|
80
|
+
<ConfigProvider
|
|
81
|
+
presentation={Presentation.Auto}
|
|
82
|
+
defaultProps={{ Dropdown: { presentation: Presentation.Default } }}
|
|
83
|
+
>
|
|
84
|
+
<Dropdown label="Status" items={items} presentation={Presentation.Sheet} />
|
|
85
|
+
</ConfigProvider>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
A nested provider inherits it unless it sets its own, the same way `defaultProps` compose. Everything built on those four components follows along: `SplitButton`, `ColorPicker`, `Breadcrumb`, `VirtualList`, and `DatePicker`/`TimeInput`/`KlipyPicker` in either their menu or their `isModal` mode. `InputDropdown` and `Tooltip` are deliberately never sheets and ignore it.
|
|
89
|
+
|
|
59
90
|
## Supported components
|
|
60
91
|
|
|
61
92
|
Each entry is a `Partial<...Props>`, so any of that component's props can be defaulted:
|
|
62
93
|
|
|
63
94
|
- **Form inputs:** `Input` · `Textarea` · `NumberInput` · `Dropdown` · `InputDropdown` · `Checkbox` · `Switch` · `RadioGroup` · `DatePicker` · `OptionPicker` · `OtpInput`
|
|
64
95
|
- **Display:** `Button` · `ActionButtons` · `Badge` · `Chip` · `Card` · `SectionHeader` · `Skeleton` · `Accordion` · `Divider` · `Timer`
|
|
65
|
-
- **Overlays:** `ConfirmModal`
|
|
96
|
+
- **Overlays:** `ConfirmModal` · `Modal` · `FloatingMenu` · `Popover`
|
|
66
97
|
|
|
67
98
|
```tsx
|
|
68
99
|
<ConfigProvider
|
package/docs/Dropdown.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**When to use:** Single-value selection from a list — country pickers, status selectors, category filters, any select-style input.
|
|
4
4
|
|
|
5
|
-
**Keywords:** combobox
|
|
5
|
+
**Keywords:** combobox, bottom sheet, action sheet, mobile, touch
|
|
6
6
|
|
|
7
7
|
**Import:** `import { Dropdown } from '@ahrowe/ui'`
|
|
8
8
|
**Types:** `import type { DropdownItem, DropdownProps } from '@ahrowe/ui'`
|
|
@@ -33,6 +33,9 @@ const statusValidator = new FormValidator('', [Validators.required()]);
|
|
|
33
33
|
// Row layout (label left, input right)
|
|
34
34
|
<Dropdown label="Status" items={items} value={status} onChange={onChange} isRow />
|
|
35
35
|
|
|
36
|
+
// Bottom sheet on small touch screens, anchored to the input everywhere else
|
|
37
|
+
<Dropdown label="Status" items={items} value={status} onChange={onChange} presentation={Presentation.Auto} />
|
|
38
|
+
|
|
36
39
|
// Infinite scroll support
|
|
37
40
|
<Dropdown
|
|
38
41
|
items={items}
|
|
@@ -56,6 +59,9 @@ const statusValidator = new FormValidator('', [Validators.required()]);
|
|
|
56
59
|
| `placeholder` | `string` | Placeholder when no selection |
|
|
57
60
|
| `onScroll` | `() => Promise<void>` | Load more on list scroll |
|
|
58
61
|
| `useMatLabelStyle` | `boolean` | Material-style floating label |
|
|
62
|
+
| `presentation` | `Presentation` | Where the list opens: anchored to the input (`Default`), always a bottom sheet, or a sheet only on a small touch screen. Imported from the same package: `import { Presentation } from '@ahrowe/ui'` |
|
|
59
63
|
| `alwaysFloatLabel` | `boolean` | Keep the material label floated in the border notch even when nothing is selected, so an empty dropdown reads as blank instead of showing the label as if it were the selected value. Only applies with `useMatLabelStyle` (default) |
|
|
60
64
|
|
|
61
65
|
The list is rendered through a portal and tracks the trigger across scroll, so it always escapes clipping ancestors (cards, scroll containers, virtualized lists). It flips to open upward automatically when there's no room below, and shifts horizontally to stay within the viewport when the trigger sits near the left or right edge of the screen. It hides (without closing) if the trigger itself scrolls behind a clipping ancestor, reappearing once it's back in view. It also re-measures when its own content changes size while open, so a list that shrinks (e.g. filtered down to fewer entries) stays anchored to the trigger instead of hanging above it.
|
|
66
|
+
|
|
67
|
+
**Bottom sheet:** `presentation={Presentation.Sheet}` drops the anchoring entirely and pins the list to the bottom of the screen instead, full width up to 560px, behind a backdrop, scrolling internally past 70% of the viewport height and lifting itself above an open on-screen keyboard. Tapping the backdrop closes the list without also pressing whatever sits behind it. It is the same list with the same keyboard handling, so selection, highlighting and infinite scroll are unchanged. `Presentation.Auto` does that only when the pointer is coarse *and* the screen is under 768px wide. The default is `Presentation.Default`. A whole app can opt in with `<ConfigProvider defaultProps={{ Dropdown: { presentation: Presentation.Auto } }}>`, or for every sheet-capable component at once with `<ConfigProvider presentation={Presentation.Auto}>` — see [ConfigProvider.md](ConfigProvider.md).
|
package/docs/FloatingMenu.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**When to use:** Contextual popover triggered by any element — action menus, dropdowns with custom content, "more options" menus, date pickers, colour swatches.
|
|
4
4
|
|
|
5
|
-
**Keywords:** context menu, dropdown menu
|
|
5
|
+
**Keywords:** context menu, dropdown menu, bottom sheet, action sheet, mobile, touch
|
|
6
6
|
|
|
7
7
|
**Import:** `import { FloatingMenu } from '@ahrowe/ui'`
|
|
8
8
|
|
|
@@ -39,8 +39,20 @@ import { FloatingMenu, Align } from '@ahrowe/ui';
|
|
|
39
39
|
</FloatingMenu>
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
+
```tsx
|
|
43
|
+
import { FloatingMenu, Presentation } from '@ahrowe/ui';
|
|
44
|
+
|
|
45
|
+
// On a small touch screen the menu becomes a bottom sheet instead of a popover anchored to
|
|
46
|
+
// the trigger; everywhere else it stays anchored.
|
|
47
|
+
<FloatingMenu presentation={Presentation.Auto} content={<MenuContent />}>
|
|
48
|
+
<button>More ▾</button>
|
|
49
|
+
</FloatingMenu>
|
|
50
|
+
```
|
|
51
|
+
|
|
42
52
|
**Align enum:** `Align.Left` | `Align.Right` | `Align.Center`
|
|
43
53
|
|
|
54
|
+
**Presentation enum:** `Presentation.Default` (anchored to the trigger) | `Presentation.Sheet` | `Presentation.Auto`
|
|
55
|
+
|
|
44
56
|
**Key props:**
|
|
45
57
|
|
|
46
58
|
| Prop | Type | Description |
|
|
@@ -51,11 +63,28 @@ import { FloatingMenu, Align } from '@ahrowe/ui';
|
|
|
51
63
|
| `isOpen` | `boolean` | Controlled open state |
|
|
52
64
|
| `onOpenChange` | `(isOpen: boolean) => void` | Open state change callback |
|
|
53
65
|
| `dontCloseOnChildClick` | `boolean` | Keep the menu open when its own content is clicked, and when the trigger itself is re-clicked while already open (default `false`) |
|
|
66
|
+
| `presentation` | `Presentation` | Where the menu opens: anchored to the trigger (`Default`), always a bottom sheet, or a sheet only on a small touch screen |
|
|
67
|
+
|
|
68
|
+
**Slots:** `root` `trigger` `menu` `menuContainer` `backdrop` (`backdrop` only exists while presenting as a sheet)
|
|
54
69
|
|
|
55
|
-
**
|
|
70
|
+
**Bottom sheet:** with `Presentation.Sheet` the menu leaves its trigger entirely: it pins to the bottom of the screen, spans the full width up to 560px, scrolls internally past 70% of the viewport height, and dims the page behind a backdrop. Scrolling on the page behind it is locked while it's open, Escape closes it even when focus never entered it, and it lifts itself above an open on-screen keyboard. Tapping the backdrop closes the sheet and nothing else: the tap is consumed by the backdrop rather than falling through to whatever sits behind it. `Presentation.Auto` applies that only when the pointer is coarse *and* the screen is under 768px wide: pointer alone would catch touchscreen laptops, width alone would catch a narrow desktop window. Set it once for a whole app via `ConfigProvider`:
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
<ConfigProvider defaultProps={{ FloatingMenu: { presentation: Presentation.Auto } }}>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
To set it for every sheet-capable component at once, use `ConfigProvider`'s own `presentation` prop instead of the four component-keyed entries — see [ConfigProvider.md](ConfigProvider.md).
|
|
77
|
+
|
|
78
|
+
The default stays `Presentation.Default`, so nothing changes for an existing app until it opts in.
|
|
79
|
+
|
|
80
|
+
**What that one setting covers.** Every component whose popup *is* a `FloatingMenu` inherits the default above, with no prop of its own: `SplitButton`'s action menu, `ColorPicker`'s picker, `Breadcrumb`'s collapsed-crumb menu, `VirtualList`'s column-visibility menu, and `DatePicker`/`TimeInput`/`KlipyPicker` when they're not already using their own `isModal`. To make one instance differ, wrap it in a nested `ConfigProvider`, which overrides only the keys it sets.
|
|
81
|
+
|
|
82
|
+
`Dropdown`, `Popover` and `Modal` place their surfaces themselves rather than through `FloatingMenu`, so each takes its own `presentation` prop and its own `ConfigProvider` key (`Modal`'s covers `DatePicker`/`TimeInput`/`KlipyPicker` in their `isModal` mode). `InputDropdown` and `Tooltip` also position themselves and deliberately have no sheet mode: a typeahead's list belongs next to the text being typed, and a tooltip is a pointer hint, not a screen-owning panel.
|
|
56
83
|
|
|
57
84
|
**Positioning:** the menu is portaled and tracks the trigger across scroll and resize, flipping above it when there's no room below. It re-measures whenever `content` changes size while open, so a menu holding a list that grows or shrinks (filtering, async loading) stays anchored to the trigger and re-evaluates whether it still needs to open upward. It's also shifted horizontally to stay within the viewport — a trigger near the left or right edge of the screen no longer lets the menu overflow off-screen, regardless of `align`.
|
|
58
85
|
|
|
59
86
|
**Closing behaviour:** by default, clicking anywhere in `content` closes the menu — including inside a nested overlay that renders through its own portal (e.g. a `Dropdown` or another `FloatingMenu` used inside `content`), even though that overlay's DOM lives outside `content`'s own subtree. Re-clicking the trigger while open is a clean toggle: it closes the menu (unless `dontCloseOnChildClick` is set, in which case it's a no-op — the trigger owns its own open/close entirely, so it never fights with an outside-click check). Set `dontCloseOnChildClick` when `content` needs several interactions before the user is done (a multi-checkbox toggle, a color picker's slider, a calendar) — the consumer is then responsible for closing explicitly, e.g. calling `onOpenChange(false)` from the handler that reacts to a final selection.
|
|
60
87
|
|
|
61
|
-
**Keyboard:** the menu is portaled to the end of the DOM, so Tab can't reach it in visual order on its own.
|
|
88
|
+
**Keyboard:** the menu is portaled to the end of the DOM, so Tab can't reach it in visual order on its own. Escape closes the menu and, when focus is inside it, returns focus to the trigger — it works wherever focus currently is, including on the trigger itself. When `content` has real focusable elements, Tab past the last focusable element (or Shift+Tab past the first) closes the menu and continues focus as if it sat right after the trigger. This doesn't include auto-focusing the first element on open — content ranges from menus to live controls (e.g. `ColorPicker`'s hue slider), where grabbing focus on open would let a stray arrow-key press change a value the user never touched. A consumer that wants that (like `SplitButton` focusing its first enabled action) implements it itself.
|
|
89
|
+
|
|
90
|
+
**Nesting inside a Modal:** an open menu registers itself as the layer above whatever it was opened from, so a menu inside a `Modal` behaves the way it looks: Escape closes the menu and leaves the modal open (a second press closes the modal), and the modal's focus trap leaves focus alone while the menu has it. Both matter because the menu is portaled, so its DOM is a sibling of the modal's panel rather than a descendant, and neither DOM position nor mount order says which one is on top. The same applies to `Dropdown` and `Popover`.
|
package/docs/Modal.md
CHANGED
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
**When to use:** Full overlay dialog — forms, detail views, confirmations, wizards that need to block the rest of the UI.
|
|
4
4
|
|
|
5
|
-
**Keywords:** popup
|
|
5
|
+
**Keywords:** popup, dialog, overlay, escape, focus trap, scroll lock, sticky header, bottom sheet, action sheet, mobile, touch
|
|
6
6
|
|
|
7
7
|
**Import:** `import { Modal } from '@ahrowe/ui'`
|
|
8
8
|
|
|
9
9
|
**Requires:** `<div id="bodyEnd"></div>` in your HTML (renders via portal).
|
|
10
10
|
|
|
11
|
+
The panel itself never scrolls: the header is fixed and the children scroll inside the `body` slot, so the title and close button stay visible no matter how long the content is.
|
|
12
|
+
|
|
13
|
+
While open, the modal traps focus (focus moves into the panel and returns to whatever was focused before on close), locks scrolling on the page behind it, and closes on Escape. Modals nest correctly: Escape closes only the topmost one, and the page stays locked until the last one closes. The same applies to anything opened from inside a modal — a `FloatingMenu`, `Dropdown` or `Popover` counts as the layer above it, so Escape closes that first, and the modal's focus trap leaves focus alone while it's open (all three render through the same portal, so their DOM sits beside the modal's panel rather than inside it).
|
|
14
|
+
|
|
11
15
|
```tsx
|
|
12
16
|
import { Modal, ActionButtons } from '@ahrowe/ui';
|
|
13
17
|
|
|
@@ -43,7 +47,27 @@ const [isOpen, setIsOpen] = useState(false);
|
|
|
43
47
|
|------|------|-------------|
|
|
44
48
|
| `isOpen` | `boolean` | Controls visibility |
|
|
45
49
|
| `title` | `string` | Header title |
|
|
46
|
-
| `onClose` | `() => void` | Called when backdrop clicked
|
|
50
|
+
| `onClose` | `() => void` | Called when the backdrop is clicked, the close button is pressed, or Escape is pressed |
|
|
47
51
|
| `hideHeader` | `boolean` | Removes the header bar |
|
|
52
|
+
| `closeOnEscape` | `boolean` | Close on Escape while this is the topmost modal. Default `true` |
|
|
53
|
+
| `presentation` | `Presentation` | Present as a bottom sheet, always (`Sheet`) or only on a small touch screen (`Auto`). `Default`, or omitting it, is the centred dialog |
|
|
54
|
+
|
|
55
|
+
**Bottom sheet:** `presentation={Presentation.Sheet}` pins the dialog to the bottom of the screen instead of centring it — full width up to 560px, rounded on its top corners only, sliding up rather than scaling in, and lifted above an open on-screen keyboard. Nothing else changes: the header stays fixed and the body scrolls, which is what a sheet needs anyway, and the backdrop, focus trap, scroll lock and Escape all behave exactly as they do centred. `Presentation.Auto` applies it only when the pointer is coarse *and* the screen is under 768px wide.
|
|
56
|
+
|
|
57
|
+
`Presentation.Default` means "wherever the component normally puts it", which for a Modal is centred rather than anchored to anything — so an app-wide `<ConfigProvider presentation={Presentation.Default}>` switches sheets off everywhere, this included.
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
import { Modal, Presentation } from '@ahrowe/ui';
|
|
61
|
+
|
|
62
|
+
<Modal isOpen={isOpen} title="Filters" onClose={close} presentation={Presentation.Auto}>
|
|
63
|
+
<FilterForm />
|
|
64
|
+
</Modal>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
A sheet-presented Modal takes 90% of the screen height rather than the 70% a menu-sized sheet uses, since it carries forms and detail views. Override either with the `--sheet-max-height` and `--sheet-safe-padding` custom properties, per instance via `style` or theme-wide via `ThemeProvider`.
|
|
68
|
+
|
|
69
|
+
**Global defaults:** adopts `ConfigProvider`, e.g. `defaultProps={{ Modal: { presentation: Presentation.Auto } }}` — which also covers `DatePicker`, `TimeInput` and `KlipyPicker` when they open in a Modal via their own `isModal`. `presentation={Presentation.Auto}` on the provider itself sets every sheet-capable component at once. See [ConfigProvider.md](ConfigProvider.md).
|
|
70
|
+
|
|
71
|
+
The panel also takes the full native `div` attribute set (`id`, `role`, `aria-*`, `data-*`, …). Pass `aria-label` when using `hideHeader`, since there is then no title to label the dialog with.
|
|
48
72
|
|
|
49
|
-
**Slots:** `container` `backdrop` `modal` `header` `title` `closeButton`
|
|
73
|
+
**Slots:** `container` `backdrop` `modal` `header` `title` `closeButton` `body`
|
package/docs/Popover.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**When to use:** A floating panel anchored to a trigger — profile cards, quick forms, action menus, help panels, filter dropdowns. Reach for `Tooltip` for plain text hints, `FloatingMenu` for a menu that mirrors the trigger width; use `Popover` when you need free content on any side with an arrow.
|
|
4
4
|
|
|
5
|
-
**Keywords:** hover card, info panel
|
|
5
|
+
**Keywords:** hover card, info panel, bottom sheet, action sheet, mobile, touch
|
|
6
6
|
|
|
7
7
|
**Import:** `import { Popover } from '@ahrowe/ui'`
|
|
8
8
|
|
|
@@ -36,6 +36,8 @@ import { Popover, PopoverPlacement, PopoverAlign, PopoverTrigger } from '@ahrowe
|
|
|
36
36
|
|
|
37
37
|
**PopoverTrigger enum:** `PopoverTrigger.Click` | `Hover` | `Focus`.
|
|
38
38
|
|
|
39
|
+
**Presentation enum:** `Presentation.Default` (anchored to the trigger) | `Presentation.Sheet` | `Presentation.Auto` — imported from the same package: `import { Presentation } from '@ahrowe/ui'`.
|
|
40
|
+
|
|
39
41
|
**Key props:**
|
|
40
42
|
|
|
41
43
|
| Prop | Type | Default | Description |
|
|
@@ -55,9 +57,16 @@ import { Popover, PopoverPlacement, PopoverAlign, PopoverTrigger } from '@ahrowe
|
|
|
55
57
|
| `openDelay` | `number` | `100` | Hover open delay in ms |
|
|
56
58
|
| `closeDelay` | `number` | `120` | Hover close delay in ms |
|
|
57
59
|
| `disabled` | `boolean` | `false` | Prevent opening |
|
|
60
|
+
| `presentation` | `Presentation` | `Default` | Where the panel opens: anchored to the trigger, always a bottom sheet, or a sheet only on a small touch screen |
|
|
61
|
+
|
|
62
|
+
**Slots:** `root` `trigger` `floating` `panel` `arrow` `backdrop` (`backdrop` only exists while presenting as a sheet)
|
|
63
|
+
|
|
64
|
+
**Bottom sheet:** `presentation={Presentation.Sheet}` drops the anchoring and the arrow, and pins the panel to the bottom of the screen instead — full width up to 560px, behind a backdrop, scrolling internally past 70% of the viewport height, lifted above an open on-screen keyboard. Scrolling on the page behind it is locked while it's open, and tapping the backdrop closes it without pressing whatever sits behind it (`closeOnOutsideClick={false}` leaves the backdrop inert but still blocking). `Presentation.Auto` applies that only when the pointer is coarse *and* the screen is under 768px wide.
|
|
65
|
+
|
|
66
|
+
Only a `Click` trigger presents as a sheet. `Hover` and `Focus` open something small next to whatever caused them, which is the opposite of a screen-owning sheet, so they stay anchored regardless of `presentation`.
|
|
58
67
|
|
|
59
|
-
**
|
|
68
|
+
**Global defaults:** adopts `ConfigProvider`, e.g. `defaultProps={{ Popover: { presentation: Presentation.Auto } }}`, or `presentation={Presentation.Auto}` on the provider itself to set every sheet-capable component at once. See [ConfigProvider.md](ConfigProvider.md).
|
|
60
69
|
|
|
61
70
|
Requires a `<div id="bodyEnd"></div>` at the app root — the panel is portaled there, same as `Tooltip` and `FloatingMenu`.
|
|
62
71
|
|
|
63
|
-
**Keyboard & focus (click trigger):** the panel is portaled to the end of the DOM, so Tab can't reach it naturally. When the panel has focusable content, opening it (click trigger only) moves focus to the first focusable element and the panel gets `role="dialog"`. Tabbing past the last element closes the popover and moves focus to the element after the trigger; Shift+Tab past the first element, and Escape, close it and return focus to the trigger. This is non-modal — focus is not trapped, and hover/focus triggers don't steal focus. Popovers with only static content don't manage focus or claim the dialog role.
|
|
72
|
+
**Keyboard & focus (click trigger):** the panel is portaled to the end of the DOM, so Tab can't reach it naturally. When the panel has focusable content, opening it (click trigger only) moves focus to the first focusable element and the panel gets `role="dialog"`. Tabbing past the last element closes the popover and moves focus to the element after the trigger; Shift+Tab past the first element, and Escape, close it and return focus to the trigger. This is non-modal — focus is not trapped, and hover/focus triggers don't steal focus. Escape closes only the innermost open layer, so a popover inside a `Modal` closes on the first press and leaves the modal open. Popovers with only static content don't manage focus or claim the dialog role.
|