@12-apps/ui 6.5.0 → 6.7.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/dist/{chunk-3KWD5Y5O.js → chunk-RXE5CUUE.js} +8 -4
- package/dist/chunk-RXE5CUUE.js.map +1 -0
- package/dist/data-display/Alert.js +1 -1
- package/dist/data-display/AsyncStateContainer.js +2 -1
- package/dist/data-display/AsyncStateContainer.js.map +1 -1
- package/dist/data-display/Banner.js +4 -2
- package/dist/data-display/Banner.js.map +1 -1
- package/dist/data-display/CardKit.js +3 -1
- package/dist/data-display/CardKit.js.map +1 -1
- package/dist/data-display/Carousel.js +5 -2
- package/dist/data-display/Carousel.js.map +1 -1
- package/dist/data-display/DataViews.js +8 -7
- package/dist/data-display/DataViews.js.map +1 -1
- package/dist/data-display/Lightbox.js +147 -117
- package/dist/data-display/Lightbox.js.map +1 -1
- package/dist/data-display/MapPreview.js +14 -10
- package/dist/data-display/MapPreview.js.map +1 -1
- package/dist/pt-BR.js +46 -3
- package/dist/pt-BR.js.map +1 -1
- package/dist/types/components/data-display/Alert/Alert.d.ts +9 -2
- package/dist/types/components/data-display/Alert/Alert.d.ts.map +1 -1
- package/dist/types/components/data-display/Alert/Alert.types.d.ts +25 -9
- package/dist/types/components/data-display/Alert/Alert.types.d.ts.map +1 -1
- package/dist/types/components/data-display/AsyncStateContainer/AsyncStateContainer.d.ts.map +1 -1
- package/dist/types/components/data-display/AsyncStateContainer/AsyncStateContainer.types.d.ts +6 -0
- package/dist/types/components/data-display/AsyncStateContainer/AsyncStateContainer.types.d.ts.map +1 -1
- package/dist/types/components/data-display/Banner/Banner.d.ts.map +1 -1
- package/dist/types/components/data-display/Banner/Banner.types.d.ts +2 -0
- package/dist/types/components/data-display/Banner/Banner.types.d.ts.map +1 -1
- package/dist/types/components/data-display/CardKit/card-actions-context.d.ts +3 -1
- package/dist/types/components/data-display/CardKit/card-actions-context.d.ts.map +1 -1
- package/dist/types/components/data-display/Carousel/Carousel.chrome.d.ts +3 -0
- package/dist/types/components/data-display/Carousel/Carousel.chrome.d.ts.map +1 -1
- package/dist/types/components/data-display/Carousel/Carousel.parts.d.ts.map +1 -1
- package/dist/types/components/data-display/Carousel/Carousel.types.d.ts +7 -0
- package/dist/types/components/data-display/Carousel/Carousel.types.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-copy.d.ts +25 -0
- package/dist/types/components/data-display/DataViews/data-views-copy.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-layout-context.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-table-parts.d.ts.map +1 -1
- package/dist/types/components/data-display/Lightbox/Lightbox.d.ts.map +1 -1
- package/dist/types/components/data-display/Lightbox/Lightbox.types.d.ts +9 -0
- package/dist/types/components/data-display/Lightbox/Lightbox.types.d.ts.map +1 -1
- package/dist/types/components/data-display/Lightbox/LightboxControls.d.ts.map +1 -1
- package/dist/types/components/data-display/Lightbox/lightbox-copy-context.d.ts +17 -0
- package/dist/types/components/data-display/Lightbox/lightbox-copy-context.d.ts.map +1 -0
- package/dist/types/components/data-display/MapPreview/MapControlBar.d.ts +7 -0
- package/dist/types/components/data-display/MapPreview/MapControlBar.d.ts.map +1 -1
- package/dist/types/components/data-display/MapPreview/MapLayers.d.ts.map +1 -1
- package/dist/types/components/data-display/MapPreview/MapPreview.types.d.ts +9 -0
- package/dist/types/components/data-display/MapPreview/MapPreview.types.d.ts.map +1 -1
- package/dist/types/components/data-display/MapPreview/MapSearchBar.d.ts +2 -0
- package/dist/types/components/data-display/MapPreview/MapSearchBar.d.ts.map +1 -1
- package/dist/types/copy.d.ts +53 -0
- package/dist/types/copy.d.ts.map +1 -1
- package/dist/types/pt-BR.d.ts +5 -1
- package/dist/types/pt-BR.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/data-display/Alert/Alert.tsx +22 -5
- package/src/components/data-display/Alert/Alert.types.ts +20 -10
- package/src/components/data-display/AsyncStateContainer/AsyncStateContainer.tsx +2 -1
- package/src/components/data-display/AsyncStateContainer/AsyncStateContainer.types.ts +6 -0
- package/src/components/data-display/Banner/Banner.tsx +5 -2
- package/src/components/data-display/Banner/Banner.types.ts +2 -0
- package/src/components/data-display/CardKit/card-actions-context.tsx +4 -0
- package/src/components/data-display/Carousel/Carousel.chrome.tsx +5 -0
- package/src/components/data-display/Carousel/Carousel.parts.tsx +4 -3
- package/src/components/data-display/Carousel/Carousel.types.ts +7 -0
- package/src/components/data-display/DataViews/data-views-copy.ts +25 -0
- package/src/components/data-display/DataViews/data-views-display-tabs.tsx +2 -2
- package/src/components/data-display/DataViews/data-views-export.tsx +1 -1
- package/src/components/data-display/DataViews/data-views-layout-context.tsx +3 -1
- package/src/components/data-display/DataViews/data-views-more-filters.tsx +2 -2
- package/src/components/data-display/DataViews/data-views-range-pill.tsx +1 -1
- package/src/components/data-display/DataViews/data-views-table-parts.tsx +1 -0
- package/src/components/data-display/Lightbox/Lightbox.tsx +3 -0
- package/src/components/data-display/Lightbox/Lightbox.types.ts +9 -0
- package/src/components/data-display/Lightbox/LightboxControls.tsx +28 -15
- package/src/components/data-display/Lightbox/lightbox-copy-context.tsx +42 -0
- package/src/components/data-display/MapPreview/MapControlBar.tsx +17 -9
- package/src/components/data-display/MapPreview/MapLayers.tsx +2 -0
- package/src/components/data-display/MapPreview/MapPreview.types.ts +10 -0
- package/src/components/data-display/MapPreview/MapSearchBar.tsx +4 -1
- package/src/copy.ts +57 -0
- package/src/pt-BR.ts +48 -1
- package/dist/chunk-3KWD5Y5O.js.map +0 -1
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type React from 'react';
|
|
2
2
|
|
|
3
3
|
export interface AsyncStateContainerProps {
|
|
4
|
+
/**
|
|
5
|
+
* What an EMPTY result reads as, when the caller supplies no
|
|
6
|
+
* `emptyComponent` of its own. REQUIRED — the fallback was the English
|
|
7
|
+
* "No data available", on the package every host renders.
|
|
8
|
+
*/
|
|
9
|
+
emptyTitle: string;
|
|
4
10
|
/**
|
|
5
11
|
* Whether the container is in a loading state
|
|
6
12
|
* @default false
|
|
@@ -202,8 +202,9 @@ const BannerTrailing: React.FC<{
|
|
|
202
202
|
variant: BannerVariant;
|
|
203
203
|
dismissible: boolean;
|
|
204
204
|
onDismissClick: () => void;
|
|
205
|
+
dismissLabel: string;
|
|
205
206
|
dataTestId?: string;
|
|
206
|
-
}> = ({ actions, variant, dismissible, onDismissClick, dataTestId }) => (
|
|
207
|
+
}> = ({ actions, variant, dismissible, onDismissClick, dismissLabel, dataTestId }) => (
|
|
207
208
|
<Box display="flex" alignItems="center" gap={1}>
|
|
208
209
|
{actions && actions.length > 0 && (
|
|
209
210
|
<Box className="banner-actions">
|
|
@@ -223,7 +224,7 @@ const BannerTrailing: React.FC<{
|
|
|
223
224
|
className="banner-dismiss"
|
|
224
225
|
size="small"
|
|
225
226
|
onClick={onDismissClick}
|
|
226
|
-
aria-label=
|
|
227
|
+
aria-label={dismissLabel}
|
|
227
228
|
data-testid={testIdFor(dataTestId, 'close')}
|
|
228
229
|
sx={{ transition: 'all 0.2s ease' }}
|
|
229
230
|
>
|
|
@@ -241,6 +242,7 @@ export const Banner = React.forwardRef<HTMLDivElement, BannerProps>(
|
|
|
241
242
|
description,
|
|
242
243
|
icon,
|
|
243
244
|
dismissible = false,
|
|
245
|
+
dismissLabel,
|
|
244
246
|
onDismiss,
|
|
245
247
|
actions,
|
|
246
248
|
sticky = false,
|
|
@@ -295,6 +297,7 @@ export const Banner = React.forwardRef<HTMLDivElement, BannerProps>(
|
|
|
295
297
|
variant={variant}
|
|
296
298
|
dismissible={dismissible}
|
|
297
299
|
onDismissClick={handleDismiss}
|
|
300
|
+
dismissLabel={dismissLabel}
|
|
298
301
|
dataTestId={dataTestId}
|
|
299
302
|
/>
|
|
300
303
|
</StyledBanner>
|
|
@@ -14,6 +14,8 @@ export interface BannerAction {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export interface BannerProps {
|
|
17
|
+
/** The dismiss button's accessible name — it carries a glyph only. REQUIRED. */
|
|
18
|
+
dismissLabel: string;
|
|
17
19
|
/** Message severity type */
|
|
18
20
|
variant?: BannerVariant;
|
|
19
21
|
/** Main message title text */
|
|
@@ -37,6 +37,7 @@ export function CardActionsProvider({
|
|
|
37
37
|
tenantSlug,
|
|
38
38
|
onRefresh,
|
|
39
39
|
errorTitle,
|
|
40
|
+
errorDismissLabel,
|
|
40
41
|
autoHideMs = 6_000,
|
|
41
42
|
children,
|
|
42
43
|
}: {
|
|
@@ -50,6 +51,8 @@ export function CardActionsProvider({
|
|
|
50
51
|
* MESSAGE beside it comes from whatever failed, which is already the host's.
|
|
51
52
|
*/
|
|
52
53
|
errorTitle: string;
|
|
54
|
+
/** The dismiss on that alert, which carries a glyph only. REQUIRED. */
|
|
55
|
+
errorDismissLabel: string;
|
|
53
56
|
/** How long the snackbar stays up. */
|
|
54
57
|
autoHideMs?: number;
|
|
55
58
|
children: ReactNode;
|
|
@@ -72,6 +75,7 @@ export function CardActionsProvider({
|
|
|
72
75
|
title={errorTitle}
|
|
73
76
|
description={error ?? ''}
|
|
74
77
|
closable
|
|
78
|
+
closeLabel={errorDismissLabel}
|
|
75
79
|
onClose={() => setError(null)}
|
|
76
80
|
data-testid="card-action-error"
|
|
77
81
|
/>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
|
|
3
3
|
import { CarouselArrows, CarouselIndicators, CarouselThumbnails } from './Carousel.parts';
|
|
4
|
+
import type { CarouselCopy } from '../../../copy';
|
|
4
5
|
import type {
|
|
5
6
|
CarouselArrowsProps,
|
|
6
7
|
CarouselIndicatorsProps,
|
|
@@ -12,6 +13,8 @@ type IndicatorPosition = NonNullable<CarouselIndicatorsProps['position']>;
|
|
|
12
13
|
type Color = NonNullable<CarouselIndicatorsProps['color']>;
|
|
13
14
|
|
|
14
15
|
export interface CarouselChromeProps {
|
|
16
|
+
/** The two arrows' accessible names, from the mount. */
|
|
17
|
+
copy: CarouselCopy;
|
|
15
18
|
items: CarouselItem[];
|
|
16
19
|
activeIndex: number;
|
|
17
20
|
loop: boolean;
|
|
@@ -34,6 +37,7 @@ export interface CarouselChromeProps {
|
|
|
34
37
|
* as three conditional blocks inside the carousel's own render.
|
|
35
38
|
*/
|
|
36
39
|
export const CarouselChrome: React.FC<CarouselChromeProps> = ({
|
|
40
|
+
copy,
|
|
37
41
|
items,
|
|
38
42
|
activeIndex,
|
|
39
43
|
loop,
|
|
@@ -52,6 +56,7 @@ export const CarouselChrome: React.FC<CarouselChromeProps> = ({
|
|
|
52
56
|
<>
|
|
53
57
|
{showArrows && !disabled && (
|
|
54
58
|
<CarouselArrows
|
|
59
|
+
copy={copy}
|
|
55
60
|
onPrev={onPrev}
|
|
56
61
|
onNext={onNext}
|
|
57
62
|
position={arrowPosition}
|
|
@@ -212,7 +212,8 @@ const arrowSx = (theme: Theme, color: Color, isOverlay: boolean): CSSObject => (
|
|
|
212
212
|
|
|
213
213
|
export const CarouselArrows: React.FC<CarouselArrowsProps> = (props) => {
|
|
214
214
|
const {
|
|
215
|
-
onPrev, onNext, position, disabled, disablePrev, disableNext, color, size, className,
|
|
215
|
+
copy, onPrev, onNext, position, disabled, disablePrev, disableNext, color, size, className,
|
|
216
|
+
style,
|
|
216
217
|
} = withDefaults(props, ARROWS_DEFAULTS) as CarouselArrowsProps &
|
|
217
218
|
Required<Pick<CarouselArrowsProps, keyof typeof ARROWS_DEFAULTS>>;
|
|
218
219
|
|
|
@@ -227,7 +228,7 @@ export const CarouselArrows: React.FC<CarouselArrowsProps> = (props) => {
|
|
|
227
228
|
disabled={disabled || disablePrev}
|
|
228
229
|
size={muiSize(size)}
|
|
229
230
|
className={className}
|
|
230
|
-
aria-label=
|
|
231
|
+
aria-label={copy.previous}
|
|
231
232
|
data-testid="carousel-prev-button"
|
|
232
233
|
sx={{ ...sx, left: offset, ...style }}
|
|
233
234
|
>
|
|
@@ -237,7 +238,7 @@ export const CarouselArrows: React.FC<CarouselArrowsProps> = (props) => {
|
|
|
237
238
|
<IconButton
|
|
238
239
|
onClick={onNext}
|
|
239
240
|
disabled={disabled || disableNext}
|
|
240
|
-
aria-label=
|
|
241
|
+
aria-label={copy.next}
|
|
241
242
|
size={muiSize(size)}
|
|
242
243
|
className={className}
|
|
243
244
|
data-testid="carousel-next-button"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ColorValue, SizeValue } from '../../../tokens/scales';
|
|
2
2
|
import type { CSSProperties, ReactNode } from 'react';
|
|
3
|
+
import type { CarouselCopy } from '../../../copy';
|
|
3
4
|
|
|
4
5
|
export interface CarouselItem {
|
|
5
6
|
id: string;
|
|
@@ -11,6 +12,11 @@ export interface CarouselItem {
|
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export interface CarouselProps {
|
|
15
|
+
/**
|
|
16
|
+
* The two arrows' accessible names. They render a glyph and nothing else,
|
|
17
|
+
* so this pair IS what a screen reader announces for them. REQUIRED.
|
|
18
|
+
*/
|
|
19
|
+
copy: CarouselCopy;
|
|
14
20
|
items: CarouselItem[];
|
|
15
21
|
variant?: 'default' | 'glass' | 'gradient' | 'elevated' | 'minimal' | 'cards';
|
|
16
22
|
size?: SizeValue;
|
|
@@ -58,6 +64,7 @@ export interface CarouselIndicatorsProps {
|
|
|
58
64
|
}
|
|
59
65
|
|
|
60
66
|
export interface CarouselArrowsProps {
|
|
67
|
+
copy: CarouselCopy;
|
|
61
68
|
onPrev: () => void;
|
|
62
69
|
onNext: () => void;
|
|
63
70
|
position?: 'inside' | 'outside' | 'overlay';
|
|
@@ -122,6 +122,11 @@ export interface DataViewsDisplayCopy {
|
|
|
122
122
|
densityUnavailableNarrow: string;
|
|
123
123
|
/** The control that opens this panel. */
|
|
124
124
|
trigger: string;
|
|
125
|
+
/** The two column headings inside the panel: which field, and in what shape. */
|
|
126
|
+
fieldHeading: string;
|
|
127
|
+
formatHeading: string;
|
|
128
|
+
/** The card-grid zoom slider, which carries a glyph at each end and no label. */
|
|
129
|
+
cardZoom: string;
|
|
125
130
|
}
|
|
126
131
|
|
|
127
132
|
export interface DataViewsExportCopy {
|
|
@@ -130,6 +135,12 @@ export interface DataViewsExportCopy {
|
|
|
130
135
|
visibleColumns(columnCount: number): string;
|
|
131
136
|
/** The control that opens the menu. */
|
|
132
137
|
trigger: string;
|
|
138
|
+
/**
|
|
139
|
+
* The word ON that control when the bar has room for it. `trigger` is its
|
|
140
|
+
* accessible name and its compact tooltip; this is what the eye reads, and
|
|
141
|
+
* it was a literal — so a host could set one and not the other.
|
|
142
|
+
*/
|
|
143
|
+
triggerLabel: string;
|
|
133
144
|
}
|
|
134
145
|
|
|
135
146
|
export interface DataViewsGridCopy {
|
|
@@ -151,9 +162,21 @@ export interface DataViewsGridCopy {
|
|
|
151
162
|
export interface DataViewsFiltersCopy {
|
|
152
163
|
moreFilters(count: number): string;
|
|
153
164
|
moreFiltersApplied(count: number, appliedCount: number): string;
|
|
165
|
+
/**
|
|
166
|
+
* The overflow control's own two words: the one ON the button, and the
|
|
167
|
+
* heading of the sheet it opens. `moreFilters` is the accessible name and
|
|
168
|
+
* counts; these are what the eye reads.
|
|
169
|
+
*/
|
|
170
|
+
moreTriggerLabel: string;
|
|
171
|
+
moreHeading: string;
|
|
154
172
|
overflowNote: string;
|
|
155
173
|
rangeEnd: string;
|
|
156
174
|
rangeInvalid: string;
|
|
175
|
+
/**
|
|
176
|
+
* A day range includes BOTH endpoints, which a reader cannot tell from two
|
|
177
|
+
* dates alone — and getting it wrong silently drops or adds a day's rows.
|
|
178
|
+
*/
|
|
179
|
+
rangeInclusiveNote: string;
|
|
157
180
|
/** The day-range presets' labels, keyed by preset id. */
|
|
158
181
|
rangePresets: Readonly<Record<string, string>>;
|
|
159
182
|
scopesLabel: string;
|
|
@@ -208,6 +231,8 @@ export interface DataViewsNavCopy {
|
|
|
208
231
|
reset: string;
|
|
209
232
|
/** The inline error when a view could not be refreshed or saved. */
|
|
210
233
|
updateFailed: string;
|
|
234
|
+
/** The dismiss on that failure's alert — a glyph with no visible label. */
|
|
235
|
+
dismissError: string;
|
|
211
236
|
saveFailed: string;
|
|
212
237
|
/** Reopen the save dialog over an existing view. */
|
|
213
238
|
editView: string;
|
|
@@ -151,7 +151,7 @@ export function SortTab({
|
|
|
151
151
|
</Box>
|
|
152
152
|
<Text variant="caption" as="p">
|
|
153
153
|
<Box component="span" sx={{ px: 0.5, color: "text.secondary" }}>
|
|
154
|
-
|
|
154
|
+
{copy.display.fieldHeading}
|
|
155
155
|
</Box>
|
|
156
156
|
</Text>
|
|
157
157
|
{fields.map((field) => (
|
|
@@ -326,7 +326,7 @@ export function DisplayTab({ testIdPrefix }: { testIdPrefix: string }): React.JS
|
|
|
326
326
|
<Box sx={{ p: 1 }}>
|
|
327
327
|
<Text variant="caption" as="p">
|
|
328
328
|
<Box component="span" sx={{ px: 0.5, color: "text.secondary" }}>
|
|
329
|
-
|
|
329
|
+
{copy.display.formatHeading}
|
|
330
330
|
</Box>
|
|
331
331
|
</Text>
|
|
332
332
|
<Box
|
|
@@ -110,7 +110,7 @@ function ExportTrigger({
|
|
|
110
110
|
{busy ? <CircularProgress size={14} /> : <DownloadRoundedIcon fontSize="small" />}
|
|
111
111
|
{!compact && (
|
|
112
112
|
<Box component="span" sx={{ display: { xs: "none", sm: "inline" } }}>
|
|
113
|
-
|
|
113
|
+
{copy.export.triggerLabel}
|
|
114
114
|
</Box>
|
|
115
115
|
)}
|
|
116
116
|
{/* Dropped with the label — see the note on the Exibir trigger. */}
|
|
@@ -7,6 +7,7 @@ import GridViewOutlinedIcon from "@mui/icons-material/GridViewOutlined";
|
|
|
7
7
|
import { Slider } from "../../form/Slider";
|
|
8
8
|
import { Box } from "../../../mui/Box";
|
|
9
9
|
|
|
10
|
+
import { useDataViewsCopy } from "./data-views-copy-context";
|
|
10
11
|
import { DATA_VIEWS_LAYOUTS, type DataViewsLayout } from "./data-views-types";
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -322,6 +323,7 @@ export function useDataViewsLayout(): DataViewsLayoutValue {
|
|
|
322
323
|
*/
|
|
323
324
|
export function DataViewsZoomSlider({ testIdPrefix }: { testIdPrefix: string }): React.JSX.Element | null {
|
|
324
325
|
const { layout, zoom, setZoom } = useDataViewsLayout();
|
|
326
|
+
const copy = useDataViewsCopy();
|
|
325
327
|
if (layout !== "cards" && layout !== "board") return null;
|
|
326
328
|
return (
|
|
327
329
|
<Box
|
|
@@ -342,7 +344,7 @@ export function DataViewsZoomSlider({ testIdPrefix }: { testIdPrefix: string }):
|
|
|
342
344
|
max={100}
|
|
343
345
|
step={5}
|
|
344
346
|
onChange={(_event, next) => setZoom(Array.isArray(next) ? (next[0] ?? 0) : next)}
|
|
345
|
-
aria-label=
|
|
347
|
+
aria-label={copy.display.cardZoom}
|
|
346
348
|
data-testid={`${testIdPrefix}-card-zoom`}
|
|
347
349
|
/>
|
|
348
350
|
</Box>
|
|
@@ -86,7 +86,7 @@ function MoreTrigger({
|
|
|
86
86
|
<FilterAltRoundedIcon fontSize="small" />
|
|
87
87
|
{!compact && (
|
|
88
88
|
<Box component="span" sx={{ display: { xs: "none", sm: "inline" } }}>
|
|
89
|
-
|
|
89
|
+
{copy.filters.moreTriggerLabel}
|
|
90
90
|
</Box>
|
|
91
91
|
)}
|
|
92
92
|
<Box
|
|
@@ -126,7 +126,7 @@ function MoreHeading(): React.JSX.Element {
|
|
|
126
126
|
>
|
|
127
127
|
<Text variant="caption" as="span">
|
|
128
128
|
<Box component="span" sx={{ textTransform: "uppercase", letterSpacing: 0.5, color: "text.disabled" }}>
|
|
129
|
-
|
|
129
|
+
{copy.filters.moreHeading}
|
|
130
130
|
</Box>
|
|
131
131
|
</Text>
|
|
132
132
|
<Text variant="caption" as="span">
|
|
@@ -133,7 +133,7 @@ export function RangeBounds<T extends Record<string, unknown>>({
|
|
|
133
133
|
data-testid={`${testId}-inclusive`}
|
|
134
134
|
sx={{ mt: 0.75, fontSize: '0.75rem', color: 'text.disabled' }}
|
|
135
135
|
>
|
|
136
|
-
|
|
136
|
+
{copy.filters.rangeInclusiveNote}
|
|
137
137
|
</Typography>
|
|
138
138
|
)}
|
|
139
139
|
</Box>
|
|
@@ -3,6 +3,7 @@ import React from 'react';
|
|
|
3
3
|
|
|
4
4
|
import { makeTestId } from './Lightbox.constants';
|
|
5
5
|
import { resolveLightboxProps } from './Lightbox.helpers';
|
|
6
|
+
import { LightboxCopyProvider } from './lightbox-copy-context';
|
|
6
7
|
import type { LightboxProps, LightboxRef } from './Lightbox.types';
|
|
7
8
|
import { LightboxCurrentMedia } from './LightboxCurrentMedia';
|
|
8
9
|
import { LightboxOverlay } from './LightboxOverlay';
|
|
@@ -32,6 +33,7 @@ export const Lightbox = React.forwardRef<LightboxRef, LightboxProps>((componentP
|
|
|
32
33
|
const { currentIndex, currentItem, zoom } = lightbox;
|
|
33
34
|
|
|
34
35
|
return (
|
|
36
|
+
<LightboxCopyProvider copy={props.copy}>
|
|
35
37
|
<Dialog
|
|
36
38
|
open={isOpen}
|
|
37
39
|
onClose={lightbox.close}
|
|
@@ -98,6 +100,7 @@ export const Lightbox = React.forwardRef<LightboxRef, LightboxProps>((componentP
|
|
|
98
100
|
}
|
|
99
101
|
/>
|
|
100
102
|
</Dialog>
|
|
103
|
+
</LightboxCopyProvider>
|
|
101
104
|
);
|
|
102
105
|
});
|
|
103
106
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CSSProperties } from 'react';
|
|
2
|
+
import type { LightboxCopy } from '../../../copy';
|
|
2
3
|
|
|
3
4
|
export interface LightboxItem {
|
|
4
5
|
src: string;
|
|
@@ -12,6 +13,14 @@ export interface AutoplayConfig {
|
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export interface LightboxProps {
|
|
16
|
+
/**
|
|
17
|
+
* Every word this viewer renders — all of them aria-labels on glyph-only
|
|
18
|
+
* controls, so this object IS what a screen-reader user hears for the whole
|
|
19
|
+
* component. REQUIRED: a default could only be one language read aloud to
|
|
20
|
+
* everybody else's users, and the sighted path looks identical either way,
|
|
21
|
+
* which is why the English literals survived so long.
|
|
22
|
+
*/
|
|
23
|
+
copy: LightboxCopy;
|
|
15
24
|
/** Controls visibility; must lock body scroll when true */
|
|
16
25
|
isOpen: boolean;
|
|
17
26
|
/** Fired on close button, backdrop click, Esc, and swipe-down (mobile) */
|
|
@@ -11,22 +11,26 @@ import type { FC } from 'react';
|
|
|
11
11
|
import React from 'react';
|
|
12
12
|
|
|
13
13
|
import { navButtonSx, OVERLAY_Z_INDEX, overlayButtonSx } from './Lightbox.constants';
|
|
14
|
+
import { useLightboxCopy } from './lightbox-copy-context';
|
|
14
15
|
|
|
15
16
|
type TestId = (suffix: string) => string;
|
|
16
17
|
|
|
17
18
|
export const LightboxCloseButton: FC<{ testId: TestId; onClose: () => void }> = ({
|
|
18
19
|
testId,
|
|
19
20
|
onClose,
|
|
20
|
-
}) =>
|
|
21
|
+
}) => {
|
|
22
|
+
const copy = useLightboxCopy();
|
|
23
|
+
return (
|
|
21
24
|
<IconButton
|
|
22
25
|
onClick={onClose}
|
|
23
26
|
sx={{ ...overlayButtonSx, position: 'absolute', top: 16, right: 16, zIndex: OVERLAY_Z_INDEX }}
|
|
24
|
-
aria-label=
|
|
27
|
+
aria-label={copy.close}
|
|
25
28
|
data-testid={testId('close')}
|
|
26
29
|
>
|
|
27
30
|
<CloseIcon />
|
|
28
31
|
</IconButton>
|
|
29
|
-
);
|
|
32
|
+
);
|
|
33
|
+
};
|
|
30
34
|
|
|
31
35
|
const arrowSx = (side: 'left' | 'right') =>
|
|
32
36
|
({
|
|
@@ -52,13 +56,15 @@ export const LightboxNavButtons: FC<LightboxNavButtonsProps> = ({
|
|
|
52
56
|
atEnd,
|
|
53
57
|
onPrev,
|
|
54
58
|
onNext,
|
|
55
|
-
}) =>
|
|
59
|
+
}) => {
|
|
60
|
+
const copy = useLightboxCopy();
|
|
61
|
+
return (
|
|
56
62
|
<>
|
|
57
63
|
<IconButton
|
|
58
64
|
onClick={onPrev}
|
|
59
65
|
disabled={atStart}
|
|
60
66
|
sx={arrowSx('left')}
|
|
61
|
-
aria-label=
|
|
67
|
+
aria-label={copy.previous}
|
|
62
68
|
data-testid={testId('prev')}
|
|
63
69
|
>
|
|
64
70
|
<PrevIcon />
|
|
@@ -68,13 +74,14 @@ export const LightboxNavButtons: FC<LightboxNavButtonsProps> = ({
|
|
|
68
74
|
onClick={onNext}
|
|
69
75
|
disabled={atEnd}
|
|
70
76
|
sx={arrowSx('right')}
|
|
71
|
-
aria-label=
|
|
77
|
+
aria-label={copy.next}
|
|
72
78
|
data-testid={testId('next')}
|
|
73
79
|
>
|
|
74
80
|
<NextIcon />
|
|
75
81
|
</IconButton>
|
|
76
82
|
</>
|
|
77
|
-
);
|
|
83
|
+
);
|
|
84
|
+
};
|
|
78
85
|
|
|
79
86
|
export interface LightboxZoomControlsProps {
|
|
80
87
|
testId: TestId;
|
|
@@ -88,7 +95,9 @@ export const LightboxZoomControls: FC<LightboxZoomControlsProps> = ({
|
|
|
88
95
|
onZoomIn,
|
|
89
96
|
onZoomOut,
|
|
90
97
|
onResetZoom,
|
|
91
|
-
}) =>
|
|
98
|
+
}) => {
|
|
99
|
+
const copy = useLightboxCopy();
|
|
100
|
+
return (
|
|
92
101
|
<Box
|
|
93
102
|
sx={{
|
|
94
103
|
position: 'absolute',
|
|
@@ -101,13 +110,13 @@ export const LightboxZoomControls: FC<LightboxZoomControlsProps> = ({
|
|
|
101
110
|
}}
|
|
102
111
|
data-testid={testId('zoom-controls')}
|
|
103
112
|
>
|
|
104
|
-
<IconButton onClick={onZoomIn} sx={overlayButtonSx} aria-label=
|
|
113
|
+
<IconButton onClick={onZoomIn} sx={overlayButtonSx} aria-label={copy.zoomIn} data-testid={testId('zoom-in')}>
|
|
105
114
|
<ZoomInIcon />
|
|
106
115
|
</IconButton>
|
|
107
116
|
<IconButton
|
|
108
117
|
onClick={onZoomOut}
|
|
109
118
|
sx={overlayButtonSx}
|
|
110
|
-
aria-label=
|
|
119
|
+
aria-label={copy.zoomOut}
|
|
111
120
|
data-testid={testId('zoom-out')}
|
|
112
121
|
>
|
|
113
122
|
<ZoomOutIcon />
|
|
@@ -115,13 +124,14 @@ export const LightboxZoomControls: FC<LightboxZoomControlsProps> = ({
|
|
|
115
124
|
<IconButton
|
|
116
125
|
onClick={onResetZoom}
|
|
117
126
|
sx={overlayButtonSx}
|
|
118
|
-
aria-label=
|
|
127
|
+
aria-label={copy.resetZoom}
|
|
119
128
|
data-testid={testId('zoom-reset')}
|
|
120
129
|
>
|
|
121
130
|
<ResetZoomIcon />
|
|
122
131
|
</IconButton>
|
|
123
132
|
</Box>
|
|
124
|
-
);
|
|
133
|
+
);
|
|
134
|
+
};
|
|
125
135
|
|
|
126
136
|
export interface LightboxAutoplayButtonProps {
|
|
127
137
|
testId: TestId;
|
|
@@ -133,7 +143,9 @@ export const LightboxAutoplayButton: FC<LightboxAutoplayButtonProps> = ({
|
|
|
133
143
|
testId,
|
|
134
144
|
isAutoPlaying,
|
|
135
145
|
onToggle,
|
|
136
|
-
}) =>
|
|
146
|
+
}) => {
|
|
147
|
+
const copy = useLightboxCopy();
|
|
148
|
+
return (
|
|
137
149
|
<IconButton
|
|
138
150
|
onClick={onToggle}
|
|
139
151
|
sx={{
|
|
@@ -144,9 +156,10 @@ export const LightboxAutoplayButton: FC<LightboxAutoplayButtonProps> = ({
|
|
|
144
156
|
transform: 'translateX(-50%)',
|
|
145
157
|
zIndex: OVERLAY_Z_INDEX,
|
|
146
158
|
}}
|
|
147
|
-
aria-label={isAutoPlaying ?
|
|
159
|
+
aria-label={isAutoPlaying ? copy.pause : copy.play}
|
|
148
160
|
data-testid={testId('autoplay')}
|
|
149
161
|
>
|
|
150
162
|
{isAutoPlaying ? <PauseIcon /> : <PlayIcon />}
|
|
151
163
|
</IconButton>
|
|
152
|
-
);
|
|
164
|
+
);
|
|
165
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, type ReactNode } from 'react';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
|
|
6
|
+
import type { LightboxCopy } from '../../../copy';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The lightbox's words, for the four control components that render them.
|
|
10
|
+
*
|
|
11
|
+
* A CONTEXT rather than props: the viewer is `Lightbox` → `LightboxOverlay` →
|
|
12
|
+
* three separate control clusters, and threading one object down three hops is
|
|
13
|
+
* how a copy port comes to exist, be required, and go unread. It stays a
|
|
14
|
+
* single REQUIRED prop at the mount, which is the only place a host answers.
|
|
15
|
+
*/
|
|
16
|
+
const LightboxCopyContext = createContext<LightboxCopy | null>(null);
|
|
17
|
+
|
|
18
|
+
export function LightboxCopyProvider({
|
|
19
|
+
copy,
|
|
20
|
+
children,
|
|
21
|
+
}: {
|
|
22
|
+
copy: LightboxCopy;
|
|
23
|
+
children: ReactNode;
|
|
24
|
+
}): React.JSX.Element {
|
|
25
|
+
return <LightboxCopyContext.Provider value={copy}>{children}</LightboxCopyContext.Provider>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The words this viewer renders — THROWS outside a provider rather than
|
|
30
|
+
* falling back.
|
|
31
|
+
*
|
|
32
|
+
* Every one of them is an aria-label on a glyph, so a fallback would be the
|
|
33
|
+
* one failure nobody sees in review: the sighted path renders identically
|
|
34
|
+
* whichever language it is in.
|
|
35
|
+
*/
|
|
36
|
+
export function useLightboxCopy(): LightboxCopy {
|
|
37
|
+
const copy = useContext(LightboxCopyContext);
|
|
38
|
+
if (!copy) {
|
|
39
|
+
throw new Error('useLightboxCopy must be rendered inside a <LightboxCopyProvider>');
|
|
40
|
+
}
|
|
41
|
+
return copy;
|
|
42
|
+
}
|
|
@@ -9,6 +9,7 @@ import React from 'react';
|
|
|
9
9
|
|
|
10
10
|
import { MAX_ZOOM, MIN_ZOOM } from './MapPreview.constants';
|
|
11
11
|
import type { MapType } from './MapPreview.types';
|
|
12
|
+
import type { MapPreviewCopy } from '../../../copy';
|
|
12
13
|
|
|
13
14
|
// Kept module-local: the inferred type of a styled() component cannot be named
|
|
14
15
|
// across a module boundary here (TS2742).
|
|
@@ -41,6 +42,12 @@ const ControlButton = styled(IconButton)(({ theme }) => ({
|
|
|
41
42
|
}));
|
|
42
43
|
|
|
43
44
|
export interface MapControlBarProps {
|
|
45
|
+
/**
|
|
46
|
+
* The six controls' names. Every one is a glyph with a `title` AND an
|
|
47
|
+
* `aria-label`, so this object is both the tooltip and what a screen reader
|
|
48
|
+
* reads — REQUIRED, because a default is one language for every adopter.
|
|
49
|
+
*/
|
|
50
|
+
copy: MapPreviewCopy;
|
|
44
51
|
zoom: number;
|
|
45
52
|
mapType: MapType;
|
|
46
53
|
onZoomIn: () => void;
|
|
@@ -51,6 +58,7 @@ export interface MapControlBarProps {
|
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
export const MapControlBar: FC<MapControlBarProps> = ({
|
|
61
|
+
copy,
|
|
54
62
|
zoom,
|
|
55
63
|
mapType,
|
|
56
64
|
onZoomIn,
|
|
@@ -60,33 +68,33 @@ export const MapControlBar: FC<MapControlBarProps> = ({
|
|
|
60
68
|
onToggleFullscreen,
|
|
61
69
|
}) => (
|
|
62
70
|
<MapControls data-testid="map-controls">
|
|
63
|
-
<Tooltip title=
|
|
71
|
+
<Tooltip title={copy.zoomIn} placement="left">
|
|
64
72
|
<ControlButton
|
|
65
73
|
size="small"
|
|
66
74
|
onClick={onZoomIn}
|
|
67
|
-
aria-label=
|
|
75
|
+
aria-label={copy.zoomIn}
|
|
68
76
|
data-testid="zoom-in-button"
|
|
69
77
|
disabled={zoom >= MAX_ZOOM}
|
|
70
78
|
>
|
|
71
79
|
<ZoomInIcon fontSize="small" />
|
|
72
80
|
</ControlButton>
|
|
73
81
|
</Tooltip>
|
|
74
|
-
<Tooltip title=
|
|
82
|
+
<Tooltip title={copy.zoomOut} placement="left">
|
|
75
83
|
<ControlButton
|
|
76
84
|
size="small"
|
|
77
85
|
onClick={onZoomOut}
|
|
78
|
-
aria-label=
|
|
86
|
+
aria-label={copy.zoomOut}
|
|
79
87
|
data-testid="zoom-out-button"
|
|
80
88
|
disabled={zoom <= MIN_ZOOM}
|
|
81
89
|
>
|
|
82
90
|
<ZoomOutIcon fontSize="small" />
|
|
83
91
|
</ControlButton>
|
|
84
92
|
</Tooltip>
|
|
85
|
-
<Tooltip title=
|
|
93
|
+
<Tooltip title={copy.center} placement="left">
|
|
86
94
|
<ControlButton
|
|
87
95
|
size="small"
|
|
88
96
|
onClick={onCenter}
|
|
89
|
-
aria-label=
|
|
97
|
+
aria-label={copy.center}
|
|
90
98
|
data-testid="center-button"
|
|
91
99
|
>
|
|
92
100
|
<CenterIcon fontSize="small" />
|
|
@@ -96,17 +104,17 @@ export const MapControlBar: FC<MapControlBarProps> = ({
|
|
|
96
104
|
<ControlButton
|
|
97
105
|
size="small"
|
|
98
106
|
onClick={onToggleMapType}
|
|
99
|
-
aria-label=
|
|
107
|
+
aria-label={copy.mapType}
|
|
100
108
|
data-testid="map-type-button"
|
|
101
109
|
>
|
|
102
110
|
<LayersIcon fontSize="small" />
|
|
103
111
|
</ControlButton>
|
|
104
112
|
</Tooltip>
|
|
105
|
-
<Tooltip title=
|
|
113
|
+
<Tooltip title={copy.fullscreen} placement="left">
|
|
106
114
|
<ControlButton
|
|
107
115
|
size="small"
|
|
108
116
|
onClick={onToggleFullscreen}
|
|
109
|
-
aria-label=
|
|
117
|
+
aria-label={copy.fullscreen}
|
|
110
118
|
data-testid="fullscreen-button"
|
|
111
119
|
>
|
|
112
120
|
<FullscreenIcon fontSize="small" />
|
|
@@ -46,6 +46,7 @@ export const MapLayers: FC<MapLayersProps> = ({ settings, zoom, mapType, search,
|
|
|
46
46
|
|
|
47
47
|
{settings.showSearch && (
|
|
48
48
|
<MapSearchBar
|
|
49
|
+
label={settings.copy.search}
|
|
49
50
|
value={search.searchValue}
|
|
50
51
|
placeholder={settings.searchPlaceholder}
|
|
51
52
|
isSearching={search.isSearching}
|
|
@@ -56,6 +57,7 @@ export const MapLayers: FC<MapLayersProps> = ({ settings, zoom, mapType, search,
|
|
|
56
57
|
|
|
57
58
|
{settings.showControls && (
|
|
58
59
|
<MapControlBar
|
|
60
|
+
copy={settings.copy}
|
|
59
61
|
zoom={zoom}
|
|
60
62
|
mapType={mapType}
|
|
61
63
|
onZoomIn={controls.handleZoomIn}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { MapPreviewCopy } from '../../../copy';
|
|
2
|
+
|
|
1
3
|
export type MapType = 'roadmap' | 'satellite' | 'hybrid' | 'terrain';
|
|
2
4
|
|
|
3
5
|
export interface LatLng {
|
|
@@ -20,6 +22,14 @@ export interface HeatmapPoint {
|
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
export interface MapPreviewProps {
|
|
25
|
+
/**
|
|
26
|
+
* The control bar's six names and the search field's own. Every control is a
|
|
27
|
+
* glyph carrying a `title` and an `aria-label`, so this is both the tooltip
|
|
28
|
+
* and what a screen reader reads. REQUIRED — `searchPlaceholder` beside it
|
|
29
|
+
* shows the shape this replaces: an English default that read as
|
|
30
|
+
* configurable right up until nobody configured it.
|
|
31
|
+
*/
|
|
32
|
+
copy: MapPreviewCopy;
|
|
23
33
|
/** Map center coordinates */
|
|
24
34
|
center?: { lat: number; lng: number };
|
|
25
35
|
/** Legacy coordinates prop (use center instead) */
|