@neo-design/mui 1.0.0-rc.6 → 1.0.0
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/README.md +3 -3
- package/package.json +2 -2
- package/theme/components.mjs +113 -0
- package/ui/BottomSheet.mjs +14 -0
- package/ui/Drawer.mjs +14 -0
- package/ui/_modal-base.mjs +7 -27
- package/ui/_scroll-state.mjs +35 -0
- package/ui/_sheet-base.mjs +139 -0
- package/ui/index.d.ts +43 -1
- package/ui/index.mjs +2 -0
package/README.md
CHANGED
|
@@ -14,10 +14,10 @@ entry point.
|
|
|
14
14
|
|
|
15
15
|
## Install
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
Install it together with the font:
|
|
18
18
|
|
|
19
19
|
```sh
|
|
20
|
-
npm i @neo-design/foundations
|
|
20
|
+
npm i @neo-design/foundations @neo-design/mui @fontsource/noto-sans
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
npm brings React, Material UI and Emotion as peers, in the versions this package supports —the `peerDependencies`
|
|
@@ -25,7 +25,7 @@ of [`package.json`](package.json)—. **With pnpm**, which does not let your app
|
|
|
25
25
|
itself, name them:
|
|
26
26
|
|
|
27
27
|
```sh
|
|
28
|
-
pnpm add @neo-design/foundations
|
|
28
|
+
pnpm add @neo-design/foundations @neo-design/mui @fontsource/noto-sans \
|
|
29
29
|
@mui/material@6 @emotion/react @emotion/styled react react-dom
|
|
30
30
|
```
|
|
31
31
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neo-design/mui",
|
|
3
|
-
"version": "1.0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Neo — the Material UI theme and the Neo* React components, on top of @neo-design/foundations.",
|
|
5
5
|
"homepage": "https://github.com/jzamorano-ui/neo-ds/blob/main/docs/dev/INTEGRATION.md",
|
|
6
6
|
"repository": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"theme"
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@neo-design/foundations": "1.0.0
|
|
32
|
+
"@neo-design/foundations": "1.0.0"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"@emotion/react": "^11",
|
package/theme/components.mjs
CHANGED
|
@@ -398,6 +398,119 @@ export const components = {
|
|
|
398
398
|
},
|
|
399
399
|
},
|
|
400
400
|
|
|
401
|
+
MuiDrawer: {
|
|
402
|
+
styleOverrides: {
|
|
403
|
+
paper: ({ theme }) => ({
|
|
404
|
+
'&.MuiDrawer-paperAnchorBottom': {
|
|
405
|
+
backgroundColor: 'var(--surface--base--default)',
|
|
406
|
+
borderStartStartRadius: 'var(--neo-radius-xl)', borderStartEndRadius: 'var(--neo-radius-xl)',
|
|
407
|
+
borderEndStartRadius: 0, borderEndEndRadius: 0,
|
|
408
|
+
maxHeight: '90dvh',
|
|
409
|
+
display: 'flex', flexDirection: 'column', alignItems: 'flex-start',
|
|
410
|
+
'& > *': { alignSelf: 'stretch' },
|
|
411
|
+
'&:focus': { outline: 0 },
|
|
412
|
+
},
|
|
413
|
+
'& .NeoBottomSheet-handle': {
|
|
414
|
+
inlineSize: '40px', blockSize: '4px', borderRadius: 'var(--neo-radius-pill)',
|
|
415
|
+
backgroundColor: 'var(--fill--base--medium)',
|
|
416
|
+
marginInline: 'auto', marginBlock: 'var(--neo-space-sm-rem)', flex: '0 0 auto',
|
|
417
|
+
},
|
|
418
|
+
'&[data-image="true"] .NeoBottomSheet-handle': { backgroundColor: 'var(--fill--base--default)' },
|
|
419
|
+
|
|
420
|
+
'& .NeoBottomSheet-header': {
|
|
421
|
+
display: 'flex', flexDirection: 'column', flex: '0 0 auto',
|
|
422
|
+
justifyContent: 'center', alignItems: 'flex-end',
|
|
423
|
+
'& > *': { alignSelf: 'stretch' },
|
|
424
|
+
paddingInline: 'var(--neo-space-lg-rem)', paddingBlock: `0 var(--neo-space-xs-rem)`,
|
|
425
|
+
},
|
|
426
|
+
'&[data-scroll="top"] .NeoBottomSheet-header, &[data-scroll="mid"] .NeoBottomSheet-header, &[data-scroll="bottom"] .NeoBottomSheet-header': {
|
|
427
|
+
borderBlockEnd: '1px solid var(--border--base--secondary)',
|
|
428
|
+
},
|
|
429
|
+
'& .NeoBottomSheet-headerRow': {
|
|
430
|
+
display: 'flex', alignItems: 'center', justifyContent: 'flex-end',
|
|
431
|
+
blockSize: '40px', gap: 'var(--neo-space-none-rem)',
|
|
432
|
+
},
|
|
433
|
+
'& .NeoBottomSheet-title': {
|
|
434
|
+
...theme.typography.bodyLgBold, color: 'var(--text--base--default)',
|
|
435
|
+
flex: '1 1 auto', minInlineSize: 0, margin: 0,
|
|
436
|
+
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
|
437
|
+
},
|
|
438
|
+
|
|
439
|
+
'& .NeoBottomSheet-headerImage': {
|
|
440
|
+
position: 'relative', blockSize: '160px', flex: '0 0 auto', overflow: 'hidden',
|
|
441
|
+
'& > img, & > picture, & > video': { inlineSize: '100%', blockSize: '100%', objectFit: 'cover', display: 'block' },
|
|
442
|
+
'& .NeoBottomSheet-handle': { position: 'absolute', insetInline: 0, insetBlockStart: 0 },
|
|
443
|
+
'& .NeoBottomSheet-close': {
|
|
444
|
+
position: 'absolute', insetBlockStart: 'var(--neo-space-lg-rem)', insetInlineEnd: 'var(--neo-space-lg-rem)',
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
|
|
448
|
+
'& .NeoBottomSheet-content': {
|
|
449
|
+
...theme.typography.bodyLgRegular, color: 'var(--text--base--default)',
|
|
450
|
+
flex: '1 1 auto', overflowY: 'auto', minBlockSize: '200px',
|
|
451
|
+
paddingInline: 'var(--neo-space-lg-rem)', paddingBlock: `0 var(--neo-space-lg-rem)`,
|
|
452
|
+
'&:focus-visible': { outline: 'none', boxShadow: focusRingInset.boxShadow },
|
|
453
|
+
},
|
|
454
|
+
|
|
455
|
+
'& .NeoBottomSheet-actions': {
|
|
456
|
+
display: 'flex', flexDirection: 'column', gap: 'var(--neo-space-sm-rem)', flex: '0 0 auto',
|
|
457
|
+
paddingInline: 'var(--neo-space-lg-rem)',
|
|
458
|
+
paddingBlock: `var(--neo-space-lg-rem) var(--neo-space-2xl-rem)`,
|
|
459
|
+
backgroundColor: 'var(--surface--base--default)',
|
|
460
|
+
'& > div': { display: 'contents' },
|
|
461
|
+
'& .MuiButton-root': { inlineSize: '100%' },
|
|
462
|
+
'&[data-scroll="top"], &[data-scroll="mid"]': { borderBlockStart: '1px solid var(--border--base--secondary)' },
|
|
463
|
+
},
|
|
464
|
+
|
|
465
|
+
'&.MuiDrawer-paperAnchorRight': {
|
|
466
|
+
backgroundColor: 'var(--surface--base--default)',
|
|
467
|
+
inlineSize: '540px', maxInlineSize: '100vw', blockSize: '100dvh', borderRadius: 0,
|
|
468
|
+
display: 'flex', flexDirection: 'column', alignItems: 'flex-start',
|
|
469
|
+
'& > *': { alignSelf: 'stretch' },
|
|
470
|
+
'&:focus': { outline: 0 },
|
|
471
|
+
},
|
|
472
|
+
'& .NeoDrawer-header': {
|
|
473
|
+
display: 'flex', flexDirection: 'column', flex: '0 0 auto',
|
|
474
|
+
justifyContent: 'center', alignItems: 'flex-end',
|
|
475
|
+
'& > *': { alignSelf: 'stretch' },
|
|
476
|
+
paddingInline: 'var(--neo-space-xl-rem)', paddingBlock: 'var(--neo-space-lg-rem)', gap: 'var(--neo-space-md-rem)',
|
|
477
|
+
},
|
|
478
|
+
'&[data-scroll="top"] .NeoDrawer-header, &[data-scroll="mid"] .NeoDrawer-header, &[data-scroll="bottom"] .NeoDrawer-header': {
|
|
479
|
+
borderBlockEnd: '1px solid var(--border--base--secondary)',
|
|
480
|
+
},
|
|
481
|
+
'& .NeoDrawer-headerRow': {
|
|
482
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
483
|
+
blockSize: '48px', gap: 'var(--neo-space-none-rem)',
|
|
484
|
+
},
|
|
485
|
+
'& .NeoDrawer-title': {
|
|
486
|
+
...theme.typography.titleSmBold, color: 'var(--text--base--default)',
|
|
487
|
+
flex: '1 1 auto', minInlineSize: 0, margin: 0,
|
|
488
|
+
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
|
489
|
+
},
|
|
490
|
+
'& .NeoDrawer-content': {
|
|
491
|
+
...theme.typography.bodyLgRegular, color: 'var(--text--base--default)',
|
|
492
|
+
flex: '1 1 auto', overflowY: 'auto',
|
|
493
|
+
paddingInline: 'var(--neo-space-xl-rem)', paddingBlock: `0 var(--neo-space-lg-rem)`,
|
|
494
|
+
'&:focus-visible': { outline: 'none', boxShadow: focusRingInset.boxShadow },
|
|
495
|
+
},
|
|
496
|
+
'& .NeoDrawer-image': {
|
|
497
|
+
blockSize: '224px', overflow: 'hidden',
|
|
498
|
+
marginInline: 'calc(-1 * var(--neo-space-xl-rem))',
|
|
499
|
+
'& > img, & > picture, & > video': { inlineSize: '100%', blockSize: '100%', objectFit: 'cover', display: 'block' },
|
|
500
|
+
},
|
|
501
|
+
'& .NeoDrawer-actions': {
|
|
502
|
+
display: 'flex', flexDirection: 'column', gap: 'var(--neo-space-md-rem)', flex: '0 0 auto',
|
|
503
|
+
paddingInline: 'var(--neo-space-xl-rem)',
|
|
504
|
+
paddingBlock: `var(--neo-space-lg-rem) var(--neo-space-2xl-rem)`,
|
|
505
|
+
backgroundColor: 'var(--surface--base--default)',
|
|
506
|
+
'& > div': { display: 'contents' },
|
|
507
|
+
'& .MuiButton-root': { inlineSize: '100%' },
|
|
508
|
+
'&[data-scroll="top"], &[data-scroll="mid"]': { borderBlockStart: '1px solid var(--border--base--secondary)' },
|
|
509
|
+
},
|
|
510
|
+
}),
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
|
|
401
514
|
MuiCheckbox: {
|
|
402
515
|
defaultProps: { disableRipple: true, checkedIcon: _h(CheckboxCheckedIcon), indeterminateIcon: _h(CheckboxIndeterminateIcon) },
|
|
403
516
|
styleOverrides: {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
// packages/mui/ui/BottomSheet.mjs — emitted without comments. The reasoning behind each decision lives in Neo's source repository.
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { withAliases } from './_alias-props.mjs';
|
|
5
|
+
import { BottomSheetBase } from './_sheet-base.mjs';
|
|
6
|
+
const h = React.createElement;
|
|
7
|
+
|
|
8
|
+
export const NeoBottomSheet = React.forwardRef(function NeoBottomSheet(rawProps, ref) {
|
|
9
|
+
const { open, onClose, title, image, actions, children, ...rest } = withAliases('NeoBottomSheet', rawProps);
|
|
10
|
+
return h(BottomSheetBase, {
|
|
11
|
+
open, onClose, title, image, actions, closable: true,
|
|
12
|
+
...rest, ref,
|
|
13
|
+
}, children);
|
|
14
|
+
});
|
package/ui/Drawer.mjs
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
// packages/mui/ui/Drawer.mjs — emitted without comments. The reasoning behind each decision lives in Neo's source repository.
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { withAliases } from './_alias-props.mjs';
|
|
5
|
+
import { DrawerBase } from './_sheet-base.mjs';
|
|
6
|
+
const h = React.createElement;
|
|
7
|
+
|
|
8
|
+
export const NeoDrawer = React.forwardRef(function NeoDrawer(rawProps, ref) {
|
|
9
|
+
const { open, onClose, title, image, actions, children, ...rest } = withAliases('NeoDrawer', rawProps);
|
|
10
|
+
return h(DrawerBase, {
|
|
11
|
+
open, onClose, title, image, actions, closable: true,
|
|
12
|
+
...rest, ref,
|
|
13
|
+
}, children);
|
|
14
|
+
});
|
package/ui/_modal-base.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import React from 'react';
|
|
|
4
4
|
import { Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, useMediaQuery } from '@mui/material';
|
|
5
5
|
import { NeoButtonIcon } from './ButtonIcon.mjs';
|
|
6
6
|
import { renderIcon } from './_icon.mjs';
|
|
7
|
+
import { useScrollState } from './_scroll-state.mjs';
|
|
7
8
|
const h = React.createElement;
|
|
8
9
|
|
|
9
10
|
const ICON_BY_TYPE = {
|
|
@@ -12,9 +13,12 @@ const ICON_BY_TYPE = {
|
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
|
|
15
|
-
const unaPorUna = (acciones) => {
|
|
16
|
+
export const unaPorUna = (acciones) => {
|
|
16
17
|
const lista = React.Children.toArray(acciones);
|
|
17
|
-
|
|
18
|
+
if (lista.length !== 1 || !React.isValidElement(lista[0])) return lista;
|
|
19
|
+
const [unico] = lista;
|
|
20
|
+
const pelado = unico.type === 'div' && !unico.props.className && !unico.props.style;
|
|
21
|
+
return unico.type === React.Fragment || pelado ? React.Children.toArray(unico.props.children) : lista;
|
|
18
22
|
};
|
|
19
23
|
|
|
20
24
|
export const ModalDialogBase = React.forwardRef(function ModalDialogBase({
|
|
@@ -27,31 +31,7 @@ export const ModalDialogBase = React.forwardRef(function ModalDialogBase({
|
|
|
27
31
|
const apiladas = fullScreen ? angosto : size === 'xs';
|
|
28
32
|
const accionesEnOrden = apiladas && actions ? unaPorUna(actions).reverse() : actions;
|
|
29
33
|
|
|
30
|
-
const
|
|
31
|
-
const [contentNode, setContentNode] = React.useState(null);
|
|
32
|
-
const setContent = React.useCallback((node) => { contentRef.current = node; setContentNode(node); }, []);
|
|
33
|
-
const [scroll, setScroll] = React.useState('none');
|
|
34
|
-
const [overflows, setOverflows] = React.useState(false);
|
|
35
|
-
const watchScroll = React.useCallback(() => {
|
|
36
|
-
const el = contentRef.current;
|
|
37
|
-
if (!el) return;
|
|
38
|
-
const extra = el.scrollHeight - el.clientHeight;
|
|
39
|
-
setOverflows(extra > 1);
|
|
40
|
-
if (extra <= 1) return setScroll('none');
|
|
41
|
-
if (el.scrollTop <= 1) return setScroll('top');
|
|
42
|
-
if (el.scrollTop >= extra - 1) return setScroll('bottom');
|
|
43
|
-
setScroll('mid');
|
|
44
|
-
}, []);
|
|
45
|
-
React.useEffect(() => {
|
|
46
|
-
const el = contentNode;
|
|
47
|
-
if (!el) return undefined;
|
|
48
|
-
watchScroll();
|
|
49
|
-
if (typeof ResizeObserver === 'undefined') return undefined;
|
|
50
|
-
const ro = new ResizeObserver(watchScroll);
|
|
51
|
-
ro.observe(el);
|
|
52
|
-
for (const h of el.children) ro.observe(h);
|
|
53
|
-
return () => ro.disconnect();
|
|
54
|
-
}, [watchScroll, open, children, contentNode]);
|
|
34
|
+
const { setRegion: setContent, watchScroll, scroll, overflows } = useScrollState([open, children]);
|
|
55
35
|
const titleId = `neo-modal-${auto}-title`;
|
|
56
36
|
const descriptionId = description ? `neo-modal-${auto}-description` : undefined;
|
|
57
37
|
const rowId = `neo-modal-${auto}-row`;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
// packages/mui/ui/_scroll-state.mjs — emitted without comments. The reasoning behind each decision lives in Neo's source repository.
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
export function useScrollState(deps = []) {
|
|
6
|
+
const ref = React.useRef(null);
|
|
7
|
+
const [node, setNode] = React.useState(null);
|
|
8
|
+
const setRegion = React.useCallback((n) => { ref.current = n; setNode(n); }, []);
|
|
9
|
+
const [scroll, setScroll] = React.useState('none');
|
|
10
|
+
const [overflows, setOverflows] = React.useState(false);
|
|
11
|
+
|
|
12
|
+
const watchScroll = React.useCallback(() => {
|
|
13
|
+
const el = ref.current;
|
|
14
|
+
if (!el) return;
|
|
15
|
+
const extra = el.scrollHeight - el.clientHeight;
|
|
16
|
+
setOverflows(extra > 1);
|
|
17
|
+
if (extra <= 1) return setScroll('none');
|
|
18
|
+
if (el.scrollTop <= 1) return setScroll('top');
|
|
19
|
+
if (el.scrollTop >= extra - 1) return setScroll('bottom');
|
|
20
|
+
setScroll('mid');
|
|
21
|
+
}, []);
|
|
22
|
+
|
|
23
|
+
React.useEffect(() => {
|
|
24
|
+
const el = node;
|
|
25
|
+
if (!el) return undefined;
|
|
26
|
+
watchScroll();
|
|
27
|
+
if (typeof ResizeObserver === 'undefined') return undefined;
|
|
28
|
+
const ro = new ResizeObserver(watchScroll);
|
|
29
|
+
ro.observe(el);
|
|
30
|
+
for (const hijo of el.children) ro.observe(hijo);
|
|
31
|
+
return () => ro.disconnect();
|
|
32
|
+
}, [watchScroll, node, ...deps]);
|
|
33
|
+
|
|
34
|
+
return { setRegion, watchScroll, scroll, overflows };
|
|
35
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
// packages/mui/ui/_sheet-base.mjs — emitted without comments. The reasoning behind each decision lives in Neo's source repository.
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { SwipeableDrawer, Drawer } from '@mui/material';
|
|
5
|
+
import { NeoButtonIcon } from './ButtonIcon.mjs';
|
|
6
|
+
import { renderIcon } from './_icon.mjs';
|
|
7
|
+
import { useScrollState } from './_scroll-state.mjs';
|
|
8
|
+
import { unaPorUna } from './_modal-base.mjs';
|
|
9
|
+
const h = React.createElement;
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
export const BottomSheetBase = React.forwardRef(function BottomSheetBase({
|
|
13
|
+
open, onClose, onOpen, title, image, actions, children, closable = true,
|
|
14
|
+
'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, className, ...rest
|
|
15
|
+
}, ref) {
|
|
16
|
+
const auto = React.useId();
|
|
17
|
+
const titleId = title && !image ? `neo-sheet-${auto}-title` : undefined;
|
|
18
|
+
|
|
19
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
20
|
+
if (!title && !ariaLabel && !ariaLabelledBy) {
|
|
21
|
+
console.warn('[Neo] BottomSheet: NeoBottomSheet needs a name: pass `title`, or —when you pass `image`— give the content a heading and point `aria-labelledby` at it.');
|
|
22
|
+
}
|
|
23
|
+
if (!onClose) {
|
|
24
|
+
console.warn('[Neo] BottomSheet: NeoBottomSheet needs `onClose`: without it the sheet has no ✕, no swipe and no way out.');
|
|
25
|
+
}
|
|
26
|
+
if (title && image) {
|
|
27
|
+
console.warn('[Neo] BottomSheet: with `image` the header has no title slot — the master draws one or the other. The `title` will not be shown.');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const { setRegion, watchScroll, scroll, overflows } = useScrollState([open, children]);
|
|
32
|
+
|
|
33
|
+
const accionesEnOrden = actions ? unaPorUna(actions).reverse() : actions;
|
|
34
|
+
|
|
35
|
+
const close = closable && onClose
|
|
36
|
+
? h(NeoButtonIcon, { key: 'x', onClick: onClose, 'aria-label': 'Cerrar',
|
|
37
|
+
variant: image ? 'primary' : 'tertiary', surface: image ? 'inverse' : 'default',
|
|
38
|
+
size: 'medium', className: 'NeoBottomSheet-close' },
|
|
39
|
+
renderIcon('system-close'))
|
|
40
|
+
: null;
|
|
41
|
+
|
|
42
|
+
const handle = h('div', { key: 'h', className: 'NeoBottomSheet-handle', 'aria-hidden': 'true' });
|
|
43
|
+
|
|
44
|
+
const header = image
|
|
45
|
+
? h('div', { key: 'hdr', className: 'NeoBottomSheet-headerImage' }, image, handle, close)
|
|
46
|
+
: h('header', { key: 'hdr', className: 'NeoBottomSheet-header' },
|
|
47
|
+
handle,
|
|
48
|
+
h('div', { className: 'NeoBottomSheet-headerRow' },
|
|
49
|
+
title ? h('h2', { id: titleId, className: 'NeoBottomSheet-title' }, title) : null,
|
|
50
|
+
close));
|
|
51
|
+
|
|
52
|
+
return h(SwipeableDrawer, {
|
|
53
|
+
ref,
|
|
54
|
+
anchor: 'bottom',
|
|
55
|
+
open: !!open,
|
|
56
|
+
onClose,
|
|
57
|
+
onOpen: onOpen || (() => {}),
|
|
58
|
+
disableSwipeToOpen: true,
|
|
59
|
+
slotProps: { backdrop: { 'aria-hidden': 'true' } },
|
|
60
|
+
className,
|
|
61
|
+
...rest,
|
|
62
|
+
PaperProps: {
|
|
63
|
+
...rest.PaperProps,
|
|
64
|
+
className: ['NeoBottomSheet', rest.PaperProps?.className].filter(Boolean).join(' '),
|
|
65
|
+
'data-image': image ? 'true' : 'false',
|
|
66
|
+
'data-scroll': scroll,
|
|
67
|
+
role: 'dialog',
|
|
68
|
+
'aria-modal': 'true',
|
|
69
|
+
'aria-labelledby': ariaLabelledBy || titleId,
|
|
70
|
+
'aria-label': (ariaLabelledBy || titleId) ? undefined : ariaLabel,
|
|
71
|
+
tabIndex: -1,
|
|
72
|
+
},
|
|
73
|
+
...(rest.slotProps ? { slotProps: { backdrop: { 'aria-hidden': 'true' }, ...rest.slotProps } } : {}),
|
|
74
|
+
},
|
|
75
|
+
header,
|
|
76
|
+
h('div', { key: 'c', className: 'NeoBottomSheet-content', ref: setRegion, onScroll: watchScroll,
|
|
77
|
+
'data-scroll': scroll,
|
|
78
|
+
...(overflows ? { tabIndex: 0, role: 'region', 'aria-labelledby': ariaLabelledBy || titleId } : null) },
|
|
79
|
+
children),
|
|
80
|
+
actions ? h('footer', { key: 'a', className: 'NeoBottomSheet-actions', 'data-scroll': scroll }, accionesEnOrden) : null,
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
export const DrawerBase = React.forwardRef(function DrawerBase({
|
|
86
|
+
open, onClose, title, image, actions, children, closable = true,
|
|
87
|
+
'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, className, ...rest
|
|
88
|
+
}, ref) {
|
|
89
|
+
const auto = React.useId();
|
|
90
|
+
const titleId = title ? `neo-drawer-${auto}-title` : undefined;
|
|
91
|
+
|
|
92
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
93
|
+
if (!title) console.warn('[Neo] Drawer: NeoDrawer needs `title`: it is the drawer\'s name for a screen reader, and the header always shows it.');
|
|
94
|
+
if (!onClose) console.warn('[Neo] Drawer: NeoDrawer needs `onClose`: without it the drawer has no ✕ and no way out.');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const { setRegion, watchScroll, scroll, overflows } = useScrollState([open, children]);
|
|
98
|
+
const accionesEnOrden = actions ? unaPorUna(actions).reverse() : actions;
|
|
99
|
+
|
|
100
|
+
const close = closable && onClose
|
|
101
|
+
? h(NeoButtonIcon, { key: 'x', onClick: onClose, 'aria-label': 'Cerrar', variant: 'tertiary', surface: 'default',
|
|
102
|
+
size: 'large', className: 'NeoDrawer-close' },
|
|
103
|
+
renderIcon('system-close'))
|
|
104
|
+
: null;
|
|
105
|
+
|
|
106
|
+
const header = h('header', { key: 'hdr', className: 'NeoDrawer-header' },
|
|
107
|
+
h('div', { className: 'NeoDrawer-headerRow' },
|
|
108
|
+
title ? h('h2', { id: titleId, className: 'NeoDrawer-title' }, title) : null,
|
|
109
|
+
close));
|
|
110
|
+
|
|
111
|
+
return h(Drawer, {
|
|
112
|
+
ref,
|
|
113
|
+
anchor: 'right',
|
|
114
|
+
open: !!open,
|
|
115
|
+
onClose,
|
|
116
|
+
slotProps: { backdrop: { 'aria-hidden': 'true' } },
|
|
117
|
+
className,
|
|
118
|
+
...rest,
|
|
119
|
+
PaperProps: {
|
|
120
|
+
...rest.PaperProps,
|
|
121
|
+
className: ['NeoDrawer', rest.PaperProps?.className].filter(Boolean).join(' '),
|
|
122
|
+
'data-scroll': scroll,
|
|
123
|
+
role: 'dialog',
|
|
124
|
+
'aria-modal': 'true',
|
|
125
|
+
'aria-labelledby': ariaLabelledBy || titleId,
|
|
126
|
+
'aria-label': (ariaLabelledBy || titleId) ? undefined : ariaLabel,
|
|
127
|
+
tabIndex: -1,
|
|
128
|
+
},
|
|
129
|
+
...(rest.slotProps ? { slotProps: { backdrop: { 'aria-hidden': 'true' }, ...rest.slotProps } } : {}),
|
|
130
|
+
},
|
|
131
|
+
header,
|
|
132
|
+
h('div', { key: 'c', className: 'NeoDrawer-content', ref: setRegion, onScroll: watchScroll,
|
|
133
|
+
'data-scroll': scroll,
|
|
134
|
+
...(overflows ? { tabIndex: 0, role: 'region', 'aria-labelledby': ariaLabelledBy || titleId } : null) },
|
|
135
|
+
image ? h('div', { key: 'img', className: 'NeoDrawer-image' }, image) : null,
|
|
136
|
+
children),
|
|
137
|
+
actions ? h('footer', { key: 'a', className: 'NeoDrawer-actions', 'data-scroll': scroll }, accionesEnOrden) : null,
|
|
138
|
+
);
|
|
139
|
+
});
|
package/ui/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
/// <reference path="../theme/neo.d.ts" />
|
|
10
10
|
import type { ReactNode, ReactElement, ForwardRefExoticComponent, RefAttributes, Ref, ChangeEvent, SyntheticEvent, MouseEvent, HTMLAttributes } from 'react';
|
|
11
11
|
import type { SxProps, Theme, ThemeOptions } from '@mui/material/styles';
|
|
12
|
-
import type { AlertProps, AutocompleteProps, BadgeProps, ButtonProps, CheckboxProps, ChipProps, CircularProgressProps, DialogProps, IconButtonProps, LinkProps, MenuProps, OutlinedInputProps, RadioProps, RadioGroupProps, SelectProps, SnackbarProps, SwitchProps, TabsProps, TooltipProps } from '@mui/material';
|
|
12
|
+
import type { AlertProps, AutocompleteProps, BadgeProps, ButtonProps, CheckboxProps, ChipProps, CircularProgressProps, DialogProps, DrawerProps, IconButtonProps, LinkProps, MenuProps, OutlinedInputProps, RadioProps, RadioGroupProps, SelectProps, SnackbarProps, SwipeableDrawerProps, SwitchProps, TabsProps, TooltipProps } from '@mui/material';
|
|
13
13
|
|
|
14
14
|
/** A handler checked bivariantly, the way React types its own event handlers: one typed with MUI's type still fits. */
|
|
15
15
|
type NeoHandler<A extends unknown[]> = { bivarianceHack(...args: A): void }['bivarianceHack'];
|
|
@@ -103,6 +103,27 @@ export interface NeoBannerProps extends Partial<Omit<HTMLAttributes<HTMLElement>
|
|
|
103
103
|
}
|
|
104
104
|
export declare const NeoBanner: ForwardRefExoticComponent<NeoBannerProps & ({ title: ReactNode } | { 'aria-label': string } | { 'aria-labelledby': string }) & RefAttributes<HTMLElement>>;
|
|
105
105
|
|
|
106
|
+
/** Rises from the bottom edge on mobile with complementary content that can wait, or a short task, while the person can still see and return to the page underneath. NO: If the content demands a decision, or reports an error the person has to resolve, it is `modal-dialog`. If it is a long task or dense information, it is `modal-fullscreen`. On desktop the same content moves to a lateral drawer: the sheet is mobile-only. */
|
|
107
|
+
export interface NeoBottomSheetProps extends Partial<Omit<SwipeableDrawerProps, 'open' | 'onClose' | 'title' | 'image' | 'actions' | 'children' | 'className' | 'sx' | 'ref'>> {
|
|
108
|
+
/** The panel or the bubble, open (controlled). */
|
|
109
|
+
open?: boolean;
|
|
110
|
+
/** The close handler. Its presence draws the ✕. */
|
|
111
|
+
onClose?: (...args: unknown[]) => void;
|
|
112
|
+
/** The title. It is a prop and not content because `aria-labelledby` anchors to it. */
|
|
113
|
+
title?: ReactNode;
|
|
114
|
+
/** */
|
|
115
|
+
image?: ReactNode;
|
|
116
|
+
/** The call to action. One at most (C1). */
|
|
117
|
+
actions?: ReactNode;
|
|
118
|
+
/** The content. Whoever uses the component provides it; in the stories it comes from the minimal example. */
|
|
119
|
+
children?: ReactNode;
|
|
120
|
+
/** Classes from the consumer. They merge with the component ones; they do not replace them. */
|
|
121
|
+
className?: string;
|
|
122
|
+
/** The MUI `sx`: per-instance styles, with access to the theme. */
|
|
123
|
+
sx?: SxProps<Theme>;
|
|
124
|
+
}
|
|
125
|
+
export declare const NeoBottomSheet: ForwardRefExoticComponent<NeoBottomSheetProps & ({ title: ReactNode } | { 'aria-label': string } | { 'aria-labelledby': string }) & RefAttributes<HTMLElement>>;
|
|
126
|
+
|
|
106
127
|
/** Runs actions. NO: To navigate it is `link`; for an on/off option it is `toggle`. */
|
|
107
128
|
export interface NeoButtonProps extends Partial<Omit<ButtonProps, 'family' | 'variant' | 'surface' | 'size' | 'label' | 'iconLeft' | 'iconRight' | 'loading' | 'disabled' | 'children' | 'className' | 'sx' | 'color' | 'ref'>> {
|
|
108
129
|
/** semantic family Default: 'system'. */
|
|
@@ -253,6 +274,27 @@ export interface NeoComboboxProps<T = string, Multiple extends boolean | undefin
|
|
|
253
274
|
}
|
|
254
275
|
export declare const NeoCombobox: <T = string, Multiple extends boolean | undefined = false, DisableClearable extends boolean | undefined = false, FreeSolo extends boolean | undefined = false>(props: NeoComboboxProps<T, Multiple, DisableClearable, FreeSolo> & ({ label: ReactNode } | { 'aria-label': string } | { 'aria-labelledby': string }) & RefAttributes<HTMLElement>) => ReactElement | null;
|
|
255
276
|
|
|
277
|
+
/** Enters from the right edge on the web with complementary or extensive content that the person opens on demand, while the page underneath stays in view. NO: If the content demands a decision, or reports an error the person has to resolve, it is `modal-dialog`. If it is a long task with steps, it is `modal-fullscreen`. On mobile the same content goes in a `bottom-sheet`: the drawer is web-only. */
|
|
278
|
+
export interface NeoDrawerProps extends Partial<Omit<DrawerProps, 'open' | 'onClose' | 'title' | 'image' | 'actions' | 'children' | 'className' | 'sx' | 'ref'>> {
|
|
279
|
+
/** The panel or the bubble, open (controlled). */
|
|
280
|
+
open?: boolean;
|
|
281
|
+
/** The close handler. Its presence draws the ✕. */
|
|
282
|
+
onClose?: (...args: unknown[]) => void;
|
|
283
|
+
/** The title. It is a prop and not content because `aria-labelledby` anchors to it. */
|
|
284
|
+
title: ReactNode;
|
|
285
|
+
/** */
|
|
286
|
+
image?: ReactNode;
|
|
287
|
+
/** The call to action. One at most (C1). */
|
|
288
|
+
actions?: ReactNode;
|
|
289
|
+
/** The content. Whoever uses the component provides it; in the stories it comes from the minimal example. */
|
|
290
|
+
children?: ReactNode;
|
|
291
|
+
/** Classes from the consumer. They merge with the component ones; they do not replace them. */
|
|
292
|
+
className?: string;
|
|
293
|
+
/** The MUI `sx`: per-instance styles, with access to the theme. */
|
|
294
|
+
sx?: SxProps<Theme>;
|
|
295
|
+
}
|
|
296
|
+
export declare const NeoDrawer: ForwardRefExoticComponent<NeoDrawerProps & RefAttributes<HTMLElement>>;
|
|
297
|
+
|
|
256
298
|
/** Takes the place of content that does not exist yet and says what can be done about it. NO: To communicate something that just happened, which is `alert`. For the wait while data loads, which is `spinner`: empty and loading are not the same, and mixing them up leaves the person waiting for something that is not coming. */
|
|
257
299
|
export interface NeoEmptyStateProps extends Partial<Omit<HTMLAttributes<HTMLElement>, 'icon' | 'title' | 'description' | 'actions' | 'headingLevel' | 'className' | 'ref'>> {
|
|
258
300
|
/** The icon node. Use the system icons (icons/), not loose SVGs. */
|
package/ui/index.mjs
CHANGED
|
@@ -19,6 +19,8 @@ export { NeoEmptyState } from './EmptyState.mjs';
|
|
|
19
19
|
export { NeoMenu } from './Menu.mjs';
|
|
20
20
|
export { NeoModalDialog } from './ModalDialog.mjs';
|
|
21
21
|
export { NeoModalFullscreen } from './ModalFullscreen.mjs';
|
|
22
|
+
export { NeoBottomSheet } from './BottomSheet.mjs';
|
|
23
|
+
export { NeoDrawer } from './Drawer.mjs';
|
|
22
24
|
export { NeoTabs } from './Tabs.mjs';
|
|
23
25
|
export { NeoBadge } from './Badge.mjs';
|
|
24
26
|
export { NeoTag } from './Tag.mjs';
|