diff --git a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap
index f41cc69220fe..848f7d0018f5 100644
--- a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap
+++ b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap
@@ -8767,9 +8767,18 @@ Map {
],
"type": "oneOf",
},
+ "leftOverflowButtonProps": Object {
+ "type": "object",
+ },
"light": Object {
"type": "bool",
},
+ "rightOverflowButtonProps": Object {
+ "type": "object",
+ },
+ "scrollDebounceWait": Object {
+ "type": "number",
+ },
"scrollIntoView": Object {
"type": "bool",
},
diff --git a/packages/react/src/components/Tabs/Tabs.js b/packages/react/src/components/Tabs/Tabs.js
index 6c6137f0c7f4..0ee9570acb4c 100644
--- a/packages/react/src/components/Tabs/Tabs.js
+++ b/packages/react/src/components/Tabs/Tabs.js
@@ -390,7 +390,7 @@ export default class Tabs extends React.Component {
};
handleOverflowNavMouseDown = (event, { direction }) => {
- // disregard mouse buttons aside from LMB
+ // disregard mouse buttons aside from left mouse button
if (event.buttons !== 1) {
return;
}
diff --git a/packages/react/src/components/Tabs/next/Tabs-test.js b/packages/react/src/components/Tabs/next/Tabs-test.js
index 83566e243f22..2ffaf140517a 100644
--- a/packages/react/src/components/Tabs/next/Tabs-test.js
+++ b/packages/react/src/components/Tabs/next/Tabs-test.js
@@ -27,9 +27,12 @@ describe('Tabs', () => {
});
it('should set a className from props on outermost element in TabList', () => {
- render(
+ const { container } = render(
-
+
Tab Label 1
Tab Label 2
Tab Label 3
@@ -42,7 +45,7 @@ describe('Tabs', () => {
);
- expect(screen.getByRole('tablist')).toHaveClass('custom-class');
+ expect(container.firstChild).toHaveClass('custom-class');
});
});
diff --git a/packages/react/src/components/Tabs/next/Tabs.js b/packages/react/src/components/Tabs/next/Tabs.js
index 11175ce1eac6..752286d2faff 100644
--- a/packages/react/src/components/Tabs/next/Tabs.js
+++ b/packages/react/src/components/Tabs/next/Tabs.js
@@ -5,16 +5,21 @@
* LICENSE file in the root directory of this source tree.
*/
-import PropTypes from 'prop-types';
-import React, { useState, useRef, useEffect } from 'react';
+import { ChevronLeft16, ChevronRight16 } from '@carbon/icons-react';
import cx from 'classnames';
+import debounce from 'lodash.debounce';
+import PropTypes from 'prop-types';
+import React, { useCallback, useState, useRef, useEffect } from 'react';
import { Tooltip } from '../../Tooltip/next';
-import { keys, match, matches } from '../../../internal/keyboard';
-import { usePrefix } from '../../../internal/usePrefix';
-import { useId } from '../../../internal/useId';
-import { getInteractiveContent } from '../../../internal/useNoInteractiveChildren';
import { useControllableState } from '../../../internal/useControllableState';
+import { useEffectOnce } from '../../../internal/useEffectOnce';
+import { useId } from '../../../internal/useId';
+import useIsomorphicEffect from '../../../internal/useIsomorphicEffect';
import { useMergedRefs } from '../../../internal/useMergedRefs';
+import { getInteractiveContent } from '../../../internal/useNoInteractiveChildren';
+import { usePrefix } from '../../../internal/usePrefix';
+import { keys, match, matches } from '../../../internal/keyboard';
+import { usePressable } from './usePressable';
// Used to manage the overall state of the Tabs
const TabsContext = React.createContext();
@@ -82,22 +87,6 @@ Tabs.propTypes = {
selectedIndex: PropTypes.number,
};
-function useEffectOnce(callback) {
- const savedCallback = useRef(callback);
- const effectGuard = useRef(false);
-
- useEffect(() => {
- savedCallback.current = callback;
- });
-
- useEffect(() => {
- if (effectGuard.current !== true) {
- effectGuard.current = true;
- savedCallback.current();
- }
- }, []);
-}
-
/**
* Get the next index for a given keyboard event given a count of the total
* items and the current index
@@ -123,10 +112,13 @@ function TabList({
'aria-label': label,
children,
className: customClassName,
- light,
- scrollIntoView,
contained = false,
iconSize,
+ leftOverflowButtonProps,
+ light,
+ rightOverflowButtonProps,
+ scrollDebounceWait = 200,
+ scrollIntoView,
...rest
}) {
const {
@@ -137,6 +129,10 @@ function TabList({
} = React.useContext(TabsContext);
const prefix = usePrefix();
const ref = useRef(null);
+ const previousButton = useRef(null);
+ const nextButton = useRef(null);
+ const [isScrollable, setIsScrollable] = useState(false);
+ const [scrollLeft, setScrollLeft] = useState(false);
const className = cx(`${prefix}--tabs`, customClassName, {
[`${prefix}--tabs--contained`]: contained,
[`${prefix}--tabs--light`]: light,
@@ -144,12 +140,50 @@ function TabList({
[`${prefix}--tabs__icon--lg`]: iconSize === 'lg',
});
+ // Previous Button
+ // VISIBLE IF:
+ // SCROLLABLE
+ // AND SCROLL_LEFT > 0
+ const buttonWidth = 44;
+ const isPreviousButtonVisible = ref.current
+ ? isScrollable && scrollLeft > 0
+ : false;
+ // Next Button
+ // VISIBLE IF:
+ // SCROLLABLE
+ // AND SCROLL_LEFT + CLIENT_WIDTH < SCROLL_WIDTH
+ const isNextButtonVisible = ref.current
+ ? scrollLeft + buttonWidth + ref.current.clientWidth <
+ ref.current.scrollWidth
+ : false;
+ const previousButtonClasses = cx(
+ `${prefix}--tab--overflow-nav-button`,
+ `${prefix}--tab--overflow-nav-button--previous`,
+ {
+ [`${prefix}--tab--overflow-nav-button--hidden`]: !isPreviousButtonVisible,
+ }
+ );
+ const nextButtonClasses = cx(
+ `${prefix}--tab--overflow-nav-button`,
+ `${prefix}--tab--overflow-nav-button--next`,
+ {
+ [`${prefix}--tab--overflow-nav-button--hidden`]: !isNextButtonVisible,
+ }
+ );
+
const tabs = [];
+ const debouncedOnScroll = useCallback(() => {
+ return debounce((event) => {
+ setScrollLeft(event.target.scrollLeft);
+ }, scrollDebounceWait);
+ }, [scrollDebounceWait]);
function onKeyDown(event) {
if (
matches(event, [keys.ArrowRight, keys.ArrowLeft, keys.Home, keys.End])
) {
+ event.preventDefault();
+
const activeTabs = tabs.filter((tab) => {
return !tab.current.disabled;
});
@@ -194,26 +228,131 @@ function TabList({
}
});
- return (
- // eslint-disable-next-line jsx-a11y/interactive-supports-focus
-
- {React.Children.map(children, (child, index) => {
- const ref = React.createRef();
- tabs.push(ref);
- return (
-
- {React.cloneElement(child, {
- ref,
- })}
-
+ useIsomorphicEffect(() => {
+ if (ref.current) {
+ setIsScrollable(ref.current.scrollWidth > ref.current.clientWidth);
+ }
+
+ function handler() {
+ if (ref.current) {
+ setIsScrollable(ref.current.scrollWidth > ref.current.clientWidth);
+ }
+ }
+
+ const debouncedHandler = debounce(handler, 200);
+ window.addEventListener('resize', debouncedHandler);
+ return () => {
+ debouncedHandler.cancel();
+ window.removeEventListener('resize', debouncedHandler);
+ };
+ }, []);
+
+ // updates scroll location for all scroll behavior.
+ useIsomorphicEffect(() => {
+ ref.current.scrollLeft = scrollLeft;
+ }, [scrollLeft]);
+
+ useIsomorphicEffect(() => {
+ const tab =
+ activation === 'manual' ? tabs[activeIndex] : tabs[selectedIndex];
+ if (tab) {
+ // The width of the "scroll buttons"
+
+ // The start and end position of the selected tab
+ const { width: tabWidth } = tab.current.getBoundingClientRect();
+ const start = tab.current.offsetLeft;
+ const end = tab.current.offsetLeft + tabWidth;
+
+ // The start and end of the visible area for the tabs
+ const visibleStart = ref.current.scrollLeft + buttonWidth;
+ const visibleEnd =
+ ref.current.scrollLeft + ref.current.clientWidth - buttonWidth;
+
+ // The beginning of the tab is clipped and not visible
+ if (start < visibleStart) {
+ setScrollLeft(start - buttonWidth);
+ }
+
+ // The end of teh tab is clipped and not visible
+ if (end > visibleEnd) {
+ setScrollLeft(end + buttonWidth - ref.current.clientWidth);
+ }
+ }
+ }, [activation, activeIndex, selectedIndex]);
+
+ usePressable(previousButton, {
+ onPress({ longPress }) {
+ if (!longPress) {
+ setScrollLeft(
+ Math.max(
+ scrollLeft - (ref.current.scrollWidth / tabs.length) * 1.5,
+ 0
+ )
);
- })}
+ }
+ },
+ onLongPress() {
+ return createLongPressBehavior(ref, 'backward', setScrollLeft);
+ },
+ });
+
+ usePressable(nextButton, {
+ onPress({ longPress }) {
+ if (!longPress) {
+ setScrollLeft(
+ Math.min(
+ scrollLeft + (ref.current.scrollWidth / tabs.length) * 1.5,
+ ref.current.scrollWidth - ref.current.clientWidth
+ )
+ );
+ }
+ },
+ onLongPress() {
+ return createLongPressBehavior(ref, 'forward', setScrollLeft);
+ },
+ });
+
+ return (
+
+
+
+
+ {/* eslint-disable-next-line jsx-a11y/interactive-supports-focus */}
+
+ {React.Children.map(children, (child, index) => {
+ const ref = React.createRef();
+ tabs.push(ref);
+ return (
+
+ {React.cloneElement(child, {
+ ref,
+ })}
+
+ );
+ })}
+
+
+
+
);
}
@@ -241,19 +380,39 @@ TabList.propTypes = {
* Specify an optional className to be added to the container node
*/
className: PropTypes.string,
+
/**
* Specify whether component is contained type
*/
-
contained: PropTypes.bool,
+
/**
* If using `IconTab`, specify the size of the icon being used.
*/
iconSize: PropTypes.oneOf(['default', 'lg']),
+
+ /**
+ * Provide the props that describe the left overflow button
+ */
+ leftOverflowButtonProps: PropTypes.object,
+
/**
* Specify whether or not to use the light component variant
*/
light: PropTypes.bool,
+
+ /**
+ * Provide the props that describe the right overflow button
+ */
+ rightOverflowButtonProps: PropTypes.object,
+
+ /**
+ * Optionally provide a delay (in milliseconds) passed to the lodash
+ * debounce of the onScroll handler. This will impact the responsiveness
+ * of scroll arrow buttons rendering when scrolling to the first or last tab.
+ */
+ scrollDebounceWait: PropTypes.number,
+
/**
* Choose whether or not to automatically scroll to newly selected tabs
* on component rerender
@@ -261,6 +420,50 @@ TabList.propTypes = {
scrollIntoView: PropTypes.bool,
};
+/**
+ * Helper function to setup the behavior when a button is "long pressed". This
+ * function will take a ref to the tablist, a direction, and a setter for
+ * scrollLeft and will update the scroll position within a
+ * requestAnimationFrame.
+ *
+ * It returns a cleanup function to be run when the long press is
+ * deactivated
+ *
+ * @param {RefObject} ref
+ * @param {'forward' | 'backward'} direction
+ * @param {Function} setScrollLeft
+ * @returns {Function}
+ */
+function createLongPressBehavior(ref, direction, setScrollLeft) {
+ // We manually override the scroll behavior to be "auto". If it is set as
+ // smooth, this animation does not update correctly
+ let defaultScrollBehavior = ref.current.style['scroll-behavior'];
+ ref.current.style['scroll-behavior'] = 'auto';
+
+ const scrollDelta = direction === 'forward' ? 5 : -5;
+ let frameId = null;
+
+ function tick() {
+ ref.current.scrollLeft = ref.current.scrollLeft + scrollDelta;
+ frameId = requestAnimationFrame(tick);
+ }
+
+ frameId = requestAnimationFrame(tick);
+
+ return () => {
+ // Restore the previous scroll behavior
+ ref.current.style['scroll-behavior'] = defaultScrollBehavior;
+
+ // Make sure that our `scrollLeft` value is in sync with the existing
+ // `ref` after our requestAnimationFrame loop above
+ setScrollLeft(ref.current.scrollLeft);
+
+ if (frameId) {
+ cancelAnimationFrame(frameId);
+ }
+ };
+}
+
const Tab = React.forwardRef(function Tab(
{
as: BaseComponent = 'button',
@@ -323,26 +526,32 @@ Tab.propTypes = {
* Provide a custom element to render instead of the default button
*/
as: PropTypes.oneOfType([PropTypes.string, PropTypes.elementType]),
+
/**
* Provide child elements to be rendered inside of `Tab`.
*/
children: PropTypes.node,
+
/**
* Specify an optional className to be added to your Tab
*/
className: PropTypes.string,
+
/**
* Whether your Tab is disabled.
*/
disabled: PropTypes.bool,
+
/**
* Provide a handler that is invoked when a user clicks on the control
*/
onClick: PropTypes.func,
+
/**
* Provide a handler that is invoked on the key down event for the control
*/
onKeyDown: PropTypes.func,
+
/*
* An optional parameter to allow overriding the anchor rendering.
* Useful for using Tab along with react-router or other client
@@ -459,6 +668,7 @@ TabPanel.propTypes = {
* Provide child elements to be rendered inside of `TabPanel`.
*/
children: PropTypes.node,
+
/**
* Specify an optional className to be added to TabPanel.
*/
@@ -481,7 +691,3 @@ TabPanels.propTypes = {
};
export { Tabs, Tab, IconTab, TabPanel, TabPanels, TabList };
-
-// TO DO: implement horizontal scroll and the following props:
-// leftOverflowButtonProps
-// rightOverflowButtonProps
diff --git a/packages/react/src/components/Tabs/next/Tabs.stories.js b/packages/react/src/components/Tabs/next/Tabs.stories.js
index 77cc087d6929..b31ff3c472df 100644
--- a/packages/react/src/components/Tabs/next/Tabs.stories.js
+++ b/packages/react/src/components/Tabs/next/Tabs.stories.js
@@ -41,24 +41,61 @@ export default {
};
export const Default = () => (
-
-
- Tab Label 1
- Tab Label 2
- Tab Label 3
- Tab Label 4 with a very long long label
- Tab Label 5
-
-
-
- Tab Panel 1 Example button
-
- Tab Panel 2
- Tab Panel 3
- Tab Panel 4
- Tab Panel 5
-
-
+
+
+
+ Tab Label 1
+ Tab Label 2
+ Tab Label 3
+ Tab Label 4 with a very long long label
+ Tab Label 5
+ Tab Label 6
+ Tab Label 7
+ Tab Label 8
+
+
+
+ Tab Panel 1 Example button
+
+ Tab Panel 2
+ Tab Panel 3
+ Tab Panel 4
+ Tab Panel 5
+ Tab Panel 6
+ Tab Panel 7
+ Tab Panel 8
+
+
+
+);
+
+export const Manual = () => (
+
+
+
+ Tab Label 1
+ Tab Label 2
+ Tab Label 3
+ Tab Label 4 with a very long long label
+ Tab Label 5
+ Tab Label 6
+ Tab Label 7
+ Tab Label 8
+
+
+
+ Tab Panel 1 Example button
+
+ Tab Panel 2
+ Tab Panel 3
+ Tab Panel 4
+ Tab Panel 5
+ Tab Panel 6
+ Tab Panel 7
+ Tab Panel 8
+
+
+
);
export const Icon20Only = () => (
diff --git a/packages/react/src/components/Tabs/next/usePressable.js b/packages/react/src/components/Tabs/next/usePressable.js
new file mode 100644
index 000000000000..eb09269d911a
--- /dev/null
+++ b/packages/react/src/components/Tabs/next/usePressable.js
@@ -0,0 +1,126 @@
+/**
+ * Copyright IBM Corp. 2016, 2018
+ *
+ * This source code is licensed under the Apache-2.0 license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import { useEffect, useRef, useState } from 'react';
+
+export function usePressable(
+ ref,
+ { onPress, onPressIn, onPressOut, onLongPress, delayLongPressMs = 500 } = {}
+) {
+ const savedOnPress = useRef(onPress);
+ const savedOnPressIn = useRef(onPressIn);
+ const savedOnPressOut = useRef(onPressOut);
+ const savedOnLongPress = useRef(onLongPress);
+ const [pendingLongPress, setPendingLongPress] = useState(false);
+ const [longPress, setLongPress] = useState(false);
+ const state = useRef({
+ longPress: false,
+ });
+
+ useEffect(() => {
+ savedOnPress.current = onPress;
+ }, [onPress]);
+
+ useEffect(() => {
+ savedOnPressIn.current = onPressIn;
+ }, [onPressIn]);
+
+ useEffect(() => {
+ savedOnPressOut.current = onPressOut;
+ }, [onPressOut]);
+
+ useEffect(() => {
+ savedOnLongPress.current = onLongPress;
+ }, [onLongPress]);
+
+ useEffect(() => {
+ const { current: element } = ref;
+
+ // Fired when a pointer becomes active buttons state.
+ function onPointerDown() {
+ setPendingLongPress(true);
+ savedOnPressIn.current?.();
+ }
+
+ // Fired when a pointer is no longer active buttons state.
+ function onPointerUp() {
+ setPendingLongPress(false);
+ setLongPress(false);
+ savedOnPressOut.current?.(state.current);
+ }
+
+ // A browser fires this event if it concludes the pointer
+ // will no longer be able to generate events (for example
+ // the related device is deactivated).
+ function onPointerCancel() {
+ setPendingLongPress(false);
+ setLongPress(false);
+ savedOnPressOut.current?.();
+ state.current.longPress = false;
+ }
+
+ // Fired when a pointer is moved out of the hit test
+ // boundaries of an element. For pen devices, this event
+ // is fired when the stylus leaves the hover range
+ // detectable by the digitizer.
+ function onPointerLeave() {
+ setPendingLongPress(false);
+ setLongPress(false);
+ savedOnPressOut.current?.();
+ state.current.longPress = false;
+ }
+
+ function onClick() {
+ setLongPress(false);
+ setPendingLongPress(false);
+ savedOnPress.current?.(state.current);
+ state.current.longPress = false;
+ }
+
+ function onTouchStart(event) {
+ // We prevent the default event on touchstart so that text selection is
+ // disabled on iOS Safari when interacting with a "pressable" element
+ event.preventDefault();
+ }
+
+ element.addEventListener('touchstart', onTouchStart);
+ element.addEventListener('pointerdown', onPointerDown);
+ element.addEventListener('pointerup', onPointerUp);
+ element.addEventListener('pointercancel', onPointerCancel);
+ element.addEventListener('pointerleave', onPointerLeave);
+ element.addEventListener('click', onClick);
+
+ return () => {
+ element.removeEventListener('touchstart', onTouchStart);
+ element.removeEventListener('pointerdown', onPointerDown);
+ element.removeEventListener('pointerup', onPointerUp);
+ element.removeEventListener('pointercancel', onPointerCancel);
+ element.removeEventListener('pointerleave', onPointerLeave);
+ element.removeEventListener('click', onClick);
+ };
+ }, [ref]);
+
+ useEffect(() => {
+ if (pendingLongPress) {
+ const timeoutId = setTimeout(() => {
+ setPendingLongPress(false);
+ setLongPress(true);
+ }, delayLongPressMs);
+
+ return () => {
+ clearTimeout(timeoutId);
+ };
+ }
+ }, [pendingLongPress, delayLongPressMs]);
+
+ useEffect(() => {
+ if (longPress) {
+ state.current.longPress = true;
+ return savedOnLongPress.current?.();
+ }
+ }, [longPress]);
+}
diff --git a/packages/react/src/internal/useEffectOnce.js b/packages/react/src/internal/useEffectOnce.js
new file mode 100644
index 000000000000..bbf520e43b35
--- /dev/null
+++ b/packages/react/src/internal/useEffectOnce.js
@@ -0,0 +1,30 @@
+/**
+ * Copyright IBM Corp. 2016, 2018
+ *
+ * This source code is licensed under the Apache-2.0 license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import { useEffect, useRef } from 'react';
+
+/**
+ * A custom hook which will call the given callback exactly once when your
+ * component is initially rendered and effects are first called
+ *
+ * @param {Function} callback
+ */
+export function useEffectOnce(callback) {
+ const savedCallback = useRef(callback);
+ const effectGuard = useRef(false);
+
+ useEffect(() => {
+ savedCallback.current = callback;
+ });
+
+ useEffect(() => {
+ if (effectGuard.current !== true) {
+ effectGuard.current = true;
+ savedCallback.current();
+ }
+ }, []);
+}
diff --git a/packages/styles/scss/components/tabs/_tabs.scss b/packages/styles/scss/components/tabs/_tabs.scss
index dd61e3d1ed0f..032f36dce706 100644
--- a/packages/styles/scss/components/tabs/_tabs.scss
+++ b/packages/styles/scss/components/tabs/_tabs.scss
@@ -40,29 +40,25 @@ $icon-tab-size: custom-property.get-var('icon-tab-size', rem(40px));
@include reset;
@include type-style('body-compact-01');
+ position: relative;
display: flex;
width: 100%;
height: auto;
min-height: rem(40px);
+ max-height: 4rem;
color: $text-primary;
&.#{$prefix}--tabs--contained {
min-height: rem(48px);
}
- .#{$prefix}--tabs__nav {
+ .#{$prefix}--tab--list {
display: flex;
- overflow: auto hidden;
- width: auto;
- max-width: 100%;
- flex-direction: row;
- padding: 0;
- margin: 0;
- list-style: none;
- outline: 0;
- // hide scroll bars
+ width: 100%;
+ overflow-x: auto;
+ scroll-behavior: smooth;
scrollbar-width: none;
- transition: max-height $duration-fast-01 motion(standard, productive);
+ will-change: scroll-position;
&::-webkit-scrollbar {
display: none;
@@ -72,37 +68,121 @@ $icon-tab-size: custom-property.get-var('icon-tab-size', rem(40px));
//-----------------------------
// Overflow Nav Buttons
//-----------------------------
- .#{$prefix}--tabs__overflow-indicator--left,
- .#{$prefix}--tabs__overflow-indicator--right {
+ .#{$prefix}--tab--overflow-nav-button {
+ @include button-reset.reset;
+
+ display: flex;
+ width: $spacing-08;
+ flex-shrink: 0;
+ align-items: center;
+ justify-content: center;
+ background-color: $background;
+
+ &:focus {
+ @include focus-outline('outline');
+ }
+ }
+
+ .#{$prefix}--tab--overflow-nav-button--hidden {
+ display: none;
+ }
+
+ &.#{$prefix}--tabs--contained .#{$prefix}--tab--overflow-nav-button {
+ width: $spacing-09;
+ margin: 0;
+ background-color: $layer-accent;
+ }
+
+ .#{$prefix}--tab--overflow-nav-button svg {
+ fill: $icon-primary;
+ }
+
+ .#{$prefix}--tab--overflow-nav-button--next {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ }
+
+ .#{$prefix}--tab--overflow-nav-button--next::before {
+ position: absolute;
z-index: 1;
+ left: -$spacing-03;
width: $spacing-03;
- flex: 1 0 auto;
+ height: 100%;
+ background: linear-gradient(
+ to right,
+ rgba(255, 255, 255, 0),
+ $background
+ );
+ content: '';
}
- .#{$prefix}--tabs__overflow-indicator--left {
- margin-right: -$spacing-03;
- background-image: linear-gradient(to left, transparent, $background);
+ &.#{$prefix}--tabs--contained
+ .#{$prefix}--tab--overflow-nav-button--next::before {
+ background-image: linear-gradient(
+ to right,
+ rgba(255, 255, 255, 0),
+ $layer-accent
+ );
}
- .#{$prefix}--tabs__overflow-indicator--right {
- margin-left: -$spacing-03;
- background-image: linear-gradient(to right, transparent, $background);
+ .#{$prefix}--tab--overflow-nav-button--previous {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ }
+
+ .#{$prefix}--tab--overflow-nav-button--previous::before {
+ position: absolute;
+ z-index: 1;
+ right: -$spacing-03;
+ width: $spacing-03;
+ height: 100%;
+ background: linear-gradient(to left, rgba(255, 255, 255, 0), $background);
+ content: '';
+ }
+
+ &.#{$prefix}--tabs--contained
+ .#{$prefix}--tab--overflow-nav-button--previous::before {
+ background-image: linear-gradient(
+ to left,
+ rgba(255, 255, 255, 0),
+ $layer-accent
+ );
}
.#{$prefix}--tabs--light .#{$prefix}--tabs__overflow-indicator--left {
- background-image: linear-gradient(to left, transparent, $layer);
+ background-image: linear-gradient(
+ to left,
+ rgba(255, 255, 255, 0),
+ $layer
+ );
}
.#{$prefix}--tabs--light .#{$prefix}--tabs__overflow-indicator--right {
- background-image: linear-gradient(to right, transparent, $layer);
+ background-image: linear-gradient(
+ to right,
+ rgba(255, 255, 255, 0),
+ $layer
+ );
}
&.#{$prefix}--tabs--contained .#{$prefix}--tabs__overflow-indicator--left {
- background-image: linear-gradient(to left, transparent, $layer-accent);
+ background-image: linear-gradient(
+ to left,
+ rgba(255, 255, 255, 0),
+ $layer-accent
+ );
}
&.#{$prefix}--tabs--contained .#{$prefix}--tabs__overflow-indicator--right {
- background-image: linear-gradient(to right, transparent, $layer-accent);
+ background-image: linear-gradient(
+ to right,
+ rgba(255, 255, 255, 0),
+ $layer-accent
+ );
}
// Safari-only media query
@@ -146,34 +226,6 @@ $icon-tab-size: custom-property.get-var('icon-tab-size', rem(40px));
}
}
- .#{$prefix}--tab--overflow-nav-button {
- @include button-reset.reset;
-
- display: flex;
- width: $spacing-08;
- flex-shrink: 0;
- align-items: center;
- justify-content: center;
-
- &:focus {
- @include focus-outline('outline');
- }
- }
-
- .#{$prefix}--tab--overflow-nav-button--hidden {
- display: none;
- }
-
- &.#{$prefix}--tabs--contained .#{$prefix}--tab--overflow-nav-button {
- width: $spacing-09;
- margin: 0;
- background-color: $layer-accent;
- }
-
- .#{$prefix}--tab--overflow-nav-button svg {
- fill: $icon-primary;
- }
-
//-----------------------------
// Item
//-----------------------------
@@ -181,6 +233,7 @@ $icon-tab-size: custom-property.get-var('icon-tab-size', rem(40px));
@include reset;
display: flex;
+ flex: 1 0 auto;
padding: 0;
cursor: pointer;
transition: background-color $duration-fast-01