@fluentui/react-northstar
Version:
A themable React component library.
426 lines (424 loc) • 16.9 kB
JavaScript
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