Skip to content

feat(icons): add category filtering and mobile category sheet - #739

Open
lain9293 wants to merge 12 commits into
mainfrom
feat/icon-categories
Open

feat(icons): add category filtering and mobile category sheet#739
lain9293 wants to merge 12 commits into
mainfrom
feat/icon-categories

Conversation

@lain9293

Copy link
Copy Markdown

Description

Adds category-based navigation to the Icons page.

Changes

  • Added 27 icon categories based on metadata from @gravity-ui/icons.
  • Added category counts and filtering.
  • Added a desktop category sidebar.
  • Added a mobile category selector with a bottom sheet.
  • Categories are disabled and ignored while text or image search is active.
  • Updated icon cards to match the Figma layout.
  • Updated @gravity-ui/icons to 2.22.0.

Testing

  • Verified category counts against the library metadata.
  • Verified desktop and mobile layouts locally.
  • Ran TypeScript, ESLint, and Stylelint checks.

Comment thread src/components/Icons/Icons.tsx Outdated
})}
onClick={() => handleSelectCategory('all')}
>
<span>All icons</span>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

i18n?

Comment thread src/components/Icons/Icons.tsx Outdated
contentClassName={b('category-sheet-content')}
visible={isCategorySheetOpen}
onClose={() => setIsCategorySheetOpen(false)}
title="Category"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

i18n?

Comment thread src/components/Icons/Icons.tsx Outdated
);

const selectedCategory = iconCategories.find(({id}) => id === categoryId);
const resultsTitle = isSearching ? 'All icons' : selectedCategory?.label ?? 'All icons';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

i18n?

@lain9293
lain9293 requested a review from teleginzhenya August 27, 2026 10:50
Comment thread src/components/Icons/Icons.tsx Outdated
!isSearching && selectedCategory
? t(`icons:categories.${selectedCategory.id}`)
: allIconsTitle;
const resultsCount = isSearching ? allIcons.length : icons.length;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Image

seems like we need to update number after filtering

/>

{isMobile && (
<Sheet

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Lets add allowHideOnContentScroll={false}, so sheet is not closing on touch from top to bottom on touch devices

teleginzhenya
teleginzhenya previously approved these changes Sep 8, 2026
imsitnikov
imsitnikov previously approved these changes Sep 8, 2026

@vvtimofeev vvtimofeev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ran this branch locally and checked the layout with Playwright across a range of viewports. The feature works as described on desktop — labels, counts and the disabled-while-searching state are fine, RU/EN translations resolve, typecheck/eslint/stylelint/madge are clean — but a few layout issues show up that CI can't catch (there is no e2e coverage for /icons):

  1. The fixed 150px grid collapses to a single column at 360px, and to two columns at 769–816px next to the sidebar.
  2. The sticky category sidebar is sized with 100vh inside a scroll container that is 81px shorter, so its last rows ("Food") are unreachable on screens under ~1300px tall.
  3. Switching to a smaller category deep in the page leaves the user at the footer.
  4. The tile hover colour sets the wrong CSS variable, so hover shows a translucent grey instead of #3c2e3a.
  5. The sheet re-opens by itself after crossing the 768px breakpoint while open, and the inner scroller in the sheet is what made allowHideOnContentScroll={false} necessary.

Details and suggested fixes are inline. The rest are smaller cleanups (Sheet styling hooks, duplicated button markup, the hardcoded category list, memoization, one RU label).

🤖 Review assisted by Claude Code; measurements are from a local Playwright run against this branch.

justify-content: center;
gap: pcVariables.$indentXS;
display: grid;
grid-template-columns: repeat(auto-fill, 150px);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

repeat(auto-fill, 150px) has no minmax() and no mobile override, so the number of tiles per row collapses on narrow content boxes. Measured on this branch (Playwright):

viewport tiles per row empty space on the right
320px 1 122px
360px (most common Android width) 1 162px
375–430px 2 7–62px
578px 2 162px
769–816px (desktop layout, sidebar shown) 2 121–168px
1280px 5 122px

At 360px "All icons" becomes a single column of 799 rows. The previous 62px flex-wrap layout filled any width and centered the rows. Something like repeat(auto-fill, minmax(…, 1fr)) / auto-fit, or a smaller tile below md, would avoid this.

position: sticky;
top: 76px;
flex: 0 0 200px;
max-height: calc(100vh - 96px);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The page doesn't scroll the window — it scrolls the OverlayScrollbars viewport below the sticky menu (81px tall), so 100vh overshoots the scrollport by the menu height. Measured at 1280×800, 1440×900 and 1920×1080: after scrolling, the aside sticks at y=157 and its bottom ends up 61px below the scrollport bottom. Its content is 1052px (28 rows) so it scrolls internally, but the last ~1.7 rows can never be fully shown — at 800px tall, "Food" sits at 825–861px and is unreachable. It only fits at viewport heights ≥ ~1300px.

top: 76px / 96px also hardcode the sticky search row height (8 + 44 + 8). Suggest deriving both from one variable and subtracting the header height (or sizing relative to the scroll container instead of 100vh).

: allIconsTitle;
const resultsCount = icons.length;

const handleSelectCategory = React.useCallback((nextCategoryId: string) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Switching categories doesn't re-anchor the scroll position. When the list shrinks from ~27,000px ("All icons") to a few rows, the scroll container clamps scrollTop and the user lands on the footer. Reproduced at 1280×800: scroll 12,000px down, click "Weather" in the sticky sidebar → scrollTop clamps to 350, the "Weather" heading is at −57px and the footer is on screen. Same on mobile after picking in the sheet (the trigger showing the new category name ends up off-screen).

handleClickToKeyword already does pageTitleRef.current?.scrollIntoView(...); the same is needed here (e.g. scroll to the results section).


&:hover {
--g-button-background-color: rgba(255, 255, 255, 0.16);
--g-button-background-color: #3c2e3a;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This never applies: uikit paints the hover state from --g-button-background-color-hover (.g-button:hover::before { background-color: var(--g-button-background-color-hover, var(--_--background-color-hover)) }), not from --g-button-background-color. Measured: the hovered tile's ::before background is rgba(255, 255, 255, 0.1) (the flat-view fallback), so on hover the solid #281a26 fill is replaced by a translucent grey instead of #3c2e3a. The same mistake existed before this PR, but with a transparent base it was invisible.

Suggested change
--g-button-background-color: #3c2e3a;
--g-button-background-color-hover: #3c2e3a;

Cleaner still: put --g-button-background-color-hover in the &.g-button block next to --g-button-background-color and drop the &:hover block (its specificity is also below the :has() selector above).

}

&__category-sheet-list {
max-height: calc(100vh - 168px);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This adds a second scroll container inside the Sheet, which already scrolls its own .g-sheet__sheet-scroll-container (capped at 0.9 × innerHeight − 20). The magic 168 (= 0.1·844 + 20 + 48 + 16) only matches a 390×844 viewport. Measured outer overflow: 412×892 → 5px, 430×932 → 9px, 768×1024 (iPad portrait, still the mobile layout) → 18px — after the inner list ends, scrolling chains to the outer container and the "Category" title jitters under the top bar. On iOS Safari (100vh = 844 while innerHeight = 664 with the toolbars) it becomes two full scroll layers.

It's also why allowHideOnContentScroll={false} became necessary: with the inner scroller the Sheet's own scrollTop stays 0, so any downward drag on the list was treated as a dismiss gesture. Dropping max-height/overflow-y here (and the prop) lets the Sheet scroll natively and restores pull-to-close; maxContentHeightCoefficient is available if the default height is too tall.


const categoryOptions = (
<React.Fragment>
<button

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This button is a copy of the mapped one below with 'all' in place of category.id, and !isSearching && categoryId === X is spelled out four times (plus the ternaries at lines 150 and 159). Something like

const activeCategoryId = isSearching ? 'all' : categoryId;

and a single ['all', ...ids].map(...) (with counts.all = allIcons.length) removes the duplication; selectedCategory (a find() whose only use is tautological), allIconsTitle and resultsCount then go away too.

Minor, same PR: gap: 24px under display: block (Icons.scss:235) and width: 100% on the block-level <section> (Icons.scss:277) are no-ops.

margin-top: 20px;
}

&__category {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The list re-implements hover/selected/disabled with literal colours (rgba(255,255,255,0.06), rgba(255,190,92,0.08)), while uikit Button has selected/disabled on theme tokens (--g-color-base-selection is already overridden globally in styles.scss) — Tags.tsx uses exactly that "All + filters" pattern, and NavigationLayout/SectionBlock.scss is the existing left-column list (active rgba(255,190,92,0.1)). Also, src/mixins.scss has window-breakpoint('md') for the md - 1 media query, and the spacing literals (32/20/16/12px) sit next to pcVariables.$indent* used elsewhere in this file. If the Figma look needs custom styling, tokens/variables would at least keep it in sync with the rest of the site.

<span className={b('results-count')}>{resultsCount}</span>
</div>
{icons.length ? (
<IconCollection icons={icons} onSelectIcon={handleSelectIcon} />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

IconCollection/IconButton aren't memoized, so every sheet open/close (and, pre-existing, every icon dialog open/close) re-renders all 799 uikit Button + Icon trees even though icons and handleSelectIcon are referentially stable — and the Sheet only appears after that pass. A same-shape tree measured 1.4–6.4ms per toggle on a fast desktop vs 0.1–0.3ms with React.memo; on a mid-range phone that's roughly 10–30ms of blocking per tap. export const IconCollection = React.memo(...) is enough (or keep the sheet state in a small child component).

Comment thread public/locales/ru/icons.json Outdated
const resultsCount = icons.length;

const handleSelectCategory = React.useCallback((nextCategoryId: string) => {
setCategoryId(nextCategoryId);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: no analytics on category selection. The page already sends icon_download via sendAnalyticsEvent, and #747 just instrumented the theme gallery filters — one icon_category_select event with the category id (and the source: sidebar/sheet) would make it possible to see which categories actually get used.

@vvtimofeev vvtimofeev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Requesting changes for the layout issues from my review above — mainly the single-column grid at 360px (IconCollection.scss:7), the sticky sidebar clipped inside the scroll container so "Food" is unreachable (Icons.scss:118), the scroll position not being restored on category change (Icons.tsx:164), the hover colour set on the wrong CSS variable (IconButton.scss:19), and the sheet re-opening on its own / nested scroller (Icons.tsx:310, Icons.scss:216). Details and suggested fixes are in the inline comments; the rest are non-blocking cleanups.

Co-authored-by: vvtimofeev <108340247+vvtimofeev@users.noreply.github.com>
@lain9293
lain9293 dismissed stale reviews from imsitnikov and teleginzhenya via 0cdde40 September 9, 2026 08:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants