UNPKG

@awsui/components-react

Version:

On July 19th, 2022, we launched [Cloudscape Design System](https://cloudscape.design). Cloudscape is an evolution of AWS-UI. It consists of user interface guidelines, front-end components, design resources, and development tools for building intuitive, en

171 lines • 8.53 kB
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { useEffect, useMemo, useRef, useState } from 'react'; import { useMergeRefs, useReducedMotion, warnOnce } from '@awsui/component-toolkit/internal'; import { getBaseProps } from '../internal/base-component'; import { useDebounceCallback } from '../internal/hooks/use-debounce-callback'; import { useVisualRefresh } from '../internal/hooks/use-visual-mode'; import { isDevelopment } from '../internal/is-development'; import { persistFlashbarDismiss, retrieveFlashbarDismiss } from '../internal/persistence'; import { focusFlashById, focusFlashFocusableArea } from './flash'; import { FOCUS_DEBOUNCE_DELAY } from './utils'; import styles from './styles.css.js'; // Exported for testing export const handleFlashDismissedInternal = (dismissedId, items, refCurrent, flashRefsCurrent) => { var _a; if (!items || !dismissedId || !refCurrent) { return; } const dismissedIndex = items.findIndex(item => { var _a; return ((_a = item.id) !== null && _a !== void 0 ? _a : '') === dismissedId; }); if (dismissedIndex === -1) { return; } let nextItemIndex = dismissedIndex + 1; if (nextItemIndex >= items.length) { nextItemIndex = dismissedIndex - 1; } // If there's no next item, focus the first instance of the h1 element if (nextItemIndex < 0 || nextItemIndex >= items.length) { const h1Element = document.querySelector('h1'); h1Element === null || h1Element === void 0 ? void 0 : h1Element.focus(); return; } const nextItemId = (_a = items[nextItemIndex].id) !== null && _a !== void 0 ? _a : nextItemIndex; // Try to focus on the next item, but with a small delay to ensure the DOM is updated // This is especially important for collapsible flashbars where the next item might become visible const attemptFocus = () => { const nextFlashElement = flashRefsCurrent[nextItemId]; if (!nextFlashElement) { // If the next flash element is not available, it might be because the flashbar is collapsed // In that case, try to focus on the notification bar button or the main element const notificationBarButton = refCurrent === null || refCurrent === void 0 ? void 0 : refCurrent.querySelector(`.${styles.button}`); if (notificationBarButton) { notificationBarButton.focus(); return; } const h1Element = document.querySelector('h1'); h1Element === null || h1Element === void 0 ? void 0 : h1Element.focus(); return; } focusFlashFocusableArea(nextFlashElement); }; setTimeout(attemptFocus, 0); }; // Common logic for collapsible and non-collapsible Flashbar export function useFlashbar({ items, onItemsAdded, onItemsChanged, onItemsRemoved, __internalRootRef, ...restProps }) { const allItemsHaveId = useMemo(() => items.every(item => 'id' in item), [items]); const baseProps = getBaseProps(restProps); const ref = useRef(null); const flashRefs = useRef({}); const mergedRef = useMergeRefs(ref, __internalRootRef); const isReducedMotion = useReducedMotion(ref); const isVisualRefresh = useVisualRefresh(); const [previousItems, setPreviousItems] = useState(items); const [nextFocusId, setNextFocusId] = useState(null); if (isDevelopment) { if (items === null || items === void 0 ? void 0 : items.some(item => item.ariaRole === 'alert' && !item.id)) { warnOnce('Flashbar', `You provided \`ariaRole="alert"\` for a flashbar item without providing an \`id\`. Focus will not be moved to the newly added flash message.`); } } // Track new or removed item IDs in state to only trigger focus changes for newly added items. // https://reactjs.org/docs/hooks-faq.html#how-do-i-implement-getderivedstatefromprops if (items) { const newItems = items.filter(({ id }) => id && !previousItems.some(item => item.id === id)); const removedItems = previousItems.filter(({ id }) => id && !items.some(item => item.id === id)); if (newItems.length > 0 || removedItems.length > 0) { setPreviousItems(items); onItemsAdded === null || onItemsAdded === void 0 ? void 0 : onItemsAdded(newItems); onItemsRemoved === null || onItemsRemoved === void 0 ? void 0 : onItemsRemoved(removedItems); onItemsChanged === null || onItemsChanged === void 0 ? void 0 : onItemsChanged({ allItemsHaveId, isReducedMotion }); const newFocusItems = newItems.filter(({ ariaRole }) => ariaRole === 'alert'); if (newFocusItems.length > 0) { setNextFocusId(newFocusItems[0].id); } } } const debouncedFocus = useDebounceCallback(focusFlashById, FOCUS_DEBOUNCE_DELAY); useEffect(() => { if (nextFocusId) { debouncedFocus(ref.current, nextFocusId); } }, [debouncedFocus, nextFocusId, ref]); const handleFlashDismissed = (dismissedId, persistenceConfig) => { handleFlashDismissedInternal(dismissedId, items, ref.current, flashRefs.current); if (persistenceConfig === null || persistenceConfig === void 0 ? void 0 : persistenceConfig.uniqueKey) { persistFlashbarDismiss(persistenceConfig); } }; return { allItemsHaveId, baseProps, isReducedMotion, isVisualRefresh, mergedRef, ref, flashRefs, handleFlashDismissed, }; } // Hook for managing flashbar items visibility with persistence export function useFlashbarVisibility(items) { const [checkedPersistenceKeys, setCheckedPersistenceKeys] = useState(() => new Set()); const [persistentItemsVisibility, setPersistentItemsVisibility] = useState(() => new Map()); const visibleItems = items.filter(item => { var _a; return !((_a = item.persistenceConfig) === null || _a === void 0 ? void 0 : _a.uniqueKey) || persistentItemsVisibility.get(item.persistenceConfig.uniqueKey) === true; }); useEffect(() => { const newPersistentItems = items.filter(item => { var _a; return ((_a = item.persistenceConfig) === null || _a === void 0 ? void 0 : _a.uniqueKey) && !checkedPersistenceKeys.has(item.persistenceConfig.uniqueKey); }); if (newPersistentItems.length === 0) { return; } let isMounted = true; const checkNewPersistentItems = async () => { try { const results = await Promise.all(newPersistentItems.map(async (item) => { try { const isDismissed = await retrieveFlashbarDismiss(item.persistenceConfig); return { key: item.persistenceConfig.uniqueKey, visible: !isDismissed, }; } catch { return { key: item.persistenceConfig.uniqueKey, visible: true, }; } })); if (!isMounted) { return; } setPersistentItemsVisibility(prev => { const updated = new Map(prev); results.forEach(({ key, visible }) => updated.set(key, visible)); return updated; }); setCheckedPersistenceKeys(prev => { const updated = new Set(prev); results.forEach(({ key }) => updated.add(key)); return updated; }); } catch { if (!isMounted) { return; } // Fallback if Promise.all itself fails, set all newPersistentItems to visible setPersistentItemsVisibility(prev => { const updated = new Map(prev); newPersistentItems.forEach(item => updated.set(item.persistenceConfig.uniqueKey, true)); return updated; }); } }; checkNewPersistentItems(); return () => { isMounted = false; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [items]); return visibleItems; } //# sourceMappingURL=common.js.map