diff --git a/dev/Generated/NavigationView.properties.cpp b/dev/Generated/NavigationView.properties.cpp index 305400d51c..c222bed2d2 100644 --- a/dev/Generated/NavigationView.properties.cpp +++ b/dev/Generated/NavigationView.properties.cpp @@ -20,6 +20,8 @@ GlobalDependencyProperty NavigationViewProperties::s_CompactPaneLengthProperty{ GlobalDependencyProperty NavigationViewProperties::s_ContentOverlayProperty{ nullptr }; GlobalDependencyProperty NavigationViewProperties::s_DisplayModeProperty{ nullptr }; GlobalDependencyProperty NavigationViewProperties::s_ExpandedModeThresholdWidthProperty{ nullptr }; +GlobalDependencyProperty NavigationViewProperties::s_FooterMenuItemsProperty{ nullptr }; +GlobalDependencyProperty NavigationViewProperties::s_FooterMenuItemsSourceProperty{ nullptr }; GlobalDependencyProperty NavigationViewProperties::s_HeaderProperty{ nullptr }; GlobalDependencyProperty NavigationViewProperties::s_HeaderTemplateProperty{ nullptr }; GlobalDependencyProperty NavigationViewProperties::s_IsBackButtonVisibleProperty{ nullptr }; @@ -143,6 +145,28 @@ void NavigationViewProperties::EnsureProperties() ValueHelper::BoxValueIfNecessary(1008.0), winrt::PropertyChangedCallback(&OnExpandedModeThresholdWidthPropertyChanged)); } + if (!s_FooterMenuItemsProperty) + { + s_FooterMenuItemsProperty = + InitializeDependencyProperty( + L"FooterMenuItems", + winrt::name_of>(), + winrt::name_of(), + false /* isAttached */, + ValueHelper>::BoxedDefaultValue(), + winrt::PropertyChangedCallback(&OnFooterMenuItemsPropertyChanged)); + } + if (!s_FooterMenuItemsSourceProperty) + { + s_FooterMenuItemsSourceProperty = + InitializeDependencyProperty( + L"FooterMenuItemsSource", + winrt::name_of(), + winrt::name_of(), + false /* isAttached */, + ValueHelper::BoxedDefaultValue(), + winrt::PropertyChangedCallback(&OnFooterMenuItemsSourcePropertyChanged)); + } if (!s_HeaderProperty) { s_HeaderProperty = @@ -462,6 +486,8 @@ void NavigationViewProperties::ClearProperties() s_ContentOverlayProperty = nullptr; s_DisplayModeProperty = nullptr; s_ExpandedModeThresholdWidthProperty = nullptr; + s_FooterMenuItemsProperty = nullptr; + s_FooterMenuItemsSourceProperty = nullptr; s_HeaderProperty = nullptr; s_HeaderTemplateProperty = nullptr; s_IsBackButtonVisibleProperty = nullptr; @@ -570,6 +596,22 @@ void NavigationViewProperties::OnExpandedModeThresholdWidthPropertyChanged( winrt::get_self(owner)->OnPropertyChanged(args); } +void NavigationViewProperties::OnFooterMenuItemsPropertyChanged( + winrt::DependencyObject const& sender, + winrt::DependencyPropertyChangedEventArgs const& args) +{ + auto owner = sender.as(); + winrt::get_self(owner)->OnPropertyChanged(args); +} + +void NavigationViewProperties::OnFooterMenuItemsSourcePropertyChanged( + winrt::DependencyObject const& sender, + winrt::DependencyPropertyChangedEventArgs const& args) +{ + auto owner = sender.as(); + winrt::get_self(owner)->OnPropertyChanged(args); +} + void NavigationViewProperties::OnHeaderPropertyChanged( winrt::DependencyObject const& sender, winrt::DependencyPropertyChangedEventArgs const& args) @@ -877,6 +919,32 @@ double NavigationViewProperties::ExpandedModeThresholdWidth() return ValueHelper::CastOrUnbox(static_cast(this)->GetValue(s_ExpandedModeThresholdWidthProperty)); } +void NavigationViewProperties::FooterMenuItems(winrt::IVector const& value) +{ + [[gsl::suppress(con)]] + { + static_cast(this)->SetValue(s_FooterMenuItemsProperty, ValueHelper>::BoxValueIfNecessary(value)); + } +} + +winrt::IVector NavigationViewProperties::FooterMenuItems() +{ + return ValueHelper>::CastOrUnbox(static_cast(this)->GetValue(s_FooterMenuItemsProperty)); +} + +void NavigationViewProperties::FooterMenuItemsSource(winrt::IInspectable const& value) +{ + [[gsl::suppress(con)]] + { + static_cast(this)->SetValue(s_FooterMenuItemsSourceProperty, ValueHelper::BoxValueIfNecessary(value)); + } +} + +winrt::IInspectable NavigationViewProperties::FooterMenuItemsSource() +{ + return ValueHelper::CastOrUnbox(static_cast(this)->GetValue(s_FooterMenuItemsSourceProperty)); +} + void NavigationViewProperties::Header(winrt::IInspectable const& value) { [[gsl::suppress(con)]] diff --git a/dev/Generated/NavigationView.properties.h b/dev/Generated/NavigationView.properties.h index 66dabb6b8e..47997f9a10 100644 --- a/dev/Generated/NavigationView.properties.h +++ b/dev/Generated/NavigationView.properties.h @@ -30,6 +30,12 @@ class NavigationViewProperties void ExpandedModeThresholdWidth(double value); double ExpandedModeThresholdWidth(); + void FooterMenuItems(winrt::IVector const& value); + winrt::IVector FooterMenuItems(); + + void FooterMenuItemsSource(winrt::IInspectable const& value); + winrt::IInspectable FooterMenuItemsSource(); + void Header(winrt::IInspectable const& value); winrt::IInspectable Header(); @@ -121,6 +127,8 @@ class NavigationViewProperties static winrt::DependencyProperty ContentOverlayProperty() { return s_ContentOverlayProperty; } static winrt::DependencyProperty DisplayModeProperty() { return s_DisplayModeProperty; } static winrt::DependencyProperty ExpandedModeThresholdWidthProperty() { return s_ExpandedModeThresholdWidthProperty; } + static winrt::DependencyProperty FooterMenuItemsProperty() { return s_FooterMenuItemsProperty; } + static winrt::DependencyProperty FooterMenuItemsSourceProperty() { return s_FooterMenuItemsSourceProperty; } static winrt::DependencyProperty HeaderProperty() { return s_HeaderProperty; } static winrt::DependencyProperty HeaderTemplateProperty() { return s_HeaderTemplateProperty; } static winrt::DependencyProperty IsBackButtonVisibleProperty() { return s_IsBackButtonVisibleProperty; } @@ -157,6 +165,8 @@ class NavigationViewProperties static GlobalDependencyProperty s_ContentOverlayProperty; static GlobalDependencyProperty s_DisplayModeProperty; static GlobalDependencyProperty s_ExpandedModeThresholdWidthProperty; + static GlobalDependencyProperty s_FooterMenuItemsProperty; + static GlobalDependencyProperty s_FooterMenuItemsSourceProperty; static GlobalDependencyProperty s_HeaderProperty; static GlobalDependencyProperty s_HeaderTemplateProperty; static GlobalDependencyProperty s_IsBackButtonVisibleProperty; @@ -245,6 +255,14 @@ class NavigationViewProperties winrt::DependencyObject const& sender, winrt::DependencyPropertyChangedEventArgs const& args); + static void OnFooterMenuItemsPropertyChanged( + winrt::DependencyObject const& sender, + winrt::DependencyPropertyChangedEventArgs const& args); + + static void OnFooterMenuItemsSourcePropertyChanged( + winrt::DependencyObject const& sender, + winrt::DependencyPropertyChangedEventArgs const& args); + static void OnHeaderPropertyChanged( winrt::DependencyObject const& sender, winrt::DependencyPropertyChangedEventArgs const& args); diff --git a/dev/NavigationView/NavigationView.cpp b/dev/NavigationView/NavigationView.cpp index b18723b7e5..294d8fc10e 100644 --- a/dev/NavigationView/NavigationView.cpp +++ b/dev/NavigationView/NavigationView.cpp @@ -36,8 +36,7 @@ static constexpr auto c_paneTitleHolderFrameworkElement = L"PaneTitleHolder"sv; static constexpr auto c_paneTitleFrameworkElement = L"PaneTitleTextBlock"sv; static constexpr auto c_rootSplitViewName = L"RootSplitView"sv; static constexpr auto c_menuItemsHost = L"MenuItemsHost"sv; -static constexpr auto c_settingsName = L"SettingsNavPaneItem"sv; -static constexpr auto c_settingsNameTopNav = L"SettingsTopNavPaneItem"sv; +static constexpr auto c_footerMenuItemsHost = L"FooterMenuItemsHost"sv; static constexpr auto c_selectionIndicatorName = L"SelectionIndicator"sv; static constexpr auto c_paneContentGridName = L"PaneContentGrid"sv; static constexpr auto c_rootGridName = L"RootGrid"sv; @@ -56,6 +55,7 @@ static constexpr auto c_flyoutRootGrid = L"FlyoutRootGrid"sv; // DisplayMode Top specific items static constexpr auto c_topNavMenuItemsHost = L"TopNavMenuItemsHost"sv; +static constexpr auto c_topNavFooterMenuItemsHost = L"TopFooterMenuItemsHost"sv; static constexpr auto c_topNavOverflowButton = L"TopNavOverflowButton"sv; static constexpr auto c_topNavMenuItemsOverflowHost = L"TopNavMenuItemsOverflowHost"sv; static constexpr auto c_topNavGrid = L"TopNavGrid"sv; @@ -85,6 +85,9 @@ static constexpr int c_toggleButtonHeightWhenShouldPreserveNavigationViewRS3Beha static constexpr int c_backButtonRowDefinition = 1; static constexpr float c_paneElevationTranslationZ = 32; +static constexpr int c_mainMenuBlockIndex = 0; +static constexpr int c_footerMenuBlockIndex = 1; + constexpr int s_itemNotFound{ -1 }; static winrt::Size c_infSize{ std::numeric_limits::infinity(), std::numeric_limits::infinity() }; @@ -106,8 +109,6 @@ void NavigationView::UnhookEventsAndClearFields(bool isFromDestructor) m_titleBarIsVisibleChangedRevoker.revoke(); m_paneToggleButtonClickRevoker.revoke(); - m_settingsItemTappedRevoker.revoke(); - m_settingsItemKeyDownRevoker.revoke(); m_settingsItem.set(nullptr); m_paneSearchButtonClickRevoker.revoke(); @@ -129,13 +130,27 @@ void NavigationView::UnhookEventsAndClearFields(bool isFromDestructor) m_leftNavItemsRepeaterElementPreparedRevoker.revoke(); m_leftNavItemsRepeaterElementClearingRevoker.revoke(); m_leftNavRepeaterLoadedRevoker.revoke(); + m_leftNavRepeaterGettingFocusRevoker.revoke(); m_leftNavRepeater.set(nullptr); m_topNavItemsRepeaterElementPreparedRevoker.revoke(); m_topNavItemsRepeaterElementClearingRevoker.revoke(); m_topNavRepeaterLoadedRevoker.revoke(); + m_topNavRepeaterGettingFocusRevoker.revoke(); m_topNavRepeater.set(nullptr); + m_leftNavFooterMenuItemsRepeaterElementPreparedRevoker.revoke(); + m_leftNavFooterMenuItemsRepeaterElementClearingRevoker.revoke(); + m_leftNavFooterMenuRepeaterLoadedRevoker.revoke(); + m_leftNavFooterMenuRepeaterGettingFocusRevoker.revoke(); + m_leftNavFooterMenuRepeater.set(nullptr); + + m_topNavFooterMenuItemsRepeaterElementPreparedRevoker.revoke(); + m_topNavFooterMenuItemsRepeaterElementClearingRevoker.revoke(); + m_topNavFooterMenuRepeaterLoadedRevoker.revoke(); + m_topNavFooterMenuRepeaterGettingFocusRevoker.revoke(); + m_topNavFooterMenuRepeater.set(nullptr); + m_topNavOverflowItemsRepeaterElementPreparedRevoker.revoke(); m_topNavOverflowItemsRepeaterElementClearingRevoker.revoke(); m_topNavRepeaterOverflowView.set(nullptr); @@ -153,9 +168,17 @@ NavigationView::NavigationView() SetDefaultStyleKey(this); SizeChanged({ this, &NavigationView::OnSizeChanged }); + + m_selectionModelSource = winrt::make>(2); + m_selectionModelSource.Append(nullptr); + m_selectionModelSource.Append(nullptr); + auto items = winrt::make>(); SetValue(s_MenuItemsProperty, items); + auto footerItems = winrt::make>(); + SetValue(s_FooterMenuItemsProperty, footerItems); + auto weakThis = get_weak(); m_topDataProvider.OnRawDataChanged( [weakThis](const winrt::NotifyCollectionChangedEventArgs& args) @@ -170,6 +193,7 @@ NavigationView::NavigationView() Loaded({ this, &NavigationView::OnLoaded }); m_selectionModel.SingleSelect(true); + m_selectionModel.Source(m_selectionModelSource); m_selectionChangedRevoker = m_selectionModel.SelectionChanged(winrt::auto_revoke, { this, &NavigationView::OnSelectionModelSelectionChanged }); m_childrenRequestedRevoker = m_selectionModel.ChildrenRequested(winrt::auto_revoke, { this, &NavigationView::OnSelectionModelChildrenRequested }); @@ -186,7 +210,12 @@ NavigationView::NavigationView() void NavigationView::OnSelectionModelChildrenRequested(const winrt::SelectionModel& selectionModel, const winrt::SelectionModelChildrenRequestedEventArgs& e) { - if (auto nvi = e.Source().try_as()) + // this is main menu or footer + if (e.SourceIndex().GetSize() == 1) + { + e.Children(e.Source()); + } + else if (auto nvi = e.Source().try_as()) { e.Children(GetChildren(nvi)); } @@ -196,6 +225,11 @@ void NavigationView::OnSelectionModelChildrenRequested(const winrt::SelectionMod } } +void NavigationView::OnFooterItemsSourceCollectionChanged(const winrt::IInspectable&, const winrt::IInspectable&) +{ + UpdateFooterRepeaterItemsSource(false /*sourceCollectionReset*/, true /*sourceCollectionChanged*/); +} + void NavigationView::OnSelectionModelSelectionChanged(const winrt::SelectionModel& selectionModel, const winrt::SelectionModelSelectionChangedEventArgs& e) { auto selectedItem = selectionModel.SelectedItem(); @@ -214,10 +248,12 @@ void NavigationView::OnSelectionModelSelectionChanged(const winrt::SelectionMode bool setSelectedItem = true; auto const selectedIndex = selectionModel.SelectedIndex(); + if (IsTopNavigationView()) { + auto const inMainMenu = selectedIndex.GetAt(0) == c_mainMenuBlockIndex; // If selectedIndex does not exist, means item is being deselected through API - const auto isInOverflow = (selectedIndex && selectedIndex.GetSize() > 0) ? !m_topDataProvider.IsItemInPrimaryList(selectedIndex.GetAt(0)) : false; + auto const isInOverflow = (selectedIndex && selectedIndex.GetSize() > 0) ? inMainMenu && !m_topDataProvider.IsItemInPrimaryList(selectedIndex.GetAt(1)) : false; if (isInOverflow) { // We only want to close the overflow flyout and move the item on selection if it is a leaf node @@ -293,7 +329,7 @@ void NavigationView::CloseFlyoutIfRequired(const winrt::NavigationViewItem& sele if (isInModeWithFlyout && selectedIndex && !DoesNavigationViewItemHaveChildren(selectedItem)) { // Item selected is a leaf node, find top level parent and close flyout - if (auto const rootItem = GetContainerForIndex(selectedIndex.GetAt(0))) + if (auto const rootItem = GetContainerForIndex(selectedIndex.GetAt(1), selectedIndex.GetAt(0) == c_footerMenuBlockIndex /* inFooter */)) { if (auto const nvi = rootItem.try_as()) { @@ -384,6 +420,8 @@ void NavigationView::OnApplyTemplate() m_leftNavRepeaterLoadedRevoker = leftNavRepeater.Loaded(winrt::auto_revoke, { this, &NavigationView::OnRepeaterLoaded }); + m_leftNavRepeaterGettingFocusRevoker = leftNavRepeater.GettingFocus(winrt::auto_revoke, { this, &NavigationView::OnRepeaterGettingFocus }); + leftNavRepeater.ItemTemplate(*m_navigationViewItemsFactory); } @@ -404,6 +442,8 @@ void NavigationView::OnApplyTemplate() m_topNavRepeaterLoadedRevoker = topNavRepeater.Loaded(winrt::auto_revoke, { this, &NavigationView::OnRepeaterLoaded }); + m_topNavRepeaterGettingFocusRevoker = topNavRepeater.GettingFocus(winrt::auto_revoke, { this, &NavigationView::OnRepeaterGettingFocus }); + topNavRepeater.ItemTemplate(*m_navigationViewItemsFactory); } @@ -444,6 +484,52 @@ void NavigationView::OnApplyTemplate() } } + // Change code to NOT do this if we're in top nav mode, to prevent it from being realized: + if (auto leftFooterMenuNavRepeater = GetTemplateChildT(c_footerMenuItemsHost, controlProtected)) + { + m_leftNavFooterMenuRepeater.set(leftFooterMenuNavRepeater); + + // API is currently in preview, so setting this via code. + // Disabling virtualization for now because of https://github.com/microsoft/microsoft-ui-xaml/issues/2095 + if (auto stackLayout = leftFooterMenuNavRepeater.Layout().try_as()) + { + auto stackLayoutImpl = winrt::get_self(stackLayout); + stackLayoutImpl->DisableVirtualization(true); + } + + m_leftNavFooterMenuItemsRepeaterElementPreparedRevoker = leftFooterMenuNavRepeater.ElementPrepared(winrt::auto_revoke, { this, &NavigationView::OnRepeaterElementPrepared }); + m_leftNavFooterMenuItemsRepeaterElementClearingRevoker = leftFooterMenuNavRepeater.ElementClearing(winrt::auto_revoke, { this, &NavigationView::OnRepeaterElementClearing }); + + m_leftNavFooterMenuRepeaterLoadedRevoker = leftFooterMenuNavRepeater.Loaded(winrt::auto_revoke, { this, &NavigationView::OnRepeaterLoaded }); + + m_leftNavFooterMenuRepeaterGettingFocusRevoker = leftFooterMenuNavRepeater.GettingFocus(winrt::auto_revoke, { this, &NavigationView::OnRepeaterGettingFocus }); + + leftFooterMenuNavRepeater.ItemTemplate(*m_navigationViewItemsFactory); + } + + // Change code to NOT do this if we're in left nav mode, to prevent it from being realized: + if (auto topFooterMenuNavRepeater = GetTemplateChildT(c_topNavFooterMenuItemsHost, controlProtected)) + { + m_topNavFooterMenuRepeater.set(topFooterMenuNavRepeater); + + // API is currently in preview, so setting this via code. + // Disabling virtualization for now because of https://github.com/microsoft/microsoft-ui-xaml/issues/2095 + if (auto stackLayout = topFooterMenuNavRepeater.Layout().try_as()) + { + auto stackLayoutImpl = winrt::get_self(stackLayout); + stackLayoutImpl->DisableVirtualization(true); + } + + m_topNavFooterMenuItemsRepeaterElementPreparedRevoker = topFooterMenuNavRepeater.ElementPrepared(winrt::auto_revoke, { this, &NavigationView::OnRepeaterElementPrepared }); + m_topNavFooterMenuItemsRepeaterElementClearingRevoker = topFooterMenuNavRepeater.ElementClearing(winrt::auto_revoke, { this, &NavigationView::OnRepeaterElementClearing }); + + m_topNavFooterMenuRepeaterLoadedRevoker = topFooterMenuNavRepeater.Loaded(winrt::auto_revoke, { this, &NavigationView::OnRepeaterLoaded }); + + m_topNavFooterMenuRepeaterGettingFocusRevoker = topFooterMenuNavRepeater.GettingFocus(winrt::auto_revoke, { this, &NavigationView::OnRepeaterGettingFocus }); + + topFooterMenuNavRepeater.ItemTemplate(*m_navigationViewItemsFactory); + } + m_topNavContentOverlayAreaGrid.set(GetTemplateChildT(c_topNavContentOverlayAreaGrid, controlProtected)); m_leftNavPaneAutoSuggestBoxPresenter.set(GetTemplateChildT(c_leftNavPaneAutoSuggestBoxPresenter, controlProtected)); m_topNavPaneAutoSuggestBoxPresenter.set(GetTemplateChildT(c_topNavPaneAutoSuggestBoxPresenter, controlProtected)); @@ -575,7 +661,7 @@ void NavigationView::UpdateRepeaterItemsSource(bool forceSelectionModelUpdate) // has changed. if (forceSelectionModelUpdate) { - m_selectionModel.Source(itemsSource); + m_selectionModelSource.SetAt(0, itemsSource); } if (IsTopNavigationView()) @@ -583,7 +669,6 @@ void NavigationView::UpdateRepeaterItemsSource(bool forceSelectionModelUpdate) UpdateLeftRepeaterItemSource(nullptr); UpdateTopNavRepeatersItemSource(itemsSource); InvalidateTopNavPrimaryLayout(); - SyncSettingsSelectionState(); } else { @@ -624,6 +709,80 @@ void NavigationView::UpdateItemsRepeaterItemsSource(const winrt::ItemsRepeater& } } +void NavigationView::UpdateFooterRepeaterItemsSource(bool sourceCollectionReset, bool sourceCollectionChanged) +{ + if (!m_appliedTemplate) return; + + auto const itemsSource = [this]() + { + if (auto const menuItemsSource = FooterMenuItemsSource()) + { + return menuItemsSource; + } + UpdateSelectionForMenuItems(); + return FooterMenuItems().as(); + }(); + + + UpdateItemsRepeaterItemsSource(m_leftNavFooterMenuRepeater.get(), nullptr); + UpdateItemsRepeaterItemsSource(m_topNavFooterMenuRepeater.get(), nullptr); + + if (!m_settingsItem || sourceCollectionChanged || sourceCollectionReset) + { + auto dataSource = winrt::make>(); + + if (!m_settingsItem) + { + m_settingsItem.set(winrt::make < ::NavigationViewItem>()); + auto settingsItem = m_settingsItem.get(); + settingsItem.Name(L"SettingsItem"); + m_navigationViewItemsFactory.get()->SettingsItem(settingsItem); + } + + if (sourceCollectionReset) + { + m_footerItemsCollectionChangedRevoker.revoke(); + m_footerItemsSource = nullptr; + } + + if (!m_footerItemsSource) + { + m_footerItemsSource = winrt::ItemsSourceView(itemsSource); + m_footerItemsCollectionChangedRevoker = m_footerItemsSource.CollectionChanged(winrt::auto_revoke, { this, &NavigationView::OnFooterItemsSourceCollectionChanged }); + } + + if (m_footerItemsSource) + { + auto settingsItem = m_settingsItem.get(); + const auto size = m_footerItemsSource.Count(); + + for (int32_t i = 0; i < size; i++) + { + auto item = m_footerItemsSource.GetAt(i).as(); + dataSource.Append(item); + } + + if (IsSettingsVisible()) + { + CreateAndHookEventsToSettings(); + // add settings item to the end of footer + dataSource.Append(settingsItem); + } + } + + m_selectionModelSource.SetAt(1, dataSource); + } + + if (IsTopNavigationView()) + { + UpdateItemsRepeaterItemsSource(m_topNavFooterMenuRepeater.get(), m_selectionModelSource.GetAt(1)); + } + else + { + UpdateItemsRepeaterItemsSource(m_leftNavFooterMenuRepeater.get(), m_selectionModelSource.GetAt(1)); + } +} + void NavigationView::OnFlyoutClosing(const winrt::IInspectable& sender, const winrt::FlyoutBaseClosingEventArgs& args) { // If the user selected an parent item in the overflow flyout then the item has not been moved to top primary yet. @@ -635,7 +794,7 @@ void NavigationView::OnFlyoutClosing(const winrt::IInspectable& sender, const wi auto const selectedIndex = m_selectionModel.SelectedIndex(); if (selectedIndex.GetSize() > 0) { - if (auto const firstContainer = GetContainerForIndex(selectedIndex.GetAt(0))) + if (auto const firstContainer = GetContainerForIndex(selectedIndex.GetAt(1), false /*infooter*/)) { if (auto const firstNVI = firstContainer.try_as()) { @@ -644,7 +803,7 @@ void NavigationView::OnFlyoutClosing(const winrt::IInspectable& sender, const wi } } - SelectandMoveOverflowItem(SelectedItem(), selectedIndex, false); + SelectandMoveOverflowItem(SelectedItem(), selectedIndex, false /*closeFlyout*/); } } } @@ -730,7 +889,7 @@ void NavigationView::RaiseItemInvokedForNavigationViewItem(const winrt::Navigati return NavigationRecommendedTransitionDirection::Default; }(); - RaiseItemInvoked(nextItem, false /*isSettings*/, nvi, recommendedDirection); + RaiseItemInvoked(nextItem, IsSettingsItem(nvi) /*isSettings*/, nvi, recommendedDirection); } void NavigationView::OnNavigationViewItemInvoked(const winrt::NavigationViewItem& nvi) @@ -772,7 +931,9 @@ bool NavigationView::IsRootItemsRepeater(const winrt::DependencyObject& element) { return (element == m_topNavRepeater.get() || element == m_leftNavRepeater.get() || - element == m_topNavRepeaterOverflowView.get()); + element == m_topNavRepeaterOverflowView.get() || + element == m_leftNavFooterMenuRepeater.get() || + element == m_topNavFooterMenuRepeater.get()); } return false; } @@ -786,6 +947,18 @@ bool NavigationView::IsRootGridOfFlyout(const winrt::DependencyObject& element) return false; } +winrt::ItemsRepeater NavigationView::GetParentRootItemsRepeaterForContainer(const winrt::NavigationViewItemBase& nvib) +{ + auto parentIR = GetParentItemsRepeaterForContainer(nvib); + auto currentNvib = nvib; + while (!IsRootItemsRepeater(parentIR)) + { + currentNvib = GetParentNavigationViewItemForContainer(currentNvib); + parentIR = GetParentItemsRepeaterForContainer(currentNvib); + } + return parentIR; +} + winrt::ItemsRepeater NavigationView::GetParentItemsRepeaterForContainer(const winrt::NavigationViewItemBase& nvib) { if (auto parent = winrt::VisualTreeHelper::GetParent(nvib)) @@ -820,6 +993,7 @@ winrt::NavigationViewItem NavigationView::GetParentNavigationViewItemForContaine winrt::IndexPath NavigationView::GetIndexPathForContainer(const winrt::NavigationViewItemBase& nvib) { auto path = std::vector(); + bool isInFooterMenu = false; winrt::DependencyObject child = nvib; auto parent = winrt::VisualTreeHelper::GetParent(child); @@ -874,6 +1048,10 @@ winrt::IndexPath NavigationView::GetIndexPathForContainer(const winrt::Navigatio path.insert(path.begin(), parentIR.GetElementIndex(child.try_as())); } + isInFooterMenu = parent == m_leftNavFooterMenuRepeater.get() || parent == m_topNavFooterMenuRepeater.get(); + + path.insert(path.begin(), isInFooterMenu ? c_footerMenuBlockIndex : c_mainMenuBlockIndex); + return IndexPath::CreateFromIndices(path); } @@ -901,8 +1079,16 @@ void NavigationView::OnRepeaterElementPrepared(const winrt::ItemsRepeater& ir, c { return NavigationViewRepeaterPosition::TopPrimary; } + if (ir == m_topNavFooterMenuRepeater.get()) + { + return NavigationViewRepeaterPosition::TopFooter; + } return NavigationViewRepeaterPosition::TopOverflow; } + if (ir == m_leftNavFooterMenuRepeater.get()) + { + return NavigationViewRepeaterPosition::LeftFooter; + } return NavigationViewRepeaterPosition::LeftNav; }(); nvibImpl->Position(position); @@ -989,62 +1175,35 @@ void NavigationView::OnRepeaterElementClearing(const winrt::ItemsRepeater& ir, c } // Hook up the Settings Item Invoked event listener -void NavigationView::CreateAndHookEventsToSettings(std::wstring_view settingsName) +void NavigationView::CreateAndHookEventsToSettings() { - winrt::IControlProtected controlProtected = *this; - auto settingsItem = GetTemplateChildT(settingsName, controlProtected); - if (settingsItem && settingsItem != m_settingsItem.get()) + if (!m_settingsItem) { - // If the old settings item is selected, move the selection to the new one. - auto selectedItem = SelectedItem(); - const bool shouldSelectSetting = selectedItem && IsSettingsItem(selectedItem); - - if (shouldSelectSetting) - { - auto scopeGuard = gsl::finally([this]() - { - m_shouldIgnoreNextSelectionChangeBecauseSettingsRestore = false; - }); - m_shouldIgnoreNextSelectionChangeBecauseSettingsRestore = true; - SetSelectedItemAndExpectItemInvokeWhenSelectionChangedIfNotInvokedFromAPI(nullptr); - } - - m_settingsItemTappedRevoker.revoke(); - m_settingsItemKeyDownRevoker.revoke(); - - m_settingsItem.set(settingsItem); - m_settingsItemTappedRevoker = settingsItem.Tapped(winrt::auto_revoke, { this, &NavigationView::OnNavigationViewItemTapped }); - m_settingsItemKeyDownRevoker = settingsItem.KeyDown(winrt::auto_revoke, { this, &NavigationView::OnNavigationViewItemKeyDown }); - - auto nvibImpl = winrt::get_self(settingsItem); - nvibImpl->SetNavigationViewParent(*this); - - // Do localization for settings item label and Automation Name - auto localizedSettingsName = ResourceAccessor::GetLocalizedStringResource(SR_SettingsButtonName); - winrt::AutomationProperties::SetName(settingsItem, localizedSettingsName); - settingsItem.Tag(box_value(localizedSettingsName)); - - UpdateSettingsItemToolTip(); + return; + } - // Add the name only in case of horizontal nav - if (!IsTopNavigationView()) - { - settingsItem.Content(box_value(localizedSettingsName)); - } + auto settingsItem = m_settingsItem.get(); + auto settingsIcon = winrt::SymbolIcon(winrt::Symbol::Setting); + settingsItem.Icon(settingsIcon); - // hook up SettingsItem - SetValue(s_SettingsItemProperty, settingsItem); + // Do localization for settings item label and Automation Name + auto localizedSettingsName = ResourceAccessor::GetLocalizedStringResource(SR_SettingsButtonName); + winrt::AutomationProperties::SetName(settingsItem, localizedSettingsName); + settingsItem.Tag(box_value(localizedSettingsName)); + UpdateSettingsItemToolTip(); - if (shouldSelectSetting) - { - auto scopeGuard = gsl::finally([this]() - { - m_shouldIgnoreNextSelectionChangeBecauseSettingsRestore = false; - }); - m_shouldIgnoreNextSelectionChangeBecauseSettingsRestore = true; - SetSelectedItemAndExpectItemInvokeWhenSelectionChangedIfNotInvokedFromAPI(m_settingsItem.get()); - } + // Add the name only in case of horizontal nav + if (!IsTopNavigationView()) + { + settingsItem.Content(box_value(localizedSettingsName)); + } + else + { + settingsItem.Content(nullptr); } + + // hook up SettingsItem + SetValue(s_SettingsItemProperty, settingsItem); } winrt::Size NavigationView::MeasureOverride(winrt::Size const& availableSize) @@ -1087,6 +1246,12 @@ void NavigationView::OnLayoutUpdated(const winrt::IInspectable& sender, const wi m_lastSelectedItemPendingAnimationInTopNav.set(nullptr); AnimateSelectionChanged(lastSelectedItemInTopNav); } + + if (m_OrientationChangedPendingAnimation) + { + m_OrientationChangedPendingAnimation = false; + AnimateSelectionChanged(SelectedItem()); + } } void NavigationView::OnSizeChanged(winrt::IInspectable const& /*sender*/, winrt::SizeChangedEventArgs const& args) @@ -1884,11 +2049,6 @@ void NavigationView::RaiseSelectionChangedEvent(winrt::IInspectable const& nextI // If nextItem is selectionsuppressed, we should undo the selection. We didn't undo it OnSelectionChange because we want change by API has the same undo logic. void NavigationView::ChangeSelection(const winrt::IInspectable& prevItem, const winrt::IInspectable& nextItem) { - // Selection changed event was requested to be ignored by settings item restoration, so let's do that - if (m_shouldIgnoreNextSelectionChangeBecauseSettingsRestore) { - return; - } - const bool isSettingsItem = IsSettingsItem(nextItem); if (IsSelectionSuppressed(nextItem)) @@ -1899,19 +2059,12 @@ void NavigationView::ChangeSelection(const winrt::IInspectable& prevItem, const } else { - // TODO: The raising of this event can probably can be moved where the settings invocation is first handled. - // Need to raise ItemInvoked for when the settings item gets invoked - if (isSettingsItem) - { - RaiseItemInvoked(nextItem, isSettingsItem); - } - // Other transition other than default only apply to topnav // when clicking overflow on topnav, transition is from bottom // otherwise if prevItem is on left side of nextActualItem, transition is from left // if prevItem is on right side of nextActualItem, transition is from right // click on Settings item is considered Default - auto recommendedDirection = [this, prevItem, nextItem, isSettingsItem]() + auto recommendedDirection = [this, prevItem, nextItem]() { if (IsTopNavigationView()) { @@ -1919,7 +2072,7 @@ void NavigationView::ChangeSelection(const winrt::IInspectable& prevItem, const { return NavigationRecommendedTransitionDirection::FromOverflow; } - else if (!isSettingsItem && prevItem && nextItem) + else if (prevItem && nextItem) { return GetRecommendedTransitionDirection(NavigationViewItemBaseOrSettingsContentFromData(prevItem), NavigationViewItemBaseOrSettingsContentFromData(nextItem)); @@ -1985,7 +2138,7 @@ void NavigationView::UpdateSelectionModelSelection(const winrt::IndexPath& ip) { auto const prevIndexPath = m_selectionModel.SelectedIndex(); m_selectionModel.SelectAt(ip); - UpdateIsChildSelected(prevIndexPath, ip); + UpdateIsChildSelected(prevIndexPath, ip); } void NavigationView::UpdateIsChildSelected(const winrt::IndexPath& prevIP, const winrt::IndexPath& nextIP) @@ -2004,8 +2157,11 @@ void NavigationView::UpdateIsChildSelected(const winrt::IndexPath& prevIP, const void NavigationView::UpdateIsChildSelectedForIndexPath(const winrt::IndexPath& ip, bool isChildSelected) { // Update the isChildSelected property for every container on the IndexPath (with the exception of the actual container pointed to by the indexpath) - auto container = GetContainerForIndex(ip.GetAt(0)); - auto index = 1; + auto container = GetContainerForIndex(ip.GetAt(1), ip.GetAt(0) == c_footerMenuBlockIndex /*inFooter*/); + // first index is fo mainmenu or footer + // second is index of item in mainmenu or footer + // next in menuitem children + auto index = 2; while (container) { if (auto const nvi = container.try_as()) @@ -2180,14 +2336,7 @@ void NavigationView::OnNavigationViewItemTapped(const winrt::IInspectable& sende { if (auto nvi = sender.try_as()) { - if (IsSettingsItem(nvi)) - { - OnSettingsInvoked(); - } - else - { - OnNavigationViewItemInvoked(nvi); - } + OnNavigationViewItemInvoked(nvi); nvi.Focus(winrt::FocusState::Pointer); args.Handled(true); } @@ -2220,39 +2369,27 @@ void NavigationView::OnNavigationViewItemKeyDown(const winrt::IInspectable& send void NavigationView::HandleKeyEventForNavigationViewItem(const winrt::NavigationViewItem& nvi, const winrt::KeyRoutedEventArgs& args) { const auto key = args.Key(); - if (IsSettingsItem(nvi)) - { - if (key == winrt::VirtualKey::Space || - key == winrt::VirtualKey::Enter) - { - args.Handled(true); - OnSettingsInvoked(); - } - } - else + switch (key) { - switch (key) - { - case winrt::VirtualKey::Enter: - case winrt::VirtualKey::Space: - args.Handled(true); - OnNavigationViewItemInvoked(nvi); - break; - case winrt::VirtualKey::Home: - args.Handled(true); - KeyboardFocusFirstItemFromItem(nvi); - break; - case winrt::VirtualKey::End: - args.Handled(true); - KeyboardFocusLastItemFromItem(nvi); - break; - case winrt::VirtualKey::Down: - FocusNextDownItem(nvi, args); - break; - case winrt::VirtualKey::Up: - FocusNextUpItem(nvi, args); - break; - } + case winrt::VirtualKey::Enter: + case winrt::VirtualKey::Space: + args.Handled(true); + OnNavigationViewItemInvoked(nvi); + break; + case winrt::VirtualKey::Home: + args.Handled(true); + KeyboardFocusFirstItemFromItem(nvi); + break; + case winrt::VirtualKey::End: + args.Handled(true); + KeyboardFocusLastItemFromItem(nvi); + break; + case winrt::VirtualKey::Down: + FocusNextDownItem(nvi, args); + break; + case winrt::VirtualKey::Up: + FocusNextUpItem(nvi, args); + break; } } @@ -2338,18 +2475,8 @@ void NavigationView::KeyboardFocusFirstItemFromItem(const winrt::NavigationViewI { auto const firstElement = [this, nvib]() { - if (IsTopNavigationView()) - { - if (IsContainerInOverflow(nvib)) - { - return m_topNavRepeaterOverflowView.get().TryGetElement(0); - } - else - { - return m_topNavRepeater.get().TryGetElement(0); - } - } - return m_leftNavRepeater.get().TryGetElement(0); + auto const parentIR = GetParentRootItemsRepeaterForContainer(nvib); + return parentIR.TryGetElement(0); }(); if (auto controlFirst = firstElement.try_as()) @@ -2360,36 +2487,81 @@ void NavigationView::KeyboardFocusFirstItemFromItem(const winrt::NavigationViewI void NavigationView::KeyboardFocusLastItemFromItem(const winrt::NavigationViewItemBase& nvib) { - auto const ir = [this, nvib]() + auto const parentIR = GetParentRootItemsRepeaterForContainer(nvib); + + if (auto itemsSourceView = parentIR.ItemsSourceView()) { - if (IsTopNavigationView()) + const auto lastIndex = itemsSourceView.Count() - 1; + if (auto lastElement = parentIR.TryGetElement(lastIndex)) { - if (IsContainerInOverflow(nvib)) + if (auto controlLast = lastElement.try_as()) { - return m_topNavRepeaterOverflowView.get(); - } - else - { - return m_topNavRepeater.get(); + controlLast.Focus(winrt::FocusState::Programmatic); } } - else - { - return m_leftNavRepeater.get(); - } - }(); + } +} - if (const auto itemsSourceView = ir.ItemsSourceView()) +void NavigationView::OnRepeaterGettingFocus(const winrt::IInspectable& sender, const winrt::GettingFocusEventArgs& args) +{ + // if focus change was invoked by tab key + // and there is selected item in ItemsRepeater that gatting focus + // we should put focus on selected item + if (m_TabKeyPrecedesFocusChange && args.InputDevice() == winrt::FocusInputDeviceKind::Keyboard && m_selectionModel.SelectedIndex()) { - const auto lastIndex = itemsSourceView.Count() - 1; - if (const auto lastElement = ir.TryGetElement(lastIndex)) + if (auto const oldFocusedElement = args.OldFocusedElement()) { - if (const auto controlLast = lastElement.try_as()) + if (auto const newRootItemsRepeater = sender.try_as()) { - controlLast.Focus(winrt::FocusState::Programmatic); + auto const isFocusOutsideCurrentRootRepeater = [this, oldFocusedElement, newRootItemsRepeater]() + { + bool isFocusOutsideCurrentRootRepeater = true; + auto treeWalkerCursor = oldFocusedElement; + + // check if last focused element was in same root repeater + while (treeWalkerCursor) + { + if (auto oldFocusedNavigationItemBase = treeWalkerCursor.try_as()) + { + auto const oldParentRootRepeater = GetParentRootItemsRepeaterForContainer(oldFocusedNavigationItemBase); + isFocusOutsideCurrentRootRepeater = oldParentRootRepeater != newRootItemsRepeater; + break; + } + + treeWalkerCursor = winrt::VisualTreeHelper::GetParent(treeWalkerCursor); + } + + return isFocusOutsideCurrentRootRepeater; + }(); + + auto const rootRepeaterForSelectedItem = [this]() + { + if (IsTopNavigationView()) + { + return m_selectionModel.SelectedIndex().GetAt(0) == c_mainMenuBlockIndex ? m_topNavRepeater.get() : m_topNavFooterMenuRepeater.get(); + } + return m_selectionModel.SelectedIndex().GetAt(0) == c_mainMenuBlockIndex ? m_leftNavRepeater.get() : m_leftNavFooterMenuRepeater.get(); + }(); + + // If focus is coming from outside the root repeater, + // and selected item is within current repeater + // we should put focus on selected item + if (auto const argsAsIGettingFocusEventArgs2 = args.try_as()) + { + if (newRootItemsRepeater == rootRepeaterForSelectedItem && isFocusOutsideCurrentRootRepeater) + { + auto const selectedContainer = GetContainerForIndexPath(m_selectionModel.SelectedIndex(), true /* lastVisible */); + if (argsAsIGettingFocusEventArgs2.TrySetNewFocusedElement(selectedContainer)) + { + args.Handled(true); + } + } + } } } } + + m_TabKeyPrecedesFocusChange = false; } void NavigationView::OnNavigationViewItemOnGotFocus(const winrt::IInspectable& sender, winrt::RoutedEventArgs const& e) @@ -2399,7 +2571,10 @@ void NavigationView::OnNavigationViewItemOnGotFocus(const winrt::IInspectable& s // Achieve selection follows focus behavior if (IsNavigationViewListSingleSelectionFollowsFocus()) { - if (nvi.SelectsOnInvoked()) + // if nvi is already selected we don't need to invoke it again + // otherwise ItemInvoked fires twice when item was tapped + // or fired when window gets focus + if (nvi.SelectsOnInvoked() && !nvi.IsSelected()) { if (IsTopNavigationView()) { @@ -2422,24 +2597,26 @@ void NavigationView::OnNavigationViewItemOnGotFocus(const winrt::IInspectable& s void NavigationView::OnSettingsInvoked() { - auto prevItem = SelectedItem(); auto settingsItem = m_settingsItem.get(); - if (IsSettingsItem(prevItem)) + if (settingsItem) { - RaiseItemInvoked(settingsItem, true /*isSettings*/); - } - else if (settingsItem) - { - SetSelectedItemAndExpectItemInvokeWhenSelectionChangedIfNotInvokedFromAPI(settingsItem); + OnNavigationViewItemInvoked(settingsItem); } } +void NavigationView::OnPreviewKeyDown(winrt::KeyRoutedEventArgs const& e) +{ + m_TabKeyPrecedesFocusChange = false; + __super::OnPreviewKeyDown(e); +} + void NavigationView::OnKeyDown(winrt::KeyRoutedEventArgs const& e) { const auto& eventArgs = e; const auto key = eventArgs.Key(); bool handled = false; + m_TabKeyPrecedesFocusChange = false; switch (key) { @@ -2463,6 +2640,11 @@ void NavigationView::OnKeyDown(winrt::KeyRoutedEventArgs const& e) case winrt::VirtualKey::GamepadRightShoulder: handled = BumperNavigation(1); break; + case winrt::VirtualKey::Tab: + // arrow keys navigation through ItemsRepeater don't get here + // so handle tab key to distinguish between tab focus and arrow focus navigation + m_TabKeyPrecedesFocusChange = true; + break; case winrt::VirtualKey::Left: auto altState = winrt::CoreWindow::GetForCurrentThread().GetKeyState(winrt::VirtualKey::Menu); const bool isAltPressed = (altState & winrt::CoreVirtualKeyStates::Down) == winrt::CoreVirtualKeyStates::Down; @@ -2514,30 +2696,52 @@ bool NavigationView::BumperNavigation(int offset) { if (auto nvi = NavigationViewItemOrSettingsContentFromData(item)) { - auto index = m_topDataProvider.IndexOf(item, NavigationViewSplitVectorID::PrimaryList); + auto indexPath = GetIndexPathForContainer(nvi); + const auto isInFooter = indexPath.GetAt(0) == c_footerMenuBlockIndex; + + const auto indexInMainList = isInFooter ? -1 : indexPath.GetAt(1); + const auto indexInFooter = isInFooter ? indexPath.GetAt(1) : -1; + + auto topNavRepeater = m_topNavRepeater.get(); + const auto topPrimaryListSize = m_topDataProvider.GetPrimaryListSize(); - if (index >= 0) + auto footerRepeater = m_topNavFooterMenuRepeater.get(); + auto footerItemsSize = FooterMenuItems().Size(); + + if (IsSettingsVisible()) { - if (auto&& topNavRepeater = m_topNavRepeater.get()) + footerItemsSize++; + } + + if (indexInMainList >= 0) + { + + if (SelectSelectableItemWithOffset(indexInMainList, offset, topNavRepeater, topPrimaryListSize)) { - const auto topPrimaryListSize = m_topDataProvider.GetPrimaryListSize(); - index += offset; + return true; + } - while (index > -1 && index < topPrimaryListSize) - { - auto newItem = topNavRepeater.TryGetElement(index); - if (auto newNavViewItem = newItem.try_as()) - { - // This is done to skip Separators or other items that are not NavigationViewItems - if (winrt::get_self(newNavViewItem)->SelectsOnInvoked()) - { - newNavViewItem.IsSelected(true); - return true; - } - } + // No sutable item found in main list so try to select item in footer + if (offset > 0) + { + return SelectSelectableItemWithOffset(-1, offset, footerRepeater, footerItemsSize); + } - index += offset; - } + return false; + } + + if (indexInFooter >= 0) + { + + if (SelectSelectableItemWithOffset(indexInFooter, offset, footerRepeater, footerItemsSize)) + { + return true; + } + + // No sutable item found in footer so try to select item in main list + if (offset < 0) + { + return SelectSelectableItemWithOffset(topPrimaryListSize, offset, topNavRepeater, topPrimaryListSize); } } } @@ -2546,6 +2750,27 @@ bool NavigationView::BumperNavigation(int offset) return false; } +bool NavigationView::SelectSelectableItemWithOffset(int startIndex, int offset, winrt::ItemsRepeater const& repeater, int repeaterCollectionSize) +{ + startIndex += offset; + while (startIndex > -1 && startIndex < repeaterCollectionSize) + { + auto newItem = repeater.TryGetElement(startIndex); + if (auto newNavViewItem = newItem.try_as()) + { + // This is done to skip Separators or other items that are not NavigationViewItems + if (winrt::get_self(newNavViewItem)->SelectsOnInvoked()) + { + newNavViewItem.IsSelected(true); + return true; + } + } + + startIndex += offset; + } + return false; +} + winrt::IInspectable NavigationView::MenuItemFromContainer(winrt::DependencyObject const& container) { if (container) @@ -2671,22 +2896,26 @@ winrt::NavigationTransitionInfo NavigationView::CreateNavigationTransitionInfo(N NavigationRecommendedTransitionDirection NavigationView::GetRecommendedTransitionDirection(winrt::DependencyObject const& prev, winrt::DependencyObject const& next) { auto recommendedTransitionDirection = NavigationRecommendedTransitionDirection::Default; - if (auto ir = m_topNavRepeater.get()) + auto ir = m_topNavRepeater.get(); + + if (prev && next && ir) { - auto prevIndex = prev ? ir.GetElementIndex(prev.try_as()) : s_itemNotFound; - auto nextIndex = next ? ir.GetElementIndex(next.try_as()) : s_itemNotFound; - if (prevIndex == s_itemNotFound || nextIndex == s_itemNotFound) - { - // One item is settings, so have problem to get the index - recommendedTransitionDirection = NavigationRecommendedTransitionDirection::Default; - } - else if (prevIndex < nextIndex) - { - recommendedTransitionDirection = NavigationRecommendedTransitionDirection::FromRight; - } - else if (prevIndex > nextIndex) + auto prevIndexPath = GetIndexPathForContainer(prev.try_as()); + auto nextIndexPath = GetIndexPathForContainer(next.try_as()); + + const auto compare = prevIndexPath.CompareTo(nextIndexPath); + + switch (compare) { - recommendedTransitionDirection = NavigationRecommendedTransitionDirection::FromLeft; + case -1: + recommendedTransitionDirection = NavigationRecommendedTransitionDirection::FromRight; + break; + case 1: + recommendedTransitionDirection = NavigationRecommendedTransitionDirection::FromLeft; + break; + default: + recommendedTransitionDirection = NavigationRecommendedTransitionDirection::Default; + break; } } return recommendedTransitionDirection; @@ -2715,16 +2944,6 @@ void NavigationView::OnSelectedItemPropertyChanged(winrt::DependencyPropertyChan ChangeSelection(oldItem, newItem); - // When we do not raise a "SelectItemChanged" event, the selection does not get animated. - // To prevent faulty visual states, we will animate that here - // Since we only do this for the settings item, check if the old item is our settings item - if (oldItem != newItem && m_shouldIgnoreNextSelectionChangeBecauseSettingsRestore) - { - ChangeSelectStatusForItem(oldItem, false /*selected*/); - ChangeSelectStatusForItem(newItem, true /*selected*/); - AnimateSelectionChanged(newItem); - } - if (m_appliedTemplate && IsTopNavigationView()) { if (!m_layoutUpdatedToken || @@ -2874,7 +3093,9 @@ void NavigationView::UpdateNavigationViewUseSystemVisual() if (SharedHelpers::IsRS1OrHigher() && !ShouldPreserveNavigationViewRS4Behavior() && m_appliedTemplate) { PropagateShowFocusVisualToAllNavigationViewItemsInRepeater(m_leftNavRepeater.get(), ShouldShowFocusVisual()); + PropagateShowFocusVisualToAllNavigationViewItemsInRepeater(m_leftNavFooterMenuRepeater.get(), ShouldShowFocusVisual()); PropagateShowFocusVisualToAllNavigationViewItemsInRepeater(m_topNavRepeater.get(), ShouldShowFocusVisual()); + PropagateShowFocusVisualToAllNavigationViewItemsInRepeater(m_topNavFooterMenuRepeater.get(), ShouldShowFocusVisual()); } } @@ -3025,9 +3246,9 @@ void NavigationView::SelectOverflowItem(winrt::IInspectable const& item, winrt:: auto const itemBeingMoved = [item, ip, this]() { - if (ip.GetSize() > 1) + if (ip.GetSize() > 2) { - return GetItemFromIndex(m_topNavRepeaterOverflowView.get(), m_topDataProvider.ConvertOriginalIndexToIndex(ip.GetAt(0))); + return GetItemFromIndex(m_topNavRepeaterOverflowView.get(), m_topDataProvider.ConvertOriginalIndexToIndex(ip.GetAt(1))); } return item; }(); @@ -3077,7 +3298,7 @@ void NavigationView::SelectOverflowItem(winrt::IInspectable const& item, winrt:: { for (std::vector::iterator it = itemsToBeRemoved.begin(); it != itemsToBeRemoved.end(); ++it) { - if (*it == ip.GetAt(0)) + if (*it == ip.GetAt(1)) { if (auto const indicator = m_activeIndicator.get()) { @@ -3121,7 +3342,7 @@ void NavigationView::SelectOverflowItem(winrt::IInspectable const& item, winrt:: // not all items have known width, need to redo the layout m_topDataProvider.MoveAllItemsToPrimaryList(); SetSelectedItemAndExpectItemInvokeWhenSelectionChangedIfNotInvokedFromAPI(item); - InvalidateTopNavPrimaryLayout(); + InvalidateTopNavPrimaryLayout(); } } @@ -3318,7 +3539,7 @@ std::vector NavigationView::FindMovableItemsBeyondAvailableWidth(float avai return m_topDataProvider.ConvertPrimaryIndexToIndex(toBeMoved); } -void NavigationView::KeepAtLeastOneItemInPrimaryList(std::vector itemInPrimaryToBeRemoved, bool shouldKeepFirst) +void NavigationView::KeepAtLeastOneItemInPrimaryList(std::vector& itemInPrimaryToBeRemoved, bool shouldKeepFirst) { if (!itemInPrimaryToBeRemoved.empty() && static_cast(itemInPrimaryToBeRemoved.size()) == m_topDataProvider.GetPrimaryListSize()) { @@ -3441,6 +3662,14 @@ void NavigationView::OnPropertyChanged(const winrt::DependencyPropertyChangedEve { UpdateRepeaterItemsSource(true /*forceSelectionModelUpdate*/); } + else if (property == s_FooterMenuItemsSourceProperty) + { + UpdateFooterRepeaterItemsSource(true /*sourceCollectionReset*/, true /*sourceCollectionChanged*/); + } + else if (property == s_FooterMenuItemsProperty) + { + UpdateFooterRepeaterItemsSource(true /*sourceCollectionReset*/, true /*sourceCollectionChanged*/); + } else if (property == s_PaneDisplayModeProperty) { // m_wasForceClosed is set to true because ToggleButton is clicked and Pane is closed. @@ -3498,7 +3727,7 @@ void NavigationView::OnPropertyChanged(const winrt::DependencyPropertyChangedEve } else if (property == s_IsSettingsVisibleProperty) { - UpdateVisualState(); + UpdateFooterRepeaterItemsSource(false /*sourceCollectionReset*/, true /*sourceCollectionChanged*/); } else if (property == s_CompactPaneLengthProperty) { @@ -3631,7 +3860,7 @@ void NavigationView::UpdatePaneDisplayMode() SwapPaneHeaderContent(m_leftNavPaneCustomContentBorder, m_paneCustomContentOnTopPane, L"PaneCustomContent"); SwapPaneHeaderContent(m_leftNavFooterContentBorder, m_paneFooterOnTopPane, L"PaneFooter"); - CreateAndHookEventsToSettings(c_settingsName); + CreateAndHookEventsToSettings(); if (winrt::IUIElement8 thisAsUIElement8 = *this) { @@ -3651,7 +3880,7 @@ void NavigationView::UpdatePaneDisplayMode() SwapPaneHeaderContent(m_paneCustomContentOnTopPane, m_leftNavPaneCustomContentBorder, L"PaneCustomContent"); SwapPaneHeaderContent(m_paneFooterOnTopPane, m_leftNavFooterContentBorder, L"PaneFooter"); - CreateAndHookEventsToSettings(c_settingsNameTopNav); + CreateAndHookEventsToSettings(); if (winrt::IUIElement8 thisAsUIElement8 = *this) { @@ -3664,6 +3893,11 @@ void NavigationView::UpdatePaneDisplayMode() UpdateContentBindingsForPaneDisplayMode(); UpdateRepeaterItemsSource(false /*forceSelectionModelUpdate*/); + UpdateFooterRepeaterItemsSource(false /*sourceCollectionReset*/, false /*sourceCollectionChanged*/); + if (auto selectedItem = SelectedItem()) + { + m_OrientationChangedPendingAnimation = true; + } } void NavigationView::UpdatePaneDisplayMode(winrt::NavigationViewPaneDisplayMode oldDisplayMode, winrt::NavigationViewPaneDisplayMode newDisplayMode) @@ -4016,34 +4250,48 @@ void NavigationView::UpdateSelectionForMenuItems() // if (!SelectedItem()) { + bool foundFirstSelected = false; + + // firstly check Menu items if (auto menuItems = MenuItems().try_as>()) { - bool foundFirstSelected = false; - for (auto menuItem : menuItems) + foundFirstSelected = UpdateSelectedItemFromMenuItems(menuItems); + } + + // then do same for footer items and tell wenever selected item alreadyfound in MenuItems + if (auto footerItems = FooterMenuItems().try_as>()) + { + UpdateSelectedItemFromMenuItems(footerItems, foundFirstSelected); + } + } +} + +bool NavigationView::UpdateSelectedItemFromMenuItems(const winrt::impl::com_ref>& menuItems, bool foundFirstSelected) +{ + for (int i = 0; i < static_cast(menuItems.Size()); i++) + { + if (auto item = menuItems.GetAt(i).try_as()) + { + if (item.IsSelected()) { - if (auto nvi = menuItem.try_as()) + if (!foundFirstSelected) { - if (nvi.IsSelected()) - { - if (!foundFirstSelected) + auto scopeGuard = gsl::finally([this]() { - auto scopeGuard = gsl::finally([this]() - { - m_shouldIgnoreNextSelectionChange = false; - }); - m_shouldIgnoreNextSelectionChange = true; - SelectedItem(nvi); - foundFirstSelected = true; - } - else - { - nvi.IsSelected(false); - } - } + m_shouldIgnoreNextSelectionChange = false; + }); + m_shouldIgnoreNextSelectionChange = true; + SelectedItem(item); + foundFirstSelected = true; + } + else + { + item.IsSelected(false); } } } } + return foundFirstSelected; } void NavigationView::OnTitleBarMetricsChanged(const winrt::IInspectable& /*sender*/, const winrt::IInspectable& /*args*/) @@ -4184,16 +4432,6 @@ void NavigationView::UpdateTitleBarPadding() } } -void NavigationView::SyncSettingsSelectionState() -{ - auto item = SelectedItem(); - auto settingsItem = m_settingsItem.get(); - if (settingsItem && item == settingsItem) - { - OnSettingsInvoked(); - } -} - void NavigationView::RaiseDisplayModeChanged(const winrt::NavigationViewDisplayMode& displayMode) { SetValue(s_DisplayModeProperty, box_value(displayMode)); @@ -4300,20 +4538,23 @@ template T NavigationView::GetContainerForData(const winrt::IInspect return nvi; } - if (auto settingsItem = m_settingsItem.get()) + // First conduct a basic top level search in main menu, which should succeed for a lot of scenarios. + const auto mainRepeater = IsTopNavigationView() ? m_topNavRepeater.get() : m_leftNavRepeater.get(); + auto itemIndex = GetIndexFromItem(mainRepeater, data); + if (itemIndex >= 0) { - if (settingsItem == data || settingsItem.Content() == data) + if (auto container = mainRepeater.TryGetElement(itemIndex)) { - return settingsItem.try_as(); + return container.try_as(); } } - // First conduct a basic top level search, which should succeed for a lot of scenarios. - auto ir = IsTopNavigationView() ? m_topNavRepeater.get() : m_leftNavRepeater.get(); - const auto itemIndex = GetIndexFromItem(ir, data); - if (ir && itemIndex >= 0) + // then look in footer menu + const auto footerRepeater = IsTopNavigationView() ? m_topNavFooterMenuRepeater.get() : m_leftNavFooterMenuRepeater.get(); + itemIndex = GetIndexFromItem(footerRepeater, data); + if (itemIndex >= 0) { - if (auto container = ir.TryGetElement(itemIndex)) + if (auto container = footerRepeater.TryGetElement(itemIndex)) { return container.try_as(); } @@ -4322,8 +4563,12 @@ template T NavigationView::GetContainerForData(const winrt::IInspect // If unsuccessful, unfortunately we are going to have to search through the whole tree // TODO: Either fix or remove implementation for TopNav. // It may not be required due to top nav rarely having realized children in its default state. - auto const repeater = IsTopNavigationView() ? m_topNavRepeater.get() : m_leftNavRepeater.get(); - if (auto const container = SearchEntireTreeForContainer(repeater, data)) + if (auto const container = SearchEntireTreeForContainer(mainRepeater, data)) + { + return container.try_as(); + } + + if (auto const container = SearchEntireTreeForContainer(footerRepeater, data)) { return container.try_as(); } @@ -4359,7 +4604,7 @@ winrt::UIElement NavigationView::SearchEntireTreeForContainer(const winrt::Items return nullptr; } -winrt::IndexPath NavigationView::SearchEntireTreeForIndexPath(const winrt::ItemsRepeater& rootRepeater, const winrt::IInspectable& data) +winrt::IndexPath NavigationView::SearchEntireTreeForIndexPath(const winrt::ItemsRepeater& rootRepeater, const winrt::IInspectable& data, bool isFooterRepeater) { for (int i = 0; i < GetContainerCountInRepeater(rootRepeater); i++) { @@ -4367,7 +4612,7 @@ winrt::IndexPath NavigationView::SearchEntireTreeForIndexPath(const winrt::Items { if (auto const nvi = container.try_as()) { - auto const ip = winrt::make(std::vector({ i })); + auto const ip = winrt::make(std::vector({ isFooterRepeater ? c_footerMenuBlockIndex : c_mainMenuBlockIndex, i })); if (auto const indexPath = SearchEntireTreeForIndexPath(nvi, data, ip)) { return indexPath; @@ -4551,20 +4796,32 @@ winrt::IndexPath NavigationView::GetIndexPathOfItem(const winrt::IInspectable& d if (IsTopNavigationView()) { // First search through primary list - if (auto const ip = SearchEntireTreeForIndexPath(m_topNavRepeater.get(), data)) + if (auto const ip = SearchEntireTreeForIndexPath(m_topNavRepeater.get(), data, false /*isFooterRepeater*/)) { return ip; } // If item was not located in primary list, search through overflow - if (auto const ip = SearchEntireTreeForIndexPath(m_topNavRepeaterOverflowView.get(), data)) + if (auto const ip = SearchEntireTreeForIndexPath(m_topNavRepeaterOverflowView.get(), data, false /*isFooterRepeater*/)) + { + return ip; + } + + // If item was not located in primary list and overflow, search through footer + if (auto const ip = SearchEntireTreeForIndexPath(m_topNavFooterMenuRepeater.get(), data, true /*isFooterRepeater*/)) { return ip; } } else { - if (auto const ip = SearchEntireTreeForIndexPath(m_leftNavRepeater.get(), data)) + if (auto const ip = SearchEntireTreeForIndexPath(m_leftNavRepeater.get(), data, false /*isFooterRepeater*/)) + { + return ip; + } + + // If item was not located in primary list, search through footer + if (auto const ip = SearchEntireTreeForIndexPath(m_leftNavFooterMenuRepeater.get(), data, true /*isFooterRepeater*/)) { return ip; } @@ -4573,15 +4830,16 @@ winrt::IndexPath NavigationView::GetIndexPathOfItem(const winrt::IInspectable& d return winrt::make(std::vector(0)); } -winrt::UIElement NavigationView::GetContainerForIndex(int index) +winrt::UIElement NavigationView::GetContainerForIndex(int index, bool inFooter) { if (IsTopNavigationView()) { // Get the repeater that is presenting the first item - auto ir = m_topDataProvider.IsItemInPrimaryList(index) ? m_topNavRepeater.get() : m_topNavRepeaterOverflowView.get(); + auto ir = inFooter ? m_topNavFooterMenuRepeater.get() + : (m_topDataProvider.IsItemInPrimaryList(index) ? m_topNavRepeater.get() : m_topNavRepeaterOverflowView.get()); - // Get the index of the first item in the repeater - const auto irIndex = m_topDataProvider.ConvertOriginalIndexToIndex(index); + // Get the index of the item in the repeater + const auto irIndex = inFooter ? index : m_topDataProvider.ConvertOriginalIndexToIndex(index); // Get the container of the first item if (auto const container = ir.TryGetElement(irIndex)) @@ -4591,41 +4849,59 @@ winrt::UIElement NavigationView::GetContainerForIndex(int index) } else { - if (auto const container = m_leftNavRepeater.get().TryGetElement(index)) + if (auto container = inFooter ? m_leftNavFooterMenuRepeater.get().TryGetElement(index) + : m_leftNavRepeater.get().TryGetElement(index)) { - return container; + return container.try_as(); } } return nullptr; } -winrt::NavigationViewItemBase NavigationView::GetContainerForIndexPath(const winrt::IndexPath& ip) +winrt::NavigationViewItemBase NavigationView::GetContainerForIndexPath(const winrt::IndexPath& ip, bool lastVisible) { if (ip && ip.GetSize() > 0) { - if (auto const container = GetContainerForIndex(ip.GetAt(0))) + if (auto const container = GetContainerForIndex(ip.GetAt(1), ip.GetAt(0) == c_footerMenuBlockIndex /*inFooter*/)) { + if (lastVisible) + { + if (auto const nvi = container.try_as()) + { + if (!nvi.IsExpanded()) + { + return nvi; + } + } + } + // TODO: Fix below for top flyout scenario once the flyout is introduced in the XAML. // We want to be able to retrieve containers for items that are in the flyout. // This will return nullptr if requesting children containers of // items in the primary list, or unrealized items in the overflow popup. // However this should not happen. - return GetContainerForIndexPath(container, ip); + return GetContainerForIndexPath(container, ip, lastVisible); } } return nullptr; } -winrt::NavigationViewItemBase NavigationView::GetContainerForIndexPath(const winrt::UIElement& firstContainer, const winrt::IndexPath& ip) + +winrt::NavigationViewItemBase NavigationView::GetContainerForIndexPath(const winrt::UIElement& firstContainer, const winrt::IndexPath& ip, bool lastVisible) { auto container = firstContainer; - if (ip.GetSize() > 1) + if (ip.GetSize() > 2) { - for (int i = 1; i < ip.GetSize(); i++) + for (int i = 2; i < ip.GetSize(); i++) { bool succeededGettingNextContainer = false; if (auto const nvi = container.try_as()) { + if (lastVisible && nvi.IsExpanded() == false) + { + return nvi; + } + if (auto const nviRepeater = winrt::get_self(nvi)->GetRepeater()) { if (auto const nextContainer = nviRepeater.TryGetElement(ip.GetAt(i))) @@ -4665,19 +4941,6 @@ winrt::ItemsRepeater NavigationView::LeftNavRepeater() return m_leftNavRepeater.get(); } -bool NavigationView::IsContainerInOverflow(const winrt::NavigationViewItemBase& nvib) -{ - if (IsTopNavigationView()) - { - auto parentIR = GetParentItemsRepeaterForContainer(nvib); - if (parentIR == m_topNavRepeaterOverflowView.get()) - { - return true; - } - } - return false; -} - winrt::NavigationViewItem NavigationView::GetSelectedContainer() { if (auto selectedItem = SelectedItem()) @@ -4724,11 +4987,11 @@ void NavigationView::ChangeIsExpandedNavigationViewItem(const winrt::NavigationV winrt::NavigationViewItem NavigationView::FindLowestLevelContainerToDisplaySelectionIndicator() { - auto indexIntoIndex = 0; + auto indexIntoIndex = 1; auto const selectedIndex = m_selectionModel.SelectedIndex(); - if (selectedIndex && selectedIndex.GetSize() > 0) + if (selectedIndex && selectedIndex.GetSize() > 1) { - if (auto container = GetContainerForIndex(selectedIndex.GetAt(indexIntoIndex))) + if (auto container = GetContainerForIndex(selectedIndex.GetAt(indexIntoIndex), selectedIndex.GetAt(0) == c_footerMenuBlockIndex /* inFooter */)) { if (auto nvi = container.try_as()) { @@ -4803,9 +5066,9 @@ winrt::ItemsRepeater NavigationView::GetChildRepeaterForIndexPath(const winrt::I winrt::IInspectable NavigationView::GetChildrenForItemInIndexPath(const winrt::IndexPath& ip, bool forceRealize) { - if (ip && ip.GetSize() > 0) + if (ip && ip.GetSize() > 1) { - if (auto const container = GetContainerForIndex(ip.GetAt(0))) + if (auto const container = GetContainerForIndex(ip.GetAt(1), ip.GetAt(0) == c_footerMenuBlockIndex /*inFooter*/)) { return GetChildrenForItemInIndexPath(container, ip, forceRealize); } @@ -4817,9 +5080,9 @@ winrt::IInspectable NavigationView::GetChildrenForItemInIndexPath(const winrt::U { auto container = firstContainer; bool shouldRecycleContainer = false; - if (ip.GetSize() > 1) + if (ip.GetSize() > 2) { - for (int i = 1; i < ip.GetSize(); i++) + for (int i = 2; i < ip.GetSize(); i++) { bool succeededGettingNextContainer = false; if (auto const nvi = container.try_as()) diff --git a/dev/NavigationView/NavigationView.h b/dev/NavigationView/NavigationView.h index e574c70649..992af81bef 100644 --- a/dev/NavigationView/NavigationView.h +++ b/dev/NavigationView/NavigationView.h @@ -61,6 +61,7 @@ class NavigationView : static void CreateAndAttachHeaderAnimation(const winrt::Visual& visual); + void OnPreviewKeyDown(winrt::KeyRoutedEventArgs const& args); void OnKeyDown(winrt::KeyRoutedEventArgs const& args); bool IsFullScreenOrTabletMode(); @@ -84,6 +85,7 @@ class NavigationView : winrt::ItemsRepeater LeftNavRepeater(); winrt::NavigationViewItem GetSelectedContainer(); winrt::ItemsRepeater GetParentItemsRepeaterForContainer(const winrt::NavigationViewItemBase& nvib); + winrt::ItemsRepeater GetParentRootItemsRepeaterForContainer(const winrt::NavigationViewItemBase& nvib); winrt::IndexPath GetIndexPathForContainer(const winrt::NavigationViewItemBase& nvib); // Hierarchical related functions @@ -107,19 +109,18 @@ class NavigationView : int GetIndexFromItem(const winrt::ItemsRepeater& ir, const winrt::IInspectable& data); static winrt::IInspectable GetItemFromIndex(const winrt::ItemsRepeater& ir, int index); winrt::IndexPath GetIndexPathOfItem(const winrt::IInspectable& data); - winrt::UIElement GetContainerForIndex(int index); - winrt::NavigationViewItemBase GetContainerForIndexPath(const winrt::IndexPath& ip); - winrt::NavigationViewItemBase GetContainerForIndexPath(const winrt::UIElement& firstContainer, const winrt::IndexPath& ip); + winrt::UIElement GetContainerForIndex(int index, bool inFooter); + winrt::NavigationViewItemBase GetContainerForIndexPath(const winrt::IndexPath& ip, bool lastVisible = false); + winrt::NavigationViewItemBase GetContainerForIndexPath(const winrt::UIElement& firstContainer, const winrt::IndexPath& ip, bool lastVisible); winrt::IInspectable GetChildrenForItemInIndexPath(const winrt::IndexPath& ip, bool forceRealize = false); winrt::IInspectable GetChildrenForItemInIndexPath(const winrt::UIElement& firstContainer, const winrt::IndexPath& ip, bool forceRealize = false); winrt::UIElement SearchEntireTreeForContainer(const winrt::ItemsRepeater& rootRepeater, const winrt::IInspectable& data); - winrt::IndexPath SearchEntireTreeForIndexPath(const winrt::ItemsRepeater& rootRepeater, const winrt::IInspectable& data); + winrt::IndexPath SearchEntireTreeForIndexPath(const winrt::ItemsRepeater& rootRepeater, const winrt::IInspectable& data, bool isFooterRepeater); winrt::IndexPath SearchEntireTreeForIndexPath(const winrt::NavigationViewItem& parentContainer, const winrt::IInspectable& data, const winrt::IndexPath& ip); winrt::ItemsRepeater GetChildRepeaterForIndexPath(const winrt::IndexPath& ip); winrt::NavigationViewItem GetParentNavigationViewItemForContainer(const winrt::NavigationViewItemBase& nvib); bool IsContainerTheSelectedItemInTheSelectionModel(const winrt::NavigationViewItemBase& nvib); - bool IsContainerInOverflow(const winrt::NavigationViewItemBase& nvib); int GetContainerCountInRepeater(const winrt::ItemsRepeater& ir); bool DoesRepeaterHaveRealizedContainers(const winrt::ItemsRepeater& ir); bool IsSettingsItem(winrt::IInspectable const& item); @@ -155,6 +156,7 @@ class NavigationView : inline NavigationViewTemplateSettings* GetTemplateSettings(); inline bool IsNavigationViewListSingleSelectionFollowsFocus(); inline void UpdateSingleSelectionFollowsFocusTemplateSetting(); + void OnFooterItemsSourceCollectionChanged(const winrt::IInspectable &, const winrt::IInspectable &); void SetSelectedItemAndExpectItemInvokeWhenSelectionChangedIfNotInvokedFromAPI(winrt::IInspectable const& item); void ChangeSelectStatusForItem(winrt::IInspectable const& item, bool selected); void UnselectPrevItem(winrt::IInspectable const& prevItem, winrt::IInspectable const& nextItem); @@ -205,7 +207,7 @@ class NavigationView : std::vector FindMovableItemsRecoverToPrimaryList(float availableWidth, std::vector const& includeItems); std::vector FindMovableItemsToBeRemovedFromPrimaryList(float widthToBeRemoved, std::vector const& excludeItems); std::vector FindMovableItemsBeyondAvailableWidth(float availableWidth); - void KeepAtLeastOneItemInPrimaryList(std::vector itemInPrimaryToBeRemoved, bool shouldKeepFirst); + void KeepAtLeastOneItemInPrimaryList(std::vector& itemInPrimaryToBeRemoved, bool shouldKeepFirst); void UpdateTopNavigationWidthCache(); int GetSelectedItemIndex(); @@ -213,11 +215,12 @@ class NavigationView : double GetPaneToggleButtonHeight(); bool BumperNavigation(int offset); + bool SelectSelectableItemWithOffset(int startIndex, int offset, winrt::ItemsRepeater const& repeater, int repeaterCollectionSize); bool IsTopNavigationView(); bool IsTopPrimaryListVisible(); - void CreateAndHookEventsToSettings(std::wstring_view settingsName); + void CreateAndHookEventsToSettings(); void OnIsPaneOpenChanged(); void UpdatePaneButtonsWidths(); void UpdateHeaderVisibility(); @@ -227,7 +230,6 @@ class NavigationView : void UpdatePaneDisplayMode(winrt::NavigationViewPaneDisplayMode oldDisplayMode, winrt::NavigationViewPaneDisplayMode newDisplayMode); void UpdatePaneVisibility(); void UpdateContentBindingsForPaneDisplayMode(); - void SyncSettingsSelectionState(); void UpdatePaneTabFocusNavigation(); void UpdatePaneToggleSize(); void UpdateBackAndCloseButtonsVisibility(); @@ -236,9 +238,11 @@ class NavigationView : void UpdateTopNavRepeatersItemSource(const winrt::IInspectable& items); static void UpdateItemsRepeaterItemsSource(const winrt::ItemsRepeater& listView, const winrt::IInspectable& itemsSource); void UpdateSelectionForMenuItems(); + bool UpdateSelectedItemFromMenuItems(const winrt::impl::com_ref>& menuItems, bool foundFirstSelected = false); bool m_InitialNonForcedModeUpdate{ true }; void UpdateRepeaterItemsSource(bool forceSelectionModelUpdate); + void UpdateFooterRepeaterItemsSource(bool sourceCollectionReseted, bool sourceCollectionChanged); void OnSizeChanged(const winrt::IInspectable& sender, const winrt::SizeChangedEventArgs& args); void OnLayoutUpdated(const winrt::IInspectable& sender, const winrt::IInspectable& e); @@ -255,6 +259,7 @@ class NavigationView : void OnNavigationViewItemTapped(const winrt::IInspectable& sender, const winrt::TappedRoutedEventArgs& args); void OnNavigationViewItemKeyDown(const winrt::IInspectable& sender, const winrt::KeyRoutedEventArgs& args); + void OnRepeaterGettingFocus(const winrt::IInspectable& sender, const winrt::GettingFocusEventArgs& args); void OnNavigationViewItemOnGotFocus(const winrt::IInspectable& sender, const winrt::RoutedEventArgs& e); void OnNavigationViewItemExpandedPropertyChanged(const winrt::DependencyObject& sender, const winrt::DependencyProperty& args); @@ -335,6 +340,8 @@ class NavigationView : tracker_ref m_closeButton{ this }; tracker_ref m_leftNavRepeater{ this }; tracker_ref m_topNavRepeater{ this }; + tracker_ref m_leftNavFooterMenuRepeater{ this }; + tracker_ref m_topNavFooterMenuRepeater{ this }; tracker_ref m_topNavOverflowButton{ this }; tracker_ref m_topNavRepeaterOverflowView{ this }; tracker_ref m_topNavGrid{ this }; @@ -373,8 +380,6 @@ class NavigationView : // Event Tokens winrt::Button::Click_revoker m_paneToggleButtonClickRevoker{}; - winrt::UIElement::Tapped_revoker m_settingsItemTappedRevoker{}; - winrt::UIElement::KeyDown_revoker m_settingsItemKeyDownRevoker{}; winrt::Button::Click_revoker m_paneSearchButtonClickRevoker{}; winrt::CoreApplicationViewTitleBar::LayoutMetricsChanged_revoker m_titleBarMetricsChangedRevoker{}; winrt::CoreApplicationViewTitleBar::IsVisibleChanged_revoker m_titleBarIsVisibleChangedRevoker{}; @@ -393,10 +398,22 @@ class NavigationView : winrt::ItemsRepeater::ElementPrepared_revoker m_leftNavItemsRepeaterElementPreparedRevoker{}; winrt::ItemsRepeater::ElementClearing_revoker m_leftNavItemsRepeaterElementClearingRevoker{}; winrt::ItemsRepeater::Loaded_revoker m_leftNavRepeaterLoadedRevoker{}; + winrt::ItemsRepeater::GettingFocus_revoker m_leftNavRepeaterGettingFocusRevoker{}; winrt::ItemsRepeater::ElementPrepared_revoker m_topNavItemsRepeaterElementPreparedRevoker{}; winrt::ItemsRepeater::ElementClearing_revoker m_topNavItemsRepeaterElementClearingRevoker{}; winrt::ItemsRepeater::Loaded_revoker m_topNavRepeaterLoadedRevoker{}; + winrt::ItemsRepeater::GettingFocus_revoker m_topNavRepeaterGettingFocusRevoker{}; + + winrt::ItemsRepeater::ElementPrepared_revoker m_leftNavFooterMenuItemsRepeaterElementPreparedRevoker{}; + winrt::ItemsRepeater::ElementClearing_revoker m_leftNavFooterMenuItemsRepeaterElementClearingRevoker{}; + winrt::ItemsRepeater::Loaded_revoker m_leftNavFooterMenuRepeaterLoadedRevoker{}; + winrt::ItemsRepeater::GettingFocus_revoker m_leftNavFooterMenuRepeaterGettingFocusRevoker{}; + + winrt::ItemsRepeater::ElementPrepared_revoker m_topNavFooterMenuItemsRepeaterElementPreparedRevoker{}; + winrt::ItemsRepeater::ElementClearing_revoker m_topNavFooterMenuItemsRepeaterElementClearingRevoker{}; + winrt::ItemsRepeater::Loaded_revoker m_topNavFooterMenuRepeaterLoadedRevoker{}; + winrt::ItemsRepeater::GettingFocus_revoker m_topNavFooterMenuRepeaterGettingFocusRevoker{}; winrt::ItemsRepeater::ElementPrepared_revoker m_topNavOverflowItemsRepeaterElementPreparedRevoker{}; winrt::ItemsRepeater::ElementClearing_revoker m_topNavOverflowItemsRepeaterElementClearingRevoker{}; @@ -404,6 +421,9 @@ class NavigationView : winrt::SelectionModel::SelectionChanged_revoker m_selectionChangedRevoker{}; winrt::SelectionModel::ChildrenRequested_revoker m_childrenRequestedRevoker{}; + winrt::ItemsSourceView::CollectionChanged_revoker m_footerItemsCollectionChangedRevoker{}; + + winrt::FlyoutBase::Closing_revoker m_flyoutClosingRevoker{}; bool m_wasForceClosed{ false }; @@ -414,6 +434,9 @@ class NavigationView : TopNavigationViewDataProvider m_topDataProvider{ this }; winrt::SelectionModel m_selectionModel{}; + winrt::IVector m_selectionModelSource{}; + + winrt::ItemsSourceView m_footerItemsSource{ nullptr }; bool m_appliedTemplate{ false }; @@ -421,8 +444,6 @@ class NavigationView : // Customer select an item from SelectedItem property->ChangeSelection update ListView->LIstView raise OnSelectChange(we want stop here)->change property do do animation again. // Customer clicked listview->listview raised OnSelectChange->SelectedItem property changed->ChangeSelection->Undo the selection by SelectedItem(prevItem) (we want it stop here)->ChangeSelection again ->... bool m_shouldIgnoreNextSelectionChange{ false }; - // Used to disable raising selection change iff settings item gets restored because of displaymode change - bool m_shouldIgnoreNextSelectionChangeBecauseSettingsRestore{ false }; // A flag to track that the selectionchange is caused by selection a item in topnav overflow menu bool m_selectionChangeFromOverflowMenu{ false }; // Flag indicating whether selection change should raise item invoked. This is needed to be able to raise ItemInvoked before SelectionChanged while SelectedItem should point to the clicked item @@ -443,5 +464,9 @@ class NavigationView : bool m_moveTopNavOverflowItemOnFlyoutClose{ false }; bool m_shouldIgnoreUIASelectionRaiseAsExpandCollapseWillRaise{ false }; + + bool m_OrientationChangedPendingAnimation{ false }; + + bool m_TabKeyPrecedesFocusChange{ false }; }; diff --git a/dev/NavigationView/NavigationView.idl b/dev/NavigationView/NavigationView.idl index 560b640f56..9420775296 100644 --- a/dev/NavigationView/NavigationView.idl +++ b/dev/NavigationView/NavigationView.idl @@ -175,6 +175,11 @@ unsealed runtimeclass NavigationView : Windows.UI.Xaml.Controls.ContentControl [MUX_DEFAULT_VALUE("1008.0")] [MUX_PROPERTY_VALIDATION_CALLBACK("CoerceToGreaterThanZero")] Double ExpandedModeThresholdWidth { get; set; }; + [WUXC_VERSION_PREVIEW] + { + Windows.Foundation.Collections.IVector FooterMenuItems{ get; }; + Object FooterMenuItemsSource{ get; set; }; + } Windows.UI.Xaml.UIElement PaneFooter { get; set; }; Object Header { get; set; }; Windows.UI.Xaml.DataTemplate HeaderTemplate { get; set; }; @@ -211,6 +216,11 @@ unsealed runtimeclass NavigationView : Windows.UI.Xaml.Controls.ContentControl static Windows.UI.Xaml.DependencyProperty IsPaneOpenProperty { get; }; static Windows.UI.Xaml.DependencyProperty CompactModeThresholdWidthProperty { get; }; static Windows.UI.Xaml.DependencyProperty ExpandedModeThresholdWidthProperty { get; }; + [WUXC_VERSION_PREVIEW] + { + static Windows.UI.Xaml.DependencyProperty FooterMenuItemsProperty{ get; }; + static Windows.UI.Xaml.DependencyProperty FooterMenuItemsSourceProperty{ get; }; + } static Windows.UI.Xaml.DependencyProperty PaneFooterProperty { get; }; static Windows.UI.Xaml.DependencyProperty HeaderProperty { get; }; static Windows.UI.Xaml.DependencyProperty HeaderTemplateProperty { get; }; diff --git a/dev/NavigationView/NavigationView.xaml b/dev/NavigationView/NavigationView.xaml index 2053a6d129..6bafd1b481 100644 --- a/dev/NavigationView/NavigationView.xaml +++ b/dev/NavigationView/NavigationView.xaml @@ -56,16 +56,6 @@ - - - - - - - - - - @@ -91,7 +81,6 @@ - @@ -216,7 +205,7 @@ Background="{ThemeResource NavigationViewTopPaneBackground}" Grid.Row="0" HorizontalAlignment="Stretch" - VerticalAlignment="Top" + VerticalAlignment="Top" Canvas.ZIndex="1" contract4Present:XYFocusKeyboardNavigation="Enabled"> @@ -302,11 +291,7 @@ - - - - + x:Name="TopNavMenuItemsOverflowHost"/> @@ -340,14 +325,16 @@ VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch" Grid.Column="7" /> - - - - + + + + + + - + - - - - + VerticalAlignment="Stretch"> - + + + + + + + + + + - - - - + AutomationProperties.AccessibilityView = "Content"/> + + + + + + - - - - - - diff --git a/dev/NavigationView/NavigationViewHelper.h b/dev/NavigationView/NavigationViewHelper.h index bcd1a69292..900f31d0e6 100644 --- a/dev/NavigationView/NavigationViewHelper.h +++ b/dev/NavigationView/NavigationViewHelper.h @@ -15,7 +15,9 @@ enum class NavigationViewRepeaterPosition { LeftNav, TopPrimary, - TopOverflow + TopOverflow, + LeftFooter, + TopFooter }; enum class NavigationViewPropagateTarget diff --git a/dev/NavigationView/NavigationViewItem.cpp b/dev/NavigationView/NavigationViewItem.cpp index 209087fc28..dc7f5c30ab 100644 --- a/dev/NavigationView/NavigationViewItem.cpp +++ b/dev/NavigationView/NavigationViewItem.cpp @@ -316,6 +316,7 @@ void NavigationViewItem::UpdateVisualStateForNavigationViewPositionChange() switch (position) { case NavigationViewRepeaterPosition::LeftNav: + case NavigationViewRepeaterPosition::LeftFooter: if (SharedHelpers::IsRS4OrHigher() && winrt::Application::Current().FocusVisualKind() == winrt::FocusVisualKind::Reveal) { // OnLeftNavigationReveal is introduced in RS6. @@ -327,6 +328,7 @@ void NavigationViewItem::UpdateVisualStateForNavigationViewPositionChange() } break; case NavigationViewRepeaterPosition::TopPrimary: + case NavigationViewRepeaterPosition::TopFooter: if (SharedHelpers::IsRS4OrHigher() && winrt::Application::Current().FocusVisualKind() == winrt::FocusVisualKind::Reveal) { stateName = c_OnTopNavigationPrimaryReveal; @@ -511,7 +513,8 @@ bool NavigationViewItem::ShouldShowContent() bool NavigationViewItem::IsOnLeftNav() const { - return Position() == NavigationViewRepeaterPosition::LeftNav; + auto const position = Position(); + return position == NavigationViewRepeaterPosition::LeftNav || position == NavigationViewRepeaterPosition::LeftFooter; } bool NavigationViewItem::IsOnTopPrimary() const diff --git a/dev/NavigationView/NavigationViewItemAutomationPeer.cpp b/dev/NavigationView/NavigationViewItemAutomationPeer.cpp index db553f6f1d..ddcee8ee29 100644 --- a/dev/NavigationView/NavigationViewItemAutomationPeer.cpp +++ b/dev/NavigationView/NavigationViewItemAutomationPeer.cpp @@ -82,12 +82,7 @@ int32_t NavigationViewItemAutomationPeer::GetPositionInSetCore() { int32_t positionInSet = 0; - if (IsSettingsItem()) - { - return 1; - } - - if (IsOnTopNavigation()) + if (IsOnTopNavigation() && !IsOnFooterNavigation()) { positionInSet = GetPositionOrSetCountInTopNavHelper(AutomationOutput::Position); } @@ -103,14 +98,13 @@ int32_t NavigationViewItemAutomationPeer::GetSizeOfSetCore() { int32_t sizeOfSet = 0; - if (IsSettingsItem()) + if (IsOnTopNavigation() && !IsOnFooterNavigation()) { - return 1; - } + if (auto navview = GetParentNavigationView()) + { + sizeOfSet = GetPositionOrSetCountInTopNavHelper(AutomationOutput::Size); - if (IsOnTopNavigation()) - { - sizeOfSet = GetPositionOrSetCountInTopNavHelper(AutomationOutput::Size); + } } else { @@ -135,7 +129,8 @@ int32_t NavigationViewItemAutomationPeer::GetLevelCore() { if (auto const indexPath = winrt::get_self(navView)->GetIndexPathForContainer(nvib)) { - return indexPath.GetSize(); + // first index in path stands for main or footer menu + return indexPath.GetSize() - 1; } } } @@ -265,7 +260,8 @@ bool NavigationViewItemAutomationPeer::IsSettingsItem() bool NavigationViewItemAutomationPeer::IsOnTopNavigation() { - return GetNavigationViewRepeaterPosition() != NavigationViewRepeaterPosition::LeftNav; + const auto position = GetNavigationViewRepeaterPosition(); + return position != NavigationViewRepeaterPosition::LeftNav && position != NavigationViewRepeaterPosition::LeftFooter; } bool NavigationViewItemAutomationPeer::IsOnTopNavigationOverflow() @@ -273,6 +269,12 @@ bool NavigationViewItemAutomationPeer::IsOnTopNavigationOverflow() return GetNavigationViewRepeaterPosition() == NavigationViewRepeaterPosition::TopOverflow; } +bool NavigationViewItemAutomationPeer::IsOnFooterNavigation() +{ + const auto position = GetNavigationViewRepeaterPosition(); + return position == NavigationViewRepeaterPosition::LeftFooter || position == NavigationViewRepeaterPosition::TopFooter; +} + NavigationViewRepeaterPosition NavigationViewItemAutomationPeer::GetNavigationViewRepeaterPosition() { if (winrt::NavigationViewItemBase navigationViewItem = Owner().try_as()) @@ -282,7 +284,7 @@ NavigationViewRepeaterPosition NavigationViewItemAutomationPeer::GetNavigationVi return NavigationViewRepeaterPosition::LeftNav; } -winrt::ItemsRepeater NavigationViewItemAutomationPeer::GetParentRepeater() +winrt::ItemsRepeater NavigationViewItemAutomationPeer::GetParentItemsRepeater() { if (auto const navview = GetParentNavigationView()) { @@ -303,9 +305,9 @@ int32_t NavigationViewItemAutomationPeer::GetPositionOrSetCountInLeftNavHelper(A { int returnValue = 0; - if (auto const repeater = GetParentRepeater()) + if (auto const repeater = GetParentItemsRepeater()) { - if (auto const parent = Navigate(winrt::AutomationNavigationDirection::Parent).try_as()) + if (auto const parent = winrt::FrameworkElementAutomationPeer::CreatePeerForElement(repeater).try_as()) { if (auto const children = parent.GetChildren()) { @@ -365,7 +367,7 @@ int32_t NavigationViewItemAutomationPeer::GetPositionOrSetCountInTopNavHelper(Au int32_t returnValue = 0; bool itemFound = false; - if (auto const parentRepeater = GetParentRepeater()) + if (auto const parentRepeater = GetParentItemsRepeater()) { if (auto const itemsSourceView = parentRepeater.ItemsSourceView()) { diff --git a/dev/NavigationView/NavigationViewItemAutomationPeer.h b/dev/NavigationView/NavigationViewItemAutomationPeer.h index f84a8a9ba0..56da6757c6 100644 --- a/dev/NavigationView/NavigationViewItemAutomationPeer.h +++ b/dev/NavigationView/NavigationViewItemAutomationPeer.h @@ -52,9 +52,10 @@ class NavigationViewItemAutomationPeer : }; winrt::NavigationView GetParentNavigationView(); - winrt::ItemsRepeater GetParentRepeater(); + winrt::ItemsRepeater GetParentItemsRepeater(); bool IsOnTopNavigation(); bool IsOnTopNavigationOverflow(); + bool IsOnFooterNavigation(); bool IsSettingsItem(); NavigationViewRepeaterPosition GetNavigationViewRepeaterPosition(); int32_t GetNavigationViewItemCountInPrimaryList(); diff --git a/dev/NavigationView/NavigationViewItemSeparator.cpp b/dev/NavigationView/NavigationViewItemSeparator.cpp index bfa2042564..e3d69aef34 100644 --- a/dev/NavigationView/NavigationViewItemSeparator.cpp +++ b/dev/NavigationView/NavigationViewItemSeparator.cpp @@ -20,7 +20,7 @@ void NavigationViewItemSeparator::UpdateVisualState(bool useTransitions) if (m_appliedTemplate) { static auto groupName = L"NavigationSeparatorLineStates"sv; - const auto stateName = (Position() != NavigationViewRepeaterPosition::TopPrimary) + const auto stateName = (Position() != NavigationViewRepeaterPosition::TopPrimary && Position() != NavigationViewRepeaterPosition::TopFooter) ? m_isClosedCompact ? L"HorizontalLineCompact"sv : L"HorizontalLine"sv diff --git a/dev/NavigationView/NavigationViewItemsFactory.cpp b/dev/NavigationView/NavigationViewItemsFactory.cpp index 55786b020c..6d2ca87752 100644 --- a/dev/NavigationView/NavigationViewItemsFactory.cpp +++ b/dev/NavigationView/NavigationViewItemsFactory.cpp @@ -28,11 +28,23 @@ void NavigationViewItemsFactory::UserElementFactory(winrt::IInspectable const& n navigationViewItemPool = std::vector(); } +void NavigationViewItemsFactory::SettingsItem(winrt::NavigationViewItemBase const& settingsItem) +{ + m_settingsItem = settingsItem; +} + + // Retrieve the element that will be displayed for a specific data item. // If the resolved element is not derived from NavigationViewItemBase, wrap in a NavigationViewItem before returning. winrt::UIElement NavigationViewItemsFactory::GetElementCore(winrt::ElementFactoryGetArgs const& args) { - auto const newContent = [itemTemplateWrapper = m_itemTemplateWrapper, args]() { + auto const newContent = [itemTemplateWrapper = m_itemTemplateWrapper, settingsItem = m_settingsItem, args]() { + // Do not template SettingsItem + if (settingsItem && settingsItem == args.Data()) + { + return args.Data(); + } + if (itemTemplateWrapper) { return itemTemplateWrapper.GetElement(args).as(); @@ -108,7 +120,10 @@ void NavigationViewItemsFactory::RecycleElementCore(winrt::ElementFactoryRecycle } } - if (m_itemTemplateWrapper) + // Do not recycle SettingsItem + bool isSettingsItem = m_settingsItem && m_settingsItem == args.Element(); + + if (m_itemTemplateWrapper && !isSettingsItem) { m_itemTemplateWrapper.RecycleElement(args); } diff --git a/dev/NavigationView/NavigationViewItemsFactory.h b/dev/NavigationView/NavigationViewItemsFactory.h index e47efdd3a9..11505e09a8 100644 --- a/dev/NavigationView/NavigationViewItemsFactory.h +++ b/dev/NavigationView/NavigationViewItemsFactory.h @@ -11,6 +11,7 @@ class NavigationViewItemsFactory : { public: void UserElementFactory(winrt::IInspectable const& newValue); + void SettingsItem(winrt::NavigationViewItemBase const& settingsItem); #pragma region IElementFactoryOverrides winrt::UIElement GetElementCore(winrt::ElementFactoryGetArgs const& args); @@ -19,6 +20,7 @@ class NavigationViewItemsFactory : private: winrt::IElementFactoryShim m_itemTemplateWrapper{ nullptr }; + winrt::NavigationViewItemBase m_settingsItem{ nullptr }; std::vector navigationViewItemPool; void UnlinkElementFromParent(winrt::ElementFactoryRecycleArgs const& args); diff --git a/dev/NavigationView/NavigationView_InteractionTests/CommonTests.cs b/dev/NavigationView/NavigationView_InteractionTests/CommonTests.cs index 0f06a964eb..657ef47811 100644 --- a/dev/NavigationView/NavigationView_InteractionTests/CommonTests.cs +++ b/dev/NavigationView/NavigationView_InteractionTests/CommonTests.cs @@ -102,9 +102,8 @@ public void IsSettingsVisibleTest() { using (var setup = new TestSetupHelper(new[] { "NavigationView Tests", testScenario.TestPageName })) { - String settings = testScenario.IsLeftNavTest ? "Settings" : "SettingsTopNavPaneItem"; Log.Comment("Verify that settings item is enabled by default"); - VerifyElement.Found(settings, FindBy.Name); + VerifyElement.Found("Settings", FindBy.Name); CheckBox settingsCheckbox = new CheckBox(FindElement.ByName("SettingsItemVisibilityCheckbox")); @@ -112,12 +111,12 @@ public void IsSettingsVisibleTest() settingsCheckbox.Uncheck(); ElementCache.Clear(); Wait.ForIdle(); - VerifyElement.NotFound(settings, FindBy.Name); + VerifyElement.NotFound("Settings", FindBy.Name); Log.Comment("Verify that settings item is visible when IsSettingsVisible == true"); settingsCheckbox.Check(); Wait.ForIdle(); - VerifyElement.Found(settings, FindBy.Name); + VerifyElement.Found("Settings", FindBy.Name); } } } @@ -143,7 +142,7 @@ public void IsPaneToggleButtonVisibleTest() Log.Comment("Verify that settings item is visible when IsSettingsVisible == true"); toggleCheckbox.Check(); Wait.ForIdle(); - VerifyElement.Found("SettingsNavPaneItem", FindBy.Id); + VerifyElement.Found("SettingsItem", FindBy.Id); } } } @@ -260,6 +259,42 @@ public void AddRemoveOriginalItemTest() } } + [TestMethod] + [TestProperty("TestSuite", "A")] + public void AddRemoveFooterItemTest() + { + var testScenarios = RegressionTestScenario.BuildLeftNavRegressionTestScenarios(); + foreach (var testScenario in testScenarios) + { + using (var setup = new TestSetupHelper(new[] { "NavigationView Tests", testScenario.TestPageName })) + { + var addButton = FindElement.ById