@12-apps/ui 6.72.0 → 6.73.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/components-catalog.md +1 -1
- package/dist/_internal/components/index.js +7 -7
- package/dist/_internal/components/navigation/SectionNav/SectionNav.bar.js +9 -6
- package/dist/_internal/components/navigation/SectionNav/SectionNav.js +8 -7
- package/dist/_internal/components/navigation/SectionNav/SectionNav.parts.js +8 -3
- package/dist/_internal/components/navigation/SectionNav/SectionNav.primary.js +11 -4
- package/dist/_internal/components/navigation/SectionNav/SectionNav.rail.js +4 -3
- package/dist/_internal/components/navigation/SectionNav/SectionNav.railCreate.js +3 -2
- package/dist/_internal/components/navigation/SectionNav/SectionNav.sheet.js +3 -2
- package/dist/_internal/components/navigation/index.js +8 -7
- package/dist/{chunk-WP7KPWCP.js → chunk-24HHUCWB.js} +14 -2
- package/dist/chunk-24HHUCWB.js.map +1 -0
- package/dist/{chunk-LWQGIN2M.js → chunk-27F2VXS4.js} +9 -6
- package/dist/chunk-27F2VXS4.js.map +1 -0
- package/dist/{chunk-WY7FR7I4.js → chunk-ILJ3FXIC.js} +17 -8
- package/dist/chunk-ILJ3FXIC.js.map +1 -0
- package/dist/{chunk-2STYL5PF.js → chunk-NM46HQSW.js} +2 -2
- package/dist/{chunk-OBGUD2PG.js → chunk-SNWCT6JN.js} +91 -42
- package/dist/chunk-SNWCT6JN.js.map +1 -0
- package/dist/{chunk-VI2IXBOO.js → chunk-UKUGSU3Y.js} +33 -14
- package/dist/chunk-UKUGSU3Y.js.map +1 -0
- package/dist/{chunk-XS4NQ6LD.js → chunk-URSYEHPU.js} +68 -36
- package/dist/chunk-URSYEHPU.js.map +1 -0
- package/dist/navigation/SectionNav.js +8 -7
- package/dist/types/components/navigation/SectionNav/SectionNav.bar.d.ts +8 -2
- package/dist/types/components/navigation/SectionNav/SectionNav.bar.d.ts.map +1 -1
- package/dist/types/components/navigation/SectionNav/SectionNav.d.ts +4 -1
- package/dist/types/components/navigation/SectionNav/SectionNav.d.ts.map +1 -1
- package/dist/types/components/navigation/SectionNav/SectionNav.parts.d.ts +12 -1
- package/dist/types/components/navigation/SectionNav/SectionNav.parts.d.ts.map +1 -1
- package/dist/types/components/navigation/SectionNav/SectionNav.primary.d.ts +16 -1
- package/dist/types/components/navigation/SectionNav/SectionNav.primary.d.ts.map +1 -1
- package/dist/types/components/navigation/SectionNav/SectionNav.rail.d.ts +3 -2
- package/dist/types/components/navigation/SectionNav/SectionNav.rail.d.ts.map +1 -1
- package/dist/types/components/navigation/SectionNav/SectionNav.railCreate.d.ts +3 -1
- package/dist/types/components/navigation/SectionNav/SectionNav.railCreate.d.ts.map +1 -1
- package/dist/types/components/navigation/SectionNav/SectionNav.types.d.ts +12 -0
- package/dist/types/components/navigation/SectionNav/SectionNav.types.d.ts.map +1 -1
- package/dist/types/components/navigation/SectionNav/index.d.ts +1 -0
- package/dist/types/components/navigation/SectionNav/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/navigation/SectionNav/SectionNav.bar.tsx +84 -30
- package/src/components/navigation/SectionNav/SectionNav.md +25 -4
- package/src/components/navigation/SectionNav/SectionNav.parts.tsx +19 -1
- package/src/components/navigation/SectionNav/SectionNav.primary.tsx +52 -11
- package/src/components/navigation/SectionNav/SectionNav.rail.tsx +90 -35
- package/src/components/navigation/SectionNav/SectionNav.railCreate.tsx +23 -5
- package/src/components/navigation/SectionNav/SectionNav.tsx +6 -0
- package/src/components/navigation/SectionNav/SectionNav.types.ts +12 -0
- package/src/components/navigation/SectionNav/index.ts +1 -0
- package/dist/chunk-LWQGIN2M.js.map +0 -1
- package/dist/chunk-OBGUD2PG.js.map +0 -1
- package/dist/chunk-VI2IXBOO.js.map +0 -1
- package/dist/chunk-WP7KPWCP.js.map +0 -1
- package/dist/chunk-WY7FR7I4.js.map +0 -1
- package/dist/chunk-XS4NQ6LD.js.map +0 -1
- /package/dist/{chunk-2STYL5PF.js.map → chunk-NM46HQSW.js.map} +0 -0
|
@@ -14,26 +14,41 @@ import {
|
|
|
14
14
|
NOT_LIVE,
|
|
15
15
|
SlotIcon,
|
|
16
16
|
controlProps,
|
|
17
|
+
useMounted,
|
|
17
18
|
destinationControl,
|
|
18
19
|
focusRing,
|
|
19
20
|
} from './SectionNav.parts';
|
|
20
|
-
import { RaisedActionButton } from './SectionNav.primary';
|
|
21
|
+
import { raisedLift, RaisedActionButton } from './SectionNav.primary';
|
|
21
22
|
import { SectionNavSheet } from './SectionNav.sheet';
|
|
22
23
|
import type { SectionNavAction, SectionNavBadge, SectionNavDestination, SectionNavMenu, SectionNavProps } from './SectionNav.types';
|
|
23
24
|
|
|
24
25
|
/** Which of the bar's two menus is open, if any. */
|
|
25
26
|
type OpenMenu = 'primary' | 'more' | null;
|
|
26
27
|
|
|
27
|
-
/**
|
|
28
|
-
|
|
28
|
+
/**
|
|
29
|
+
* The bar's height above the safe area — tall enough for an icon over a label;
|
|
30
|
+
* a compact bar holds the icon alone.
|
|
31
|
+
*/
|
|
32
|
+
const barHeight = (theme: Parameters<typeof rem>[0], compact = false): string => rem(theme, compact ? 48 : 60);
|
|
33
|
+
|
|
34
|
+
/** How the bar is drawn — `compact` is `SectionNavProps.compact`. */
|
|
35
|
+
export interface SectionNavBarOptions {
|
|
36
|
+
compact?: boolean;
|
|
37
|
+
}
|
|
29
38
|
|
|
30
39
|
/**
|
|
31
40
|
* The bar's full height on screen, the safe area included — where its sheets
|
|
32
41
|
* stop, and what a host reserves at the foot of its content so nothing it
|
|
33
42
|
* floats (a toast, a receipt) lands under the bar.
|
|
34
43
|
*/
|
|
35
|
-
export function sectionNavBarInset(theme: Parameters<typeof rem>[0]): string {
|
|
36
|
-
return `calc(${barHeight(theme)} + env(safe-area-inset-bottom))`;
|
|
44
|
+
export function sectionNavBarInset(theme: Parameters<typeof rem>[0], options: SectionNavBarOptions = {}): string {
|
|
45
|
+
return `calc(${barHeight(theme, options.compact === true)} + env(safe-area-inset-bottom))`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Where the bar's sheets stop: clear of the raised button's {@link raisedLift}, which drew over their last row (FUT-3399). */
|
|
49
|
+
export function sectionNavSheetOffset(theme: Parameters<typeof rem>[0], raised: boolean, options: SectionNavBarOptions = {}): string {
|
|
50
|
+
const inset = sectionNavBarInset(theme, options);
|
|
51
|
+
return raised ? `calc(${inset} + ${rem(theme, raisedLift(options.compact === true))})` : inset;
|
|
37
52
|
}
|
|
38
53
|
|
|
39
54
|
/**
|
|
@@ -62,6 +77,7 @@ function Slot({
|
|
|
62
77
|
control,
|
|
63
78
|
expanded,
|
|
64
79
|
loading = false,
|
|
80
|
+
compact = false,
|
|
65
81
|
}: {
|
|
66
82
|
label: string;
|
|
67
83
|
icon: ReactNode;
|
|
@@ -73,17 +89,21 @@ function Slot({
|
|
|
73
89
|
control: Record<string, unknown>;
|
|
74
90
|
expanded?: boolean;
|
|
75
91
|
loading?: boolean;
|
|
92
|
+
/** Icon only: the label stays the slot's name, out of sight, and its tooltip. */
|
|
93
|
+
compact?: boolean;
|
|
76
94
|
}): React.JSX.Element {
|
|
77
95
|
const theme = useTheme();
|
|
96
|
+
const animate = useMounted();
|
|
78
97
|
return (
|
|
79
98
|
<Box
|
|
80
99
|
{...control}
|
|
100
|
+
title={compact ? label : undefined}
|
|
81
101
|
aria-current={current ? 'page' : undefined}
|
|
82
102
|
aria-expanded={expanded}
|
|
83
103
|
aria-busy={loading || undefined}
|
|
84
104
|
data-lit={lit ? 'true' : undefined}
|
|
85
105
|
data-testid={testId}
|
|
86
|
-
sx={slotSx(theme, lit)}
|
|
106
|
+
sx={slotSx(theme, lit, compact)}
|
|
87
107
|
>
|
|
88
108
|
<CountedIcon
|
|
89
109
|
icon={
|
|
@@ -107,6 +127,7 @@ function Slot({
|
|
|
107
127
|
lineHeight: 1.3,
|
|
108
128
|
fontWeight: lit ? 700 : 600,
|
|
109
129
|
color: lit ? 'text.primary' : 'inherit',
|
|
130
|
+
...labelFoldSx(theme, compact, animate),
|
|
110
131
|
}}
|
|
111
132
|
>
|
|
112
133
|
{label}
|
|
@@ -115,6 +136,23 @@ function Slot({
|
|
|
115
136
|
);
|
|
116
137
|
}
|
|
117
138
|
|
|
139
|
+
/**
|
|
140
|
+
* The label folding away in a compact bar: it shrinks to no height and fades
|
|
141
|
+
* while the bar shrinks, rather than leaving in one frame. Never `display:
|
|
142
|
+
* none` — it is still the slot's accessible name. With reduced motion it
|
|
143
|
+
* simply goes.
|
|
144
|
+
*/
|
|
145
|
+
function labelFoldSx(theme: Theme, compact: boolean, animate: boolean): Record<string, unknown> {
|
|
146
|
+
return {
|
|
147
|
+
maxHeight: compact ? 0 : rem(theme, 24),
|
|
148
|
+
opacity: compact ? 0 : 1,
|
|
149
|
+
transition: animate
|
|
150
|
+
? theme.transitions.create(['max-height', 'opacity'], { duration: theme.transitions.duration.shorter })
|
|
151
|
+
: 'none',
|
|
152
|
+
'@media (prefers-reduced-motion: reduce)': { transition: 'none' },
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
118
156
|
/** The pill behind a slot's icon — tinted while the slot is lit. */
|
|
119
157
|
function pillSx(theme: Theme): SxProps<Theme> {
|
|
120
158
|
return {
|
|
@@ -129,7 +167,7 @@ function pillSx(theme: Theme): SxProps<Theme> {
|
|
|
129
167
|
}
|
|
130
168
|
|
|
131
169
|
/** A slot's look: icon over label, a pill behind the lit icon, dimmed when disabled. */
|
|
132
|
-
function slotSx(theme: Theme, lit: boolean): SxProps<Theme> {
|
|
170
|
+
function slotSx(theme: Theme, lit: boolean, compact: boolean): SxProps<Theme> {
|
|
133
171
|
return {
|
|
134
172
|
...CONTROL_RESET,
|
|
135
173
|
...focusRing(theme),
|
|
@@ -139,7 +177,7 @@ function slotSx(theme: Theme, lit: boolean): SxProps<Theme> {
|
|
|
139
177
|
flexDirection: 'column',
|
|
140
178
|
alignItems: 'center',
|
|
141
179
|
justifyContent: 'center',
|
|
142
|
-
gap: 0.25,
|
|
180
|
+
gap: compact ? 0 : 0.25,
|
|
143
181
|
position: 'relative',
|
|
144
182
|
color: lit ? 'primary.main' : 'text.secondary',
|
|
145
183
|
// The current slot is marked by more than its colour: a tinted pill behind
|
|
@@ -158,6 +196,7 @@ interface SlotContext {
|
|
|
158
196
|
linkComponent: ElementType | undefined;
|
|
159
197
|
copy: SectionNavCopy;
|
|
160
198
|
dataTestId: string;
|
|
199
|
+
compact: boolean;
|
|
161
200
|
}
|
|
162
201
|
|
|
163
202
|
/** The raised button: an action runs on tap; a menu opens its sheet. */
|
|
@@ -175,6 +214,7 @@ function PrimarySlot({
|
|
|
175
214
|
label={primary.label}
|
|
176
215
|
icon={primary.icon}
|
|
177
216
|
captioned
|
|
217
|
+
compact={context.compact}
|
|
178
218
|
disabled={primary.disabled}
|
|
179
219
|
loading={primary.loading}
|
|
180
220
|
dataTestId={testId}
|
|
@@ -191,6 +231,7 @@ function PrimarySlot({
|
|
|
191
231
|
icon={primary.icon}
|
|
192
232
|
open={context.open === 'primary'}
|
|
193
233
|
closeLabel={context.copy.close}
|
|
234
|
+
compact={context.compact}
|
|
194
235
|
disabled={primary.disabled}
|
|
195
236
|
dataTestId={testId}
|
|
196
237
|
onClick={() => context.toggle('primary')}
|
|
@@ -211,6 +252,7 @@ function MoreSlot({ more, context }: { more: SectionNavMenu; context: SlotContex
|
|
|
211
252
|
expanded={open === 'more'}
|
|
212
253
|
copy={context.copy}
|
|
213
254
|
testId={more.dataTestId ?? `${context.dataTestId}-more`}
|
|
255
|
+
compact={context.compact}
|
|
214
256
|
control={controlProps({
|
|
215
257
|
href: undefined,
|
|
216
258
|
linkComponent: context.linkComponent,
|
|
@@ -240,6 +282,7 @@ function DestinationSlot({
|
|
|
240
282
|
copy={context.copy}
|
|
241
283
|
testId={destination.dataTestId ?? `${context.dataTestId}-dest-${destination.id}`}
|
|
242
284
|
loading={destination.loading === true}
|
|
285
|
+
compact={context.compact}
|
|
243
286
|
control={destinationControl(destination, context.linkComponent, () => {
|
|
244
287
|
context.close();
|
|
245
288
|
destination.onSelect?.();
|
|
@@ -265,6 +308,32 @@ function BarSlotView({
|
|
|
265
308
|
return <DestinationSlot destination={slot.destination} context={context} />;
|
|
266
309
|
}
|
|
267
310
|
|
|
311
|
+
/** The bar's own box: docked, a hairline on top, raised over the backdrop only while its sheet is open. */
|
|
312
|
+
function barSx(theme: Theme, sheetOpen: boolean, compact: boolean, animate: boolean): SxProps<Theme> {
|
|
313
|
+
return {
|
|
314
|
+
flex: 'none',
|
|
315
|
+
display: 'flex',
|
|
316
|
+
alignItems: 'stretch',
|
|
317
|
+
boxSizing: 'border-box',
|
|
318
|
+
height: sectionNavBarInset(theme, { compact }),
|
|
319
|
+
// Folding to icons and back is a change of height the eye follows;
|
|
320
|
+
// with reduced motion it simply happens.
|
|
321
|
+
transition: animate ? theme.transitions.create('height', { duration: theme.transitions.duration.shorter }) : 'none',
|
|
322
|
+
'@media (prefers-reduced-motion: reduce)': { transition: 'none' },
|
|
323
|
+
paddingBottom: 'env(safe-area-inset-bottom)',
|
|
324
|
+
borderTop: `1px solid ${theme.palette.divider}`,
|
|
325
|
+
bgcolor: 'background.paper',
|
|
326
|
+
// Above the backdrop ONLY while one of its own sheets is open, so the
|
|
327
|
+
// primary button can close what it opened. Raised all the time it
|
|
328
|
+
// would sit over every dialog the page opens, and on a small phone a
|
|
329
|
+
// dialog's own footer is exactly where the bar is. `stackedOverlayZIndex`,
|
|
330
|
+
// not `zIndex.modal + 1`: a host lifting a bottom Drawer clear of a
|
|
331
|
+
// stacked sheet catches this bar's OWN sheets too.
|
|
332
|
+
position: 'relative',
|
|
333
|
+
zIndex: sheetOpen ? stackedOverlayZIndex(theme) + 1 : 'auto',
|
|
334
|
+
} as SxProps<Theme>;
|
|
335
|
+
}
|
|
336
|
+
|
|
268
337
|
/** The phone layout: a bottom bar, with its menus as bottom sheets. */
|
|
269
338
|
export function SectionNavBar({
|
|
270
339
|
label,
|
|
@@ -274,10 +343,12 @@ export function SectionNavBar({
|
|
|
274
343
|
linkComponent,
|
|
275
344
|
copy,
|
|
276
345
|
dataTestId,
|
|
346
|
+
compact = false,
|
|
277
347
|
}: Required<Pick<SectionNavProps, 'label' | 'destinations' | 'copy' | 'dataTestId'>> &
|
|
278
|
-
Pick<SectionNavProps, 'primary' | 'more'> & { linkComponent: ElementType | undefined }): React.JSX.Element {
|
|
348
|
+
Pick<SectionNavProps, 'primary' | 'more' | 'compact'> & { linkComponent: ElementType | undefined }): React.JSX.Element {
|
|
279
349
|
const sheetMenu = isMenu(primary) ? primary : undefined;
|
|
280
350
|
const theme = useTheme();
|
|
351
|
+
const animate = useMounted();
|
|
281
352
|
const [open, setOpen] = useState<OpenMenu>(null);
|
|
282
353
|
const toggle = (menu: Exclude<OpenMenu, null>): void => setOpen((current) => (current === menu ? null : menu));
|
|
283
354
|
const close = (): void => setOpen(null);
|
|
@@ -288,24 +359,7 @@ export function SectionNavBar({
|
|
|
288
359
|
component="nav"
|
|
289
360
|
aria-label={label}
|
|
290
361
|
data-testid={dataTestId}
|
|
291
|
-
sx={
|
|
292
|
-
flex: 'none',
|
|
293
|
-
display: 'flex',
|
|
294
|
-
alignItems: 'stretch',
|
|
295
|
-
boxSizing: 'border-box',
|
|
296
|
-
height: sectionNavBarInset(theme),
|
|
297
|
-
paddingBottom: 'env(safe-area-inset-bottom)',
|
|
298
|
-
borderTop: `1px solid ${theme.palette.divider}`,
|
|
299
|
-
bgcolor: 'background.paper',
|
|
300
|
-
// Above the backdrop ONLY while one of its own sheets is open, so the
|
|
301
|
-
// primary button can close what it opened. Raised all the time it
|
|
302
|
-
// would sit over every dialog the page opens, and on a small phone a
|
|
303
|
-
// dialog's own footer is exactly where the bar is. `stackedOverlayZIndex`,
|
|
304
|
-
// not `zIndex.modal + 1`: a host lifting a bottom Drawer clear of a
|
|
305
|
-
// stacked sheet catches this bar's OWN sheets too.
|
|
306
|
-
position: 'relative',
|
|
307
|
-
zIndex: open === null ? 'auto' : stackedOverlayZIndex(theme) + 1,
|
|
308
|
-
}}
|
|
362
|
+
sx={barSx(theme, open !== null, compact, animate)}
|
|
309
363
|
>
|
|
310
364
|
{barSlots(destinations, more !== undefined, primary !== undefined).map((slot) => (
|
|
311
365
|
<BarSlotView
|
|
@@ -313,7 +367,7 @@ export function SectionNavBar({
|
|
|
313
367
|
slot={slot}
|
|
314
368
|
primary={primary}
|
|
315
369
|
more={more}
|
|
316
|
-
context={{ open, toggle, close, linkComponent, copy, dataTestId }}
|
|
370
|
+
context={{ open, toggle, close, linkComponent, copy, dataTestId, compact }}
|
|
317
371
|
/>
|
|
318
372
|
))}
|
|
319
373
|
</Box>
|
|
@@ -325,7 +379,7 @@ export function SectionNavBar({
|
|
|
325
379
|
linkComponent={linkComponent}
|
|
326
380
|
copy={copy}
|
|
327
381
|
testId={`${dataTestId}-primary-sheet`}
|
|
328
|
-
bottomOffset={
|
|
382
|
+
bottomOffset={sectionNavSheetOffset(theme, primary !== undefined, { compact })}
|
|
329
383
|
/>
|
|
330
384
|
) : null}
|
|
331
385
|
{more ? (
|
|
@@ -336,7 +390,7 @@ export function SectionNavBar({
|
|
|
336
390
|
linkComponent={linkComponent}
|
|
337
391
|
copy={copy}
|
|
338
392
|
testId={`${dataTestId}-more-sheet`}
|
|
339
|
-
bottomOffset={
|
|
393
|
+
bottomOffset={sectionNavSheetOffset(theme, primary !== undefined, { compact })}
|
|
340
394
|
/>
|
|
341
395
|
) : null}
|
|
342
396
|
</>
|
|
@@ -19,7 +19,7 @@ action, the sheets, and the rail's sections.
|
|
|
19
19
|
|
|
20
20
|
| `layout` | draws | menus |
|
|
21
21
|
|---|---|---|
|
|
22
|
-
| `bar` | a bottom row: the destinations, `more` last, `primary` raised in the middle | open as bottom sheets over the page, stopping above the bar |
|
|
22
|
+
| `bar` | a bottom row: the destinations, `more` last, `primary` raised in the middle | open as bottom sheets over the page, stopping above the bar — and above the raised `primary` when there is one, so it never draws over a sheet |
|
|
23
23
|
| `rail` | a vertical list: `back`, `heading`, a `primary` menu as one button, the destinations, then `more` | `primary` opens as a menu anchored to its button (named by `title`); `more` is listed under its `title` |
|
|
24
24
|
|
|
25
25
|
Which layout to use is the HOST's call. A bar beside a sidebar is two
|
|
@@ -50,6 +50,26 @@ screen.
|
|
|
50
50
|
carries no words. `PT_BR_SECTION_NAV_COPY` and `EN_US_SECTION_NAV_COPY` are
|
|
51
51
|
named packs; `SECTION_NAV_COPY` is the locale pack.
|
|
52
52
|
- **dataTestId**: the prefix for every test id (`section-nav`).
|
|
53
|
+
- **compact**: `boolean`, default `false`. Icons only, a size down, for a host
|
|
54
|
+
that folds its chrome so the content gets the screen. See *Compact*.
|
|
55
|
+
|
|
56
|
+
## Compact
|
|
57
|
+
|
|
58
|
+
`compact` draws the same navigation as icons only. Nothing is removed: every
|
|
59
|
+
label stays its control's accessible name (out of sight) and its `title`
|
|
60
|
+
tooltip, every count stays on its icon, every menu still opens.
|
|
61
|
+
|
|
62
|
+
| `layout` | compact draws |
|
|
63
|
+
|---|---|
|
|
64
|
+
| `bar` | 48px instead of 60; each slot its icon, its label folding away (shrinking and fading) as the bar shrinks; the raised button 40px instead of 52, lifted less, with its label as a tooltip and no caption under an action. Height and labels animate (200ms) unless the reader asked for reduced motion. |
|
|
65
|
+
| `rail` | each row, the way back (an arrow when it brings no icon) and a `primary` menu's button as their icon, centred; the heading, `more`'s title and its group titles out of sight. Made for a column 64px wide; the host sets the width. |
|
|
66
|
+
|
|
67
|
+
`sectionNavBarInset(theme, { compact: true })` is the compact bar's full
|
|
68
|
+
height, so a host's reserved foot follows the bar as it folds.
|
|
69
|
+
|
|
70
|
+
A slot's name includes its count ("Board, 3 pending") while its tooltip is the
|
|
71
|
+
label alone, so a screen reader may read the label a second time as the
|
|
72
|
+
description; the tooltip is there for a pointer, which sees no words otherwise.
|
|
53
73
|
|
|
54
74
|
## A bar of verbs
|
|
55
75
|
|
|
@@ -103,8 +123,8 @@ every label is semibold.
|
|
|
103
123
|
|
|
104
124
|
## Docking the bar
|
|
105
125
|
|
|
106
|
-
Dock the bar at the foot of a flex column. `sectionNavBarInset(theme)`
|
|
107
|
-
full height, the phone's safe area included. Reserve it at the foot of the
|
|
126
|
+
Dock the bar at the foot of a flex column. `sectionNavBarInset(theme)` (with
|
|
127
|
+
`{ compact: true }` for the compact bar) is its full height, the phone's safe area included. Reserve it at the foot of the
|
|
108
128
|
content, so nothing the page floats (a toast, a receipt) lands under the bar.
|
|
109
129
|
|
|
110
130
|
The bar rises above a modal backdrop only while one of its OWN sheets is open,
|
|
@@ -123,7 +143,8 @@ the slot reports the sheet through `aria-expanded`.
|
|
|
123
143
|
The raised round button on its own, for a host that already draws its own bar:
|
|
124
144
|
drop it between two tabs in a flex row. Without `open` it is a plain action.
|
|
125
145
|
With `captioned` its label is drawn under it, hidden from assistive tech so
|
|
126
|
-
the button is named once. `
|
|
146
|
+
the button is named once. `compact` draws it 40px rather than 52, with no
|
|
147
|
+
caption. `disabled` is native; `loading` keeps it focusable
|
|
127
148
|
with `aria-busy` and `aria-disabled`, and drops the click.
|
|
128
149
|
With `open` it reports `aria-expanded`, takes `closeLabel` as its name, and its
|
|
129
150
|
icon turns 45° while open.
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import Box from '@mui/material/Box/index.js';
|
|
2
2
|
import CircularProgress from '@mui/material/CircularProgress/index.js';
|
|
3
3
|
import type { SxProps, Theme } from '@mui/material/styles/index.js';
|
|
4
|
-
import type
|
|
4
|
+
import { useEffect, useState, type ElementType, type ReactNode } from 'react';
|
|
5
5
|
|
|
6
6
|
import { rem } from '../../../tokens/scales';
|
|
7
|
+
import { SR_ONLY_SX } from '../../form/Label/Label.styles';
|
|
7
8
|
import { Badge } from '../../data-display/Badge';
|
|
8
9
|
|
|
9
10
|
import { shownCount } from './SectionNav.helpers';
|
|
@@ -153,4 +154,21 @@ export const CONTROL_RESET = {
|
|
|
153
154
|
[NOT_LIVE]: { cursor: 'default' },
|
|
154
155
|
} as const;
|
|
155
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Out of sight, still read: a compact nav keeps each label as its control's
|
|
159
|
+
* accessible name. The same box `Label`'s `srOnly` draws.
|
|
160
|
+
*/
|
|
161
|
+
export const VISUALLY_HIDDEN = SR_ONLY_SX;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* False on the first render, true from then on — so a fold animates when the
|
|
165
|
+
* host CHANGES `compact`, never when a page simply opens folded (which drew
|
|
166
|
+
* the labels sliding out over the neighbouring badges on every load).
|
|
167
|
+
*/
|
|
168
|
+
export function useMounted(): boolean {
|
|
169
|
+
const [mounted, setMounted] = useState(false);
|
|
170
|
+
useEffect(() => setMounted(true), []);
|
|
171
|
+
return mounted;
|
|
172
|
+
}
|
|
173
|
+
|
|
156
174
|
export { Box };
|
|
@@ -4,7 +4,7 @@ import type { ReactNode } from 'react';
|
|
|
4
4
|
|
|
5
5
|
import { rem } from '../../../tokens/scales';
|
|
6
6
|
|
|
7
|
-
import { Box, CONTROL_RESET, NOT_LIVE, SlotIcon
|
|
7
|
+
import { Box, CONTROL_RESET, NOT_LIVE, SlotIcon } from './SectionNav.parts';
|
|
8
8
|
|
|
9
9
|
export interface RaisedActionButtonProps {
|
|
10
10
|
/**
|
|
@@ -28,6 +28,12 @@ export interface RaisedActionButtonProps {
|
|
|
28
28
|
* bar of places can leave its one action to the icon.
|
|
29
29
|
*/
|
|
30
30
|
captioned?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* A size down — 40px rather than 52, lifted less — for a compact bar. It
|
|
33
|
+
* draws no caption: a compact bar is icons only, and the name stays the
|
|
34
|
+
* button's `aria-label`.
|
|
35
|
+
*/
|
|
36
|
+
compact?: boolean;
|
|
31
37
|
/** Dimmed and not operable: a native `disabled` button. */
|
|
32
38
|
disabled?: boolean;
|
|
33
39
|
/**
|
|
@@ -58,6 +64,7 @@ export function RaisedActionButton({
|
|
|
58
64
|
open,
|
|
59
65
|
closeLabel,
|
|
60
66
|
captioned = false,
|
|
67
|
+
compact = false,
|
|
61
68
|
disabled = false,
|
|
62
69
|
loading = false,
|
|
63
70
|
dataTestId = 'raised-action',
|
|
@@ -79,18 +86,36 @@ export function RaisedActionButton({
|
|
|
79
86
|
component="button"
|
|
80
87
|
type="button"
|
|
81
88
|
{...raisedState({ onClick, disabled, loading })}
|
|
82
|
-
|
|
89
|
+
{...raisedNames({ label, closeLabel, expanded, compact })}
|
|
83
90
|
aria-expanded={open}
|
|
84
91
|
data-testid={dataTestId}
|
|
85
|
-
sx={raisedSx(theme, expanded)}
|
|
92
|
+
sx={raisedSx(theme, expanded, compact)}
|
|
86
93
|
>
|
|
87
94
|
<SlotIcon icon={icon} loading={loading} />
|
|
88
95
|
</Box>
|
|
89
|
-
{captioned
|
|
96
|
+
<RaisedCaption shown={captioned && !compact} label={label} inert={disabled || loading} />
|
|
90
97
|
</Box>
|
|
91
98
|
);
|
|
92
99
|
}
|
|
93
100
|
|
|
101
|
+
/**
|
|
102
|
+
* The button's name — `closeLabel` while what it opened is open — and, on a
|
|
103
|
+
* compact bar where no word is drawn, the label as its tooltip.
|
|
104
|
+
*/
|
|
105
|
+
function raisedNames({
|
|
106
|
+
label,
|
|
107
|
+
closeLabel,
|
|
108
|
+
expanded,
|
|
109
|
+
compact,
|
|
110
|
+
}: {
|
|
111
|
+
label: string;
|
|
112
|
+
closeLabel: string | undefined;
|
|
113
|
+
expanded: boolean;
|
|
114
|
+
compact: boolean;
|
|
115
|
+
}): { 'aria-label': string; title: string | undefined } {
|
|
116
|
+
return { 'aria-label': expanded && closeLabel ? closeLabel : label, title: compact ? label : undefined };
|
|
117
|
+
}
|
|
118
|
+
|
|
94
119
|
/**
|
|
95
120
|
* The button's operability. Only `disabled` is native: an in-flight button
|
|
96
121
|
* keeps its focus, reports `aria-busy` and `aria-disabled`, and drops the click.
|
|
@@ -114,10 +139,11 @@ function raisedState({
|
|
|
114
139
|
}
|
|
115
140
|
|
|
116
141
|
/**
|
|
117
|
-
* The label under a captioned button. Hidden from assistive tech: the button
|
|
142
|
+
* The label under a captioned button — none on a compact bar. Hidden from assistive tech: the button
|
|
118
143
|
* already carries the same words as its name, and reading them twice is noise.
|
|
119
144
|
*/
|
|
120
|
-
function RaisedCaption({ label, inert }: { label: string; inert: boolean }): React.JSX.Element {
|
|
145
|
+
function RaisedCaption({ shown, label, inert }: { shown: boolean; label: string; inert: boolean }): React.JSX.Element | null {
|
|
146
|
+
if (!shown) return null;
|
|
121
147
|
return (
|
|
122
148
|
<Typography
|
|
123
149
|
component="span"
|
|
@@ -130,16 +156,31 @@ function RaisedCaption({ label, inert }: { label: string; inert: boolean }): Rea
|
|
|
130
156
|
);
|
|
131
157
|
}
|
|
132
158
|
|
|
159
|
+
/**
|
|
160
|
+
* How far the round button stands out of the bar's top edge, in design px — what
|
|
161
|
+
* the bar's sheets must clear so the button never draws over one (FUT-3399).
|
|
162
|
+
* The compact bar's smaller button stands out less.
|
|
163
|
+
*/
|
|
164
|
+
export const RAISED_LIFT = 18;
|
|
165
|
+
export const RAISED_LIFT_COMPACT = 12;
|
|
166
|
+
|
|
167
|
+
/** The lift of the round button on a bar of this form. */
|
|
168
|
+
export function raisedLift(compact: boolean): number {
|
|
169
|
+
return compact ? RAISED_LIFT_COMPACT : RAISED_LIFT;
|
|
170
|
+
}
|
|
171
|
+
|
|
133
172
|
/** The round button: lifted half out of the bar, its icon turning while what it opened is open. */
|
|
134
|
-
function raisedSx(theme: Theme, expanded: boolean): SxProps<Theme> {
|
|
135
|
-
const size = rem(theme, 52);
|
|
173
|
+
function raisedSx(theme: Theme, expanded: boolean, compact: boolean): SxProps<Theme> {
|
|
174
|
+
const size = rem(theme, compact ? 40 : 52);
|
|
136
175
|
return {
|
|
137
176
|
...CONTROL_RESET,
|
|
138
|
-
|
|
177
|
+
// The ring OUTSIDE the button: the shared ring sits inside the box, in the
|
|
178
|
+
// primary colour, where this button's own fill swallowed it whole.
|
|
179
|
+
'&:focus-visible': { outline: `${rem(theme, 2)} solid ${theme.palette.primary.main}`, outlineOffset: rem(theme, 2) },
|
|
139
180
|
width: size,
|
|
140
181
|
height: size,
|
|
141
182
|
// Lifted half out of the bar: the one control that is not a place.
|
|
142
|
-
marginTop: rem(theme, -
|
|
183
|
+
marginTop: rem(theme, -raisedLift(compact)),
|
|
143
184
|
borderRadius: '50%',
|
|
144
185
|
display: 'flex',
|
|
145
186
|
alignItems: 'center',
|
|
@@ -149,7 +190,7 @@ function raisedSx(theme: Theme, expanded: boolean): SxProps<Theme> {
|
|
|
149
190
|
boxShadow: theme.shadows[6],
|
|
150
191
|
[NOT_LIVE]: { cursor: 'default', bgcolor: 'action.disabledBackground', color: 'text.disabled', boxShadow: 'none' },
|
|
151
192
|
'& svg': {
|
|
152
|
-
fontSize: rem(theme, 26),
|
|
193
|
+
fontSize: rem(theme, compact ? 22 : 26),
|
|
153
194
|
transition: theme.transitions.create('transform', { duration: theme.transitions.duration.shorter }),
|
|
154
195
|
transform: expanded ? 'rotate(45deg)' : 'none',
|
|
155
196
|
},
|