Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding collapsible Tabs #1145

Merged
merged 45 commits into from Nov 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
a43777b
initial progress on tabs collapsing behavior
LFDanLu Oct 3, 2020
7b53e75
fixing up Tab story so can add tabs via buttons
LFDanLu Oct 5, 2020
9e5e166
moving tabs icon prop to title and updating aria label
LFDanLu Oct 5, 2020
6c6a183
Pulling v2 css to have hidden tablist overlap picker
LFDanLu Oct 5, 2020
f258982
Exporting Item from Tabs package and fixing some aria stuff
LFDanLu Oct 5, 2020
f50a629
Merge branch 'main' of https://github.com/adobe/react-spectrum into t…
LFDanLu Oct 6, 2020
2978f82
fixing tabs collapse in rtl
LFDanLu Oct 6, 2020
8600724
fixing more RTL things in Tabs
LFDanLu Oct 6, 2020
7824f16
actually getting tab collapse to work
LFDanLu Oct 7, 2020
f80b6f7
fixing lint
LFDanLu Oct 7, 2020
2573fef
moving local css to spectrum css temp
LFDanLu Oct 7, 2020
427bc9a
adding tests
LFDanLu Oct 7, 2020
f8acf57
Merge branch 'main' into tabs_collapse
LFDanLu Oct 7, 2020
ca49758
removing persistent tablist in favor of resizing stategy like breadcr…
LFDanLu Oct 8, 2020
fc02b87
fixing lint
LFDanLu Oct 8, 2020
3610774
Merge branch 'tabs_collapse' of https://github.com/adobe/react-spectr…
LFDanLu Oct 8, 2020
cd1c806
making the dropdown tab css classname a bit better
LFDanLu Oct 8, 2020
75395ce
adding more tabs to the other stories for easier ocollapse testing
LFDanLu Oct 8, 2020
80977cd
addressing some review comments
LFDanLu Oct 9, 2020
30cfdba
Passing tabs specific fieldbutton styles via sllots
LFDanLu Oct 12, 2020
b9578ae
making selected tab update to next/previous key if selected tab is de…
LFDanLu Oct 12, 2020
1eb399c
Adding more tabs tests
LFDanLu Oct 12, 2020
b8e7d36
fix lint
LFDanLu Oct 12, 2020
6cba460
only do initial tabline positioning transform for horizontal tabs
LFDanLu Oct 12, 2020
b708909
adding collapsibleTabs component for clarity
LFDanLu Oct 13, 2020
2950593
moving collapse check back to Tabs to fix TabLine positioning updates
LFDanLu Oct 13, 2020
1cf80c4
removing orientation from CollapsibleTabList
LFDanLu Oct 13, 2020
58ec201
adding ref checks in useResizeObserver and adding orientation flip st…
LFDanLu Oct 13, 2020
3d8f7ce
Partial update for review comments
LFDanLu Oct 13, 2020
43767cc
updating collapsible tablist spectrum classes
LFDanLu Oct 14, 2020
8e92911
selecting first tab if selected tab is deleted
LFDanLu Oct 14, 2020
ef50b99
updating focus ring for collapsed tab picker
LFDanLu Oct 14, 2020
613f666
Merge branch 'main' into tabs_collapse
LFDanLu Oct 14, 2020
7c8d0cb
addressing code review
LFDanLu Oct 21, 2020
8195b11
fixing css so compact tab picker and compact tabs are vertically posi…
LFDanLu Oct 23, 2020
558a968
removing excess space
LFDanLu Oct 23, 2020
96645c4
moving tabs collapsed styles into horizontal tab scope
LFDanLu Oct 23, 2020
a47a95e
Merge branch 'main' into tabs_collapse
LFDanLu Oct 23, 2020
55c6764
Merge branch 'main' of https://github.com/adobe/react-spectrum into t…
LFDanLu Oct 28, 2020
62215a7
fixing merge
LFDanLu Oct 28, 2020
eac285a
addressing review comments
LFDanLu Nov 5, 2020
7278408
addressing review comments
LFDanLu Nov 6, 2020
40c87bb
fixing tabline position when tablist expands
LFDanLu Nov 6, 2020
21be5ae
Merge branch 'main' into tabs_collapse
devongovett Nov 13, 2020
67110ee
Merge branch 'main' into tabs_collapse
dannify Nov 13, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
75 changes: 71 additions & 4 deletions packages/@adobe/spectrum-css-temp/components/tabs/index.css
Expand Up @@ -17,6 +17,21 @@ governing permissions and limitations under the License.
--spectrum-tabs-compact-item-height: calc(var(--spectrum-tabs-quiet-compact-height) - var(--spectrum-tabs-rule-height));
}

.spectrum-TabsPanel {
display: flex;
/* This is so TabsPanel can collapse with wrapping block/flex containers out of the box. If end user wants to place element next to tabpanel,
they must include flex: 1 1 auto and min-width: 0 on the Tabs component. */
width: 100%;
snowystinger marked this conversation as resolved.
Show resolved Hide resolved
}

.spectrum-TabsPanel--horizontal {
flex-direction: column;
}

.spectrum-TabsPanel--vertical {
flex-direction: row;
}

.spectrum-Tabs {
display: flex;

Expand Down Expand Up @@ -65,9 +80,7 @@ governing permissions and limitations under the License.
block-size: var(--spectrum-tabs-item-height);

& + .spectrum-Tabs-itemLabel {
/* icons are inexplicably offset by 3px. Subtract this value from the icon-gap
to correct the gap in CSS */
margin-inline-start: calc(var(--spectrum-tabs-icon-gap) - var(--spectrum-global-dimension-size-40));
margin-inline-start: var(--spectrum-tabs-icon-gap);
}
}

Expand Down Expand Up @@ -105,14 +118,23 @@ governing permissions and limitations under the License.

.spectrum-Tabs-selectionIndicator {
position: absolute;
left: 0;
inset-inline-start: 0;

/* Be below the tab */
z-index: 0;

transition: transform var(--spectrum-tabs-selection-indicator-animation-duration) ease-in-out;
transform-origin: top left;


[dir='ltr'] .spectrum-Tabs--horizontal & {
transform: translateX(var(--spectrum-tabs-focus-ring-padding-x));
}

[dir='rtl'] .spectrum-Tabs--horizontal & {
transform: translateX(calc(var(--spectrum-tabs-focus-ring-padding-x) * -1));
}

border-radius: var(--spectrum-tabs-rule-border-radius);
}

Expand Down Expand Up @@ -149,6 +171,36 @@ governing permissions and limitations under the License.
inset-block-end: calc(var(--spectrum-tabs-rule-height) * -1);
}

&.spectrum-Tabs--isCollapsed {
inset-inline-start: 0;
block-size: var(--spectrum-tabs-item-height);

/* FieldButton Picker focus ring override */
& button {
&::before {
content: '';
position: absolute;
top: 50%;

box-sizing: border-box;

block-size: var(--spectrum-tabs-focus-ring-height);
margin-block-start: calc(calc(var(--spectrum-tabs-focus-ring-height) / -2) + calc(var(--spectrum-tabs-rule-height) / 2));
inset-inline-start: calc(var(--spectrum-tabs-focus-ring-padding-x) * -1);
inset-inline-end: calc(var(--spectrum-tabs-focus-ring-padding-x) * -1);
border: var(--spectrum-tabs-focus-ring-size) solid transparent;
border-radius: var(--spectrum-tabs-focus-ring-border-radius);

pointer-events: none;
}
}

&.spectrum-Tabs--compact {
& button {
height: calc(var(--spectrum-tabs-compact-item-height));
}
}
}

&.spectrum-Tabs--compact {
/* The ActionButton is taller than the tabs, so don't push tabs around */
Expand Down Expand Up @@ -204,3 +256,18 @@ governing permissions and limitations under the License.
inset-inline-start: calc(var(--spectrum-tabs-vertical-rule-width) * -1);
}
}

.spectrum-TabsPanel-collapseWrapper {
display: flex;
flex-grow: 1;
flex-shrink: 0;
flex-basis: 0%;
overflow: hidden;
position: relative;
}

.spectrum-TabsPanel-tabs {
flex-grow: 1;
flex-shrink: 0;
flex-basis: 0%;
}
11 changes: 11 additions & 0 deletions packages/@adobe/spectrum-css-temp/components/tabs/skin.css
Expand Up @@ -12,6 +12,17 @@ governing permissions and limitations under the License.

.spectrum-Tabs {
border-block-end-color: var(--spectrum-tabs-rule-color);

&.spectrum-Tabs--isCollapsed {
:focus-ring {
/* FieldButton Picker focus ring override */
box-shadow: none;

&::before {
border-color: var(--spectrum-tabs-focus-ring-color);
}
}
}
}

.spectrum-Tabs--vertical {
Expand Down
12 changes: 3 additions & 9 deletions packages/@react-aria/tabs/src/useTabs.ts
Expand Up @@ -10,7 +10,7 @@
* governing permissions and limitations under the License.
*/
import {HTMLAttributes, Key, RefObject, useMemo, useState} from 'react';
import {mergeProps, useId} from '@react-aria/utils';
import {mergeProps, useId, useLabels} from '@react-aria/utils';
import {SingleSelectListState} from '@react-stately/list';
import {TabAriaProps, TabsAriaProps} from '@react-types/tabs';
import {TabsKeyboardDelegate} from './TabsKeyboardDelegate';
Expand All @@ -30,7 +30,6 @@ const tabsIds = new WeakMap<SingleSelectListState<unknown>, string>();
export function useTabs<T>(props: TabsAriaProps<T>, state: SingleSelectListState<T>, ref): TabsAria {
let {
isDisabled,
'aria-label': ariaLabel,
orientation = 'horizontal',
keyboardActivation = 'automatic'
} = props;
Expand All @@ -55,11 +54,6 @@ export function useTabs<T>(props: TabsAriaProps<T>, state: SingleSelectListState
disallowEmptySelection: true
});

// Ensure a tab is always selected
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it looks like this was moved to the react-spectrum package, why was that? just curiosity

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figured it would be nice to allow users to customize their own tab fallback strategy when a tab isn't selected or the selected tab is deleted from the child list so I pulled it out to the spectrum package

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I think we need to at least have a default. I think it's weird that you'd need to implement this yourself when it will be most common for the first tab to be selected on mount. If you wish to control it some other way, you can use the controlled props. IMO this logic should be in stately.

if (manager.isEmpty) {
manager.replaceSelection(delegate.getFirstKey());
}

// Compute base id for all tabs
let tabsId = useId();
tabsIds.set(state, tabsId);
Expand All @@ -69,13 +63,13 @@ export function useTabs<T>(props: TabsAriaProps<T>, state: SingleSelectListState
onFocusWithinChange: setFocusWithin
});
let tabIndex = isFocusWithin ? -1 : 0;
let tabListLabelProps = useLabels({...props, id: tabsId});

return {
tabListProps: {
...mergeProps(focusWithinProps, collectionProps),
...mergeProps(focusWithinProps, collectionProps, tabListLabelProps),
role: 'tablist',
'aria-disabled': isDisabled,
'aria-label': ariaLabel,
tabIndex: isDisabled ? null : tabIndex
},
tabPanelProps: {
Expand Down
9 changes: 6 additions & 3 deletions packages/@react-aria/utils/src/useResizeObserver.ts
Expand Up @@ -13,7 +13,8 @@ export function useResizeObserver<T extends HTMLElement>(options: useResizeObser
const {ref, onResize} = options;

useEffect(() => {
if (!ref) {
let element = ref?.current;
if (!element) {
return;
}

Expand All @@ -31,10 +32,12 @@ export function useResizeObserver<T extends HTMLElement>(options: useResizeObser

onResize();
});
resizeObserverInstance.observe(ref.current);
resizeObserverInstance.observe(element);

return () => {
resizeObserverInstance.unobserve(ref.current);
if (element) {
resizeObserverInstance.unobserve(element);
}
};
}

Expand Down
50 changes: 2 additions & 48 deletions packages/@react-spectrum/breadcrumbs/src/Breadcrumbs.tsx
Expand Up @@ -11,11 +11,11 @@
*/
import {ActionButton} from '@react-spectrum/button';
import {BreadcrumbItem} from './BreadcrumbItem';
import {classNames, useDOMRef, useStyleProps} from '@react-spectrum/utils';
import {classNames, useDOMRef, useStyleProps, useValueEffect} from '@react-spectrum/utils';
import {DOMRef} from '@react-types/shared';
import FolderBreadcrumb from '@spectrum-icons/ui/FolderBreadcrumb';
import {Menu, MenuTrigger} from '@react-spectrum/menu';
import React, {Key, ReactElement, useCallback, useRef, useState} from 'react';
import React, {Key, ReactElement, useCallback, useRef} from 'react';
import {SpectrumBreadcrumbsProps} from '@react-types/breadcrumbs';
import styles from '@adobe/spectrum-css-temp/components/breadcrumb/vars.css';
import {useBreadcrumbs} from '@react-aria/breadcrumbs';
Expand Down Expand Up @@ -217,52 +217,6 @@ function Breadcrumbs<T>(props: SpectrumBreadcrumbsProps<T>, ref: DOMRef) {
);
}

// This hook works like `useState`, but when setting the value, you pass a generator function
// that can yield multiple values. Each yielded value updates the state and waits for the next
// layout effect, then continues the generator. This allows sequential updates to state to be
// written linearly.
function useValueEffect(defaultValue) {
let [value, setValue] = useState(defaultValue);
let effect = useRef(null);

// Store the function in a ref so we can always access the current version
// which has the proper `value` in scope.
let nextRef = useRef(null);
nextRef.current = () => {
// Run the generator to the next yield.
let newValue = effect.current.next();

// If the generator is done, reset the effect.
if (newValue.done) {
effect.current = null;
return;
}

// If the value is the same as the current value,
// then continue to the next yield. Otherwise,
// set the value in state and wait for the next layout effect.
if (value === newValue.value) {
nextRef.current();
} else {
setValue(newValue.value);
}
};

useLayoutEffect(() => {
// If there is an effect currently running, continue to the next yield.
if (effect.current) {
nextRef.current();
}
});

let queue = useCallback(fn => {
effect.current = fn();
nextRef.current();
}, [effect, nextRef]);

return [value, queue];
}

/**
* Breadcrumbs show hierarchy and navigational context for a user’s location within an application.
*/
Expand Down
1 change: 1 addition & 0 deletions packages/@react-spectrum/button/package.json
Expand Up @@ -40,6 +40,7 @@
"@react-spectrum/utils": "^3.3.0",
"@react-stately/toggle": "^3.2.1",
"@react-types/button": "^3.2.1",
"@react-types/provider": "^3.1.1",
"@react-types/shared": "^3.2.1",
"@spectrum-icons/ui": "^3.2.0"
},
Expand Down
12 changes: 9 additions & 3 deletions packages/@react-spectrum/button/src/FieldButton.tsx
Expand Up @@ -11,7 +11,8 @@
*/

import {ButtonProps} from '@react-types/button';
import {classNames, SlotProvider, useFocusableRef, useStyleProps} from '@react-spectrum/utils';
import {classNames, SlotProvider, useFocusableRef, useSlotProps, useStyleProps} from '@react-spectrum/utils';
import {CSSModule} from '@react-types/provider';
import {DOMProps, FocusableRef, StyleProps} from '@react-types/shared';
import {FocusRing} from '@react-aria/focus';
import {mergeProps} from '@react-aria/utils';
Expand All @@ -23,27 +24,32 @@ import {useHover} from '@react-aria/interactions';
interface FieldButtonProps extends ButtonProps, DOMProps, StyleProps {
isQuiet?: boolean,
isActive?: boolean,
validationState?: 'valid' | 'invalid'
validationState?: 'valid' | 'invalid',
focusRingStyles?: CSSModule
}

// @private
function FieldButton(props: FieldButtonProps, ref: FocusableRef) {
props = useSlotProps(props, 'button');
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts on this approach for propagating focus ring overrides?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm wondering if button should be the default slot name....
is it ok to conflict? https://github.com/adobe/react-spectrum/blob/main/packages/%40react-spectrum/button/src/Button.tsx#L32
fortunately they can be overridden, but i wonder if it's counterintuitive or not

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The extent of my logic was that they should share the same slot name since they are both buttons and may end up being used in similar slot positions lol. Happy to change it to fieldbutton or something

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, i had that thought as well, i haven't settled on a strong opinion yet, lets see if @devongovett has a strong opinion

let {
isQuiet,
isDisabled,
validationState,
children,
autoFocus,
isActive,
focusRingStyles,
...otherProps
} = props;
let domRef = useFocusableRef(ref) as RefObject<HTMLButtonElement>;
let {buttonProps, isPressed} = useButton(props, domRef);
let {hoverProps, isHovered} = useHover({isDisabled});
let {styleProps} = useStyleProps(otherProps);

let focusRingClass = focusRingStyles ? classNames(focusRingStyles, 'focus-ring') : null;

return (
<FocusRing focusRingClass={classNames(styles, 'focus-ring')} autoFocus={autoFocus}>
<FocusRing focusRingClass={classNames(styles, 'focus-ring', focusRingClass)} autoFocus={autoFocus}>
<button
{...mergeProps(buttonProps, hoverProps)}
ref={domRef}
Expand Down
3 changes: 3 additions & 0 deletions packages/@react-spectrum/tabs/package.json
Expand Up @@ -41,8 +41,11 @@
"@react-spectrum/button": "^3.2.1",
"@react-spectrum/menu": "^3.2.1",
"@react-spectrum/picker": "^3.2.1",
"@react-spectrum/text": "^3.1.0",
"@react-spectrum/utils": "^3.3.0",
"@react-stately/collections": "^3.2.1",
"@react-stately/list": "^3.2.1",
"@react-types/select": "^3.1.1",
"@react-types/shared": "^3.2.1",
"@react-types/tabs": "3.0.0-alpha.1",
"@spectrum-icons/workflow": "^3.2.0"
Expand Down