Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
e4d4c17
S2 SelectBox initial implementation
Jul 14, 2025
a431ce0
Added tests and fixed linting
DPandyan Jul 14, 2025
7c6a91b
updated tests to remove styles
DPandyan Jul 14, 2025
b0a9b78
selectbox refactor and tests refactor
DPandyan Jul 15, 2025
151e4f6
stories changes and various edits
DPandyan Jul 15, 2025
d64c17b
replaced aria components with a gridlist
DPandyan Jul 16, 2025
23590e2
fixed borders and redid stories/tests
DPandyan Jul 17, 2025
1a7e962
removed extraneous overrides and isEmphasized prop
DPandyan Jul 17, 2025
29734bb
lint and removed XS
DPandyan Jul 18, 2025
cab2c79
test-utils-internal swap
DPandyan Jul 21, 2025
2f1c99d
test-utils-internal swap
DPandyan Jul 21, 2025
695706b
Merge branch 'main' of github.com:DPandyan/react-spectrum
DPandyan Jul 21, 2025
afa410e
Merge branch 'main' into main
DPandyan Jul 21, 2025
ea0f8d5
swapped useId library
DPandyan Jul 22, 2025
19a536c
swapped to listbox
DPandyan Jul 26, 2025
a779329
added additional props and removed s2 checkbox
DPandyan Jul 28, 2025
eb96cba
revised tests, added additional props, swapped to UI checkmark to avo…
DPandyan Jul 28, 2025
7ca7820
adjusted illustration size
DPandyan Jul 30, 2025
60d2d3b
removed the sizing and fixed various alignment issues. replaced check…
DPandyan Aug 1, 2025
cd3299f
Merge branch 'main' into main
DPandyan Aug 1, 2025
bd3d6b7
fixed group disabled checkbox issue
DPandyan Aug 1, 2025
85f7e21
pruned styles
DPandyan Aug 1, 2025
bdee23a
fixed label overflow, description still remaining
DPandyan Aug 4, 2025
9ad81e6
moved selectbox into same file, reworked grid, addressed various gh c…
DPandyan Aug 7, 2025
48c7e56
Merge branch 'main' into main
DPandyan Aug 7, 2025
267cf7b
s2(SelectBoxGroup): center label in horizontal layout when only label…
DPandyan Aug 12, 2025
4a6f681
s2(SelectBoxGroup): support [DEFAULT_SLOT] as label styles for text-o…
DPandyan Aug 12, 2025
c542032
removed extra story and count check
DPandyan Aug 12, 2025
13e72b1
Merge branch 'main' into main
DPandyan Aug 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
379 changes: 379 additions & 0 deletions packages/@react-spectrum/s2/src/SelectBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,379 @@
/*
* Copyright 2025 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 CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

import {box, iconStyles} from './Checkbox';
import Checkmark from '../ui-icons/Checkmark';
import {ContextValue} from 'react-aria-components';
import {FocusableRef, FocusableRefValue} from '@react-types/shared';
import {getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'};
import {IllustrationContext} from '../src/Icon';
import React, {createContext, forwardRef, ReactNode, useContext, useRef} from 'react';
import {SelectBoxContext} from './SelectBoxGroup';
import {style} from '../style' with {type: 'macro'};
import {useFocusableRef} from '@react-spectrum/utils';
import {useSpectrumContextProps} from './useSpectrumContextProps';

export interface SelectBoxProps extends StyleProps {
/**
* The value of the SelectBox.
*/
value: string,
/**
* The label for the element.
*/
children?: ReactNode,
/**
* Whether the SelectBox is disabled.
*/
isDisabled?: boolean
}

export const SelectBoxSpectrumContext = createContext<ContextValue<Partial<SelectBoxProps>, FocusableRefValue<HTMLDivElement>>>(null);

const selectBoxStyles = style({
display: 'flex',
flexDirection: {
default: 'column',
orientation: {
horizontal: 'row'
}
},
justifyContent: {
default: 'center',
orientation: {
horizontal: 'start'
}
},
alignItems: 'center',
font: 'ui',
flexShrink: 0,
boxSizing: 'border-box',
overflow: 'hidden',
position: 'relative',
// Sizing
width: {
default: 170,
orientation: {
horizontal: 368
}
},
height: {
default: 170,
orientation: {
horizontal: '100%'
}
},
minWidth: {
default: 144,
orientation: {
horizontal: 188
}
},
maxWidth: {
default: 170,
orientation: {
horizontal: 480
}
},
minHeight: {
default: 144,
orientation: {
horizontal: 80
}
},
maxHeight: {
default: 170,
orientation: {
horizontal: 240
}
},
// Spacing
padding: {
default: 24,
orientation: {
horizontal: 16
}
},
paddingStart: {
orientation: {
horizontal: 24
}
},
paddingEnd: {
orientation: {
horizontal: 32
}
},
gap: {
default: 8,
orientation: {
horizontal: 8
}
},
// Visual styling
borderRadius: 'lg',
backgroundColor: {
default: 'layer-2',
isDisabled: 'layer-1'
},
color: {
isDisabled: 'disabled'
},
boxShadow: {
default: 'emphasized',
isHovered: 'elevated',
isSelected: 'elevated',
forcedColors: 'none',
isDisabled: 'emphasized'
},
borderWidth: 2,
borderStyle: 'solid',
borderColor: {
default: 'transparent',
isSelected: 'gray-900',
isFocusVisible: 'blue-900',
isDisabled: 'transparent'
},
transition: 'default',
cursor: {
default: 'pointer',
isDisabled: 'default'
},
outlineStyle: 'none'
}, getAllowedOverrides());

const contentContainer = style({
display: 'flex',
flexDirection: {
default: 'column',
orientation: {
horizontal: 'row'
}
},
justifyContent: 'center',
alignItems: 'center',
textAlign: {
default: 'center',
orientation: {
horizontal: 'start'
}
},
gap: {
default: 8,
orientation: {
horizontal: 12
}
},
flex: {
orientation: {
horizontal: '1 0 0'
}
},
width: '100%',
overflow: 'hidden'
}, getAllowedOverrides());

const illustrationContainer = style({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minSize: 48,
flexShrink: 0,
color: {
isDisabled: 'disabled'
},
opacity: {
isDisabled: 0.4
}
});

const textContainer = style({
display: 'flex',
flexDirection: {
orientation: {
horizontal: 'column'
}
},
justifyContent: 'center',
alignItems: {
default: 'center',
orientation: {
horizontal: 'start'
}
},
gap: {
default: 12,
orientation: {
horizontal: 2
}
},
flex: {
orientation: {
horizontal: '1 0 0'
}
},
width: '100%',
minWidth: 0,
color: {
default: 'neutral',
isDisabled: 'disabled'
},
}, getAllowedOverrides());

const descriptionText = style({
alignSelf: 'stretch',
width: '100%',
minWidth: 0,
overflow: 'hidden',
maxHeight: {
orientation: {
horizontal: 120,
}
},
color: {
default: 'neutral',
isDisabled: 'disabled'
}
});

const labelText = style({
display: 'block',
width: '100%',
overflow: 'hidden',
minWidth: 0,
textAlign: {
default: 'center',
orientation: {
horizontal: 'start'
}
},
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
alignSelf: {
orientation: {
horizontal: 'stretch'
}
},
fontWeight: {
orientation: {
horizontal: 'bold'
}
},
color: {
default: 'neutral',
isDisabled: 'disabled'
}
});


const SelectBoxRenderPropsContext = createContext<{
isHovered?: boolean,
isFocusVisible?: boolean,
isPressed?: boolean
}>({});

/**
* SelectBox components allow users to select options from a list.
* Works as content within a ListBoxItem.
*/
export const SelectBox = /*#__PURE__*/ forwardRef(function SelectBox(props: SelectBoxProps, ref: FocusableRef<HTMLDivElement>) {
[props, ref] = useSpectrumContextProps(props, ref, SelectBoxSpectrumContext);
let {
children,
value,
isDisabled: individualDisabled = false,
UNSAFE_style
} = props;
let divRef = useRef<HTMLDivElement | null>(null);
let domRef = useFocusableRef(ref, divRef);

let contextValue = useContext(SelectBoxContext);
let {
orientation = 'vertical',
selectedKeys,
isDisabled: groupDisabled = false,
isCheckboxSelection = false
} = contextValue;

let renderProps = useContext(SelectBoxRenderPropsContext);

const size = 'M';
const isDisabled = individualDisabled || groupDisabled;
const isSelected = selectedKeys === 'all' || (selectedKeys && selectedKeys.has(value));

const childrenArray = React.Children.toArray(children);
const illustrationSlot = childrenArray.find((child: any) => child?.props?.slot === 'illustration');
const textSlot = childrenArray.find((child: any) => child?.props?.slot === 'text');
const descriptionSlot = childrenArray.find((child: any) => child?.props?.slot === 'description');
const otherChildren = childrenArray.filter((child: any) =>
!['illustration', 'text', 'description'].includes(child?.props?.slot)
);

return (
<div
ref={domRef}
className={selectBoxStyles({
size,
orientation,
isDisabled,
isSelected,
isHovered: renderProps.isHovered || false,
isFocusVisible: renderProps.isFocusVisible || false
}, props.styles)}
style={UNSAFE_style}>
{isCheckboxSelection && (isSelected || (!isDisabled && renderProps.isHovered)) && (
<div
className={style({
position: 'absolute',
top: 16,
left: 16
})}
aria-hidden="true">
<div
className={box({
isSelected,
isDisabled,
size: 'M'
} as any)}>
{isSelected && (
<Checkmark
size="S"
className={iconStyles} />
)}
</div>
</div>
)}
{!!illustrationSlot && (
<div className={illustrationContainer({size: 'S', orientation, isDisabled})}>
<IllustrationContext.Provider value={{size: 'S'}}>
{illustrationSlot}
</IllustrationContext.Provider>
</div>
)}
<div className={contentContainer({size, orientation})}>
<div className={textContainer({size, orientation, isDisabled})}>
<div className={labelText({orientation, isDisabled})}>
{textSlot}
</div>
{!!descriptionSlot && orientation === 'horizontal' && (
<div
className={descriptionText({size, orientation, isDisabled})}>
{descriptionSlot}
</div>
)}
</div>
</div>
{otherChildren}
</div>
);
});

export {SelectBoxRenderPropsContext};
Loading