UNPKG

@fluentui/react-northstar

Version:
426 lines (424 loc) 16.9 kB
import _invoke from "lodash/invoke"; import _debounce from "lodash/debounce"; import * as customPropTypes from '@fluentui/react-proptypes'; import { carouselBehavior } from '@fluentui/accessibility'; import * as React from 'react'; import * as PropTypes from 'prop-types'; import cx from 'classnames'; import { Ref } from '@fluentui/react-component-ref'; import { Animation } from '../Animation/Animation'; import { createShorthandFactory, commonPropTypes, childrenExist, isFromKeyboard as isEventFromKeyboard, createShorthand } from '../../utils'; import { CarouselItem } from './CarouselItem'; import { Text } from '../Text/Text'; import { CarouselNavigation } from './CarouselNavigation'; import { CarouselNavigationItem } from './CarouselNavigationItem'; import { CarouselPaddle } from './CarouselPaddle'; import { getElementType, useAccessibility, useStyles, useFluentContext, useTelemetry, useUnhandledProps, useStateManager, mergeVariablesOverrides } from '@fluentui/react-bindings'; import { createCarouselManager } from '@fluentui/state'; import { CarouselPaddlesContainer } from './CarouselPaddlesContainer'; import { getAnimationName } from './utils'; export var carouselClassName = 'ui-carousel'; export var carouselSlotClassNames = { itemsContainer: carouselClassName + "__itemscontainer", paddleNext: carouselClassName + "__paddlenext", paddlePrevious: carouselClassName + "__paddleprevious", pagination: carouselClassName + "__pagination", navigation: carouselClassName + "__navigation" }; function useDirection(activeIndex, circular, itemsLength) { var prevActiveIndex = React.useRef(activeIndex); React.useEffect(function () { prevActiveIndex.current = activeIndex; }, [activeIndex]); var direction = React.useMemo(function () { if (circular) { if (activeIndex === 0 && prevActiveIndex.current === itemsLength - 1) { return 'start'; } if (activeIndex === itemsLength - 1 && prevActiveIndex.current === 0) { return 'end'; } } if (activeIndex > prevActiveIndex.current) { return 'start'; } if (activeIndex < prevActiveIndex.current) { return 'end'; } return undefined; }, [activeIndex, circular, itemsLength]); return direction; } /** * A Carousel displays data organised as a gallery. * * @accessibility * Implements [ARIA Carousel](https://www.w3.org/WAI/tutorials/carousels/structure/) design pattern. * @accessibilityIssues * [VoiceOver doens't narrate label referenced by aria-labelledby attribute, when role is "tabpanel"](https://bugs.chromium.org/p/chromium/issues/detail?id=1040924) */ export var Carousel = /*#__PURE__*/function () { var Carousel = /*#__PURE__*/React.forwardRef(function (props, ref) { var context = useFluentContext(); var _useTelemetry = useTelemetry(Carousel.displayName, context.telemetry), setStart = _useTelemetry.setStart, setEnd = _useTelemetry.setEnd; setStart(); var accessibility = props.accessibility, items = props.items, circular = props.circular, getItemPositionText = props.getItemPositionText, paddlePrevious = props.paddlePrevious, paddleNext = props.paddleNext, navigation = props.navigation, thumbnails = props.thumbnails, children = props.children, ariaRoleDescription = props['aria-roledescription'], ariaLabel = props['aria-label'], className = props.className, design = props.design, styles = props.styles, variables = props.variables, disableClickableNav = props.disableClickableNav, animationEnterFromPrev = props.animationEnterFromPrev, animationEnterFromNext = props.animationEnterFromNext, animationExitToPrev = props.animationExitToPrev, animationExitToNext = props.animationExitToNext; var ElementType = getElementType(props); var _useStateManager = useStateManager(createCarouselManager, { mapPropsToInitialState: function mapPropsToInitialState() { return { activeIndex: props.defaultActiveIndex }; }, mapPropsToState: function mapPropsToState() { return { activeIndex: props.activeIndex }; } }), state = _useStateManager.state, actions = _useStateManager.actions; var ariaLiveOn = state.ariaLiveOn, shouldFocusContainer = state.shouldFocusContainer, isFromKeyboard = state.isFromKeyboard, activeIndex = state.activeIndex; var dir = useDirection(activeIndex, circular, items == null ? void 0 : items.length); var itemRefs = React.useMemo(function () { return Array.from({ length: items == null ? void 0 : items.length }, function () { return /*#__PURE__*/React.createRef(); }); }, // As we are using "panels.length" it's fine to have dependency on them // eslint-disable-next-line react-hooks/exhaustive-deps [items == null ? void 0 : items.length]); var nextPaddleHidden = items !== undefined && !circular && activeIndex === items.length - 1; var previousPaddleHidden = items !== undefined && !circular && activeIndex === 0; var unhandledProps = useUnhandledProps(Carousel.handledProps, props); var getA11yProps = useAccessibility(accessibility, { debugName: Carousel.displayName, actionHandlers: { showNextSlideByKeyboardNavigation: function showNextSlideByKeyboardNavigation(e) { e.preventDefault(); showNextSlide(e, true); }, showPreviousSlideByKeyboardNavigation: function showPreviousSlideByKeyboardNavigation(e) { e.preventDefault(); showPreviousSlide(e, true); }, showNextSlideByPaddlePress: function showNextSlideByPaddlePress(e) { e.preventDefault(); showNextSlide(e, false); handleNextPaddleFocus(); }, showPreviousSlideByPaddlePress: function showPreviousSlideByPaddlePress(e) { e.preventDefault(); showPreviousSlide(e, false); handlePreviousPaddleFocus(); } }, mapPropsToBehavior: function mapPropsToBehavior() { return { paddlePreviousHidden: previousPaddleHidden, paddleNextHidden: nextPaddleHidden, navigation: navigation, ariaLiveOn: ariaLiveOn, 'aria-roledescription': ariaRoleDescription, 'aria-label': ariaLabel }; } }); var _useStyles = useStyles(Carousel.displayName, { className: carouselClassName, mapPropsToStyles: function mapPropsToStyles() { return { shouldFocusContainer: shouldFocusContainer, isFromKeyboard: isFromKeyboard }; }, mapPropsToInlineStyles: function mapPropsToInlineStyles() { return { className: className, design: design, styles: styles, variables: variables }; }, rtl: context.rtl }), classes = _useStyles.classes; var paddleNextRef = React.useRef(); var paddlePreviousRef = React.useRef(); var focusItemAtIndex = React.useMemo(function () { return _debounce(function (index) { var _itemRefs$index$curre; (_itemRefs$index$curre = itemRefs[index].current) == null ? void 0 : _itemRefs$index$curre.focus(); }, 400); }, [itemRefs]); React.useEffect(function () { return function () { focusItemAtIndex.cancel(); }; }, [focusItemAtIndex, items]); var setActiveIndex = function setActiveIndex(e, index, focusItem) { var lastItemIndex = items.length - 1; var nextActiveIndex = index; if (index < 0) { if (!circular) { return; } nextActiveIndex = lastItemIndex; } if (index > lastItemIndex) { if (!circular) { return; } nextActiveIndex = 0; } actions.setIndexes(nextActiveIndex); _invoke(props, 'onActiveIndexChange', e, Object.assign({}, props, { activeIndex: index })); if (focusItem) { focusItemAtIndex(nextActiveIndex); } }; var overrideItemProps = function overrideItemProps(predefinedProps) { return { onFocus: function onFocus(e, itemProps) { actions.setShouldFocusContainer(e.currentTarget === e.target); actions.setIsFromKeyboard(isEventFromKeyboard()); _invoke(predefinedProps, 'onFocus', e, itemProps); }, onBlur: function onBlur(e, itemProps) { actions.setShouldFocusContainer(e.currentTarget.contains(e.relatedTarget)); actions.setIsFromKeyboard(false); _invoke(predefinedProps, 'onBlur', e, itemProps); } }; }; var renderContent = function renderContent() { return /*#__PURE__*/React.createElement("div", getA11yProps('itemsContainerWrapper', { className: classes.itemsContainerWrapper }), /*#__PURE__*/React.createElement("div", getA11yProps('itemsContainer', { className: cx(carouselSlotClassNames.itemsContainer, classes.itemsContainer) }), items && items.map(function (item, index) { var itemRef = itemRefs[index]; var active = activeIndex === index; var animationName = getAnimationName({ active: active, dir: dir, animationEnterFromPrev: animationEnterFromPrev, animationEnterFromNext: animationEnterFromNext, animationExitToPrev: animationExitToPrev, animationExitToNext: animationExitToNext }); return /*#__PURE__*/React.createElement(Animation, { visible: active, key: item['key'] || index, mountOnEnter: true, unmountOnExit: true, name: animationName }, /*#__PURE__*/React.createElement(Ref, { innerRef: itemRef }, CarouselItem.create(item, { defaultProps: function defaultProps() { return Object.assign({ active: active, navigation: !!navigation }, getItemPositionText && { itemPositionText: getItemPositionText(index, items.length) }); }, overrideProps: overrideItemProps }))); }))); }; var handleNextPaddleFocus = function handleNextPaddleFocus() { // if 'next' paddle will disappear, will focus 'previous' one. if (!navigation && activeIndex >= props.items.length - 2 && !circular) { paddlePreviousRef.current.focus(); } }; var handlePreviousPaddleFocus = function handlePreviousPaddleFocus() { // if 'previous' paddle will disappear, will focus 'next' one. if (!navigation && activeIndex <= 1 && !circular) { paddleNextRef.current.focus(); } }; var showPreviousSlide = function showPreviousSlide(e, focusItem) { setActiveIndex(e, +activeIndex - 1, focusItem); }; var showNextSlide = function showNextSlide(e, focusItem) { setActiveIndex(e, +activeIndex + 1, focusItem); }; var handlePaddleOverrides = function handlePaddleOverrides(predefinedProps, paddleName) { return { variables: mergeVariablesOverrides(variables, predefinedProps.variables), onClick: function onClick(e, paddleProps) { if (disableClickableNav) return; _invoke(predefinedProps, 'onClick', e, paddleProps); if (paddleName === 'paddleNext') { showNextSlide(e, false); handleNextPaddleFocus(); } else if (paddleName === 'paddlePrevious') { showPreviousSlide(e, false); handlePreviousPaddleFocus(); } }, onBlur: function onBlur(e, paddleProps) { if (e.relatedTarget !== paddleNextRef.current) { actions.setAriaLiveOn(false); } }, onFocus: function onFocus(e, paddleProps) { _invoke(predefinedProps, 'onFocus', e, paddleProps); actions.setAriaLiveOn(true); } }; }; var paddles = /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Ref, { innerRef: paddlePreviousRef }, CarouselPaddle.create(paddlePrevious, { defaultProps: function defaultProps() { return getA11yProps('paddlePrevious', { className: carouselSlotClassNames.paddlePrevious, previous: true, hidden: previousPaddleHidden, disableClickableNav: disableClickableNav }); }, overrideProps: function overrideProps(predefinedProps) { return handlePaddleOverrides(predefinedProps, 'paddlePrevious'); } })), /*#__PURE__*/React.createElement(Ref, { innerRef: paddleNextRef }, CarouselPaddle.create(paddleNext, { defaultProps: function defaultProps() { return getA11yProps('paddleNext', { className: carouselSlotClassNames.paddleNext, next: true, hidden: nextPaddleHidden, disableClickableNav: disableClickableNav }); }, overrideProps: function overrideProps(predefinedProps) { return handlePaddleOverrides(predefinedProps, 'paddleNext'); } }))); var renderPaddles = function renderPaddles() { return createShorthand(CarouselPaddlesContainer, {}, { overrideProps: function overrideProps() { return { children: paddles }; } }); }; var renderNavigation = function renderNavigation() { if (!items || !items.length) { return null; } return navigation ? CarouselNavigation.create(navigation, { defaultProps: function defaultProps() { return { className: carouselSlotClassNames.navigation, iconOnly: true, activeIndex: activeIndex, thumbnails: thumbnails, disableClickableNav: disableClickableNav }; }, overrideProps: function overrideProps(predefinedProps) { return { onItemClick: function onItemClick(e, itemProps) { if (disableClickableNav) return; var index = itemProps.index; setActiveIndex(e, index, true); _invoke(predefinedProps, 'onClick', e, itemProps); } }; } }) : getItemPositionText ? /*#__PURE__*/React.createElement(Text, { "aria-hidden": "true", align: "center", as: "div", className: carouselSlotClassNames.pagination, content: getItemPositionText(+activeIndex, items.length) }) : null; }; var element = /*#__PURE__*/React.createElement(ElementType, getA11yProps('root', Object.assign({ className: classes.root, ref: ref }, unhandledProps)), childrenExist(children) ? children : /*#__PURE__*/React.createElement(React.Fragment, null, renderPaddles(), renderContent(), renderNavigation())); setEnd(); return element; }); Carousel.displayName = 'Carousel'; Carousel.propTypes = Object.assign({}, commonPropTypes.createCommon({ content: false }), { activeIndex: PropTypes.number, 'aria-roledescription': PropTypes.string, 'aria-label': PropTypes.string, circular: PropTypes.bool, defaultActiveIndex: PropTypes.number, getItemPositionText: PropTypes.func, items: customPropTypes.collectionShorthand, navigation: PropTypes.oneOfType([customPropTypes.collectionShorthand, customPropTypes.itemShorthand]), navigationPosition: PropTypes.oneOf(['below', 'above', 'start', 'end']), onActiveIndexChange: PropTypes.func, paddleNext: customPropTypes.itemShorthand, paddlesPosition: PropTypes.oneOf(['inside', 'outside', 'inline']), paddlePrevious: customPropTypes.itemShorthand, thumbnails: PropTypes.bool, disableClickableNav: PropTypes.bool, animationEnterFromPrev: PropTypes.string, animationEnterFromNext: PropTypes.string, animationExitToPrev: PropTypes.string, animationExitToNext: PropTypes.string }); Carousel.defaultProps = { accessibility: carouselBehavior, paddlePrevious: {}, paddleNext: {}, animationEnterFromPrev: 'carousel-slide-to-previous-enter', animationEnterFromNext: 'carousel-slide-to-next-enter', animationExitToPrev: '', animationExitToNext: '' }; Carousel.Item = CarouselItem; Carousel.Navigation = CarouselNavigation; Carousel.NavigationItem = CarouselNavigationItem; Carousel.Paddle = CarouselPaddle; Carousel.PaddlesContainer = CarouselPaddlesContainer; Carousel.handledProps = Object.keys(Carousel.propTypes); Carousel.create = createShorthandFactory({ Component: Carousel, mappedArrayProp: 'items' }); return Carousel; }(); //# sourceMappingURL=Carousel.js.map