📄 useLandmark.js
/home/palash/git/site/node_modules/react-aria/dist/private/landmark/useLandmark.js
Language: js • Lines: 404
import {getEventTarget as $d8ac7ed472840322$export$e58f029f0fbfdb29, nodeContains as $d8ac7ed472840322$export$4282f70798064fe0} from "../utils/shadowdom/DOMFunctions.js";
import {useLayoutEffect as $53fed047b798be36$export$e5c5a5f917a5871c} from "../utils/useLayoutEffect.js";
import {useState as $cSnI5$useState, useCallback as $cSnI5$useCallback, useEffect as $cSnI5$useEffect} from "react";
import {useSyncExternalStore as $cSnI5$useSyncExternalStore} from "use-sync-external-store/shim/index.js";

/*
 * Copyright 2022 Adobe. All rights reserved.
 * This file is licensed to you under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License. You may obtain a copy
 * of the License at http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under
 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
 * OF ANY KIND, either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */ 



// Increment this version number whenever the
// LandmarkManagerApi or Landmark interfaces change.
const $6ea972a5373aaa15$var$LANDMARK_API_VERSION = 1;
// Symbol under which the singleton landmark manager instance is attached to the document.
const $6ea972a5373aaa15$var$landmarkSymbol = Symbol.for('react-aria-landmark-manager');
function $6ea972a5373aaa15$var$subscribe(fn) {
    document.addEventListener('react-aria-landmark-manager-change', fn);
    return ()=>document.removeEventListener('react-aria-landmark-manager-change', fn);
}
function $6ea972a5373aaa15$var$getLandmarkManager() {
    if (typeof document === 'undefined') return null;
    // Reuse an existing instance if it has the same or greater version.
    let instance = document[$6ea972a5373aaa15$var$landmarkSymbol];
    if (instance && instance.version >= $6ea972a5373aaa15$var$LANDMARK_API_VERSION) return instance;
    // Otherwise, create a new instance and dispatch an event so anything using the existing
    // instance updates and re-registers their landmarks with the new one.
    document[$6ea972a5373aaa15$var$landmarkSymbol] = new $6ea972a5373aaa15$var$LandmarkManager();
    document.dispatchEvent(new CustomEvent('react-aria-landmark-manager-change'));
    return document[$6ea972a5373aaa15$var$landmarkSymbol];
}
// Subscribes a React component to the current landmark manager instance.
function $6ea972a5373aaa15$var$useLandmarkManager() {
    return (0, $cSnI5$useSyncExternalStore)($6ea972a5373aaa15$var$subscribe, $6ea972a5373aaa15$var$getLandmarkManager, $6ea972a5373aaa15$var$getLandmarkManager);
}
class $6ea972a5373aaa15$var$LandmarkManager {
    setupIfNeeded() {
        if (this.isListening) return;
        document.addEventListener('keydown', this.f6Handler, {
            capture: true
        });
        document.addEventListener('focusin', this.focusinHandler, {
            capture: true
        });
        document.addEventListener('focusout', this.focusoutHandler, {
            capture: true
        });
        this.isListening = true;
    }
    teardownIfNeeded() {
        if (!this.isListening || this.landmarks.length > 0 || this.refCount > 0) return;
        document.removeEventListener('keydown', this.f6Handler, {
            capture: true
        });
        document.removeEventListener('focusin', this.focusinHandler, {
            capture: true
        });
        document.removeEventListener('focusout', this.focusoutHandler, {
            capture: true
        });
        this.isListening = false;
    }
    focusLandmark(landmark, direction) {
        var _this_landmarks_find_focus, _this_landmarks_find;
        (_this_landmarks_find = this.landmarks.find((l)=>l.ref.current === landmark)) === null || _this_landmarks_find === void 0 ? void 0 : (_this_landmarks_find_focus = _this_landmarks_find.focus) === null || _this_landmarks_find_focus === void 0 ? void 0 : _this_landmarks_find_focus.call(_this_landmarks_find, direction);
    }
    /**
   * Return set of landmarks with a specific role.
   */ getLandmarksByRole(role) {
        return new Set(this.landmarks.filter((l)=>l.role === role));
    }
    /**
   * Return first landmark with a specific role.
   */ getLandmarkByRole(role) {
        return this.landmarks.find((l)=>l.role === role);
    }
    addLandmark(newLandmark) {
        this.setupIfNeeded();
        if (this.landmarks.find((landmark)=>landmark.ref === newLandmark.ref) || !newLandmark.ref.current) return;
        if (this.landmarks.filter((landmark)=>landmark.role === 'main').length > 1 && process.env.NODE_ENV !== 'production') console.error('Page can contain no more than one landmark with the role "main".');
        if (this.landmarks.length === 0) {
            this.landmarks = [
                newLandmark
            ];
            this.checkLabels(newLandmark.role);
            return;
        }
        // Binary search to insert new landmark based on position in document relative to existing landmarks.
        // https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition
        let start = 0;
        let end = this.landmarks.length - 1;
        while(start <= end){
            let mid = Math.floor((start + end) / 2);
            let comparedPosition = newLandmark.ref.current.compareDocumentPosition(this.landmarks[mid].ref.current);
            let isNewAfterExisting = Boolean(comparedPosition & Node.DOCUMENT_POSITION_PRECEDING || comparedPosition & Node.DOCUMENT_POSITION_CONTAINS);
            if (isNewAfterExisting) start = mid + 1;
            else end = mid - 1;
        }
        this.landmarks.splice(start, 0, newLandmark);
        this.checkLabels(newLandmark.role);
    }
    updateLandmark(landmark) {
        let index = this.landmarks.findIndex((l)=>l.ref === landmark.ref);
        if (index >= 0) {
            this.landmarks[index] = {
                ...this.landmarks[index],
                ...landmark
            };
            this.checkLabels(this.landmarks[index].role);
        }
    }
    removeLandmark(ref) {
        this.landmarks = this.landmarks.filter((landmark)=>landmark.ref !== ref);
        this.teardownIfNeeded();
    }
    /**
   * Warn if there are 2+ landmarks with the same role but no label.
   * Labels for landmarks with the same role must also be unique.
   *
   * See https://www.w3.org/WAI/ARIA/apg/practices/landmark-regions/.
   */ checkLabels(role) {
        let landmarksWithRole = this.getLandmarksByRole(role);
        if (landmarksWithRole.size > 1) {
            let duplicatesWithoutLabel = [
                ...landmarksWithRole
            ].filter((landmark)=>!landmark.label);
            if (duplicatesWithoutLabel.length > 0 && process.env.NODE_ENV !== 'production') console.warn(`Page contains more than one landmark with the '${role}' role. If two or more landmarks on a page share the same role, all must be labeled with an aria-label or aria-labelledby attribute: `, duplicatesWithoutLabel.map((landmark)=>landmark.ref.current));
            else if (process.env.NODE_ENV !== 'production') {
                let labels = [
                    ...landmarksWithRole
                ].map((landmark)=>landmark.label);
                let duplicateLabels = labels.filter((item, index)=>labels.indexOf(item) !== index);
                duplicateLabels.forEach((label)=>{
                    console.warn(`Page contains more than one landmark with the '${role}' role and '${label}' label. If two or more landmarks on a page share the same role, they must have unique labels: `, [
                        ...landmarksWithRole
                    ].filter((landmark)=>landmark.label === label).map((landmark)=>landmark.ref.current));
                });
            }
        }
    }
    /**
   * Get the landmark that is the closest parent in the DOM.
   * Returns undefined if no parent is a landmark.
   */ closestLandmark(element) {
        let landmarkMap = new Map(this.landmarks.map((l)=>[
                l.ref.current,
                l
            ]));
        let currentElement = element;
        while(currentElement && !landmarkMap.has(currentElement) && currentElement !== document.body && currentElement.parentElement)currentElement = currentElement.parentElement;
        return landmarkMap.get(currentElement);
    }
    /**
   * Gets the next landmark, in DOM focus order, or previous if backwards is specified.
   * If last landmark, next should be the first landmark.
   * If not inside a landmark, will return first landmark.
   * Returns undefined if there are no landmarks.
   */ getNextLandmark(element, { backward: backward }) {
        var _this_landmarks_nextLandmarkIndex_ref_current;
        let currentLandmark = this.closestLandmark(element);
        let nextLandmarkIndex = backward ? this.landmarks.length - 1 : 0;
        if (currentLandmark) nextLandmarkIndex = this.landmarks.indexOf(currentLandmark) + (backward ? -1 : 1);
        let wrapIfNeeded = ()=>{
            // When we reach the end of the landmark sequence, fire a custom event that can be listened for by applications.
            // If this event is canceled, we return immediately. This can be used to implement landmark navigation across iframes.
            if (nextLandmarkIndex < 0) {
                if (!element.dispatchEvent(new CustomEvent('react-aria-landmark-navigation', {
                    detail: {
                        direction: 'backward'
                    },
                    bubbles: true,
                    cancelable: true
                }))) return true;
                nextLandmarkIndex = this.landmarks.length - 1;
            } else if (nextLandmarkIndex >= this.landmarks.length) {
                if (!element.dispatchEvent(new CustomEvent('react-aria-landmark-navigation', {
                    detail: {
                        direction: 'forward'
                    },
                    bubbles: true,
                    cancelable: true
                }))) return true;
                nextLandmarkIndex = 0;
            }
            if (nextLandmarkIndex < 0 || nextLandmarkIndex >= this.landmarks.length) return true;
            return false;
        };
        if (wrapIfNeeded()) return undefined;
        // Skip over hidden landmarks.
        let i = nextLandmarkIndex;
        while((_this_landmarks_nextLandmarkIndex_ref_current = this.landmarks[nextLandmarkIndex].ref.current) === null || _this_landmarks_nextLandmarkIndex_ref_current === void 0 ? void 0 : _this_landmarks_nextLandmarkIndex_ref_current.closest('[aria-hidden=true]')){
            nextLandmarkIndex += backward ? -1 : 1;
            if (wrapIfNeeded()) return undefined;
            if (nextLandmarkIndex === i) break;
        }
        return this.landmarks[nextLandmarkIndex];
    }
    /**
   * Look at next landmark. If an element was previously focused inside, restore focus there.
   * If not, focus the landmark itself.
   * If no landmarks at all, or none with focusable elements, don't move focus.
   */ f6Handler(e) {
        if (e.key === 'F6') {
            // If alt key pressed, focus main landmark, otherwise navigate forward or backward based on shift key.
            let handled = e.altKey ? this.focusMain() : this.navigate((0, $d8ac7ed472840322$export$e58f029f0fbfdb29)(e), e.shiftKey);
            if (handled) {
                e.preventDefault();
                e.stopPropagation();
            }
        }
    }
    focusMain() {
        let main = this.getLandmarkByRole('main');
        if (main && main.ref.current && main.ref.current.isConnected) {
            this.focusLandmark(main.ref.current, 'forward');
            return true;
        }
        return false;
    }
    navigate(from, backward) {
        let nextLandmark = this.getNextLandmark(from, {
            backward: backward
        });
        if (!nextLandmark) return false;
        // If something was previously focused in the next landmark, then return focus to it
        if (nextLandmark.lastFocused) {
            let lastFocused = nextLandmark.lastFocused;
            if ((0, $d8ac7ed472840322$export$4282f70798064fe0)(document.body, lastFocused)) {
                lastFocused.focus();
                return true;
            }
        }
        // Otherwise, focus the landmark itself
        if (nextLandmark.ref.current && nextLandmark.ref.current.isConnected) {
            this.focusLandmark(nextLandmark.ref.current, backward ? 'backward' : 'forward');
            return true;
        }
        return false;
    }
    /**
   * Sets lastFocused for a landmark, if focus is moved within that landmark.
   * Lets the last focused landmark know it was blurred if something else is focused.
   */ focusinHandler(e) {
        let currentLandmark = this.closestLandmark((0, $d8ac7ed472840322$export$e58f029f0fbfdb29)(e));
        if (currentLandmark && currentLandmark.ref.current !== (0, $d8ac7ed472840322$export$e58f029f0fbfdb29)(e)) this.updateLandmark({
            ref: currentLandmark.ref,
            lastFocused: (0, $d8ac7ed472840322$export$e58f029f0fbfdb29)(e)
        });
        let previousFocusedElement = e.relatedTarget;
        if (previousFocusedElement) {
            let closestPreviousLandmark = this.closestLandmark(previousFocusedElement);
            if (closestPreviousLandmark && closestPreviousLandmark.ref.current === previousFocusedElement) closestPreviousLandmark.blur();
        }
    }
    /**
   * Track if the focus is lost to the body. If it is, do cleanup on the landmark that last had
   * focus.
   */ focusoutHandler(e) {
        let previousFocusedElement = (0, $d8ac7ed472840322$export$e58f029f0fbfdb29)(e);
        let nextFocusedElement = e.relatedTarget;
        // the === document seems to be a jest thing for focus to go there on generic blur event such as landmark.blur();
        // browsers appear to send focus instead to document.body and the relatedTarget is null when that happens
        if (!nextFocusedElement || nextFocusedElement === document) {
            let closestPreviousLandmark = this.closestLandmark(previousFocusedElement);
            if (closestPreviousLandmark && closestPreviousLandmark.ref.current === previousFocusedElement) closestPreviousLandmark.blur();
        }
    }
    createLandmarkController() {
        let instance = this;
        instance.refCount++;
        instance.setupIfNeeded();
        return {
            navigate (direction, opts) {
                let element = (opts === null || opts === void 0 ? void 0 : opts.from) || document.activeElement;
                return instance.navigate(element, direction === 'backward');
            },
            focusNext (opts) {
                let element = (opts === null || opts === void 0 ? void 0 : opts.from) || document.activeElement;
                return instance.navigate(element, false);
            },
            focusPrevious (opts) {
                let element = (opts === null || opts === void 0 ? void 0 : opts.from) || document.activeElement;
                return instance.navigate(element, true);
            },
            focusMain () {
                return instance.focusMain();
            },
            dispose () {
                if (instance) {
                    instance.refCount--;
                    instance.teardownIfNeeded();
                    instance = null;
                }
            }
        };
    }
    registerLandmark(landmark) {
        if (this.landmarks.find((l)=>l.ref === landmark.ref)) this.updateLandmark(landmark);
        else this.addLandmark(landmark);
        return ()=>this.removeLandmark(landmark.ref);
    }
    constructor(){
        this.landmarks = [];
        this.isListening = false;
        this.refCount = 0;
        this.version = $6ea972a5373aaa15$var$LANDMARK_API_VERSION;
        this.f6Handler = this.f6Handler.bind(this);
        this.focusinHandler = this.focusinHandler.bind(this);
        this.focusoutHandler = this.focusoutHandler.bind(this);
    }
}
function $6ea972a5373aaa15$export$a8e2debc6521490c() {
    // Get the current landmark manager and create a controller using it.
    let instance = $6ea972a5373aaa15$var$getLandmarkManager();
    let controller = instance === null || instance === void 0 ? void 0 : instance.createLandmarkController();
    let unsubscribe = $6ea972a5373aaa15$var$subscribe(()=>{
        // If the landmark manager changes, dispose the old
        // controller and create a new one.
        controller === null || controller === void 0 ? void 0 : controller.dispose();
        instance = $6ea972a5373aaa15$var$getLandmarkManager();
        controller = instance === null || instance === void 0 ? void 0 : instance.createLandmarkController();
    });
    // Return a wrapper that proxies requests to the current controller instance.
    return {
        navigate (direction, opts) {
            return controller.navigate(direction, opts);
        },
        focusNext (opts) {
            return controller.focusNext(opts);
        },
        focusPrevious (opts) {
            return controller.focusPrevious(opts);
        },
        focusMain () {
            return controller.focusMain();
        },
        dispose () {
            controller === null || controller === void 0 ? void 0 : controller.dispose();
            unsubscribe();
            controller = undefined;
            instance = null;
        }
    };
}
function $6ea972a5373aaa15$export$4cc632584fd87fae(props, ref) {
    const { role: role, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, focus: focus } = props;
    let manager = $6ea972a5373aaa15$var$useLandmarkManager();
    let label = ariaLabel || ariaLabelledby;
    let [isLandmarkFocused, setIsLandmarkFocused] = (0, $cSnI5$useState)(false);
    let defaultFocus = (0, $cSnI5$useCallback)(()=>{
        setIsLandmarkFocused(true);
    }, [
        setIsLandmarkFocused
    ]);
    let blur = (0, $cSnI5$useCallback)(()=>{
        setIsLandmarkFocused(false);
    }, [
        setIsLandmarkFocused
    ]);
    (0, $53fed047b798be36$export$e5c5a5f917a5871c)(()=>{
        if (manager) return manager.registerLandmark({
            ref: ref,
            label: label,
            role: role,
            focus: focus || defaultFocus,
            blur: blur
        });
    }, [
        manager,
        label,
        ref,
        role,
        focus,
        defaultFocus,
        blur
    ]);
    (0, $cSnI5$useEffect)(()=>{
        var _ref_current;
        if (isLandmarkFocused) (_ref_current = ref.current) === null || _ref_current === void 0 ? void 0 : _ref_current.focus();
    }, [
        isLandmarkFocused,
        ref
    ]);
    return {
        landmarkProps: {
            role: role,
            tabIndex: isLandmarkFocused ? -1 : undefined,
            'aria-label': ariaLabel,
            'aria-labelledby': ariaLabelledby
        }
    };
}


export {$6ea972a5373aaa15$export$a8e2debc6521490c as UNSTABLE_createLandmarkController, $6ea972a5373aaa15$export$4cc632584fd87fae as useLandmark};
//# sourceMappingURL=useLandmark.js.map