@eifi1/ui-kit 0.51.7 → 0.52.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 +141 -23
- package/dist/account/account-parts.d.ts +10 -8
- package/dist/account/account-parts.js +1 -18
- package/dist/account/account-parts.js.map +1 -1
- package/dist/account/data-export-setting-labels.js +1 -1
- package/dist/account/data-export-setting-labels.js.map +1 -1
- package/dist/account/delete-account-setting-labels.js +1 -1
- package/dist/account/delete-account-setting-labels.js.map +1 -1
- package/dist/account/email-change-setting-labels.js +1 -1
- package/dist/account/email-change-setting-labels.js.map +1 -1
- package/dist/account/passkeys-setting.d.ts +9 -2
- package/dist/account/passkeys-setting.js +41 -29
- package/dist/account/passkeys-setting.js.map +1 -1
- package/dist/account/sessions-setting.d.ts +3 -2
- package/dist/account/sessions-setting.js +10 -8
- package/dist/account/sessions-setting.js.map +1 -1
- package/dist/admin/admin-action-confirm.js +1 -3
- package/dist/admin/admin-action-confirm.js.map +1 -1
- package/dist/admin/admin-parts.d.ts +6 -3
- package/dist/admin/admin-parts.js +6 -21
- package/dist/admin/admin-parts.js.map +1 -1
- package/dist/admin/mail-test-card-labels.js +1 -1
- package/dist/admin/mail-test-card-labels.js.map +1 -1
- package/dist/admin/plan-change-labels.d.ts +2 -2
- package/dist/admin/plan-change-labels.js +1 -17
- package/dist/admin/plan-change-labels.js.map +1 -1
- package/dist/admin/plan-change.d.ts +3 -42
- package/dist/admin/plan-change.js +1 -1
- package/dist/admin/plan-change.js.map +1 -1
- package/dist/admin/reviewer-scope-editor.js +3 -3
- package/dist/admin/reviewer-scope-editor.js.map +1 -1
- package/dist/auth/auth-errors-labels.d.ts +11 -0
- package/dist/auth/auth-errors-labels.js +13 -0
- package/dist/auth/auth-errors-labels.js.map +1 -0
- package/dist/auth/auth-errors.d.ts +7 -23
- package/dist/auth/auth-errors.js +2 -15
- package/dist/auth/auth-errors.js.map +1 -1
- package/dist/auth/complete-name-dialog.js +2 -1
- package/dist/auth/complete-name-dialog.js.map +1 -1
- package/dist/auth/forgot-password-form-labels.js +1 -1
- package/dist/auth/forgot-password-form-labels.js.map +1 -1
- package/dist/auth/forgot-password-form.js +7 -2
- package/dist/auth/forgot-password-form.js.map +1 -1
- package/dist/auth/register-form-labels.js +1 -1
- package/dist/auth/register-form-labels.js.map +1 -1
- package/dist/auth/reset-password-form-labels.js +1 -1
- package/dist/auth/reset-password-form-labels.js.map +1 -1
- package/dist/auth/reset-password-form.js +5 -1
- package/dist/auth/reset-password-form.js.map +1 -1
- package/dist/auth/sign-in-form-labels.js +1 -1
- package/dist/auth/sign-in-form-labels.js.map +1 -1
- package/dist/auth/status-parts.d.ts +0 -3
- package/dist/auth/status-parts.js +0 -4
- package/dist/auth/status-parts.js.map +1 -1
- package/dist/auth/verify-email-labels.js +1 -1
- package/dist/auth/verify-email-labels.js.map +1 -1
- package/dist/auth/verify-email.js +7 -2
- package/dist/auth/verify-email.js.map +1 -1
- package/dist/billing/cancel-subscription.d.ts +20 -8
- package/dist/billing/cancel-subscription.js +7 -5
- package/dist/billing/cancel-subscription.js.map +1 -1
- package/dist/billing/dialog-close.d.ts +29 -0
- package/dist/billing/dialog-close.js +8 -0
- package/dist/billing/dialog-close.js.map +1 -0
- package/dist/billing/plan-card.d.ts +14 -4
- package/dist/billing/plan-card.js +15 -8
- package/dist/billing/plan-card.js.map +1 -1
- package/dist/billing/plan-change-labels.d.ts +42 -0
- package/dist/billing/plan-change-labels.js +22 -0
- package/dist/billing/plan-change-labels.js.map +1 -0
- package/dist/billing/plan-switch.d.ts +19 -7
- package/dist/billing/plan-switch.js +8 -4
- package/dist/billing/plan-switch.js.map +1 -1
- package/dist/chart.d.ts +12 -0
- package/dist/chart.js +16 -1
- package/dist/chart.js.map +1 -1
- package/dist/components/account-chips.d.ts +5 -5
- package/dist/components/account-chips.js +11 -13
- package/dist/components/account-chips.js.map +1 -1
- package/dist/components/alert-banner.d.ts +16 -5
- package/dist/components/alert-banner.js +46 -1
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +13 -0
- package/dist/components/amount-input.js +1 -0
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.js +57 -65
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/bulk-action-bar.d.ts +12 -2
- package/dist/components/bulk-action-bar.js +65 -21
- package/dist/components/bulk-action-bar.js.map +1 -1
- package/dist/components/button-look.d.ts +5 -5
- package/dist/components/button-look.js.map +1 -1
- package/dist/components/button.d.ts +2 -2
- package/dist/components/button.js.map +1 -1
- package/dist/components/chart-edge.d.ts +16 -0
- package/dist/components/chart-edge.js +14 -1
- package/dist/components/chart-edge.js.map +1 -1
- package/dist/components/chart-figure.d.ts +13 -0
- package/dist/components/chart-figure.js +15 -0
- package/dist/components/chart-figure.js.map +1 -0
- package/dist/components/chart-zoom-core.js +1 -4
- package/dist/components/chart-zoom-core.js.map +1 -1
- package/dist/components/chart.d.ts +8 -1
- package/dist/components/chart.js +19 -10
- package/dist/components/chart.js.map +1 -1
- package/dist/components/combobox-core.d.ts +111 -2
- package/dist/components/combobox-core.js +148 -42
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +1 -1
- package/dist/components/combobox.js +141 -130
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/copy-button.d.ts +6 -5
- package/dist/components/copy-button.js +5 -3
- package/dist/components/copy-button.js.map +1 -1
- package/dist/components/country-select.js +2 -2
- package/dist/components/country-select.js.map +1 -1
- package/dist/components/data-table-column-settings.d.ts +1 -1
- package/dist/components/data-table-column-settings.js +33 -7
- package/dist/components/data-table-column-settings.js.map +1 -1
- package/dist/components/data-table-filters.d.ts +4 -2
- package/dist/components/data-table-filters.js +2 -2
- package/dist/components/data-table-filters.js.map +1 -1
- package/dist/components/data-table-fit.d.ts +6 -4
- package/dist/components/data-table-fit.js +4 -1
- package/dist/components/data-table-fit.js.map +1 -1
- package/dist/components/data-table-groups.d.ts +1 -1
- package/dist/components/data-table-groups.js.map +1 -1
- package/dist/components/data-table-head.d.ts +1 -1
- package/dist/components/data-table-head.js +15 -12
- package/dist/components/data-table-head.js.map +1 -1
- package/dist/components/data-table-mobile.d.ts +12 -7
- package/dist/components/data-table-mobile.js +43 -30
- package/dist/components/data-table-mobile.js.map +1 -1
- package/dist/components/data-table-pagination.js +11 -6
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table-resize.d.ts +1 -1
- package/dist/components/data-table-resize.js.map +1 -1
- package/dist/components/data-table-row-actions.d.ts +2 -2
- package/dist/components/data-table-row-actions.js.map +1 -1
- package/dist/components/data-table-row.d.ts +6 -2
- package/dist/components/data-table-row.js +17 -10
- package/dist/components/data-table-row.js.map +1 -1
- package/dist/components/data-table-selection.d.ts +1 -1
- package/dist/components/data-table-selection.js.map +1 -1
- package/dist/components/data-table-shared.d.ts +10 -2
- package/dist/components/data-table-shared.js +18 -0
- package/dist/components/data-table-shared.js.map +1 -1
- package/dist/components/data-table-sort.d.ts +21 -2
- package/dist/components/data-table-sort.js +24 -0
- package/dist/components/data-table-sort.js.map +1 -1
- package/dist/components/data-table-virtual.d.ts +1 -1
- package/dist/components/data-table-virtual.js +18 -6
- package/dist/components/data-table-virtual.js.map +1 -1
- package/dist/components/data-table.d.ts +13 -4
- package/dist/components/data-table.js +139 -36
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-field.d.ts +24 -2
- package/dist/components/date-field.js +33 -8
- package/dist/components/date-field.js.map +1 -1
- package/dist/components/date-picker.d.ts +15 -2
- package/dist/components/date-picker.js +55 -26
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/date-range-picker.d.ts +47 -20
- package/dist/components/date-range-picker.js +23 -7
- package/dist/components/date-range-picker.js.map +1 -1
- package/dist/components/error-boundary.js +37 -30
- package/dist/components/error-boundary.js.map +1 -1
- package/dist/components/{ui-labels.d.ts → field-base-labels.d.ts} +0 -2
- package/dist/components/{ui-labels.js → field-base-labels.js} +2 -9
- package/dist/components/field-base-labels.js.map +1 -0
- package/dist/components/field-base.d.ts +4 -3
- package/dist/components/field-base.js +3 -3
- package/dist/components/field-base.js.map +1 -1
- package/dist/components/field-sync-labels.d.ts +1 -0
- package/dist/components/field-sync-labels.js.map +1 -1
- package/dist/components/field-sync.d.ts +23 -1
- package/dist/components/field-sync.js.map +1 -1
- package/dist/components/form-actions.js +11 -5
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/hover-menu.d.ts +3 -0
- package/dist/components/hover-menu.js +5 -3
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/icon-button.js +14 -9
- package/dist/components/icon-button.js.map +1 -1
- package/dist/components/inline-confirm.d.ts +43 -0
- package/dist/components/inline-confirm.js +67 -0
- package/dist/components/inline-confirm.js.map +1 -0
- package/dist/components/inline-entity-combobox.d.ts +4 -3
- package/dist/components/inline-entity-combobox.js +148 -140
- package/dist/components/inline-entity-combobox.js.map +1 -1
- package/dist/components/input.js +1 -1
- package/dist/components/input.js.map +1 -1
- package/dist/components/jump-calendar.js +28 -112
- package/dist/components/jump-calendar.js.map +1 -1
- package/dist/components/legal-page.d.ts +3 -1
- package/dist/components/legal-page.js +3 -2
- package/dist/components/legal-page.js.map +1 -1
- package/dist/components/legal.d.ts +4 -2
- package/dist/components/legal.js.map +1 -1
- package/dist/components/lightbox.d.ts +2 -2
- package/dist/components/lightbox.js +17 -6
- package/dist/components/lightbox.js.map +1 -1
- package/dist/components/list.d.ts +6 -2
- package/dist/components/list.js +3 -2
- package/dist/components/list.js.map +1 -1
- package/dist/components/measured-grid.d.ts +1 -1
- package/dist/components/measured-grid.js +2 -1
- package/dist/components/measured-grid.js.map +1 -1
- package/dist/components/mini-calendar.js +3 -5
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/modal.js +3 -1
- package/dist/components/modal.js.map +1 -1
- package/dist/components/month-grid.d.ts +55 -0
- package/dist/components/month-grid.js +148 -0
- package/dist/components/month-grid.js.map +1 -0
- package/dist/components/month-picker.d.ts +36 -4
- package/dist/components/month-picker.js +123 -146
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-select.d.ts +2 -2
- package/dist/components/multi-select.js +11 -6
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/picker-sheet.js +2 -1
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/pie-chart-labels.d.ts +7 -0
- package/dist/components/pie-chart-labels.js +1 -0
- package/dist/components/pie-chart-labels.js.map +1 -1
- package/dist/components/pie-chart.d.ts +5 -3
- package/dist/components/pie-chart.js +7 -22
- package/dist/components/pie-chart.js.map +1 -1
- package/dist/components/popover.d.ts +3 -0
- package/dist/components/popover.js +7 -1
- package/dist/components/popover.js.map +1 -1
- package/dist/components/row-actions.js +2 -1
- package/dist/components/row-actions.js.map +1 -1
- package/dist/components/section-nav.d.ts +7 -0
- package/dist/components/section-nav.js.map +1 -1
- package/dist/components/series-chart-axes.d.ts +3 -3
- package/dist/components/series-chart-axes.js.map +1 -1
- package/dist/components/series-chart-labels.d.ts +7 -4
- package/dist/components/series-chart-labels.js.map +1 -1
- package/dist/components/series-chart.d.ts +31 -7
- package/dist/components/series-chart.js +72 -29
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/share-card.js +3 -3
- package/dist/components/share-card.js.map +1 -1
- package/dist/components/sign-chip-labels.d.ts +1 -0
- package/dist/components/sign-chip-labels.js.map +1 -1
- package/dist/components/sign-chip.d.ts +10 -1
- package/dist/components/sign-chip.js.map +1 -1
- package/dist/components/tabs-labels.d.ts +2 -0
- package/dist/components/tabs-labels.js +11 -0
- package/dist/components/tabs-labels.js.map +1 -0
- package/dist/components/tabs.d.ts +1 -1
- package/dist/components/tabs.js +1 -1
- package/dist/components/tabs.js.map +1 -1
- package/dist/components/toggle-legend.d.ts +2 -2
- package/dist/components/toggle-legend.js +6 -6
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/translation-review-editor.d.ts +4 -2
- package/dist/components/translation-review-editor.js +2 -3
- package/dist/components/translation-review-editor.js.map +1 -1
- package/dist/components/translation-review.d.ts +2 -1
- package/dist/components/translation-review.js +2 -2
- package/dist/components/translation-review.js.map +1 -1
- package/dist/components/treemap-labels.d.ts +26 -0
- package/dist/components/treemap-labels.js +10 -0
- package/dist/components/treemap-labels.js.map +1 -0
- package/dist/components/treemap-layout.d.ts +62 -0
- package/dist/components/treemap-layout.js +126 -0
- package/dist/components/treemap-layout.js.map +1 -0
- package/dist/components/treemap.d.ts +140 -17
- package/dist/components/treemap.js +324 -97
- package/dist/components/treemap.js.map +1 -1
- package/dist/components/use-table-state.d.ts +5 -5
- package/dist/components/use-table-state.js +4 -1
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/data-table.d.ts +6 -2
- package/dist/data-table.js +7 -2
- package/dist/data-table.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/eslint/role-utilities.d.ts +36 -7
- package/dist/eslint/role-utilities.js +42 -9
- package/dist/eslint/role-utilities.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +25 -16
- package/dist/feedback/feedback-dialog.js +6 -2
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-mutation.d.ts +30 -0
- package/dist/feedback/feedback-mutation.js +16 -0
- package/dist/feedback/feedback-mutation.js.map +1 -0
- package/dist/feedback/feedback-page-link.d.ts +53 -0
- package/dist/feedback/feedback-page-link.js +37 -0
- package/dist/feedback/feedback-page-link.js.map +1 -0
- package/dist/feedback/feedback-public-list.d.ts +12 -3
- package/dist/feedback/feedback-public-list.js +2 -1
- package/dist/feedback/feedback-public-list.js.map +1 -1
- package/dist/feedback/feedback-replies.d.ts +3 -1
- package/dist/feedback/feedback-replies.js +5 -4
- package/dist/feedback/feedback-replies.js.map +1 -1
- package/dist/feedback/feedback-row-detail-labels.js +5 -0
- package/dist/feedback/feedback-row-detail-labels.js.map +1 -1
- package/dist/feedback/feedback-row-detail.d.ts +64 -28
- package/dist/feedback/feedback-row-detail.js +30 -28
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback/feedback-status-undo.d.ts +30 -11
- package/dist/feedback/feedback-status-undo.js +10 -15
- package/dist/feedback/feedback-status-undo.js.map +1 -1
- package/dist/feedback/feedback-submit-dialog.d.ts +1 -1
- package/dist/feedback/feedback-submit-dialog.js +1 -1
- package/dist/feedback/feedback-submit-dialog.js.map +1 -1
- package/dist/feedback/feedback-submit.js +9 -3
- package/dist/feedback/feedback-submit.js.map +1 -1
- package/dist/feedback/feedback-swipe.d.ts +1 -1
- package/dist/feedback/feedback-swipe.js.map +1 -1
- package/dist/feedback/feedback-table.d.ts +21 -9
- package/dist/feedback/feedback-table.js +45 -25
- package/dist/feedback/feedback-table.js.map +1 -1
- package/dist/feedback/feedback-thread-labels.d.ts +2 -2
- package/dist/feedback/feedback-thread-labels.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +24 -23
- package/dist/feedback/feedback-thread.js +2 -1
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/feedback.d.ts +34 -0
- package/dist/feedback.js +42 -0
- package/dist/feedback.js.map +1 -1
- package/dist/hooks/use-focus-trap.d.ts +35 -0
- package/dist/hooks/use-focus-trap.js +6 -4
- package/dist/hooks/use-focus-trap.js.map +1 -1
- package/dist/hooks/use-hotkey.d.ts +5 -2
- package/dist/hooks/use-hotkey.js +5 -4
- package/dist/hooks/use-hotkey.js.map +1 -1
- package/dist/hooks/use-long-press.d.ts +7 -4
- package/dist/hooks/use-long-press.js +3 -0
- package/dist/hooks/use-long-press.js.map +1 -1
- package/dist/hooks/use-media-query.js +12 -4
- package/dist/hooks/use-media-query.js.map +1 -1
- package/dist/hooks/use-mounted.d.ts +22 -0
- package/dist/hooks/use-mounted.js +24 -0
- package/dist/hooks/use-mounted.js.map +1 -0
- package/dist/hooks/use-overlay-history.js +19 -3
- package/dist/hooks/use-overlay-history.js.map +1 -1
- package/dist/i18n/defaults.js +4 -5
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.js +7 -1
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +9 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +20 -41
- package/dist/i18n/locales/en.js.map +3 -3
- package/dist/i18n/locales/es.js +7 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +20 -10
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +9 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +7 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +8 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +5 -0
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +64 -17
- package/dist/index.js +77 -12
- package/dist/index.js.map +1 -1
- package/dist/landing/routing.d.ts +3 -1
- package/dist/landing/routing.js +4 -7
- package/dist/landing/routing.js.map +1 -1
- package/dist/lib/broadcast.d.ts +17 -0
- package/dist/lib/broadcast.js +30 -0
- package/dist/lib/broadcast.js.map +1 -0
- package/dist/lib/date-format.d.ts +37 -2
- package/dist/lib/date-format.js +34 -8
- package/dist/lib/date-format.js.map +1 -1
- package/dist/lib/day-format.d.ts +46 -0
- package/dist/lib/day-format.js +46 -0
- package/dist/lib/day-format.js.map +1 -0
- package/dist/lib/describe-failure.d.ts +14 -0
- package/dist/lib/describe-failure.js +9 -0
- package/dist/lib/describe-failure.js.map +1 -0
- package/dist/lib/direction.d.ts +16 -0
- package/dist/lib/direction.js +9 -1
- package/dist/lib/direction.js.map +1 -1
- package/dist/lib/focus-left.d.ts +63 -0
- package/dist/lib/focus-left.js +67 -0
- package/dist/lib/focus-left.js.map +1 -0
- package/dist/lib/format.js +5 -14
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/http-status.d.ts +24 -0
- package/dist/lib/http-status.js +14 -0
- package/dist/lib/http-status.js.map +1 -0
- package/dist/lib/intl-cache.d.ts +14 -0
- package/dist/lib/intl-cache.js +15 -0
- package/dist/lib/intl-cache.js.map +1 -0
- package/dist/lib/isin.d.ts +10 -0
- package/dist/lib/isin.js.map +1 -1
- package/dist/lib/iso-day.d.ts +11 -0
- package/dist/lib/iso-day.js +8 -0
- package/dist/lib/iso-day.js.map +1 -0
- package/dist/lib/lazy-with-fallback.d.ts +31 -0
- package/dist/lib/lazy-with-fallback.js +40 -0
- package/dist/lib/lazy-with-fallback.js.map +1 -0
- package/dist/lib/path-pattern.d.ts +34 -0
- package/dist/lib/path-pattern.js +24 -0
- package/dist/lib/path-pattern.js.map +1 -0
- package/dist/lib/reduced-motion.d.ts +2 -1
- package/dist/lib/reduced-motion.js.map +1 -1
- package/dist/lib/round.d.ts +2 -1
- package/dist/lib/round.js +2 -1
- package/dist/lib/round.js.map +1 -1
- package/dist/lib/safe-storage.d.ts +24 -5
- package/dist/lib/safe-storage.js +27 -2
- package/dist/lib/safe-storage.js.map +1 -1
- package/dist/lib/session-refresh.js +1 -1
- package/dist/lib/session-refresh.js.map +1 -1
- package/dist/lib/share-format.d.ts +13 -0
- package/dist/lib/share-format.js +10 -0
- package/dist/lib/share-format.js.map +1 -0
- package/dist/lib/text-case.d.ts +17 -0
- package/dist/lib/text-case.js +8 -0
- package/dist/lib/text-case.js.map +1 -0
- package/dist/lib/text-match.d.ts +29 -0
- package/dist/lib/text-match.js +20 -2
- package/dist/lib/text-match.js.map +1 -1
- package/dist/lib/thenable.d.ts +13 -0
- package/dist/lib/thenable.js +8 -0
- package/dist/lib/thenable.js.map +1 -0
- package/dist/lib/translation-review.d.ts +7 -2
- package/dist/lib/translation-review.js +4 -4
- package/dist/lib/translation-review.js.map +1 -1
- package/dist/notifications/device-zone-report.js +1 -1
- package/dist/notifications/device-zone-report.js.map +1 -1
- package/dist/notifications/notification-bell.d.ts +2 -1
- package/dist/notifications/notification-bell.js +3 -3
- package/dist/notifications/notification-bell.js.map +1 -1
- package/dist/notifications/notification-inbox.d.ts +10 -6
- package/dist/notifications/notification-inbox.js +26 -48
- package/dist/notifications/notification-inbox.js.map +1 -1
- package/dist/notifications/notification-parts.d.ts +3 -2
- package/dist/notifications/notification-parts.js +1 -3
- package/dist/notifications/notification-parts.js.map +1 -1
- package/dist/notifications/notification-types-setting.js +1 -1
- package/dist/notifications/notification-types-setting.js.map +1 -1
- package/dist/notifications/notifications-labels.d.ts +6 -0
- package/dist/notifications/notifications-labels.js +1 -0
- package/dist/notifications/notifications-labels.js.map +1 -1
- package/dist/notifications/push-device-setting.js +1 -1
- package/dist/notifications/push-device-setting.js.map +1 -1
- package/dist/notifications/push-devices-setting.d.ts +2 -1
- package/dist/notifications/push-devices-setting.js +27 -49
- package/dist/notifications/push-devices-setting.js.map +1 -1
- package/dist/notifications/quiet-hours-setting.d.ts +10 -3
- package/dist/notifications/quiet-hours-setting.js +65 -26
- package/dist/notifications/quiet-hours-setting.js.map +1 -1
- package/dist/notifications/unsubscribe-panel.js +4 -7
- package/dist/notifications/unsubscribe-panel.js.map +1 -1
- package/dist/notifications/use-push-device.d.ts +39 -1
- package/dist/notifications/use-push-device.js +28 -7
- package/dist/notifications/use-push-device.js.map +1 -1
- package/dist/offline/query-persistence.d.ts +3 -1
- package/dist/offline/query-persistence.js +19 -2
- package/dist/offline/query-persistence.js.map +1 -1
- package/dist/rhf/fields.d.ts +11 -4
- package/dist/rhf/fields.js.map +1 -1
- package/dist/search/command-palette.js +1 -1
- package/dist/search/command-palette.js.map +1 -1
- package/dist/search/global-search.js +19 -9
- package/dist/search/global-search.js.map +1 -1
- package/dist/search/use-command-key.d.ts +12 -1
- package/dist/search/use-command-key.js +3 -12
- package/dist/search/use-command-key.js.map +1 -1
- package/dist/search.d.ts +7 -0
- package/dist/search.js +10 -0
- package/dist/search.js.map +1 -1
- package/dist/settings/use-settings-focus.js +1 -0
- package/dist/settings/use-settings-focus.js.map +1 -1
- package/dist/shell/account-menu-language.d.ts +21 -5
- package/dist/shell/account-menu-language.js +3 -1
- package/dist/shell/account-menu-language.js.map +1 -1
- package/dist/shell/account-menu-text-size.d.ts +26 -9
- package/dist/shell/account-menu-text-size.js +3 -2
- package/dist/shell/account-menu-text-size.js.map +1 -1
- package/dist/shell/dev-menu.js +5 -18
- package/dist/shell/dev-menu.js.map +1 -1
- package/dist/shell/role-switcher.d.ts +7 -0
- package/dist/shell/role-switcher.js.map +1 -1
- package/dist/shell/top-bar-actions.d.ts +1 -1
- package/dist/shell/top-bar-actions.js.map +1 -1
- package/dist/shell/top-bar-parts-labels.d.ts +0 -2
- package/dist/shell/top-bar-parts-labels.js +0 -1
- package/dist/shell/top-bar-parts-labels.js.map +1 -1
- package/dist/shell.d.ts +27 -8
- package/dist/shell.js +28 -7
- package/dist/shell.js.map +1 -1
- package/dist/sync/engine.d.ts +30 -4
- package/dist/sync/engine.js +172 -52
- package/dist/sync/engine.js.map +1 -1
- package/dist/sync.d.ts +6 -2
- package/dist/sync.js +6 -1
- package/dist/sync.js.map +1 -1
- package/dist/testing/css-variables.js +3 -8
- package/dist/testing/css-variables.js.map +1 -1
- package/dist/theme/contrast.d.ts +2 -1
- package/dist/theme/contrast.js +3 -8
- package/dist/theme/contrast.js.map +1 -1
- package/dist/theme/palette-store.js +6 -6
- package/dist/theme/palette-store.js.map +1 -1
- package/dist/theme/text-size.js +3 -8
- package/dist/theme/text-size.js.map +1 -1
- package/dist/theme/theme-store.js +3 -6
- package/dist/theme/theme-store.js.map +1 -1
- package/dist/tour/tour-catalogue.d.ts +7 -4
- package/dist/tour/tour-catalogue.js +12 -10
- package/dist/tour/tour-catalogue.js.map +1 -1
- package/dist/tour/tour-dock.d.ts +0 -2
- package/dist/tour/tour-dock.js +0 -2
- package/dist/tour/tour-dock.js.map +1 -1
- package/dist/tour/tour-gates.d.ts +4 -0
- package/dist/tour/tour-gates.js +14 -0
- package/dist/tour/tour-gates.js.map +1 -1
- package/dist/tour/tour-overlay.d.ts +27 -0
- package/dist/tour/tour-overlay.js +741 -0
- package/dist/tour/tour-overlay.js.map +1 -0
- package/dist/tour/tour.d.ts +13 -3
- package/dist/tour/tour.js +59 -781
- package/dist/tour/tour.js.map +1 -1
- package/dist/tour/use-tour-progress.js +1 -1
- package/dist/tour/use-tour-progress.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +5 -1
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard/types.d.ts +6 -2
- package/dist/wizard/types.js.map +1 -1
- package/dist/wizard/use-wizard.d.ts +3 -1
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/validation.d.ts +3 -1
- package/dist/wizard/validation.js.map +1 -1
- package/package.json +10 -6
- package/src/account/account-parts.ts +5 -25
- package/src/account/data-export-setting-labels.ts +1 -1
- package/src/account/delete-account-setting-labels.ts +1 -1
- package/src/account/email-change-setting-labels.ts +1 -1
- package/src/account/passkeys-setting.tsx +60 -38
- package/src/account/sessions-setting.tsx +18 -12
- package/src/admin/admin-action-confirm.tsx +1 -5
- package/src/admin/admin-parts.ts +11 -25
- package/src/admin/mail-test-card-labels.ts +1 -1
- package/src/admin/plan-change-labels.ts +5 -23
- package/src/admin/plan-change.tsx +7 -44
- package/src/admin/reviewer-scope-editor.tsx +3 -3
- package/src/auth/auth-errors-labels.ts +23 -0
- package/src/auth/auth-errors.ts +11 -38
- package/src/auth/complete-name-dialog.tsx +4 -1
- package/src/auth/forgot-password-form-labels.ts +1 -1
- package/src/auth/forgot-password-form.tsx +11 -6
- package/src/auth/register-form-labels.ts +1 -1
- package/src/auth/reset-password-form-labels.ts +1 -1
- package/src/auth/reset-password-form.tsx +8 -4
- package/src/auth/sign-in-form-labels.ts +1 -1
- package/src/auth/status-parts.tsx +0 -6
- package/src/auth/verify-email-labels.ts +1 -1
- package/src/auth/verify-email.tsx +11 -6
- package/src/billing/cancel-subscription.tsx +26 -12
- package/src/billing/dialog-close.ts +34 -0
- package/src/billing/plan-card.tsx +36 -11
- package/src/billing/plan-change-labels.ts +66 -0
- package/src/billing/plan-switch.tsx +26 -12
- package/src/chart.ts +21 -3
- package/src/components/account-chips.tsx +20 -23
- package/src/components/alert-banner.tsx +82 -5
- package/src/components/amount-input.tsx +18 -7
- package/src/components/autocomplete.tsx +64 -70
- package/src/components/bulk-action-bar.tsx +70 -2
- package/src/components/button-look.ts +5 -5
- package/src/components/button.tsx +2 -2
- package/src/components/chart-edge.ts +34 -1
- package/src/components/chart-figure.tsx +35 -0
- package/src/components/chart-zoom-core.tsx +6 -5
- package/src/components/chart.tsx +26 -5
- package/src/components/combobox-core.tsx +293 -62
- package/src/components/combobox.tsx +183 -153
- package/src/components/copy-button.tsx +34 -23
- package/src/components/country-select.tsx +16 -11
- package/src/components/data-table-column-settings.tsx +52 -10
- package/src/components/data-table-filters.ts +6 -4
- package/src/components/data-table-fit.ts +19 -5
- package/src/components/data-table-groups.tsx +1 -1
- package/src/components/data-table-head.tsx +32 -22
- package/src/components/data-table-mobile.tsx +61 -26
- package/src/components/data-table-pagination.tsx +21 -6
- package/src/components/data-table-resize.ts +1 -1
- package/src/components/data-table-row-actions.tsx +3 -3
- package/src/components/data-table-row.tsx +25 -14
- package/src/components/data-table-selection.ts +1 -1
- package/src/components/data-table-shared.tsx +33 -1
- package/src/components/data-table-sort.ts +57 -2
- package/src/components/data-table-virtual.ts +32 -8
- package/src/components/data-table.tsx +239 -68
- package/src/components/date-field.tsx +65 -8
- package/src/components/date-picker.tsx +125 -48
- package/src/components/date-range-picker.tsx +82 -31
- package/src/components/error-boundary.tsx +53 -39
- package/src/components/field-base-labels.ts +20 -0
- package/src/components/field-base.tsx +13 -7
- package/src/components/field-sync-labels.ts +1 -0
- package/src/components/field-sync.tsx +23 -1
- package/src/components/form-actions.tsx +20 -5
- package/src/components/hover-menu.tsx +18 -8
- package/src/components/icon-button.tsx +14 -9
- package/src/components/inline-confirm.tsx +116 -0
- package/src/components/inline-entity-combobox.tsx +197 -153
- package/src/components/input.tsx +1 -1
- package/src/components/jump-calendar.tsx +30 -146
- package/src/components/legal-page.tsx +10 -7
- package/src/components/legal.tsx +4 -2
- package/src/components/lightbox.tsx +21 -8
- package/src/components/list.tsx +10 -4
- package/src/components/measured-grid.tsx +8 -4
- package/src/components/mini-calendar.tsx +6 -5
- package/src/components/modal.tsx +6 -1
- package/src/components/month-grid.tsx +229 -0
- package/src/components/month-picker.tsx +192 -200
- package/src/components/multi-select.tsx +21 -9
- package/src/components/picker-sheet.tsx +12 -9
- package/src/components/pie-chart-labels.ts +8 -0
- package/src/components/pie-chart.tsx +22 -28
- package/src/components/popover.tsx +18 -1
- package/src/components/row-actions.tsx +2 -1
- package/src/components/section-nav.tsx +7 -0
- package/src/components/series-chart-axes.ts +3 -3
- package/src/components/series-chart-labels.ts +7 -4
- package/src/components/series-chart.tsx +178 -69
- package/src/components/share-card.tsx +6 -6
- package/src/components/sign-chip-labels.ts +1 -0
- package/src/components/sign-chip.tsx +10 -1
- package/src/components/tabs-labels.ts +13 -0
- package/src/components/tabs.tsx +2 -2
- package/src/components/toggle-legend.tsx +16 -11
- package/src/components/translation-review-editor.tsx +6 -5
- package/src/components/translation-review.tsx +4 -3
- package/src/components/treemap-labels.ts +39 -0
- package/src/components/treemap-layout.ts +210 -0
- package/src/components/treemap.tsx +722 -133
- package/src/components/use-table-state.ts +12 -9
- package/src/data-table.ts +9 -2
- package/src/eslint/role-utilities.ts +97 -19
- package/src/feedback/feedback-dialog.tsx +30 -19
- package/src/feedback/feedback-mutation.ts +41 -0
- package/src/feedback/feedback-page-link.ts +90 -0
- package/src/feedback/feedback-public-list.tsx +17 -5
- package/src/feedback/feedback-replies.tsx +13 -6
- package/src/feedback/feedback-row-detail-labels.ts +5 -0
- package/src/feedback/feedback-row-detail.tsx +103 -67
- package/src/feedback/feedback-status-undo.ts +41 -27
- package/src/feedback/feedback-submit-dialog.tsx +2 -2
- package/src/feedback/feedback-submit.tsx +21 -7
- package/src/feedback/feedback-swipe.tsx +1 -1
- package/src/feedback/feedback-table.tsx +86 -38
- package/src/feedback/feedback-thread-labels.ts +2 -2
- package/src/feedback/feedback-thread.tsx +27 -25
- package/src/feedback.ts +42 -0
- package/src/hooks/use-focus-trap.ts +8 -24
- package/src/hooks/use-hotkey.ts +24 -7
- package/src/hooks/use-long-press.ts +16 -2
- package/src/hooks/use-media-query.ts +20 -4
- package/src/hooks/use-mounted.ts +39 -0
- package/src/hooks/use-overlay-history.ts +32 -3
- package/src/i18n/defaults.ts +4 -5
- package/src/i18n/german.ts +8 -2
- package/src/i18n/kit-labels.tsx +9 -2
- package/src/i18n/locales/es.ts +8 -2
- package/src/i18n/locales/fr.ts +21 -13
- package/src/i18n/locales/hu.ts +10 -2
- package/src/i18n/locales/it.ts +8 -2
- package/src/i18n/locales/zh.ts +9 -2
- package/src/i18n/review.ts +5 -0
- package/src/index.ts +103 -13
- package/src/landing/routing.tsx +14 -10
- package/src/lib/broadcast.ts +52 -0
- package/src/lib/date-format.ts +89 -13
- package/src/lib/day-format.ts +96 -0
- package/src/lib/describe-failure.ts +18 -0
- package/src/lib/direction.ts +39 -0
- package/src/lib/focus-left.ts +156 -0
- package/src/lib/format.ts +5 -17
- package/src/lib/http-status.ts +38 -0
- package/src/lib/intl-cache.ts +25 -0
- package/src/lib/isin.ts +10 -0
- package/src/lib/iso-day.ts +13 -0
- package/src/lib/lazy-with-fallback.ts +82 -0
- package/src/lib/path-pattern.ts +55 -0
- package/src/lib/reduced-motion.ts +2 -1
- package/src/lib/round.ts +7 -2
- package/src/lib/safe-storage.ts +48 -6
- package/src/lib/session-refresh.ts +1 -1
- package/src/lib/share-format.ts +18 -0
- package/src/lib/text-case.ts +19 -0
- package/src/lib/text-match.ts +57 -2
- package/src/lib/thenable.ts +19 -0
- package/src/lib/translation-review.ts +13 -5
- package/src/notifications/device-zone-report.ts +1 -1
- package/src/notifications/notification-bell.tsx +7 -4
- package/src/notifications/notification-inbox.tsx +40 -58
- package/src/notifications/notification-parts.tsx +3 -4
- package/src/notifications/notification-types-setting.tsx +1 -1
- package/src/notifications/notifications-labels.ts +7 -0
- package/src/notifications/push-device-setting.tsx +1 -1
- package/src/notifications/push-devices-setting.tsx +30 -55
- package/src/notifications/quiet-hours-setting.tsx +93 -31
- package/src/notifications/unsubscribe-panel.tsx +4 -7
- package/src/notifications/use-push-device.ts +76 -10
- package/src/offline/query-persistence.ts +35 -3
- package/src/rhf/fields.tsx +11 -4
- package/src/search/command-palette.tsx +2 -4
- package/src/search/global-search.tsx +36 -15
- package/src/search/use-command-key.ts +13 -12
- package/src/search.ts +9 -0
- package/src/settings/use-settings-focus.ts +3 -0
- package/src/shell/account-menu-language.tsx +27 -5
- package/src/shell/account-menu-text-size.tsx +32 -10
- package/src/shell/dev-menu.tsx +7 -21
- package/src/shell/role-switcher.tsx +7 -0
- package/src/shell/top-bar-actions.tsx +1 -1
- package/src/shell/top-bar-parts-labels.ts +0 -3
- package/src/shell.ts +40 -9
- package/src/sync/engine.ts +339 -61
- package/src/sync.ts +8 -1
- package/src/testing/css-variables.ts +8 -9
- package/src/theme/contrast.ts +8 -9
- package/src/theme/palette-store.ts +9 -9
- package/src/theme/text-size.ts +6 -8
- package/src/theme/theme-store.ts +7 -9
- package/src/tour/tour-catalogue.tsx +26 -15
- package/src/tour/tour-dock.ts +0 -2
- package/src/tour/tour-gates.ts +23 -0
- package/src/tour/tour-overlay.tsx +1170 -0
- package/src/tour/tour.tsx +108 -1202
- package/src/tour/use-tour-progress.ts +1 -1
- package/src/wizard/stepper-nav.tsx +3 -1
- package/src/wizard/types.ts +11 -2
- package/src/wizard/use-wizard.ts +2 -1
- package/src/wizard/validation.ts +8 -2
- package/tokens.css +2 -1
- package/dist/components/ui-labels.js.map +0 -1
- package/dist/shell/dev-switchers.d.ts +0 -6
- package/dist/shell/dev-switchers.js +0 -12
- package/dist/shell/dev-switchers.js.map +0 -1
- package/src/components/ui-labels.ts +0 -24
- package/src/shell/dev-switchers.tsx +0 -18
|
@@ -8,6 +8,13 @@ export interface PieChartLabels {
|
|
|
8
8
|
* German, "42%" in English) — the function only arranges them.
|
|
9
9
|
*/
|
|
10
10
|
slice: (label: string, value: string, percent: string) => string;
|
|
11
|
+
/**
|
|
12
|
+
* One slice's accessible name under `redact` (0.52): the label and the share, with no
|
|
13
|
+
* amount. `redact` blurs every figure on screen but not the ring, and the ring's names
|
|
14
|
+
* are attributes on untagged elements, which a session replay or a redaction keyed on
|
|
15
|
+
* `[data-private]` reads as they are. `percent` as for {@link PieChartLabels.slice}.
|
|
16
|
+
*/
|
|
17
|
+
sliceShare: (label: string, percent: string) => string;
|
|
11
18
|
/** The caption over the donut's centre figure. */
|
|
12
19
|
total: string;
|
|
13
20
|
/** What a chart with nothing to draw says, when the caller passes no `empty`. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/pie-chart-labels.ts"],"sourcesContent":["// All user-facing strings for `PieChart`, as the `pieChart` namespace of\n// `<UiKitProvider labels>`, and per chart the `labels` prop over that.\n//\n// A module of its own, with no recharts in it, for the reason `series-chart-labels`\n// is one: the label registry (src/i18n) imports the defaults, and must not pull the\n// chart library in behind them.\n\nexport interface PieChartLabels {\n /** Accessible name of the group of keyboard stops laid over the slices — one tab\n * stop, arrow keys between them. */\n slices: string;\n /**\n * One slice's accessible name. `value` arrives through `formatValue`, `percent` as\n * the slice's share of what is drawn, already formatted in the locale (\"42 %\" in\n * German, \"42%\" in English) — the function only arranges them.\n */\n slice: (label: string, value: string, percent: string) => string;\n /** The caption over the donut's centre figure. */\n total: string;\n /** What a chart with nothing to draw says, when the caller passes no `empty`. */\n empty: string;\n /** Accessible name of the chart's legend (a group of switches, one per slice). */\n legend: string;\n}\n\nexport const DEFAULT_PIE_CHART_LABELS: PieChartLabels = {\n slices: \"Chart slices\",\n slice: (label, value, percent) => `${label}: ${value} (${percent})`,\n total: \"Total\",\n empty: \"No data\",\n legend: \"Categories\",\n};\n"],"mappings":";
|
|
1
|
+
{"version":3,"sources":["../../src/components/pie-chart-labels.ts"],"sourcesContent":["// All user-facing strings for `PieChart`, as the `pieChart` namespace of\n// `<UiKitProvider labels>`, and per chart the `labels` prop over that.\n//\n// A module of its own, with no recharts in it, for the reason `series-chart-labels`\n// is one: the label registry (src/i18n) imports the defaults, and must not pull the\n// chart library in behind them.\n\nexport interface PieChartLabels {\n /** Accessible name of the group of keyboard stops laid over the slices — one tab\n * stop, arrow keys between them. */\n slices: string;\n /**\n * One slice's accessible name. `value` arrives through `formatValue`, `percent` as\n * the slice's share of what is drawn, already formatted in the locale (\"42 %\" in\n * German, \"42%\" in English) — the function only arranges them.\n */\n slice: (label: string, value: string, percent: string) => string;\n /**\n * One slice's accessible name under `redact` (0.52): the label and the share, with no\n * amount. `redact` blurs every figure on screen but not the ring, and the ring's names\n * are attributes on untagged elements, which a session replay or a redaction keyed on\n * `[data-private]` reads as they are. `percent` as for {@link PieChartLabels.slice}.\n */\n sliceShare: (label: string, percent: string) => string;\n /** The caption over the donut's centre figure. */\n total: string;\n /** What a chart with nothing to draw says, when the caller passes no `empty`. */\n empty: string;\n /** Accessible name of the chart's legend (a group of switches, one per slice). */\n legend: string;\n}\n\nexport const DEFAULT_PIE_CHART_LABELS: PieChartLabels = {\n slices: \"Chart slices\",\n slice: (label, value, percent) => `${label}: ${value} (${percent})`,\n sliceShare: (label, percent) => `${label}: ${percent}`,\n total: \"Total\",\n empty: \"No data\",\n legend: \"Categories\",\n};\n"],"mappings":";AAgCO,MAAM,2BAA2C;AAAA,EACtD,QAAQ;AAAA,EACR,OAAO,CAAC,OAAO,OAAO,YAAY,GAAG,KAAK,KAAK,KAAK,KAAK,OAAO;AAAA,EAChE,YAAY,CAAC,OAAO,YAAY,GAAG,KAAK,KAAK,OAAO;AAAA,EACpD,OAAO;AAAA,EACP,OAAO;AAAA,EACP,QAAQ;AACV;","names":[]}
|
|
@@ -72,7 +72,8 @@ export interface PieChartProps extends Omit<ComponentPropsWithRef<"div">, "child
|
|
|
72
72
|
* Tag every figure ON SCREEN `data-private` — the tooltip bubble, the centre and
|
|
73
73
|
* `"value"` slice labels — so the host's demo-mode rule blurs it. A percentage alone
|
|
74
74
|
* reveals no amount, so `"percent"` labels stay readable, and the slices themselves
|
|
75
|
-
* are not tagged: a blurred ring is no picture at all.
|
|
75
|
+
* are not tagged: a blurred ring is no picture at all. So a slice's NAME says no
|
|
76
|
+
* amount either (0.52): "Occupied: 60%" (`labels.sliceShare`), not `labels.slice`.
|
|
76
77
|
*/
|
|
77
78
|
redact?: boolean;
|
|
78
79
|
/** What to show with nothing to draw. Default: `labels.empty`. */
|
|
@@ -90,8 +91,9 @@ export interface PieChartProps extends Omit<ComponentPropsWithRef<"div">, "child
|
|
|
90
91
|
* clockwise from twelve o'clock, the order of the legend — with ←/→ following the
|
|
91
92
|
* reading direction in RTL; Home/End go to the first and last. Each slice is named
|
|
92
93
|
* "Label: value (xx%)" (`labels.slice`), because a colour and an angle are nothing a
|
|
93
|
-
* screen reader can say
|
|
94
|
-
* `onSliceClick`, Enter and Space
|
|
94
|
+
* screen reader can say — under `redact` "Label: xx%" (`labels.sliceShare`) — and
|
|
95
|
+
* focusing one reads it out in the donut's centre. With `onSliceClick`, Enter and Space
|
|
96
|
+
* activate it as a click does.
|
|
95
97
|
*
|
|
96
98
|
* The legend switches slices off (`legend="toggle"`); the percentages are always of
|
|
97
99
|
* what is drawn. The plot is not mirrored in RTL (see `ChartContainer`); the legend
|
|
@@ -3,7 +3,8 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { Pie, PieChart as RechartsPieChart, Sector } from "recharts";
|
|
5
5
|
import { cn } from "../lib/cn.js";
|
|
6
|
-
import {
|
|
6
|
+
import { dirOf, rovingTarget } from "../lib/direction.js";
|
|
7
|
+
import { shareFormat } from "../lib/share-format.js";
|
|
7
8
|
import { useKitLabels, useKitLocale } from "../i18n/kit-labels.js";
|
|
8
9
|
import { paletteFor } from "../theme/chart-palette.js";
|
|
9
10
|
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "./chart.js";
|
|
@@ -44,21 +45,11 @@ function PieChart({
|
|
|
44
45
|
const { scale } = useTextSize();
|
|
45
46
|
const box = chartHeightProps(height);
|
|
46
47
|
const numberFormat = useMemo(() => new Intl.NumberFormat(locale), [locale]);
|
|
47
|
-
const percentFormats = useMemo(
|
|
48
|
-
() => ({
|
|
49
|
-
whole: new Intl.NumberFormat(locale, { style: "percent", maximumFractionDigits: 0 }),
|
|
50
|
-
fine: new Intl.NumberFormat(locale, { style: "percent", maximumFractionDigits: 1 })
|
|
51
|
-
}),
|
|
52
|
-
[locale]
|
|
53
|
-
);
|
|
54
48
|
const formatValue = useCallback(
|
|
55
49
|
(v) => formatValueProp ? formatValueProp(v) : formatWithMinus(numberFormat, v),
|
|
56
50
|
[formatValueProp, numberFormat]
|
|
57
51
|
);
|
|
58
|
-
const formatShare =
|
|
59
|
-
(share) => (share < 0.1 ? percentFormats.fine : percentFormats.whole).format(share),
|
|
60
|
-
[percentFormats]
|
|
61
|
-
);
|
|
52
|
+
const formatShare = useMemo(() => shareFormat(locale), [locale]);
|
|
62
53
|
const [ownHidden, setOwnHidden] = useState(() => /* @__PURE__ */ new Set());
|
|
63
54
|
const hidden = hiddenProp ?? ownHidden;
|
|
64
55
|
const toggle = (key) => {
|
|
@@ -109,16 +100,10 @@ function PieChart({
|
|
|
109
100
|
const d = drawn[index];
|
|
110
101
|
if (!d) return /* @__PURE__ */ jsx(Sector, { ...sector }, key ?? void 0);
|
|
111
102
|
const onKeyDown = (event) => {
|
|
112
|
-
const
|
|
113
|
-
let target;
|
|
114
|
-
if (step) target = index + step;
|
|
115
|
-
else if (event.key === "ArrowDown") target = index + 1;
|
|
116
|
-
else if (event.key === "ArrowUp") target = index - 1;
|
|
117
|
-
else if (event.key === "Home") target = 0;
|
|
118
|
-
else if (event.key === "End") target = drawn.length - 1;
|
|
103
|
+
const target = rovingTarget(event.key, dirOf(event.currentTarget), index, drawn.length, true);
|
|
119
104
|
if (target !== void 0) {
|
|
120
105
|
event.preventDefault();
|
|
121
|
-
refs.current[
|
|
106
|
+
refs.current[target]?.focus();
|
|
122
107
|
} else if (clickable && (event.key === "Enter" || event.key === " ")) {
|
|
123
108
|
event.preventDefault();
|
|
124
109
|
clickRef.current?.(d.slice.key, d.slice);
|
|
@@ -132,7 +117,7 @@ function PieChart({
|
|
|
132
117
|
},
|
|
133
118
|
role: clickable ? "button" : "img",
|
|
134
119
|
tabIndex: index === 0 ? 0 : -1,
|
|
135
|
-
"aria-label": labels.slice(d.slice.label, formatValue(d.value), formatShare(d.share)),
|
|
120
|
+
"aria-label": redact ? labels.sliceShare(d.slice.label, formatShare(d.share)) : labels.slice(d.slice.label, formatValue(d.value), formatShare(d.share)),
|
|
136
121
|
"data-slice": d.slice.key,
|
|
137
122
|
onKeyDown,
|
|
138
123
|
onFocus: () => {
|
|
@@ -145,7 +130,7 @@ function PieChart({
|
|
|
145
130
|
}
|
|
146
131
|
);
|
|
147
132
|
},
|
|
148
|
-
[drawn, labels, formatValue, formatShare, clickable]
|
|
133
|
+
[drawn, labels, formatValue, formatShare, clickable, redact]
|
|
149
134
|
);
|
|
150
135
|
const label = useCallback(
|
|
151
136
|
(props) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/pie-chart.tsx"],"sourcesContent":["// A part-to-whole chart: a donut (or a full pie), one slice per category, its ANGLE\n// the category's share of what is drawn. Lifted out of kastlan, where the dashboard's\n// occupancy card and the reports' maintenance card each drew a recharts `<Pie>` by\n// hand inside the kit's `ChartContainer` — with a legend that could not switch\n// anything, slices a keyboard could not reach, and colours from a local hex list.\n//\n// It uses the kit's own chart shell (`ChartContainer`, `ChartTooltipContent`) and\n// ramp (`paletteFor`), so it re-skins with the palette like every other chart, and the\n// kit's own legends (`ToggleLegend`, `StaticLegend`).\nimport { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport type { ComponentProps, ComponentPropsWithRef, KeyboardEvent, ReactNode } from \"react\";\nimport { Pie, PieChart as RechartsPieChart, Sector } from \"recharts\";\nimport type { PieLabelRenderProps, PieSectorShapeProps } from \"recharts\";\nimport { cn } from \"../lib/cn\";\nimport { horizontalStep } from \"../lib/direction\";\nimport { useKitLabels, useKitLocale, type LabelOverride } from \"../i18n/kit-labels\";\nimport { paletteFor } from \"../theme/chart-palette\";\nimport { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig } from \"./chart\";\nimport { DEFAULT_PIE_CHART_LABELS, type PieChartLabels } from \"./pie-chart-labels\";\nimport { StaticLegend, ToggleLegend, toggleHidden, type LegendEntry } from \"./toggle-legend\";\nimport { useTextSize } from \"../theme/text-size\";\nimport { chartHeightProps, type ChartHeight } from \"./chart-height\";\nimport { formatWithMinus } from \"../lib/minus\";\n\n/** One slice. */\nexport interface PieChartSlice {\n /** Stable key, handed back by {@link PieChartProps.onSliceClick} and used by the\n * legend's hidden set. Labels can collide; keys must not. */\n key: string;\n /** What the slice is called — in the legend, the tooltip and its accessible name. */\n label: string;\n /**\n * The slice's size. A value that is not a positive finite number — `null` for a\n * figure that is missing, a zero, a negative — is NOT drawn and not in the legend:\n * an angle cannot be negative, and a zero-degree slice is one a pointer and a\n * keyboard would both stop on to find nothing.\n */\n value: number | null | undefined;\n /** Overrides the by-index palette colour (`paletteFor(i)`, `i` the slice's index in\n * `data` AS GIVEN) — a status that has a colour of its own. Any CSS colour. */\n color?: string;\n}\n\n/**\n * Everything a `<div>` takes reaches the chart's root, so a test id or a `data-tour`\n * anchor can find it. `children` is the chart's own.\n */\nexport interface PieChartProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\"> {\n data: readonly PieChartSlice[];\n /** `\"donut\"` (default) leaves a hole with the total in it; `\"pie\"` is the full disc. */\n variant?: \"donut\" | \"pie\";\n /**\n * Height of the plot (the legend is under it): a CSS length (`\"20rem\"`,\n * `\"min(20rem, 50dvh)\"`), a Tailwind class, or px. Default `\"18.75rem\"` — 300 px at\n * Normal, growing with the text size so the slice labels keep their room (0.32,\n * docs/text-size-harmonization.md §10.10). A number does not follow the text size.\n */\n height?: ChartHeight;\n /** How a value is written in the tooltip, the slice's name, a `\"value\"` slice label\n * and the donut's total. Default: `Intl.NumberFormat` in `locale`. */\n formatValue?: (value: number) => string;\n /**\n * What is written beside each slice: its share (`\"percent\"`, the default), its\n * value, or nothing. Only on slices of at least {@link minLabelShare} — a label on a\n * sliver sits on its neighbour's. The figure is in the tooltip and the slice's name\n * either way.\n */\n sliceLabels?: \"percent\" | \"value\" | \"none\";\n /** The smallest share (0–1) that gets a slice label. Default 0.04. */\n minLabelShare?: number;\n /** Draw the donut's centre: {@link centerLabel} over {@link centerValue}. Default\n * `true`; ignored for `variant=\"pie\"`, which has no centre to draw in. */\n center?: boolean;\n /** The caption in the donut's centre. Default: `labels.total`. */\n centerLabel?: ReactNode;\n /** The figure in the donut's centre. Default: the drawn total through `formatValue`. */\n centerValue?: ReactNode;\n /**\n * The legend under the chart. `\"toggle\"` (default): each entry is a switch that\n * takes its slice off the chart, and the percentages are then of what is left —\n * the shares always read \"of this picture\". `\"static\"`: a key with nothing to press.\n */\n legend?: \"toggle\" | \"static\" | \"none\";\n /** The slices switched off, for a caller that owns the set (a URL, a saved view).\n * Left out, the chart keeps its own. */\n hiddenSlices?: ReadonlySet<string>;\n /** Called with the new hidden set whenever the legend switches a slice. */\n onHiddenSlicesChange?: (hidden: ReadonlySet<string>) => void;\n /** A slice was activated — by click, or Enter/Space on a focused slice. Makes each\n * slice a `role=\"button\"`. */\n onSliceClick?: (key: string, slice: PieChartSlice) => void;\n /**\n * Tag every figure ON SCREEN `data-private` — the tooltip bubble, the centre and\n * `\"value\"` slice labels — so the host's demo-mode rule blurs it. A percentage alone\n * reveals no amount, so `\"percent\"` labels stay readable, and the slices themselves\n * are not tagged: a blurred ring is no picture at all.\n */\n redact?: boolean;\n /** What to show with nothing to draw. Default: `labels.empty`. */\n empty?: ReactNode;\n /** BCP 47 tag for the numbers and percentages. Default: the provider's. */\n locale?: string;\n labels?: LabelOverride<PieChartLabels>;\n}\n\n/** A slice as drawn: only the ones with a positive value, in `data` order. */\ninterface DrawnSlice {\n slice: PieChartSlice;\n value: number;\n color: string;\n /** Of what is drawn, 0–1. */\n share: number;\n}\n\n// Slices carry their own `fill`, and `ChartTooltipContent` falls back to it for the\n// swatch, so no series config is needed — and none is wanted, because a config key is a\n// CSS identifier and a slice key is the caller's data (see `ChartStyle`).\nconst PIE_CONFIG: ChartConfig = {};\n\nconst RADIAN = Math.PI / 180;\n\n/** The shared tooltip with the bubble tagged `data-private` — see the treemap's\n * `RedactedTooltipContent` for why it has to BE the element recharts clones. */\nfunction RedactedTooltipContent(props: ComponentProps<typeof ChartTooltipContent>) {\n return (\n <div data-private>\n <ChartTooltipContent {...props} />\n </div>\n );\n}\n\n/**\n * Part-to-whole chart: a donut (or `variant=\"pie\"`), one slice per\n * {@link PieChartSlice}, coloured from the chart ramp by index.\n *\n * **Every slice is reachable from the keyboard.** The slices are ONE tab stop (a roving\n * tabindex, as in the kit's grids): ←/→ and ↑/↓ step to the next and previous slice —\n * clockwise from twelve o'clock, the order of the legend — with ←/→ following the\n * reading direction in RTL; Home/End go to the first and last. Each slice is named\n * \"Label: value (xx%)\" (`labels.slice`), because a colour and an angle are nothing a\n * screen reader can say, and focusing one reads it out in the donut's centre. With\n * `onSliceClick`, Enter and Space activate it as a click does.\n *\n * The legend switches slices off (`legend=\"toggle\"`); the percentages are always of\n * what is drawn. The plot is not mirrored in RTL (see `ChartContainer`); the legend\n * and the centre follow the page.\n *\n * With nothing to draw it says so (`empty`) rather than drawing an empty box.\n *\n * Needs `recharts`, like everything behind `@eifi1/ui-kit/chart`.\n */\nexport function PieChart({\n data,\n variant = \"donut\",\n height = \"18.75rem\",\n formatValue: formatValueProp,\n sliceLabels = \"percent\",\n minLabelShare = 0.04,\n center = true,\n centerLabel,\n centerValue,\n legend = \"toggle\",\n hiddenSlices: hiddenProp,\n onHiddenSlicesChange,\n onSliceClick,\n redact = false,\n empty,\n locale: localeProp,\n labels: labelsProp,\n className,\n style,\n ...rest\n}: PieChartProps) {\n const labels = useKitLabels(\"pieChart\", DEFAULT_PIE_CHART_LABELS, labelsProp);\n const locale = useKitLocale(localeProp);\n // The slice labels are SVG text in px: they follow the text size by this (§10.10).\n const { scale } = useTextSize();\n const box = chartHeightProps(height);\n const numberFormat = useMemo(() => new Intl.NumberFormat(locale), [locale]);\n const percentFormats = useMemo(\n () => ({\n whole: new Intl.NumberFormat(locale, { style: \"percent\", maximumFractionDigits: 0 }),\n fine: new Intl.NumberFormat(locale, { style: \"percent\", maximumFractionDigits: 1 }),\n }),\n [locale],\n );\n // Memoised, like everything the sectors are drawn from: they must not be rebuilt on\n // this component's own re-renders (see `pie` below).\n const formatValue = useCallback(\n (v: number) => (formatValueProp ? formatValueProp(v) : formatWithMinus(numberFormat, v)),\n [formatValueProp, numberFormat],\n );\n // One decimal under 10 %, where rounding to whole percents starts to lie — a slice\n // of 0.4 % labelled \"0%\" says it is not there.\n const formatShare = useCallback(\n (share: number) => (share < 0.1 ? percentFormats.fine : percentFormats.whole).format(share),\n [percentFormats],\n );\n\n const [ownHidden, setOwnHidden] = useState<ReadonlySet<string>>(() => new Set());\n const hidden = hiddenProp ?? ownHidden;\n const toggle = (key: string) => {\n const next = toggleHidden(hidden, key);\n if (hiddenProp === undefined) setOwnHidden(next);\n onHiddenSlicesChange?.(next);\n };\n\n // Coloured by index into `data` AS GIVEN, before anything is dropped, so a slice\n // keeps its colour when a neighbour is switched off or has no value.\n const { drawable, drawn, total, pieData } = useMemo(() => {\n const drawable = data\n .map((slice, i) => ({ slice, value: slice.value, color: slice.color ?? paletteFor(i) }))\n .filter(\n (d): d is { slice: PieChartSlice; value: number; color: string } =>\n typeof d.value === \"number\" && Number.isFinite(d.value) && d.value > 0,\n );\n const shown = drawable.filter((d) => !hidden.has(d.slice.key));\n const total = shown.reduce((acc, d) => acc + d.value, 0);\n const drawn: DrawnSlice[] = shown.map((d) => ({ ...d, share: total > 0 ? d.value / total : 0 }));\n const pieData = drawn.map((d) => ({\n key: d.slice.key,\n label: d.slice.label,\n value: d.value,\n fill: d.color,\n }));\n return { drawable, drawn, total, pieData };\n }, [data, hidden]);\n\n // The roving tab stop, and the slice that has focus (which the centre reads out).\n const [stop, setStop] = useState(0);\n const [focused, setFocused] = useState<number | undefined>(undefined);\n const current = Math.min(stop, Math.max(0, drawn.length - 1));\n const refs = useRef<(SVGGElement | null)[]>([]);\n const focusedSlice = focused !== undefined ? drawn[focused] : undefined;\n\n // Read through a ref: the sectors are drawn once per `data`, and a caller's inline\n // `onSliceClick` is a new function every render.\n const clickRef = useRef(onSliceClick);\n useLayoutEffect(() => {\n clickRef.current = onSliceClick;\n });\n const clickable = onSliceClick !== undefined;\n\n /**\n * One sector, wrapped in its keyboard stop. recharts renders it inside a layer of its\n * own that carries the pointer's hover and click, so those are untouched.\n *\n * Focus does NOT drive the recharts tooltip (`defaultIndex`), as it does in\n * `SeriesChart`: an active pie sector is re-parented into an extra layer, which\n * remounts this element and drops the focus it just took. The donut's centre is the\n * keyboard's read-out instead.\n */\n const shape = useCallback(\n (props: PieSectorShapeProps) => {\n const {\n index,\n isActive: _isActive,\n tabIndex: _tabIndex,\n animationElapsedTime: _elapsed,\n isAnimating: _animating,\n isEntrance: _entrance,\n // recharts hands the shape its own React `key` among the props. Spread into\n // JSX, a `key` inside a props object is a warning on every render (React 19),\n // so it is lifted out here and passed on its own.\n key,\n ...sector\n } = props as PieSectorShapeProps & {\n animationElapsedTime?: number;\n isAnimating?: boolean;\n isEntrance?: boolean;\n key?: string | number | null;\n };\n const d = drawn[index];\n if (!d) return <Sector key={key ?? undefined} {...sector} />;\n const onKeyDown = (event: KeyboardEvent<SVGGElement>) => {\n const step = horizontalStep(event.key, event.currentTarget);\n let target: number | undefined;\n if (step) target = index + step;\n else if (event.key === \"ArrowDown\") target = index + 1;\n else if (event.key === \"ArrowUp\") target = index - 1;\n else if (event.key === \"Home\") target = 0;\n else if (event.key === \"End\") target = drawn.length - 1;\n if (target !== undefined) {\n event.preventDefault();\n // Round the circle: past the last slice is the first, as the eye goes.\n refs.current[((target % drawn.length) + drawn.length) % drawn.length]?.focus();\n } else if (clickable && (event.key === \"Enter\" || event.key === \" \")) {\n event.preventDefault();\n clickRef.current?.(d.slice.key, d.slice);\n }\n };\n return (\n <g\n ref={(node) => {\n refs.current[index] = node;\n }}\n role={clickable ? \"button\" : \"img\"}\n // The first slice until the effect below moves the stop.\n tabIndex={index === 0 ? 0 : -1}\n aria-label={labels.slice(d.slice.label, formatValue(d.value), formatShare(d.share))}\n data-slice={d.slice.key}\n onKeyDown={onKeyDown}\n onFocus={() => {\n setStop(index);\n setFocused(index);\n }}\n onBlur={() => setFocused(undefined)}\n // CSS beats a presentation attribute, so this recolours the sector's own\n // stroke for as long as it has keyboard focus.\n className=\"outline-none [&:focus-visible>path]:stroke-primary\"\n >\n <Sector key={key ?? undefined} {...sector} />\n </g>\n );\n },\n [drawn, labels, formatValue, formatShare, clickable],\n );\n\n const label = useCallback(\n (props: PieLabelRenderProps) => {\n const d = drawn[props.index ?? -1];\n const cx = Number(props.cx);\n const cy = Number(props.cy);\n const radius = Number(props.outerRadius);\n const mid = Number(props.midAngle);\n if (!d || d.share < minLabelShare || ![cx, cy, radius, mid].every(Number.isFinite)) return null;\n const r = radius + 10 * scale;\n const x = cx + r * Math.cos(-mid * RADIAN);\n const y = cy + r * Math.sin(-mid * RADIAN);\n const isValue = sliceLabels === \"value\";\n return (\n <text\n x={x}\n y={y}\n textAnchor={x >= cx ? \"start\" : \"end\"}\n dominantBaseline=\"central\"\n fontSize={11 * scale}\n fill=\"var(--text-secondary)\"\n data-private={redact && isValue ? \"\" : undefined}\n style={{ pointerEvents: \"none\" }}\n >\n {isValue ? formatValue(d.value) : formatShare(d.share)}\n </text>\n );\n },\n [drawn, minLabelShare, sliceLabels, redact, formatValue, formatShare, scale],\n );\n\n const withLabels = sliceLabels !== \"none\";\n // Room outside the ring for the labels, or the ring fills the box.\n const outer = withLabels ? 72 : 88;\n const inner = variant === \"donut\" ? Math.round(outer * 0.6) : 0;\n\n /**\n * The `<Pie>`, as ONE element for as long as what it draws is unchanged. recharts\n * keys its sectors by an animation id that is renewed whenever the Pie's props object\n * is — i.e. on every render of the Pie — so re-rendering it remounts every sector,\n * the one with focus included, which dropped the focus to <body> on the very state\n * update that focusing a slice makes. An unchanged element is one React does not\n * re-render, so the sectors stay put, and the roving tab stop is moved on them in\n * place (the effect below).\n */\n const pie = useMemo(\n () => (\n <Pie\n data={pieData}\n dataKey=\"value\"\n nameKey=\"label\"\n cx=\"50%\"\n cy=\"50%\"\n outerRadius={`${outer}%`}\n innerRadius={`${inner}%`}\n startAngle={90}\n endAngle={-270}\n // The card showing through between the slices, as the treemap's gutter.\n stroke=\"var(--bg-surface)\"\n strokeWidth={2}\n rootTabIndex={-1}\n isAnimationActive={false}\n shape={shape}\n label={withLabels ? label : false}\n labelLine={false}\n onClick={\n clickable\n ? (_entry: unknown, index: number) => {\n const d = drawn[index];\n if (d) clickRef.current?.(d.slice.key, d.slice);\n }\n : undefined\n }\n />\n ),\n [pieData, drawn, shape, label, withLabels, outer, inner, clickable],\n );\n\n // The roving tab stop, moved on the sectors in place (they are not re-rendered).\n useEffect(() => {\n refs.current.forEach((el, i) => el?.setAttribute(\"tabindex\", i === current ? \"0\" : \"-1\"));\n });\n\n const legendEntries: LegendEntry[] = drawable.map((d) => ({\n key: d.slice.key,\n label: d.slice.label,\n color: d.color,\n }));\n\n const emptyBox = (\n <div className={cn(\"flex items-center justify-center text-sm text-muted\", box.className)} style={box.style}>\n {empty ?? labels.empty}\n </div>\n );\n\n // Nothing drawable at all: no chart, and no legend of switches for nothing.\n if (drawable.length === 0) {\n return (\n <div {...rest} className={cn(\"w-full\", className)} style={style}>\n {emptyBox}\n </div>\n );\n }\n\n const Content = redact ? RedactedTooltipContent : ChartTooltipContent;\n\n return (\n <div {...rest} className={cn(\"w-full\", className)} style={style}>\n {drawn.length === 0 ? (\n // Every slice switched off: the legend below is how they come back.\n emptyBox\n ) : (\n <div role=\"group\" aria-label={labels.slices} className={cn(\"relative\", box.className)} style={box.style}>\n <ChartContainer\n config={PIE_CONFIG}\n className={cn(\"h-full\", clickable && \"[&_.recharts-pie-sector]:cursor-pointer\")}\n >\n <RechartsPieChart accessibilityLayer={false}>\n <ChartTooltip\n content={\n <Content\n hideLabel\n valueFormatter={(v) => `${formatValue(v)} (${formatShare(total > 0 ? v / total : 0)})`}\n />\n }\n />\n {pie}\n </RechartsPieChart>\n </ChartContainer>\n {variant === \"donut\" && center && (\n <div\n data-private={redact ? \"\" : undefined}\n // While a slice has keyboard focus the centre reads it out — the\n // keyboard's tooltip. Hidden from assistive technology then: the slice's\n // own name has just said the same.\n aria-hidden={focusedSlice ? true : undefined}\n className=\"pointer-events-none absolute inset-0 flex flex-col items-center justify-center text-center\"\n >\n {/* The label wraps from Large up, inside the hole's 40 % (text size\n §10.17, round 0.34 T1: the sweep read \"Spent in Sept…\"). */}\n <span className=\"max-w-[40%] truncate-until-large text-xs text-muted\">\n {focusedSlice ? focusedSlice.slice.label : (centerLabel ?? labels.total)}\n </span>\n <span className=\"max-w-[40%] truncate text-lg font-semibold tabular-nums text-primary\">\n {focusedSlice ? formatValue(focusedSlice.value) : (centerValue ?? formatValue(total))}\n </span>\n {focusedSlice && (\n <span className=\"text-xs tabular-nums text-muted\">{formatShare(focusedSlice.share)}</span>\n )}\n </div>\n )}\n </div>\n )}\n {legend === \"toggle\" && (\n <ToggleLegend\n entries={legendEntries}\n hidden={hidden}\n onToggle={toggle}\n showSingle\n className=\"justify-center\"\n labels={{ legend: labels.legend }}\n />\n )}\n {legend === \"static\" && (\n <StaticLegend entries={legendEntries} className=\"justify-center\" aria-label={labels.legend} />\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["drawable","total","drawn","pieData"]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/pie-chart.tsx"],"sourcesContent":["// A part-to-whole chart: a donut (or a full pie), one slice per category, its ANGLE\n// the category's share of what is drawn. Lifted out of kastlan, where the dashboard's\n// occupancy card and the reports' maintenance card each drew a recharts `<Pie>` by\n// hand inside the kit's `ChartContainer` — with a legend that could not switch\n// anything, slices a keyboard could not reach, and colours from a local hex list.\n//\n// It uses the kit's own chart shell (`ChartContainer`, `ChartTooltipContent`) and\n// ramp (`paletteFor`), so it re-skins with the palette like every other chart, and the\n// kit's own legends (`ToggleLegend`, `StaticLegend`).\nimport { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport type { ComponentProps, ComponentPropsWithRef, KeyboardEvent, ReactNode } from \"react\";\nimport { Pie, PieChart as RechartsPieChart, Sector } from \"recharts\";\nimport type { PieLabelRenderProps, PieSectorShapeProps } from \"recharts\";\nimport { cn } from \"../lib/cn\";\nimport { dirOf, rovingTarget } from \"../lib/direction\";\nimport { shareFormat } from \"../lib/share-format\";\nimport { useKitLabels, useKitLocale, type LabelOverride } from \"../i18n/kit-labels\";\nimport { paletteFor } from \"../theme/chart-palette\";\nimport { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig } from \"./chart\";\nimport { DEFAULT_PIE_CHART_LABELS, type PieChartLabels } from \"./pie-chart-labels\";\nimport { StaticLegend, ToggleLegend, toggleHidden, type LegendEntry } from \"./toggle-legend\";\nimport { useTextSize } from \"../theme/text-size\";\nimport { chartHeightProps, type ChartHeight } from \"./chart-height\";\nimport { formatWithMinus } from \"../lib/minus\";\n\n/** One slice. */\nexport interface PieChartSlice {\n /** Stable key, handed back by {@link PieChartProps.onSliceClick} and used by the\n * legend's hidden set. Labels can collide; keys must not. */\n key: string;\n /** What the slice is called — in the legend, the tooltip and its accessible name. */\n label: string;\n /**\n * The slice's size. A value that is not a positive finite number — `null` for a\n * figure that is missing, a zero, a negative — is NOT drawn and not in the legend:\n * an angle cannot be negative, and a zero-degree slice is one a pointer and a\n * keyboard would both stop on to find nothing.\n */\n value: number | null | undefined;\n /** Overrides the by-index palette colour (`paletteFor(i)`, `i` the slice's index in\n * `data` AS GIVEN) — a status that has a colour of its own. Any CSS colour. */\n color?: string;\n}\n\n/**\n * Everything a `<div>` takes reaches the chart's root, so a test id or a `data-tour`\n * anchor can find it. `children` is the chart's own.\n */\nexport interface PieChartProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\"> {\n data: readonly PieChartSlice[];\n /** `\"donut\"` (default) leaves a hole with the total in it; `\"pie\"` is the full disc. */\n variant?: \"donut\" | \"pie\";\n /**\n * Height of the plot (the legend is under it): a CSS length (`\"20rem\"`,\n * `\"min(20rem, 50dvh)\"`), a Tailwind class, or px. Default `\"18.75rem\"` — 300 px at\n * Normal, growing with the text size so the slice labels keep their room (0.32,\n * docs/text-size-harmonization.md §10.10). A number does not follow the text size.\n */\n height?: ChartHeight;\n /** How a value is written in the tooltip, the slice's name, a `\"value\"` slice label\n * and the donut's total. Default: `Intl.NumberFormat` in `locale`. */\n formatValue?: (value: number) => string;\n /**\n * What is written beside each slice: its share (`\"percent\"`, the default), its\n * value, or nothing. Only on slices of at least {@link minLabelShare} — a label on a\n * sliver sits on its neighbour's. The figure is in the tooltip and the slice's name\n * either way.\n */\n sliceLabels?: \"percent\" | \"value\" | \"none\";\n /** The smallest share (0–1) that gets a slice label. Default 0.04. */\n minLabelShare?: number;\n /** Draw the donut's centre: {@link centerLabel} over {@link centerValue}. Default\n * `true`; ignored for `variant=\"pie\"`, which has no centre to draw in. */\n center?: boolean;\n /** The caption in the donut's centre. Default: `labels.total`. */\n centerLabel?: ReactNode;\n /** The figure in the donut's centre. Default: the drawn total through `formatValue`. */\n centerValue?: ReactNode;\n /**\n * The legend under the chart. `\"toggle\"` (default): each entry is a switch that\n * takes its slice off the chart, and the percentages are then of what is left —\n * the shares always read \"of this picture\". `\"static\"`: a key with nothing to press.\n */\n legend?: \"toggle\" | \"static\" | \"none\";\n /** The slices switched off, for a caller that owns the set (a URL, a saved view).\n * Left out, the chart keeps its own. */\n hiddenSlices?: ReadonlySet<string>;\n /** Called with the new hidden set whenever the legend switches a slice. */\n onHiddenSlicesChange?: (hidden: ReadonlySet<string>) => void;\n /** A slice was activated — by click, or Enter/Space on a focused slice. Makes each\n * slice a `role=\"button\"`. */\n onSliceClick?: (key: string, slice: PieChartSlice) => void;\n /**\n * Tag every figure ON SCREEN `data-private` — the tooltip bubble, the centre and\n * `\"value\"` slice labels — so the host's demo-mode rule blurs it. A percentage alone\n * reveals no amount, so `\"percent\"` labels stay readable, and the slices themselves\n * are not tagged: a blurred ring is no picture at all. So a slice's NAME says no\n * amount either (0.52): \"Occupied: 60%\" (`labels.sliceShare`), not `labels.slice`.\n */\n redact?: boolean;\n /** What to show with nothing to draw. Default: `labels.empty`. */\n empty?: ReactNode;\n /** BCP 47 tag for the numbers and percentages. Default: the provider's. */\n locale?: string;\n labels?: LabelOverride<PieChartLabels>;\n}\n\n/** A slice as drawn: only the ones with a positive value, in `data` order. */\ninterface DrawnSlice {\n slice: PieChartSlice;\n value: number;\n color: string;\n /** Of what is drawn, 0–1. */\n share: number;\n}\n\n// Slices carry their own `fill`, and `ChartTooltipContent` falls back to it for the\n// swatch, so no series config is needed — and none is wanted, because a config key is a\n// CSS identifier and a slice key is the caller's data (see `ChartStyle`).\nconst PIE_CONFIG: ChartConfig = {};\n\nconst RADIAN = Math.PI / 180;\n\n/** The shared tooltip with the bubble tagged `data-private` — see the treemap's\n * `RedactedTooltipContent` for why it has to BE the element recharts clones. */\nfunction RedactedTooltipContent(props: ComponentProps<typeof ChartTooltipContent>) {\n return (\n <div data-private>\n <ChartTooltipContent {...props} />\n </div>\n );\n}\n\n/**\n * Part-to-whole chart: a donut (or `variant=\"pie\"`), one slice per\n * {@link PieChartSlice}, coloured from the chart ramp by index.\n *\n * **Every slice is reachable from the keyboard.** The slices are ONE tab stop (a roving\n * tabindex, as in the kit's grids): ←/→ and ↑/↓ step to the next and previous slice —\n * clockwise from twelve o'clock, the order of the legend — with ←/→ following the\n * reading direction in RTL; Home/End go to the first and last. Each slice is named\n * \"Label: value (xx%)\" (`labels.slice`), because a colour and an angle are nothing a\n * screen reader can say — under `redact` \"Label: xx%\" (`labels.sliceShare`) — and\n * focusing one reads it out in the donut's centre. With `onSliceClick`, Enter and Space\n * activate it as a click does.\n *\n * The legend switches slices off (`legend=\"toggle\"`); the percentages are always of\n * what is drawn. The plot is not mirrored in RTL (see `ChartContainer`); the legend\n * and the centre follow the page.\n *\n * With nothing to draw it says so (`empty`) rather than drawing an empty box.\n *\n * Needs `recharts`, like everything behind `@eifi1/ui-kit/chart`.\n */\nexport function PieChart({\n data,\n variant = \"donut\",\n height = \"18.75rem\",\n formatValue: formatValueProp,\n sliceLabels = \"percent\",\n minLabelShare = 0.04,\n center = true,\n centerLabel,\n centerValue,\n legend = \"toggle\",\n hiddenSlices: hiddenProp,\n onHiddenSlicesChange,\n onSliceClick,\n redact = false,\n empty,\n locale: localeProp,\n labels: labelsProp,\n className,\n style,\n ...rest\n}: PieChartProps) {\n const labels = useKitLabels(\"pieChart\", DEFAULT_PIE_CHART_LABELS, labelsProp);\n const locale = useKitLocale(localeProp);\n // The slice labels are SVG text in px: they follow the text size by this (§10.10).\n const { scale } = useTextSize();\n const box = chartHeightProps(height);\n const numberFormat = useMemo(() => new Intl.NumberFormat(locale), [locale]);\n // Memoised, like everything the sectors are drawn from: they must not be rebuilt on\n // this component's own re-renders (see `pie` below).\n const formatValue = useCallback(\n (v: number) => (formatValueProp ? formatValueProp(v) : formatWithMinus(numberFormat, v)),\n [formatValueProp, numberFormat],\n );\n // One decimal under 10 %, the rule Treemap says its shares by too (lib/share-format).\n const formatShare = useMemo(() => shareFormat(locale), [locale]);\n\n const [ownHidden, setOwnHidden] = useState<ReadonlySet<string>>(() => new Set());\n const hidden = hiddenProp ?? ownHidden;\n const toggle = (key: string) => {\n const next = toggleHidden(hidden, key);\n if (hiddenProp === undefined) setOwnHidden(next);\n onHiddenSlicesChange?.(next);\n };\n\n // Coloured by index into `data` AS GIVEN, before anything is dropped, so a slice\n // keeps its colour when a neighbour is switched off or has no value.\n const { drawable, drawn, total, pieData } = useMemo(() => {\n const drawable = data\n .map((slice, i) => ({ slice, value: slice.value, color: slice.color ?? paletteFor(i) }))\n .filter(\n (d): d is { slice: PieChartSlice; value: number; color: string } =>\n typeof d.value === \"number\" && Number.isFinite(d.value) && d.value > 0,\n );\n const shown = drawable.filter((d) => !hidden.has(d.slice.key));\n const total = shown.reduce((acc, d) => acc + d.value, 0);\n const drawn: DrawnSlice[] = shown.map((d) => ({ ...d, share: total > 0 ? d.value / total : 0 }));\n const pieData = drawn.map((d) => ({\n key: d.slice.key,\n label: d.slice.label,\n value: d.value,\n fill: d.color,\n }));\n return { drawable, drawn, total, pieData };\n }, [data, hidden]);\n\n // The roving tab stop, and the slice that has focus (which the centre reads out).\n const [stop, setStop] = useState(0);\n const [focused, setFocused] = useState<number | undefined>(undefined);\n const current = Math.min(stop, Math.max(0, drawn.length - 1));\n const refs = useRef<(SVGGElement | null)[]>([]);\n const focusedSlice = focused !== undefined ? drawn[focused] : undefined;\n\n // Read through a ref: the sectors are drawn once per `data`, and a caller's inline\n // `onSliceClick` is a new function every render.\n const clickRef = useRef(onSliceClick);\n useLayoutEffect(() => {\n clickRef.current = onSliceClick;\n });\n const clickable = onSliceClick !== undefined;\n\n /**\n * One sector, wrapped in its keyboard stop. recharts renders it inside a layer of its\n * own that carries the pointer's hover and click, so those are untouched.\n *\n * Focus does NOT drive the recharts tooltip (`defaultIndex`), as it does in\n * `SeriesChart`: an active pie sector is re-parented into an extra layer, which\n * remounts this element and drops the focus it just took. The donut's centre is the\n * keyboard's read-out instead.\n */\n const shape = useCallback(\n (props: PieSectorShapeProps) => {\n const {\n index,\n isActive: _isActive,\n tabIndex: _tabIndex,\n animationElapsedTime: _elapsed,\n isAnimating: _animating,\n isEntrance: _entrance,\n // recharts hands the shape its own React `key` among the props. Spread into\n // JSX, a `key` inside a props object is a warning on every render (React 19),\n // so it is lifted out here and passed on its own.\n key,\n ...sector\n } = props as PieSectorShapeProps & {\n animationElapsedTime?: number;\n isAnimating?: boolean;\n isEntrance?: boolean;\n key?: string | number | null;\n };\n const d = drawn[index];\n if (!d) return <Sector key={key ?? undefined} {...sector} />;\n const onKeyDown = (event: KeyboardEvent<SVGGElement>) => {\n // Round the circle: past the last slice is the first, as the eye goes.\n const target = rovingTarget(event.key, dirOf(event.currentTarget), index, drawn.length, true);\n if (target !== undefined) {\n event.preventDefault();\n refs.current[target]?.focus();\n } else if (clickable && (event.key === \"Enter\" || event.key === \" \")) {\n event.preventDefault();\n clickRef.current?.(d.slice.key, d.slice);\n }\n };\n return (\n <g\n ref={(node) => {\n refs.current[index] = node;\n }}\n role={clickable ? \"button\" : \"img\"}\n // The first slice until the effect below moves the stop.\n tabIndex={index === 0 ? 0 : -1}\n // Under `redact` with no amount: the ring is left untagged so it stays a picture,\n // and its names are attributes a session replay or a redaction keyed on\n // `[data-private]` reads as they are — \"Occupied: €6.00 (60%)\" told the amount the\n // blurred centre and tooltip hid (0.52 code review; Marcel's decision).\n aria-label={\n redact\n ? labels.sliceShare(d.slice.label, formatShare(d.share))\n : labels.slice(d.slice.label, formatValue(d.value), formatShare(d.share))\n }\n data-slice={d.slice.key}\n onKeyDown={onKeyDown}\n onFocus={() => {\n setStop(index);\n setFocused(index);\n }}\n onBlur={() => setFocused(undefined)}\n // CSS beats a presentation attribute, so this recolours the sector's own\n // stroke for as long as it has keyboard focus.\n className=\"outline-none [&:focus-visible>path]:stroke-primary\"\n >\n <Sector key={key ?? undefined} {...sector} />\n </g>\n );\n },\n [drawn, labels, formatValue, formatShare, clickable, redact],\n );\n\n const label = useCallback(\n (props: PieLabelRenderProps) => {\n const d = drawn[props.index ?? -1];\n const cx = Number(props.cx);\n const cy = Number(props.cy);\n const radius = Number(props.outerRadius);\n const mid = Number(props.midAngle);\n if (!d || d.share < minLabelShare || ![cx, cy, radius, mid].every(Number.isFinite)) return null;\n const r = radius + 10 * scale;\n const x = cx + r * Math.cos(-mid * RADIAN);\n const y = cy + r * Math.sin(-mid * RADIAN);\n const isValue = sliceLabels === \"value\";\n return (\n <text\n x={x}\n y={y}\n textAnchor={x >= cx ? \"start\" : \"end\"}\n dominantBaseline=\"central\"\n fontSize={11 * scale}\n fill=\"var(--text-secondary)\"\n data-private={redact && isValue ? \"\" : undefined}\n style={{ pointerEvents: \"none\" }}\n >\n {isValue ? formatValue(d.value) : formatShare(d.share)}\n </text>\n );\n },\n [drawn, minLabelShare, sliceLabels, redact, formatValue, formatShare, scale],\n );\n\n const withLabels = sliceLabels !== \"none\";\n // Room outside the ring for the labels, or the ring fills the box.\n const outer = withLabels ? 72 : 88;\n const inner = variant === \"donut\" ? Math.round(outer * 0.6) : 0;\n\n /**\n * The `<Pie>`, as ONE element for as long as what it draws is unchanged. recharts\n * keys its sectors by an animation id that is renewed whenever the Pie's props object\n * is — i.e. on every render of the Pie — so re-rendering it remounts every sector,\n * the one with focus included, which dropped the focus to <body> on the very state\n * update that focusing a slice makes. An unchanged element is one React does not\n * re-render, so the sectors stay put, and the roving tab stop is moved on them in\n * place (the effect below).\n */\n const pie = useMemo(\n () => (\n <Pie\n data={pieData}\n dataKey=\"value\"\n nameKey=\"label\"\n cx=\"50%\"\n cy=\"50%\"\n outerRadius={`${outer}%`}\n innerRadius={`${inner}%`}\n startAngle={90}\n endAngle={-270}\n // The card showing through between the slices, as the treemap's gutter.\n stroke=\"var(--bg-surface)\"\n strokeWidth={2}\n rootTabIndex={-1}\n isAnimationActive={false}\n shape={shape}\n label={withLabels ? label : false}\n labelLine={false}\n onClick={\n clickable\n ? (_entry: unknown, index: number) => {\n const d = drawn[index];\n if (d) clickRef.current?.(d.slice.key, d.slice);\n }\n : undefined\n }\n />\n ),\n [pieData, drawn, shape, label, withLabels, outer, inner, clickable],\n );\n\n // The roving tab stop, moved on the sectors in place (they are not re-rendered).\n useEffect(() => {\n refs.current.forEach((el, i) => el?.setAttribute(\"tabindex\", i === current ? \"0\" : \"-1\"));\n });\n\n const legendEntries: LegendEntry[] = drawable.map((d) => ({\n key: d.slice.key,\n label: d.slice.label,\n color: d.color,\n }));\n\n const emptyBox = (\n <div className={cn(\"flex items-center justify-center text-sm text-muted\", box.className)} style={box.style}>\n {empty ?? labels.empty}\n </div>\n );\n\n // Nothing drawable at all: no chart, and no legend of switches for nothing.\n if (drawable.length === 0) {\n return (\n <div {...rest} className={cn(\"w-full\", className)} style={style}>\n {emptyBox}\n </div>\n );\n }\n\n const Content = redact ? RedactedTooltipContent : ChartTooltipContent;\n\n return (\n <div {...rest} className={cn(\"w-full\", className)} style={style}>\n {drawn.length === 0 ? (\n // Every slice switched off: the legend below is how they come back.\n emptyBox\n ) : (\n <div role=\"group\" aria-label={labels.slices} className={cn(\"relative\", box.className)} style={box.style}>\n <ChartContainer\n config={PIE_CONFIG}\n className={cn(\"h-full\", clickable && \"[&_.recharts-pie-sector]:cursor-pointer\")}\n >\n <RechartsPieChart accessibilityLayer={false}>\n <ChartTooltip\n content={\n <Content\n hideLabel\n valueFormatter={(v) => `${formatValue(v)} (${formatShare(total > 0 ? v / total : 0)})`}\n />\n }\n />\n {pie}\n </RechartsPieChart>\n </ChartContainer>\n {variant === \"donut\" && center && (\n <div\n data-private={redact ? \"\" : undefined}\n // While a slice has keyboard focus the centre reads it out — the\n // keyboard's tooltip. Hidden from assistive technology then: the slice's\n // own name has just said the same.\n aria-hidden={focusedSlice ? true : undefined}\n className=\"pointer-events-none absolute inset-0 flex flex-col items-center justify-center text-center\"\n >\n {/* The label wraps from Large up, inside the hole's 40 % (text size\n §10.17, round 0.34 T1: the sweep read \"Spent in Sept…\"). */}\n <span className=\"max-w-[40%] truncate-until-large text-xs text-muted\">\n {focusedSlice ? focusedSlice.slice.label : (centerLabel ?? labels.total)}\n </span>\n <span className=\"max-w-[40%] truncate text-lg font-semibold tabular-nums text-primary\">\n {focusedSlice ? formatValue(focusedSlice.value) : (centerValue ?? formatValue(total))}\n </span>\n {focusedSlice && (\n <span className=\"text-xs tabular-nums text-muted\">{formatShare(focusedSlice.share)}</span>\n )}\n </div>\n )}\n </div>\n )}\n {legend === \"toggle\" && (\n <ToggleLegend\n entries={legendEntries}\n hidden={hidden}\n onToggle={toggle}\n showSingle\n className=\"justify-center\"\n labels={{ legend: labels.legend }}\n />\n )}\n {legend === \"static\" && (\n <StaticLegend entries={legendEntries} className=\"justify-center\" aria-label={labels.legend} />\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["drawable","total","drawn","pieData"]}
|
|
@@ -26,6 +26,9 @@ export interface PopoverProps extends Omit<ComponentPropsWithoutRef<"div">, "chi
|
|
|
26
26
|
toggle: () => void;
|
|
27
27
|
ref: RefObject<HTMLButtonElement | null>;
|
|
28
28
|
}) => ReactNode;
|
|
29
|
+
/** The panel's content. `close` shuts the panel and, when focus is in it, puts focus
|
|
30
|
+
* on the trigger before returning — so a dialog the same handler opens gives it back
|
|
31
|
+
* there (0.52). */
|
|
29
32
|
children: (close: () => void) => ReactNode;
|
|
30
33
|
/**
|
|
31
34
|
* The panel's width: `"18rem"` (the default) grows with the text size — the panel
|
|
@@ -32,6 +32,11 @@ function Popover({
|
|
|
32
32
|
const [open, setOpen] = useState(false);
|
|
33
33
|
const [dir, setDir] = useState("ltr");
|
|
34
34
|
const close = () => setOpen(false);
|
|
35
|
+
const closeFromPanel = () => {
|
|
36
|
+
const focusInside = panelRef.current?.contains(document.activeElement) ?? false;
|
|
37
|
+
setOpen(false);
|
|
38
|
+
if (focusInside) triggerRef.current?.focus();
|
|
39
|
+
};
|
|
35
40
|
const toggle = () => {
|
|
36
41
|
setDir(dirOf(triggerRef.current));
|
|
37
42
|
setOpen((v) => !v);
|
|
@@ -106,7 +111,8 @@ function Popover({
|
|
|
106
111
|
"z-50 overflow-y-auto rounded-md border border-subtle bg-surface p-3 shadow-lg outline-none",
|
|
107
112
|
className
|
|
108
113
|
),
|
|
109
|
-
|
|
114
|
+
// eslint-disable-next-line react-hooks/refs -- handed to the items' handlers: it reads the trigger and the focus when an item is chosen, never while rendering
|
|
115
|
+
children: children(closeFromPanel)
|
|
110
116
|
}
|
|
111
117
|
),
|
|
112
118
|
document.body
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/popover.tsx"],"sourcesContent":["import { useMemo, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, KeyboardEvent, ReactNode, Ref, RefObject } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { anchoredInline, useAnchoredPanel, viewportSize } from \"../hooks/use-anchored-panel\";\nimport { runOwnKeyDown, takeEscape, useDismissLayer, useOutsideClick } from \"../hooks/use-dismiss\";\nimport { useFocusTrap } from \"../hooks/use-focus-trap\";\nimport { lengthPx, useRemPx, type RemLength } from \"../hooks/use-breakpoint\";\nimport { cn } from \"../lib/cn\";\nimport { dirOf } from \"../lib/direction\";\nimport type { Direction } from \"../lib/direction\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { DEFAULT_POPOVER_LABELS } from \"./popover-labels\";\n// From ../lib/refs, not field-parts (round 0.36): field-parts renders a HelpPopover for a\n// field's `help`, so importing from there would close a cycle, as it did for Tooltip.\nimport { mergeRefs } from \"../lib/refs\";\n\n/** The panel's accessible name. A `role=\"dialog\"` with no name announces as \"dialog\"\n * and nothing else, which is worse than the unlabelled div it replaced — so it is a\n * prop with an English default, like every other string this package renders. Callers\n * should pass the control's own name (\"Filter\", \"Choose a date\"); the default is only\n * there so an unmigrated call site is still named. */\nexport interface PopoverLabels {\n panel: string;\n}\n\n// Defined in ./popover-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_POPOVER_LABELS };\n\n/**\n * Exported, and extending the `<div>` attributes, so a consumer wrapping this — which is\n * what `DateField` and `FilterPopover` in this very package do — can hand anything\n * through to the PANEL: a `data-tour` anchor for the kit's own guided tour, a test id,\n * an `aria-describedby`, or an `aria-label` naming this particular popover.\n *\n * `children` is omitted from those attributes rather than inherited: here it is a render\n * prop taking the panel's `close`, not a `ReactNode`, so the two cannot be reconciled.\n */\nexport interface PopoverProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\"> {\n trigger: (state: { open: boolean; toggle: () => void; ref: RefObject<HTMLButtonElement | null> }) => ReactNode;\n children: (close: () => void) => ReactNode;\n /**\n * The panel's width: `\"18rem\"` (the default) grows with the text size — the panel\n * holds text, and a 288 px calendar at 150 % cut its own month off (0.32,\n * docs/text-size-harmonization.md §3.2). A plain number is px, fixed, as before.\n * Either way the panel is never wider than the viewport less 8 px a side: 18rem is\n * 432 px at Extra large, more than a 360 px phone has.\n */\n width?: number | RemLength;\n labels?: LabelOverride<PopoverLabels>;\n /** Id for the panel, so a trigger wearing `role=\"combobox\"` can `aria-controls` it.\n * Required by ARIA on that role, and the panel is rendered here rather than by the\n * caller, so the id has to come in from outside. */\n panelId?: string;\n /** Extra classes for the PANEL. The trigger is the caller's own element. */\n className?: string;\n /** The PANEL, while it is open — where the rest of the props go (0.34, API-2, refactor\n * audit 2026-10). Merged with the panel's own ref (focus trap, outside click), so\n * neither loses it. The trigger is the caller's own element, ref and all. */\n ref?: Ref<HTMLDivElement>;\n}\n\n/** 288 px at Normal, in rem so it follows the text size (§3.2). */\nconst POPOVER_WIDTH: RemLength = \"18rem\";\n\n/** The gap the panel keeps from either edge of the viewport, in px. */\nconst EDGE = 8;\n\n/**\n * Portal-rendered popover anchored under its trigger button. The panel is fixed\n * to the viewport (so table/overflow ancestors can't clip it) and re-aligns on\n * scroll/resize; it closes on outside-click and Escape.\n *\n * ## Trapped, not merely focused — and why, because it is the closer call\n *\n * The audit's finding was that this panel *\"portals out of the tab order with no role\n * and no focus move\"*: Tab from the trigger went straight past the panel into the page\n * behind it, so the calendar, the calculator keypad and every column filter in the kit\n * were mouse-only. Two shapes fix that, and a popover is not automatically a dialog, so\n * this was chosen rather than inherited:\n *\n * - **The light one** — move focus in on open, hand it back on close, and let Tab walk\n * OUT of the panel into whatever follows the trigger. That is the right shape for a\n * non-modal popover sitting inline in the document.\n * - **The trap** — Tab cycles inside the panel; Escape and a click outside are the\n * ways out. What is here.\n *\n * The deciding fact is the portal. This panel is rendered to the END of `<body>`, not\n * beside its trigger, so \"let Tab walk out\" does not mean \"carry on from the trigger\" —\n * it means land at the end of the document, several hundred controls from where the\n * user was. Tabbing out of it would be a worse place to arrive than the one the finding\n * complains about, and fixing that properly needs a sentinel-and-return dance that\n * nothing in this package has. The trap gets the user back to the trigger every time,\n * because `useFocusTrap` restores focus on close.\n *\n * And the panels this kit actually opens are self-contained: pick a date, tap out a\n * sum, narrow a column. Each of them commits and closes, and each already has two\n * documented ways out wired above — Escape and an outside press — so containment costs\n * a keyboard user nothing they had.\n *\n * **It stays non-modal in every other respect**, deliberately: no backdrop, no body\n * scroll lock, and no `aria-modal`, so the page behind is still readable by a screen\n * reader, still scrollable, and still clickable — clicking it is what dismisses this.\n * Only Tab is contained. A popover that took the page away as well would be a\n * {@link Modal}, and there is one of those.\n */\nexport function Popover({\n trigger,\n children,\n width = POPOVER_WIDTH,\n labels,\n panelId,\n className,\n style,\n \"aria-label\": ariaLabel,\n onKeyDown: onKeyDownProp,\n ref,\n ...rest\n}: PopoverProps) {\n const triggerRef = useRef<HTMLButtonElement>(null);\n const panelRef = useRef<HTMLDivElement>(null);\n const setPanel = useMemo(() => mergeRefs(panelRef, ref), [ref]);\n const [open, setOpen] = useState(false);\n // The trigger's reading direction, read when it is pressed. The panel is portalled to\n // <body>, out of any `dir=\"rtl\"` subtree the trigger sits in, so without carrying it\n // across a right-to-left calendar or filter rendered left-to-right.\n const [dir, setDir] = useState<Direction>(\"ltr\");\n const close = () => setOpen(false);\n const toggle = () => {\n setDir(dirOf(triggerRef.current));\n setOpen((v) => !v);\n };\n // prop > `<UiKitProvider labels={{ popover }}>` > English. The provider's\n // `popover.panel` is only the last-resort name for a panel no caller named.\n const text = useKitLabels(\"popover\", DEFAULT_POPOVER_LABELS, labels);\n\n // Align the panel's END edge to the trigger's — right in LTR, left in RTL; it was\n // always `rect.right`, so an RTL panel hung off the wrong side — and clamp it\n // horizontally; the\n // hook owns the vertical half — below by default, flipped above (and height-capped)\n // when that is where the room is, e.g. once a mobile keyboard has eaten the bottom\n // of the screen (feedback #135). It re-measures on scroll/resize.\n const placement = useAnchoredPanel(triggerRef, open);\n const rect = placement.rect;\n // In px for the arithmetic below, at the text size in force; capped at the glass — the\n // root's client width, not `innerWidth`, which on a phone is the layout viewport grown\n // by whatever overflows (live #381, Wave 2 DUP-7).\n const remPx = useRemPx();\n const glass = rect ? viewportSize().width : 0;\n const panelWidth = rect ? Math.min(lengthPx(width, remPx), Math.max(0, glass - 2 * EDGE)) : 0;\n const pos = rect\n ? {\n // Flipped above, the panel hangs from its bottom edge, so one shorter than its\n // cap still meets the trigger (keksdose K1); the edge not in use is cleared.\n top: placement.above ? undefined : placement.top,\n bottom: placement.above ? placement.bottom : undefined,\n left: anchoredInline(rect, panelWidth, { dir, align: \"end\", edge: EDGE, viewportWidth: glass }).left,\n }\n : null;\n\n // A press in a list opened from inside — a CurrencySelect's, a combobox's, another\n // popover's — reaches the panel through the React tree and is inside (dev#623).\n const markInside = useOutsideClick([triggerRef, panelRef], close, open);\n // A layer on the Escape stack while open (Wave 2, DUP-1): a list opened inside closes\n // first, and the dialog this popover sits in only after it — the calculator on an\n // amount field in a Modal is exactly that.\n const layer = useDismissLayer(open, close);\n\n useFocusTrap(panelRef, {\n active: open && pos !== null,\n // The panel keeps a focus one of its own children has already taken. `FilterPopover`\n // autofocuses its text box on mount — that is what the press on \"Filter\" was FOR,\n // and it is a prop there precisely so the popover keeps it — and a trap that\n // unconditionally focused the container would take it straight back off again,\n // one effect later and invisibly.\n initialFocus: () => {\n const panel = panelRef.current;\n const active = document.activeElement;\n return panel?.contains(active) ? (active as HTMLElement) : panel;\n },\n });\n\n const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n // A caller's own handler first (0.32.2: RowActions stops Enter / Space at the panel);\n // it used to be dropped, overwritten by this one. A `preventDefault()` on Escape\n // there keeps the popover open — the owner's claim, as on the dialogs. And as there,\n // the handler sees Escape only while this popover is the layer on top: a tooltip or a\n // menu open over it takes that press first (round 0.35 R2, the dialogs' rule).\n runOwnKeyDown(e, onKeyDownProp, layer);\n // Focus is in here, the normal case: the key is handed to the top layer and STOPPED,\n // so it does not travel on up the React tree to a dialog around the popover.\n takeEscape(e);\n };\n\n return (\n <>\n {trigger({ open, toggle, ref: triggerRef })}\n {open &&\n pos &&\n createPortal(\n /* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- the\n dialog container, per the APG dialog pattern: `onKeyDown` is Escape-to-close for\n whatever inside it holds focus. */\n <div\n // `...rest` first: the role, the id the trigger's `aria-controls` points at,\n // the `tabIndex` the trap needs and the measured position are what make this\n // a popover, and a caller reaching for a `data-tour` anchor must not be able\n // to remove one of them by accident.\n {...rest}\n ref={setPanel}\n // A kit layer, for the tour's layer order (0.50, docs/tours-harmonization.md §11.6).\n data-kit-layer=\"\"\n role=\"dialog\"\n id={panelId}\n // The caller's own `aria-label` wins over {@link PopoverLabels.panel}, which\n // is the package's English fallback for an unnamed panel. Both name the same\n // element; the DOM spelling is the one being standardised on, so where both\n // are present it is the one that survives.\n aria-label={ariaLabel ?? text.panel}\n dir={rest.dir ?? dir}\n // Without it the panel cannot take focus at all, and the trap above would\n // silently do nothing on a panel whose children are not yet tabbable.\n tabIndex={-1}\n onKeyDown={handleKeyDown}\n onPointerDownCapture={(e) => {\n rest.onPointerDownCapture?.(e);\n markInside(e);\n }}\n style={{\n // Merged over the caller's, never replaced by it: these are measured\n // against the trigger every scroll and resize, and a panel that dropped\n // them would paint at the top-left of the viewport.\n ...style,\n position: \"fixed\",\n top: pos.top,\n bottom: pos.bottom,\n left: pos.left,\n width: panelWidth,\n maxHeight: placement.maxHeight,\n }}\n className={cn(\n \"z-50 overflow-y-auto rounded-md border border-subtle bg-surface p-3 shadow-lg outline-none\",\n className,\n )}\n >\n {children(close)}\n </div>,\n document.body,\n )}\n </>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/popover.tsx"],"sourcesContent":["import { useMemo, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, KeyboardEvent, ReactNode, Ref, RefObject } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { anchoredInline, useAnchoredPanel, viewportSize } from \"../hooks/use-anchored-panel\";\nimport { runOwnKeyDown, takeEscape, useDismissLayer, useOutsideClick } from \"../hooks/use-dismiss\";\nimport { useFocusTrap } from \"../hooks/use-focus-trap\";\nimport { lengthPx, useRemPx, type RemLength } from \"../hooks/use-breakpoint\";\nimport { cn } from \"../lib/cn\";\nimport { dirOf } from \"../lib/direction\";\nimport type { Direction } from \"../lib/direction\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { DEFAULT_POPOVER_LABELS } from \"./popover-labels\";\n// From ../lib/refs, not field-parts (round 0.36): field-parts renders a HelpPopover for a\n// field's `help`, so importing from there would close a cycle, as it did for Tooltip.\nimport { mergeRefs } from \"../lib/refs\";\n\n/** The panel's accessible name. A `role=\"dialog\"` with no name announces as \"dialog\"\n * and nothing else, which is worse than the unlabelled div it replaced — so it is a\n * prop with an English default, like every other string this package renders. Callers\n * should pass the control's own name (\"Filter\", \"Choose a date\"); the default is only\n * there so an unmigrated call site is still named. */\nexport interface PopoverLabels {\n panel: string;\n}\n\n// Defined in ./popover-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_POPOVER_LABELS };\n\n/**\n * Exported, and extending the `<div>` attributes, so a consumer wrapping this — which is\n * what `DateField` and `FilterPopover` in this very package do — can hand anything\n * through to the PANEL: a `data-tour` anchor for the kit's own guided tour, a test id,\n * an `aria-describedby`, or an `aria-label` naming this particular popover.\n *\n * `children` is omitted from those attributes rather than inherited: here it is a render\n * prop taking the panel's `close`, not a `ReactNode`, so the two cannot be reconciled.\n */\nexport interface PopoverProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\"> {\n trigger: (state: { open: boolean; toggle: () => void; ref: RefObject<HTMLButtonElement | null> }) => ReactNode;\n /** The panel's content. `close` shuts the panel and, when focus is in it, puts focus\n * on the trigger before returning — so a dialog the same handler opens gives it back\n * there (0.52). */\n children: (close: () => void) => ReactNode;\n /**\n * The panel's width: `\"18rem\"` (the default) grows with the text size — the panel\n * holds text, and a 288 px calendar at 150 % cut its own month off (0.32,\n * docs/text-size-harmonization.md §3.2). A plain number is px, fixed, as before.\n * Either way the panel is never wider than the viewport less 8 px a side: 18rem is\n * 432 px at Extra large, more than a 360 px phone has.\n */\n width?: number | RemLength;\n labels?: LabelOverride<PopoverLabels>;\n /** Id for the panel, so a trigger wearing `role=\"combobox\"` can `aria-controls` it.\n * Required by ARIA on that role, and the panel is rendered here rather than by the\n * caller, so the id has to come in from outside. */\n panelId?: string;\n /** Extra classes for the PANEL. The trigger is the caller's own element. */\n className?: string;\n /** The PANEL, while it is open — where the rest of the props go (0.34, API-2, refactor\n * audit 2026-10). Merged with the panel's own ref (focus trap, outside click), so\n * neither loses it. The trigger is the caller's own element, ref and all. */\n ref?: Ref<HTMLDivElement>;\n}\n\n/** 288 px at Normal, in rem so it follows the text size (§3.2). */\nconst POPOVER_WIDTH: RemLength = \"18rem\";\n\n/** The gap the panel keeps from either edge of the viewport, in px. */\nconst EDGE = 8;\n\n/**\n * Portal-rendered popover anchored under its trigger button. The panel is fixed\n * to the viewport (so table/overflow ancestors can't clip it) and re-aligns on\n * scroll/resize; it closes on outside-click and Escape.\n *\n * ## Trapped, not merely focused — and why, because it is the closer call\n *\n * The audit's finding was that this panel *\"portals out of the tab order with no role\n * and no focus move\"*: Tab from the trigger went straight past the panel into the page\n * behind it, so the calendar, the calculator keypad and every column filter in the kit\n * were mouse-only. Two shapes fix that, and a popover is not automatically a dialog, so\n * this was chosen rather than inherited:\n *\n * - **The light one** — move focus in on open, hand it back on close, and let Tab walk\n * OUT of the panel into whatever follows the trigger. That is the right shape for a\n * non-modal popover sitting inline in the document.\n * - **The trap** — Tab cycles inside the panel; Escape and a click outside are the\n * ways out. What is here.\n *\n * The deciding fact is the portal. This panel is rendered to the END of `<body>`, not\n * beside its trigger, so \"let Tab walk out\" does not mean \"carry on from the trigger\" —\n * it means land at the end of the document, several hundred controls from where the\n * user was. Tabbing out of it would be a worse place to arrive than the one the finding\n * complains about, and fixing that properly needs a sentinel-and-return dance that\n * nothing in this package has. The trap gets the user back to the trigger every time,\n * because `useFocusTrap` restores focus on close.\n *\n * And the panels this kit actually opens are self-contained: pick a date, tap out a\n * sum, narrow a column. Each of them commits and closes, and each already has two\n * documented ways out wired above — Escape and an outside press — so containment costs\n * a keyboard user nothing they had.\n *\n * **It stays non-modal in every other respect**, deliberately: no backdrop, no body\n * scroll lock, and no `aria-modal`, so the page behind is still readable by a screen\n * reader, still scrollable, and still clickable — clicking it is what dismisses this.\n * Only Tab is contained. A popover that took the page away as well would be a\n * {@link Modal}, and there is one of those.\n */\nexport function Popover({\n trigger,\n children,\n width = POPOVER_WIDTH,\n labels,\n panelId,\n className,\n style,\n \"aria-label\": ariaLabel,\n onKeyDown: onKeyDownProp,\n ref,\n ...rest\n}: PopoverProps) {\n const triggerRef = useRef<HTMLButtonElement>(null);\n const panelRef = useRef<HTMLDivElement>(null);\n const setPanel = useMemo(() => mergeRefs(panelRef, ref), [ref]);\n const [open, setOpen] = useState(false);\n // The trigger's reading direction, read when it is pressed. The panel is portalled to\n // <body>, out of any `dir=\"rtl\"` subtree the trigger sits in, so without carrying it\n // across a right-to-left calendar or filter rendered left-to-right.\n const [dir, setDir] = useState<Direction>(\"ltr\");\n const close = () => setOpen(false);\n // The `close` the children get also hands focus to the trigger, AT ONCE, when it is\n // inside the panel. The trap restores it too, but only after the commit — and a dialog\n // opened in the same handler records its own restore target during that commit's\n // render: the menu item that is about to go. RowActions' \"Delete\" asks first, and the\n // answered confirm dropped focus on <body>, mid-table (refactor audit 2026-10-30,\n // INP-2). Escape and an outside press keep the plain close: nothing opens after them.\n const closeFromPanel = () => {\n const focusInside = panelRef.current?.contains(document.activeElement) ?? false;\n setOpen(false);\n if (focusInside) triggerRef.current?.focus();\n };\n const toggle = () => {\n setDir(dirOf(triggerRef.current));\n setOpen((v) => !v);\n };\n // prop > `<UiKitProvider labels={{ popover }}>` > English. The provider's\n // `popover.panel` is only the last-resort name for a panel no caller named.\n const text = useKitLabels(\"popover\", DEFAULT_POPOVER_LABELS, labels);\n\n // Align the panel's END edge to the trigger's — right in LTR, left in RTL; it was\n // always `rect.right`, so an RTL panel hung off the wrong side — and clamp it\n // horizontally; the\n // hook owns the vertical half — below by default, flipped above (and height-capped)\n // when that is where the room is, e.g. once a mobile keyboard has eaten the bottom\n // of the screen (feedback #135). It re-measures on scroll/resize.\n const placement = useAnchoredPanel(triggerRef, open);\n const rect = placement.rect;\n // In px for the arithmetic below, at the text size in force; capped at the glass — the\n // root's client width, not `innerWidth`, which on a phone is the layout viewport grown\n // by whatever overflows (live #381, Wave 2 DUP-7).\n const remPx = useRemPx();\n const glass = rect ? viewportSize().width : 0;\n const panelWidth = rect ? Math.min(lengthPx(width, remPx), Math.max(0, glass - 2 * EDGE)) : 0;\n const pos = rect\n ? {\n // Flipped above, the panel hangs from its bottom edge, so one shorter than its\n // cap still meets the trigger (keksdose K1); the edge not in use is cleared.\n top: placement.above ? undefined : placement.top,\n bottom: placement.above ? placement.bottom : undefined,\n left: anchoredInline(rect, panelWidth, { dir, align: \"end\", edge: EDGE, viewportWidth: glass }).left,\n }\n : null;\n\n // A press in a list opened from inside — a CurrencySelect's, a combobox's, another\n // popover's — reaches the panel through the React tree and is inside (dev#623).\n const markInside = useOutsideClick([triggerRef, panelRef], close, open);\n // A layer on the Escape stack while open (Wave 2, DUP-1): a list opened inside closes\n // first, and the dialog this popover sits in only after it — the calculator on an\n // amount field in a Modal is exactly that.\n const layer = useDismissLayer(open, close);\n\n useFocusTrap(panelRef, {\n active: open && pos !== null,\n // The panel keeps a focus one of its own children has already taken. `FilterPopover`\n // autofocuses its text box on mount — that is what the press on \"Filter\" was FOR,\n // and it is a prop there precisely so the popover keeps it — and a trap that\n // unconditionally focused the container would take it straight back off again,\n // one effect later and invisibly.\n initialFocus: () => {\n const panel = panelRef.current;\n const active = document.activeElement;\n return panel?.contains(active) ? (active as HTMLElement) : panel;\n },\n });\n\n const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n // A caller's own handler first (0.32.2: RowActions stops Enter / Space at the panel);\n // it used to be dropped, overwritten by this one. A `preventDefault()` on Escape\n // there keeps the popover open — the owner's claim, as on the dialogs. And as there,\n // the handler sees Escape only while this popover is the layer on top: a tooltip or a\n // menu open over it takes that press first (round 0.35 R2, the dialogs' rule).\n runOwnKeyDown(e, onKeyDownProp, layer);\n // Focus is in here, the normal case: the key is handed to the top layer and STOPPED,\n // so it does not travel on up the React tree to a dialog around the popover.\n takeEscape(e);\n };\n\n return (\n <>\n {trigger({ open, toggle, ref: triggerRef })}\n {open &&\n pos &&\n createPortal(\n /* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- the\n dialog container, per the APG dialog pattern: `onKeyDown` is Escape-to-close for\n whatever inside it holds focus. */\n <div\n // `...rest` first: the role, the id the trigger's `aria-controls` points at,\n // the `tabIndex` the trap needs and the measured position are what make this\n // a popover, and a caller reaching for a `data-tour` anchor must not be able\n // to remove one of them by accident.\n {...rest}\n ref={setPanel}\n // A kit layer, for the tour's layer order (0.50, docs/tours-harmonization.md §11.6).\n data-kit-layer=\"\"\n role=\"dialog\"\n id={panelId}\n // The caller's own `aria-label` wins over {@link PopoverLabels.panel}, which\n // is the package's English fallback for an unnamed panel. Both name the same\n // element; the DOM spelling is the one being standardised on, so where both\n // are present it is the one that survives.\n aria-label={ariaLabel ?? text.panel}\n dir={rest.dir ?? dir}\n // Without it the panel cannot take focus at all, and the trap above would\n // silently do nothing on a panel whose children are not yet tabbable.\n tabIndex={-1}\n onKeyDown={handleKeyDown}\n onPointerDownCapture={(e) => {\n rest.onPointerDownCapture?.(e);\n markInside(e);\n }}\n style={{\n // Merged over the caller's, never replaced by it: these are measured\n // against the trigger every scroll and resize, and a panel that dropped\n // them would paint at the top-left of the viewport.\n ...style,\n position: \"fixed\",\n top: pos.top,\n bottom: pos.bottom,\n left: pos.left,\n width: panelWidth,\n maxHeight: placement.maxHeight,\n }}\n className={cn(\n \"z-50 overflow-y-auto rounded-md border border-subtle bg-surface p-3 shadow-lg outline-none\",\n className,\n )}\n >\n {\n // eslint-disable-next-line react-hooks/refs -- handed to the items' handlers: it reads the trigger and the focus when an item is chosen, never while rendering\n children(closeFromPanel)\n }\n </div>,\n document.body,\n )}\n </>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -3,6 +3,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { isValidElement, useContext } from "react";
|
|
4
4
|
import { MoreHorizontal } from "lucide-react";
|
|
5
5
|
import { cn } from "../lib/cn.js";
|
|
6
|
+
import { hasContent } from "../lib/presence.js";
|
|
6
7
|
import { useLargeText } from "../hooks/use-large-text.js";
|
|
7
8
|
import { useKitLabels } from "../i18n/kit-labels.js";
|
|
8
9
|
import { Popover } from "./popover.js";
|
|
@@ -180,7 +181,7 @@ function stopActivationKeys(e) {
|
|
|
180
181
|
}
|
|
181
182
|
function useActionHref(action) {
|
|
182
183
|
const reason = useCommitReason(action.commit, action.disabledReason);
|
|
183
|
-
const refused = reason
|
|
184
|
+
const refused = hasContent(reason);
|
|
184
185
|
return action.href !== void 0 && !action.pending && !refused ? action.href : void 0;
|
|
185
186
|
}
|
|
186
187
|
function MenuAction({
|