Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
defbd29
fix(search-dropdown): rename CSS file to resolve import error
jacksonkasi1 Mar 30, 2025
9c53a33
feat(search-dropdown): add SearchDropdown components to index export
jacksonkasi1 Mar 30, 2025
084a8ff
fix eslint
jacksonkasi1 Mar 30, 2025
cf8d9cf
fix(search-dropdown): update clear button positioning and enhance val…
jacksonkasi1 Mar 30, 2025
7dae847
fix(search-dropdown): update value change handling to accept null
jacksonkasi1 Mar 30, 2025
d5d4941
fix(search-dropdown): restore rest prop handling in input element
jacksonkasi1 Mar 30, 2025
3b23d40
refactor(search-dropdown): clean up CSS styles and improve layout
jacksonkasi1 Mar 30, 2025
e972995
refactor(search-dropdown): streamline input styles and enhance placeh…
jacksonkasi1 Mar 30, 2025
ca45f28
fix(search-dropdown): set width to 100% for improved layout
jacksonkasi1 Mar 30, 2025
26ebfd8
feat(search-dropdown): enhance search functionality and user experience
jacksonkasi1 Mar 30, 2025
d6edd65
feat(search-dropdown): implement clear functionality on Escape key press
jacksonkasi1 Mar 30, 2025
fda3b1c
feat(search-dropdown): add icon support to search dropdown
jacksonkasi1 Mar 30, 2025
73dd410
refactor(search-dropdown): improve option rendering and styling
jacksonkasi1 Mar 30, 2025
c7c3219
refactor(search-dropdown): remove fixed position style from story
jacksonkasi1 Mar 30, 2025
303ff05
feat(search-dropdown): add custom styles story for enhanced flexibility
jacksonkasi1 Mar 30, 2025
ca14750
refactor(search-dropdown): update styles for improved layout and cons…
jacksonkasi1 Mar 30, 2025
1ceaba1
feat(search-dropdown): enhance menu positioning logic for better visi…
jacksonkasi1 Mar 30, 2025
b024f42
feat(search-dropdown): improve menu visibility and positioning updates
jacksonkasi1 Mar 30, 2025
f49a4a3
feat(search-dropdown): add inputProps for enhanced input customization
jacksonkasi1 Mar 30, 2025
bd88421
refactor(search-dropdown): update styles and accessibility for improv…
jacksonkasi1 Mar 30, 2025
85c4f4d
feat(search-dropdown): enhance filtering logic for improved option vi…
jacksonkasi1 Mar 30, 2025
468d462
fix(search-dropdown): update menu behavior on selection
jacksonkasi1 Mar 30, 2025
ab0cb69
feat(search-dropdown): update stories to include new features and imp…
jacksonkasi1 Mar 30, 2025
7066599
fix(search-dropdown): update story title for consistency
jacksonkasi1 Mar 30, 2025
6f6b36b
fix search dropdown z-index
jacksonkasi1 Mar 31, 2025
81a0e82
update search dropdow menu cotainer index to 1
jacksonkasi1 Mar 31, 2025
1d6561c
added border radius to search dropdown input
jacksonkasi1 Mar 31, 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
3 changes: 3 additions & 0 deletions packages/ui/src/components/search-dropdown/private/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { INVALID_ID } from '../../../utilities/private/constants.js'

export type Id = typeof INVALID_ID | string
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import {
INVALID_ID,
ITEM_ID_DATA_ATTRIBUTE_NAME,
VIEWPORT_MARGIN
} from '../../../utilities/private/constants.js'
import { Id } from './types.js'

export function updateMenuElementLayout(
rootElement: HTMLDivElement,
menuElement: HTMLDivElement,
selectedId: Id
) {
const rootElementBoundingClientRect = rootElement.getBoundingClientRect()
const rootWidth = rootElement.offsetWidth
const rootHeight = rootElement.offsetHeight
const rootLeft = rootElementBoundingClientRect.left
const rootTop = rootElementBoundingClientRect.top

menuElement.style.minWidth = `${rootWidth}px`

const menuElementMaxWidth = window.innerWidth - 2 * VIEWPORT_MARGIN
menuElement.style.maxWidth = `${menuElementMaxWidth}px`

const menuElementMaxHeight = window.innerHeight - 2 * VIEWPORT_MARGIN
menuElement.style.maxHeight = `${menuElementMaxHeight}px`

const menuWidth = menuElement.offsetWidth
const menuHeight = menuElement.offsetHeight
const menuScrollHeight = menuElement.scrollHeight
const menuPaddingTop = parseInt(
window.getComputedStyle(menuElement).paddingTop,
10
)
const labelElement = getSelectedLabelElement(menuElement, selectedId)

const left = computeLeft({
menuWidth,
rootLeft
})
menuElement.style.left = `${left}px`

const top = computeTop({
menuHeight,
rootTop,
selectedTop: labelElement.offsetTop
})
menuElement.style.top = `${top}px`

const isScrollable = menuScrollHeight > menuHeight
if (!isScrollable) {
return
}
menuElement.scrollTop = computeScrollTop({
menuHeight,
menuPaddingTop,
menuScrollHeight,
rootHeight,
rootTop,
selectedTop: labelElement.offsetTop
})
}

function getSelectedLabelElement(
menuElement: HTMLDivElement,
selectedId: Id
): HTMLLabelElement {
const inputElement = menuElement.querySelector<HTMLInputElement>(
selectedId === INVALID_ID
? `[${ITEM_ID_DATA_ATTRIBUTE_NAME}]`
: `[${ITEM_ID_DATA_ATTRIBUTE_NAME}='${selectedId}']`
)
if (inputElement === null) {
throw new Error('`inputElement` is `null`')
}
const labelElement = inputElement.parentElement
if (labelElement === null) {
throw new Error('`labelElement` is `null`')
}
return labelElement as HTMLLabelElement
}

function computeLeft(options: { menuWidth: number; rootLeft: number }): number {
const { menuWidth, rootLeft } = options
if (rootLeft <= VIEWPORT_MARGIN) {
return VIEWPORT_MARGIN
}
const viewportWidth = window.innerWidth
if (rootLeft + menuWidth > viewportWidth - VIEWPORT_MARGIN) {
return viewportWidth - VIEWPORT_MARGIN - menuWidth
}
return rootLeft
}

function computeTop(options: {
menuHeight: number
rootTop: number
selectedTop: number
}): number {
const { menuHeight, rootTop, selectedTop } = options
const viewportHeight = window.innerHeight
if (
rootTop <= VIEWPORT_MARGIN ||
menuHeight === viewportHeight - 2 * VIEWPORT_MARGIN
) {
return VIEWPORT_MARGIN
}
// Position the selected element at `rootTop`
const top = rootTop - selectedTop
const minimumTop = VIEWPORT_MARGIN
const maximumTop = viewportHeight - VIEWPORT_MARGIN - menuHeight
return restrictToRange(top, minimumTop, maximumTop)
}

function computeScrollTop(options: {
menuHeight: number
menuPaddingTop: number
menuScrollHeight: number
rootHeight: number
rootTop: number
selectedTop: number
}): number {
const {
menuHeight,
menuPaddingTop,
menuScrollHeight,
rootHeight,
rootTop,
selectedTop
} = options
const restrictedRootTop = restrictToRange(
rootTop,
VIEWPORT_MARGIN,
window.innerHeight - VIEWPORT_MARGIN - rootHeight + menuPaddingTop / 2
)
const scrollTop = selectedTop - (restrictedRootTop - VIEWPORT_MARGIN)
const minimumScrollTop = 0
const maximumScrollTop = menuScrollHeight - menuHeight
return restrictToRange(scrollTop, minimumScrollTop, maximumScrollTop)
}

function restrictToRange(
value: number,
minimum: number,
maximum: number
): number {
return Math.min(Math.max(value, minimum), maximum)
}
147 changes: 147 additions & 0 deletions packages/ui/src/components/search-dropdown/search-dropdown.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
.searchDropdown {
position: relative;
display: flex;
width: 100%;
flex-direction: column;
outline: none;
}

.searchDropdown:focus {
outline: none;
}

.disabled,
.disabled * {
cursor: not-allowed;
}

.inputContainer {
position: relative;
display: flex;
width: 100%;
align-items: center;
}

.input {
display: block;
width: 100%;
height: var(--space-32);
padding: var(--space-0) var(--space-32);
border-radius: var(--border-radius-6);
background-color: var(--figma-color-bg-secondary);
color: var(--figma-color-text);
cursor: text;
}
.disabled .input {
color: var(--figma-color-text-disabled);
cursor: not-allowed;
}
.input::placeholder {
color: var(--figma-color-text-tertiary);
}

.searchIcon {
position: absolute;
top: var(--space-4);
left: var(--space-4);
color: var(--figma-color-icon-secondary);
pointer-events: none; /* so that clicking the icon focuses the textbox */
}
.disabled .searchIcon {
color: var(--figma-color-icon-disabled);
}
.input:focus ~ .searchIcon {
color: var(--figma-color-icon);
}

.icon {
position: absolute;
top: 50%;
left: var(--space-3);
color: var(--figma-color-icon-secondary);
pointer-events: none;
transform: translateY(-50%);
}

.clearButton {
position: absolute;
top: var(--space-0);
right: var(--space-0);
width: var(--space-32);
height: var(--space-32);
padding: calc(var(--space-4) - var(--border-width-1));
color: var(--figma-color-icon-secondary);
}
.searchTextbox:not(.disabled) .clearButton:hover,
.searchTextbox:not(.disabled) .clearButton:focus-visible {
color: var(--figma-color-icon);
}
.disabled .clearButton {
color: var(--figma-color-icon-disabled);
}

.clearButtonBox {
display: block;
border: var(--border-width-1) solid transparent;
border-radius: var(--border-radius-4);
}
.searchTextbox:not(.disabled) .clearButton:focus-visible .clearButtonBox {
border-color: var(--figma-color-border-selected);
}
.clearButtonBox svg {
display: block;
}

.chevronIcon {
position: absolute;
right: 0;
display: flex;
width: var(--space-32);
height: var(--space-32);
align-items: center;
justify-content: center;
}

.valueDisplay {
display: none;
}

/* Position the menu in the document body */
:global(.menu-container) {
position: fixed;
z-index: var(--z-index-1);
width: 100%;
padding: var(--space-4) var(--space-4);
}

.searchContainer {
position: sticky;
z-index: var(--z-index-3);
top: 0;
width: 100%;
padding: var(--space-8) var(--space-8);
border-bottom: var(--border-width-1) solid var(--figma-color-border);
background-color: var(--figma-color-bg);
}

.searchInputWrapper {
position: relative;
display: flex;
width: 100%;
align-items: center;
}

.searchInput {
width: 100%;
height: var(--space-32);
padding: var(--space-0) var(--space-8) var(--space-0) var(--space-32);
border: var(--border-width-1) solid var(--figma-color-border);
background-color: var(--figma-color-bg);
color: var(--figma-color-text);
font-size: var(--font-size-12);
}

.searchInput:focus {
border-color: var(--figma-color-border-selected);
outline: 0;
}
Loading